content: /*************************************************************************
  *                                                                        *
  *  This file is part of the 20n/act project.                             *
  *  20n/act enables DNA prediction for synthetic biology/bioengineering.  *
  *  Copyright (C) 2017 20n Labs, Inc.                                     *
  *                                                                        *
  *  Please direct all queries to act@20n.com.                             *
  *                                                                        *
  *  This program is free software: you can redistribute it and/or modify  *
  *  it under the terms of the GNU General Public License as published by  *
  *  the Free Software Foundation, either version 3 of the License, or     *
  *  (at your option) any later version.                                   *
  *                                                                        *
  *  This program is distributed in the hope that it will be useful,       *
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of        *
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
  *  GNU General Public License for more details.                          *
  *                                                                        *
  *  You should have received a copy of the GNU General Public License     *
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>. *
  *                                                                        *
  *************************************************************************/
  
  package com.twentyn.bioreactor.pH;
  
  import com.fasterxml.jackson.databind.Module;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import com.twentyn.bioreactor.util.Time;
  import org.apache.commons.cli.CommandLine;
  import org.apache.commons.cli.CommandLineParser;
  import org.apache.commons.cli.DefaultParser;
  import org.apache.commons.cli.HelpFormatter;
  import org.apache.commons.cli.Option;
  import org.apache.commons.cli.Options;
  import org.apache.commons.cli.ParseException;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  
  import com.fasterxml.jackson.datatype.joda.JodaModule;
  import org.joda.time.DateTime;
  
  import com.twentyn.bioreactor.sensors.PHSensorData;
  
  public class ControlSystem {
  
    private static final Logger LOGGER = LogManager.getFormatterLogger(ControlSystem.class);
    private static final String SENSOR_READING_FILE_LOCATION = "/tmp/sensors/v1/pH/reading.json";
    private static final Double MARGIN_OF_ACCEPTANCE_IN_PH = 0.5;
    private static final Integer WAIT_TIME = 20000;
    private static final Integer PUMP_TIME_WAIT_IN_MILLI_SECONDS = 1000;
    private static final Integer WAIT_TIME_BETWEEN_ACTION_IN_MILLI_SECONDS = 100;
  
    private static final String OPTION_TARGET_PH = "p";
    private static final String OPTION_SENSOR_READING_FILE_LOCATION = "s";
    private static final String OPTION_CONTROL_SOLUTION = "c";
    private static final String HELP_MESSAGE = "This class runs the control system of one fermentation run";
  
    public static final List<Option.Builder> OPTION_BUILDERS = new ArrayList<Option.Builder>() {{
      add(Option.builder(OPTION_SENSOR_READING_FILE_LOCATION)
          .argName("sensor reading file location")
          .desc("The file location of sensor reading data")
          .hasArg()
          .longOpt("sensor-reading-file-location")
      );
      add(Option.builder(OPTION_TARGET_PH)
          .argName("target ph")
          .desc("The target pH of the system")
          .hasArg().required()
          .longOpt("target-ph")
      );
      add(Option.builder(OPTION_CONTROL_SOLUTION)
          .argName("control solution")
          .desc("The solution from which we can control the pH. The control solution can have two values: ACID or BASE.")
          .hasArg().required()
          .longOpt("control-solution")
      );
      add(Option.builder("h")
          .argName("help")
          .desc("Prints this help message")
          .longOpt("help")
      );
    }};
    public static final HelpFormatter HELP_FORMATTER = new HelpFormatter();
  
    static {
      HELP_FORMATTER.setWidth(100);
    }
  
    public enum SOLUTION {
      ACID,
      BASE
    }
  
    private MotorPinConfiguration motorPinConfiguration;
    private SOLUTION solution;
    private Double targetPH;
    private ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private File pHSensorDataFile;
  
    public ControlSystem(MotorPinConfiguration initializedMotorPinConfiguration,
                         SOLUTION solution,
                         Double targetPH,
                         File pHSensorDataFile) {
      this.motorPinConfiguration = initializedMotorPinConfiguration;
      this.solution = solution;
      this.targetPH = targetPH;
      this.pHSensorDataFile = pHSensorDataFile;
    }
  
    public void registerModuleForObjectMapper(Module module) {
      this.OBJECT_MAPPER.registerModule(module);
    }
  
    // TODO: Move this functionality to the sensor module in the future since the control system is not responsible
    // for where the data is in a file or not.
    private PHSensorData readPhSensorData(File sensorDataFile) throws IOException {
      PHSensorData sensorData = OBJECT_MAPPER.readValue(sensorDataFile, PHSensorData.class);
      return sensorData;
    }
  
    private void takeAction() throws InterruptedException {
      LOGGER.info("Pump more solution");
      this.motorPinConfiguration.switchMotorOn();
      Thread.sleep(PUMP_TIME_WAIT_IN_MILLI_SECONDS);
      LOGGER.info("Stop pumping");
      this.motorPinConfiguration.switchMotorOff();
    }
  
    private Long timeDifference(DateTime longerTime, DateTime shorterTime) {
      return longerTime.getMillis() - shorterTime.getMillis();
    }
  
    private void shutdownFermentation() {
      this.motorPinConfiguration.shutdownFermentation();
    }
  
    private Boolean pHOutOfRange(Double phValue) {
      return (phValue < this.targetPH - MARGIN_OF_ACCEPTANCE_IN_PH && this.solution.equals(SOLUTION.BASE)) ||
          (phValue > this.targetPH + MARGIN_OF_ACCEPTANCE_IN_PH && this.solution.equals(SOLUTION.ACID));
    }
  
    private void run() throws InterruptedException {
      DateTime lastTimeSinceDoseAdministered = Time.now();
      DateTime currTime;
  
      while (true) {
        try {
          Thread.sleep(WAIT_TIME_BETWEEN_ACTION_IN_MILLI_SECONDS);
  
          currTime = Time.now();
          Long timeDiff = timeDifference(currTime, lastTimeSinceDoseAdministered);
  
          PHSensorData phSensorData = readPhSensorData(this.pHSensorDataFile);
          Double phValue = phSensorData.getpH();
          LOGGER.info("PH value is %d", phValue);
  
          if (timeDiff <= WAIT_TIME || !pHOutOfRange(phValue)) {
            continue;
          }
  
          takeAction();
          LOGGER.info("Took action when pH was %d", phValue);
          lastTimeSinceDoseAdministered = new DateTime();
  
        } catch (IOException e) {
          LOGGER.error("Could not read pH value due to IOException. Error is %s:", e.getMessage());
        } catch (InterruptedException e) {
          LOGGER.error("Could not read pH value due to InterruptedException. Error is %s:", e.getMessage());
        }
      }
    }
  
    public static void main(String[] args) throws Exception {
  
      Options opts = new Options();
      for (Option.Builder b : OPTION_BUILDERS) {
        opts.addOption(b.build());
      }
  
      CommandLine cl = null;
      try {
        CommandLineParser parser = new DefaultParser();
        cl = parser.parse(opts, args);
      } catch (ParseException e) {
        LOGGER.error(String.format("Argument parsing failed: %s\n", e.getMessage()));
        HELP_FORMATTER.printHelp(ControlSystem.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
        System.exit(1);
      }
  
      if (cl.hasOption("help")) {
        HELP_FORMATTER.printHelp(ControlSystem.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
        return;
      }
  
      SOLUTION solution = null;
      String acidOrBase = cl.getOptionValue(OPTION_CONTROL_SOLUTION);
  
      if (acidOrBase.equals(SOLUTION.ACID.name())) {
        solution = SOLUTION.ACID;
      }
  
      if (acidOrBase.equals(SOLUTION.BASE.name())) {
        solution = SOLUTION.BASE;
      }
  
      if (solution == null) {
        LOGGER.error("Input solution is neither %s or %s", SOLUTION.ACID.name(), SOLUTION.BASE.name());
        return;
      }
  
      Double targetPH = Double.parseDouble(cl.getOptionValue(OPTION_TARGET_PH));
  
      File sensorReadingDataFile = new File(cl.getOptionValue(OPTION_SENSOR_READING_FILE_LOCATION, SENSOR_READING_FILE_LOCATION));
  
      MotorPinConfiguration motorPinConfiguration = new MotorPinConfiguration(MotorPinConfiguration.PinNumberingScheme.BOARD);
      motorPinConfiguration.initializeGPIOPinsAndSetConfigToStartState();
  
      ControlSystem controlSystem = new ControlSystem(motorPinConfiguration, solution, targetPH, sensorReadingDataFile);
      controlSystem.registerModuleForObjectMapper(new JodaModule());
      try {
        controlSystem.run();
      } finally {
        LOGGER.info("Shutting down");
        controlSystem.shutdownFermentation();
      }
    }
  }
  