/*
* Copyright (C) 2011 Jason von Nieda <jason@vonnieda.org>
*
* This file is part of OpenPnP.
*
* OpenPnP 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.
*
* OpenPnP 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 OpenPnP. If not, see
* <http://www.gnu.org/licenses/>.
*
* For more information about OpenPnP visit http://openpnp.org
*/
package org.openpnp.machine.reference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openpnp.machine.reference.ReferenceGlueDispenseJobProcessor.JobDispense.Status;
import org.openpnp.model.BoardLocation;
import org.openpnp.model.Configuration;
import org.openpnp.model.Job;
import org.openpnp.model.Location;
import org.openpnp.model.Placement;
import org.openpnp.spi.FiducialLocator;
import org.openpnp.spi.Head;
import org.openpnp.spi.Machine;
import org.openpnp.spi.PasteDispenser;
import org.openpnp.spi.base.AbstractPasteDispenseJobProcessor;
import org.openpnp.util.FiniteStateMachine;
import org.openpnp.util.MovableUtils;
import org.openpnp.util.Utils2D;
import org.pmw.tinylog.Logger;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Root;
@Root
public class ReferenceGlueDispenseJobProcessor extends AbstractPasteDispenseJobProcessor {
enum State {
Uninitialized,
PreFlight,
FiducialCheck,
Dispense,
Cleanup,
Stopped
}
enum Message {
Initialize,
Next,
Complete,
Abort,
Skip,
Reset
}
public static class JobDispense {
public enum Status {
Pending,
Processing,
Skipped,
Complete
}
public final BoardLocation boardLocation;
public final Placement placement;
public Status status = Status.Pending;
public JobDispense(BoardLocation boardLocation, Placement placement) {
this.boardLocation = boardLocation;
this.placement = placement;
}
public double getDistance()
{
Location zeroLocation=this.boardLocation.getLocation().derive((double) 0,(double) 0,(double) 0,(double) 0);
return zeroLocation.getLinearDistanceTo((Utils2D.calculateBoardPlacementLocation(this.boardLocation,this.placement.getLocation())));
}
}
@Attribute(required = false)
protected boolean parkWhenComplete = false;
private FiniteStateMachine<State, Message> fsm = new FiniteStateMachine<>(State.Uninitialized);
protected Job job;
protected Machine machine;
protected Head head;
protected PasteDispenser pasteDispenser;
protected List<JobDispense> jobDispenses = new ArrayList<>();
protected Map<BoardLocation, Location> boardLocationFiducialOverrides = new HashMap<>();
public ReferenceGlueDispenseJobProcessor() {
fsm.add(State.Uninitialized, Message.Initialize, State.PreFlight, this::doInitialize);
fsm.add(State.PreFlight, Message.Next, State.FiducialCheck, this::doPreFlight,
Message.Next);
fsm.add(State.PreFlight, Message.Abort, State.Cleanup, Message.Next);
fsm.add(State.FiducialCheck, Message.Next, State.Dispense, this::doFiducialCheck, Message.Next);
fsm.add(State.FiducialCheck, Message.Skip, State.Dispense, Message.Next);
fsm.add(State.FiducialCheck, Message.Abort, State.Cleanup, Message.Next);
fsm.add(State.Dispense, Message.Next, State.Cleanup, this::doDispense);
fsm.add(State.Dispense, Message.Skip, State.Cleanup, this::doSkip, Message.Next);
fsm.add(State.Dispense, Message.Abort, State.Cleanup, Message.Next);
fsm.add(State.Cleanup, Message.Next, State.Stopped, this::doCleanup, Message.Reset);
fsm.add(State.Stopped, Message.Reset, State.Uninitialized, this::doReset);
}
public synchronized void initialize(Job job) throws Exception {
this.job = job;
fsm.send(Message.Initialize);
}
public synchronized boolean next() throws Exception {
if(fsm.getState() == State.Uninitialized)
return false;
fsm.send(Message.Next);
if (fsm.getState() == State.Stopped) {
/*
* If we've reached the Stopped state the process is complete. We reset the FSM and
* return false to indicate that we're finished.
*/
fsm.send(Message.Reset);
return false;
}
else if (fsm.getState() == State.Dispense && isJobComplete()) {
/*
* If we've reached the Dispense state and there are no more dispenses to work on the job
* is complete. We send the Complete Message to start the cleanup process.
*/
fsm.send(Message.Complete);
return false;
}
return true;
}
public synchronized void abort() throws Exception {
fsm.send(Message.Abort);
}
public synchronized void skip() throws Exception {
fsm.send(Message.Skip);
}
public boolean canSkip() {
return fsm.canSend(Message.Skip);
}
/**
* Validate that there is a job set before allowing it to start.
*
* @throws Exception
*/
protected void doInitialize() throws Exception {
if (job == null) {
throw new Exception("Can't initialize with a null Job.");
}
}
/**
* Create some internal shortcuts to various buried objects.
*
* Check for obvious setup errors in the job: Feeders are available and enabled, Placements all
* have valid parts, Parts all have height values set, Each part has at least one compatible
* nozzle tip.
*
* Populate the jobPlacements list with all the placements that we'll perform for the entire
* job.
*
* Safe-Z the machine, discard any currently picked parts.
*
* @throws Exception
*/
protected void doPreFlight() throws Exception {
// Create some shortcuts for things that won't change during the run
this.machine = Configuration.get().getMachine();
this.head = this.machine.getDefaultHead();
this.pasteDispenser = this.head.getDefaultPasteDispenser();
this.jobDispenses.clear();
this.boardLocationFiducialOverrides.clear();
fireTextStatus("Checking job for setup errors.");
for (BoardLocation boardLocation : job.getBoardLocations()) {
// Only check enabled boards
if (!boardLocation.isEnabled()) {
continue;
}
for (Placement placement : boardLocation.getBoard().getPlacements()) {
// Ignore placements that aren't set to be glued
if (!placement.getGlue()) {
continue;
}
// Ignore placements that aren't on the side of the board we're processing.
if (placement.getSide() != boardLocation.getSide()) {
continue;
}
JobDispense jobDispense = new JobDispense(boardLocation, placement);
jobDispenses.add(jobDispense);
}
}
// Do a very basic sort by distance to stop the machine going randomly round the PCB
Collections.sort(jobDispenses, new Comparator<JobDispense>() {
@Override
public int compare(JobDispense c1, JobDispense c2) {
return new Double(c1.getDistance()).compareTo(new Double(c2.getDistance()));
}
});
// Everything looks good, so prepare the machine.
fireTextStatus("Preparing machine.");
// Safe Z the machine
head.moveToSafeZ();
}
protected void doFiducialCheck() throws Exception {
fireTextStatus("Performing fiducial checks.");
FiducialLocator locator = Configuration.get().getMachine().getFiducialLocator();
for (BoardLocation boardLocation : job.getBoardLocations()) {
if (!boardLocation.isEnabled()) {
continue;
}
if (!boardLocation.isCheckFiducials()) {
continue;
}
Location location = locator.locateBoard(boardLocation);
boardLocationFiducialOverrides.put(boardLocation, location);
Logger.debug("Fiducial check for {}", boardLocation);
}
}
protected void doDispense() throws Exception {
for(JobDispense jobDispense : jobDispenses) {
/* if (jobDispense.stepComplete) {
continue;
}*/
BoardLocation boardLocation = jobDispense.boardLocation;
Placement placement= jobDispense.placement;
// Check if there is a fiducial override for the board location and if so, use it.
if (boardLocationFiducialOverrides.containsKey(boardLocation)) {
BoardLocation boardLocation2 = new BoardLocation(boardLocation.getBoard());
boardLocation2.setSide(boardLocation.getSide());
boardLocation2.setLocation(boardLocationFiducialOverrides.get(boardLocation));
boardLocation = boardLocation2;
}
Location dispenseLocation =
Utils2D.calculateBoardPlacementLocation(boardLocation, placement.getLocation());
MovableUtils.moveToLocationAtSafeZ(pasteDispenser, dispenseLocation);
pasteDispenser.dispense(null,null,0);
pasteDispenser.moveToSafeZ();
// Mark the dispense as finished
jobDispense.status = Status.Complete;
Logger.debug("Dispensed {} ", dispenseLocation);
}
}
protected void doCleanup() throws Exception {
fireTextStatus("Cleaning up.");
// Safe Z the machine
head.moveToSafeZ();
}
protected void doReset() throws Exception {
this.job = null;
}
/**
* Discard the picked part, if any. Remove the currently processing PlannedPlacement from the
* list and mark the JobPlacement as Skipped.
*
* @throws Exception
*/
protected void doSkip() throws Exception {
//
}
protected boolean isJobComplete() {
return jobDispenses.isEmpty();
}
/*
@Override
public Wizard getConfigurationWizard() {
return new Ref(this);
}
public boolean isParkWhenComplete() {
return parkWhenComplete;
}
public void setParkWhenComplete(boolean parkWhenComplete) {
this.parkWhenComplete = parkWhenComplete;
}
*/
}