ESP8266

2nd March 2018

uLisp, the compact Lisp interpreter for small microcontrollers, is now available in a beta version for the popular ESP8266.

As well as all the existing commands provided by uLisp, as described in Language reference, uLisp for the ESP8266 includes wi-fi networking commands to allow you to take advantage of the ESP8266's wi-fi capabilities with the convenience of uLisp's compact syntax; see Wi-fi commands.

For examples of using uLisp to take advantage of the ESP8266's wi-fi capabilities see Wi-fi examples.

Downloading uLisp for the ESP8266

Download the 2.0 beta 2 version of uLisp for the ESP8266 here:

uLisp ESP8266 Version 2.0 beta 2 - 13th March 2018

or get it from GitHub at https://github.com/technoblogy/ulisp-esp8266.

This download is a text file. To compile it in the Arduino IDE either save it as a text file and rename it to a .cpp file, or copy and paste the text into a new empty project file. For information about installing the ESP8266 core see ESP8266 core for Arduino on GitHub.

Please provide feedback and suggestions about uLisp in the uLisp forum.

Language overview

The following table gives a summary of the different boards currently supported by uLisp for the ESP8266:

Platform Module Long symbols Cells Image GC time Benchmark
Adafruit Huzzah Feather ESP-12F Yes 2560 887 0.5 msecs 11 secs
Adafruit Huzzah Breakout ESP-12 Yes 2560 887 0.5 msecs 11 secs
NodeMCU 1.0 ESP-12E Yes 2560 887 0.5 msecs 11 secs

For details of these boards see Boards below. uLisp will probably work on other ESP8266 boards, but they haven't been tested.

Cells gives the number of Lisp cells of storage available, equivalent to 8 bytes.

Image gives the number of cells that can be saved permanently to flash using save-image.

GC time gives the time taken for a garbage collection.

Benchmark gives the time taken to run the tak benchmark; see Benchmarks.

Because the ESP8266 is a 32-bit processor note the following differences from the Arduino ATmega version of uLisp:

  • Lisp cells are 8 bytes.
  • Integers are 32 bits, so have the range 2147483647 to -2147483648.
  • Strings are packed more economically, four characters per Lisp cell.

Note also that they are 3.3 V processors, and the I/O lines could be damaged by higher voltages.

Digital I/O

The ESP8266 provides 16 GPIO pins with a maximum current capability of 12mA. The pin numbers used by pinmode(), digitalread(), and digitalwrite() correspond directly to the ESP8266 GPIO pin numbers.

Digital pins 0 to 15 can be defined by pinmode() as 0 or nil (INPUT), 1 or t (OUTPUT), or 2 (INPUT_PULLUP). Pin 16 can be 0 or nil (INPUT), 1 or t (OUTPUT) or 4 (INPUT_PULLDOWN).

Analogue input

The ESP8266 has a single ADC pin which can be read using analogread(17). The input voltage range is 0 V to 1.0 V, corresponding to readings of 0 to 1024.

Analogue outputs

Analogue output using PWM can be used on pins 0 to 16, using analogwrite(). Note that the range is 0 to 1023, not 0 to 255 as on the Arduino, and the PWM frequency is 1kHz.

Note

The uLisp function note allows you to play notes on a well-tempered scale using any of the I/O pins.

Wi-fi commands

The ESP8266 is all about Wi-Fi, so uLisp for the ESP8266 provides several additional commands to interface with the wi-fi features of the chip. These are implemented by the following functions:


wifi-connect function

Syntax: (wifi-connect [ssid password])

Connects to the wi-fi network ssid using password password. It returns the IP address as a string. It may take a few seconds to connect. For example:

> (wifi-connect "Geronimo" "topsecret")
"10.0.1.28"

With no parameters wifi-connect disables the network connection.

Note that the ESP8266 stores the last used access point in EEPROM, so if it loses the connection for some reason it will reconnect to the last used access point once it is back online and you do not need to give a wifi-connect command again.

wifi-localip function

Syntax: (wifi-localip)

Returns the IP address of the local network as a string.

with-client special form

Syntax: (with-client (stream [address port]) form*)

The with-client form evaluates the forms with stream bound to a wifi-stream.

If address and port are specified, with-client allows you to communicate with the IP address defined by address and port. The address can be an integer representing a 32-bit IP address, or a string representing a domain name.

If with-client is called with just the stream parameter it allows you to communicate with a client connected to a server started with wifi-server. If there are no active clients with-client returns nil immediately without evaluating the forms. 

In each case you can then use the standard uLisp I/O functions to write to or read from the stream. See Wi-fi examples below for examples of using with-client.

available function

Syntax: (available stream)

Returns the number of bytes available for reading from the stream, or zero if no bytes are available.

connected function

Syntax: (connected stream)

Returns t or nil to indicate if the client on stream is still connected.

wifi-server function

Syntax: (wifi-server)

Starts a server running. It returns nil.

wifi-softap function

Syntax: (wifi-softap ssid [password channel hidden])

Set up a soft access point to establish a Wi-Fi network. Returns the IP address as a string or nil if unsuccessful. For example:

> (wifi-softap "Buckyball")
"192.168.4.1"

With one parameter ssid it sets up an open Wi-Fi network. One or more of the remaining parameters can be specified. The channel is an optional channel, default 1. If hidden is set to non-nil the access point will be hidden.

With no parameters wifi-softap disables the soft access point.


Wi-fi examples

The following examples demonstrate using the wi-fi capabilities of uLisp with an ESP8266 board. Note that these examples don't take any account of security; if you are going to use them as the basis for your application please consider security carefully.

Connecting to a wi-fi network

The first step is to connect to a local wi-fi access point, with a command such as:

> (wifi-connect "Geronimo" "secret99")
"10.0.1.28"

where Geronimo is the network name, or SSID, and secret99 is the password. It returns the IP address as a string.

Connecting to a web page

This example connects to a web site and reads the contents of the page. First we define a function println that prints a string followed by the line ending required by most web protocols: return, newline:

(defun println (x s) (princ x s) (princ #\return s) (princ #\newline s))

Here's the routine to connect to the uLisp website, read a page, and list it to the Serial Monitor:

(with-client (s "www.ulisp.com" 80)
   (println "GET /rss?1DQ5 HTTP/1.0" s)
   (println "Host: www.ulisp.com" s)
   (println "Connection: close" s) 
   (println "" s)
   (loop
    (let ((line (read-line s)))
      (unless (null line) (princ line) (terpri))
      (unless (connected s) (return)))))

The above example reads the RSS version of the uLisp News page, with the XML formatting.

Reading a function from a web page

The following example reads the uLisp definition of a function secret from a web page on the uLisp site, and evaluates it to add it to the uLisp image:

(with-client (s "www.ulisp.com" 80)
  (println "GET /list?21Z0 HTTP/1.0" s)
  (println "Host: www.ulisp.com" s)
  (println "Connection: close" s) 
  (println "" s)
  (loop (when (= 1 (length (read-line s))) (return)))
  (eval (read s))
  (secret))

It uses the println function from the previous example. The loop form reads past the webpage header. The eval reads the function definition from the page, and the form (secret) evaluates it.

Sending an email

This example uses with-client to connect to an SMTP mail server, on port 25, and then sends an email. For example, you could design a project that sent an email containing temperature data every hour.

The mail server needs to be one to which you have login access, using a username and password. The addresses in the following example won't work; you need to substitute your own. The username and password are provided on the two lines following the AUTH LOGIN command; these need to be encoded using Base64 encoding [1]:

(with-client (s "mail.ulisp.com" 25)
   (println "EHLO ulisp.com" s)
   (println "AUTH LOGIN" s)
   (println "RGF2aWQ=" s) 
   (println "dG9wc2VjcmV0" s)
   (println "MAIL FROM:david@ulisp.com" s)
   (println "RCPT TO:lisa@gmail.com" s)
   (println "DATA" s)
   (println "Fancy a curry tonight?" s)
   (println "." s)
   (println "QUIT" s))

There are other headers you can provide, such as a subject line, but this example shows the minimum. The body of the message follows the DATA command, and is terminated by a line containing just a full stop.

This example assumes that there are no errors, and ignores the responses from the server. A better implementation would read the server response and check the return code at the start of each line.

Sending an email with feedback

The following example is identical to the previous one, except that the commands and responses are echoed to the Serial Monitor. This uses a new function talk to wait until data is available to be read, and then prints it:

(defun talk (x s)
  (println x s) 
  (princ x) (terpri)
  (loop (unless (zerop (available s)) (return)))
  (loop
   (princ (read-line s)) (terpri)
   (when (zerop (available s)) (return))))

Here's the new email sending routine:

(with-client (s "mail.ulisp.com" 25)
  (talk "EHLO ulisp.com" s)
  (talk "AUTH LOGIN" s)
  (talk "RGF2aWQ=" s) 
  (talk "dG9wc2VjcmV0" s)
  (talk "MAIL FROM:david@ulisp.com" s)
  (talk "RCPT TO:lisa@gmail.com" s)
  (talk "DATA" s)
  (println "Fancy a curry tonight?" s)
  (talk "." s)
  (talk "QUIT" s)))

Note that there's no response after the DATA command so we use println for the message.

Serving a web page

The following example reads the ADC input and displays its value on a web page served from the ESP8266.

First we run the server:

> (wifi-server)
nil

The address we need to put into a web browser to fetch the page is the IP address that was returned by wifi-connect.

The following routine then serves the web page.

(with-client (s)
  (loop
   (let ((line (read-line s)))
     (when (null line) (return))
     (print line)))
  (println "HTTP/1.1 200 OK" s)
  (println "Content-Type: text/html" s)
  (println "Connection: close" s)
  (println "" s)
  (princ "<!DOCTYPE HTML><html><body><h1>ADC Value: " s)
  (princ (analogread 17) s)
  (println "</h1></body></html>" s)
  (println "" s))

The call to with-client does nothing and returns nil if there's no web browser trying to connect. Otherwise the loop form reads the request from the web browser and displays it. The println statements then submits the web page to be displayed in the browser.

Creating a soft access point

As well as connecting to an existing wi-fi network the ESP8266 can create its own wi-fi network, called a soft access point. You can connect to this from a computer or mobile device, and access a web page served from the soft access point.

For example, you could use an ESP8266 to create a wireless sensor that you could interrogate from a mobile phone, even if the sensor wasn't in range of a wireless network.

To create a soft access point called Bonzo give the command:

> (wifi-softap "Bonzo")
"192.168.4.1"

The command returns the IP address of the soft access point. If you now connect a computer or mobile device to the network Bonzo, and connect a web browser to the IP address 192.168.4.1, you can access a web page served as in the previous example.

Lisp server

The next example uses the ESP8266 as a Lisp server. You can type Lisp expressions in a terminal window on your computer. They will be transferred via wi-fi to the ESP8266, evaluated by uLisp running there, and the result will be returned to the terminal window on your computer.

You can use this to add functions to the uLisp workspace, read from or write to ports on the ESP8266, etc.

First open a terminal window on your computer and give a telnet or nc (netcat) command to listen on a port such as 1234 (on recent Macs telnet is deprecated):

nc -l 1234

Define the following function in uLisp:

(defun lisp-server ()
  (with-client (s (ip '(10 0 1 3)) 1234)
    (print "Listening...")
    (loop
     (unless (= 0 (available s))
       (let ((line (read-line s)))
         (print line)
         (println (eval (read-from-string line)) s)))
     (delay 1000))))

Replace (10 0 1 3) with the correct IP address for your computer on the local network.

This also requires the following two utilities:

(defun ip (lis)
  (let ((x 0))
    (mapc (lambda (n) (setq x (+ (ash x 8) n))) (reverse lis))
    x))

(defun println (x s) (princ x s) (princ #\return s) (princ #\newline s))

The ip function converts a list of four integers into a 32-bit number representing an IP address, and println is the line-printing function we used earlier.

Now run the Lisp server by typing:

(lisp-server)

You should now be able to type Lisp expressions in the Terminal window, and see the result returned there:

david$ nc -l 1236
(defun sq (x) (* x x))
sq
(sq 222)
49284

Scrolling text display server

The final example is a bit more complicated. You can send a message via wi-fi from a web browser, and it will be displayed as scrolling text on a four-character 14-segment starburst display connected to the ESP8266 I2C port. You could have it on your mantlepiece, displaying welcome messages to visitors:

ESP8266Scroller.jpg

To display the message you enter the following URL:

http://10.0.1.27/?Happy+Birthday+Lisa!

where 10.0.1.27 is whatever the wifi-connect command reported as the IP address of the ESP8266's wi-fi connection, and the text following the '?' is the message you want to display. To make it a valid URL you need to put '+' signs instead of spaces, and avoid special characters that are invalid in URLs.

Here's the main display-server routine:

(defun display-server ()
  (wifi-server)
  (on 8)
  (loop
   (with-client (s)
     (let* ((line (read-line s))
            (text (subseq line 6 (- (length line) 10))))
       (loop (unless (= 0 (available s))) (return))
       (println "HTTP/1.1 200 OK" s)
       (println "Content-Type: text/html" s)
       (println "Connection: close" s)
       (println "" s)
       (print text)
       (princ "<!DOCTYPE HTML><html>Received OK</html>" s)
       (println "" s)
       (scroll text)))
   (delay 1000)))

It checks once a second to see if a web client is connected. If it is, the program reads the first line of the request from the web browser, which will be something like:

GET /?Happy+Birthday+Lisa! HTTP/1.1 

The subseq statement extracts the message from this line, and the program then sends a response to the web client. Finally, the program calls scroll to display the text as a scrolling message on the LED display. This part is based on my earlier uLisp application Scrolling text display.

Here's the whole program: Scrolling display server program.


Boards


Adafruit Huzzah Feather

My recommended ESP8266 board is the Adafruit Huzzah Feather, an 80 MHz ESP8266-based board with 4 Mbytes of flash powered by Ai-Thinker’s ESP-12S. It's compatible with Adafruit's range of Feather add-ons, and includes a high-quality SiLabs CP2104 USB-Serial chip which supports uploading at 921600 baud. It also has auto-reset so you don't need to press buttons before uploading. It includes a built-in battery connector and battery charger for a LiPo battery.

ESP8266Feather.jpg 

LEDs

The Adafruit Huzzah Feather has a red LED connected to the GPIO pin 0, and a blue LED connected to the GPIO pin 2 which you can flash alternately with the following program:

(defun blink (x)
  (pinmode 0 t)
  (pinmode 2 t)
  (digitalwrite 0 x)
  (digitalwrite 2 (not x))
  (delay 1000)
  (blink (not x)))

Run it by typing:

(blink t)

Exit from the program by entering ~.

Any pin can also be used as an analogue pin, so you can pulsate the blue LED slowly on and off with the program:

(defun pulse ()
  (let (down)
    (loop
     (dotimes (x 1024) 
       (delay 1) 
       (analogwrite 2 (if down (- 1023 x) x)))
     (setq down (not down)))))

Run it by typing:

(pulse)

Exit from the program by entering ~.

Adafruit Huzzah Breakout

The Adafruit Huzzah Breakout is similar to the Adafruit Huzzah Feather, except that you have to provide your own USB-to-serial interface. I used Sparkfun's FTDI Basic Breakout [2]. You can use either a 5 V or a 3.3 V USB-to-serial interface as the Huzzah Breakout includes a 3.3 V regulator:

ESP8266Huzzah.jpg

One negative feature about the board is that you have to press two buttons to enter the bootloader each time you want to upload a program from the Arduino IDE. Read the Adafruit Huzzah ESP8266 breakout instructions for uploading a program using the Arduino IDE.

LEDs

The Adafruit Huzzah Breakout has a red LED connected to the GPIO pin 0, and a blue LED connected to the GPIO pin 2 which you can flash alternately with the following program:

(defun blink (x)
  (pinmode 0 t)
  (pinmode 2 t)
  (digitalwrite 0 x)
  (digitalwrite 2 (not x))
  (delay 1000)
  (blink (not x)))

Run it by typing:

(blink t)

Exit from the program by entering ~.

Any pin can also be used as an analogue pin, so you can pulsate the blue LED slowly on and off with the program:

(defun pulse ()
  (let (down)
    (loop
     (dotimes (x 1024) 
       (delay 1) 
       (analogwrite 2 (if down (- 1023 x) x)))
     (setq down (not down)))))

Run it by typing:

(pulse)

NodeMCU

The NodeMCU is widely available at low cost on sites such as Banggood, eBay, and Aliexpress. It's based on the ESP8266-12E and includes 4 Mbytes of Flash. Two versions of the board are available: with the CP2104 USB-Serial chip, or with the CH340 USB-serial chip. You can tell the difference from the shape of the chip next to the USB socket. I chose the CP2102 version as it is reputed to have better driver support on the Mac than the CH340:

ESP8266NodeMCU.jpg

11 I/O pins are accessible on the board, labelled D0 to D10. These correspond to the following pin numbers, as used by the uLisp routines such as pinmode, digitalread, digitalwrite, analogwrite, and note:

Label: D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 D10
Pin: 16 5 4 0 2 14 12 13 15 3 1

LEDs

The NodeMCU board has two blue LEDs on GPIO pins 2 and 16. You can flash them alternately with the following program:

(defun blink (x)
  (pinmode 2 t)
  (pinmode 16 t)
  (digitalwrite 2 x)
  (digitalwrite 16 (not x))
  (delay 1000)
  (blink (not x)))

Run it by typing:

(blink t)

Exit from the program by entering ~.

Any pin can also be used as an analogue pin, so you can pulsate the blue LED on GPIO 2 slowly on and off with the program:

(defun pulse ()
  (let (down)
    (loop
     (dotimes (x 1024) 
       (delay 1) 
       (analogwrite 2 (if down (- 1023 x) x)))
     (setq down (not down)))))

Run it by typing:

(pulse)

Exit from the program by entering ~.

Acknowledgements

I'd like to thank Holger Zahnleiter and Jan Delgado for getting the first version of uLisp working on the ESP8266. Also, the ESP8266 version of uLisp wouldn't be possible without Ivan Grokhotkov's amazing work on the Arduino Core for the ESP8266.


  1. ^ There are several websites that will do this encoding for you; for example https://www.base64encode.org/.
  2. ^ SparkFun FTDI Basic Breakout - 5V on SparkFun.