diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index 9577125..5f39b4f 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -4,7 +4,7 @@ org.baxter.disco ocr Disco OCR Accuracy Over Life Testing - 4.3.8 + 4.3.9 Testing Discos for long-term accuracy, using automated optical character recognition. Baxter International @@ -95,9 +95,6 @@ maven-javadoc-plugin 3.4.1 - - private - diff --git a/pom.xml b/pom.xml index 325be6a..b5c99f2 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.baxter.disco ocr - 4.3.8 + 4.3.9 jar Disco OCR Accuracy Over Life Testing Testing Discos for long-term accuracy, using automated optical character recognition. diff --git a/runScript.sh b/runScript.sh index ac392f5..057bfd5 100644 --- a/runScript.sh +++ b/runScript.sh @@ -1,2 +1,2 @@ #! /usr/bin/env sh -sudo java -jar discoTesting-4.3.8.jar 2>/dev/null +sudo java -jar discoTesting-4.3.9.jar 2>/dev/null diff --git a/src/main/java/org/baxter/disco/ocr/Cli.java b/src/main/java/org/baxter/disco/ocr/Cli.java index 86fc702..29941b0 100644 --- a/src/main/java/org/baxter/disco/ocr/Cli.java +++ b/src/main/java/org/baxter/disco/ocr/Cli.java @@ -3,12 +3,11 @@ package org.baxter.disco.ocr; //Standard imports import java.io.File; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Scanner; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; /** * CLI for the Fixture. @@ -18,20 +17,22 @@ import java.util.concurrent.locks.ReentrantLock; * classes). * * @author Blizzard Finnegan - * @version 1.8.0, 16 Mar. 2023 + * @version 1.9.0, 20 Mar. 2023 */ public class Cli { /** * Complete build version number */ - private static final String version = "4.3.8"; + private static final String version = "4.3.9"; /** * Currently saved iteration count. */ private static int iterationCount = 10; + private static boolean endOfCycles = false; + /** * Scanner used for monitoring user input. * This is a global object, so that functions @@ -65,11 +66,6 @@ public class Cli */ private static final int cameraMenuOptionCount = 7; - /** - * Lock object, used for temporary interruption of {@link #runTests()} - */ - public static final Lock LOCK = new ReentrantLock(); - static { @@ -139,6 +135,7 @@ public class Cli setActiveCameras(); break; case 5: + endOfCycles = false; if(!camerasConfigured) { prompt("You have not configured the cameras yet! Are you sure you would like to continue? (y/N): "); @@ -647,21 +644,34 @@ public class Cli try{ Thread.sleep(2000); } catch(Exception e){ ErrorLogging.logError(e); } } - Map resultMap = new HashMap<>(); - Map cameraToFile = new HashMap<>(); - - //Initialise cameraToFile, so keys don't shuffle. - for(String cameraName : cameraList) - { - cameraToFile.put(cameraName,new File("/dev/null")); - } + final Map serials = ConfigFacade.getSerials(); ErrorLogging.logError("DEBUG: Starting tests..."); - //All portions of the test check with the GPIO Run/Pause switch before - //continuing, using the Lock object. + + final LinkedBlockingQueue dataEntryQueue = new LinkedBlockingQueue<>(); + + Thread writeThread = new Thread( () -> { + while(dataEntryQueue.size() > 0 && !endOfCycles) + { + Cycle cycle = null; + + do + { + try{ cycle = dataEntryQueue.poll(Long.MAX_VALUE,TimeUnit.SECONDS); } + catch(Exception e){ ErrorLogging.logError(e); } + } + while(cycle == null); + + DataSaving.writeValues(cycle,serials); + } + }); + + writeThread.start(); + for(int i = 0; i < localIterations; i++) { + Cycle cycle = new Cycle(i); println(""); ErrorLogging.logError("===================================="); ErrorLogging.logError("Starting iteration " + (i+1) + " of " + localIterations + "..."); @@ -675,9 +685,7 @@ public class Cli fail = false; if(safeGPIO) { - while(!LOCK.tryLock()) {} MovementFacade.iterationMovement(prime); - LOCK.unlock(); //Wait for the DUT to display an image try{ Thread.sleep(2000); } catch(Exception e){ ErrorLogging.logError(e); } @@ -685,27 +693,19 @@ public class Cli for(String cameraName : cameraList) { - while(!LOCK.tryLock()) {} - File file = OpenCVFacade.completeProcess(cameraName); - LOCK.unlock(); - - while(!LOCK.tryLock()) {} - cameraToFile.replace(cameraName,file); - LOCK.unlock(); + Thread cameraThread = new Thread(() -> { + File file = OpenCVFacade.completeProcess(cameraName); + Double result = TesseractFacade.imageToDouble(file); + ErrorLogging.logError("Parsed value from camera " + cameraName +": " + result); + synchronized(cycle) { cycle.addCamera(cameraName,file,result); } + }); + cameraThread.start(); + try{ cameraThread.join(); } catch(Exception e){ ErrorLogging.logError(e); } } for(String cameraName : cameraList) { - while(!LOCK.tryLock()) {} - File file = cameraToFile.get(cameraName); - LOCK.unlock(); - while(!LOCK.tryLock()) {} - Double result = TesseractFacade.imageToDouble(file); - LOCK.unlock(); - while(!LOCK.tryLock()) {} - resultMap.put(file,result); - ErrorLogging.logError("Parsed value from camera " + cameraName +": " + result); - LOCK.unlock(); + double result = cycle.getValue(cameraName); if(result <= 10 || result >= 100 || result == Double.NEGATIVE_INFINITY) @@ -723,14 +723,9 @@ public class Cli } } while(fail); - - while(!LOCK.tryLock()) {} - DataSaving.writeValues(i,resultMap,cameraToFile); - LOCK.unlock(); - - //DO NOT CLEAR camera to file Map. This will change the order of the objects within it - resultMap.clear(); + dataEntryQueue.add(cycle); } + endOfCycles = true; println("======================================="); println("Testing complete!"); } diff --git a/src/main/java/org/baxter/disco/ocr/Cycle.java b/src/main/java/org/baxter/disco/ocr/Cycle.java new file mode 100644 index 0000000..c152bfa --- /dev/null +++ b/src/main/java/org/baxter/disco/ocr/Cycle.java @@ -0,0 +1,84 @@ +package org.baxter.disco.ocr; + +import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * A single cycle in a test. + * + * @author Blizzard Finnegan + * @version 1.0.0, 20 Mar. 2023 + */ +public class Cycle +{ + /** + * Current cycle number. + */ + private final int cycleNumber; + + /** + * Map of values, paired to their camera. + * + * Key: String of the name of the camera + * Value: Pair[File,Double] of the information for this cycle. + * + * Pair[File,Double]: + * First: File of the location of the image from the camera. + * Second: Double generated by Tesseract parsing the above image. + */ + private final Map> cycleValues = new TreeMap<>(); + + /** + * Default constructor. + */ + public Cycle(int cycleNumber) + { this.cycleNumber = cycleNumber; } + + /** + * Getter for this object's cycle number. + * + * @return int of the current cycle number. + */ + public int getcycleNumber() { return this.cycleNumber; } + + /** + * Get all cameras contained within this object. + * + * @return Set of Strings of all cameras available in this object. + */ + public Set getCameras() { return this.cycleValues.keySet(); } + + /** + * Getter for the given camera's associated image File. + * + * @param cameraName name of the camera to get the image from + * + * @return File of the image from the camera + */ + public File getImage(String cameraName) + { return this.cycleValues.get(cameraName).first(); } + + /** + * Get the Tesseract parsed value of the image from the given camera. + * + * @param cameraName name of the camera to get the image from + * + * @return Double of the Tesseract-parsed value of the image. + */ + public Double getValue(String cameraName) + { return this.cycleValues.get(cameraName).second(); } + + /** + * Add a camera to the object, with its accociated image and value. + * + * @param cameraName Name of the camera associated with the image and value + * @param image File object denoting the location of the image from the camera + * @param parsedValue Double of the value generated by Tesseract from the image + * + * @return true if saved successfully, else false. + */ + public void addCamera(String cameraName, File image, Double parsedValue) + { this.cycleValues.put(cameraName,new Pair(image, parsedValue)); } +} diff --git a/src/main/java/org/baxter/disco/ocr/DataSaving.java b/src/main/java/org/baxter/disco/ocr/DataSaving.java index 0f2a415..d4b0d19 100644 --- a/src/main/java/org/baxter/disco/ocr/DataSaving.java +++ b/src/main/java/org/baxter/disco/ocr/DataSaving.java @@ -32,7 +32,7 @@ import static org.apache.poi.hssf.util.HSSFColor.HSSFColorPredefined; * Facade for saving data out to a file. * * @author Blizzard Finnegan - * @version 5.0.1, 10 Mar. 2023 + * @version 6.0.0, 10 Mar. 2023 */ public class DataSaving { @@ -167,7 +167,7 @@ public class DataSaving * * @param cameraCount The number of cameras that were used. */ - private static void updateFormulas(int cameraCount) + private static synchronized void updateFormulas(int cameraCount) { ErrorLogging.logError("DEBUG: Updating formulas in Excel sheet..."); int rowIndex = 0; @@ -227,30 +227,29 @@ public class DataSaving * Writes line to XLSX file. * * @param cycle What test cycle is being saved to the file - * @param inputMap Map[String,Double] list of inputs + * @param serials Map[String,String] list of camera serials * * @return Returns whether values were saved successfully. */ - public static boolean writeValues(int cycle, Map inputMap, Map cameraToFile) + public static synchronized boolean writeValues(Cycle cycle, Map serials) { - ErrorLogging.logError("DEBUG: Writing values for " + (cycle + 1) + " cycle to worksheet."); + int cycleNumber = cycle.getcycleNumber() + 1; + ErrorLogging.logError("DEBUG: Writing values for " + (cycleNumber) + " cycle to worksheet."); boolean output = false; int cellnum = 0; int startingRow = outputSheet.getLastRowNum(); - HSSFRow row = (cycle == 1) ? outputSheet.getRow(startingRow) : outputSheet.createRow(++startingRow); - List cameraNames = new ArrayList<>(cameraToFile.keySet()); - - cycle++; + HSSFRow row = (cycle.getcycleNumber() == 1) ? outputSheet.getRow(startingRow) : outputSheet.createRow(++startingRow); + List cameraNames = new ArrayList<>(cycle.getCameras()); HSSFCell indexCell = row.createCell(cellnum++); - indexCell.setCellValue(cycle); + indexCell.setCellValue(cycleNumber); for(String cameraName : cameraNames) { - String serialNumber = ConfigFacade.getSerial(cameraName); + String serialNumber = serials.get(cameraName); HSSFCell serialCell = row.createCell(cellnum++); serialCell.setCellValue(serialNumber); - File file = cameraToFile.get(cameraName); + File file = cycle.getImage(cameraName); HSSFCell imageCell = row.createCell(cellnum++); try { @@ -275,7 +274,7 @@ public class DataSaving //Put the OCR value into the sheet HSSFCell ocrCell = row.createCell(cellnum++); - Double ocrRead = inputMap.get(file); + Double ocrRead = cycle.getValue(cameraName); if(ocrRead.equals(Double.NEGATIVE_INFINITY)) { ocrCell.setCellValue("ERROR!"); diff --git a/src/main/java/org/baxter/disco/ocr/ErrorLogging.java b/src/main/java/org/baxter/disco/ocr/ErrorLogging.java index f13949c..6e241ac 100644 --- a/src/main/java/org/baxter/disco/ocr/ErrorLogging.java +++ b/src/main/java/org/baxter/disco/ocr/ErrorLogging.java @@ -87,7 +87,7 @@ public class ErrorLogging * * @param error Any error being thrown to be logged. */ - public static void logError(Throwable error) + public static synchronized void logError(Throwable error) { String errorStackTrace = ExceptionUtils.getStackTrace(error); String errorMessage = datetime.format(LocalDateTime.now()) + " - " + errorStackTrace; @@ -100,7 +100,7 @@ public class ErrorLogging * * @param error Any information to save to the log file. */ - public static void logError(String error) + public static synchronized void logError(String error) { String errorMessage = datetime.format(LocalDateTime.now()) + "\t- " + error; fileOut.println(errorMessage); diff --git a/src/main/java/org/baxter/disco/ocr/MovementFacade.java b/src/main/java/org/baxter/disco/ocr/MovementFacade.java index c1f53d9..09b92c4 100644 --- a/src/main/java/org/baxter/disco/ocr/MovementFacade.java +++ b/src/main/java/org/baxter/disco/ocr/MovementFacade.java @@ -17,7 +17,7 @@ import com.pi4j.io.gpio.digital.PullResistance; * Currently missing Run switch compatibility. * * @author Blizzard Finnegan - * @version 3.1.0, 16 Mar. 2023 + * @version 3.3.0, 20 Mar. 2023 */ public class MovementFacade { @@ -32,6 +32,11 @@ public class MovementFacade */ private static Thread runSwitchThread; + /** + * Lock object for multithreading/run switch. + */ + private static final Object lockObject = new Object(); + /** * Fraction of the total travel time, so the arm won't push through the limit switch. */ @@ -185,23 +190,27 @@ public class MovementFacade ErrorLogging.logError("DEBUG: Starting lock thread..."); runSwitchThread = new Thread(() -> { - boolean unlock = false; while(!exit) { if(runSwitch.isOn()) { - ErrorLogging.logError("DEBUG: Run switch turned off!"); - while(!Cli.LOCK.tryLock()) - {} - unlock = true; + synchronized(lockObject) + { + while(runSwitch.isOn()) + { + try{ Thread.sleep(100); } catch(Exception e){ErrorLogging.logError(e);} + if(runSwitch.isOff()) + { + try{ Thread.sleep(100); } catch(Exception e){ErrorLogging.logError(e);} + if(runSwitch.isOff()) + { + break; + } + } + } + } } - else - { - //ErrorLogging.logError("Run switch on!"); - if(unlock) - { Cli.LOCK.unlock(); unlock = false; } - } - try{ Thread.sleep(100); } catch(Exception e) { ErrorLogging.logError(e); } + try{ Thread.sleep(1); } catch(Exception e){ErrorLogging.logError(e);} } }, "Run switch monitor."); runSwitchThread.start(); @@ -255,33 +264,41 @@ public class MovementFacade if(upperLimit.isHigh()) { ErrorLogging.logError("DEBUG: Motor at highest point! Lowering to reset."); - motorDirectionDown(); - motorOn(); - try{ Thread.sleep(500); } - catch (Exception e){ ErrorLogging.logError(e); } - motorOff(); + synchronized(lockObject) + { + motorDirectionDown(); + motorOn(); + try{ Thread.sleep(500); } + catch (Exception e){ ErrorLogging.logError(e); } + motorOff(); + } } ErrorLogging.logError("DEBUG: Moving motor to highest point."); motorDirectionUp(); - motorOn(); + synchronized(lockObject) + { + motorOn(); - for(counter = 0; counter < TIMEOUT; counter++) - { - try{ Thread.sleep(POLL_WAIT); } catch(Exception e){ ErrorLogging.logError(e); } - if(upperLimit.isOn()) - { - try{ Thread.sleep(1); } catch(Exception e){ErrorLogging.logError(e); } - if(upperLimit.isOn()) break; + for(counter = 0; counter < TIMEOUT; counter++) + { + try{ Thread.sleep(POLL_WAIT); } catch(Exception e){ ErrorLogging.logError(e); } + if(upperLimit.isOn()) + { + try{ Thread.sleep(1); } catch(Exception e){ErrorLogging.logError(e); } + if(upperLimit.isOn()) break; + } } + motorOff(); } - motorOff(); + if(counter < TIMEOUT) { ErrorLogging.logError("DEBUG: Motor returned after " + counter + " polls."); ErrorLogging.logError("DEBUG: --------------------------------------"); return counter; } + else { ErrorLogging.logError("DEBUG: No motor return after 3 seconds."); @@ -372,21 +389,25 @@ public class MovementFacade ErrorLogging.logError("DEBUG: Travel time: " + totalPollCount); ErrorLogging.logError("DEBUG: High speed poll count: " + highSpeedPolls); ErrorLogging.logError("DEBUG: ============================="); - motorOn(); - for(int i = 0; i < highSpeedPolls; i++) + + synchronized(lockObject) { - try{ Thread.sleep(POLL_WAIT); } catch(Exception e){ ErrorLogging.logError(e); } - if(limitSense.isOn()) + motorOn(); + for(int i = 0; i < highSpeedPolls; i++) { - try{ Thread.sleep(5); } catch(Exception e){ErrorLogging.logError(e); } - if(limitSense.isOn()) + try{ Thread.sleep(POLL_WAIT); } catch(Exception e){ ErrorLogging.logError(e); } + if(limitSense.isOn()) { - motorOff(); - break; + try{ Thread.sleep(5); } catch(Exception e){ErrorLogging.logError(e); } + if(limitSense.isOn()) + { + motorOff(); + break; + } } } + motorOff(); } - motorOff(); output = (limitSense.isOn() ? FinalState.UNSAFE : FinalState.SAFE); @@ -412,11 +433,14 @@ public class MovementFacade */ public static void pressButton() { - ErrorLogging.logError("DEBUG: Pressing button..."); - pistonActivate.on(); - try{ Thread.sleep(1000); } catch(Exception e) {ErrorLogging.logError(e);}; - ErrorLogging.logError("DEBUG: Releasing button..."); - pistonActivate.off(); + synchronized(lockObject) + { + ErrorLogging.logError("DEBUG: Pressing button..."); + pistonActivate.on(); + try{ Thread.sleep(1000); } catch(Exception e) {ErrorLogging.logError(e);}; + ErrorLogging.logError("DEBUG: Releasing button..."); + pistonActivate.off(); + } } /** diff --git a/src/main/java/org/baxter/disco/ocr/Pair.java b/src/main/java/org/baxter/disco/ocr/Pair.java new file mode 100644 index 0000000..0378cda --- /dev/null +++ b/src/main/java/org/baxter/disco/ocr/Pair.java @@ -0,0 +1,39 @@ +package org.baxter.disco.ocr; + +/** + * Standard read-only pair object. + * + * @author Blizzard Finnegan + * @version 1.0.0, 20 Mar. 2023 + */ +public class Pair +{ + /** + * The first object in the pair. + */ + private final E first; + + /** + * The second object in the pair. + */ + private final F second; + + /** + * Default constructor. + */ + public Pair(E first, F second) + { + this.first = first; + this.second = second; + } + + /** + * Getter for the first value. + */ + public E first() { return first; } + + /** + * Getter for the second value. + */ + public F second() { return second; } +}