Streams

Streams are Lisp's way of providing a unified interface to input and output protocols such as Serial, I2C, SPI, SD Cards, Wi-Fi, strings, or graphics. The latest release of uLisp, 4.8, features an improved implementation of streams which is more efficient, and provides the additional benefit that you can extend uLisp by defining custom streams.

Following a suggestion made by user @dragoncoder047 streams now use a lookup table. This is not only more efficient, but also allows you to define additional streams in an Extensions File.

For example, you could write an Extensions File that implements the 1-Wire protocol and a 1-Wire stream, allowing you to use functions such as print, format, and read to communicate over 1-Wire. I have provided an example Extensions File that implements the Lisp form with-input-from-string, not currently a standard part of uLisp, by implementing a string input stream.

For a more advanced extension that takes advantage of streams see LoRa extension.

Introduction

uLisp takes advantage of streams to allow you to use all the standard input and output functions with any input or output device. Most versions of uLisp provide the following streams:

Stream Streamtype Description
SERIALSTREAM 0 Reading from and writing to a Serial interface.
I2CSTREAM 1 Reading from and writing to an I2C device.
SPISTREAM 2 Reading from and writing to an SPI device.
SDSTREAM 3 Reading from and writing to SD cards.
WIFISTREAM 4 Reading from and writing to Wi-Fi protocols.
STRINGSTREAM 5 Reading from or writing to a Lisp string.
GFXSTREAM 6 Writing text to a TFT colour display.

To tell an input or output function what stream you want to read from or write to you pass it a stream object as a parameter. All the built-in input/output functions accept a stream parameter; in many cases this is optional, if there is a default stream that should be used.

For example, to print a string "Hello" to the Serial Monitor you just do:

(print "Hello")

You can also use print to print to an I2C device. For example:

(with-i2c (str #x68)
  (print "Hello" str))

This assumes that it is meaningful to send a text string to that device.

Stream objects

A stream object consists of an 8-bit stream type, and an 8-bit address. The address is used to distinguish between multiple streams of the same type, and its use depends on the stream type. For example, with an I2C stream it's the I2C address of the device being communicated with.

The function stream() creates a stream object:

object *stream (uint8_t streamtype, uint8_t address) {
  object *ptr = myalloc();
  ptr->type = STREAM;
  ptr->integer = streamtype<<8 | address;
  return ptr;
}

If you print the value of a stream object it shows the stream type and the address; for example:

> (with-serial (str 1) str)
<serial-stream 1>

Stream definitions

Each type of stream is assigned an index number by an enum such as:

enum stream { SERIALSTREAM, I2CSTREAM, SPISTREAM, SDSTREAM, STRINGSTREAM, GFXSTREAM };

User-defined streams in an Extensions File start at 16, as specified by:

#define USERSTREAMS 16

The behaviour of each stream is defined by a stream lookup table, which needs to be in same order as enum stream:

const stream_entry_t stream_table[] = {
  { serialstreamname, pfun_serial, gfun_serial },
  { i2cstreamname, pfun_i2c, gfun_i2c },
  { spistreamname, pfun_spi, gfun_spi },
  { sdstreamname, pfun_sd, gfun_sd },
  { wifistreamname, pfun_wifi, gfun_wifi },
  { stringstreamname, pfun_string, NULL },
  { gfxstreamname, pfun_gfx, NULL },
};

Each entry in this table specifies a pointer to the stream name, a stream writing function such as pfun_serial, and a stream reading function, such as gfun_serial.

The stream names are used by printobject() to print the stream name, and are defined by statements such as:

const char serialstreamname[] = "serial";

with -stream added to each.

The stream writing and stream reading functions are passed a single parameter giving the address of the stream, and are defined for each stream with a function such as:

pfun_t pfun_serial (uint8_t address) {
  pfun_t pfun;
  if (address == 0) pfun = pserial;
  else if (address == 1) pfun = serial1write;
  else if (address == 2) pfun = serial2write;
  else if (address == 3) pfun = serial3write;
  return pfun;
}

The address parameter is used in some streams to select between different hardware ports, or in the case of I2C between different I2C addresses. In SDSTREAM and STRINGSTREAM it is ignored.

Finally, the functions pstreamfun and gstreamfun take a stream object, look up the appropriate stream entry in stream_table[], and return the appropriate C read or write function to call for that stream:

pfun_t pstreamfun (object *args) {
  nstream_t nstream = SERIALSTREAM;
  int address = 0;
  pfun_t pfun = pserial;
  if (args != NULL && first(args) != NULL) {
    int stream = isstream(first(args));
    nstream = stream>>8; address = stream & 0xFF;
  }
  bool n = nstream<USERSTREAMS;
  pstream_ptr_t streamfunction = streamtable(n?0:1)[n?nstream:nstream-USERSTREAMS].pfunptr;
  pfun = streamfunction(address);
  return pfun;
}

User-defined streams

If you uncomment the #define at the start of the uLisp source file:

#define streamextensions

you can define one or more additional types of stream in C in the uLisp Extensions File. If the index of the stream is greater than USERSTREAMS the functions pstreamfun and gstreamfun will read the definition of the stream from the stream_table2[] table defined in the Extensions File.

For example, to define a new stream MYSTREAM you need to include the following sections in the Extensions File:

An enum stream2:

enum stream2 { MYSTREAM = USERSTREAMS };

The stream writing function and the corresponding stream table entry:

void mywrite (char c) { Mypackage.write(c); }

pfun_t pfun_mystream (uint8_t address) {
  (void) address;
  return mywrite;
}

The address parameter could be used to select between different stream writing functions, such as for different ports. In this example it's ignored.

The stream reading function and the corresponding stream table entry:

int myread () { return Mypackage.read(); }

gfun_t gfun_mystream (uint8_t address) {
  (void) address;
  return myread;
}

Again, the address is ignored.

Finally, the stream name and the supplementary stream lookup table:

const char mystreamname[] = "mystream";

const stream_entry_t stream_table2[] = {
 { mystreamname, pfun_mystream, gfun_mystream },
};

An example – a String Input Stream

To demonstrate the use of user-defined streams the Extension File StringInput.ino implements a simplified version of the standard Common Lisp form with-input-from-string using a string input stream. Here's an example of its use:

(defun test ()
  (let ((text (format nil "You say Goodbye~%And I say Hello~%~%")))
    (with-input-from-string (str text)
      (loop
       (let ((line (read-line str)))
         (when (zerop (length line)) (return))
         (print line))))))

> (test)

"You say Goodbye" 
"And I say Hello" 
nil

The demo program (test) reads lines from the string text until it reaches a blank line.

Loading the string input extension

To add the string input extension to uLisp:

  • Put it in the same folder as your uLisp source file.
  • In the main uLisp source file uncomment the lines:
#define extensions
#define streamextensions
  • Compile uLisp and upload it to your board.