Vor einiger Zeit haben ich den HuCon-Roboter-Bausatz erhalten. Der HuCon wird mit Blockly und Python programmiert. Ich programmiere schon lange auf PCs (vor allem Java und Clojure), aber mit Microcontrollern habe ich nur wenig Erfahrung. Als erstes habe ich mich mit der Installation von OpenWrt auf dem Onion Omega2+ beschäftigt.
Nun möchte ich gerne Clojure für die Programmierung verwenden. Clojure ist (meiner Meinung nach) eine tolle Programmiersprache, die auf der JVM läuft. Es gibt zwar eine JVM für OpenWrt für den Omega2+/mipsel_24kc und somit könnte man vielleicht auch Clojure/JVM auf dem Micro betreiben. Aber ich wollte ausprobieren, ob es möglich ist, mit weniger Ressourcen (RAM, CPU) auszukommen. Also habe ich mich mit ClojureScript beschäftigt. ClojureScript kann via Node.js auf vielen Microcontrollern ausgeführt werden.
In diesem Artikel beschreibe ich meine Erfahrungen und zeige, wie man einen Microcontroller interaktiv mit Clojure(Script) programmieren kann. Ich finde, dass Clojure sich gut als Lern- und Lehrsprache eignet (ich bin aber keine Pädagoge; das ist nur meine laienhafte persönliche Meinung), es macht extrem viel Spaß, Clojure zu programmieren und gerade die interaktive und explorative Art, wie man Clojure-Programme schreibt, passt super zur Programmierung von Microcontrollern. Man braucht keine langen Think-Code-Run-Zyklen, man kann seinen Code ständig anpassen und ausprobieren. Die Feedback-Schleife ist extrem kurz und unmittelbar.
Noch eine Bemerkung: das Schreiben dieses Artikels hat sich wie Yak Shaving angefühlt. Neben dem eigentlichen Ziel, ClojureScript auf einem Micro-Controller auszuführen, musste ich eine ganze Reihe weiterer Probleme lösen bzw. Informationen einholen: für den Emacs den Markdown-Mode installieren, mich mit Github-Gist-Checkouts beschäftigen, damit ich diesen Text überhaupt als Gist veröffentlichen kann, auf dem Micro den Swap aktivieren und extroot installieren, damit ich genug Platz auf dem Micro habe, mich mit den Firewall-Regeln meiner Fritzbox und meines Windows-PC auseinandersetzen, Man-Pages & Co. zu rsync, sshfs, samba lesen, auf dem Micro die opkg-Konfiguration anpassen, weil ich plötzlich keine Pakete mehr über
https:
Quellen installieren konnte, weil ich keine passenden CA-Zertifikate mehr hatte etc. etc. Das macht IT zum einen natürlich interessant -- und es macht auch Spaß! -- , aber es führt auch dazu, dass scheinbar leichte Dinge 5-mal länger dauern, als ursprünglich erwartet. Hoffentlich helfen dir die Informationen, die ich hier zusammengetragen habe.
In diesem Artikel beschreibe ich, wie man ClojureScript-Programme via Node.js auf einem Omega2+ laufen lassen kann. Hier findest du eine Einführung zu Omega2+ und Node.js.
ClojureScript ist ein Kompiler, der Clojure-Programme in JavaScript übersetzt. JavaScript-Programme können auf vielen Microcontrollern via Node.js ausgeführt werden.
Der Onion Omega2+ ist ein Microcontroller, auf dem das Betriebssystem OpenWrt läuft. Über den OpenWrt-Paketmanager opkg kann Node.js installiert werden.
Natürlich brauchst du einen Microcontroller, wenn du deine Programme wirklich auf dem Micro ausführen möchtest. Du kannst aber fast alle Sachen, die ich ihr beschreibe, auch ohne Micro durchführen. So kannst du dich mit den Tools, Clojure, ClojureScript usw. schon beschäftigen, bevor du dann später vielleicht wirklich dein Programm auf dem Micro ausführst.
-
Microcontroller: du brauchst einen SSH/Shell-Zugang zu deinem Micro. Am besten du verbindest ihn via WLAN mit deinem Hausnetz. Hier findest du eine Anleitung von mir, die dir vielleicht hilft.
-
Docker Desktop for Windows: Mit Docker kannst du auf viele vorgefertigte Programme zugreifen, ohne diese lokal auf einem PC installieren zu müssen. Ich werde in den folgenden Beispielen Docker verwenden. Du kannst die Programme aber auch einzeln lokal installieren, wenn du Docker nicht installieren möchtest.
-
Node.js: du benötigst Node.js sowohl auf deinem PC (z.B. um dort den ClojureScript-Code zu kompilieren) als auch auf dem Mirco (um den JavaScript-Code auszuführen).
-
npm: npm benutzt du, um vorgefertigte JavaScript-Pakete und deren Abhängigkeiten zu installieren. Wir brauchen es nur auf dem PC (nicht auf dem Micro).
-
JDK: der ClojureScript-Compiler ist in Clojure geschrieben und läuft auf der JVM. Daher benötigst du ein JDK (vielleicht reicht auch die JRE, das habe ich bisher nicht ausprobiert).
Ich nutze
shadow-cljs
in
dem Docker-Container
theasp/clojurescript-nodejs:shadow-cljs-alpine
,
um unsere erste Anwendung zu bauen.
Du kannst die Tools auch einzeln auf deinem Windows/Linux PC installieren. Du musst nicht Docker verwenden.
Ich habe mir das (leere) Verzeichnis C:\henrik42\
als
Basis-Verzeichnis für die folgenden Aktivitäten angelegt. Du solltest
dir auch ein leeres Verzeichnis anlegen.
Damit du die Projekt-Dateien mit deinem Editor von Windows aus
bearbeiten kannst, binden wir das Windows-Verzeichnis C:\henrik42\
in dem Docker-Container an /home/node
.
Du wirst das Verzeichnis in Docker Desktop sharen müssen, damit du es an den Container binden kannst.
C:\henrik42>docker run --rm -it --name foobar --user node -v C:\henrik42\:/home/node -w /home/node theasp/clojurescript-nodejs:shadow-cljs-alpine bash
Ich lege explizit ein Verzeichnis projekte/
für die folgenden
Aktivitäten an, damit ich nachvollziehen kann, welche Dateien
anschließend genau wo durch die Tools erstellt werden. Gerade im
Zusammenhang mit Node/npm durchschaue ich noch nicht, was die Tools
ganz genau treiben.
bash-5.0$ mkdir -p projekte && cd projekte
Als erstes legen wir via npx create-cljs-project hello-world-app
das
Projekt hello-world-app
an:
bash-5.0$ npx create-cljs-project hello-world-app
npx: installed 1 in 1.534s
shadow-cljs - creating project: /home/node/projekte/hello-world-app
Creating: /home/node/projekte/hello-world-app/package.json
Creating: /home/node/projekte/hello-world-app/shadow-cljs.edn
Creating: /home/node/projekte/hello-world-app/.gitignore
Creating: /home/node/projekte/hello-world-app/src/main
Creating: /home/node/projekte/hello-world-app/src/test
Installing shadow-cljs in project.
npm notice created a lockfile as package-lock.json. You should commit this file.
+ shadow-cljs@2.11.10
added 99 packages from 106 contributors and audited 99 packages in 20.176s
found 0 vulnerabilities
Done. Actual project initialization will follow soon.
Der Aufruf legt neben dem Projektverzeichnis
/home/node/projekte/hello-world-app
auch die Verzeichnisse
/home/node/.config
und /home/node/.npm
an. Falls du direkt unter
Windows auf deinem PC arbeitest (und nicht in dem Docker-Container),
werden diese Verzeichnisse wahrscheinlich unter
APPDATA=C:\Users\<deine-user-id>\AppData\Roaming
angelegt.
In dem Projekt-Verzeichnis findest du unter
/home/node/projekte/hello-world-app/node_modules
schon eine Reihe
von npm-Paketen.
Falls du den Docker-Container verwendest, findest du diese Dateien und
Verzeichnisse auch unter c:\henrik42\projekte\hello-world-app\
:
c:\>dir c:\henrik42\projekte\hello-world-app
Datenträger in Laufwerk C: ist OSDisk
Volumeseriennummer: 006E-D511
Verzeichnis von c:\henrik42\projekte\hello-world-app
25.12.2020 12:39 <DIR> .
25.12.2020 12:39 <DIR> ..
25.12.2020 12:38 154 .gitignore
25.12.2020 12:39 <DIR> node_modules
25.12.2020 12:39 33.446 package-lock.json
25.12.2020 12:39 152 package.json
25.12.2020 12:38 118 shadow-cljs.edn
25.12.2020 12:38 <DIR> src
4 Datei(en), 33.870 Bytes
Nun schreiben wir das erste Programm nach
c:/henrik42/projekte/hello-world-app/src/main/hello_world/core.cljs
:
Verzeichnisse, die zu Clojure Namespaces gehören, müssen anstatt des
-
(Minus) einen_
(Unterstrich) verwenden. Daher wird für den Namespacehello-world
das Verzeichnishello_world
angelegt.
(ns hello-world.core)
(defn greatings []
(println "Hello, world!"))
Als nächstes musst du festlegen, was mit dem Code passieren soll. Dazu
konfigurierst du in
C:\henrik42\projekte\hello-world-app\shadow-cljs.edn
den Build
:my-app
:
Wir verwenden hier
node-script
als Build-Ziel. Der ClojureScript-Compiler kann auch JavaScript erzeugen, das im Browser ausgeführt werden kann. Mit ClojureScript kann man eben auch Single-Page-Webanwendungen bauen (z.B. mit React bzw. Reagent).
{:source-paths
["src/dev"
"src/main"
"src/test"]
:dependencies
[]
:builds
{:my-app {:target :node-script
:main hello-world.core/greatings
:output-to "hello-world.js"}}}
Nun kannst du das Clojure(Script)-Programm in JavaScript übersetzen. Bedenke, dass der Compiler, der ClojureScript nach JavaScript übersetzt, in Clojure/Java geschrieben ist und auf der JVM läuft.
bash-5.0$ cd /home/node/projekte/hello-world-app/
bash-5.0$ shadow-cljs compile my-app
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - updating dependencies
Retrieving thheller/shadow-cljs/2.8.76/shadow-cljs-2.8.76.pom from https://clojars.org/repo/
Retrieving org/clojure/clojure/1.10.1/clojure-1.10.1.pom from https://repo1.maven.org/maven2/
[...]
Retrieving thheller/shadow-cljsjs/0.0.21/shadow-cljsjs-0.0.21.jar from https://clojars.org/repo/
Retrieving edn-query-language/eql/0.0.3/eql-0.0.3.jar from https://clojars.org/repo/
shadow-cljs - dependencies updated
[:my-app] Compiling ...
[:my-app] Build completed. (41 files, 40 compiled, 0 warnings, 50.16s)
Bei diesem Build passieren eine ganze Menge Dinge:
- die Bibliotheken, die der Compiler benötigt (Maven-Artefakte),
werden nach
/home/node/.m2/repository
bzw.c:\henrik42\.m2\repository\
geladen und dort gecacht (lokales Maven-Repository). Dieser Download kann eine Zeit lang dauern. Wenn du den Compiler in Zukunft nochmal ausführst, erfolgt der Download nicht erneut, solange die JAR-Dateien noch im lokalen Maven-Repo vorhanden sind.
Wenn du mit Docker-Containern arbeitest, solltest du darauf achten, dass du diese Caches über Bind-Mounts immer an dasselbe Windows-Verzeichnis bindest. Ansonsten würden die Downloads immer wieder erfolgen und das kostet dann unnötig Zeit.
- der Compiler speichert sich Informationen über den Build in
c:\henrik42\projekte\hello-world-app\.shadow-cljs\
. Dadurch benötigen anschließend weitere Builds viel weniger Zeit. In dem Beispiel oben hat der Compiler 50,16 Sekunden gebraucht. Wenn ich ihn nun erneut ausführe, benötigt er nur 3,1 Sekunden.
bash-5.0$ shadow-cljs compile my-app
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
[:my-app] Compiling ...
[:my-app] Build completed. (41 files, 0 compiled, 0 warnings, 3.10s)
- das Ergebnis des Builds wird nach
C:\henrik42\projekte\hello-world-app\hello-world.js
geschrieben.
Nun können wir das Programm ausführen.
bash-5.0$ node hello-world.js
Hello, world!
Mit diesem Test prüfe ich nur, ob der Build im Prinzip funktioniert. Schließlich wollen wir unser Programm ja nicht auf unserem PC sondern auf dem Microcontroller ausführen. Und falls unser Programm versuchen würde, etwas zu tun, was nur auf dem Micro geht (z.B. irgendwelche GPIOs zu setzen), dann könnten wir das Programm auch nicht auf dem PC ausprobieren.
Zwischenstand
OK, was haben wir bisher geschafft?
Wir haben den ClojureScript-Compiler und npm
am Laufen
(inkl. Download der Clojure(Script) Dependencies/Bibliotheken) und
können das Kompilat (also die JavaScript-Datei) mit node
auf dem PC
ausführen.
Der Test sieht erstmal gut aus. Aber wenn wir mal in
C:\henrik42\projekte\hello-world-app\hello-world.js
schauen,
erkennen wir, dass das Programm scheinbar weiteren Code nachlädt:
#!/usr/bin/env node
(function(){
var shadow$provide = {};
var SHADOW_IMPORT_PATH = __dirname + '/.shadow-cljs/builds/my-app/dev/out/cljs-runtime';
if (__dirname == '.') { SHADOW_IMPORT_PATH = "/home/node/projekte/hello-world-app/.shadow-cljs/builds/my-app/dev/out/cljs-runtime"; }
global.$CLJS = global;
global.shadow$provide = {};
try {require('source-map-support').install();}
catch (e) {console.warn('no "source-map-support" (run "npm install source-map-support --save-dev" to get it)');}
[...]
Ich benutze folgenden Aufruf, um zu ermitteln, was wir wirklich
benötigen, um hello-world.js
auszuführen. Über -v C:\henrik42\projekte\hello-world-app\hello-world.js:/the-app/hello-world.js
binden wir ausschließlich diese eine Datei in den Container nach
/the-app/hello-world.js
und führen sie aus.
C:\>docker run --rm -it --user node -v C:\henrik42\projekte\hello-world-app\hello-world.js:/the-app/hello-world.js -w /the-app/ theasp/clojurescript-nodejs:shadow-cljs-alpine node hello-world.js
no "source-map-support" (run "npm install source-map-support --save-dev" to get it)
internal/fs/utils.js:220
throw err;
^
Error: ENOENT: no such file or directory, open '/the-app/.shadow-cljs/builds/my-app/dev/out/cljs-runtime/goog.debug.error.js'
at Object.openSync (fs.js:440:3)
at Object.readFileSync (fs.js:342:35)
at global.SHADOW_IMPORT (/the-app/hello-world.js:52:15)
at /the-app/hello-world.js:2206:1
at Object.<anonymous> (/the-app/hello-world.js:2247:3)
at Module._compile (internal/modules/cjs/loader.js:1121:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1160:10)
at Module.load (internal/modules/cjs/loader.js:976:32)
at Function.Module._load (internal/modules/cjs/loader.js:884:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:67:12) {
errno: -2,
syscall: 'open',
code: 'ENOENT',
path: '/the-app/.shadow-cljs/builds/my-app/dev/out/cljs-runtime/goog.debug.error.js'
}
Die Fehlermeldung legt nahe, dass versucht wird, die Datei
/the-app/.shadow-cljs/builds/my-app/dev/out/cljs-runtime/goog.debug.error.js
zu laden. Scheinbar benötigen wir also auch Teile aus
c:\henrik42\projekte\hello-world-app\.shadow-cljs\
für die
Ausführung und binden dies nun via -v c:\henrik42\projekte\hello-world-app\.shadow-cljs\:/the-app/.shadow-cljs
in den Container:
C:\>docker run --rm -it --user node -v C:\henrik42\projekte\hello-world-app\hello-world.js:/the-app/hello-world.js -v c:\henrik42\projekte\hello-world-app\.shadow-cljs\:/the-app/.shadow-cljs -w /the-app/ theasp/clojurescript-nodejs:shadow-cljs-alpine node hello-world.js
no "source-map-support" (run "npm install source-map-support --save-dev" to get it)
Hello, world!
Schon besser. Aber scheinbar wird das npm Module
source-map-support
vermisst. Dies finden wir unter
c:\henrik42\projekte\hello-world-app\node_modules\source-map-support\
. Wir
wissen nicht, ob und welche weiteren node_modules
unser Programm
benötigt, also stellen wir sie alle via -v c:\henrik42\projekte\hello-world-app\node_modules\:/the-app/node_modules
zur Verfügung:
C:\>docker run --rm -it --user node -v C:\henrik42\projekte\hello-world-app\hello-world.js:/the-app/hello-world.js -v c:\henrik42\projekte\hello-world-app\.shadow-cljs\:/the-app/.shadow-cljs -v c:\henrik42\projekte\hello-world-app\node_modules\:/the-app/node_modules -w /the-app/ theasp/clojurescript-nodejs:shadow-cljs-alpine node hello-world.js
Hello, world!
OK, das sieht ganz gut aus. Das Thema Module-Abhängigkeiten-zur-Laufzeit müssen wir uns später nochmal anschauen.
EDIT 2021-01-04: nach ein paar Hinweisen auf Slack bin ich auf eine weitere Lösung gekommen. Die findest du am Ende des Artikels.
Wir wissen nun, welche Dateien wir auf den Micro übertragen müssen. Auf dem Micro benötigen wir 55,7 MB Plattenplatz.
bash-5.0$ du -sch hello-world.js .shadow-cljs/ node_modules/
72.0K hello-world.js
34.0M .shadow-cljs/
21.6M node_modules/
55.7M total
Ich verpacke und komprimiere die Dateien via
tar
und übertrage
die Dateien via scp
.
Zuvor habe ich mir einen Non-Root-User
henrik42
auf dem Omega2+ angelegt. Auf dem PC bauchen wirtar
undscp
und auf dem Micro brauchen wirtar
und Dropbear oder OpenSSH.
C:\>tar -C C:/henrik42/projekte/hello-world-app -zcf - hello-world*.js node_modules .shadow-cljs | ssh henrik42@Omega-DC43 "tar -C /home/henrik42 -vzxf -"
Nun auf den Micro. Die Ausführungszeit von etwa 17 Sekunden ist für ein Hello-World erst einmal ernüchternd.
henrik42@Omega-DC43:~$ ls -Al
drwxr-xr-x 4 henrik42 henrik42 4096 Dec 26 11:20 .shadow-cljs
-rw-r--r-- 1 henrik42 henrik42 73584 Dec 26 11:24 hello-world.js
drwxr-xr-x 84 henrik42 henrik42 4096 Dec 26 11:10 node_modules
henrik42@Omega-DC43:~$ time node hello-world.js
Hello, world!
real 0m17.252s
user 0m15.756s
sys 0m1.046s
Auf dem PC (im Docker Container) sehen die Zeiten (etwa 0,3 Sekunden) besser aus.
bash-5.0$ time node hello-world.js
Hello, world!
real 0m0.259s
user 0m0.156s
sys 0m0.025s
Zwischenstand
OK, was haben wir bisher geschafft?
Wir können auf dem PC ClojureScript nach JavaScript kompilieren, das Kompilat auf den Micro-Controller kopieren und dort via Node.js ausführen.
Eigentlich sind wir jetzt schon fertig ;-)
shadow-cljs
bietet die Möglichkeit, den Build so durchzuführen, dass
alle Abhängigkeiten direkt in das Build-Target eingebaut werden,
anstatt auf externe Dateien zu verweisen. Vielleicht hat das Einfluss
auf die Ausführungsgeschwindigkeit.
Dies ist mein zweiter Build. Der erste hat deutlich länger als 11,63 Sekunden gedautert.
bash-5.0$ shadow-cljs release my-app
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
[:my-app] Compiling ...
[:my-app] Build completed. (41 files, 0 compiled, 0 warnings, 11.63s)
Wir wiederholen den Test und binden nur hello-world.js
in den
Container:
C:\>docker run --rm -it --user node -v C:\henrik42\projekte\hello-world-app\hello-world.js:/the-app/hello-world.js -w /the-app/ theasp/clojurescript-nodejs:shadow-cljs-alpine node hello-world.js
Hello, world!
Diesmal brauchen wir also weder
c:\henrik42\projekte\hello-world-app\.shadow-cljs\
noch
c:\henrik42\projekte\hello-world-app\node_modules
für die
Ausführung.
Diesen Stand kopiere ich nun nochmal auf den Micro:
C:\henrik42\projekte\hello-world-app>scp hello-world.js henrik42@Omega-DC43:
Und dort messen wir wieder die Ausführungszeit. Scheint mit 12 Sekunden etwa 5 Sekunden schneller zu sein.
henrik42@Omega-DC43:~$ time node hello-world.js
Hello, world!
real 0m12.302s
user 0m11.500s
sys 0m0.634s
Ich wollte es nochmal etwas genauer wissen. Daher habe ich dieses
kleine Programm run-hello-world.js
geschrieben:
startTime = new Date();
require("./hello-world.js");
console.log((new Date() - startTime) + " Millisekunden");
Auf dem Micro liefert die Ausführung für die release
-Version:
henrik42@Omega-DC43:~$ time node run-hello-world.js
Hello, world!
510 Millisekunden
real 0m12.243s
user 0m11.496s
sys 0m0.595s
Und auf dem PC im Docker-Container:
bash-5.0$ time node run-hello-world.js
Hello, world!
13 Millisekunden
real 0m0.051s
user 0m0.039s
sys 0m0.010s
Und als Vergleich nochmal die Zeiten für die ursprüngliche
compile
-Version auf dem Micro und dem PC.
henrik42@Omega-DC43:~$ time node run-hello-world.js
Hello, world!
5443 Millisekunden
real 0m17.392s
user 0m15.875s
sys 0m0.969s
bash-5.0$ time node run-hello-world.js
Hello, world!
182 Millisekunden
real 0m0.219s
user 0m0.132s
sys 0m0.028s
Die lange Ausführungszeit auf dem Micro ergibt sich also vor allem aus
der etwa 12 Sekunden langen Start-Up-Zeit von node
. Die
Ausführungszeiten von etwa 5400 Millisekunden für die compile
und
500 Millisekunden für release
Variante reichen zwar immer noch nicht
an die Zeiten auf dem PC heran, aber das war auch nicht zu erwarten.
Falls wir also eine lang laufende Anwendung (wie z.B. eine Robotersteuerung) mit ClojureScript & Node.js auf den Micro bringen wollen, müssen wir mit einer 12 Sekunden Start-Up-Zeit und einer gewissen Ladezeit (min. 0,5 Sekunden) rechnen. Wie flüssig die Anwendung anschließend weiterläuft, müssen wir erst noch herausbekommen.
Bisher haben wir den Compiler einfach über shadow-cljs compile my-app
aufgerufen. Da der Compiler in einer JVM läuft, muss man bei
jedem Compile auf die Start-Up-Zeit der JVM warten.
Der folgende compile
dauert nur 3 Sekunden, die ganze
Programmausführung jedoch 23 Sekunden.
bash-5.0$ time shadow-cljs compile my-app
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
[:my-app] Compiling ...
[:my-app] Build completed. (41 files, 0 compiled, 0 warnings, 3.03s)
real 0m23.109s
user 0m26.261s
sys 0m1.870s
shadow-cljs
bietet aber die Möglichkeit, in einem
Server-Modus
zu starten. D.h., wir starten die JVM einmalig und anschließend
triggern wir den Compile zwar explizit von außen, aber wir brauchen
nicht mehr auf den Start-Up der JVM zu warten.
bash-5.0$ shadow-cljs server
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - server version: 2.8.76 running at http://localhost:9630
shadow-cljs - nREPL server started on port 43999
Und in einer zweiten Shell starten wir nun den Compile. Wichtig ist,
dass der Prozess, der hier gestartet wird, den zuvor gestarteten
Server unter localhost:9630
erreichen kann.
Da ich mit Docker arbeite, starte ich diese zweite Shell in demselben Container via
docker exec -it -w /home/node/projekte/hello-world-app/ foobar bash
. Das muss man nicht so machen, aber so ist es am einfachsten.
bash-5.0$ time shadow-cljs compile my-app
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - connected to server
[:my-app] Compiling ...
[:my-app] Build completed. (41 files, 0 compiled, 0 warnings, 2.93s)
real 0m3.187s
user 0m0.098s
sys 0m0.009s
Viel besser! Nun brauchen wir nur 3 Sekunden anstatt 23 Sekunden.
Zwischenstand
OK, was haben wir bisher geschafft?
Wir haben die Round-Trip-Zeit für unseren Think-Code-Run-Zyklus weiter
verkürzt. Leider benötigt das Kopieren auf den Micro via tar/scp
und
das erneute Ausführen inkl. Start-Up-Zeit für node
immer noch zu
viel Zeit.
Wenn man Clojure-Programme schreibt, möchte man während des
Programmierens schon direkt den geschriebenen Code ausprobieren und
damit auch testen. Wir möchten also, dass Änderungen an unserem
Programm
c:/henrik42/projekte/hello-world-app/src/main/hello_world/core.cljs
sofort dazu führen, dass durch den Compiler der zugehörige
JavaScript-Code erzeugt wird. Und ich möchte, dass der neue Code auch
sofort von der laufenden node
-Anwendung geladen und wirksam wird
(Hot-Code-Reload).
shadow-cljs
unterstützt dieses Vorgehen durch watch
. Aus Gründen
(die weiter unten erläutert werden) erweitern wir die :builds
in
C:\henrik42\projekte\hello-world-app\shadow-cljs.edn
um
:with-server
, um damit den anschließenden Compile in die Ziel-Datei
hello-world-with-server.js
zu lenken.
{:source-paths
["src/dev"
"src/main"
"src/test"]
:dependencies
[]
:builds
{:my-app {:target :node-script
:main hello-world.core/greatings
:output-to "hello-world.js"}
:with-server {:target :node-script
:main hello-world.core/greatings
:output-to "hello-world-with-server.js"}}}
Nun starten wir wieder einen Server-Prozess, nur diesmal über die
Aktion watch with-server
anstatt server
.
Falls du noch den Server aus dem oben aufgeführten Beispiel laufen hast, solltest du ihn nun beenden, bevor du fortfährst.
bash-5.0$ shadow-cljs watch with-server
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - server version: 2.8.76 running at http://localhost:9630
shadow-cljs - nREPL server started on port 35703
shadow-cljs - watching build :with-server
[:with-server] Configuring build.
[:with-server] Compiling ...
[:with-server] Build completed. (60 files, 1 compiled, 0 warnings, 4.52s)
Nun kannst du das Programm wieder auf dem PC testen (dazu musst du eine zweite Shell aufmachen; vgl. oben):
bash-5.0$ node hello-world-with-server.js
Hello, world!
Das Programm beendet sich nicht! Stattdessen verbindet es sich mit dem
zuvor gestarteten Server (via localhost:9630
) und wartet (das kannst
du mit netstat
prüfen; vgl. unten).
Nun ändern wir
C:\henrik42\projekte\hello-world-app\src\main\hello_world\core.cljs
und fügen eine Zeile hinzu.
(ns hello-world.core)
(defn greatings []
(println "Hello, world!"))
(println "Geladen")
Nachdem die Datei gespeichert ist, setzt sofort der Compile ein:
[:with-server] Compiling ...
[:with-server] Build completed. (60 files, 1 compiled, 0 warnings, 0.42s)
Und in node
-Shell sehen wir:
bash-5.0$ node hello-world-with-server.js
Hello, world!
Geladen
Die node
-Anwendung hat eine
WebSocket zum
shadow-cljs
-Server aufgebaut. Über diese WebSocket (auf Server-Port
9630
) veranlasst der Server die node
-Anwendung, den neu
kompilierten Code (d.h. die Datei) zu laden.
In diesem Fall wird der Clojure-Namespace neu geladen. Es wird nicht erneut die Funktion
greatings
ausgeführt.
Wir können mit netstat -na
die Verbindungen aufführen:
bash-5.0$ netstat -na
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:39597 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:40661 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:44617 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:9630 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9630 127.0.0.1:47248 ESTABLISHED
tcp 0 0 127.0.0.1:47248 127.0.0.1:9630 ESTABLISHED
Der Server öffnet drei weitere Ports, die wir uns später anschauen.
Wenn du jetzt den shadow-cljs
-Server beendest, indem du CTRL-C
drückst, erfolgt folgende Ausgabe in der node
-Shell und die
Anwendung beendet sich.
REPL client disconnected
Wenn du nun die node
-Anwendung erneut startest (ohne den Server
erneut zu starten), siehst du eine Fehlermeldung und die Anwendung
beendet sich sofort:
bash-5.0$ node hello-world-with-server.js
Geladen
Hello, world!
REPL client error Error: connect ECONNREFUSED 127.0.0.1:9630
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1134:14) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 9630
}
REPL client disconnected
Dies ist der Grund dafür gewesen, dass ich das
watch
-Compile-Ergebnis in eine andere Datei schreiben lassen
wollte. Du kannst mit diff hello-world.js hello-world-with-server.js
die beiden Dateien vergleichen und wirst ein paar Unterschiede
feststellen, auf die ich hier aber nicht im Detail eingehen
möchte. Entscheidend ist, dass du verstehst, dass sich die
node
-Anwendung via HTTP/WebSocket-Verbindung mit dem Server
verbindet. Und diese Logik ist in das Kompilat eingebaut
worden. Falls du also node hello-world-with-server.js
ausführst und
keinen Server am laufen hast, wirst du immer eine Fehlermeldung
erhalten.
Nun kopieren wir die node
-Anwendung wieder auf den Micro:
C:\>tar -C C:/henrik42/projekte/hello-world-app -zcf - hello-world*.js node_modules .shadow-cljs | ssh henrik42@Omega-DC43 "tar -C /home/henrik42 -vzxf -"
Und starten erst den watch
-Server auf dem PC und dann die Anwendung
auf dem Micro:
henrik42@Omega-DC43:~$ node hello-world-with-server.js
Geladen
Hello, world!
REPL client error { Error: connect ECONNREFUSED 127.0.0.1:9630
at Object._errnoException (util.js:1022:11)
at _exceptionWithHostPort (util.js:1044:20)
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1198:14)
code: 'ECONNREFUSED',
errno: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 9630 }
REPL client disconnected
Das klappt natürlich nicht! Denn der shadow-cljs
-Server läuft ja auf
meinem PC und die node
-Anwendung versucht die WebSocket unter
127.0.0.1:9630
zu erreichen.
Wir müssen der node
-Anwendung also sagen, unter welcher URL sie die
WebSocket des shadow-cljs
-Server erreicht. Das machen wir in
C:\henrik42\projekte\hello-world-app\shadow-cljs.edn
über
:devtools-url
:
IP/Hostname: du musst hier die URL eintragen, unter der dein Micro-Controller den Server auf deinem PC erreichen kann. Da ich manchmal über mein Heim-WLAN (FritzBox) mit dem Micro arbeite und mich ein anders Mal direkt über den WLAN-AP des Omega2+ mit dem Micro verbinden kann, hat mein PC aus Sicht des Micro mal die eine, mal die andere IP (und eine Route zum PC braucht der Micro auch! D.h. auch, dass die FritzBox die Route unterstützen muss!!). Außerdem möchte ich die
node
-Anwendung natürlich auch weiterhin auf meinem PC direkt testen können (so wie unsere ersten Tests oben) - d.h. der PC muss sich selber auch unter der angegebenen IP/Hostnamen kennen. Bisher habe ich noch keine smarte Idee, wie man diese Einstellungen alle am besten macht.Windows-Defender-Firewall: damit der Micro (bzw. überhaupt irgendein Rechner) auf deinem PC Port 9630 zugreifen kann, muss du den Port in deiner Firewall auf dem PC freischalten. Dazu habe ich auf dem PC den Micro WLAN-AP als privates Netzwerk ausgewiesen und eine eingehende Regel für Port 9630 eingerichtet.
Docker: falls du mir Docker arbeitest, musst du nun noch via
-p 9630:9630
den Port 9630 in den Container durchleiten:
C:\henrik42>docker run --rm -it --name foobar --user node -v C:\henrik42\:/home/node -w /home/node -p 9630:9630 theasp/clojurescript-nodejs:shadow-cljs-alpine bash
{:source-paths
["src/dev"
"src/main"
"src/test"]
:dependencies
[]
:builds
{:my-app {:target :node-script
:main hello-world.core/greatings
:output-to "hello-world.js"}
:with-server {:target :node-script
:devtools {:devtools-url "http://192.168.3.142:9630"}
:main hello-world.core/greatings
:output-to "hello-world-with-server.js"}}}
Nun also wieder den watch
-Server starten ...
bash-5.0$ shadow-cljs watch with-server
... und sobald der Compile erfolgt ist (denn die Verbindungs-Logik/Konfiguration steckt im Kompilat!), die Dateien auf den Micro kopieren ...
C:\>tar -C C:/henrik42/projekte/hello-world-app -zcf - hello-world*.js node_modules .shadow-cljs | ssh henrik42@Omega-DC43 "tar -C /home/henrik42 -vzxf -"
... und auf dem Micro das Programm starten:
henrik42@Omega-DC43:~$ node hello-world-with-server.js
Geladen
Hello, world!
Nun ändern wir die letzte Zeile in unserem Programm
C:\henrik42\projekte\hello-world-app\src\main\hello_world\core.cljs
:
(ns hello-world.core)
(defn greatings []
(println "Hello, world!"))
(println "Geladen Interaktiv!")
Sobald du die Datei speicherst, erscheint die Compile-Meldung und auf
dem Micro wird die Zeile Geladen
ausgegeben.
Warum wird nicht Geladen Interaktiv!
ausgegeben?
Wir haben zwar eine WebSocket-Verbindung zwischen node
-Anwendung und
watch
-Server, aber über diese wird die node
-Anwendung durch den
watch
-Server nur angewiesen, die Dateien neu zu laden. Und die
neuen Dateien (bzw. die neue Version dieser Dateien) befinden sich
eben bisher nur auf dem PC und nicht auf dem Micro (weil wir sie
bisher nicht erneut dorthin kopiert haben). Solange man die
node
-Anwendung lokal auf dem PC testet, funktioniert das tadellos,
weil dann sowohl der watch
-Server als auch die node
-Anwendung auf
dieselben Dateien zugreifen. Nur durch die Trennung auf
zwei Rechner (bzw. durch die Trennung der Dateisysteme) entsteht
das Problem.
Mir fallen dazu folgende Lösungen ein:
-
Rsync: immer, wenn der
watch
-Server einen Compile durchgeführt hat, ruft er einrsync
auf, um die neuen Stände der Dateien auf den Micro zu bringen, bevor er anschließend dienode
-Anwendung veranlasst, die Dateien neu zu laden. Dadurch würde dienode
-Anwendung immer den neuen/aktuellen Stand lesen. Für diese Lösung bräuchten wir aber eine Möglichkeit, demwatch
-Server zu sagen, dass er zum entscheidenden Zeitpunktrsync
aufrufen soll. -
Samba: anstatt die Dateien vom PC auf den Micro zu kopieren, greift der Micro einfach direkt über einen SAMBA-Mount auf die Dateien des PC zu. In diesem Szenario entstehen gar keine Kopien auf dem Micro (wir bräuchten die Dateien auch nicht mehr zu kopieren), der Micro greift einfach automatisch immer auf den aktuellensten Stand der Dateien zu. Diese Lösung führt jedoch dazu, dass du nun immer den PC benötigst, wenn du das Programm auf dem Micro ausführen möchtest (für die Entwicklung ist das ok. Um dann in Produktion zu gehen, müsstest du die Dateien dann einmalig auf den Micro kopieren).
-
SSHFS: diese Option verhält sich wie die Anbindung via SAMBA. Leider kann ich SSHFS wohl leider nicht installieren, weil mein Kernel zu alt ist.
root@Omega-DC43:~# opkg install sshfs
Installing sshfs (2.10-1) to root...
Downloading https://downloads.openwrt.org/releases/18.06.6/packages/mipsel_24kc/packages/sshfs_2.10-1_mipsel_24kc.ipk
Collected errors:
* satisfy_dependencies_for: Cannot satisfy the following dependencies for sshfs:
* kernel (= 4.14.81-1-d8e1d03c55ce087331756a8674577a86) *
* opkg_install_cmd: Cannot install package sshfs.
Die Übertragung der Dateien via rsync
ist für sich schon eine
Verbesserung zu dem Verfahren via tar
/scp
, das ich bisher
verwendet habe: es geht einfach viel schneller!!
Ich verwende einen rsync
-Daemon auf dem Micro. Hier
/root/rsyncd.conf
:
gid = henrik42
uid = henrik42
read only = false
use chroot = true
transfer logging = true
log format = %h %o %f %l %b
log file = /root/rsync.log
[henrik42]
path = /home/henrik42
Nun den rsync
-Daemon starten:
WARNUNG: Ich führe das
rsync
alsroot
aus. Das ist nicht gut. Das Programm sollte aus Sicherheitsgründen als Usernode
ausgeführt werden.
root@Omega-DC43:~# rsync --daemon --address=0.0.0.0 --config=/root/rsyncd.conf --no-detach --port 4567 -v
Und auf dem PC die Dateien übertragen. Zuvor habe ich auf dem Micro alle Dateien gelöscht, um die Übertragungszeiten messen zu können.
henrik42@Omega-DC43:~$ rm -rf .shadow-cljs/ node_modules/ *.js
Hier der erste Aufruf:
$ time rsync --stats --times --inplace -r .shadow-cljs node_modules hello-world*.js rsync://henrik42@192.168.3.1:4567/henrik42/
Number of files: 1855
Number of files transferred: 1571
Total file size: 93938849 bytes
Total transferred file size: 93938849 bytes
Literal data: 93938849 bytes
Matched data: 0 bytes
File list size: 38476
File list generation time: 0.001 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 94056255
Total bytes received: 30993
sent 94056255 bytes received 30993 bytes 2162925.24 bytes/sec
total size is 93938849 speedup is 1.00
real 0m42,617s
user 0m0,046s
sys 0m0,062s
Und nun gleich nochmal. Einfach um zu sehen, wie lange es dauert festzustellen, dass nichts übertragen werden muss:
$ time rsync --stats --times --inplace -r .shadow-cljs node_modules hello-world*.js rsync://henrik42@192.168.3.1:4567/henrik42/
Number of files: 1855
Number of files transferred: 0
Total file size: 93938849 bytes
Total transferred file size: 0 bytes
Literal data: 0 bytes
Matched data: 0 bytes
File list size: 38476
File list generation time: 0.001 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 38769
Total bytes received: 292
sent 38769 bytes received 292 bytes 26040.67 bytes/sec
total size is 93938849 speedup is 2404.93
real 0m0,643s
user 0m0,031s
sys 0m0,031s
Und nun mache ich eine Änderung am Code und übertrage den auf den Micro:
$ time rsync --stats --times --inplace -r .shadow-cljs node_modules hello-world*.js rsync://henrik42@192.168.3.1:4567/henrik42/
Number of files: 1855
Number of files transferred: 7
Total file size: 93938861 bytes
Total transferred file size: 88478 bytes
Literal data: 12384 bytes
Matched data: 76094 bytes
File list size: 38476
File list generation time: 0.001 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 51884
Total bytes received: 1207
sent 51884 bytes received 1207 bytes 106182.00 bytes/sec
total size is 93938861 speedup is 1769.39
real 0m0,677s
user 0m0,000s
sys 0m0,015s
OK, das sind Zeiten, mit denen man arbeiten kann.
Ich habe eine ganze Weile rumprobiert, bis es wirklich gut geklappt hat. Entscheidend ist, dass für die Ermittlung, welche Dateien sich geändert haben und übertragen werden müssen, der Zeitstempel verwendet wird. Wenn ich auf CHECKSUM-Überprüfung wechsel, dauert es 100x länger. Das ist einfach zu viel für den Micro (CPU, Dateizugriff). Deswegen ist
--times
auch entscheidend. Die Option--inplace
bringt nochmal etwas Zeit, wenn tatsächlich Dateien übertragen werden.
Nun müssen wir nur noch den watch
-Server dazu bringen, rsync
aufzurufen, nachdem er die neuen Dateien geschrieben hat und
bevor er die node
-Anwendung dazu auffordert, diese Dateien neu
zu laden.
Wenn du mit dem Docker-Container
theasp/clojurescript-nodejs:shadow-cljs-alpine
arbeitest, musst du nochrsync
installieren bzw. am besten baust du dir ein eigenes Image und verwendest das.
Das geht über den Eintrag :build-hooks
in
C:\henrik42\projekte\hello-world-app\shadow-cljs.edn
:
{:source-paths
["src/dev"
"src/main"
"src/test"]
:dependencies
[]
:builds
{:my-app {:target :node-script
:main hello-world.core/greatings
:output-to "hello-world.js"}
:with-server {:target :node-script
:build-hooks [(rsync.rsync/rsync)]
:devtools {:devtools-url "http://192.168.3.142:9630"}
:main hello-world.core/greatings
:output-to "hello-world-with-server.js"}}}
Und in C:\henrik42\projekte\hello-world-app\src\dev\rsync\rsync.clj
implementieren wir den Hook in Clojure.
(ns rsync.rsync
(:require [clojure.java.shell :as sh]))
(defn rsync
{:shadow.build/stage :flush}
[build-state & args]
(sh/sh "rsync" "--stats" "--times" "--inplace" "-r"
".shadow-cljs"
"node_modules"
"hello-world-with-server.js"
"rsync://henrik42@192.168.3.1:4567/henrik42/")
build-state)
Nun muss einmal der watch
-Server neu gestartet werden. Wenn du nun
die letzte Codezeile in
C:\henrik42\projekte\hello-world-app\src\main\hello_world\core.cljs
in (println "Geladen Interaktiv! WOW!")
änderst, solltest du nach
wenigen Augenblicken die Ausgabe auf dem Micro sehen.
Zwischenstand
OK, was haben wir bisher geschafft?
Wir haben auf unserem PC einen
ClojureScript-zu-JavaScript-Compiler-Server-Prozess, der unseren
ClojureScript-Code, jedesmal wenn wir ihn ändern/speichern, sofort
kompiliert, die Dateien via rsync
auf den Micro kopiert und der
laufenden node
-Anwendung über eine WebSocket einen Trigger gibt, so
dass die node
-Anwendung die soeben kopierten Dateien in-flight neu
liest/auswertet. Der neue Code wird in der node
-Anwendung sofort
wirksam, ohne dass wir die node
-Anwendung durchstarten müssten.
Aber es geht noch weiter.
Bisher müssen wir Code, den wir während des Programmierens
ausprobieren wollen, als Funktion in eine der Quelldateien schreiben
und warten, dass der Compiler die Quellen übersetzt, die Dateien dann
via rsync
übertragen werden und dann innerhalb der laufenden
node
-Anwendung ausgeführt werden. D.h. auch, dass der Code, den wir
ausprobieren wollen, entweder durch das Neuladen eines Namespaces oder
durch einen langlaufenden Server-Prozess innerhalb der
node
-Anwendung angestoßen werden können muss. Diese Einschränkung
ist etwas sperrig und umständlich.
In der Clojure-Welt verwendet man daher gerne die
REPL. Auch
shadow-cljs
bietet
REPL-Unterstützung. Dabei
musst du berücksichtigen, dass du es mit (mindestens) zwei
Laufzeiten zu tun hast, mit denen du via REPL interagieren kannst: zum
einen kannst du auf den watch
-Server via REPL zugreifen und zum
anderen kannst du auf die node
-Anwendung via REPL zugreifen. Wir
schauen uns beide Optionen an.
Es gibt weitere Konstellationen, bei denen z.B. auf deinem PC ein JVM-Prozess inkl. REPL-Prompt gestartet werden und zusätzlich auch ein
node
-Prozess auf dem PC gestartet wird und die JVM-REPL dann mit demnode
-Prozess via WebSocket gekoppelt sind. Aber ich will mich hier um die Anbindung an den Micro konzentrieren.
Als erstes bauen wir eine Clojure REPL zu dem laufenden
watch
-Server auf. Du kannst anhand von (type 1)
leicht sehen, dass
dies eine Clojure-REPL und nicht eine ClojureScript-REPL ist.
bash-5.0$ shadow-cljs clj-repl with-server
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - connected to server
shadow-cljs - REPL - see (help)
To quit, type: :repl/quit
shadow.user=> (type 1)
java.lang.Long
Du kannst auch verifizieren, dass die REPL wirklich in dem
watch
-Server läuft, indem du einfach etwas ausgibt. Die Ausgabe
sollte in der Konsole/Shell des watch
-Servers erscheinen.
shadow.user=> (.println System/out "boo!")
nil
Mit dieser REPL kannst du nun Clojure programmieren. Z.B. könntest
du den Code von rsync.rsync/rsync
interaktiv entwickeln und testen.
Du kannst aber nun auch eine ClojureScript REPL zu der
node
-Anwendung aufbauen und auf dem Mirco etwas ausgeben. Du hast
jetzt vollen Zugriff auf die node
-Laufzeit.
(shadow/repl :with-server)
cljs.user=> (type 1)
#object[Number]
cljs.user=> (println "boo!")
nil
Du kannst jetzt auch schon über die ClojureProgramme auf dem Omega2+ Micro aufrufen - wie z.B.
fast-gpio set 44 1
um die LED zu steuern.
Die Anbindung zwischen
node
-Anwendung undwatch
-Server ist recht robust. Du kannst z.B. dienode
-Anwendung stoppen und wenn du dann versuchst, über die CLJS REPL etwa auszuwerten, bekommst du eine freundliche Fehlermeldung. Wenn du dienode
-Anwendung dann wieder startest, kannst du die CLJS REPL weiter verwenden. Du hast dann natürlich den "Zustand" der zuvor gestoppten Laufzeit verloren.
> (type 1)
No application has connected to the REPL server. Make sure your JS environment has loaded your compiled ClojureScript code.
Anstatt erst die Clojure-REPL zu starten und dann erst die CLJS-REPL, kannst du auch gleich die CLJS-REPL starten:
bash-5.0$ shadow-cljs cljs-repl with-server
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - connected to server
cljs.user=> (type 1)
#object[Number]
Zwischenstand
OK, was haben wir bisher geschafft?
Wir können nun nicht nur unseren Code in unseren Quelltext-Dateien
ständig ändern, kompilieren und auf dem Micro testen/ausführen,
sondern wir haben auf unserem PC eine interaktive Shell (REPL) in
unsere node
-Anwendung. Über diese REPL haben wir volle Kontrolle
über die node
-Anwendung. Auf diese Weise können wir Code interaktiv
ausprobieren und den Zustand unseres Programms inspizieren und
kontrollieren/ändern.
Es fehlt nun noch ein letzter Schritt.
Eine REPL ist super, um über einen Prompt/Shell interaktiv mit einer Clojure(Script)-Laufzeit zu interagieren (z.B. um eine Funktion mit bestimmten Argumentwerten aufzurufen). Wenn man aber z.B. eine neue Funktion ausprobieren möchte, so müsste man sie vollständig in die REPL eingeben. Das ist umständlich.
Ich könnte die Funktion natürlich auch in eine der ClojureScript-Dateien schreiben und den Kompiler die Datei übersetzen lassen. Aber dann würde der gesamte Namespace in der
node
-Laufzeit neu geladen werden. Ich möchte aber nur eine Funktion zufügen bzw. ändern.
Es wäre viel schöner, wenn man den Clojure-Code in dem eigenen Lieblingseditor schreiben könnte und den Code direkt aus dem Editor heraus in die REPL "übergeben" könnte.
Genau dafür gibt es nREPL.
Hier musst du zwei Szenarien auseinanderhalten: du kannst mit irgendeinem Editor deine ClojureScript-Quelltext-Dateien editieren und diese werden durch den
watch
-Server sofort kompiliert, sobald du die Datei speicherst. Dazu brauchst du weder die REPL noch nREPL.nREPL ist eine Schnittstelle, die es Editoren und anderen Tools ermöglicht, sich über ein Netzwerk mit einer laufenden Clojure-Laufzeit zu verbinden. Über diese Verbindung können dann bestimmte Aktionen erfolgen -- wie z.B. die Auswertung von Clojure-Ausdrücken.
Der watch
-Server startet einen solchen nREPL-Server (vgl. nREPL server started on port 37973
). Du kannst den Port fix einstellen,
musst es aber nicht.
~/projekte/hello-world-app $ shadow-cljs watch with-server
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - server version: 2.8.76 running at http://localhost:9630
shadow-cljs - nREPL server started on port 37973
shadow-cljs - watching build :with-server
[:with-server] Configuring build.
[:with-server] Compiling ...
[:with-server] Build completed. (60 files, 1 compiled, 0 warnings, 7.08s)
Da ich den
watch
-Server in einem Docker-Container laufen lasse, mein Emacs-Editor aber unter Windows läuft, muss ich den nREPL-Server auf einem fixen Port starten, damit ich ihn dann in meinem Docker-Container via-p <port>:<port>
nach außen publizieren kann.
In C:\henrik42\projekte\hello-world-app\shadow-cljs.edn
trage ich
:nrepl {:port 9000}
ein:
{:source-paths
["src/dev"
"src/main"
"src/test"]
:dependencies
[]
:nrepl {:port 9000}
:builds
{:my-app {:target :node-script
:main hello-world.core/greatings
:output-to "hello-world.js"}
:with-server {:target :node-script
:build-hooks [(rsync.rsync/rsync)]
:devtools {:devtools-url "http://192.168.3.142:9630"}
:main hello-world.core/greatings
:output-to "hello-world-with-server.js"}}}
Und ich starte den Docker-Container neu. Der nREPL-Server wird im Container auf Port 9000 gebunden. Und ich publiziere ihn nach außen ebenfalls auf Port 9000. Ich hatte bisher das
rsync
immer manuell unter Windows gestartet. Da ich nun aber dasrsync
durch denwatch
-Server starten werde (vgl. unten), muss ichrsync
noch viaapk add rsync
explizit installieren.
C:\>docker run --rm -it --name foobar --user root -v C:\henrik42\:/home/node -w /home/node -p 9630:9630 -p 9000:9000 theasp/clojurescript-nodejs:shadow-cljs-alpine bash
bash-5.0# apk add rsync
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/community/x86_64/APKINDEX.tar.gz
(1/4) Installing libacl (2.2.52-r6)
(2/4) Installing libattr (2.4.48-r0)
(3/4) Installing popt (1.16-r7)
(4/4) Installing rsync (3.1.3-r1)
Executing busybox-1.30.1-r2.trigger
OK: 115 MiB in 67 packages
bash-5.0# su node
~ $ cd projekte/hello-world-app/
~/projekte/hello-world-app $ shadow-cljs watch with-server
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - server version: 2.8.76 running at http://localhost:9630
shadow-cljs - nREPL server started on port 9000
shadow-cljs - watching build :with-server
[:with-server] Configuring build.
[:with-server] Compiling ...
[:with-server] Build completed. (60 files, 1 compiled, 0 warnings, 7.72s)
Es gibt für viele Editoren
nREPL-Integrationen. Ich
benutze Emacs/CIDER. Nachdem
ich den Emacs mit localhost:9000
verbunden habe, bin ich zwar mit
der nREPL verbunden, aber dies ist eine Clojure-nREPL.
Du kannst dich gleichzeitig mit einer REPL (vgl. oben) und via nREPL mit dem
watch
-Server verbinden.
shadow.user> (type 1)
java.lang.Long
shadow.user>
Nun kannst du die nREPL aber umschalten auf CLJS-nREPL:
shadow.user> (shadow/repl :with-server)
To quit, type: :cljs/quit
[:selected :with-server]
cljs.user>
Wenn du nun Clojure-Ausdrücke auswertest, erfolgt die Auswertung in
der node
-Anwendung. Der Compile erfolgt natürlich weiterhin im
ClojureScript-Compiler, der in der JVM läuft. D.h. in diesem Fall wird
der Clojure(Script)-Code weiterhin in der JVM nach JavaScript
kompiliert. Der Kopierschritt via rsync
ist in diesem Fall aber
nicht nötig. Stattdessen wird der JavaScript-Code über die WebSocket
in die node
-Anwendung gebracht und dort ausgeführt.
cljs.user> (type 1)
#object[Number]
Pew! Das war ein langer Weg! Wo sind wir angekommen?
-
Wir können mit unserem Lieblingseditor (z.B. Notepad++, Emacs, vi, VSCode, Eclipse) ClojureScript-Programme schreiben.
-
Diese werden bei jedem "Speichern" direkt von einem Kompiler, der auf unserem PC (optional in einem Docker-Container) in einer JVM läuft, in JavaScript übersetzt und dieser JS-Code wird in Dateien geschrieben.
-
Diese JavaScript-Dateien können wir via
rsync
(odertar/scp
) auf einen Micro-Controller (oder anderen Rechner/PC/Server) bringen und dort unser ClojureScript-Programm via Node.js ausführen.
Für ein besseres Programmier-Erlebnis haben wir weitere Optionen:
-
Wir können während der Entwicklung lokal auf unserem PC Node.js benutzen, um unsere Anwendung auszuführen (also ohne den Micro). Jedesmal, wenn der Compiler neuen JavaScript-Code produziert, wird dieser Code sofort von unserer Anwendung geladen und ist dann in der Anwendung wirksam (Hot-Code-Reload).
-
Wir können uns über eine REPL mit unserer Node.js-Anwendung verbinden und interaktiv mit unserem Programm kommunizieren (wie in einer Windows/UNIX-Shell). So können wir u.a. Funktionen definieren und aufrufen.
-
Das alles können wir aber auch mit unserem Micro machen! Dazu übertragen wir automatisch den gerade neu kompilierten JavaScript-Code via
rsync
auf den Micro und dort läuft dann die Node.js-Anwendung, die wir mit der REPL auf unserem PC verbinden können. Wir haben somit auf dem PC eine Shell in unsere Node.js-Anwendung hinein, die auf dem Micro läuft. -
Für eine noch direktere Think-Code-Run-Feedback-Schleife können wir die nREPL-Integration unseres Editors verwenden, um Funktionen und ganze Dateien (Namespace) direkt aus dem Editor heraus in der Node.js-Anwendung auszuwerten.
That's all Folks! Viel Spaß!
Anstatt mehrere Verzeichnisse auf den Micro-Controller via rsync
zu
kopieren, können wir den gesamten Code, den wir genötigen, in einem
einzigen Verzeichnis zusammenfassen.
Dazu tragen wir in
C:\henrik42\projekte\hello-world-app\shadow-cljs.edn
das
Zielverzeichnis :output-dir
ein und passen :output-to
an:
{:source-paths
["src/dev"
"src/main"
"src/test"]
:dependencies
[]
:nrepl {:port 9000}
:builds
{:my-app {:target :node-script
:main hello-world.core/greatings
:output-to "hello-world.js"}
:with-server {:target :node-script
:build-hooks [(rsync.rsync/rsync)]
:devtools {:devtools-url "http://192.168.3.142:9630"}
:main hello-world.core/greatings
:output-dir "hello-world-target"
:output-to "hello-world-target/hello-world-with-server.js"}}}
In C:\henrik42\projekte\hello-world-app\src\dev\rsync\rsync.clj
passen wir die rsync
-Anweisung an. Außerdem müssen wir wegen eines
kleinen Bugs den Hook mit den nebenläufigen Builds-Tasks via
shadow.build.async/wait-for-pending-tasks!
synchronisieren.
(ns rsync.rsync
(:require [clojure.java.shell :as sh]
[shadow.build.async]))
(defn rsync
{:shadow.build/stage :flush}
[build-state & args]
(let [cmd ["rsync" "--stats" "--times" "--inplace" "-r"
"hello-world-target"
"rsync://henrik42@192.168.3.1:4567/henrik42/"]]
(println "Synchronisiere mit Build-Tasks ....")
(shadow.build.async/wait-for-pending-tasks! build-state)
(println (apply str "exec: " (interpose " " cmd)))
(println (apply sh/sh cmd))
build-state))
Nun musst du einmalig das Verzeichnis
c:\henrik42\projekte\hello-world-app\hello-world-target\
erstellen
und folgende Befehle ausführen:
npm init -y
npm install ws source-map-support
Anschließend kannst du den watch
-Server neu starten.
~/projekte/hello-world-app $ shadow-cljs watch with-server
shadow-cljs - config: /home/node/projekte/hello-world-app/shadow-cljs.edn cli version: 2.8.76 node: v13.2.0
shadow-cljs - server version: 2.8.76 running at http://localhost:9630
shadow-cljs - nREPL server started on port 9000
shadow-cljs - watching build :with-server
[:with-server] Configuring build.
[:with-server] Compiling ...
Synchronisiere mit Build-Tasks ....
exec: rsync --stats --times --inplace -r hello-world-target rsync://henrik42@192.168.3.1:4567/henrik42/
{:exit 0, :out
Number of files: 179 (reg: 169, dir: 10)
Number of created files: 179 (reg: 169, dir: 10)
Number of deleted files: 0
Number of regular files transferred: 169
Total file size: 7,024,417 bytes
Total transferred file size: 7,024,417 bytes
Literal data: 7,024,417 bytes
Matched data: 0 bytes
File list size: 0
File list generation time: 0.002 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 7,036,915
Total bytes received: 3,303
sent 7,036,915 bytes received 3,303 bytes 1,564,492.89 bytes/sec
total size is 7,024,417 speedup is 1.00
, :err }
[:with-server] Build completed. (60 files, 1 compiled, 0 warnings, 8.79s)
Auf dem Micro startest du die Anwendung inkl. REPL/nREPL-Anbindung mit
henrik42@Omega-DC43:~$ node hello-world-target/hello-world-with-server.js
Alles zusammen braucht nun 7,1 MB. Das ist erheblich weniger als die
55,7 MB, die wir ursprünglich übertragen hatten, bevor wir
:output-dir
verwendet hatten.
henrik42@Omega-DC43:~$ du -sh hello-world-target
7.1M hello-world-target