diff --git a/cli.cpp b/cli.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..40257c97e970feb82d26accaa8efd41e8f1d16ad
--- /dev/null
+++ b/cli.cpp
@@ -0,0 +1,131 @@
+
+#include "cli.h"
+
+void Cli::resetNextWord ()
+{
+    _currentWordIndex = -1;
+    _previousWordLength = 0;
+}
+
+const char* Cli::findNextWord ()
+{
+    if (_currentWordIndex >= 0)
+        // skip current word
+        while (_currentWordIndex < (int)_currentInput.length())
+        {
+            if (_currentInput[_currentWordIndex] == 32)
+                break;
+            _currentWordIndex++;
+        }
+    else
+        _currentWordIndex = 0;
+    
+    size_t _previousWordLength = 0;
+    // skip spaces
+    while (_currentWordIndex < (int)_currentInput.length())
+    {
+        if (_currentInput[_currentWordIndex] != 32)
+            break;
+        _currentWordIndex++;
+        _previousWordLength++;
+    }
+    
+    const char* ret = _currentWordIndex < (int)_currentInput.length()? _currentInput.c_str() + _currentWordIndex: nullptr;
+    //if (ret)
+    //    Serial.printf("# found new start of word '%s'\n", ret);
+    return ret;
+}
+
+void Cli::copyNextToTemp ()
+{
+    findNextWord();
+    int start = _currentWordIndex;
+    findNextWord();
+    if (start < _currentWordIndex)
+        _temp = _currentInput.substring(start, _currentWordIndex);
+    else
+        _temp.clear();
+}
+
+bool Cli::kw (const __FlashStringHelper* keyWord, const char* input)
+{
+    // return true if input matches keyWord
+    auto len = strlen_P((PGM_P)keyWord);
+    return len && strncasecmp_P((PGM_P)keyWord, input, len) == 0;
+}
+
+bool Cli::kw (const __FlashStringHelper* keyWord)
+{
+    return kw(keyWord, _currentInput.c_str() + _currentWordIndex);
+}
+
+void Cli::syntax ()
+{
+    syntax(_currentInput.c_str() + _currentWordIndex);
+}
+
+void Cli::syntaxFull ()
+{
+    syntax((const char*)nullptr);
+}
+
+void Cli::syntax (const __FlashStringHelper* cmd)
+{
+    syntax((PGM_P)cmd);
+}
+
+void Cli::syntax (const char* cmd)
+{
+    if (!cmd || kw(F("AT"), cmd))   Serial.printf("# CLI: AT -> OK\n");
+    if (!cmd || kw(F("HELP"), cmd)) Serial.printf("# CLI: HELP [CMD]\n");
+    if (!cmd || kw(F("RAT"), cmd))  Serial.printf("# CLI: RAT [ C | I ] [ <rate> [ <unit>(UM/MM/UH/MH) ] ]\n");
+    if (!cmd || kw(F("VOL"), cmd))  Serial.printf("# CLI: VOL [ <vol> | <unit>(UL/ML) ]\n");
+}
+
+void Cli::loop (Stream& input)
+{
+    while (true)
+    {
+        if (!input.available())
+            return;
+        int c = input.read();
+        if (c == 13)
+            break;
+        if (c >= 32 && c <= 127)
+            _currentInput += (char)c;
+    }
+    
+    resetNextWord();
+    
+    if (findNextWord())
+    {
+        if (kw(F("AT")))
+        {
+            Serial.printf("OK\n");
+        }
+        else if (kw(F("HELP")))
+        {
+            copyNextToTemp();
+            if (_temp.length())
+                syntax(_temp.c_str());
+            else
+                syntaxFull();
+        }
+        else if (kw(F("RAT")))
+        {
+            copyNextToTemp();
+        }
+        else if (kw(F("VOL")))
+        {
+        }
+        else
+        {
+            Serial.printf("# CLI: invalid command '%s'\n", _currentInput.c_str() + _currentWordIndex);
+        }
+    }
+    
+    if (findNextWord())
+        Serial.printf("# CLI: garbage at end of line: '%s'\n", _currentInput.c_str() + _currentWordIndex);
+    resetNextWord();
+    _currentInput.clear();
+}
diff --git a/cli.h b/cli.h
new file mode 100644
index 0000000000000000000000000000000000000000..bdd52b4d84a421f5c39578a9e0536f520ef06a82
--- /dev/null
+++ b/cli.h
@@ -0,0 +1,39 @@
+
+#pragma once
+
+#include <Arduino.h>
+
+#include "syringe.h"
+
+class Cli
+{
+private:
+
+    Syringe& syringe;
+
+    String _currentInput;
+    String _temp;
+    int _currentWordIndex;
+    int _previousWordLength;
+
+    void resetNextWord ();
+    const char* findNextWord ();
+    void copyNextToTemp ();
+    bool kw (const __FlashStringHelper* keyWord, const char* input);
+    bool kw (const __FlashStringHelper* keyWord);
+    void syntax ();
+    void syntaxFull ();
+    void syntax (const __FlashStringHelper* cmd);
+    void syntax (const char* cmd);
+
+public:
+
+    Cli (Syringe& syringe, int bufferLen = 32): syringe(syringe)
+    {
+        _currentInput.reserve(bufferLen);
+        _temp.reserve(bufferLen);
+        resetNextWord();
+    }
+
+    void loop (Stream& input);
+};
diff --git a/host_gui/make.sh b/host_gui/make.sh
index 36a876f3e7075b901c9ecf221333d6a4c26563ec..5cbfdac1256461e83a4792e09e6a395101d54f8a 100755
--- a/host_gui/make.sh
+++ b/host_gui/make.sh
@@ -3,19 +3,16 @@
 pwd=$(pwd)
 cd ${ESP8266ARDUINO}/tests/host
 make FORCE32=0 ssl
-make -j 10 FORCE32=0 D=1 USERCFLAGS="-I ${ARDUINOLIB}/emuAsync/replacement" ULIBDIRS=${ARDUINOLIB}/emuAsync:${ARDUINOLIB}/ESPUI:${ARDUINOLIB}/ArduinoJson:${ARDUINOLIB}/arduinoWebSockets ${pwd}/../pousseseringue-arduino
+make -j 10 FORCE32=0 USERCFLAGS="-I ${ARDUINOLIB}/emuAsync/replacement" ULIBDIRS=${ARDUINOLIB}/emuAsync:${ARDUINOLIB}/ESPUI:${ARDUINOLIB}/ArduinoJson:${ARDUINOLIB}/arduinoWebSockets ${pwd}/../pousseseringue-arduino
 
-(./bin/pousseseringue-arduino/pousseseringue-arduino -b "$@" 2>&1 | grep -v '^http-server loop: conn=') & pid=$!
-trap "kill ${pid}" EXIT INT
+(
+    true '----------------------------------------'
+    true '----------- starting firefox -----------'
+    true '----------------------------------------'
+    sleep 5
+    firefox -new-window http://localhost:9080
+) &
 
-sleep 1
-true '----------------------------------------'
-true '----------- starting firefox -----------'
-true '----------------------------------------'
-sleep 4
-firefox -new-window http://localhost:9080
+./bin/pousseseringue-arduino/pousseseringue-arduino "$@"
 
-while true; do
-    true '^C to quit'
-    read junk
-done
+stty sane
diff --git a/pousseseringue-arduino.cpp b/pousseseringue-arduino.cpp
index ed4a1eb9823fe17b28db09610cd6782a1272e4ec..3ca8f5db320778b1d626ea2fcbf5a2506f5e9738 100644
--- a/pousseseringue-arduino.cpp
+++ b/pousseseringue-arduino.cpp
@@ -56,6 +56,7 @@
 #endif // !CORE_MOCK
 #include "Debouncer.h"          // local, debouncer, short counter, long detector
 #include "syringe.h"
+#include "cli.h"
 #include "common.h"
 
 
@@ -93,6 +94,7 @@ int rpming = 0; // -1, 0 or +1
 
 // single syringe global instance
 Syringe syringe;
+Cli console(syringe);
 
 const char* oledMode ()
 {
@@ -320,7 +322,9 @@ void loop()
     }
     stepper.run();
     #endif // !CORE_MOCK
+
+    console.loop(Serial);
     webLoop();
     
-    dnsServer.processNextRequest(); // AP redirection
+    dnsServer.processNextRequest(); // access-point redirection
 }
diff --git a/pousseseringue-arduino.ino b/pousseseringue-arduino.ino
index e70951cbf584db88608da47144d6c847bb6d8936..8629455d7b9b3a8250b3441bdd4c7442fc2884e4 100644
--- a/pousseseringue-arduino.ino
+++ b/pousseseringue-arduino.ino
@@ -7,4 +7,5 @@
   #include "web.cpp"
   #include "syringe.cpp"
   #include "motor.cpp"
+  #include "cli.cpp"
 #endif // !CORE_MOCK
diff --git a/syringe.cpp b/syringe.cpp
index 7a831b35bb0534fb3399e8761f9798699438bffd..429389d4c216888c695d9c58f3dffc441b155aa6 100644
--- a/syringe.cpp
+++ b/syringe.cpp
@@ -13,7 +13,7 @@ float Syringe::volumeToDistance(float volume)
 	return distance;
 }
 
-void Syringe::configureSyringe(Syringe_configuration_t config)
+void Syringe::configureSyringe(const Syringe_configuration_t& config)
 {
 	this->current_configuration = config;
 	this->piston_surface = M_PI * pow(config.diameter,2) / 4.;
diff --git a/syringe.h b/syringe.h
index 49dea578113e5d65ed020218cd2830f5d604ddd5..caca6dd1f862c2594b9be6a571f8710417d7f49c 100644
--- a/syringe.h
+++ b/syringe.h
@@ -22,7 +22,7 @@ private:
 
 public:
 	// Configuration
-	void configureSyringe(Syringe_configuration_t config);
+	void configureSyringe(const Syringe_configuration_t& config);
 	void setInitialContent(float initial_volume);
 
 	// Actions