mgr's weblog

uLisp on M5Stack (ESP32):
support for the LED matrix of the M5Atom Matrix

December 9, 2021, Lisp
Last edited on December 14, 2021

I got a good friend join the uLisp fun and he extended my support for the single LED of the M5Atom Lite to support the 25 LEDs of the M5Atom Matrix. The single LED has just the same interface as the LED matrix, as expected.

Thanks, Thorsten!

It has a nice backwards compatible interface, the functions atomled (for C) and atom-led (for Lisp) just have a new second argument index, which is 0 by default, for the first— or, in case of the M5Atom Lite, only—LED.

The C function you can call like this:

atomled(0x00ff00);
/* or: */
atomled(0x00ff00, 23);

where 0x00ff00 describes a RGB color in 32 bits.

And the uLisp function you can call very similarly like this:

(atom-led #xffff00)
#| or: |#
(atom-led #xffff00 23)

I have merged it to my repository ulisp-esp-m5stack already. Activate the new flag #define enable_m5atom_led_matrix in addition to #define enable_m5atom_led to use the whole LED matrix of the M5Atom Matrix instead of just the first LED.


See also built-in LED of the M5Atom Lite.

uLisp on M5Stack (ESP32):
built-in LED of the M5Atom Lite

December 8, 2021, Lisp

I just published support of the M5Atom Lite LED at ulisp-esp-m5stack.

There is a C function that you can call like this:

atomled(0x00ff00);

where 0x00ff00 describes a RGB color in 32 bits.

And a uLisp function that you can call very similarly like this:

(atom-led #xffff00)

Activate #define enable_m5atom_led to get it. That will also automatically init_atomled(); in setup() after booting the ESP32.

I have actually tried the libraries FastLED (by Daniel Garcia, version 3.4.0), Easy Neopixels (by Evelyn MAsso, version 0.2.3), and NeoPixelBus (by Makuna, version 2.6.9) as well, but settled to use the library Adafruit NeoPixel (by Adafruit, version 1.10.0). It is small, doesn't have tons of bloat, works for me and has a nice interface that makes my implementation so tiny you would think it was almost no work.

uLisp on M5Stack (ESP32):
new version published

December 6, 2021, Lisp
Last edited on December 6, 2021

I got notified that I haven't updated ulisp-esp-m5stack at GitHub for quite a while. Sorry, for that. Over the last months I worked on a commercial project using uLisp and forgot to update the public repository. At least I have bumped ulisp-esp-m5stack to my version of it from May 13th, 2021 now.

It is a—then—unpublished version of uLisp named 3.6b which contains a bug fix for a GC bug in with-output-to-string and a bug fix for lispstring, both authored by David Johnson-Davies who sent them to my via email for testing. Thanks a lot again! It seems they are also included in the uLisp Version 3.6b that David published on 20th June 2021.

I know there David published a couple of new releases of uLisp in the meantime with many more interesting improvements but this is the version I am using since May together with a lot of changes by me which I hope to find time to release as well in the near future.

Error-handling in uLisp by Gohecca

I am using Goheeca's Error-handling code since June and I couldn't work without it anymore. I just noticed that he allowed my to push his work to my repository in July already. So I just also published my branch error-handling to ulisp-esp-m5stack/error-handling. It's Goheeca's patches together with a few small commits by me on top of it, mainly to achieve this (as noted in the linked forum thread already):

To circumvent the limitation of the missing multiple-values that you mentioned with regard to ignore-errors, I have added a GlobalErrorString to hold the last error message and a function get-error to retrieve it. I consider this to be a workaround but it is good enough to show error messages in the little REPL of the Lisp handheld.


See also "Stand-alone uLisp computer (with code!)".

uLisp on M5Stack (ESP32):
controlling relays connected to I2C via a PCF8574

October 18, 2021, Lisp
Last edited on October 18, 2021

relay module connected to I2C via a PCF8574; click for a larger version (180 kB).

Looking at the data sheet of the PCF8574 I found that it will be trivially simple to use it to control a relay board without any lower level Arduino library: Just write a second byte in addtion to the address to the I2C bus directly with uLisp's WITH-I2C.

Each bit of the byte describes the state of one of the eight outputs, or rather its inverted state as the PCF8574 has open-drain outputs and thus setting an output to LOW opens a connection to ground (with up to 25 mA), while HIGH disables the relay. (The data sheets actually say they are push-pull outputs but as high-level output the maximum current is just 1 mA which is not much and for this purpuse certainly not enough.)

The whole job can basically done with one or two lines. Here is switching on the forth relay (that is number 3 with zero-based counting):

(with-i2c (str #x20)
  (write-byte (logand #xff (lognot (ash 1 3))) str))

Here is my whole initial library:

#| control a relay module connected to I2C via a PCF8574 module |#
#| written by Max-Gerd Retzlaff <m.retzlaff@gmx.net>, 2021 |#
 
#| the current state of the relay |#
(defvar *relay* 0)
 
#| address of the PCF8574 module connected to the relay |#
(defvar *relay-address* #x20)
 
#| show state of relay as binary number |#
(defun show-relay ()
  (format nil "~8,'0b" *relay*))
 
#| translate *relay* to relay byte as sent to the PCF8574 |#
(defun relay-byte ()
  (logand #xff (lognot *relay*)))
 
#| actually set real relay via i2c |#
(defun set-relay ()
  (with-i2c (str #x20)
    (write-byte (relay-byte) str)))
 
#| initialize relay |#
(defvar init-relay set-relay)
 
#| switch on relay N |#
(defun relay-on (n)
  (setf *relay* (logior *relay* (ash 1 n)))
  (set-relay))
 
#| switch off relay N |#
(defun relay-off (n)
  (setf *relay* (logand *relay* (lognot (ash 1 n))))
  (set-relay))
 
#| set relay N to STATE |#
(defun relay! (n state)
  ((if state relay-on relay-off) n))
 
#| query state of relay N |#
(defun relay? (n)
  (= 1 (logand 1 (ash *relay* (- n)))))

Be sure to read the newer data sheets "PCF8574 Remote 8-Bit I/O Expander for I2C Bus" by Texas Instruments, revised in March 2015, or "PCF8574; PCF8574A – Remote 8-bit I/O expander for I2C-bus with interrup" by NXP, revised on 27 May 2013, and not the ancient one by Philips of 2002 that many link to. The new ones are much more detailed and explanatory.


See also "Stand-alone uLisp computer (with code!)", "temperature sensors via one wire", "Curl/Wget for uLisp", time via NTP, lispstring without escaping and more space, flash support, muting of the speaker and backlight control and uLisp on M5Stack (ESP32).

uLisp on M5Stack (ESP32):
Stand-alone uLisp computer (with code!)

April 2, 2021, Lisp
Last edited on December 6, 2021

Last Thursday, I started to use the m5stack faces keyboard I mentioned before and wrote a keyboard interpreter and REPL so that this makes another little handheld self-containd uLisp computer. Batteries are included so this makes it stand-alone and take-along. :)

I have made this as a present to my nephew who just turned eight last Saturday. Let's see how this can be used to actually teach a bit of Lisp. The first programming language needs to be Lisp, of course!

Programming “Hello World!” on the M5Stack with Faces keyboard; click for a larger version (252 kB).

Implementation

Talking to the keyboard was actually embarrassingly easy as the M5Faces input panels like the keyboard have their own separate processor, a Atmel MEGA328, and could actually run uLisp themselves. The code of the keyboard is in the official m5stack GitHub account in the file KeyBoard.ino.

The second reason is that they communicate with the ESP32 of the M5Core via I2C and uLisp comes with a WITH-I2C form as part of the I2C and SPI serial interface. So nothing to be done in C and everything can be implemented in uLisp code.

The read keyboard code was written straight forward and is simply:

(defvar *faces-kbd-addr* #x08)
(defvar *faces-kbd-pin* 5)
 
(defun init-kbd ()
  (pinmode *faces-kbd-pin* :input )
  (digitalwrite *faces-kbd-pin* :high))
 
(defun kbd-pressed-p ()
  (null (digitalread *faces-kbd-pin*)))
 
(defun raw-read-kbd ()
  (with-i2c (str *FACES-KBD-ADDR* 1)
    (read-byte str)))
 
(defun decode-char (code)
  (if (< code 128)
      (code-char code)
      code))
 
(defun read-kbd ()
  (when (kbd-pressed-p)
    (let ((raw (raw-read-kbd)))
      (when raw ;; might be nil if i2c unsuccessful as the kbd got disconnected
        (decode-char raw)))))
 
#|
(loop (let ((key (read-kbd))) (when key (print key))) (check-three-finger-salute) (delay 10))
|#

And the code of the first very simple REPL is this:

(defun input->line (input &optional (prompt ""))
  (apply concatenate 'string prompt (reverse (mapcar string input))))
 
(defvar *prompt* "> ")
 
(defun new-prompt ()
  (push *prompt* *screen-lines*)
  (redraw-screen))
 
(defun update-prompt (input)
  (pop *screen-lines*)
  (push (input->line input *prompt*) *screen-lines*)
  (redraw-screen))
 
(defun kbd-repl (&optional (welcome t))
  (when welcome
    (to-screen "This is ulisp-esp-m5stack!"))
  (new-prompt)
  (let (input)
    (loop
       (when (and (button-pressed-p *button-1*)
                  (button-pressed-p *button-3*))
         (to-screen 'exit-repl)
         (return))
       (let ((key (read-kbd)))
         (when key
           (print key)
           (case key
             (#\backspace
              (if input
                  (progn
                    (princ 'rubout)
                    (pop input)
                    (update-prompt input))
                  (princ 'no-input)))
             (167 #| alt-c |#
              (princ 'abort)
              (setf input nil))
             (#\Return
              (to-screen (eval (read-from-string (input->line input))))
              (new-prompt)
              (setf input nil))
             (t
              (if (and key
                       (characterp key)
                       (<= 32 (char-code key) 126))
                  (progn
                    (princ 'add)
                    (push key input)
                    (update-prompt input))
                  (princ 'ignored)))))
       (delay 10)))))
 
(defun start ()
  (set-text-size 2) (setf *max-lines* 14)
  (set-text-wrap t)
  (init-kbd)
  (check-three-finger-salute)
  (clear-screen)
  (kbd-repl))

I have to admit it is a harsh environment to learn Lisp right now. One error and everything freezes. Some would claim that makes for fast learning like in the good old days on paper. haha But I noted that someone named Goheeca is working on Error handling in uLisp. Wonderful! I have to check that out. That is absolutely necessary for my IOT client and HTTP code to make it robust and reliable. [Update: I merged the error-handling code and now you get informed about errors instead of a freeze. See "uLisp on M5Stack (ESP32): new version published".]


See also "temperature sensors via one wire", "Curl/Wget for uLisp", time via NTP, lispstring without escaping and more space, flash support, muting of the speaker and backlight control and uLisp on M5Stack (ESP32).

uLisp on M5Stack (ESP32):
temperature sensors via one wire

March 27, 2021, Lisp
Last edited on March 27, 2021

I added support for Dallas temperature sensors to ulisp-esp-m5stack. Activate #define enable_dallastemp in order to use it. It bases on the Arduino libraries OneWire.h DallasTemperature.h.

I used pin 16 to connect my sensors but you can change ONE_WIRE_BUS to use a different pin. As the OneWire library uses simple bit bagging and no hardware support, e. g. UART, any general-purpose input/output (GPIO) pin will work.

The interface consists of four uLisp functions: INIT-TEMP, GET-TEMP, SET-TEMP-RESOLUTION, and GET-TEMP-DEVICES-COUNT. Here is their documentation:

Function init-temp
 
Syntax:
   init-temp
     => result-list

Arguments and values:
   result-list---a list of device addresses; each address being a list of 8 integer values specifying a device address.

Description:
   Detects all supported temperature sensors connected via one wire bus to the pin ONE_WIRE_BUS and returns the list of the sensors' device addresses.

   All sensors are configured to use the resolution specified by default DEFAULT_TEMPERATURE_PRECISION via a broadcast. Note that a sensor might choose a different resolution if the desired resolution is not supported. See also: set-temp-resolution.

Function get-temp
 
Syntax:
   get-temp address
     => temperature

Arguments and values:
   address---a list of 8 integer values specifying a device address.

   temperature---an integer value; the measured temperature in Celsius.

Description:
   Requests the sensor specified by address to measure and compute a new temperature reading, retrieves the value from the sensor device and returns the temperature in Celsius.

Function set-temp-resolution
 
Syntax:
   set-temp-resolution address [resolution]
     => actual-resolution

Arguments and values:
   address---a list of 8 integer values specifying a device address.

   resolution---an integer value.

   actual-resolution---an integer value.

Description:
   Tries to configure the sensor specified by address to use the given resolution and returns the actual resolution that the devices is set to after the attempt.

   Note that a sensor might choose a different resolution if the desired resolution is not supported. In this case, the returned actual-resolution differs from the argument resolution.

If the argument resolution is missing, instead the default given by DEFAULT_TEMPERATURE_PRECISION is used.

Function get-temp-devices-count
 
Syntax:
   get-temp-devices-count
     => count

Arguments and values:
   count---an integer value; the number of detected supported temperature sensors.

Description:
   Returns the number of temperature sensors supported by this interface that were detected by the last call to INIT-TEMP. Note that this might not be the correct current count if sensors were removed or added since the last call to INIT-TEMP.

Findings from reading DallasTemperature.h and DallasTemperature.cpp

These are the notes I wrote down when reading the source code of the Dallas temperature sensor library and my conclusion how to best use it which lead to my implementation for uLisp.

1. The process of counting the number of devices is efficiently done in parallel by a binary tree algorithm.

2. The result of the search is the number of devices with their addresses.

3. The DallasTemperature library keeps only a count of devices and a count of supported temperature sensors (ds18Count) in memory, not an indexed list of addresses. This is done in DallasTemperature::begin() by doing a search but only the counts are kept, no addresses are stored. Sadly, it also does not return anything.

4. getAddress() does a search again to determine the address for an device index. So it is faster to just get a sensor reading by using the address not the index, it safes one search.

5. Sadly, there is not command to get a list of addresses in a row. So at least once you have to do getAddress() to actually get the addresses of all devices.

5. requestTemperature() can be applied to a single device only or to all devices in parallel. It is as fast to request a temperature from all devices as only one device.

6. Actually getting the temperature reading works only one at a time. getTemp*(deviceAddress) is faster than getTemp*ByIndex(index) as the latter has to do a search first (see 4.).

7. There are these temperature resolutions: 9, 10, 11, and 12 bits. The conversion (=reading) times are:
9 bit – 94 ms
10 bit – 188 ms
11 bit – 375 ms
12 bit – 750 ms

8. setResolution() can either set all devices in parallel or only set one device at a time (only by address, there is no setResultionByIndex()).

9. The temperatures are internally stored in 1/128 degree steps. This is the "raw" readings returned by DallasTemperature::getTemp() as int16_t.

DallasTemperature::getTempC returns "(float) raw * 0.0078125f" and
DallasTemperature::getTempF returns "((float) raw * 0.0140625f) + 32.0f".

In case of an error,
getTempC() will return DEVICE_DISCONNECTED_C which is "(float)-127",
getTempF() will return DEVICE_DISCONNECTED_F which is "(float)-196.6", and
getTemp() will return DEVICE_DISCONNECTED_RAW which is "(int16_t)-7040", respectively.

10. If you don't need the actual temperature but just to monitor that the temperature is in a defined range, it is not necessary to read the temperatures at all (which has to happen one sensor at a time). Instead, you can use the alarm signaling.

For that, you can set a high and a low alarm temperature per device and then you can do an alarm search to determine in parallel if there are sensors with alarms. The range can be half open, that is you can also only define high and low alarm temperatures.

DallasTemperature::alarmSearch() returns one device address with an alarm at a time. It is also possible to install an alarm handler and then call DallasTemperature::processAlarms() which will do repeated alarm searches and call the handler for each device with an alarm.

11. isConnected(deviceAddress) can be used to determine if a certain sensor is still available. It will return quickly when it is not but transfer a full sensor reading in case it is still available. The library currently does not support a case where parallel search is used to determine if known devices are still present.

12. The search is deterministic, it seems, so as long as you don't change sensors, the indices stay the same. If you add and remove a sensor, existing sensor might get new indices. So it seems actually not to be safe to use *ByIndex() functions.

13. getDeviceCount() gives you the number of all devices, getDS18Count() the number of all supported DS18 sensors. But no function gives you the list of indices or addresses of all supported DS19 sensors.

validFamily(deviceAddress) lets you check by address if a device is supported. Supported are DS18S20MODEL (also DS1820), DS18B20MODEL (also MAX31820), DS1822MODEL, DS1825MODEL, and DS28EA00MODEL.

getAddress() just checks if the address is valid (using validAddress(deviceAddress)) but not if the device is actually known. As getAddress() already calls validAddress() for you, there should be no need to ever call validAddress() from user code. If you just request a temperature from all devices till getDeviceCount() you'll also send requests to unsupported devices.

In conclusion, this seems to be the best approach to setup all devices:

  1. Call getDS18Count() once to determine that there are any supported temperature sensors at all.
  2. Iterate over all devices, that is, from index "0" up to "getDeviceCount() - 1".
  3. Call getAddress() for each index (this will also check validAddress())
  4. and then call validFamily() for the address.
  5. If validFamily() returns true, store the address for later temperature readings.
  6. This is also a good time to call setResolution() as per default each device is left at its individual default resolution if you have sensors of different kinds. Either call getResolution(newResolution) to set all devices in parallel, or setResolution(address, newResolution) in the loop right after each call to validFamily() to set up individual resolutions.

To read sensor values:

  1. Call requestTemperature() to request all sensors to do new readings in parallel,
  2. then iterate over the stored list of DS18 addresses and
  3. call getTempC(address), getTempF(address), or getTemp(address) for each address and
  4. check for error return values (see Finding 9.).

Note: getTempC() and getTempF() will call getTemp() internally and that one will also use isConnected(). So there should be no need to call isConnected() from user code if you check for the error return values of the functions (see Finding 8.)



This is the last thing I promised to release in my previous post of February 15, 2021. Documentation takes time! But I programmed new features last Thursday so stay tuned.


See also "Curl/Wget for uLisp", time via NTP, lispstring without escaping and more space, flash support, muting of the speaker and backlight control and uLisp on M5Stack (ESP32).

"Curl/Wget for uLisp"
Or: An HTTP(s) get/post/put function for uLisp

February 23, 2021, Lisp
Last edited on March 27, 2021

Oh, I forgot to continue posting… I just published a quite comprehensive HTTP function supporting put, post, get, auth, HTTP and HTTPS, and more for uLisp at ulisp-esp-m5stack.

Activate #define enable_http and #define enable_http_keywords to get it; the keywords used by the http function are to be enabled separately as they might be used more general and not just by this function.

Note that you need to connect to the internet first. Usually with WIFI-CONNECT.

Here is the full documentation with example calls:

Function http
 
Syntax:
   http url &key verbose
                 (https t)
                 auth
                 (user default_username)
                 (password default_password)
                 accept
                 content-type
                 (method :get)
                 data
     => result-string

Arguments and values:
   verbose---t, or nil (the default); affects also debug output of the argument decoding itself and should be put in first position in a call for full effect.

   https---t (the default), nil, or a certificate as string; uses default certificate in C string root_ca if true; url needs to fit: "http://..." for true and and "https://..." for false.

   auth---t, or nil (the default).

   user---a string, or nil (the default); uses default value in C string default_username if nil; only used if :auth t.

   password---a string, or nil (the default); uses default value in C string default_password if nil; only used if :auth t.

   accept---nil (the default), or a string.

   content-type---nil (the default), or a string.

   method---:get (the default), :put, or :post.

   data---nil (the default), or a string; only necessary in case of :method :put or :method :post; error for :method :get.

Examples:
   ;; HTTP GET:
   (http "http://192.168.179.41:2342" :https nil)
 
   ;; HTTP PUT:
   (http "http://192.168.179.41:2342"
         :https nil
         :accept "application/n-quads"
         :content-type "application/n-quads"
         :auth t :user "foo" :password "bar"
         :method :put
         :data (format nil "<http://example.com/button> <http://example.com/pressed> \"~a\" .~%"
                           (get-time)))

It can be tested with an minimal HTTP server simulation using bash and netcat:

while true; do echo -e "HTTP/1.1 200 OK\n\n $(date)" | nc -l -p 2342 -q 1; done
(To test with HTTPS in a similar fashion you can use openssl s_server, as explained, for example, in the article Create a simple HTTPS server with OPENSSL S_SERVER by Joris Visscher on July 22, 2015, but then you need to use certificates.)


See also Again more features for uLisp on M5Stack (ESP32):
time via NTP, lispstring without escaping and more space
, More features for uLisp on M5Stack (ESP32):
flash support, muting of the speaker and backlight control
and uLisp on M5Stack (ESP32).

Older entries...

Select a Theme:

Basilique du Sacré-Cœur de Montmartre (Paris) Parc Floral de Paris Castillo de Santa Barbara (Alicante) About the photos

Entries: