コード例 #1
0
  public boolean avrdude(Collection params) throws RunnerException {
    List commandDownloader = new ArrayList();
    commandDownloader.add("avrdude");

    // Point avrdude at its config file since it's in a non-standard location.
    if (Base.isMacOS()) {
      commandDownloader.add("-C" + "hardware/tools/avr/etc/avrdude.conf");
    } else if (Base.isWindows()) {
      String userdir = System.getProperty("user.dir") + File.separator;
      commandDownloader.add("-C" + userdir + "hardware/tools/avr/etc/avrdude.conf");
    } else {
      // ???: is it better to have Linux users install avrdude themselves, in
      // a way that it can find its own configuration file?
      commandDownloader.add("-C" + "hardware/tools/avrdude.conf");
    }

    if (Preferences.getBoolean("upload.verbose")) {
      commandDownloader.add("-v");
      commandDownloader.add("-v");
      commandDownloader.add("-v");
      commandDownloader.add("-v");
    } else {
      commandDownloader.add("-q");
      commandDownloader.add("-q");
    }
    // XXX: quick hack to chop the "atmega" off of "atmega8" and "atmega168",
    // then shove an "m" at the beginning.  won't work for attiny's, etc.
    commandDownloader.add(
        "-pm" + Preferences.get("boards." + Preferences.get("board") + ".build.mcu").substring(6));
    commandDownloader.addAll(params);

    return executeUploadCommand(commandDownloader);
  }
コード例 #2
0
ファイル: Uploader.java プロジェクト: yellowpark/Arduino
  protected boolean executeUploadCommand(Collection commandDownloader) throws RunnerException {
    firstErrorFound = false; // haven't found any errors yet
    secondErrorFound = false;
    notFoundError = false;
    int result = 0; // pre-initialized to quiet a bogus warning from jikes

    String userdir = System.getProperty("user.dir") + File.separator;

    try {
      String[] commandArray = new String[commandDownloader.size()];
      commandDownloader.toArray(commandArray);

      String avrBasePath;

      if (Base.isLinux()) {
        avrBasePath = new String(Base.getHardwarePath() + "/tools/");
      } else {
        avrBasePath = new String(Base.getHardwarePath() + "/tools/avr/bin/");
      }

      commandArray[0] = avrBasePath + commandArray[0];

      if (verbose || Preferences.getBoolean("upload.verbose")) {
        for (int i = 0; i < commandArray.length; i++) {
          System.out.print(commandArray[i] + " ");
        }
        System.out.println();
      }
      Process process = Runtime.getRuntime().exec(commandArray);
      new MessageSiphon(process.getInputStream(), this);
      new MessageSiphon(process.getErrorStream(), this);

      // wait for the process to finish.  if interrupted
      // before waitFor returns, continue waiting
      //
      boolean compiling = true;
      while (compiling) {
        try {
          result = process.waitFor();
          compiling = false;
        } catch (InterruptedException intExc) {
        }
      }
      if (exception != null) {
        exception.hideStackTrace();
        throw exception;
      }
      if (result != 0) return false;
    } catch (Exception e) {
      String msg = e.getMessage();
      if ((msg != null)
          && (msg.indexOf("uisp: not found") != -1)
          && (msg.indexOf("avrdude: not found") != -1)) {
        // System.err.println("uisp is missing");
        // JOptionPane.showMessageDialog(editor.base,
        //                              "Could not find the compiler.\n" +
        //                              "uisp is missing from your PATH,\n" +
        //                              "see readme.txt for help.",
        //                              "Compiler error",
        //                              JOptionPane.ERROR_MESSAGE);
        return false;
      } else {
        e.printStackTrace();
        result = -1;
      }
    }
    // System.out.println("result2 is "+result);
    // if the result isn't a known, expected value it means that something
    // is fairly wrong, one possibility is that jikes has crashed.
    //
    if (exception != null) throw exception;

    if ((result != 0) && (result != 1)) {
      exception = new RunnerException(SUPER_BADNESS);
      // editor.error(exception);
      // PdeBase.openURL(BUGS_URL);
      // throw new PdeException(SUPER_BADNESS);
    }

    return (result == 0); // ? true : false;
  }
コード例 #3
0
ファイル: DrawbotGUI.java プロジェクト: bertbalcaen/DrawBot
public class DrawbotGUI extends JPanel implements ActionListener, SerialPortEventListener {
  static final long serialVersionUID = 1;
  private static final String cue = "> ";
  private static final String eol = ";";
  private static final String NL = System.getProperty("line.separator");

  private static DrawbotGUI singletonObject;

  // Serial connection
  private static int BAUD_RATE = 57600;
  private CommPortIdentifier portIdentifier;
  private CommPort commPort;
  private SerialPort serialPort;
  private InputStream in;
  private OutputStream out;
  private String[] portsDetected;
  private boolean portOpened = false;
  private boolean portConfirmed = false;

  // Preferences
  private Preferences prefs = Preferences.userRoot().node("DrawBot");
  private String[] recentFiles;
  private String recentPort;

  // Robot config
  private long robot_uid = 0;

  private double limit_top = 10;
  private double limit_bottom = -10;
  private double limit_left = -10;
  private double limit_right = 10;

  // paper area (stock material
  private double paper_top = 10;
  private double paper_bottom = -10;
  private double paper_left = -10;
  private double paper_right = 10;
  private boolean m1invert = false;
  private boolean m2invert = false;

  // GUI elements
  private static JFrame mainframe;
  private JMenuBar menuBar;
  private JMenuItem buttonOpenFile, buttonExit;
  private JMenuItem buttonConfig, buttonRescan, buttonJogMotors;
  private JMenuItem buttonStart, buttonPause, buttonHalt, buttonDrive;
  private JMenuItem buttonZoomIn, buttonZoomOut;
  private JMenuItem buttonAbout, buttonCheckForUpdate;

  private JMenuItem[] buttonRecent = new JMenuItem[10];
  private JMenuItem[] buttonPorts;

  private JTextPane log;
  private JScrollPane logPane;
  HTMLEditorKit kit;
  HTMLDocument doc;

  private DrawPanel previewPane;
  private StatusBar statusBar;

  // parsing input from Drawbot
  private String line3 = "";

  // reading file
  private boolean running = false;
  private boolean paused = true;
  private long linesTotal = 0;
  private long linesProcessed = 0;
  private boolean fileOpened = false;
  private ArrayList<String> gcode;
  private float estimated_time = 0;

  // Singleton stuff
  private DrawbotGUI() {
    LoadConfig();
    GetRecentFiles();
    GetRecentPaperSize();
    GetRecentPort();
  }

  public static DrawbotGUI getSingleton() {
    if (singletonObject == null) {
      singletonObject = new DrawbotGUI();
    }
    return singletonObject;
  }

  //  data access
  public ArrayList<String> getGcode() {
    return gcode;
  }

  // manages the vertical split in the GUI
  public class Splitter extends JSplitPane {
    static final long serialVersionUID = 1;

    public Splitter(int split_direction) {
      super(split_direction);
      setResizeWeight(0.9);
      setDividerLocation(0.9);
    }
  }

  public void LoadImage(String filename) {
    BufferedImage img;

    try {
      img = ImageIO.read(new File(filename));

      Filter_BlackAndWhite bwc = new Filter_BlackAndWhite();
      img = bwc.Process(img);

      Filter_Resize rs =
          new Filter_Resize(paper_top, paper_bottom, paper_left, paper_right, 1, 0.9f);
      img = rs.Process(img);

      Filter_DitherFloydSteinberg dither = new Filter_DitherFloydSteinberg();
      img = dither.Process(img);

      String ngcPair = filename.substring(0, filename.lastIndexOf('.')) + ".ngc";
      Filter_TSPGcodeGenerator tsp = new Filter_TSPGcodeGenerator(ngcPair);
      tsp.Process(img);
    } catch (IOException e) {
    }
  }

  // returns angle of dy/dx as a value from 0...2PI
  private double atan3(double dy, double dx) {
    double a = Math.atan2(dy, dx);
    if (a < 0) a = (Math.PI * 2.0) + a;
    return a;
  }

  // appends a message to the log tab and system out.
  public void Log(String msg) {
    try {
      kit.insertHTML(doc, doc.getLength(), msg, 0, 0, null);
      int over_length = doc.getLength() - msg.length() - 5000;
      doc.remove(0, over_length);
    } catch (BadLocationException e) {
      // TODO Auto-generated catch block
      // e.printStackTrace();
    } catch (IOException e) {
      // TODO Auto-generated catch block
      // e.printStackTrace();
    }
  }

  public void ClearLog() {
    try {
      doc.replace(0, doc.getLength(), "", null);
      kit.insertHTML(doc, 0, "", 0, 0, null);
    } catch (BadLocationException e) {
      // TODO Auto-generated catch block
      // e.printStackTrace();
    } catch (IOException e) {
      // TODO Auto-generated catch block
      // e.printStackTrace();
    }
  }

  public void ClosePort() {
    if (portOpened) {
      if (serialPort != null) {
        try {
          // Close the I/O streams.
          out.close();
          in.close();
          // Close the port.
          serialPort.removeEventListener();
          serialPort.close();
        } catch (IOException e) {
          // Don't care
        }
      }

      ClearLog();
      portOpened = false;
      portConfirmed = false;
      previewPane.setConnected(false);
      UpdateMenuBar();
    }
  }

  // open a serial connection to a device.  We won't know it's the robot until
  public int OpenPort(String portName) {
    if (portOpened && portName.equals(recentPort)) return 0;

    ClosePort();

    Log("<font color='green'>Connecting to " + portName + "...</font>\n");

    // find the port
    try {
      portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
    } catch (Exception e) {
      Log("<span style='color:red'>Ports could not be identified:" + e.getMessage() + "</span>\n");
      e.printStackTrace();
      return 1;
    }

    if (portIdentifier.isCurrentlyOwned()) {
      Log(
          "<span style='color:red'>Error: Another program is currently using this port."
              + "</span>\n");
      return 2;
    }

    // open the port
    try {
      commPort = portIdentifier.open("DrawbotGUI", 2000);
    } catch (Exception e) {
      Log("Port could not be opened:" + e.getMessage() + NL);
      e.printStackTrace();
      return 3;
    }

    if ((commPort instanceof SerialPort) == false) {
      Log("<span style='color:red'>Only serial ports are handled by this example." + "</span>\n");
      return 4;
    }

    // set the port parameters (like baud rate)
    serialPort = (SerialPort) commPort;
    try {
      serialPort.setSerialPortParams(
          BAUD_RATE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
    } catch (Exception e) {
      Log("<span style='color:red'>Port could not be configured:" + e.getMessage() + "</span>\n");
      return 5;
    }

    try {
      in = serialPort.getInputStream();
      out = serialPort.getOutputStream();
    } catch (Exception e) {
      Log("<span style='color:red'>Streams could not be opened:" + e.getMessage() + "</span>\n");
      return 6;
    }

    try {
      serialPort.addEventListener(this);
      serialPort.notifyOnDataAvailable(true);
    } catch (TooManyListenersException e) {
      Log("<span style='color:red'>Streams could not be opened:" + e.getMessage() + "</span>\n");
      return 7;
    }

    Log("<span style='green'>Opened.</span>\n");
    SetRecentPort(portName);
    portOpened = true;
    UpdateMenuBar();

    return 0;
  }

  /**
   * Complete the handshake, load robot-specific configuration, update the menu, repaint the preview
   * with the limits.
   *
   * @return true if handshake succeeds.
   */
  public boolean ConfirmPort() {
    if (portConfirmed == true) return true;
    String hello = "HELLO WORLD! I AM DRAWBOT #";
    int found = line3.lastIndexOf(hello);
    if (found >= 0) {
      portConfirmed = true;

      // get the UID reported by the robot
      String[] lines = line3.substring(found + hello.length()).split("\\r?\\n");
      if (lines.length > 0) {
        try {
          robot_uid = Long.parseLong(lines[0]);
        } catch (NumberFormatException e) {
        }
      }

      // new robots have UID=0
      if (robot_uid == 0) GetNewRobotUID();

      mainframe.setTitle("Drawbot #" + Long.toString(robot_uid) + " connected");

      // load machine specific config
      GetRecentPaperSize();
      LoadConfig();
      if (limit_top == 0 && limit_bottom == 0 && limit_left == 0 && limit_right == 0) {
        UpdateConfig();
      }

      previewPane.setMachineLimits(limit_top, limit_bottom, limit_left, limit_right);
      SendConfig();

      UpdateMenuBar();
      previewPane.setConnected(true);
    }
    return portConfirmed;
  }

  /** based on http://www.exampledepot.com/egs/java.net/Post.html */
  private void GetNewRobotUID() {
    try {
      // Send data
      URL url = new URL("http://marginallyclever.com/drawbot_getuid.php");
      URLConnection conn = url.openConnection();
      BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
      robot_uid = Long.parseLong(rd.readLine());
      rd.close();
    } catch (Exception e) {
    }

    // did read go ok?
    if (robot_uid != 0) {
      SendLineToRobot("UID " + robot_uid);
    }
  }

  // find all available serial ports for the settings->ports menu.
  public String[] ListSerialPorts() {
    @SuppressWarnings("unchecked")
    Enumeration<CommPortIdentifier> ports =
        (Enumeration<CommPortIdentifier>) CommPortIdentifier.getPortIdentifiers();
    ArrayList<String> portList = new ArrayList<String>();
    while (ports.hasMoreElements()) {
      CommPortIdentifier port = (CommPortIdentifier) ports.nextElement();
      if (port.getPortType() == CommPortIdentifier.PORT_SERIAL) {
        portList.add(port.getName());
      }
    }
    portsDetected = (String[]) portList.toArray(new String[0]);
    return portsDetected;
  }

  // pull the last connected port from prefs
  public void GetRecentPort() {
    recentPort = prefs.get("recent-port", "");
  }

  // update the prefs with the last port connected and refreshes the menus.
  // @TODO: only update when the port is confirmed?
  public void SetRecentPort(String portName) {
    prefs.put("recent-port", portName);
    recentPort = portName;
    UpdateMenuBar();
  }

  // save paper limits
  public void SetRecentPaperSize() {
    String id = Long.toString(robot_uid);
    prefs.putDouble(id + "_paper_left", paper_left);
    prefs.putDouble(id + "_paper_right", paper_right);
    prefs.putDouble(id + "_paper_top", paper_top);
    prefs.putDouble(id + "_paper_bottom", paper_bottom);
    previewPane.setPaperSize(paper_top, paper_bottom, paper_left, paper_right);
  }

  public void GetRecentPaperSize() {
    String id = Long.toString(robot_uid);
    paper_left = Double.parseDouble(prefs.get(id + "_paper_left", "0"));
    paper_right = Double.parseDouble(prefs.get(id + "_paper_right", "0"));
    paper_top = Double.parseDouble(prefs.get(id + "_paper_top", "0"));
    paper_bottom = Double.parseDouble(prefs.get(id + "_paper_bottom", "0"));
  }

  // close the file, clear the preview tab
  public void CloseFile() {
    if (fileOpened == true) {
      fileOpened = false;
    }
  }

  void EstimateDrawTime() {
    int i, j;

    float drawScale = 1.0f;
    double px = 0, py = 0, pz = 0;
    float feed_rate = 1.0f;

    estimated_time = 0;
    float estimated_length = 0;
    int estimate_count = 0;

    for (i = 0; i < gcode.size(); ++i) {
      String line = gcode.get(i);
      String[] pieces = line.split(";");
      if (pieces.length == 0) continue;

      String[] tokens = pieces[0].split("\\s");
      if (tokens.length == 0) continue;

      for (j = 0; j < tokens.length; ++j) {
        if (tokens[j].equals("G20")) drawScale = 0.393700787f;
        if (tokens[j].equals("G21")) drawScale = 0.1f;
        if (tokens[j].startsWith("F")) {
          feed_rate = Float.valueOf(tokens[j].substring(1)) * drawScale;
          Log("<span style='color:green'>feed rate=" + feed_rate + "</span>\n");
          feed_rate *= 1;
        }
      }

      double x = px;
      double y = py;
      double z = pz;
      double ai = px;
      double aj = py;
      for (j = 1; j < tokens.length; ++j) {
        if (tokens[j].startsWith("X")) x = Float.valueOf(tokens[j].substring(1)) * drawScale;
        if (tokens[j].startsWith("Y")) y = Float.valueOf(tokens[j].substring(1)) * drawScale;
        if (tokens[j].startsWith("Z")) z = Float.valueOf(tokens[j].substring(1)) * drawScale;
        if (tokens[j].startsWith("I")) ai = px + Float.valueOf(tokens[j].substring(1)) * drawScale;
        if (tokens[j].startsWith("J")) aj = py + Float.valueOf(tokens[j].substring(1)) * drawScale;
      }

      if (tokens[0].equals("G00")
          || tokens[0].equals("G0")
          || tokens[0].equals("G01")
          || tokens[0].equals("G1")) {
        // draw a line
        double ddx = x - px;
        double ddy = y - py;
        double dd = Math.sqrt(ddx * ddx + ddy * ddy);
        estimated_time += dd / feed_rate;
        estimated_length += dd;
        ++estimate_count;
        px = x;
        py = y;
        pz = z;
      } else if (tokens[0].equals("G02")
          || tokens[0].equals("G2")
          || tokens[0].equals("G03")
          || tokens[0].equals("G3")) {
        // draw an arc
        int dir = (tokens[0].equals("G02") || tokens[0].equals("G2")) ? -1 : 1;
        double dx = px - ai;
        double dy = py - aj;
        double radius = Math.sqrt(dx * dx + dy * dy);

        // find angle of arc (sweep)
        double angle1 = atan3(dy, dx);
        double angle2 = atan3(y - aj, x - ai);
        double theta = angle2 - angle1;

        if (dir > 0 && theta < 0) angle2 += 2.0 * Math.PI;
        else if (dir < 0 && theta > 0) angle1 += 2.0 * Math.PI;

        theta = Math.abs(angle2 - angle1);

        // Draw the arc from a lot of little line segments.
        for (int k = 0; k <= theta * DrawPanel.STEPS_PER_DEGREE; ++k) {
          double angle3 =
              (angle2 - angle1) * ((double) k / (theta * DrawPanel.STEPS_PER_DEGREE)) + angle1;
          float nx = (float) (ai + Math.cos(angle3) * radius);
          float ny = (float) (aj + Math.sin(angle3) * radius);

          double ddx = nx - px;
          double ddy = ny - py;
          double dd = Math.sqrt(ddx * ddx + ddy * ddy);
          estimated_time += dd / feed_rate;
          estimated_length += dd;
          ++estimate_count;
          px = nx;
          py = ny;
        }
        double ddx = x - px;
        double ddy = y - py;
        double dd = Math.sqrt(ddx * ddx + ddy * ddy);
        estimated_time += dd / feed_rate;
        estimated_length += dd;
        ++estimate_count;
        px = x;
        py = y;
        pz = z;
      }
    } // for ( each instruction )
    estimated_time += estimate_count * 0.007617845117845f;

    Log(
        "<font color='green'>"
            + estimate_count
            + " line segments.\n"
            + estimated_length
            + "cm of line.\n"
            + "Estimated "
            + statusBar.formatTime((long) (estimated_time * 10000))
            + "s to draw.</font>\n");
  }

  /**
   * Opens a file. If the file can be opened, get a drawing time estimate, update recent files list,
   * and repaint the preview tab.
   *
   * @param filename what file to open
   */
  public void LoadGCode(String filename) {
    CloseFile();

    try {
      Scanner scanner = new Scanner(new FileInputStream(filename));
      linesTotal = 0;
      gcode = new ArrayList<String>();
      try {
        while (scanner.hasNextLine()) {
          gcode.add(scanner.nextLine());
          ++linesTotal;
        }
      } finally {
        scanner.close();
      }
    } catch (IOException e) {
      Log("<span style='color:red'>File could not be opened.</span>\n");
      RemoveRecentFile(filename);
      return;
    }

    previewPane.setGCode(gcode);
    fileOpened = true;

    EstimateDrawTime();
    UpdateRecentFiles(filename);
    Halt();
  }

  /**
   * changes the order of the recent files list in the File submenu, saves the updated prefs, and
   * refreshes the menus.
   *
   * @param filename the file to push to the top of the list.
   */
  public void UpdateRecentFiles(String filename) {
    int cnt = recentFiles.length;
    String[] newFiles = new String[cnt];

    newFiles[0] = filename;

    int i, j = 1;
    for (i = 0; i < cnt; ++i) {
      if (!filename.equals(recentFiles[i]) && recentFiles[i] != "") {
        newFiles[j++] = recentFiles[i];
        if (j == cnt) break;
      }
    }

    recentFiles = newFiles;

    // update prefs
    for (i = 0; i < cnt; ++i) {
      if (recentFiles[i] != null && !recentFiles[i].isEmpty()) {
        prefs.put("recent-files-" + i, recentFiles[i]);
      }
    }

    UpdateMenuBar();
  }

  // A file failed to load.  Remove it from recent files, refresh the menu bar.
  public void RemoveRecentFile(String filename) {
    int i;
    for (i = 0; i < recentFiles.length - 1; ++i) {
      if (recentFiles[i] == filename) {
        break;
      }
    }
    for (; i < recentFiles.length - 1; ++i) {
      recentFiles[i] = recentFiles[i + 1];
    }
    recentFiles[recentFiles.length - 1] = "";

    // update prefs
    for (i = 0; i < recentFiles.length; ++i) {
      if (recentFiles[i] != null && !recentFiles[i].isEmpty()) {
        prefs.put("recent-files-" + i, recentFiles[i]);
      }
    }

    UpdateMenuBar();
  }

  // Load recent files from prefs
  public void GetRecentFiles() {
    recentFiles = new String[10];

    int i;
    for (i = 0; i < recentFiles.length; ++i) {
      recentFiles[i] = prefs.get("recent-files-" + i, "");
    }
  }

  // User has asked that a file be opened.
  public void OpenFileOnDemand(String filename) {
    Log("<font color='green'>Opening file " + recentFiles[0] + "...</font>\n");

    String ext = filename.substring(filename.lastIndexOf('.'));
    if (!ext.equalsIgnoreCase(".ngc")) {
      LoadImage(filename);
    } else {
      LoadGCode(filename);
    }

    statusBar.Clear();
  }

  // creates a file open dialog. If you don't cancel it opens that file.
  public void OpenFileDialog() {
    // Note: source for ExampleFileFilter can be found in FileChooserDemo,
    // under the demo/jfc directory in the Java 2 SDK, Standard Edition.
    String filename = (recentFiles[0].length() > 0) ? filename = recentFiles[0] : "";

    FileFilter filterImage =
        new FileNameExtensionFilter(
            "Images (jpg/bmp/png/gif)", "jpg", "jpeg", "png", "wbmp", "bmp", "gif");
    FileFilter filterGCODE = new FileNameExtensionFilter("GCODE files (ngc)", "ngc");

    JFileChooser fc = new JFileChooser(new File(filename));
    fc.addChoosableFileFilter(filterImage);
    fc.addChoosableFileFilter(filterGCODE);
    if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
      OpenFileOnDemand(fc.getSelectedFile().getAbsolutePath());
    }
  }

  public void GoHome() {
    SendLineToRobot("G00 X0 Y0 Z0");
  }

  /**
   * Open the config dialog, send the config update to the robot, save it for future, and refresh
   * the preview tab.
   */
  public void UpdateConfig() {
    final JDialog driver = new JDialog(mainframe, "Configure Limits", true);
    driver.setLayout(new GridBagLayout());

    final JTextField mtop = new JTextField(String.valueOf(limit_top));
    final JTextField mbottom = new JTextField(String.valueOf(limit_bottom));
    final JTextField mleft = new JTextField(String.valueOf(limit_left));
    final JTextField mright = new JTextField(String.valueOf(limit_right));

    final JTextField ptop = new JTextField(String.valueOf(paper_top));
    final JTextField pbottom = new JTextField(String.valueOf(paper_bottom));
    final JTextField pleft = new JTextField(String.valueOf(paper_left));
    final JTextField pright = new JTextField(String.valueOf(paper_right));

    final JButton cancel = new JButton("Cancel");
    final JButton save = new JButton("Save");

    GridBagConstraints c = new GridBagConstraints();
    c.gridx = 3;
    c.gridy = 0;
    driver.add(mtop, c);
    c.gridx = 3;
    c.gridy = 5;
    driver.add(mbottom, c);
    c.gridx = 0;
    c.gridy = 3;
    driver.add(mleft, c);
    c.gridx = 5;
    c.gridy = 3;
    driver.add(mright, c);

    c.gridx = 3;
    c.gridy = 1;
    driver.add(ptop, c);
    c.gridx = 3;
    c.gridy = 4;
    driver.add(pbottom, c);
    c.gridx = 1;
    c.gridy = 3;
    driver.add(pleft, c);
    c.gridx = 4;
    c.gridy = 3;
    driver.add(pright, c);

    c.gridx = 4;
    c.gridy = 6;
    driver.add(save, c);
    c.gridx = 5;
    c.gridy = 6;
    driver.add(cancel, c);

    Dimension s = ptop.getPreferredSize();
    s.width = 80;
    ptop.setPreferredSize(s);
    pbottom.setPreferredSize(s);
    pleft.setPreferredSize(s);
    pright.setPreferredSize(s);
    mtop.setPreferredSize(s);
    mbottom.setPreferredSize(s);
    mleft.setPreferredSize(s);
    mright.setPreferredSize(s);

    ActionListener driveButtons =
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            Object subject = e.getSource();
            if (subject == save) {
              paper_top = Float.valueOf(ptop.getText());
              paper_bottom = Float.valueOf(pbottom.getText());
              paper_right = Float.valueOf(pright.getText());
              paper_left = Float.valueOf(pleft.getText());
              limit_top = Float.valueOf(mtop.getText());
              limit_bottom = Float.valueOf(mbottom.getText());
              limit_right = Float.valueOf(mright.getText());
              limit_left = Float.valueOf(mleft.getText());
              previewPane.setMachineLimits(limit_top, limit_bottom, limit_left, limit_right);
              previewPane.setPaperSize(paper_top, paper_bottom, paper_left, paper_right);
              SetRecentPaperSize();
              SaveConfig();
              SendConfig();
              driver.dispose();
            }
            if (subject == cancel) {
              driver.dispose();
            }
          }
        };

    save.addActionListener(driveButtons);
    cancel.addActionListener(driveButtons);
    SendLineToRobot("M114"); // "where" command
    driver.pack();
    driver.setVisible(true);
  }

  void LoadConfig() {
    String id = Long.toString(robot_uid);
    limit_top = Double.valueOf(prefs.get(id + "_limit_top", "0"));
    limit_bottom = Double.valueOf(prefs.get(id + "_limit_bottom", "0"));
    limit_left = Double.valueOf(prefs.get(id + "_limit_left", "0"));
    limit_right = Double.valueOf(prefs.get(id + "_limit_right", "0"));
    m1invert = Boolean.parseBoolean(prefs.get(id + "_m1invert", "false"));
    m2invert = Boolean.parseBoolean(prefs.get(id + "_m2invert", "false"));
  }

  void SaveConfig() {
    String id = Long.toString(robot_uid);
    prefs.put(id + "_limit_top", Double.toString(limit_top));
    prefs.put(id + "_limit_bottom", Double.toString(limit_bottom));
    prefs.put(id + "_limit_right", Double.toString(limit_right));
    prefs.put(id + "_limit_left", Double.toString(limit_left));
    prefs.put(id + "_m1invert", Boolean.toString(m1invert));
    prefs.put(id + "_m2invert", Boolean.toString(m2invert));
  }

  void SendConfig() {
    if (!portConfirmed) return;

    // Send a command to the robot with new configuration values
    String line =
        "CONFIG T"
            + limit_top
            + " B"
            + limit_bottom
            + " L"
            + limit_left
            + " R"
            + limit_right
            + " I"
            + (m1invert ? "-1" : "1")
            + " J"
            + (m2invert ? "-1" : "1");
    SendLineToRobot(line);
    SendLineToRobot("TELEPORT X0 Y0 Z0");
  }

  // Take the next line from the file and send it to the robot, if permitted.
  public void SendFileCommand() {
    if (running == false
        || paused == true
        || fileOpened == false
        || portConfirmed == false
        || linesProcessed >= linesTotal) return;

    String line;
    do {
      // are there any more commands?
      line = gcode.get((int) linesProcessed++).trim();
      previewPane.setLinesProcessed(linesProcessed);
      statusBar.SetProgress(linesProcessed, linesTotal);
      // loop until we find a line that gets sent to the robot, at which point we'll
      // pause for the robot to respond.  Also stop at end of file.
    } while (ProcessLine(line) && linesProcessed < linesTotal);

    if (linesProcessed == linesTotal) {
      // end of file
      Halt();
    }
  }

  /**
   * removes comments, processes commands drawbot shouldn't have to handle.
   *
   * @param line command to send
   * @return true if the robot is ready for another command to be sent.
   */
  public boolean ProcessLine(String line) {
    // tool change request?
    String[] tokens = line.split("\\s");

    // tool change?
    if (Arrays.asList(tokens).contains("M06") || Arrays.asList(tokens).contains("M6")) {
      for (int i = 0; i < tokens.length; ++i) {
        if (tokens[i].startsWith("T")) {
          JOptionPane.showMessageDialog(
              null, "Please change to tool #" + tokens[i].substring(1) + " and click OK.");
        }
      }
      // still ready to send
      return true;
    }

    // end of program?
    if (tokens[0] == "M02" || tokens[0] == "M2") {
      Halt();
      return false;
    }

    // other machine code to ignore?
    if (tokens[0].startsWith("M")) {
      Log("<font color='pink'>" + line + "</font>\n");
      return true;
    }

    // contains a comment?  if so remove it
    int index = line.indexOf('(');
    if (index != -1) {
      String comment = line.substring(index + 1, line.lastIndexOf(')'));
      line = line.substring(0, index).trim();
      Log("<font color='grey'>* " + comment + "</font\n");
      if (line.length() == 0) {
        // entire line was a comment.
        return true; // still ready to send
      }
    }

    // send relevant part of line to the robot
    SendLineToRobot(line);

    return false;
  }

  /**
   * Sends a single command the robot. Could be anything.
   *
   * @param line command to send.
   * @return true means the command is sent. false means it was not.
   */
  public void SendLineToRobot(String line) {
    if (!portConfirmed) return;

    line += eol;
    Log("<font color='white'>" + line + "</font>");
    try {
      out.write(line.getBytes());
    } catch (IOException e) {
    }
  }

  /**
   * stop sending file commands to the robot.
   *
   * @todo add an e-stop command?
   */
  public void Halt() {
    running = false;
    paused = false;
    linesProcessed = 0;
    previewPane.setLinesProcessed(0);
    previewPane.setRunning(running);
    UpdateMenuBar();
  }

  // The user has done something.  respond to it.
  public void actionPerformed(ActionEvent e) {
    Object subject = e.getSource();

    if (subject == buttonZoomIn) {
      previewPane.ZoomIn();
      return;
    }
    if (subject == buttonZoomOut) {
      previewPane.ZoomOut();
      return;
    }
    if (subject == buttonOpenFile) {
      OpenFileDialog();
      return;
    }

    if (subject == buttonStart) {
      if (fileOpened) {
        paused = false;
        running = true;
        UpdateMenuBar();
        linesProcessed = 0;
        previewPane.setRunning(running);
        previewPane.setLinesProcessed(linesProcessed);
        statusBar.Start();
        SendFileCommand();
      }
      return;
    }
    if (subject == buttonPause) {
      if (running) {
        if (paused == true) {
          buttonPause.setText("Pause");
          paused = false;
          // @TODO: if the robot is not ready to unpause, this might fail and the program would
          // appear to hang.
          SendFileCommand();
        } else {
          buttonPause.setText("Unpause");
          paused = true;
        }
      }
      return;
    }
    if (subject == buttonDrive) {
      Drive();
      return;
    }
    if (subject == buttonHalt) {
      Halt();
      return;
    }
    if (subject == buttonRescan) {
      ListSerialPorts();
      UpdateMenuBar();
      return;
    }
    if (subject == buttonConfig) {
      UpdateConfig();
      return;
    }
    if (subject == buttonJogMotors) {
      JogMotors();
      return;
    }
    if (subject == buttonAbout) {
      JOptionPane.showMessageDialog(
          null,
          "Created by Dan Royer ([email protected]).\n\n"
              + "Find out more at http://www.marginallyclever.com/\n"
              + "Get the latest version and read the documentation online @ http://github.com/i-make-robots/DrawBot/");
      return;
    }
    if (subject == buttonCheckForUpdate) {
      CheckForUpdate();
      return;
    }
    if (subject == buttonExit) {
      System.exit(0); // @TODO: be more graceful?
      return;
    }

    int i;
    for (i = 0; i < 10; ++i) {
      if (subject == buttonRecent[i]) {
        OpenFileOnDemand(recentFiles[i]);
        return;
      }
    }

    for (i = 0; i < portsDetected.length; ++i) {
      if (subject == buttonPorts[i]) {
        OpenPort(portsDetected[i]);
        return;
      }
    }
  }

  // Deal with something robot has sent.
  public void serialEvent(SerialPortEvent events) {
    switch (events.getEventType()) {
      case SerialPortEvent.DATA_AVAILABLE:
        try {
          final byte[] buffer = new byte[1024];
          int len = in.read(buffer);
          if (len > 0) {
            String line2 = new String(buffer, 0, len);
            Log("<span style='color:#FFA500'>" + line2.replace("\n", "<br>") + "</span>");
            line3 += line2;
            // wait for the cue ("> ") to send another command
            if (line3.lastIndexOf(cue) != -1) {
              if (ConfirmPort()) {
                line3 = "";
                SendFileCommand();
              }
            }
          }
        } catch (IOException e) {
        }
        break;
    }
  }

  public void CheckForUpdate() {
    /*
    try {
        // Send data
    	URL url = new URL("http://marginallyclever.com/drawbot_get.php");
        URLConnection conn = url.openConnection();
        BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        robot_uid = Long.parseLong(rd.readLine());
        rd.close();
    } catch (Exception e) {}
     */
    // TODO Get latest version
    // TODO Offer to download latest version?
  }

  /** Open the config dialog, update the paper size, refresh the preview tab. */
  public void Drive() {
    JDialog driver = new JDialog(mainframe, "Manual Control", true);
    driver.setLayout(new GridBagLayout());

    JButton find = new JButton("FIND HOME");
    JButton home = new JButton("GO HOME");
    JButton center = new JButton("THIS IS HOME");

    JButton up1 = new JButton("Y1");
    JButton up10 = new JButton("Y10");
    JButton up100 = new JButton("Y100");

    JButton down1 = new JButton("Y-1");
    JButton down10 = new JButton("Y-10");
    JButton down100 = new JButton("Y-100");

    JButton left1 = new JButton("X-1");
    JButton left10 = new JButton("X-10");
    JButton left100 = new JButton("X-100");

    JButton right1 = new JButton("X1");
    JButton right10 = new JButton("X10");
    JButton right100 = new JButton("X100");

    GridBagConstraints c = new GridBagConstraints();
    c.gridx = 3;
    c.gridy = 0;
    driver.add(up100, c);
    c.gridx = 3;
    c.gridy = 1;
    driver.add(up10, c);
    c.gridx = 3;
    c.gridy = 2;
    driver.add(up1, c);
    c.gridx = 3;
    c.gridy = 4;
    driver.add(down1, c);
    c.gridx = 3;
    c.gridy = 5;
    driver.add(down10, c);
    c.gridx = 3;
    c.gridy = 6;
    driver.add(down100, c);

    c.gridx = 0;
    c.gridy = 3;
    driver.add(left100, c);
    c.gridx = 1;
    c.gridy = 3;
    driver.add(left10, c);
    c.gridx = 2;
    c.gridy = 3;
    driver.add(left1, c);
    c.gridx = 4;
    c.gridy = 3;
    driver.add(right1, c);
    c.gridx = 5;
    c.gridy = 3;
    driver.add(right10, c);
    c.gridx = 6;
    c.gridy = 3;
    driver.add(right100, c);

    c.gridx = 3;
    c.gridy = 3;
    driver.add(home, c);
    c.gridx = 6;
    c.gridy = 0;
    driver.add(center, c);
    c.gridx = 6;
    c.gridy = 1;
    driver.add(find, c);

    ActionListener driveButtons =
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            Object subject = e.getSource();
            JButton b = (JButton) subject;
            String t = b.getText();
            if (t == "GO HOME") {
              GoHome();
              SendLineToRobot("M114");
            } else if (t == "FIND HOME") {
              SendLineToRobot("G28");
            } else if (t == "THIS IS HOME") {
              SendLineToRobot("TELEPORT XO YO");
            } else {
              SendLineToRobot("G91");
              SendLineToRobot("G00 " + b.getText());
              SendLineToRobot("G90");
              SendLineToRobot("M114");
            }
          }
        };

    up1.addActionListener(driveButtons);
    up10.addActionListener(driveButtons);
    up100.addActionListener(driveButtons);
    down1.addActionListener(driveButtons);
    down10.addActionListener(driveButtons);
    down100.addActionListener(driveButtons);
    left1.addActionListener(driveButtons);
    left10.addActionListener(driveButtons);
    left100.addActionListener(driveButtons);
    right1.addActionListener(driveButtons);
    right10.addActionListener(driveButtons);
    right100.addActionListener(driveButtons);
    center.addActionListener(driveButtons);
    home.addActionListener(driveButtons);
    find.addActionListener(driveButtons);
    SendLineToRobot("M114");
    driver.pack();
    driver.setVisible(true);
  }

  protected void JogMotors() {
    JDialog driver = new JDialog(mainframe, "Jog Motors", true);
    driver.setLayout(new GridBagLayout());
    GridBagConstraints c = new GridBagConstraints();

    final JButton buttonAneg = new JButton("IN");
    final JButton buttonApos = new JButton("OUT");
    final JCheckBox m1i = new JCheckBox("Invert", m1invert);

    final JButton buttonBneg = new JButton("IN");
    final JButton buttonBpos = new JButton("OUT");
    final JCheckBox m2i = new JCheckBox("Invert", m2invert);

    c.gridx = 0;
    c.gridy = 0;
    driver.add(new JLabel("L"), c);
    c.gridx = 0;
    c.gridy = 1;
    driver.add(new JLabel("R"), c);

    c.gridx = 1;
    c.gridy = 0;
    driver.add(buttonAneg, c);
    c.gridx = 1;
    c.gridy = 1;
    driver.add(buttonBneg, c);

    c.gridx = 2;
    c.gridy = 0;
    driver.add(buttonApos, c);
    c.gridx = 2;
    c.gridy = 1;
    driver.add(buttonBpos, c);

    c.gridx = 3;
    c.gridy = 0;
    driver.add(m1i, c);
    c.gridx = 3;
    c.gridy = 1;
    driver.add(m2i, c);

    ActionListener driveButtons =
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            Object subject = e.getSource();
            if (subject == buttonApos) SendLineToRobot("D00 L100");
            if (subject == buttonAneg) SendLineToRobot("D00 L-100");
            if (subject == buttonBpos) SendLineToRobot("D00 R100");
            if (subject == buttonBneg) SendLineToRobot("D00 R-100");
            SendLineToRobot("M114");
          }
        };

    ActionListener invertButtons =
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            m1invert = m1i.isSelected();
            m2invert = m2i.isSelected();

            SaveConfig();
            SendConfig();
          }
        };

    buttonApos.addActionListener(driveButtons);
    buttonAneg.addActionListener(driveButtons);

    buttonBpos.addActionListener(driveButtons);
    buttonBneg.addActionListener(driveButtons);

    m1i.addActionListener(invertButtons);
    m2i.addActionListener(invertButtons);

    SendLineToRobot("M114");
    driver.pack();
    driver.setVisible(true);
  }

  public JMenuBar CreateMenuBar() {
    // If the menu bar exists, empty it.  If it doesn't exist, create it.
    menuBar = new JMenuBar();

    UpdateMenuBar();

    return menuBar;
  }

  // Rebuild the contents of the menu based on current program state
  public void UpdateMenuBar() {
    JMenu menu;
    int i;

    menuBar.removeAll();

    // Build the first menu.
    menu = new JMenu("File");
    menu.setMnemonic(KeyEvent.VK_F);
    menuBar.add(menu);

    buttonOpenFile = new JMenuItem("Open File...", KeyEvent.VK_O);
    buttonOpenFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.ALT_MASK));
    buttonOpenFile.getAccessibleContext().setAccessibleDescription("Open a g-code file...");
    buttonOpenFile.addActionListener(this);
    menu.add(buttonOpenFile);

    menu.addSeparator();

    // list recent files
    if (recentFiles != null && recentFiles.length > 0) {
      // list files here
      for (i = 0; i < recentFiles.length; ++i) {
        if (recentFiles[i] == null || recentFiles[i].length() == 0) break;
        buttonRecent[i] = new JMenuItem((1 + i) + " " + recentFiles[i], KeyEvent.VK_1 + i);
        if (buttonRecent[i] != null) {
          buttonRecent[i].addActionListener(this);
          menu.add(buttonRecent[i]);
        }
      }
      if (i != 0) menu.addSeparator();
    }

    buttonExit = new JMenuItem("Exit", KeyEvent.VK_Q);
    buttonExit.getAccessibleContext().setAccessibleDescription("Goodbye...");
    buttonExit.addActionListener(this);
    menu.add(buttonExit);

    menuBar.add(menu);

    // settings menu
    menu = new JMenu("Settings");
    menu.setMnemonic(KeyEvent.VK_T);
    menu.getAccessibleContext().setAccessibleDescription("Adjust the robot settings.");

    JMenu subMenu = new JMenu("Port");
    subMenu.setMnemonic(KeyEvent.VK_P);
    subMenu.getAccessibleContext().setAccessibleDescription("What port to connect to?");
    subMenu.setEnabled(!running);
    ButtonGroup group = new ButtonGroup();

    ListSerialPorts();
    buttonPorts = new JRadioButtonMenuItem[portsDetected.length];
    for (i = 0; i < portsDetected.length; ++i) {
      buttonPorts[i] = new JRadioButtonMenuItem(portsDetected[i]);
      if (recentPort.equals(portsDetected[i]) && portOpened) {
        buttonPorts[i].setSelected(true);
      }
      buttonPorts[i].addActionListener(this);
      group.add(buttonPorts[i]);
      subMenu.add(buttonPorts[i]);
    }

    subMenu.addSeparator();

    buttonRescan = new JMenuItem("Rescan", KeyEvent.VK_N);
    buttonRescan.getAccessibleContext().setAccessibleDescription("Rescan the available ports.");
    buttonRescan.addActionListener(this);
    subMenu.add(buttonRescan);

    menu.add(subMenu);

    buttonConfig = new JMenuItem("Configure limits", KeyEvent.VK_L);
    buttonConfig.getAccessibleContext().setAccessibleDescription("Adjust the robot & paper shape.");
    buttonConfig.addActionListener(this);
    buttonConfig.setEnabled(portConfirmed && !running);
    menu.add(buttonConfig);

    buttonJogMotors = new JMenuItem("Jog Motors", KeyEvent.VK_J);
    buttonJogMotors.addActionListener(this);
    buttonJogMotors.setEnabled(portConfirmed && !running);
    menu.add(buttonJogMotors);

    buttonDrive = new JMenuItem("Drive Manually", KeyEvent.VK_R);
    buttonDrive.getAccessibleContext().setAccessibleDescription("Etch-a-sketch style driving");
    buttonDrive.addActionListener(this);
    buttonDrive.setEnabled(portConfirmed && !running);
    menu.add(buttonDrive);

    menuBar.add(menu);

    // Draw menu
    menu = new JMenu("Draw");
    menu.setMnemonic(KeyEvent.VK_D);
    menu.getAccessibleContext().setAccessibleDescription("Start & Stop progress");

    buttonStart = new JMenuItem("Start", KeyEvent.VK_S);
    buttonStart.getAccessibleContext().setAccessibleDescription("Start sending g-code");
    buttonStart.addActionListener(this);
    buttonStart.setEnabled(portConfirmed && !running);
    menu.add(buttonStart);

    buttonPause = new JMenuItem("Pause", KeyEvent.VK_P);
    buttonPause.getAccessibleContext().setAccessibleDescription("Pause sending g-code");
    buttonPause.addActionListener(this);
    buttonPause.setEnabled(portConfirmed && running);
    menu.add(buttonPause);

    buttonHalt = new JMenuItem("Halt", KeyEvent.VK_H);
    buttonHalt.getAccessibleContext().setAccessibleDescription("Halt sending g-code");
    buttonHalt.addActionListener(this);
    buttonHalt.setEnabled(portConfirmed && running);
    menu.add(buttonHalt);

    menuBar.add(menu);

    // tools menu
    menu = new JMenu("Tools");
    buttonZoomOut = new JMenuItem("Zoom -");
    buttonZoomOut.addActionListener(this);
    buttonZoomOut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, ActionEvent.ALT_MASK));
    menu.add(buttonZoomOut);

    buttonZoomIn = new JMenuItem("Zoom +", KeyEvent.VK_EQUALS);
    buttonZoomIn.addActionListener(this);
    buttonZoomIn.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, ActionEvent.ALT_MASK));
    menu.add(buttonZoomIn);

    menuBar.add(menu);

    // Help menu
    menu = new JMenu("Help");
    menu.setMnemonic(KeyEvent.VK_H);
    menu.getAccessibleContext().setAccessibleDescription("Get help");

    buttonAbout = new JMenuItem("About", KeyEvent.VK_A);
    menu.getAccessibleContext().setAccessibleDescription("Find out about this program");
    buttonAbout.addActionListener(this);
    menu.add(buttonAbout);

    buttonCheckForUpdate = new JMenuItem("Check for updates", KeyEvent.VK_U);
    menu.getAccessibleContext().setAccessibleDescription("Is there a newer version available?");
    buttonCheckForUpdate.addActionListener(this);
    buttonCheckForUpdate.setEnabled(false);
    menu.add(buttonCheckForUpdate);

    menuBar.add(menu);

    // finish
    menuBar.updateUI();
  }

  public Container CreateContentPane() {
    // Create the content-pane-to-be.
    JPanel contentPane = new JPanel(new BorderLayout());
    contentPane.setOpaque(true);

    // the log panel
    log = new JTextPane();
    log.setEditable(false);
    log.setBackground(Color.BLACK);
    logPane = new JScrollPane(log);
    kit = new HTMLEditorKit();
    doc = new HTMLDocument();
    log.setEditorKit(kit);
    log.setDocument(doc);
    DefaultCaret c = (DefaultCaret) log.getCaret();
    c.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
    ClearLog();

    // the preview panel
    previewPane = new DrawPanel();
    previewPane.setPaperSize(paper_top, paper_bottom, paper_left, paper_right);

    // status bar
    statusBar = new StatusBar();
    Font f = statusBar.getFont();
    statusBar.setFont(f.deriveFont(Font.BOLD, 15));
    Dimension d = statusBar.getMinimumSize();
    d.setSize(d.getWidth(), d.getHeight() + 30);
    statusBar.setMinimumSize(d);

    // layout
    Splitter split = new Splitter(JSplitPane.VERTICAL_SPLIT);
    split.add(previewPane);
    split.add(logPane);
    split.setDividerSize(8);

    contentPane.add(statusBar, BorderLayout.SOUTH);
    contentPane.add(split, BorderLayout.CENTER);

    // open the file
    if (recentFiles[0].length() > 0) {
      OpenFileOnDemand(recentFiles[0]);
    }

    // connect to the last port
    ListSerialPorts();
    if (Arrays.asList(portsDetected).contains(recentPort)) {
      OpenPort(recentPort);
    }

    return contentPane;
  }

  // Create the GUI and show it.  For thread safety, this method should be invoked from the
  // event-dispatching thread.
  private static void CreateAndShowGUI() {
    // Create and set up the window.
    mainframe = new JFrame("No Drawbot Connected");
    mainframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // Create and set up the content pane.
    DrawbotGUI demo = DrawbotGUI.getSingleton();
    mainframe.setJMenuBar(demo.CreateMenuBar());
    mainframe.setContentPane(demo.CreateContentPane());

    // Display the window.
    mainframe.setSize(800, 700);
    mainframe.setVisible(true);
  }

  public static void main(String[] args) {
    // Schedule a job for the event-dispatching thread:
    // creating and showing this application's GUI.
    javax.swing.SwingUtilities.invokeLater(
        new Runnable() {
          public void run() {
            CreateAndShowGUI();
          }
        });
  }
}