Wi-Fi examples

The following examples demonstrate using the Wi-Fi capabilities of uLisp with one of the ESP8266 or ESP32 boards supported by uLisp. 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.

Update

24th January 2022: Updated the description of running the Lisp server example to make it clearer.

Connecting to Wi-Fi

Connecting to a Wi-Fi network

The first step before running any of these examples 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.

The following examples all make use of the following 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))

Web client

Connecting to a web page

This example webpage connects to the uLisp website, reads the RSS version of the uLisp News page (with XML formatting), and lists it to the Serial Monitor:

(defun webpage ()
  (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 (unless (zerop (available s)) (return)))
    (loop
     (when (zerop (available s)) (return))
     (princ (read-line s))
     (terpri))))

To run it evaluate:

(webpage)

Reading and evaluating a function from a web page

The following example decode 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:

(defun decode ()
  (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)))

To run it evaluate:

(decode)

The loop form reads past the webpage header. The eval reads the function definition from the page, and the form (secret) evaluates it, printing a secret message.

Sending email

Sending an email

This example defines a function send that lets you send a string in an email. It 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. The routine base64 takes a string and a stream, and outputs the Base64-encoded version of the string to the stream, using char64 to generate the Base64 character set:

(defun char64 (n)
  (char "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
        (logand n #x3F)))
(defun base64 (str stm)
  (let* ((l (length str))
         (max (ceiling l 3)))
    (setq str (concatenate 'string str (string #\soh) (string #\soh)))
    (dotimes (i max)
      (let* ((w (* i 3))
             (a (char-code (char str w)))
             (b (char-code (char str (1+ w))))
             (c (char-code (char str (+ 2 w)))))
        (princ (char64 (ash a -2)) stm)
        (princ (char64 (logior (ash a 4) (ash b -4))) stm)
        (princ (if (>= (1+ w) l) "=" (char64 (logior (ash b 2) (ash c -6)))) stm)
        (princ (if (>= (+ 2 w) l) "=" (char64 c)) stm)))
    (princ #\return stm) (princ #\newline stm)))

Here's the routine send to send an email:

(defun send (message)
  (with-client (s "mail.mysite.com" 25)
    (println "EHLO mysite.com" s)
    (println "AUTH LOGIN" s)
    (base64 "david" s) 
    (base64 "secret" s)
    (println "MAIL FROM:david@mysite.co.uk" s)
    (println "RCPT TO:david@othersite.com" s)
    (println "DATA" s)
    (println message s)
    (println "." s)
    (println "QUIT" s)))

For example, you could run this by evaluating:

(send "Fancy a curry tonight?")

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 is useful if the email fails to send, as you can see the server's error message; for example:

535 Error: authentication failed

This version uses a new function talk to wait until data from the server 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, send2:

(defun send2 (message)
  (with-client (s "mail.mysite.com" 25)
    (talk "EHLO mysite.com" s)
(talk "AUTH LOGIN" s) (base64 "david" s) (base64 "secret" s) (talk "MAIL FROM:david@mysite.co.uk" s) (talk "RCPT TO:david@othersite.com" s)
(talk "DATA" s) (println message s) (talk "." s) (talk "QUIT" s)))

We don't echo the username or password. Note that there's no response after the DATA command so we use println for the message.

Web server

Serving a web page

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

First we run the server:

> (wifi-server)
nil

Now define the following function webpage:

(defun webpage ()
  (loop
   (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 0) s)
    (println "</h1></body></html>" s)
    (println "" s))
   (delay 5000)))

On the ESP8266 the analogue input is on pin 17, so change (analogread 0) to (analogread 17).

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 submit the web page to be displayed in the browser.

Run webpage by typing:

(webpage)

It will wait in a loop waiting for a client to connect.

Now go to a web browser and enter the address wifi-connect returned earlier. You should see a page showing the value on ADC 0.

You can exit from webpage by typing ~.

Creating a soft access point

As well as connecting to an existing wi-fi network the ESP8266 or ESP32 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 or ESP32 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 or ESP32 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 or ESP32, 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 board, etc.

Connect to your Wi-Fi with your network name and password

Type:

> (wifi-connect "Home" "secret")
"10.0.1.49"

Define the lisp-server function and the utilities

Define the following function in uLisp:

(defun lisp-server (&rest iplist)
  (with-client (s (ip iplist) 8080)
    (print "Listening...")
    (loop
     (unless (= 0 (available s))
       (let ((line (read-line s)))
         (print line)
         (println (eval (read-from-string line)) s)))
     (delay 1000))))

Here I've used 8080 as the port number.

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.

Discover the IP address of your remote computer

This can be the one connected to the ESP8266 or ESP32, or any other computer on the network.

For example you can use ipconfig at the terminal:

$ ipconfig getifaddr en0
10.0.1.44

or you might need to use en1.

Start telnet from the terminal on your remote computer to the port you're going to use

Give a telnet or nc (netcat) command to listen on the port 8080 (on recent Macs telnet is deprecated):

$ nc -l 8080

It should hang up.

Run lisp-server with the IP address of the remote computer

For example, enter:

> (lisp-server 10 0 1 44)

"Listening..."

If you omitted to start the telnet session in the previous step it will just return with nil.

Enter some Lisp

Now at the telnet session you can type:

(defun sq (x) (* x x))

and it will echo

sq

Then you can type:

(sq 123)

and it will echo

15129

To exit from the telnet session press Ctrl-Z.

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.