Skip to content

Instantly share code, notes, and snippets.

@mfikes
Last active July 6, 2022 22:32
Show Gist options
  • Save mfikes/5ed90e461229161ba9197461af888107 to your computer and use it in GitHub Desktop.
Save mfikes/5ed90e461229161ba9197461af888107 to your computer and use it in GitHub Desktop.
ClojureScript REPL into ESP32

To set up a REPL that has cljs.core available, we need to re-partition the ESP32 and allocate more memory in Espruino for "JsVars".

The default Espruino bootloader.bin, pre-built variants of partitions_esprinuo.bin and rebuilt espruino_esp32.bin, and the ClojureScript runtime core.bin are also available here.

bootloader.bin gets flashed to 0x1000, partitions_espruino.bin gets flashed to 0x8000, and espruino_esp32.bin gets flashed to 0x10000 as per a usual ESP32 Espruino setup, and we put the ClojureScript runtime at 0x2C0000:

Assuming port is /dev/cu.SLAB_USBtoUART and that you can communicate via USB at 2 Mbits/s, execute this commands:

esptool.py --port /dev/cu.SLAB_USBtoUART --baud 2000000 write_flash 0x1000 bootloader.bin 0x8000 partitions_espruino.bin 0x10000 espruino_esp32.bin 0x2C0000 core.bin

Below is additional description on how these images are built in case you want to experiment for yourself.

Details on Partitions and More JSVars

Here is more detail on how the above bin images were created.

First, re-partition so that we have sufficient space to put cljs.core into the "boot ROM". The default partition table is here and you can see that js_code has by default 256 KiB, which isn't enough to hold cljs.core.

To accomplish this, we will allocate 1 MiB for js_code by changing the storage partition so that instead of starting at 0x300000, it starts at 0x3C0000.

To make the new partitions image involves cloning EspruinoBuildTools, editing the esp32/build/app/partitions_espruino.csv file in it like so

-js_code,data,0,0x2C0000,256K
-storage,data,0,0x300000,1024K
+js_code,data,0,0x2C0000,1024K
+storage,data,0,0x3C0000,256K

and then running the build-partition.sh script in that repository.

To rebuild the Espruino engine, in the Espruino reporsitory, change boards/ESP32.py to specify the larger partition:

-    'pages' : 64,
+    'pages' : 256,

Additionally, when loading cljs.core we will need around 28,000 JsVars, so you can edit targets/esp32/main.c to allocate more:

-  if(heapVars > 20000) heapVars = 20000;  //WROVER boards have much more RAM, so we set a limit
+  if(heapVars > 55000) heapVars = 55000;  //WROVER boards have much more RAM, so we set a limit

cljs.core

This js_code bin has cljs.core in it:

It was produced from this JavaScript:

This cljs.core JavaScript is taken from a release build of Planck (so that it has :simple applied to it), and it has been subsequently edited to at least set up some of the needed goog and cljs.core objects near the top. It has also had the problematic break labels removed.

You'd flash core.bin to 0x2C0000, just as main.bin was done in this earlier gist.

REPL

Once you have flashed your ESP32 with the new partitions, engine and cljs.core binaries, connect to it in the Espruino Web IDE. Give it some time to connect as it is now loading cljs.core at boot (approximately 15 seconds).

You can confirm that the new binaries and core is in place (55000 JsVars available, and 27674 being used by cljs.core). Connect to the ESP32 using USB and a terminal emulator, such as screen:

screen /dev/cu.SLAB_USBtoUART 115200

Then you can interact:

>process.memory()
={ free: 27326, usage: 27674, total: 55000, history: 3,
  gc: 0, gctime: 366.053 }

Ensure that your ESP32 is set up to connect to your WiFi.

var ssid = 'YOUR_SSID';
var password = 'YOUR_SSID_PASSWORD';

var wifi = require('Wifi');
wifi.connect(ssid, {password: password}, function() {
    console.log('Connected to Wifi.  IP address is:', wifi.getIP().ip);
    wifi.save(); // Next reboot will auto-connect
});

Then evaluate the following JavaScript in the connected terminal to set it up so that it can accept REPL connections:

cljs.user = {};
var accum = "";
var evalme = false;

var server = require("net").createServer(function(c) {
  console.log("New REPL Connection");
  current_c = c;
  c.on("data", function(data) {
    //console.log(">" + data);
    if (data.startsWith("(function (){try{return cljs.core.pr_str")) {
      evalme = true;
    }
    if (data.endsWith("\0")) {
      if (evalme) {
        //console.log("evaluating: " + accum + data);
        try {
          c.write(
            JSON.stringify({ status: "success", value: eval(accum + data) })
          );
        } catch (e) {
          c.write(JSON.stringify({ status: "exception",
                                  value: ""+e,
                                 stacktrace: e.stack}));
        }
        accum = "";
        evalme = false;
        c.write("\0");
      } else if (data === ":cljs/quit") {
        c.end();
      } else {
        c.write(JSON.stringify({ status: "success", value: "true" }));
        accum = "";
        evalme = false;
        c.write("\0");
      }
    } else {
      accum = accum + data;
    }
  });
});

server.listen(53000);
print("Ready for REPL connections.")

Also, even though cljs.core will now be available in its entirety via core.bin loaded from the js_code partition, I haven't dealt with cljs.core's dependencies on Google Closure Libraries, but if you then evaluate the following JavaScript in the Espruino Web IDE left panel, you will have enough functionality to use a REPL:

goog.string.StringBuffer = function (a,c){null!=a&&this.append.apply(this,arguments)};
goog.string.StringBuffer.prototype.append = function (a,c,d){this.buffer_+=String(a);if(null!=c)for(var b=1;b<arguments.length;b++)this.buffer_+=arguments[b];return this};
goog.string.StringBuffer.prototype.toString = function (){return this.buffer_};
goog.typeOf = function (a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";
else if("function"==b&&"undefined"==typeof a.call)return"object";return b};
goog.isString = function (a){return"string"==typeof a};
goog.isFunction = function (a){return"function"==goog.typeOf(a)};
goog.string.StringBuffer.prototype.buffer_ = '';

Then to connect with Ambly, first, on macOS advertise the IP address of the ESP32 WROVER (replace 10.0.1.55 with the IP address your WROVER has) by running this (and leaving it running---later this will be built directly into the ESP32 WROVER code so that it advertises itself):

dns-sd -P "Ambly ESP32 WROVER" _http._tcp local 53001 ambly.local 10.0.1.18

Then run some modified Ambly code from its ESP32 branch via

clj -Sdeps '{:deps {org.clojure/clojurescript {:mvn/version "1.10.520"} ambly {:git/url "https://github.com/mfikes/ambly" :sha "7e84e590aeb66db09dd76e55ef701bef97bc3145"}}}' -m cljs.main -re ambly

This will start up the discovery process (seeing the IP and port you've advertised above via dns-sd), connect you to your ESP32 and initiate a REPL session:

[1] ESP32 WROVER

[R] Refresh

Choice: 1

Connecting to ESP32 WROVER ...

ClojureScript 1.10.520
cljs.user=> 3
3
cljs.user=>
@qjarvisholland
Copy link

This looks like fun thanks for the excellent documentation Mike

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment