4.2.2018 - MacBook pro vs. Tuxedo

In diesem Post vergleiche ich zwei Computer, die man eigentlich nicht vergleichen kann: Ein 2013er MacBook pro 15 ‘’ Retina Display und ein 2018er Tuxedo Notebook mit 14’’ Bildschitm und Ubuntu Linux 16.04.

Anlass dieses Vergleichs war, dass mein auch unterwegs vielgenutztes MacBook sich immer öfter unvermittelt ausschaltete, wenn es etwas mehr Leistung erbringen musste, auch wenn die Akkuanzeige noch bei 60% oder mehr stand. Als Ursache kristallisierte sich schon bald der fast 5 Jahre alte Akku mit fast tausend Ladezyklen heraus. Er hatte offensichtlich nicht nur an Kapazität verloren, sondern er schaffte es auch nicht mehr, dem Betriebssystem rechtzeitig mitzuteilen, wenn sein Saft dem Ende zuging.

Wer Apple kennt, der weiss: Selbermachen ist schwierig. Mit meinen zwei linken Händen wollte ich diesem teuren Teil nicht an die Eingeweide, Also fragte ich den freundlichen Apple Reseller, der einen Kostenvoranschlag von 300.- machte und eine Reparaturzeit von 3 Tagen in Aussicht stellte. Nun bin ich, was Computerfirmen betrifft, ein etwas gebranntes Kind und wusste, dass drei Tage sich in dieser Branche oft als drei Wochen oder mehr entpuppen können, und da ich den Laptop oft benötige, musste ein Ersatz für die Reparaturzeit her.

Ich bin zwar kein Windows-Hasser, aber mit Windows 10 kann ich absolut nichts anfangen. Ein MacBook wiederum ist als Notbehelf einfach zu teuer. Ein Linux-Laptop sollte es sein. Linux selbst auf einem Standard-Laptop zu installieren, ist allerdings immer ein Abenteuer mit ungewissem Ausgang: Oft gehen die Stromsparmechanismen nicht korrekt, der Lüfter dreht in den höchsten Tönen, oder die speziellen Tasten etwa zur Bildschirmhelligkeits-Regelung gehen nicht, oder er schläft beim Zuklappen nicht ein, oder erwacht beim Aufklappen nicht mehr.

Da trifft es sich gut, dass es (mindestens) eine Firma gibt, die sich auf Linux-Computer und Laptops spezialisiert hat: Tuxedo Computer in Deutschland.

Da mir niedriges Gewicht und lange Akkuleistung wichtiger sind, als Spieletauglichkeit, wählte ich das 14 Zoll-Modell aus der Book-BU Serie. Tuxedo Notebooks kann man sehr weitgehend Konfigurieren. Ich entschied mich für eine eher preiswerte Konfiguration mit 16GB RAM, einer 250GB SSD als einzigem Laufwerk und eine Core-i5-CPU.

Die Lieferung erfolgte sehr schnell, nach 4 Tagen war es da. Mitgeliefert hat Tuxedo ein paar Gadgets wie Kugelschreiber und Sticker, sowie einen Zugang für die “Tuxedo-Cloud”, der allerdings bei mir nicht funktionierte. Egal, brauche ich eh nicht. Zeit für einen Vergleich. Vorauszuschicken ist, dass das MacBook 2013 stolze 2700.- gekostet hat, das Tuxedo-Book nun knapp 900.-.

Äusserlichkeiten

Die Eleganz und Verarbeitungsqualität des MacBook sind legendär. Aber das Tuxedo macht gar keine schlechte Figur daneben. Es ist ebenfalls sehr schlank und schick geformt. Dass es aus Plastik ist, bewirkt gegenüber dem Aluminium-Macbook natürlich eine weniger edle Anmutung, dafür ist es leichter. Die Verarbeitungsqualität erhält das Prädikat “brauchbar”. Manche Teile stehen fühlbar ein wenig über. Die Tastatur ist angenehm gross, aber im Gegensatz zu der des Mac nicht beleuchtet. Kleiner Minuspunkt bei schlechten Lichtverhältnissen.

Ich würde sagen: Äusseres: 10:6 für das Macbook.

Schauen, Hören und Fühlen

Der Bildschirm des Macbook ist über jeden Zweifel erhaben - solange das Licht nicht von hintern kommt. Dann kann man es als Rasierspiegel benutzen, aber nicht mehr zum Arbeiten. Der Tuxedo glänzt mit einem matten Display, allerdings nicht mit Retina-Auflösung.

Das Tippgefühl ist beim Mac besser. Beim Tuxedo ist es zwar auch nicht schlecht, die Tasten haben einen angenehmen Hub und deutlichen Druckpunkt, aber mich störte, dass die Leertaste etwas störrisch war und manchmal mit Nachdruck bedient werden wollte. Vermutlich könnte man das sogar als Garantiefall reklamieren, aber so schlimm war es dann doch wieder nicht. Raffiniert finde ich, dass der Deckel so gestaltet ist, das er das Notebook hinten ein wenig anhebt, wenn man ihn aufklappt, so dass die Tastatur ergonomisch optimal leicht geneigt steht.

Worin die beiden sich aber wirklich um Längen unterscheiden, ist das Touchpad. Das des Macbook ist gross, präzise und empfindlich, trotzdem aber praktisch immun gegen ungewollte Bewegungen und Berührungen. In all den Jahren hatte ich so gut wie nie das Bedürfnis, eine Maus anzuschliessen. Beim Tuxedo kam dieses Bedürfnis sehr schnell. Natürlich kann man es mit dem Touchpad bedienen, aber wie gesagt: Hier geht es um Welten, wenn man es mit dem MacBook vergleicht.

Das MacBook hört man fast nie. Wenn man doch einmal den Lüfter hört, dann in einer wenig störenden Tonlage. Beim Tuxedo schaltet sich der etwas höher sirrende Lüfter schon beim Compilieren oder Video schneiden hörbar ein. Allerdings nur in sehr ruhiger Umgebung störend.

Meine Wertung: 10:5 fürs MacBook in dieser Kategorie.

Leistung

Beide Computer fühlen sich subjektiv sehr schnell an. Störende Wartezeiten gibt es nie, Programmstarts sind praktisch augenblicklich. Aber auch objektiv geben sich beide keine Blösse. Obwohl das Tuxedo “nur” einen i5 Prozessor hat, gegenüber dem i7 des Mac (allerdings einer älteren Generation), kann es problemlos mithalten. Als Benchmark zum Beispiel das Compilieren von elexis-3-base:

  • MacBook pro 16GB, SSD, Core i7-3740: 3’ 13”.
  • Tuxedo BU1407 16GB, SSD, Core i5-8250: 2’ 57”

Auch Geekbench sieht die beiden dicht beieinander:

  • MacBook: Single-Core: 3484, Multi-Core: 13484 Punkte.
  • Tuxedo: Single-Core: 3308, Multi-Core: 12632 Punkte.

Hier kann die Wertung nur ausgeglichen sein: 10:10 Punkte.

Batterielaufzeit

Naja, die Batterie des MacBook war ja am Ende. Am Anfang hielt sie rund 10 Stunden durch, und erst nach etwa 3 Jahren bemerkte ich eine gewisse Verschlechterung.

Die Batterie des Tuxedo hielt gut 8 Stunden durch, über Langzeiterfahrungen kann ich naturgemäss noch nichts berichten. Jedenfalls ist die Laufzeit auch für längere Bahnfahrten und Sitzungen gut ausreichend.

10:9 fürs MacBook.

Konfigurierbarkeit

Ein Blick auf die Website zeigt: Bei Tuxedo kann man sich den Laptop bis ins letzte Detail zusammenstellen. Mehrere unterschiedliche Laufwerke, verschiedene Prozessoren usw. Beim Apple gibt es nur eine Hand voll Konfigurationen.

7:10 für Tuxedo

Wartbarkeit und Erweiterbarkeit

Das Macbook musste ich für einen Akkuwechsel zum Händler bringen. Bei Tuxedo ist das Wechseln des Akku eine Sache von zwei Schiebern und einmal Einrasten. Ein Ersatzakku kostet bei Tuxedo weniger als ein Drittel der Reparatur des Mac. Natürlich weiss ich nicht, ob es diesen Akku noch zu kaufen gibt, wenn ich ihn mal brauche. Diese Langzeit-Sicherheit ist bei Apple sicher grösser.

Auch sonst ist der Tuxedo weit offen: Man kann Laufwerke und Speicher problemlos selbst austauschen bzw. erweitern, und man kann für alles günstige Standardteile verwenden.

3:10 für Tuxedo.

Systemintegration und Betriebssystem

Kein Zweifel: Beim Macbook sind Hardware und Betriebssystem optimal aufeinander abgestimmt. Das ist eine grosse Stärke der Apple-Computer. Aber auch Tuxedo ist die Integration recht gut gelungen. Die Laptop-spezifischen Knöpfe tun, was sie sollen, die Stromsparmodi funktionieren, man kann einstellen, dass er beim Zuklappen einschlafen soll, und er erwacht (meistens) auch wieder, wenn man ihn aufklappt. Meistens, nicht immer. Beim Aktualisieren des Betriebssystems nach dem ersten Aufstarten tat er ein wenig zickig, aber dann lief alles rund.

Beide Betriebssysteme sind recht offen. Beim MacBook kann man via Homebrew praktisch alle verfügbaren Linux-Programme laufen lassen, und der Darwin-Systemkern ist ebenso OpenSource wie der Kern von Linux. Apple macht es einem aber in den letzten Versionen ein wenig arg schwer, Fremdsoftware laufen zu lassen, scheint sich also von der Offenheit wegbewegen zu wollen.

Unterm Strich haben beide Konzepte ihre Vor- und Nachteile. Mit Linux kann an “alles” machen, aber man muss manchmal doch lange suchen und googeln, bevor man herausgefunden hat, wie es geht. Standardaufgaben wie Drucker anschliessen, löst Ubuntu Linux inzwischen aber genauso mühelos wie Windows oder MacOS: Anschliessen, geht. Zumindest, wenn es kein Exot ist.

Zusammengefasst schenken sich die Kontrahenten hier nichts, auch wenn ihre Stärken und Schwächen unterschiedlich verteilt sind.

8:8

Fazit

Wenn man einfach nur die Punkte zusammenzählt, liegt Tuxedo mit 58:58 Punkten gleich auf mit dem dreimal so teuren Mac. Das hat es allerdings vor allem seiner leichten Reparierbarkeit zu verdanken. Und natürlich wird man nie alle Bereiche gleich werten.

Und noch ein PS

Meine Befürchtung wegen der Reparaturzeit hat sich übrigens nicht bewahrheitet. Nach knapp einer Woche konnte ich das Macbook wieder abholen, und der Kostenvoranschlag war eingehalten.

28.3.2017 - Garagentor-Fernbedienung, Zweiter Teil

Zum ersten Versuch

Der zweite Entwurf

Nachdem ich sowohl den Finger, als auch den Transistor mit dem Lötkolben verbrannt hatte, stellte sich heraus: Ich bin elektrotechnisch eben doch ein Warmduscher, und so kaufte ich anstelle der Eigenbau-Relaisplatine ein PiFace Digital 2. Das kann man einfach auf den Raspberry draufstecken, und es bietet nicht nur zwei Relais, sondern auch 8 Aus- und 8 Eingänge. Ein wenig Overkill für unser Projekt, aber zumindest einen der Eingänge können wir noch sinnvoll nutzen. Die Relais können bis zu 20V/5A schalten, mehr als genug für den GaragentorTaster, der mit 12V und wenigen mA betrieben wird.

Der Hardware-Aufbau sieht so aus:

  • Eines der PiFace-relais wird parallel zum Garagentor-Taster geschaltet
  • Ein Mikroschalter wird so befestigt, dass er bei geöffnetem Garagentor betätigt wird. Dieser Schalter wird zwischen Inputpin 0 und GND des PiFace geschaltet. Damit “weiss” die Garagentor-Fernbedienung, wenn das Tor offen steht.

Ein weiteres Problem mit dem ersten Versuch war, dass das Einbuchen ins häusliche WLAN zu lange dauert. Wenn man vor der Garage steht, mag man nicht eine Minute warten, bis das Handy Kontakt hat. Also wird der Software-Teil so umgebaut, dass man das Tor übers Internet bedienen kann. Es wird somit, modern ausgedrückt, Teil des IoT.

Damit entwickeln sich eine Reihe zusätzlicher Anforderungen für den Garagentor-Server:

  1. Es muss eine DynDNS-Umleitung eingerichtet werden, damit man die Garage mit einem sprechenden Namen (some.where.ch) anstatt einer kryptischen Internetadresse (85.34.223.12) erreichen kann (welche obendrein bei den meisten Providern fast täglich ändert).
  2. Der Raspberry muss so konfiguriert werden, dass er den DynDNS Provider mit der jeweils aktuellen IP versorgt.
  3. Der häusliche Router muss Zugriffe auf den Garagengtorserver an diesen durchreichen.
  4. Der Kontakt zum Server muss verschlüsselt erfolgen, damit Passwoörter nicht abgehört werden können.
  5. Hackversuche müssen unterbunden werden.
  6. Zugriffe müssen geloggt werden, damit Angriffe erkannt und analysiert werden können.

Somit wird aus dem kleinen Projekt des ersten Teils ein etwas grösseres Projekt, welches auch ganz gut als allgemeines NodeJS/Express-Server Beispiel dienen kann. Ich führe daher im Folgenden alles ein wenig deutlicher aus. Ganz am Ende findet sich dann der komplette Quellcode.

1. Dynamische DNS

Ein Teilnehmer im Internet ist durch seine IP-Adresse eindeutig indentifiziert. Es gibt zwei varianten: Das ältere IPV4, das aus 4 Gruppen von Zahlen zwischen 0 und 255 zusammengesetzt ist, etwa so: 85.198.16.82. Und das neuere IPV6, das aus 8 Gruppen von maximal 4-stelligen hexadezimalen Zahlen besteht, etwa so: 2f45:b8:8256:1fd:0:0:2345:ff1a. Die meisten Provider setzen derzeit noch auf IPV4 und erteilen eine IP je Router. Diese IP wird dynamisch erteilt, also meistens einmal pro Tag (und bei jedem neuen Verbindungsaufbau) neu vergeben. Fixe IPs kann man zwar von manchem Providern bekommen, aber meist nur für recht viel Geld. Um einen privaten Server von aussen zu erreichen, muss man also irgendwie herausfinden, welche IP er gerade hat.

Die Lösung hierfür heisst DynDNS: Unser Server teilt einem speziellen Internet-Service regelmässig mit, welche IP er gerade hat, und dieser publiziert diese IP unter einem symbolischen Namen. Man kann also zum Beispiel “mein.garagentor.ch” reservieren, Falls das noch nicht vergeben ist, und einen DynDNS Service einrichten, der jeden Internet-Zugriff auf diesen Namen auf unseren Server leitet.

www.dyndns.com ist der Pionier solcher Services, aber es gibt auch etliche Andere. ich verwende ZoneEdit, weil ich dort ohnehin einige Domains gehostet habe, und der DynDNS Service nichts extra kostet.

2. Raspberry anweisen, mit DynDNS zu kooperieren

Dafür gibt es fixfertige Lösungen:

sudo apt-get install ddclient

Den ddclient muss man für dne gewählten DynDNS-Diens konfigurieren, dann wird er fortan immer die aktuelle IP-Adresse an den Dienst melden-

3. Router konfigurieren

Da die vom Provider erteilte IP pro Router gilt, haben alle Computer “hinter” dem Router gegen aussen dieselbe IP. Der Router hat die Aufgabe, Zugriffe auf die inneren Computer zu verteilen, für die er ein internes Netzwerk mit eigenen IP-Nummern (z.B. 192.168.1.5) aufbaut. Wenn ein Browser jetzt auf https://meingaragentor.ch:2017 zugreift, dann wird er melden: “Diese Website ist nicht erreichbar”. Er weiss ja nicht, wo der Server lauert, der diesen Dienst bereitstellt. Wir müssen also den Route anweisen, den Port 2017 zu unserem Raspberry weiterzuleiten.

Das dafür nötige Vorgehen ist bei jedem Router anders. Das entsprechende Menü heisst oft “NAT” oder “Dienst freigeben”.

4. Kontakt zum Server verschlüsseln

Dies geschieht, indem man das https:// anstatt dem http:// Protokoll verwendet. Dies muss auf dem Server eingerichtet werden. Bisher hatten wir:

    app.listen(2017,function(){
      console.log("Garagenserver läuft an port 2017")
    })

Was einen HTTP-Server erstellt. Neu schreiben wir nun:

    https.createServer({
      key: fs.readFileSync('key.pem'),
      cert: fs.readFileSync('cert.pem')
    }, app).listen(2017)

Wobei “key” der private Schlüssel des Servers und “cert” das Zertifikat ist, das die Identität des Servers bestätigt. Bis vor kurzem war ein solches Zertifikat eine teure Sache. Man musste sich bei einer Zertifizierungsstelle als Inhaber der Website ausweisen und dann einen jährlichen Betrag bezahlen. Heute gibt es mit Letsencrypt eine kostenlose Variante.

So oder so kann man auch einfach ein selbstsigniertes Zertifikat benutzen. Dafür genügt ein simples:

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes

Damit erstellt man einen Schlüssel und bestätigt sich selber, wer man ist. Natürlich mekren alle Browser diese “Schummelei” und warnen vor einer nicht-sicheren Verbindung. Das ist aber nicht ganz korrekt: Die Verschlüsselung ist absolut gleichwertig mit der, die man mit einem “offiziellen” Zertifikat erhält. Nur der Identitätsnachweis des Serverbetreibers ist nicht gesichert. Aber das kann uns egal sein, da wir ja selber der Serverbetreiber sind. Also wenn man mit der hässlichen Browserwarnung leben kann, dann ist ein selbstsigniertes Zertifikat der bequemste Weg.

5. Hackversuche unterbinden

Hier geht es nicht einmal so sehr um den Einbrecher, der durch die garage ins Haus eindringen will. Für den ist es allemal einfacher, ein Fenster einzuschlagen, als Passwörter zu hacken. Aber wenn man vom Internet aus erreichbar ist, dann ist man auch Hackern ausgesetzt, die einfach zum Spass irgendwo Schaden anrichten wollen, und auch leistungsfähigen automatischen Hackprogrammen, die Computer erobern wollen, um sie dann zum Beispiel als Spamschleudern oder als Selver für illegale Inhalte zu missbrauchen. Daher:

  • Auf dem Raspberry läuft nur, was für den Garagenserver notwenidig ist. Was nicht vorhanden ist, kann auch nicht gehackt werden. Also kein VNC Zugang, keine Windows-Dateifreigabe etc.
  • Der Router gibt nur den einen Port weiter, der benötigt wird, also in unserem Beispiel 2017. Alle anderen Ports werden abgewiesen.
  • Der Server hat nur passwortgeschützte Funktionen. Ohne Passwort kann man gar nichts machen.
  • Wenn jemand ein falsches Passwort eingibt, wird eine Zwangspause eingelegt. Dies stört Programme, die vollautomatisch hunderte von Passwörtern pro Sekunde ausprobieren können. Wir verwenden folgende Methode: Beim ersten Fehlversuch beträgt die Pause 3 Sekunden, bei jedem weiteren Fehlversuch verdoppelt sie sich. Wer also fünf Mal ein falsches Passwort eingibt, muss sich vor dem nächsten Versuch eineinhalb Minuten gedulden, nach zehn Mal schon fast eine Stunde.

6. Zugriffe loggen

Es könnte immer noch sein, dass ein raffinierter Hacker einen Weg findet, an den wir nicht gedacht haben. Also muss der Server alles, was er tut in ein Logfile schreiben. Wenn man das ab und zu durchsieht, kann man verdächtige Aktionen bemerken.

Das Programm

Vorbereitung

Listings

/**
 *  Garagentor-Fernbedienung mit Raspberry Pi
 */

"use strict"

// Damit wir das Programm auf einem normalen PC ohne GPIO testen können. Wenn es auf dem echten Pi läuft, true setzen
const realpi = false
// Pin des piface für den output. pin 1 ist das linke Relais.
const output_pin = 1
// pin für den Schalter, der feststellt, ob das Garagentor offen ist
const input_pin = 0
// Dauer des simulierten Tastendrucks in Millisekunden
const time_to_push = 1200
// Dauer des Öffnungs/Schliessvorgangs des Tors
const time_to_run = 4000
// Aussperren bei falscher Passworteingabe
const lock_time = 3000

const fs = require('fs')
const https = require('https')
const express = require('express')
const nconf = require('nconf')
const hash = require('crypto-js/sha256')
const path = require('path')
const bodyParser = require('body-parser');
const salt = "um Hackern mit 'rainbow tables' die Suppe zu versalzen"
const favicon = require('serve-favicon');

nconf.file('users.json')
const app = express()
// Dieses Flag nutzen wir später, um den Server temporär inaktiv zu schalten.
let disabled = false;
// Dieses Flag zeigt an, dass das Garagentor gerade fährt
let running = false
// Hier sammeln wir schiefgegangene Login-Versuche
const failures = {}

app.set('view-cache', true)
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(express.static(path.join(__dirname, 'public')));

/*
 HTTPS-Server erstellen, damit Usernamen und Passwörter verschlüsselt übermittelt werden.
 Als Zertifikat kann man entweder ein self-signed certificate verwenden
 (openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes).
 Dann muss man allerdings damit leben, dass der Browser eine "Sicherheitswarnung" ausgibt.

 Oder man erstellt ein Zertifikat via letsencrypt, mit "sudo certbot certonly --manual" und kopiert
 "privkey.pem" nach "key.pem" und "fullchain.pem" nach "cert.pem".
 Dann muss man allerdings, das Zertifikat alle drei Monate erneuern, weil Letsencrypt keine länger gültigen
 Zertifikate ausstellt.

 Oder man kauft irgendwo ein kostenpflichtiges Zertifikat. Aber das scheint mir für einen Garagentorantrieb
 eigentlich zu aufwändig.
 */
https.createServer({
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
}, app).listen(2017)

// Auf einem echten Pi ist pfio auf das piface (https://www.npmjs.com/package/piface) gesetzt
// Auf einem anderen PC wird es einfach mit leeren Funktionen simuliert.
let pfio
if (realpi) {
  pfio = require('piface')
  pfio.init()
} else {
  let pinstate = 1
  pfio = {
    digital_write: function () {
    },
    digital_read: function (pin) {
      pinstate = pinstate ? 0 : 1
      return pinstate
    }
  }
}

/**
 * Expressjs sagen, dass die Views im Verzeichnis "views" zu finden sind, und dass
 * pug benötigt wird, um sie nach HTML zu konvertieren.
 */
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

/**
 * Expressjs mitteilen, dass wir json- und urlencoded Parameter im request body erwarten
 */
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
  extended: false
}));

/**
 * Endpoint für https://adresse:2017/
 *  Login-Screen anzeigen
 */
app.get("/", function (request, response) {
  response.render("garage")
})

/**
 Check ob der Server inaktiv geschaltet ist, oder das Garagentor gerade läuft.
 Wird vor jeden POST-Request ("/*") geschaltet.
 */
app.post("/*", function (req, resp, next) {
  if (disabled) {
    resp.render("answer", {
      message: "Der Server ist derzeit inaktiv geschaltet."
    })
  } else if (running) {
    resp.render("answer", {
      message: "Das Garagentor fährt gerade. Bitte warten."
    })
  } else {
    next()
  }
})

/**
 Check, ob der aktuelle Anwender gesperrt ist
 */
function isLocked(lockinfo) {
  if (lockinfo) {
    let now = new Date().getTime()
    let locked_until = lockinfo.time + (Math.pow(2, lockinfo.attempt) * lock_time)
    if (now < locked_until) {
      return true;
    }
  }
  return false;
}

/**
 * Aktuellen User sperren. Wenn er schon gesperrt ist, Sperrzeit verlängern
 * @param user user
 * @returns {number} Zahl an Sekunden der aktuellen Sperrzeit
 */
function setLock(user) {
  let now = new Date().getTime()
  let lockinf = failures[user] ? failures[user] : {"attempt": 0}
  lockinf["attempt"] += 1
  lockinf["time"] = now
  failures[user] = lockinf
  return Math.round((Math.pow(2, lockinf.attempt) * lock_time) / 1000)
}

/**
 * Zugriffstest;
 * wird vor alle https://server:2017/garage/... Anfragen POST requessts geschaltet
 * Wenn ein user gesperrt ist, dann prüfe, ob die Sperre abgelaufen ist. Wenn nein, abweisen
 * Sonst:
 * Wenn das Passwort korrekt ist, allfällige Sperren löschen
 * Wenn das Passwort falsch ist, Sperre erneut setzen, Dauer erhöhen (2^attempt*lock_time)
 */
app.post("/garage/*", function (request, response, next) {
  let user = request.body.username.toLocaleLowerCase()
  if (isLocked(failures[user])) {
    response.render("answer", {message: "Sperre wegen falscher Passworteingabe. Bitte etwas später nochmal versuchen."})
  } else {
    let password = JSON.stringify(hash(request.body.password + salt))
    let valid = nconf.get(user)
    if (valid && valid === password) {
      delete failures[user]
      next()
    } else {
      console.log("Loginfehler mit Name " + user + ", " + new Date())
      let secs = setLock(user)
      response.render("answer", {
        message: "Wer bist denn du??? Sperre " + secs + " Sekunden."
      })
    }
  }
})

/**
 * Zugriffstest für Admin-Funktionen.
 * Wird vor alle https://server:2017/adm/... GET requests geschaltet.
 * Gemeinsame Syntax: /adm/masterpassword/funktion/parameter.
 * Bei falschem Masterpasswort: Sperre setzen bzw. verlängern.
 */
app.get("/adm/:master/*", function (req, resp, next) {
  if (isLocked(failures['admin'])) {
    resp.render("answer", {message: "Sperre wegen falscher Passworteingabe. Bitte etwas später nochmal versuchen."})
  } else {
    let master = JSON.stringify(hash(req.params.master + salt))
    let stored = nconf.get("admin")
    if (!stored) {
      nconf.set("admin", master)
      stored = master
    }
    if (stored === master) {
      delete failures['admin']
      next()
    } else {
      console.log("Admin-Fehler" + req.params.username + ", " + new Date())
      let secs = setLock("admin")
      resp.render("answer", {
        message: "Insufficient rights. Wait " + secs + " seconds."
      })
    }
  }
})

/*
 Nach dem Login-Screen und erfolgreicher Passworteingabe: Aktuellen Zustand des Tors anzeigen.
 */
app.post("/garage/login", function (request, response) {
  let state = pfio.digital_read(input_pin)
  let action = state === 1 ? "Schliessen" : "Öffnen"
  response.render("confirm", {
    name: request.body.username,
    pwd: request.body.password,
    status: state === 1 ? "offen" : "geschlossen",
    action: action
  })

})

/*
 "Taste drücken".  Kontakt wird für time_to_push Millisekunden geschlossen. Für time_to_run Millisekunden werden
 keine weiteren Kommandos entgegengenommen, um dem Tor Zeit zu geben, ganz hoch oder runter zu fahren.
 */
app.post("/garage/action", function (request, response) {
  console.log("Garage " + request.body.action + ", " + new Date())
  running=true
  pfio.digital_write(output_pin, 1)
  setTimeout(function () {
    pfio.digital_write(output_pin, 0)
  }, time_to_push);
  setTimeout(function(){
    running=false
  },time_to_run)
  response.render("answer", {
    message: "Auftrag ausgeführt, " + request.body.username
  })
})

/**
 * Einen neuen User eintragen.
 */
app.get("/adm/:master/add/:username/:password", function (req, resp) {
  var user = req.params.username.toLocaleLowerCase()
  var password = JSON.stringify(hash(req.params['password'] + salt))
  nconf.set(user, password)
  nconf.save()
  resp.render("answer", {
    message: "Ok"
  })

})

/**
 * Einen User löschen.
 */
app.get("/adm/:master/remove/:username", function (req, resp) {
  nconf.set(req.params.username, undefined)
  nconf.save()
  resp.render("answer", {
    message: "ok"
  })
})

/**
 * Server inaktiv schalten.
 */
app.get("/adm/:master/disable", function (req, resp) {
  disabled = true
  resp.render("answer", {
    message: "disabled"
  })
})

/**
 * Server aktiv schalten.
 */
app.get("/adm/:master/enable", function (req, resp) {
  disabled = false
  resp.render("answer", {
    message: "enabled"
  })
})

/**
 * Passwort ändern
 */
app.post("/garage/chpwd",function(req,resp) {
  let npwd=req.body.npwd
  if(npwd && npwd.length>4 && /\d/.test(npwd) && /[a-zA-Z]/.test(npwd)) {
    nconf.set(req.body.username, JSON.stringify(hash(req.body.npwd + salt)))
    nconf.save()
    console.log(req.body.username + " changed password, " + new Date())
    resp.render("answer", {message: "Ab sofort gilt das neue Passwort"})
  }else{
    resp.render("answer",{message:"Das neue Passwort muss mindestens 5 Zeichen lang sein und sowohl Zahlen als auch Buchstaben enthalten."})
  }
})

/**
 * Logfile auslesen
 */
app.get("/adm/:master/log",function(req,resp){
  fs.readFile("../forever.log",function(err,data){
    if(err){
      resp.render("answer",{message:err})
    }else{
      var lines=data.toString().split("\n")
      resp.render("answer",{message:"<p>"+lines.join("<br>")+"</p>"})
    }
  })
})

Die Views

Views sind das, was der Server an den Browser zurückliefert, also das, was man letztlich zu Gesicht bekommt. In diesem Beispiel sind alle Views in der Beschreibungssprache “pug” verfasst, die vor der Ausgabe vom Server nach HTML umgewandelt werden.

layout.pug

Das ist der gemeinsame Rahmen um alle Views. Die jeweils spezifische View wird bei “content” eingesetzt. Hier wird das “Materialize”- Styling geladen und festgelegt, dass die Seite auf kleinen Bildschirmen vergrössert dargestellt werden soll. Ausserdem deklarieren wir, wo sich die Icons für iOS (apple-touch-icon) un Android (manifest.json) befinden, falls der User eine Verknüpfung auf dem Handy-Desktop anlegen will.

doctype html
html
  head
    title= "Unsere Garage"
    link(rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/css/materialize.min.css")
    link(href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet")
    meta(content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=2, user-scalable=0" name="viewport")
    link(rel="apple-touch-icon" href="/garage-57x57.png")
    link(rel="manifest" href="/manifest.json")
  body
    block content
    script(src="simple.js")

garage.pug

Das ist die Einstiegsseite, die auf den Aufruf von “/” gezeigt wird.

extends layout.pug
block content
  .container
    .row
      h1.col.s12 Garage
      form.col.s12(method="post" action="/garage/login")
        .row
          .col.s12
            .input-field.col.s12
              i.material-icons.prefix account_circle
              input(type="text" name="username" placeholder="Username" id="uname")
            .input-field.col.s8
              i.material-icons.prefix vpn_key
              input(type="password" name="password" placeholder="Passwort" id="pwd")
            .btn.btn-flat.col.s4(onClick="enterPwd()") ändern...

            button.btn.waves-effect.waves-light.col.s12(type="submit" name="action") Senden
              i.material-icons.right send

answer.pug

Mit dieser Seite gibt der Server seine Antworten zurück. In der Variablen “message” befindet sich der Text der Antwort.

extends layout.pug
block content
  .container
    .row
      p.col.s12(style="font-size:large") Die Garage sagt: !{message}
      a.col.s3.btn.waves-effect.waves-light(href="/") Zurück
      a.col.s3.offset-s1.btn.waves-effect.waves-light(href="http://www.google.ch") Irgendwo anders hin

confirm.pug

Hier wird dem user nach erfolgtem Login die Möglichkeit gegeben, das Tor zu öffnen oder zu schliessen. In der Variable “Status” steht, ob die garage derzeit offen oder zu ist, in “action” steht, was der User jetzt tun kann (schliessen oder öffnen).

extends layout.pug
block content
  .container
    .row
      h3(style="color:blue") Willkommen, #{name}
      p Die Garage ist <b>#{status}</b>
      form.col.s12(method="post" action="/garage/action")
        input(type="hidden" name="username" value=name)
        input(type="hidden" name="password" value=pwd)
        input(type="hidden" name="action" value=action)
        button.col.s3.btn.waves-effect.waves-light(type="submit" name="action") #{action}
        a.col.s3.offset-s1.btn.waves-effect.waves-li

16.3.2017 - Garagentor-Fernbedienung

Zur zweiten Version

Eine defekte Garagentor-Fernbedienung gab den Ausschlag: Da unser Torantrieb schon das biblische-Alter von 15 Jahren hat, war ein Ersatzteil natürlich nicht mehr zu bekommen. Es gab nur noch Universalfernbedienungen für um die 100.-. Da kommt man ins Grübeln. Der Materialwert einer solchen Fernbedieung dürfte unter 10.- sein.

Ausserdem gibt es noch ein zweites, davon unabhängiges Problem: Wir haben nie genug Schlüssel, und die Jungs vergessen ihren manchmal, wenn sie weg gehen. Aber was wir alle immer dabei haben, ist das Smartphone. Nun denn, die Idee, das Garagentor mit dem Smartphone zu öffnen ist, zugegeben, nicht ganz neu, aber es geht viel billiger und einfacher, als gedacht:

Innen hat der Torantrieb einen Taster, der einfach einen Kontakt schliesst und den Torantrieb so einschaltet. Mit diesem Taster kann man das Tor von Hand bedienen, wenn man sich in der Garage befindet. Das Tor fährt bei jedem Druck auf den Taster abwechselnd runter oder hoch, bzw. bleibt stehen, wenn es gerade am Fahren ist.

Natürlich kann man so einen Kontakt auch mit einem Relais, statt mit einem Handtaster schliessen.

Und ein Relais kann man mit einer beliebigen Elektronikschaltung steuern. Oder mit einem GPIO Ausgang eines Raspberry Pi. Und der Raspberry Pi wiederum kann via Netzwerk oder via Bluethooth mit der Umwelt drahtlos in Kontakt treten.

Also ist die Idee sehr einfach: Auf dem Raspi läuft ein REST-Server. Sobald man im häuslichen WLAN eingebucht ist, kann man mit dem Browser des Handys auf diesen REST-Server zugreifen, und einen GPIO-Pin schalten, der wiederum ein Relais ansteuert, welches den Kontakt schliesst, der das Garagentor losfahren oder stoppen lässt. Da das WLAN mit WPA2 gesichert ist, und da der REST-Server nur vom WLAN aus erreichbar ist, braucht man sich um die Absicherung nicht allzu viele Gedanken zu machen. Eine einfache Passwortabfrage genügt. Und es wird dann geradezu trivial, Besuchern einen temporären Schlüssel zu geben: Man braucht sie nur auf dem Server einzutragen und am Ende wieder zu löschen.

Software

Da ich eher ein Software-Mensch bin, mache ich mich zuerst an den Server:

  • Einen Raspi aus dem keller holen, es ist ein B+. Zwar langsamer, als die aktuellen 2er und 3er Versionen, aber für unsere Zwecke sollte es genügen.
  • NOOBS herunterladen und auf die SD-Karte aufspielen, für silentinstall konfigurieren, da ich keine Lust habe, den Raspi an Tastatur und Bildschirm anzuschliessen.
  • per ssh mit dem Raspi verbinden, und das Passwort ändern. Dafür sorgen, dass er beim Start nicht in den grafischen Modus hochfährt, um nicht noch mehr von der spärlichen Leistung zu verbraten.
  • Wlan-Stick anstecken und konfigurieren.
  • NodeJS und NPM installieren (was weniger einfach ist, als gedacht, weil die in Raspbian mitgelieferte NodeJS-Version hoffnungslos veraltet ist.)

Einschub: NodeJS Version 7.x auf einem Raspberry Pi mit ARM6 installieren:

  sudo apt-get remove nodejs
  cd
  mkdir apps
  cd apps
  wget https://nodejs.org/dist/v7.7.3/node-v7.7.3-linux-armv6l.tar.xz
  tar -xf node-v7.7.3-linux-armv6l.tar.xz
  mv node-v7.7.3-linux-armv6l node7
  sudo ln -s /home/pi/apps/node7/bin/node /usr/bin/node
  sudo ln -s /home/pi/apps/node7/bin/npm /usr/bin/npm
  echo export PATH=$PATH:/home/pi/apps/node7/bin >>../.profile
  • Ein Projekt aufsetzen:

Im Terminal:

  mkdir garage
  cd garage
  npm init
  npm install
  npm install --save express nconf crypto-js

Dann kann man einen trivialen REST-Server erstellen:

    // garage.js
    var express=require('express')
    var nconf=require('nconf')
    var hash=require('crypto-js/sha256')
    nconf.file('users.json')
    var salt="um Hackern mit 'rainbow tables' die Suppe zu versalzen"

    var app=express()

    app.get("/garage/:user/:password",function(request,response){
      var user=JSON.stringify(hash(request.params['user']+salt))
      var password=JSON.stringify(hash(request.params['password']+salt))
      var valid=nconf.get(user)
      if(valid && valid == password){
        console.log("Die Garage öffnet sich!")
        response.send("Willkommen, "+request.params['user'])
      }else{
        response.send("Wer bist denn du???")
      }
    })

    app.listen(3000,function(){
      console.log("Garagenserver läuft an port 3000")
    })

Mit node garage.js kann man das Ding starten. Dann können wir mit http://raspi:3000/garage/hans/peter zugreifen und erhalten immer “Wer bist denn du???” zurück, da ja noch kein User ‘hans’ mit Paswort ‘peter’ bekannt ist.

Das wollen wir nun ändern:

Damit keine Usernamen und Passwörter im Klartext in users.json stehen, werden sie gehasht. Allerdings ist es dann nicht ganz einfach, sie in die Konfiguration zu kriegen. Da dies ein kleines Projekt mit einer absehbaren Zahl von Usern ist, machen wir uns das sehr einfach: Es gibt einen zweiten REST-Endpoint zum Eintragen von Usern, den wir vor der app.listen()-Zeile einfügen:

    app.get("/adduser/:username/:password",function(req,resp){
      var user=JSON.stringify(hash(req.params['username']+salt))
      var password=JSON.stringify(hash(req.params['password']+salt))
        nconf.set(user,password)
        nconf.save()
        resp.send("Ok")
    })

Jetzt können wir höchst simpel mit http://raspi:3000/adduser/hans/peter unseren ersten User eintragen. Danach sollte http://raspi:3000/garage/hans/peter eine freundliche Willkommensmeldung zurückliefern. (Über die Tatsache, dass die Nachricht “Die Garage öffnet sich!” eine klassische Fake-News ist, wollen wir jetzt mal gnädig hinwegsehen.)

Natürlich wäre das nun doch allzu unsicher: So könnte sich ja jeder selbst als zugelassener User eintragen :) Der Endpunkt “adduser” wird deshalb im Normalbetrieb auskommentiert und nur dann “scharf” geschaltet, wenn der Admin einen neuen User zufügen will. Wie gesagt, wir sprechen von einem System mit einer sehr überschaubaren Zahl von Usern. Wem das zu unbequem ist, der kann ja einen Admin-Zugang erstellen, um User zu verwalten.

Über die Klartextübermittlung von Usernamen und Passwörtern braucht man sich übrigens keine Gedanken zu machen, sofern man selbst die Kontrolle über den Router hat: Das mit WPA2 gesicherte WLAN verschlüsselt alle übertragenen Daten.

Damit ist der Software-Teil des Garagenentoröffners fertig.

Hardware

Für den Hardware-Teil muss man wissen:

Die GPIOs des Raspberry bringen grade mal 3.3 Volt mit äusserst spärlichen Milliampères auf die Beine. Wenn man da herzlos ein handelsübliches Relais anschliesst, brennt der raspi glatt durch. Oder das Relais reagiert nicht. Oder was auch immer (ich bin ja kein Elektroniker). Jedenfalls wird es nicht klappen.

Man braucht also eine Steuerelektronik. Hinter diesem hochtrabenden Namen verbirgt sich ein Transistor, eine Diode und ein Widerstand. Für jemandem mit zwei linken Händen wie mich kann das schon limitierend sein. Aber viel kann ja nicht passieren. Schlimmstenfalls verbrennt man beim Löten den Transistor oder die Finger. Natürlich kann man auch fertige Relaismodule für den Raspberry kaufen, aber wir sind ja keine Warmduscher.

Hier kann man zum Beispiel nachlesen, wie man so etwas aufbaut.

Zusammenbringen

Nun weiss unser Server allerdings noch nicht, wie er den passenden GPIO Pin schalten kann. Wie fast immer bei NodeJS ist guter Rat billig: npm install --save rpio.

Dann fügen wir im Kopf von ‘garage.js’ folgende Zeilen hinzu:

var rpio=require('rpio')
rpio.open(12,rpio.OUTPUT,rpio.LOW)  // definierten Ausgangszustand erstellen

Und ändern dann den Erfolg-Teil des garage-Endpoints wie folgt:

    if(valid && valid == password){
      console.log("Das Garagentor tut etwas!")
      rpio.write(12,rpio.HIGH)
      rpio.sleep(1)
      rpio.write(12,rpio.LOW)
      response.send("Auftrag ausgeführt, "+request.params['user'])
    }

Also: Wir “drücken den Knopf” eine Sekunde lang und lassen ihn dann wieder los. Wenn das Garagentor sich zuletzt geöffnet hat, dann schliesst es sich jetzt, wenn es sich gerade bewegt, dann bleibt es stehen, und wenn es sich zuletzt geschlossen hatte, dann geht es jetzt auf.

Ziemlich einfach, nicht wahr? Und die Kosten? Ein Raspberry Pi B+ kostet um die 35.-, Gehäuse 15.-, Netzteil 15.-, das Relais 5.- und die restlichen Bauteile sind im Rappenbereich. Aber natürlich darf man die Zeit nicht als Arbeitszeit nicht rechnen :)

Natürlich kann und sollte man am Programm, speziell am UI, noch einiges verfeinern, aber das sei als Übung der interessierten Leserschaft überlassen :)

2.3.2017 - asynchroner code

Asynchrones Programmieren erhält in Zeiten der Web-Apps immer grössere Bedeutung. Nehmen wir folgende, unschuldig aussehende Funktion, die irgendwelche Daten holt und zurückliefert (Pseudocode):

  function fetchData(){
      a=fetchSomething()
      /* irgendwelche Zwischenschritte */
      return a
    }

    Hauptprogamm(){
      coolData=fetchData()
      display(coolData)
      /* Mach weiter, update UI */
    }

So weit, so einfach und klar. Das funktioniert super, wenn fetchSomething() ausreichend schnell ist. Doch was, wenn diese Zugriffe über eine langsame Internet-Verbindung gehen? Dann scheint das Hauptprogramm bei fetchData() für den Anwender eingefroren zu sein, während es auf die Anlieferung der Daten wartet.

Um das zu verhindern, entwickelte man die Technik der asynchronen Programmierung. Da JavaScript als klassische Webapp-Sprache, die typischerweise single threaded im Browser abläuft, besonders stark mit diesem Problem konfrontiert ist, ist asynchrone Programmierung in JavaScipt auch am weitesten Verbreitet.

Wir schreiben den Code also so um:

    function fetchData(callback){
      a=fetchSomething()
      /* irgendwelche Zwischenschritte */
      callback(undefined, a)
      /* bzw, wenn ein Fehler passiert wäre; */
      callback(error,undefined)
    }

    Hauptprogramm(){
      fetchData(doDisplay)
      /* erledige andere Dinge, halte das UI am Leben */
    }

    function doDisplay(error,coolData){
      if(error){
        // Ooops!
      }else{
        display(coolData)
      }
    }

Viel besser. Die Funktion fetchData() kehrt sofort zurück und das Hauptprogramm kann normal weiter laufen. Wenn das Resultat da ist, wird die beim Aufruf angegebene callback-Funktion “doDisplay” mit dem Resultat aufgerufen und die Anwendung kann sich dann um die Wieterverarbeitung kümmern. Per Konvention ist bei diesen “NodeJS-Style” Callbacks der erste Parameter immer ein Fehler-Objekt oder eine Fehlermeldung.

Man kann das mit anonymen Funktionen noch ein wenig übersichtlicher gestalten:

    Hauptprogramm(){
      fetchData(function(error,coolData){
        if(!error){
          display(coolData)
        }
      })
    }

Das ist funktional exakt dasselbe, aber einfacher lesbar. Und mit dieser Programmiertechnik haben Web-Entwickler seit vielen Jahren problemlos das Web 2.0 aufgebaut. Allerdings wurden mit zunehmender Komplexität der Web-Apps auch die Klagen über die “Callback-Hölle” immer lauter. Was ist damit gemeint?

Nehmen wir mal an, wir benötigen nicht nur ein, sondern mehrere externe Datenquellen zum Aufbau unserer Anzeige. Nennen wir dieObjekte, die wir benötigen: A, B und C. Das würden wir so programmieren:

       function fetchABC(callback){
        fetchA(function(A){
          /* Irgendwelche Zwischenschritte */
          fetchB(function(B){
            /* irgendwelche Zwischenschritte */
            /* oops da ist ein Fehler passiert */
            fetchC(function(C){
              /* Aufbereitung der Daten */
              callback(coolResult)
              })
            })
          })
       }

      Hauptprogramm(){
        fetchABC(function(error,coolData){
            display(coolData)
          })
        }

Es gibt hier zwei Probleme:

  • Der Code wird zu Spaghetticode und zunehemd schlechter lesbar

  • Fehlerbehandlung wird schwierig. Was soll man mit dem Fehler anstellen, der in irgendeiner der Verschachtelungen auftritt? Mit der Behandlung des Fehlers auf jeder Stufe wird der Code noch schlechter lesbar. Häufig gelingt das auch nicht korrekt, und das Programm stürzt einfach ab, wenn tief unten in der Verschachtelung ein Fehler auftritt.

Die aktuelle JavaScript Version, die von neueren Chrome- und Firefox Browsern auch schon unterstützt wird, bringt Abhilfe. Zuerst noch zur Klärung: Diese JavaScript Version hört auf den Namen ES 6, aber auch auf den Namen ES 2015, ECMA6 und ECMA 2015, was manchmal ein wenig verwirrlich ist.

Wie auch immer. Die Idee ist: Anstatt die länger dauernde Aufgabe zu lösen, und dann ein Callback mit der Lösung aufzurufen, liefern wir sofort ein Objekt zurück, welches nicht die Lösung ist, sondern welches verspricht, die Lösung irgendwann zu liefern.

      Hauptprogramm(){
        fetchABC().then(coolData=>{
            display(coolData)
          }).catch(error=>{
            display("oops: "+error)
            })
      }

fetchABC() hat eine Promise zurückgeliefert (wir kommen weiter unten darauf, wie das geht), und der Programmteil nach “then” ist die Einlösung des Versprechens. Elegant ist der “catch”-Teil: Wenn das Versprechen nicht erfüllt werden konnte (ja, liebe Kinder, Versprechen werden manchmal gebrochen!), dann wird dieser Teil ausgeführt. Das Schöne ist, egal in welcher Verschachtelungstiefe dir Ursache war, das Programm gelangt immer an diese Stelle zur Fehlerbehandlung.

Wie wurde nun also diese Promise hergestellt? Wir brauchen drei Datenelemente A, B und C, welche von verschiedenen langsamen, aber voneinander unabhängigen Quellen stammen. Da die Quellen unabhängig sind, können wir alle drei Abfragen quasi-gleichzeitig abschicken, wenn wir nur sicherstellen, dass wir alle drei Resultate haben, bevor wir weiter machen. Das war mit den bisher gezeigten Techniken nicht so einfach zu machen. Jetzt ist es fast trivial:

      function fetchABC(){
        promiseA=fetchA()
        promiseB=fetchB()
        promiseC=fetchC()
        return Promise.all([promiseA,promiseB,promiseC]).then(resolve=>{
          /* Allfällig Verarbeitungsschritte */
          })
      }

Die Funktion gibt eine Promise zurück, welche erst dann eingelöst wird, wenn alle drei Vorbedingungs-Promises erfüllt sind.
Das ist alles.

Wirklich schön wird es nun mit dem nächsten Schritt, der im Grund nur eine Syntaxänderung ist, aber asynchrone Programmierung gleich einfach macht, wie synchrone Programmierung:

      async function fetch_and_show(){
        let coolData=await fetchABC()
        display(coolData)
      }

Hinter den Kulissen wird hier immer noch mit Promises hantiert, aber die ganzen komplizierten Konstrukte sind aus dem Quellcode verschwunden. Die Schlüsselwörter async und await können mit Typescript ab Version 2 und mit JavaScript ab Version ECMA6 eingesetzt werden.

Ach ja, aufmerksame Leser/innen werden vielleicht monieren, dass ich eigentlich immer noch nicht gezeigt habe, wie man eine Promise herstellt, zum Beispiel die oben erwähnte PromiseA.

Man kann das auf drei verschiedene Arten tun:

1.: Trivial:

Die Funktion ruft ihrerseits eine Funktion auf, die eine Promise zurückliefert, zum Beispiel eine Funktion eines asynchrones, Promise-enabled Datenbank- oder HTTP-Treibers.

2.: Promise explizit erstellen:

    function fetchA(){
        return new Promise((resolve,reject)=>{
            let result=getTheDataFromRemoteWhichtakesSomeTime()
            if(allesHatGeklappt){
                resolve(result)
            }else{
                reject(new Error("Ups, da ging was schief")
            }
        })
    }

Wir erzeugen eine neue Promise, die Methoden für die Erfüllung oder den Bruch des Versprechens enthält und liefern diese Promise sofort zurück.

3.: Promise implizit erstellen

    async function fetchA(){
        return getTheDataFromRemoteWhichtakesSomeTime()
    }

Jede mit “async” gekennzeichnete Funktion liefert implizit eine Promise zurück. Die Promise hat -ebenfalls implizit- einen resolve-Zweig, der den deklarierten return-Wert der Funktion liefert, und einen reject-Zweig, der bei jeder während der Ausführung geworfenen Exception mit diesem Exception Object aufgerufen wird.