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.

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@interface.co.uk" s)
    (println "RCPT TO:david@technoblogy.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@ulisp.com" s) (talk "RCPT TO:david@technoblogy.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)))

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.

Define the following function in uLisp:

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

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.

Next 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

Now run the Lisp server, and specify the ip address of your computer, by typing something like:

(lisp-server 10 0 1 3)

Replace 10 0 1 3 with the correct IP address for your computer on the local network (not the address returned by wifi-connect earlier). On a Mac you can find this address in the Network panel in System Preferences. Note that you should give the ip address as a list of four numbers with no periods, as this is what the ip function expects.

The Lisp server should print:

"Listening..."

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.