Wi-Fi examples

The following examples demonstrate using the Wi-Fi capabilities of uLisp with an ESP8266, ESP32, or Raspberry Pi Pico W 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.

Update

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

13th September 2022: Simplified the Lisp server example by giving the IP address of the remote computer as a string.

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 Wi-Fi board.

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).

On the Raspberry Pi Pico W the analogue inputs are on pins 26 to 29, so for example change (analogread 0) to (analogread 26).

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 the ADC input.

You can exit from webpage by typing ~.

Creating a soft access point

As well as connecting to an existing Wi-Fi network the Wi-Fi boards can create their 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 a Wi-Fi board 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 Wi-Fi board as a Lisp server. You can type Lisp expressions in a terminal window on a remote computer. They will be transferred via Wi-Fi to the board, evaluated by uLisp running there, and the result will be returned to the terminal window on the remote 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

On the local computer connected to the Wi-Fi board 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.

As before, this also requires the line-printing function println we used earlier:

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

Discover the IP address of the remote computer

The remote computer can be any computer on the network, including the local computer.

One way to get the IP address is to 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 the 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 a string giving the IP address of the remote computer

For example, enter:

> (lisp-server "10.0.1.4")

"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 on the remote computer 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 Wi-Fi board's 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 Wi-Fi board'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.