mgr's weblog

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

April 2, 2021, Lisp
Last edited on April 3, 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).


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)
(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*)
(defun update-prompt (input)
  (pop *screen-lines*)
  (push (input->line input *prompt*) *screen-lines*)
(defun kbd-repl (&optional (welcome t))
  (when welcome
    (to-screen "This is ulisp-esp-m5stack!"))
  (let (input)
       (when (and (button-pressed-p *button-1*)
                  (button-pressed-p *button-3*))
         (to-screen 'exit-repl)
       (let ((key (read-kbd)))
         (when key
           (print key)
           (case key
              (if input
                    (princ 'rubout)
                    (pop input)
                    (update-prompt input))
                  (princ 'no-input)))
             (167 #| alt-c |#
              (princ 'abort)
              (setf input nil))
              (to-screen (eval (read-from-string (input->line input))))
              (setf input nil))
              (if (and key
                       (characterp key)
                       (<= 32 (char-code key) 126))
                    (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)

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.

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

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

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

   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
     => count

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

   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
   http url &key verbose
                 (https t)
                 (user default_username)
                 (password default_password)
                 (method :get)
     => 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.

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

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

Again more features for uLisp on M5Stack (ESP32):
time via NTP, lispstring without escaping and more space

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

I just pushed three small things to ulisp-esp-m5stack: Get time via NTP, add optional escape parameter to function lispstring, increased WORKSPACESIZE and SYMBOLTABLESIZE for M5Stack.

Getting time via NTP

Enable #define enable_ntptime to get time via NTP. New functions INIT-NTP and GET-TIME. Note that you need to connect to the internet first. Usually with WIFI-CONNECT.

Function init-ntp
     => nil

   Initializes and configures NTP.

Function get-time
     => timestamp

Arguments and values:
   timestamp---a string; containing a timestamp in the format of xsd:dateTime.

   Returns a timestamp in the format of xsd:dateTime.

Add optional escape parameter to function lispstring

I have changed the function lispstring to have an optional escape parameter to switch off the default behavior of handling the backslash escape character. The default behavior is not changed.

The C function lispstring takes a C char* string and return a uLisp string object. When parsing data in the n-triples format retrieved via HTTP I noticed that the data got modified already by lispstring which broke my parser implemented in uLisp.

As lispstring might be used in other contexts that expect this behavior, I just added the option to switch the un-escaping off.


The M5Stack ESP32 has 320 kB of usable DRAM in total. Although with a lot of restrictions (see next section),

I increased WORKSPACESIZE to 9000 cells, which equals 72,000 bytes, and SYMBOLTABLESIZE to 2048 bytes. These sizes seem to work still safely even with bigger applications and a lot of consing.

Warning: You cannot load images created with different settings!

The SRAM of the M5Stack ESP32

In total the M5Stack ESP32 comes with 520 kB of SRAM. The catch is that the ESP32 is based on the Harvard architecture and 192 kB is in the SRAM0 block intended(!) for instructions (IRAM). There is another 128 kB block in block SRAM1 which can be used either for instructions or data (DRAM). The third block SRAM2 has got a size of 200 kB and is for data only. But 8 kB of SRAM2 is lost for ROM mappings.

The ESP-IDF and thus also the Arduino environment use only SRAM0 for instructions and SRAM1 and SRAM2 for data, which is fine for uLisp as it is an interpreter and therefore more RAM for data is perfect. SRAM0 will just hold the machine code of the uLisp implementation but no code written in the language uLisp.

Of the remaining 320 kB another 54 kB will be dedicated for Bluetooth if Bluetooth is enabled in ESP-IDF (which it is by default, #define CONFIG_BT_RESERVE_DRAM 0xdb5c) in the SRAM2 block. And if trace memory is enabled, another 32 kB of SRAM1 are reserved (by default it is disabled, #define CONFIG_TRACEMEM_RESERVE_DRAM 0x0).

So, by default with Bluetooth enabled and trace memory disabled, 266 kB are left. At the bottom of SRAM2 right after the 62 kB used for Bluetooth and ROM are the application's data and BSS segments. Sadly, at around the border between SRAM1 and SRAM2 there seem to be two small reserved regions again of a bit more the 1 kB each, limiting statically allocated memory.

Thus, the "shared data RAM" segment dram0_0_seg in the linker script memory layout is configured to have a default length of 0x2c200 - CONFIG_BT_RESERVE_DRAM. That is, 176.5 kB (= 180,736 bytes) without Bluetooth and 121.66 kB (= 124,580 bytes) with Bluetooth enabled.

But actually I have already written more than I have intended for this blog post and the rest of my notes, calculations and experiments will have to wait for a future article. For now, I just increased the size of the statically allocated uLisp workspace to make more use of the available memory of the ESP32 in the M5Stack.

See also More features for uLisp on M5Stack (ESP32):
flash support, muting of the speaker and backlight control
and uLisp on M5Stack (ESP32).


Espressif Systems, ESP32 Technical Reference Manual, Shanghai, 2020, section 2.3.2 Embedded Memory.

More features for uLisp on M5Stack (ESP32):
flash support, muting of the speaker and backlight control

February 15, 2021, Lisp
Last edited on February 23, 2021

I finished the IoT sensor device prototype and shipped it last Thursday. It just has a stub bootstrap system in the flash via uLisp's Lisp Library and downloads the actual application in a second boot phase via HTTPS. More on that later.

To make it happen I've added a bunch of things to ulisp-esp-m5stack: flash support, fixes for some quirks of the M5Stack, time via NTP, an HTTP function supporting methods PUT, POST, GET, Auth, HTTP and HTTP, temperature sensors via one wire, and more. I plan to publish all these features in the next days.

Today you get: flash support, muting of the builtin speaker and control of the LED backlight of the builtin display.

Flash support

My M5Stacks all have got 4 MB of flash memory attached via SPI. Newer version seem to come with 16 MB. The original ESP32 version of uLisp already uses flash to store workspace images with SAVE-IMAGE but it uses Arduino's EEPROM emulation that is limited to only use 4 kB (of the whole 4 MB). Now you can increase your workspace size and still store images without having to insert an SD card. If you want it, just activate the line #define flashsupport.

To add this was simple as the interface to use SD cards is the same as for flash. Most of the code to support flash is shared between both. The flash memory will be formatted with SPIFFS (SPI flash file system). When I decode the binary partition table that the Arduino software creates when compiling uLisp with the command:

python ulisp-esp.ino.partitions.bin ulisp-esp.ino.partitions.csv
I get:

# Espressif ESP32 Partition Table
# Name, Type, SubType, Offset, Size, Flags
So there is 1,472 kb free in the SPIFFS which is certainly enough for some workspace images. And maybe some other files. If you need more, you can still use an SD card.

Muting of the builtin speaker

The M5Stack / M5Core Basic comes with a little 1W speaker connected to analog PIN 25. If you don't configure the PIN and set it to zero, the speaker gives a little chirping sound when you send data to other devices and it clicks when you blank the display. This can be quite annoying.

The function MUTE-SPEAKER does this little configuration task and the nuisance stops. There is also the define flag #define m5stack_boot_mute which mutes the speaker on startup.

Brightness of the backlight

The builtin screen of the M5Stack / M5Core Basic has an LED backlight connected to pin 32 and the intensity can be controlled via PWM channel 7. You can change the birghtness with the new function SETUP-BACKLIGHT-PWM:

Syntax: setup-backlight-pwm [brightness] => nil

Brightness between 0-255: 0 is off, 255 brightest, 80 (the default) offers a good compromise: good readability for inside rooms. Set to small value to conserve power.

M5Display::begin already does such a setup with a default brightness value of 80.

See also uLisp on M5Stack (ESP32).

StumpWM: vsplit-three

February 7, 2021, Lisp
Last edited on February 7, 2021

A good ten month ago I switched away from a full desktop environment being finally tired enough that user software gets more and ever more features and tries to anticipate more and more what I might want but in the end my own computer never actually does what I want and only that. PulseAudio being the most dreaded example of a piece of code that gets more, more and more magic and complexity and in the end it never does what you actually want while at the same time telling it to do so got completely impossible because of many layers of abstractions and magic. PulseAudio has a lot of "rules", "profiles", "device intended roles", "autodetecting", "automatic setup and routing" and "other housekeeping actions". Look at this article PulseAudio under the hood by Victor Gaydov (which is also the source of the terms I just quoted): It has 174 occurrences of words starting with "auto-": automatically – 106, automatic – 27, autoload – 16, autospawn – 14, autodetect – 4, autoexit – 2, automate – 2, auto timing – 1, auto switch – 1, and once "magically" when it is even too much for the author.

So, more control and less clutter instead. After years again I use just a good old window manager, individual programs, and got rid of PulseAudio.

I switched to StumpWM which is written in Common Lisp. It is easy to modify and try stuff. While it's running. I have it run Slime so that I can connect to it from Emacs and hack stuff that is missing. From time to time I got StumpWM hanging while hacking, so I added a signal handler for POSIX signal SIGHUP to force a hard stumpwm restart. (There is a new version of that signal handler without the CFFI dependency but that pull request is not merged yet.) When I did something stupid I switch to a console, fire a killall -HUP stumpwm to have it reset hard. Since then I haven't lost a X11 session even while changing quite a bit.

Right away I wrote some minimalistic modules to control audio volume (stump-volume-control) and to play internet radio stations (stump-radio). They put the multimedia keys or other extra keys on most keyboards to use. Both modules have also been added to stumpwm-contrib.

Note that StumpWM is a tiling window manager and keyboard driven. So no windows with decorations, just frames. But if you really want a window, you can call FLOAT-THIS and the current frame turns into a window. Or you can make a floating group (aka virtual desktop) with GNEW-FLOAT and all windows in that group are actual windows.

Recently I got a new wide 34″ screen with a resolution of 3440x1440 pixels. I noticed that just splitting the screen space vertically into two frames makes the text appear either too far left or too much on the right, and also both are too big. There is VSPLIT-EQUALLY but with the screen equally divided into three columns, the main program in the middle is too narrow.

I want the whole thing more adapted to my taste. To do so I just wrote this little function and command:

(defun vsplit-three (&key (ratio .3) (group (current-group)) (dir :column))
  (let ((split-1 (- 1 ratio))
         (split-2 ( ratio (/ 1 split-1))))
    (stumpwm::split-frame-in-dir group dir split-1)
    (stumpwm::split-frame-in-dir group dir split-2)
(defcommand (vsplit-three stumpwm::tile-group) (&optional (ratio ".3"))
  (vsplit-three :ratio (read-from-string ratio)))
(define-key root-map (kbd "O") "vsplit-three")

When called with a default ratio of 3/10 it makes two small frames left and right that take 30 % of the total screen real estate and a middle frame that gets the remaining 40 %. This seems to be good for generic use. A value of 0.27 seems to be better for coding with a slightly bigger main window. And for the web browsing group, I rather have it 25 % – 50 % – 25 % right now.

uLisp on M5Stack (ESP32)

January 28, 2021, Lisp
Last edited on February 16, 2021

About two years I ago I bought a couple of M5Stack ESP32 computers on a Maker Fair. I made little use of them so far but now I started to put uLisp on them and suddenly it is much more fun.

“Hello uLisp!” on the M5Stack; click for a larger version (323 kB).

Specifically, I got a M5Stack ESP32 Basic Core (now sold as an "IoT Development KIT") and two M5Stack ESP32 Faces Kits. Basically, they are full-blown computers on a better level than you've had as a PC in the mid-90ties:

  • 240 MHz dual-core ESP32 processor,
  • 520 KB SRAM,
  • 16 MB Flash – my datasheet actually says: 4 MB Flash,
  • 2 inch TFT LCD display with 320x240 pixels and 262 K colors (ILITEK ILI9341),
  • WiFi with built-in antenna,
  • Bluetooth,
  • USB-C
  • SD card slot
  • 110 mAh battery (M5Stack Basic Core) – my datasheet says: 150 mAh,
    600 mAh battery (M5Stack Faces Kit)
  • 1 W Speaker
  • microphone
  • 3 buttons

As a first test I have made use of the buttons, the built-in TFT display, the Wi-Fi support, Network Time Protocol (NTP), HTTP and HTTPS requests. Actually, this was a small IoT demo to have it store ISO 8601 timestamps on button presses in the graph database Dydra via the SPARQL 1.1 Graph Store HTTP Protocol.

It was quite easy to do as some ESP32 boards are already supported by the ESP version of uLisp, ulisp-esp, which can just be uploaded to the M5Stack using the Arduino software. But this version doesn't support any of the built-in hardware of the M5Stack systems. Luckily, uLisp is easy to enhance.

I forked ulisp-esp and published my first adaptations for the M5Stack on GitHub as ulisp-esp-m5stack. As a start, the version I pushed today has the full graphics extension working on the M5Stack.

Also, you have to live with the fact that I have included a Makefile and some helper scripts to compile and test the distribution of uLisp without having to use the GUI of the Arduino software. (Of course, the Arduino software is still used for the real work, so you have to have it and the M5Stack extensions installed and the command "arduino" needs to point to the right location to execute it.)

In a follow-up project I have already used a DS18B20 digital temperature sensor as well as an analog pH sensor and wrote a little library in Lisp to derive pH values from the measured analog voltage. But that is stuff for a later post… As always I am happy for comments via email!

Full disclosure note

I work for the company behind Dydra. This influenced my choice of the database. Still, this is my personal blog and I write in my own time.

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