Wi-Fi examples

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

Updates

28th June 2023: The examples have been substantially updated.

2nd July 2023: Added a title comment to each function.

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.

To avoid leaving your Wi-Fi password in a source file, the following command prompts you to enter it:

(wifi-connect "Geronimo" (progn (princ "Password: ") (string (read))))

For example:

> (wifi-connect "Geronimo" (progn (princ "Password: ") (string (read))))
Password: secret99
"10.0.1.28"

Web client

Connecting to a web page

This example webpage shows how to read the contents of a web page. It connects to the uLisp website, reads the RSS version of the uLisp News page (with XML formatting), and lists it to the Serial Monitor:

; Print the contents of a web page
(defun webpage ()
  (let ((println #'(lambda (x s) (format s "~a~a~%" x #\return))))
    (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
       (delay 100)
       (when (zerop (available s)) (return))
       (princ (read-line s))
       (terpri)))))

This function, and many of the subsequent examples, defines a local function println that prints a string followed by the line ending required by most web protocols: return, newline.

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:

; Read and evaluate a function from a web page
(defun decode ()
  (let ((println #'(lambda (x s) (format s "~a~a~%" x #\return))))
    (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:

; Character translation table for base64
(defun char64 (n)
  (char "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
        (logand n #x3F)))
; Base64 encoder
(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:

; Send an email
(defun send (message)
  (let ((println #'(lambda (x s) (format s "~a~a~%" x #\return))))
    (with-client (s "mail.mysite.com" 25)
      (println "EHLO mysite.com" s)
(println "AUTH LOGIN" s) (base64 "david" s) (base64 "secret99" s) (println "MAIL FROM:david@mysite.com" s)
(println "RCPT TO:lisa@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 local function talk to wait until data from the server is available to be read, and then prints it. Here's the new email sending routine, send2:

; Send an email with feedback
(defun send2 (message)
  (let* ((println #'(lambda (x s) (format s "~a~a~%" x #\return)))
         (talk #'(lambda (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))))))
    (with-client (s "mail.mysite.com" 25)
      (talk "EHLO mysite.com" s)
(talk "AUTH LOGIN" s) (base64 "david" s) (base64 "secret99" s) (talk "MAIL FROM:david@mysite.com" s) (talk "RCPT TO:lisa@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 an ADC input and displays its value on a web page served from the Wi-Fi board. Here's the function web-server:

; Serve a web page displaying an ADC value
(defun web-server (adc)
  (let ((println #'(lambda (x s) (format s "~a~a~%" x #\return))))
    (wifi-server)
    (format t "Connect your web browser to ~a" (wifi-localip))
    (loop   
     (with-client (s)
       (loop (when (= 1 (length (read-line s))) (return)))
       (println "HTTP/1.1 200 OK" s)
       (println "Content-Type: text/html" s)
       (println "Connection: close" s)
       (println "Refresh: 5" s)
       (println "" s)
       (princ "<!DOCTYPE HTML><html><body><h1>ADC Value: " s)
       (princ (analogread adc) s)
       (println "</h1></body></html>" s)
       (println "" s))
     (delay 1000))))

The routine first calls (wifi-server) which starts a web server.

The routine then waits in a loop waiting for a client. The call to with-client returns nil if there's no web browser trying to connect. Otherwise it reads the request from the web browser and displays it. The println statements then submit the web page to be displayed in the browser.

Running the web-server

Run web-server by typing:

(web-server 0)

where the parameter is a suitable ADC input.

It then displays a message such as:

Connect your web browser to 10.0.1.28

where the address is the same as the address returned earlier by wifi-connect.

Now go to a web browser and enter that address in the address bar. You should see a page showing the value on the ADC input.

You can exit from web-server by typing ~.

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 control a device from a mobile phone, even if the device wasn't in range of a wireless network. The following example illustrates this by allowing you to turn on or off the builtin LED using buttons on a web page.

Creating a soft access point

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

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

The password should be at least 8 characters; it can be omitted on some platforms.

The command returns the IP address of the soft access point.

Control panel

The following routine control-panel allows you to control an LED from a web page:

; Control an LED from a web page
(defun control-panel ()
  (let ((println #'(lambda (x s) (format s "~a~a~%" x #\return))))
    (pinmode :led-builtin t)
    (wifi-server)
    (format t "Connect your network and web browser to ~a" (wifi-localip))
    (loop
     (with-client (s)
       ; First line is request
       (let ((line (read-line s)))
          (when (search "GET /ON" line) (digitalwrite :led-builtin t))
          (when (search "GET /OFF" line) (digitalwrite :led-builtin nil)))
       ; Read up to blank line
       (loop (when (= 1 (length (read-line s))) (return)))
       ; Send response page
       (println "HTTP/1.1 200 OK" s)
       (println "Content-Type: text/html" s)
       (println "Connection: close" s)
       (println "" s)
       (princ "<!DOCTYPE HTML><html><body>" s)
       (princ "<p>Control LED: <a href='/ON'><button> On </button></a>" s)
       (princ "<a href='/OFF'><button> Off </button></a></p>" s)
       (princ "</body></html>" s)
       (println "" s))
     (delay 5000))))

Running the control panel

Run the control panel by typing:

(control-panel)

It then displays a message such as:

Connect your network and web browser to 192.168.4.1

where the address is the address returned earlier by wifi-softap.

Now connect a computer or mobile device to the network Bonzo, and connect a web browser to the IP address displayed in the message.

After a short delay you should see the control panel web page:

ControlLED.gif

You can click the On or Off buttons to control the LED.

To exit from control-panel type ~.

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

As in the earlier examples, on the local computer connected to the Wi-Fi board evaluate:

(wifi-connect "Geronimo" (progn (princ "Password: ") (string (read))))

and enter your Wi-Fi password.

Define the lisp-server function

Define the following function in uLisp:

; Run a Lisp server to evaluate Lisp expressions remotely
(defun lisp-server (ipstring)
  (let ((println #'(lambda (x s) (format s "~a~a~%" x #\return))))
    (with-client (s ipstring 8080)
      (print "Listening...")
      (loop
       (unless (= 0 (available s))
         (let* ((line (read-line s))
                (result (ignore-errors (eval (read-from-string line)))))
           (print line)
           (println (if (eq result nothing) "Error!" result) s)))
       (delay 1000)))))

Here I've used 8080 as the port number.

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

The Lisp server should respond in the Serial Monitor with:

"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 Lisp espressions such as:

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

and it will echo

sq

Then you can type:

(sq 123)

and it will echo

15129

Lisp errors are trapped by ignore-errors. On errors this returns the special value nothing, in which case Error! is displayed. For example, if you type:

(/ 1 0)

it will echo:

Error!

To exit from the telnet session press Ctrl-Z.