/* * 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.feeder; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import javax.swing.Action; import org.openpnp.gui.MainFrame; import org.openpnp.gui.support.PropertySheetWizardAdapter; import org.openpnp.gui.support.Wizard; import org.openpnp.machine.reference.ReferenceFeeder; import org.openpnp.machine.reference.feeder.wizards.ReferenceStripFeederConfigurationWizard; import org.openpnp.model.Length; import org.openpnp.model.LengthUnit; import org.openpnp.model.Location; import org.openpnp.model.Point; import org.openpnp.spi.Camera; import org.openpnp.spi.Nozzle; import org.openpnp.spi.PropertySheetHolder; import org.openpnp.util.MovableUtils; import org.openpnp.util.Utils2D; import org.openpnp.vision.FluentCv; import org.simpleframework.xml.Attribute; import org.simpleframework.xml.Element; /** * Implementation of Feeder that indexes through a strip of cut tape. This is a specialization of * the tray feeder that knows specifics about tape so that vision capabilities can be added. */ /** * SMD tape standard info from http://www.liteplacer.com/setup-tape-positions-2/ * * holes 1.5mm * * hole pitch 4mm * * first part center to reference hole linear is 2mm * * tape width is multiple of 4mm * * part pitch is multiple of 4mm except for 0402 and smaller, where it is 2mm * * hole to part lateral is tape width / 2 - 0.5mm */ public class ReferenceStripFeeder extends ReferenceFeeder { public enum TapeType { WhitePaper("White Paper"), BlackPlastic("Black Plastic"), ClearPlastic("Clear Plastic"); private String name; TapeType(String name) { this.name = name; } public String toString() { return name; } } @Element(required = false) private Location referenceHoleLocation = new Location(LengthUnit.Millimeters); @Element(required = false) private Location lastHoleLocation = new Location(LengthUnit.Millimeters); @Element(required = false) private Length partPitch = new Length(4, LengthUnit.Millimeters); @Element(required = false) private Length tapeWidth = new Length(8, LengthUnit.Millimeters); @Attribute(required = false) private TapeType tapeType = TapeType.WhitePaper; @Attribute(required = false) private boolean visionEnabled = true; @Attribute private int feedCount = 0; private Length holeDiameter = new Length(1.5, LengthUnit.Millimeters); private Length holePitch = new Length(4, LengthUnit.Millimeters); private Length referenceHoleToPartLinear = new Length(2, LengthUnit.Millimeters); private Location visionOffsets; private Location visionLocation; public Length getHoleDiameterMin() { return getHoleDiameter().multiply(0.9); } public Length getHoleDiameterMax() { return getHoleDiameter().multiply(1.1); } public Length getHolePitchMin() { return getHolePitch().multiply(0.9); } public Length getHoleDistanceMin() { return getTapeWidth().multiply(0.25); } public Length getHoleDistanceMax() { return getTapeWidth().multiply(1.5); } public Length getHoleLineDistanceMax() { return new Length(0.5, LengthUnit.Millimeters); } public int getHoleBlurKernelSize() { return 9; } @Override public Location getPickLocation() throws Exception { int feedCount = this.feedCount; /* * As a special case, before the feeder has been fed we return the pick location * as if the feeder had been fed. This keeps us from returning a pick location * that is off the edge of the strip. */ if (feedCount == 0) { feedCount = 1; } // Find the location of the part linearly along the tape Location[] lineLocations = getIdealLineLocations(); // 20160608 - ldpgh/lutz_dd // partPichtAdjusted:double ... match prev. partPitch.getValue() // partPitchAdjusted is the euclidian distance of ReferenceHole and NextHole and divided by // the amount of part locations in between. This Part count is derived from the distance // and the given partPitch in GUI and afterwards rounded to the next integer value. // partPitch/partPitchAdjusted limitation // It's the P1 value according to EIA-481-C, October 2003, pg. 9, 11, 13 // Accuracy variations as specified in the document are not taken into account! double partPitchAdjusted = lineLocations[0].getLinearDistanceTo(lineLocations[1]); partPitchAdjusted = partPitchAdjusted / (Math.round(partPitchAdjusted / partPitch.getValue())); Location l = getPointAlongLine(lineLocations[0], lineLocations[1], new Length((feedCount - 1) * partPitchAdjusted, partPitch.getUnits())); // Create the offsets that are required to go from a reference hole // to the part in the tape Length x = getHoleToPartLateral().convertToUnits(l.getUnits()); Length y = referenceHoleToPartLinear.convertToUnits(l.getUnits()); Point p = new Point(x.getValue(), y.getValue()); // Determine the angle that the tape is at double angle = getAngleFromPoint(lineLocations[0], lineLocations[1]); // Rotate the part offsets by the angle to move it into the right // coordinate space p = Utils2D.rotatePoint(p, angle); // And add the offset to the location we calculated previously l = l.add(new Location(l.getUnits(), p.x, p.y, 0, 0)); // Add in the angle of the tape plus the angle of the part in the tape // so that the part is picked at the right angle l = l.derive(null, null, null, angle + getLocation().getRotation()); // and if vision was performed, add the offsets if (visionEnabled && visionOffsets != null) { l = l.add(visionOffsets); } return l; } public Location[] getIdealLineLocations() { if (visionLocation == null) { return new Location[] {referenceHoleLocation, lastHoleLocation}; } double d1 = referenceHoleLocation.getLinearLengthTo(lastHoleLocation) .convertToUnits(LengthUnit.Millimeters).getValue(); double d2 = referenceHoleLocation.getLinearLengthTo(visionLocation) .convertToUnits(LengthUnit.Millimeters).getValue(); if (d2 > d1) { return new Location[] {referenceHoleLocation, visionLocation}; } else { return new Location[] {referenceHoleLocation, lastHoleLocation}; } } public void feed(Nozzle nozzle) throws Exception { setFeedCount(getFeedCount() + 1); updateVisionOffsets(nozzle); } private void updateVisionOffsets(Nozzle nozzle) throws Exception { if (!visionEnabled) { return; } // go to where we expect to find the next reference hole Camera camera = nozzle.getHead().getDefaultCamera(); Location expectedLocation = null; Location[] lineLocations = getIdealLineLocations(); if (partPitch.convertToUnits(LengthUnit.Millimeters).getValue() < 4) { // For tapes with a part pitch < 4 we need to check each hole // twice since there are two parts per reference hole. // Note the use of holePitch here and partPitch in the // alternate case below. expectedLocation = getPointAlongLine(lineLocations[0], lineLocations[1], holePitch.multiply((feedCount - 1) / 2)); } else { // For tapes with a part pitch >= 4 there is always a reference // hole 2mm from a part so we just multiply by the part pitch // skipping over holes that are not reference holes. expectedLocation = getPointAlongLine(lineLocations[0], lineLocations[1], partPitch.multiply(feedCount - 1)); } MovableUtils.moveToLocationAtSafeZ(camera, expectedLocation); // and look for the hole Location actualLocation = findClosestHole(camera); if (actualLocation == null) { throw new Exception( "Feeder " + getName() + ": Unable to locate reference hole. End of strip?"); } // make sure it's not too far away Length distance = actualLocation.getLinearLengthTo(expectedLocation) .convertToUnits(LengthUnit.Millimeters); if (distance.getValue() > 2) { throw new Exception( "Feeder " + getName() + ": Unable to locate reference hole. End of strip?"); } visionOffsets = actualLocation.subtract(expectedLocation).derive(null, null, 0d, 0d); visionLocation = actualLocation; } private Location findClosestHole(Camera camera) { List<Location> holeLocations = new ArrayList<>(); BufferedImage image = new FluentCv().setCamera(camera).settleAndCapture("original").toGray() .blurGaussian(getHoleBlurKernelSize()) .findCirclesHough(getHoleDiameterMin(), getHoleDiameterMax(), getHolePitchMin(), "circles") .convertCirclesToLocations(holeLocations).drawCircles("original").toBufferedImage(); if (holeLocations.isEmpty()) { return null; } try { MainFrame.get().getCameraViews().getCameraView(camera).showFilteredImage(image, 500); } catch (Exception e) { // if we aren't running in the UI this will fail, and that's okay } return holeLocations.get(0); } private Length getHoleToPartLateral() { Length tapeWidth = this.tapeWidth.convertToUnits(LengthUnit.Millimeters); return new Length(tapeWidth.getValue() / 2 - 0.5, LengthUnit.Millimeters); } static public Location getPointAlongLine(Location a, Location b, Length distance) { Point vab = b.subtract(a).getXyPoint(); double lab = a.getLinearDistanceTo(b); Point vu = new Point(vab.x / lab, vab.y / lab); vu = new Point(vu.x * distance.getValue(), vu.y * distance.getValue()); return a.add(new Location(a.getUnits(), vu.x, vu.y, 0, 0)); } // Stolen from StackOverflow static public double getAngleFromPoint(Location firstPoint, Location secondPoint) { double angle = 0.0; // above 0 to 180 degrees if ((secondPoint.getX() > firstPoint.getX())) { angle = (Math.atan2((secondPoint.getX() - firstPoint.getX()), (firstPoint.getY() - secondPoint.getY())) * 180 / Math.PI); } // above 180 degrees to 360/0 else if ((secondPoint.getX() <= firstPoint.getX())) { angle = 360 - (Math.atan2((firstPoint.getX() - secondPoint.getX()), (firstPoint.getY() - secondPoint.getY())) * 180 / Math.PI); } return angle; } public TapeType getTapeType() { return tapeType; } public void setTapeType(TapeType tapeType) { this.tapeType = tapeType; } public Location getReferenceHoleLocation() { return referenceHoleLocation; } public void setReferenceHoleLocation(Location referenceHoleLocation) { this.referenceHoleLocation = referenceHoleLocation; visionLocation = null; } public Location getLastHoleLocation() { return lastHoleLocation; } public void setLastHoleLocation(Location lastHoleLocation) { this.lastHoleLocation = lastHoleLocation; visionLocation = null; } public Length getHoleDiameter() { return holeDiameter; } public void setHoleDiameter(Length holeDiameter) { this.holeDiameter = holeDiameter; } public Length getHolePitch() { return holePitch; } public void setHolePitch(Length holePitch) { this.holePitch = holePitch; } public Length getPartPitch() { return partPitch; } public void setPartPitch(Length partPitch) { this.partPitch = partPitch; } public Length getTapeWidth() { return tapeWidth; } public void setTapeWidth(Length tapeWidth) { this.tapeWidth = tapeWidth; } public int getFeedCount() { return feedCount; } public void setFeedCount(int feedCount) { int oldValue = this.feedCount; this.feedCount = feedCount; this.visionOffsets = null; firePropertyChange("feedCount", oldValue, feedCount); } public Length getReferenceHoleToPartLinear() { return referenceHoleToPartLinear; } public void setReferenceHoleToPartLinear(Length referenceHoleToPartLinear) { this.referenceHoleToPartLinear = referenceHoleToPartLinear; } public boolean isVisionEnabled() { return visionEnabled; } public void setVisionEnabled(boolean visionEnabled) { this.visionEnabled = visionEnabled; } @Override public String toString() { return getName(); } @Override public Wizard getConfigurationWizard() { return new ReferenceStripFeederConfigurationWizard(this); } @Override public String getPropertySheetHolderTitle() { return getClass().getSimpleName() + " " + getName(); } @Override public PropertySheetHolder[] getChildPropertySheetHolders() { // TODO Auto-generated method stub return null; } @Override public Action[] getPropertySheetHolderActions() { // TODO Auto-generated method stub return null; } } // this code left here in case we want to use it in the future. it is for // calculating how many parts there are based on the first and last reference hole. //// figure out how many parts there should be by taking the delta //// between the two holes and dividing it by part pitch // double holeToHoleDistance = lastHoleLocation.getLinearDistanceTo(referenceHoleLocation); //// take the ceil of the distance to account for any minor offset from //// center of the hole // holeToHoleDistance = Math.ceil(holeToHoleDistance); // double partPitch = this.partPitch.convertToUnits(lastHoleLocation.getUnits()).getValue(); //// And floor the part count because you can't have a partial part. // double partCount = Math.floor(holeToHoleDistance / partPitch); // //// if (feedCount > partCount) { //// throw new Exception(String.format("No more parts available in feeder %s", getName())); //// } //