public static void main(String[] args) throws Exception {
    System.out.println(args.length + " parameter(s).");
    if (args.length > 0) {
      if (args[0].equals("-cal")) {
        calibration = true;
        ansiConsole = false;
      }
    }

    LelandPrototype lp = new LelandPrototype();

    if (!calibration) {
      props = new Properties();
      try {
        props.load(new FileInputStream("props.properties"));
      } catch (IOException ioe) {
        displayAppErr(ioe);
        //  ioe.printStackTrace();
      }

      try {
        windowWidth =
            Integer.parseInt(
                LelandPrototype.getAppProperties()
                    .getProperty("smooth.width", "10")); // For smoothing
        alfa =
            Double.parseDouble(
                LelandPrototype.getAppProperties().getProperty("low.pass.filter.alfa", "0.5"));
      } catch (NumberFormatException nfe) {
        nfe.printStackTrace();
      }

      try {
        cleaningDelay =
            Long.parseLong(props.getProperty("cleaning.delay", "86400")); // Default: one day
      } catch (NumberFormatException nfe) {
        nfe.printStackTrace();
      }

      try {
        nbSeenInARow = Integer.parseInt(props.getProperty("seen.in.a.row", "40"));
      } catch (NumberFormatException nfe) {
        nfe.printStackTrace();
      }

      try {
        rangeSensorHeight = Double.parseDouble(props.getProperty("range.sensor.height", "10"));
      } catch (NumberFormatException nfe) {
        nfe.printStackTrace();
      }

      try {
        fileLogger = new BufferedWriter(new FileWriter(LOG_FILE));
      } catch (Exception ex) {
        ex.printStackTrace();
      }

      if (ansiConsole) AnsiConsole.systemInstall();

      final ReadWriteFONA fona;

      if ("true".equals(props.getProperty("with.fona", "false"))) {
        System.setProperty("baud.rate", props.getProperty("baud.rate"));
        System.setProperty("serial.port", props.getProperty("serial.port"));

        fona = new ReadWriteFONA(lp);
        fona.openSerialInput();
        fona.startListening();
        while (!fonaReady) {
          System.out.println("Waiting for the FONA device to come up...");
          try {
            Thread.sleep(1000L);
          } catch (InterruptedException ie) {
          }
        }
        fona.requestBatteryState();
        fona.requestNetworkStatus();
        displayAppMess(">>> FONA Ready, moving on");
        smsProvider = fona;
      } else {
        System.out.println("Will simulate the phone calls.");
      }
      delay(1);

      wsUri = props.getProperty("ws.uri", "");
      phoneNumber_1 = props.getProperty("phone.number.1", "14153505547");
      phoneNumber_2 = props.getProperty("phone.number.2", "14153505547");
      phoneNumber_3 = props.getProperty("phone.number.3", "14153505547");
      boatName = props.getProperty("boat.name", "Never Again XXIII");

      try {
        rm = new RelayManager();
        rm.set("00", RelayManager.RelayState.ON);
      } catch (Exception ex) {
        System.err.println("You're not on the PI, hey?");
        ex.printStackTrace();
      }

      if (wsUri.trim().startsWith("ws://")) {
        log(">>> Connecting to the WebSocket server [" + wsUri + "]");
        initWebSocketConnection(wsUri);
      } else {
        log(">>> No WebSocket server");
        delay(1);
      }
    }

    final SevenADCChannelsManager sacm = (calibration ? null : new SevenADCChannelsManager(lp));
    final SurfaceDistanceManager sdm = new SurfaceDistanceManager(lp);
    sdm.startListening();

    final Thread me = Thread.currentThread();

    Runtime.getRuntime()
        .addShutdownHook(
            new Thread() {
              public void run() {
                // Cleanup
                System.out.println();
                if (sacm != null) sacm.quit();
                if (smsProvider != null) smsProvider.closeChannel();
                synchronized (me) {
                  me.notify();
                }
                gpio.shutdown();
                if (channelLogger != null) {
                  try {
                    for (BufferedWriter bw : channelLogger) {
                      bw.close();
                    }
                  } catch (Exception ex) {
                    ex.printStackTrace();
                  }
                }
                System.out.println("Program stopped by user's request.");
              }
            });

    if (!calibration) {
      if ("true".equals(props.getProperty("log.channels", "false"))) {
        channelLogger = new BufferedWriter[SevenADCChannelsManager.getChannel().length];
        for (int i = 0; i < SevenADCChannelsManager.getChannel().length; i++) {
          channelLogger[i] =
              new BufferedWriter(
                  new FileWriter(CHANNEL_PREFIX + CHANNEL_NF.format(i) + CHANNEL_SUFFIX));
        }
      }

      // CLS
      if (ansiConsole) AnsiConsole.out.println(EscapeSeq.ANSI_CLS);
    }
    synchronized (me) {
      System.out.println("Main thread waiting...");
      me.wait();
    }
    System.out.println("Done.");
  }
  // User Interface ... Sovietic! And business logic.
  private static void manageData() {
    int maxWaterLevel = -1;
    //  int maxOilLevel   = -1;
    // Clear the screen, cursor on top left.
    String str = "";
    int y = 1;
    if (ansiConsole) {
      // AnsiConsole.out.println(EscapeSeq.ANSI_CLS);
      //    AnsiConsole.out.println(EscapeSeq.ansiLocate(0, y++));
      // Firts line to erase what was there before starting.
      AnsiConsole.out.println(
          EscapeSeq.ansiLocate(0, y++)
              + "WT:"
              + SevenADCChannelsManager.getWaterThreshold()
              +
              // ", OT:" + SevenADCChannelsManager.getOilThreshold() +
              ", Sensor Height: "
              + DF23.format(rangeSensorHeight)
              + " cm, D2S:"
              + DF23.format(distanceToSurface * 100)
              + " cm                      ");
      str = EscapeSeq.ansiLocate(0, y++) + "+---+--------+---------+";
      AnsiConsole.out.println(str);
      str = EscapeSeq.ansiLocate(0, y++) + "| C |  Vol % |   Mat   |";
      AnsiConsole.out.println(str);

      str = EscapeSeq.ansiLocate(0, y++) + "+---+--------+---------+";
      AnsiConsole.out.println(str);
    }
    for (int chan = data.length - 1; chan >= 0; chan--) // Top to bottom
    {
      if (previousMaterial[chan] != null && previousMaterial[chan] == data[chan].getMaterial())
        nbSameMaterialInARow[chan] += 1;
      else nbSameMaterialInARow[chan] = 0;

      String color = EscapeSeq.ANSI_BLACK; // ANSI_DEFAULT_BACKGROUND;

      if (nbSameMaterialInARow[chan] > nbSeenInARow) {
        //        if (data[chan].getMaterial().equals(SevenADCChannelsManager.Material.OIL))
        //          color = EscapeSeq.ANSI_RED;
        //        else
        if (data[chan].getMaterial().equals(SevenADCChannelsManager.Material.WATER))
          color = EscapeSeq.ANSI_BLUE;
      }

      String prefix =
          EscapeSeq.ansiLocate(0, y++)
              + EscapeSeq.ansiSetTextAndBackgroundColor(EscapeSeq.ANSI_WHITE, color)
              + EscapeSeq.ANSI_BOLD;
      String suffix =
          EscapeSeq.ANSI_NORMAL + EscapeSeq.ANSI_DEFAULT_BACKGROUND + EscapeSeq.ANSI_DEFAULT_TEXT;
      str =
          "| "
              + Integer.toString(chan + 1)
              + " | "
              + lpad(DF4.format(data[chan].getPercent()), " ", 4)
              + " % | "
              + lpad(materialToString(data[chan].getMaterial()), " ", 7)
              + " |";
      str += (" " + nbSameMaterialInARow[chan] + "   ");
      // if (maxOilLevel == -1 &&
      // data[chan].getMaterial().equals(SevenADCChannelsManager.Material.OIL))
      //   maxOilLevel = chan;
      if (maxWaterLevel == -1
          && data[chan].getMaterial().equals(SevenADCChannelsManager.Material.WATER))
        maxWaterLevel = chan;
      if (ansiConsole) AnsiConsole.out.println(prefix + str + suffix);

      previousMaterial[chan] = data[chan].getMaterial();
    }
    if (ansiConsole) {
      str = EscapeSeq.ansiLocate(0, y++) + "+---+--------+---------+";
      AnsiConsole.out.println(str);
    }
    double waterThickness = (maxWaterLevel + 1) * SENSOR_SPACING;
    double oilThickness = rangeSensorHeight - (waterThickness + (distanceToSurface * 100));
    if (ansiConsole) {
      str =
          EscapeSeq.ansiLocate(0, y++)
              + "Water:"
              + waterThickness
              + ", OT:"
              + oilThickness
              + " cm"
              + "                  ";
      AnsiConsole.out.println(str);
    }
    if (webSocketClient != null) {
      JSONObject json = new JSONObject();
      json.put("water", waterThickness);
      json.put("oil", oilThickness);
      try {
        webSocketClient.send(json.toString());
      } // [1..100]
      catch (Exception ex) {
        displayAppErr(ex);
        //  ex.printStackTrace();
      }
    }
    //  log(">>> To BusinessLogic (" + maxWaterLevel + ", " + maxOilLevel + ")");
    //  businessLogic(maxWaterLevel, maxOilLevel); // Before
    if (deviceStarted) businessLogic(maxWaterLevel, oilThickness);
  }