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; }
+}