diff --git a/ampel-firmware/ampel-firmware.ino b/ampel-firmware/ampel-firmware.ino
index 8d04504665e014bd52e1d66f38c0e14864d2fc9e..39f6b0c3b9ede61f313a190e4c292eb6c9bde408 100644
--- a/ampel-firmware/ampel-firmware.ino
+++ b/ampel-firmware/ampel-firmware.ino
@@ -56,17 +56,6 @@
  * and define your credentials and parameters in 'config.h'.
  */
 
-/*****************************************************************
- * PreInit                                                       *
- *****************************************************************/
-void preinit() {
-#if !defined(AMPEL_WIFI) && defined(ESP8266)
-  // WiFi would be initialized otherwise (on ESP8266), even if unused.
-  // see https://github.com/esp8266/Arduino/issues/2111#issuecomment-224251391
-  ESP8266WiFiClass::preinitWiFiOff();
-#endif
-}
-
 /*****************************************************************
  * Setup                                                         *
  *****************************************************************/
@@ -78,14 +67,17 @@ void setup() {
 
   pinMode(0, INPUT); // Flash button (used for forced calibration)
 
-  led_effects::setupRing();
-
-  sensor::initialize();
-
+  Serial.println();
   Serial.print(F("Sensor ID: "));
   Serial.println(ampel.sensorId);
   Serial.print(F("Board    : "));
   Serial.println(ampel.board);
+  Serial.print(F("Firmware : "));
+  Serial.println(ampel.version);
+
+  led_effects::setupRing();
+
+  sensor::initialize();
 
 #ifdef AMPEL_CSV
   csv_writer::initialize(ampel.sensorId);
@@ -132,7 +124,6 @@ void checkSerialInput();
 /*****************************************************************
  * Main loop                                                     *
  *****************************************************************/
-
 void loop() {
 #if defined(AMPEL_LORAWAN) && defined(ESP32)
   //LMIC Library seems to be very sensitive to timing issues, so run it first.
diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp
index 5795697b05eb325ae2f4e8822f5f994bcffa043a..e68b2ea43514e685332229e33eab2f0ea54d4a57 100644
--- a/ampel-firmware/co2_sensor.cpp
+++ b/ampel-firmware/co2_sensor.cpp
@@ -1,12 +1,16 @@
 #include "co2_sensor.h"
 
 namespace config {
-  // Values should be defined in config.h
-  uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor)
+  // UPPERCASE values should be defined in config.h
+  uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor).
   const uint16_t altitude_above_sea_level = ALTITUDE_ABOVE_SEA_LEVEL; // [m]
   uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm]
-  int8_t max_deviation_during_calibration = 30; // [ppm]
-  int8_t enough_stable_measurements = 60;
+  const uint16_t measurement_timestep_bootup = 5; // [s] Measurement timestep during acclimatization.
+  const uint8_t max_deviation_during_bootup = 20; // [%]
+  const int8_t max_deviation_during_calibration = 30; // [ppm]
+  const int16_t timestep_during_calibration = 10; // [s] WARNING: Measurements can be unreliable for timesteps shorter than 10s.
+  const int8_t stable_measurements_before_calibration = 120 / timestep_during_calibration; // [-] Stable measurements during at least 2 minutes.
+
 #ifdef TEMPERATURE_OFFSET
   // Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be offset?
   // NOTE: Sign isn't relevant. The returned temperature will always be shifted down.
@@ -28,31 +32,27 @@ namespace sensor {
 
   /**
    * Define sensor states
-   * INITIAL -> initial state
-   * BOOTUP -> state after initializing the sensor, i.e. after scd.begin()
+   * BOOTUP -> initial state, until first >0 ppm values are returned
    * READY -> sensor does output valid information (> 0 ppm) and no other condition takes place
    * NEEDS_CALIBRATION -> sensor measurements are too low (< 250 ppm)
-   * PREPARE_CALIBRATION -> forced calibration was initiated, waiting for stable measurements
-   * CALIBRATION -> the sensor does calibrate itself
+   * PREPARE_CALIBRATION_UNSTABLE -> forced calibration was initiated, last measurements were too far apart
+   * PREPARE_CALIBRATION_STABLE -> forced calibration was initiated, last measurements were close to each others
    */
   enum state {
-    INITIAL,
     BOOTUP,
     READY,
     NEEDS_CALIBRATION,
     PREPARE_CALIBRATION_UNSTABLE,
-    PREPARE_CALIBRATION_STABLE,
-    CALIBRATION
+    PREPARE_CALIBRATION_STABLE
   };
   const char *state_names[] = {
-      "INITIAL",
       "BOOTUP",
       "READY",
       "NEEDS_CALIBRATION",
       "PREPARE_CALIBRATION_UNSTABLE",
-      "PREPARE_CALIBRATION_STABLE",
-      "CALIBRATION" };
-  state current_state = INITIAL;
+      "PREPARE_CALIBRATION_STABLE" };
+
+  state current_state = BOOTUP;
   void switchState(state);
 
   void initialize() {
@@ -78,47 +78,51 @@ namespace sensor {
       ESP.restart();
     }
 
-    switchState(BOOTUP);
-
-    // SCD30 has its own timer.
-    //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
-    Serial.print(F("Setting SCD30 timestep to "));
-    Serial.print(config::measurement_timestep);
-    Serial.println(" s.");
-    scd30.setMeasurementInterval(config::measurement_timestep); // [s]
+    // Changes of the SCD30's measurement timestep do not come into effect
+    // before the next measurement takes place. That means that after a hard reset
+    // of the ESP the SCD30 sometimes needs a long time until switching back to 2 s
+    // for acclimatization. Resetting it after startup seems to fix this behaviour.
+    scd30.reset();
 
     Serial.print(F("Setting temperature offset to -"));
     Serial.print(abs(config::temperature_offset));
-    Serial.println(" K.");
+    Serial.println(F(" K."));
     scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down.
     delay(100);
 
     Serial.print(F("Temperature offset is : -"));
     Serial.print(scd30.getTemperatureOffset());
-    Serial.println(" K");
+    Serial.println(F(" K"));
 
     Serial.print(F("Auto-calibration is "));
     Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
 
-    sensor_console::defineIntCommand("co2", setCO2forDebugging, F(" 1500 (Sets co2 level, for debugging purposes)"));
-    sensor_console::defineIntCommand("timer", setTimer, F(" 30 (Sets measurement interval, in s)"));
-    sensor_console::defineCommand("calibrate", startCalibrationProcess, F(" (Starts calibration process)"));
+    // SCD30 has its own timer.
+    //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
+    Serial.println();
+    Serial.print(F("Setting SCD30 timestep to "));
+    Serial.print(config::measurement_timestep_bootup);
+    Serial.println(F(" s during acclimatization."));
+    scd30.setMeasurementInterval(config::measurement_timestep_bootup); // [s]
+
+    sensor_console::defineIntCommand("co2", setCO2forDebugging, F("1500 (Sets co2 level, for debugging purposes)"));
+    sensor_console::defineIntCommand("timer", setTimer, F("30 (Sets measurement interval, in s)"));
+    sensor_console::defineCommand("calibrate", startCalibrationProcess, F("(Starts calibration process)"));
     sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM,
-        F(" 600 (Starts calibration process, to given ppm)"));
+        F("600 (Starts calibration process, to given ppm)"));
     sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow,
-        F(" 600 (Calibrates right now, to given ppm)"));
-    sensor_console::defineIntCommand("auto_calibrate", setAutoCalibration,
-        F(" 0/1 (Disables/enables autocalibration)"));
+        F("600 (Calibrates right now, to given ppm)"));
+    sensor_console::defineIntCommand("auto_calibrate", setAutoCalibration, F("0/1 (Disables/enables autocalibration)"));
   }
 
-  //NOTE: should timer deviation be used to adjust measurement_timestep?
-  void checkTimerDeviation() {
-    static int32_t previous_measurement_at = 0;
-    int32_t now = millis();
-    Serial.print(F("Measurement time offset : "));
-    Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000);
-    Serial.println(" ms.");
-    previous_measurement_at = now;
+  bool hasSensorSettled() {
+    static uint16_t last_co2 = 0;
+    uint16_t delta;
+    delta = abs(co2 - last_co2);
+    last_co2 = co2;
+    // We assume the sensor has acclimated to the environment if measurements
+    // change less than a specified percentage of the current value.
+    return (co2 > 0 && delta < ((uint32_t) co2 * config::max_deviation_during_bootup / 100));
   }
 
   bool countStableMeasurements() {
@@ -128,30 +132,32 @@ namespace sensor {
         && co2 < (previous_co2 + config::max_deviation_during_calibration)) {
       stable_measurements++;
       Serial.print(F("Number of stable measurements : "));
-      Serial.println(stable_measurements);
+      Serial.print(stable_measurements);
+      Serial.print(F(" / "));
+      Serial.println(config::stable_measurements_before_calibration);
       switchState(PREPARE_CALIBRATION_STABLE);
     } else {
       stable_measurements = 0;
       switchState(PREPARE_CALIBRATION_UNSTABLE);
     }
     previous_co2 = co2;
-    return (stable_measurements == config::enough_stable_measurements);
+    return (stable_measurements == config::stable_measurements_before_calibration);
   }
 
   void startCalibrationProcess() {
     /** From the sensor documentation:
-     * For best results, the sensor has to be run in a stable environment in continuous mode at
-     * a measurement rate of 2s for at least two minutes before applying the FRC command and sending the reference value.
+     * Before applying FRC, SCD30 needs to be operated for 2 minutes with the desired measurement period in continuous mode.
      */
-    Serial.println(F("Setting SCD30 timestep to 2s, prior to calibration."));
-    scd30.setMeasurementInterval(2); // [s] The change will only take effect after next measurement.
+    Serial.print(F("Setting SCD30 timestep to "));
+    Serial.print(config::timestep_during_calibration);
+    Serial.println(F("s, prior to calibration."));
+    scd30.setMeasurementInterval(config::timestep_during_calibration); // [s] The change will only take effect after next measurement.
     Serial.println(F("Waiting until the measurements are stable for at least 2 minutes."));
     Serial.println(F("It could take a very long time."));
     switchState(PREPARE_CALIBRATION_UNSTABLE);
   }
 
   void calibrateAndRestart() {
-    switchState(CALIBRATION);
     Serial.print(F("Calibrating SCD30 now..."));
     scd30.setAltitudeCompensation(config::altitude_above_sea_level);
     scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
@@ -177,19 +183,28 @@ namespace sensor {
     if (config::debug_sensor_states) {
       Serial.print(F("Changing sensor state: "));
       Serial.print(state_names[current_state]);
-      Serial.print(" -> ");
+      Serial.print(F(" -> "));
       Serial.println(state_names[new_state]);
     }
     current_state = new_state;
   }
 
   void switchStateForCurrentPPM() {
-    if (co2 == 0) {
-      // NOTE: Data is available, but it's sometimes erroneous: the sensor outputs
-      // zero ppm but non-zero temperature and non-zero humidity.
-      Serial.println(F("Invalid sensor data - CO2 concentration supposedly 0 ppm"));
-      switchState(BOOTUP);
-    } else if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_STABLE)) {
+    if (current_state == BOOTUP) {
+      if (!hasSensorSettled()) {
+        return;
+      }
+      switchState(READY);
+      Serial.println(F("Sensor acclimatization finished."));
+      Serial.print(F("Setting SCD30 timestep to "));
+      Serial.print(config::measurement_timestep);
+      Serial.println(F(" s."));
+      if (config::measurement_timestep < 10) {
+        Serial.println(F("WARNING: Timesteps shorter than 10s can lead to unreliable measurements!"));
+      }
+      scd30.setMeasurementInterval(config::measurement_timestep); // [s]
+    }
+    if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_STABLE)) {
       // Check for pre-calibration states first, because we do not want to
       // leave them before calibration is done.
       bool ready_for_calibration = countStableMeasurements();
@@ -236,8 +251,6 @@ namespace sensor {
     case PREPARE_CALIBRATION_STABLE:
       led_effects::showWaitingLED(color::green);
       break;
-    case CALIBRATION: // Nothing to do, will restart soon.
-      break;
     default:
       Serial.println(F("Encountered unknown sensor state")); // This should not happen.
     }
@@ -250,7 +263,6 @@ namespace sensor {
     bool freshData = scd30.dataAvailable();
 
     if (freshData) {
-      // checkTimerDeviation();
       ntp::getLocalTime(timestamp);
       co2 = scd30.getCO2();
       temperature = scd30.getTemperature();
@@ -264,7 +276,9 @@ namespace sensor {
 
     showState();
 
-    return freshData;
+    // Report data for further processing only if the data is reliable
+    // (state 'READY') or manual calibration is necessary (state 'NEEDS_CALIBRATION').
+    return freshData && (current_state == READY || current_state == NEEDS_CALIBRATION);
   }
 
   /*****************************************************************
@@ -288,8 +302,8 @@ namespace sensor {
     if (timestep >= 2 && timestep <= 1800) {
       Serial.print(F("Setting Measurement Interval to : "));
       Serial.print(timestep);
-      Serial.println("s.");
-      sensor::scd30.setMeasurementInterval(timestep);
+      Serial.println(F("s."));
+      scd30.setMeasurementInterval(timestep);
       config::measurement_timestep = timestep;
       led_effects::showKITTWheel(color::green, 1);
     }
@@ -300,13 +314,18 @@ namespace sensor {
       Serial.print(F("Force calibration, at "));
       config::co2_calibration_level = calibrationLevel;
       Serial.print(config::co2_calibration_level);
-      Serial.println(" ppm.");
-      sensor::startCalibrationProcess();
+      Serial.println(F(" ppm."));
+      startCalibrationProcess();
     }
   }
 
   void calibrateSensorRightNow(int32_t calibrationLevel) {
-    stable_measurements = config::enough_stable_measurements;
-    calibrateSensorToSpecificPPM(calibrationLevel);
+    if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
+      Serial.print(F("Force calibration, right now, at "));
+      config::co2_calibration_level = calibrationLevel;
+      Serial.print(config::co2_calibration_level);
+      Serial.println(F(" ppm."));
+      calibrateAndRestart();
+    }
   }
 }
diff --git a/ampel-firmware/config.public.h b/ampel-firmware/config.public.h
index 1ca67666cb3f76cb20ca1a9d03dc1b38d6bc3558..44a2bc95b176e1fe6d281a5a1d72dfa7d9c8fd86 100644
--- a/ampel-firmware/config.public.h
+++ b/ampel-firmware/config.public.h
@@ -27,7 +27,9 @@
  */
 
 // How often should measurement be performed, and displayed?
-//NOTE: SCD30 timer does not seem to be very precise. Variations may occur.
+//WARNING: On some sensors, measurements become very unreliable when timestep is set to 2s.
+//NOTE: 10s or longer should be fine in order to get reliable results.
+//NOTE: SCD30 timer does not seem to be very precise. Time variations may occur.
 #  define MEASUREMENT_TIMESTEP 60 // [s] Value between 2 and 1800 (range for SCD30 sensor)
 
 // How often should measurements be appended to CSV ?
@@ -64,6 +66,8 @@
 // MIN_BRIGHTNESS, if defined, should be between 0 and MAX_BRIGHTNESS - 1
 // If MIN_BRIGHTNESS is not set, or if it is set to MAX_BRIGHTNESS, breathing is disabled.
 #  define MIN_BRIGHTNESS 60
+// How many LEDs in the ring? 12 and 16 are currently supported. If undefined, 12 is used as default.
+#  define LED_COUNT 12
 
 /**
  * WEB SERVER
@@ -101,7 +105,7 @@
  */
 #  define ALLOW_MQTT_COMMANDS false
 
-// How often measurements should be sent to MQTT server?
+// How often should measurements be sent to MQTT server?
 // Probably a good idea to use a multiple of MEASUREMENT_TIMESTEP, so that averages can be calculated
 // Set to 0 if you want to send values after each measurement
 // #  define MQTT_SENDING_INTERVAL MEASUREMENT_TIMESTEP * 5 // [s]
diff --git a/ampel-firmware/csv_writer.cpp b/ampel-firmware/csv_writer.cpp
index a83bed666455545d2d5cb7c68a3d583d88f7b40a..d0828a40999dc7326d1ce19f9a4808be10dd6852 100644
--- a/ampel-firmware/csv_writer.cpp
+++ b/ampel-firmware/csv_writer.cpp
@@ -118,9 +118,9 @@ namespace csv_writer {
     showFilesystemContent();
     Serial.println();
 
-    sensor_console::defineIntCommand("csv", setCSVinterval, F(" 60 (Sets CSV writing interval, in s)"));
-    sensor_console::defineCommand("format_filesystem", formatFilesystem, F(" (Deletes the whole filesystem)"));
-    sensor_console::defineCommand("show_csv", showCSVContent, F(" (Displays the complete CSV file on Serial)"));
+    sensor_console::defineIntCommand("csv", setCSVinterval, F("60 (Sets CSV writing interval, in s)"));
+    sensor_console::defineCommand("format_filesystem", formatFilesystem, F("(Deletes the whole filesystem)"));
+    sensor_console::defineCommand("show_csv", showCSVContent, F("(Displays the complete CSV file on Serial)"));
   }
 
   File openOrCreate() {
diff --git a/ampel-firmware/led_effects.cpp b/ampel-firmware/led_effects.cpp
index ee0fccde5222ecf935dc444d86ed8663d5291a77..f0b672c9d0c76e1cac6916d94783e85577f387fd 100644
--- a/ampel-firmware/led_effects.cpp
+++ b/ampel-firmware/led_effects.cpp
@@ -12,8 +12,28 @@ namespace config {
   const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
   const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel
   const uint16_t poor_air_quality_ppm = 1600; // Above this threshold, LED breathing effect is faster.
-  //NOTE: Use a class instead? NightMode could then be another state.
-  bool night_mode = false;
+  bool night_mode = false; //NOTE: Use a class instead? NightMode could then be another state.
+
+#if !defined(LED_COUNT)
+#  define LED_COUNT 12
+#endif
+
+  const uint16_t led_count = LED_COUNT;
+
+#if LED_COUNT == 12
+  //NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index.
+  const uint16_t co2_ticks[led_count + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm]
+  // For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°),
+  // last 4 LEDs will be pure red (hue angle 0°), LEDs in-between will be yellowish.
+  const uint16_t led_hues[led_count] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle]
+#elif LED_COUNT == 16
+  const uint16_t co2_ticks[led_count + 1] = { 0, 400, 500, 600, 700, 800, 900, 1000, 1100,
+                                           1200, 1300, 1400, 1500, 1600, 1800, 2000, 2200 }; // [ppm]
+  const uint16_t led_hues[led_count] = { 21845U, 20024U, 18204U, 16383U, 14563U, 12742U, 10922U, 9102U,
+                                          7281U, 5461U, 3640U, 1820U, 0, 0, 0, 0 }; // [hue angle]
+#else
+#  error "Only 12 and 16 LEDs rings are currently supported."
+#endif
 }
 
 #if defined(ESP8266)
@@ -24,20 +44,7 @@ const int NEOPIXELS_PIN = 5;
 const int NEOPIXELS_PIN = 23;
 #endif
 
-const int NUMPIXELS = 12;
-//NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index.
-const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm]
-// const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1800, 2000, 2200 }; // [ppm]
-// For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°),
-// last 4 LEDs will be pure red (hue angle 0°), LEDs in-between will be yellowish.
-// For reference, this python code can be used to generate the array
-//    NUMPIXELS = 12
-//    RED_LEDS = 4
-//    hues = [ (2**16-1) // 3 * max(NUMPIXELS - RED_LEDS - i, 0) // (NUMPIXELS - RED_LEDS) for i in range(NUMPIXELS) ]
-//    '{' + ', '.join([str(hue) + ('U' if hue else '') for hue in hues]) + '}; // [hue angle]'
-const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle]
-// const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 20024U, 18204U, 16383U, 14563U, 12742U, 10922U, 9102U, 7281U, 5461U, 3640U, 1820U, 0, 0, 0, 0 }; // [hue angle]
-Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
+Adafruit_NeoPixel pixels(config::led_count, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
 
 namespace led_effects {
   //On-board LED on D4, aka GPIO02
@@ -70,11 +77,19 @@ namespace led_effects {
     onBoardLEDOff();
   }
 
+  void showColor(int32_t color) {
+    config::night_mode = true; // In order to avoid overwriting the desired color next time CO2 is displayed
+    pixels.setBrightness(255);
+    pixels.fill(color);
+    pixels.show();
+  }
+
   void setupRing() {
     pixels.begin();
     pixels.setBrightness(config::max_brightness);
     LEDsOff();
-    sensor_console::defineCommand("night_mode", toggleNightMode, F(" (Toggles night mode on/off)"));
+    sensor_console::defineCommand("night_mode", toggleNightMode, F("(Toggles night mode on/off)"));
+    sensor_console::defineIntCommand("color", showColor, F("0xFF0015 (Shows color, specified as RGB, for debugging)"));
   }
 
   void toggleNightMode() {
@@ -89,14 +104,15 @@ namespace led_effects {
 
   //NOTE: basically one iteration of KITT wheel
   void showWaitingLED(uint32_t color) {
+    using namespace config;
     delay(80);
-    if (config::night_mode) {
+    if (night_mode) {
       return;
     }
     static uint16_t kitt_offset = 0;
     pixels.clear();
-    for (int j = config::kitt_tail; j >= 0; j--) {
-      int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function
+    for (int j = kitt_tail; j >= 0; j--) {
+      int ledNumber = abs((kitt_offset - j + led_count) % (2 * led_count) - led_count) % led_count; // Triangular function
       pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255);
     }
     pixels.show();
@@ -108,7 +124,7 @@ namespace led_effects {
   // Takes approximately 1s for each direction.
   void showKITTWheel(uint32_t color, uint16_t duration_s) {
     pixels.setBrightness(config::max_brightness);
-    for (int i = 0; i < duration_s * NUMPIXELS; ++i) {
+    for (int i = 0; i < duration_s * config::led_count; ++i) {
       showWaitingLED(color);
     }
   }
@@ -118,10 +134,10 @@ namespace led_effects {
    * For example, for 1500ppm, every LED between 0 and 7 (500 -> 1400ppm) should be on, LED at 8 (1600ppm) should be half-on.
    */
   uint8_t getLedBrightness(uint16_t co2, int ledId) {
-    if (co2 >= CO2_TICKS[ledId + 1]) {
+    if (co2 >= config::co2_ticks[ledId + 1]) {
       return 255;
     } else {
-      if (2 * co2 >= CO2_TICKS[ledId] + CO2_TICKS[ledId + 1]) {
+      if (2 * co2 >= config::co2_ticks[ledId] + config::co2_ticks[ledId + 1]) {
         // Show partial LED if co2 more than halfway between ticks.
         return 27; // Brightness isn't linear, so 27 / 255 looks much brighter than 10%
       } else {
@@ -150,9 +166,9 @@ namespace led_effects {
       return;
     }
     pixels.setBrightness(config::max_brightness);
-    for (int ledId = 0; ledId < NUMPIXELS; ++ledId) {
+    for (int ledId = 0; ledId < config::led_count; ++ledId) {
       uint8_t brightness = getLedBrightness(co2, ledId);
-      pixels.setPixelColor(ledId, pixels.ColorHSV(LED_HUES[ledId], 255, brightness));
+      pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness));
     }
     pixels.show();
     if (config::brightness_amplitude > 0) {
@@ -160,17 +176,18 @@ namespace led_effects {
     }
   }
 
-  void showRainbowWheel(uint16_t duration_ms, uint16_t hue_increment) {
+  void showRainbowWheel(uint16_t duration_ms) {
     if (config::night_mode) {
       return;
     }
     static uint16_t wheel_offset = 0;
+    static uint16_t sine_offset = 0;
     unsigned long t0 = millis();
     pixels.setBrightness(config::max_brightness);
     while (millis() - t0 < duration_ms) {
-      for (int i = 0; i < NUMPIXELS; i++) {
-        pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
-        wheel_offset += hue_increment;
+      for (int i = 0; i < config::led_count; i++) {
+        pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / config::led_count + wheel_offset));
+        wheel_offset += (pixels.sine8(sine_offset++ / 50) - 127) / 2;
       }
       pixels.show();
       delay(10);
@@ -206,7 +223,7 @@ namespace led_effects {
     pixels.fill(color::blue);
     pixels.show();
     int countdown;
-    for (countdown = NUMPIXELS; countdown >= 0 && !digitalRead(0); countdown--) {
+    for (countdown = config::led_count; countdown >= 0 && !digitalRead(0); countdown--) {
       pixels.setPixelColor(countdown, color::black);
       pixels.show();
       Serial.println(countdown);
diff --git a/ampel-firmware/led_effects.h b/ampel-firmware/led_effects.h
index 96367ef5f2909d38377e2441f91d4e2e1abe2198..a910d307f7c6294db889e704f745c3777136d19b 100644
--- a/ampel-firmware/led_effects.h
+++ b/ampel-firmware/led_effects.h
@@ -29,7 +29,7 @@ namespace led_effects {
   int countdownToZero();
   void showWaitingLED(uint32_t color);
   void showKITTWheel(uint32_t color, uint16_t duration_s = 2);
-  void showRainbowWheel(uint16_t duration_ms = 1000, uint16_t hue_increment = 50);
+  void showRainbowWheel(uint16_t duration_ms = 1000);
   void displayCO2color(uint16_t co2);
 }
 #endif
diff --git a/ampel-firmware/lorawan.cpp b/ampel-firmware/lorawan.cpp
index 784b46fb61b25bc8d3868a3a541baa14f241150a..746893b3698137ff14417158e5a2728fd8ad4a25 100644
--- a/ampel-firmware/lorawan.cpp
+++ b/ampel-firmware/lorawan.cpp
@@ -47,7 +47,7 @@ namespace lorawan {
     LMIC_reset();
     // Join, but don't send anything yet.
     LMIC_startJoining();
-    sensor_console::defineIntCommand("lora", setLoRaInterval, F(" 300 (Sets LoRaWAN sending interval, in s)"));
+    sensor_console::defineIntCommand("lora", setLoRaInterval, F("300 (Sets LoRaWAN sending interval, in s)"));
   }
 
   // Checks if OTAA is connected, or if payload should be sent.
@@ -96,7 +96,7 @@ namespace lorawan {
           printHex2(artKey[i]);
         }
         Serial.println();
-        Serial.print("  NwkSKey: ");
+        Serial.print(F("  NwkSKey: "));
         for (size_t i = 0; i < sizeof(nwkKey); ++i) {
           if (i != 0)
             Serial.print("-");
diff --git a/ampel-firmware/mqtt.cpp b/ampel-firmware/mqtt.cpp
index 22a0e2def350a38b77336b954395ae5d55005a8e..cb0c5019c00d14094d6389684bb120eb061197f6 100644
--- a/ampel-firmware/mqtt.cpp
+++ b/ampel-firmware/mqtt.cpp
@@ -36,9 +36,9 @@ namespace mqtt {
     // mqttClient.setSocketTimeout(config::mqtt_timeout); //NOTE: somehow doesn't seem to have any effect on connect()
     mqttClient.setServer(config::mqtt_server, config::mqtt_port);
 
-    sensor_console::defineIntCommand("mqtt", setMQTTinterval, F(" 60 (Sets MQTT sending interval, in s)"));
-    sensor_console::defineCommand("local_ip", sendInfoAboutLocalNetwork,
-        F(" (Sends local IP and SSID via MQTT. Can be useful to find sensor)"));
+    sensor_console::defineIntCommand("mqtt", setMQTTinterval, F("60 (Sets MQTT sending interval, in s)"));
+    sensor_console::defineCommand("send_local_ip", sendInfoAboutLocalNetwork,
+        F("(Sends local IP and SSID via MQTT. Can be useful to find sensor)"));
   }
 
   void publish(const char *timestamp, int16_t co2, float temperature, float humidity) {
@@ -79,7 +79,7 @@ namespace mqtt {
       command[i] = message[i];
     }
     command[length] = 0;
-    sensor_console::runCommand(command);
+    sensor_console::execute(command);
     led_effects::onBoardLEDOff();
   }
 
diff --git a/ampel-firmware/sensor_console.cpp b/ampel-firmware/sensor_console.cpp
index 5f1876ddda8f2405b4f1a2eea253408123408824..5fc3377ced8a4d77b9337fad19c71483159ac74c 100644
--- a/ampel-firmware/sensor_console.cpp
+++ b/ampel-firmware/sensor_console.cpp
@@ -6,75 +6,112 @@ namespace sensor_console {
 
   uint8_t commands_count = 0;
 
+  enum input_type {
+    NONE,
+    INT32,
+    STRING
+  };
+
   struct Command {
     const char *name;
     union {
+      void (*voidFunction)();
       void (*intFunction)(int32_t);
-      void (*voidFunction)(void);
+      void (*strFunction)(char*);
     };
     const char *doc;
-    bool has_parameter;
+    input_type parameter_type;
+  };
+
+  struct CommandLine {
+    char function_name[MAX_COMMAND_SIZE];
+    input_type argument_type;
+    int32_t int_argument;
+    char str_argument[MAX_COMMAND_SIZE];
   };
 
   Command commands[MAX_COMMANDS];
 
-  //NOTE: Probably possible to DRY (with templates?)
-  void defineCommand(const char *name, void (*function)(void), const __FlashStringHelper *doc_fstring) {
-    const char *doc = (const char*) doc_fstring;
+  bool addCommand(const char *name, const __FlashStringHelper *doc_fstring) {
     if (commands_count < MAX_COMMANDS) {
       commands[commands_count].name = name;
-      commands[commands_count].voidFunction = function;
-      commands[commands_count].doc = doc;
-      commands[commands_count].has_parameter = false;
-      commands_count++;
+      commands[commands_count].doc = (const char*) doc_fstring;
+      return true;
     } else {
       Serial.println(F("Too many commands have been defined."));
+      return false;
+    }
+  }
+
+  void defineCommand(const char *name, void (*function)(), const __FlashStringHelper *doc_fstring) {
+    if (addCommand(name, doc_fstring)) {
+      commands[commands_count].voidFunction = function;
+      commands[commands_count++].parameter_type = NONE;
     }
   }
 
   void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring) {
-    const char *doc = (const char*) doc_fstring;
-    if (commands_count < MAX_COMMANDS) {
-      commands[commands_count].name = name;
+    if (addCommand(name, doc_fstring)) {
       commands[commands_count].intFunction = function;
-      commands[commands_count].doc = doc;
-      commands[commands_count].has_parameter = true;
-      commands_count++;
-    } else {
-      Serial.println(F("Too many commands have been defined."));
+      commands[commands_count++].parameter_type = INT32;
+    }
+  }
+
+  void defineStringCommand(const char *name, void (*function)(char*), const __FlashStringHelper *doc_fstring) {
+    if (addCommand(name, doc_fstring)) {
+      commands[commands_count].strFunction = function;
+      commands[commands_count++].parameter_type = STRING;
     }
   }
 
   /*
-   * Tries to split a string command (e.g. 'mqtt 60' or 'show_csv') into a function_name and an argument.
-   * Returns 0 if both are found, 1 if there is a problem and 2 if no argument is found.
+   * Tries to split a string command (e.g. 'mqtt 60' or 'show_csv') into
+   * a CommandLine struct (function_name, argument_type and argument)
    */
-  uint8_t parseCommand(const char *command, char *function_name, int32_t &argument) {
-    char split_command[MAX_COMMAND_SIZE];
-    strlcpy(split_command, command, MAX_COMMAND_SIZE);
-    char *arg;
-    char *part1;
-    part1 = strtok(split_command, " ");
-    if (!part1) {
+  void parseCommand(const char *command, CommandLine &command_line) {
+    if (strlen(command) == 0) {
       Serial.println(F("Received empty command"));
-      // Empty string
-      return 1;
+      command_line.argument_type = NONE;
+      return;
     }
-    strlcpy(function_name, part1, MAX_COMMAND_SIZE);
-    arg = strtok(NULL, " ");
-    uint8_t code = 0;
-    if (arg) {
-      char *end;
-      argument = strtol(arg, &end, 10);
-      if (*end) {
-        // Second argument isn't a number
-        code = 2;
-      }
+
+    char *first_space;
+    first_space = strchr(command, ' ');
+
+    if (first_space == NULL) {
+      command_line.argument_type = NONE;
+      strlcpy(command_line.function_name, command, MAX_COMMAND_SIZE);
+      return;
+    }
+
+    strlcpy(command_line.function_name, command, first_space - command + 1);
+    strlcpy(command_line.str_argument, first_space + 1, MAX_COMMAND_SIZE - (first_space - command) - 1);
+
+    char *end;
+    command_line.int_argument = strtol(command_line.str_argument, &end, 0); // Accepts 123 or 0xFF00FF
+
+    if (*end) {
+      command_line.argument_type = STRING;
     } else {
-      // No argument
-      code = 2;
+      command_line.argument_type = INT32;
+    }
+  }
+
+  int compareCommandNames(const void *s1, const void *s2) {
+    struct Command *c1 = (struct Command*) s1;
+    struct Command *c2 = (struct Command*) s2;
+    return strcmp(c1->name, c2->name);
+  }
+
+  void listAvailableCommands() {
+    qsort(commands, commands_count, sizeof(commands[0]), compareCommandNames);
+    for (uint8_t i = 0; i < commands_count; i++) {
+      Serial.print(F("  "));
+      Serial.print(commands[i].name);
+      Serial.print(F(" "));
+      Serial.print(commands[i].doc);
+      Serial.println(F("."));
     }
-    return code;
   }
 
   /*
@@ -88,7 +125,7 @@ namespace sensor_console {
     case '\n': // end of text
       Serial.println();
       input_line[input_pos] = 0;
-      runCommand(input_line);
+      execute(input_line);
       input_pos = 0;
       break;
     case '\r': // discard carriage return
@@ -112,50 +149,40 @@ namespace sensor_console {
     }
   }
 
-  int compareName(const void *s1, const void *s2) {
-    struct Command *c1 = (struct Command*) s1;
-    struct Command *c2 = (struct Command*) s2;
-    return strcmp(c1->name, c2->name);
-  }
-
-  void listAvailableCommands() {
-    qsort(commands, commands_count, sizeof(commands[0]), compareName);
-    for (uint8_t i = 0; i < commands_count; i++) {
-      Serial.print("  ");
-      Serial.print(commands[i].name);
-      Serial.print(commands[i].doc);
-      Serial.println(".");
-    }
-  }
-
   /*
-   * Tries to find the corresponding callback for a given command. Name and number of argument should fit.
+   * Tries to find the corresponding callback for a given command. Name and parameter type should fit.
    */
-  void runCommand(const char *command) {
-    char function_name[MAX_COMMAND_SIZE];
-    int32_t argument = 0;
-    bool has_argument;
-    has_argument = (parseCommand(command, function_name, argument) == 0);
-
+  void execute(const char *command_str) {
+    CommandLine input;
+    parseCommand(command_str, input);
     for (uint8_t i = 0; i < commands_count; i++) {
-      if (!strcmp(function_name, commands[i].name) && has_argument == commands[i].has_parameter) {
+      if (!strcmp(input.function_name, commands[i].name) && input.argument_type == commands[i].parameter_type) {
         Serial.print(F("Calling : "));
-        Serial.print(function_name);
-        if (has_argument) {
-          Serial.print(F("("));
-          Serial.print(argument);
-          Serial.println(F(")"));
-          commands[i].intFunction(argument);
-        } else {
+        Serial.print(input.function_name);
+        switch (input.argument_type) {
+        case NONE:
           Serial.println(F("()"));
           commands[i].voidFunction();
+          return;
+        case INT32:
+          Serial.print(F("("));
+          Serial.print(input.int_argument);
+          Serial.println(F(")"));
+          commands[i].intFunction(input.int_argument);
+          return;
+        case STRING:
+          Serial.print(F("('"));
+          Serial.print(input.str_argument);
+          Serial.println(F("')"));
+          commands[i].strFunction(input.str_argument);
+          return;
         }
-        return;
       }
     }
     Serial.print(F("'"));
-    Serial.print(command);
+    Serial.print(command_str);
     Serial.println(F("' not supported. Available commands :"));
     listAvailableCommands();
   }
+
 }
diff --git a/ampel-firmware/sensor_console.h b/ampel-firmware/sensor_console.h
index b46e212e06c308e15ba7497509c33ffd104932ad..5cf4450d19535d19e871f5d4d48b11d09521070e 100644
--- a/ampel-firmware/sensor_console.h
+++ b/ampel-firmware/sensor_console.h
@@ -8,11 +8,13 @@
  */
 
 namespace sensor_console {
-  void defineCommand(const char *command, void (*function)(void), const __FlashStringHelper *ifsh);
-  void defineIntCommand(const char *command, void (*function)(int32_t), const __FlashStringHelper *ifsh);
+  void defineCommand(const char *name, void (*function)(), const __FlashStringHelper *doc_fstring);
+  void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring);
+  void defineStringCommand(const char *name, void (*function)(char*), const __FlashStringHelper *doc_fstring);
 
   void processSerialInput(const byte in_byte);
-  void runCommand(const char *command);
+
+  void execute(const char *command_line);
 }
 
 #endif
diff --git a/ampel-firmware/util.cpp b/ampel-firmware/util.cpp
index e3b2d6e8913172cce6e4dd868aace8c03e8be02f..5fcb36cb454c42b606fd632e7db9be5faf9108b8 100644
--- a/ampel-firmware/util.cpp
+++ b/ampel-firmware/util.cpp
@@ -7,6 +7,13 @@ namespace config {
 
 #if defined(ESP8266)
 const char *current_board = "ESP8266";
+#  if !defined(AMPEL_WIFI)
+void preinit() {
+  // WiFi would be initialized otherwise (on ESP8266), even if unused.
+  // see https://github.com/esp8266/Arduino/issues/2111#issuecomment-224251391
+  ESP8266WiFiClass::preinitWiFiOff();
+}
+#  endif
 #elif defined(ESP32)
 const char *current_board = "ESP32";
 #else
@@ -74,11 +81,11 @@ char* getSensorId() {
 
 Ampel::Ampel() :
     board(current_board), sensorId(getSensorId()), max_loop_duration(0) {
-  sensor_console::defineIntCommand("set_time", ntp::setLocalTime, F(" 1618829570 (Sets time to the given UNIX time)"));
-  sensor_console::defineCommand("free", Ampel::showFreeSpace, F(" (Displays available heap space)"));
+  sensor_console::defineIntCommand("set_time", ntp::setLocalTime, F("1618829570 (Sets time to the given UNIX time)"));
+  sensor_console::defineCommand("free", Ampel::showFreeSpace, F("(Displays available heap space)"));
   sensor_console::defineCommand("reset", []() {
     ESP.restart();
-  }, F(" (Restarts the sensor)"));
+  }, F("(Restarts the sensor)"));
 }
 
 Ampel ampel;
diff --git a/ampel-firmware/util.h b/ampel-firmware/util.h
index db424db6892d1beea14f3e4151c91356af464e4e..60406a891a776132bf936ee3b454304b5b74b5b7 100644
--- a/ampel-firmware/util.h
+++ b/ampel-firmware/util.h
@@ -38,6 +38,7 @@ class Ampel {
 private:
   static void showFreeSpace();
 public:
+  const char *version = "v0.1.0"; // Update manually after significant changes.
   const char *board;
   const char *sensorId;
   uint32_t max_loop_duration;
diff --git a/ampel-firmware/web_server.cpp b/ampel-firmware/web_server.cpp
index 40cd15b3e7e5a1dae0777a36dfe85138f58bb7a5..b17e666d08ca932290d15f56061fd1d65ac4f8ea 100644
--- a/ampel-firmware/web_server.cpp
+++ b/ampel-firmware/web_server.cpp
@@ -115,6 +115,7 @@ namespace web_server {
             "<tr><td>Largest heap block</td><td>%6d bytes</td></tr>\n"
             "<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n"
             "<tr><td>Board</td><td>%s</td></tr>\n"
+            "<tr><td>Ampel firmware</td><td>%s</td></tr>\n"
             "<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
             "</table>\n"
             "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
@@ -219,8 +220,8 @@ namespace web_server {
 #endif
         );
 
-    Serial.print(F("INFO - Header size : "));
-    Serial.print(strlen(content));
+    //    Serial.print(F("INFO - Header size : "));
+    //    Serial.print(strlen(content));
     http.setContentLength(CONTENT_LENGTH_UNKNOWN);
     http.send_P(200, PSTR("text/html"), content);
 
@@ -239,11 +240,11 @@ namespace web_server {
 #endif
         config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", ampel.sensorId, ampel.sensorId,
         wifi::local_ip, wifi::local_ip, ESP.getFreeHeap(), esp_get_max_free_block_size(), ampel.max_loop_duration,
-        ampel.board, dd, hh, mm, ss);
+        ampel.board, ampel.version, dd, hh, mm, ss);
 
-    Serial.print(F(" - Body size : "));
+    //    Serial.print(F(" - Body size : "));
+    //    Serial.print(strlen(content));
     http.sendContent(content);
-    Serial.print(strlen(content));
 
     // Script
     snprintf_P(content, sizeof(content), script_template
@@ -252,8 +253,8 @@ namespace web_server {
 #endif
         );
 
-    Serial.print(F(" - Script size : "));
-    Serial.println(strlen(content));
+    //    Serial.print(F(" - Script size : "));
+    //    Serial.println(strlen(content));
     http.sendContent(content);
   }
 
@@ -292,7 +293,7 @@ namespace web_server {
     }
     http.sendHeader("Location", "/");
     http.send(303);
-    sensor_console::runCommand(http.arg("send").c_str());
+    sensor_console::execute(http.arg("send").c_str());
   }
 
   void handlePageNotFound() {
diff --git a/ampel-firmware/wifi_util.cpp b/ampel-firmware/wifi_util.cpp
index a845140a8aa80e66c0279c7b74e486847aaf6355..4cdc7a2a597d3d66523c6e37b837346e5231311e 100644
--- a/ampel-firmware/wifi_util.cpp
+++ b/ampel-firmware/wifi_util.cpp
@@ -14,8 +14,38 @@ namespace config {
 
 namespace wifi {
   char local_ip[16]; // "255.255.255.255\0"
+
+  void scanNetworks() {
+    Serial.println();
+    Serial.println(F("WiFi - Scanning..."));
+    bool async = false;
+    bool showHidden = true;
+    int n = WiFi.scanNetworks(async, showHidden);
+    for (int i = 0; i < n; ++i) {
+      Serial.print(F("  * '"));
+      Serial.print(WiFi.SSID(i));
+      Serial.print(F("' ("));
+      int16_t quality = 2 * (100 + WiFi.RSSI(i));
+      Serial.print(util::min(util::max(quality, 0), 100));
+      Serial.println(F(" %)"));
+    }
+    Serial.println(F("Done!"));
+    Serial.println();
+  }
+
+  void showLocalIp() {
+    Serial.print(F("WiFi - Local IP : "));
+    Serial.println(wifi::local_ip);
+    Serial.print(F("WiFi - SSID : "));
+    Serial.println(WIFI_SSID);
+  }
+
   // Initialize Wi-Fi
   void connect(const char *hostname) {
+
+    sensor_console::defineCommand("wifi_scan", scanNetworks, F("(Scans available WiFi networks)"));
+    sensor_console::defineCommand("local_ip", showLocalIp, F("(Displays local IP and current SSID)"));
+
     //NOTE: WiFi Multi could allow multiple SSID and passwords.
     WiFi.persistent(false); // Don't write user & password to Flash.
     WiFi.mode(WIFI_STA); // Set ESP to be a WiFi-client only