Threading overhaul + Updated datasaving

Data-saving at high cycle count takes too long, causing the DUT to fall
asleep. Data-saving has been extracted into its own thread.

Additionally, Run/pause switch has been re-threaded using synchronized
statements, rather than lock statements, making the run switch work
properly (finally).

Data is now saved in the new Cycle object, rather than passing around
multiple maps, hopefully improving memory footprint.
This commit is contained in:
Blizzard Finnegan 2023-03-20 14:45:04 -04:00
parent 52fe20fba5
commit e37f7b71f9
No known key found for this signature in database
GPG key ID: DE547EDF547DDA49
9 changed files with 246 additions and 108 deletions

View file

@ -4,7 +4,7 @@
<groupId>org.baxter.disco</groupId> <groupId>org.baxter.disco</groupId>
<artifactId>ocr</artifactId> <artifactId>ocr</artifactId>
<name>Disco OCR Accuracy Over Life Testing</name> <name>Disco OCR Accuracy Over Life Testing</name>
<version>4.3.8</version> <version>4.3.9</version>
<description>Testing Discos for long-term accuracy, using automated optical character recognition.</description> <description>Testing Discos for long-term accuracy, using automated optical character recognition.</description>
<organization> <organization>
<name>Baxter International</name> <name>Baxter International</name>
@ -95,9 +95,6 @@
<plugin> <plugin>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version> <version>3.4.1</version>
<configuration>
<show>private</show>
</configuration>
</plugin> </plugin>
</plugins> </plugins>
</reporting> </reporting>

View file

@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.baxter.disco</groupId> <groupId>org.baxter.disco</groupId>
<artifactId>ocr</artifactId> <artifactId>ocr</artifactId>
<version>4.3.8</version> <version>4.3.9</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Disco OCR Accuracy Over Life Testing</name> <name>Disco OCR Accuracy Over Life Testing</name>
<description>Testing Discos for long-term accuracy, using automated optical character recognition.</description> <description>Testing Discos for long-term accuracy, using automated optical character recognition.</description>

View file

@ -1,2 +1,2 @@
#! /usr/bin/env sh #! /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

View file

@ -3,12 +3,11 @@ package org.baxter.disco.ocr;
//Standard imports //Standard imports
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.locks.Lock; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit;
/** /**
* CLI for the Fixture. * CLI for the Fixture.
@ -18,20 +17,22 @@ import java.util.concurrent.locks.ReentrantLock;
* classes). * classes).
* *
* @author Blizzard Finnegan * @author Blizzard Finnegan
* @version 1.8.0, 16 Mar. 2023 * @version 1.9.0, 20 Mar. 2023
*/ */
public class Cli public class Cli
{ {
/** /**
* Complete build version number * Complete build version number
*/ */
private static final String version = "4.3.8"; private static final String version = "4.3.9";
/** /**
* Currently saved iteration count. * Currently saved iteration count.
*/ */
private static int iterationCount = 10; private static int iterationCount = 10;
private static boolean endOfCycles = false;
/** /**
* Scanner used for monitoring user input. * Scanner used for monitoring user input.
* This is a global object, so that functions * This is a global object, so that functions
@ -65,11 +66,6 @@ public class Cli
*/ */
private static final int cameraMenuOptionCount = 7; private static final int cameraMenuOptionCount = 7;
/**
* Lock object, used for temporary interruption of {@link #runTests()}
*/
public static final Lock LOCK = new ReentrantLock();
static static
{ {
@ -139,6 +135,7 @@ public class Cli
setActiveCameras(); setActiveCameras();
break; break;
case 5: case 5:
endOfCycles = false;
if(!camerasConfigured) if(!camerasConfigured)
{ {
prompt("You have not configured the cameras yet! Are you sure you would like to continue? (y/N): "); 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); } try{ Thread.sleep(2000); } catch(Exception e){ ErrorLogging.logError(e); }
} }
Map<File,Double> resultMap = new HashMap<>(); final Map<String,String> serials = ConfigFacade.getSerials();
Map<String,File> cameraToFile = new HashMap<>();
//Initialise cameraToFile, so keys don't shuffle.
for(String cameraName : cameraList)
{
cameraToFile.put(cameraName,new File("/dev/null"));
}
ErrorLogging.logError("DEBUG: Starting tests..."); 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<Cycle> 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++) for(int i = 0; i < localIterations; i++)
{ {
Cycle cycle = new Cycle(i);
println(""); println("");
ErrorLogging.logError("===================================="); ErrorLogging.logError("====================================");
ErrorLogging.logError("Starting iteration " + (i+1) + " of " + localIterations + "..."); ErrorLogging.logError("Starting iteration " + (i+1) + " of " + localIterations + "...");
@ -675,9 +685,7 @@ public class Cli
fail = false; fail = false;
if(safeGPIO) if(safeGPIO)
{ {
while(!LOCK.tryLock()) {}
MovementFacade.iterationMovement(prime); MovementFacade.iterationMovement(prime);
LOCK.unlock();
//Wait for the DUT to display an image //Wait for the DUT to display an image
try{ Thread.sleep(2000); } catch(Exception e){ ErrorLogging.logError(e); } try{ Thread.sleep(2000); } catch(Exception e){ ErrorLogging.logError(e); }
@ -685,27 +693,19 @@ public class Cli
for(String cameraName : cameraList) for(String cameraName : cameraList)
{ {
while(!LOCK.tryLock()) {} Thread cameraThread = new Thread(() -> {
File file = OpenCVFacade.completeProcess(cameraName); File file = OpenCVFacade.completeProcess(cameraName);
LOCK.unlock(); Double result = TesseractFacade.imageToDouble(file);
ErrorLogging.logError("Parsed value from camera " + cameraName +": " + result);
while(!LOCK.tryLock()) {} synchronized(cycle) { cycle.addCamera(cameraName,file,result); }
cameraToFile.replace(cameraName,file); });
LOCK.unlock(); cameraThread.start();
try{ cameraThread.join(); } catch(Exception e){ ErrorLogging.logError(e); }
} }
for(String cameraName : cameraList) for(String cameraName : cameraList)
{ {
while(!LOCK.tryLock()) {} double result = cycle.getValue(cameraName);
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();
if(result <= 10 || if(result <= 10 ||
result >= 100 || result >= 100 ||
result == Double.NEGATIVE_INFINITY) result == Double.NEGATIVE_INFINITY)
@ -723,14 +723,9 @@ public class Cli
} }
} }
while(fail); while(fail);
dataEntryQueue.add(cycle);
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();
} }
endOfCycles = true;
println("======================================="); println("=======================================");
println("Testing complete!"); println("Testing complete!");
} }

View file

@ -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<String,Pair<File,Double>> 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<String> 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<File,Double>(image, parsedValue)); }
}

View file

@ -32,7 +32,7 @@ import static org.apache.poi.hssf.util.HSSFColor.HSSFColorPredefined;
* Facade for saving data out to a file. * Facade for saving data out to a file.
* *
* @author Blizzard Finnegan * @author Blizzard Finnegan
* @version 5.0.1, 10 Mar. 2023 * @version 6.0.0, 10 Mar. 2023
*/ */
public class DataSaving public class DataSaving
{ {
@ -167,7 +167,7 @@ public class DataSaving
* *
* @param cameraCount The number of cameras that were used. * @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..."); ErrorLogging.logError("DEBUG: Updating formulas in Excel sheet...");
int rowIndex = 0; int rowIndex = 0;
@ -227,30 +227,29 @@ public class DataSaving
* Writes line to XLSX file. * Writes line to XLSX file.
* *
* @param cycle What test cycle is being saved to the 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. * @return Returns whether values were saved successfully.
*/ */
public static boolean writeValues(int cycle, Map<File,Double> inputMap, Map<String,File> cameraToFile) public static synchronized boolean writeValues(Cycle cycle, Map<String,String> 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; boolean output = false;
int cellnum = 0; int cellnum = 0;
int startingRow = outputSheet.getLastRowNum(); int startingRow = outputSheet.getLastRowNum();
HSSFRow row = (cycle == 1) ? outputSheet.getRow(startingRow) : outputSheet.createRow(++startingRow); HSSFRow row = (cycle.getcycleNumber() == 1) ? outputSheet.getRow(startingRow) : outputSheet.createRow(++startingRow);
List<String> cameraNames = new ArrayList<>(cameraToFile.keySet()); List<String> cameraNames = new ArrayList<>(cycle.getCameras());
cycle++;
HSSFCell indexCell = row.createCell(cellnum++); HSSFCell indexCell = row.createCell(cellnum++);
indexCell.setCellValue(cycle); indexCell.setCellValue(cycleNumber);
for(String cameraName : cameraNames) for(String cameraName : cameraNames)
{ {
String serialNumber = ConfigFacade.getSerial(cameraName); String serialNumber = serials.get(cameraName);
HSSFCell serialCell = row.createCell(cellnum++); HSSFCell serialCell = row.createCell(cellnum++);
serialCell.setCellValue(serialNumber); serialCell.setCellValue(serialNumber);
File file = cameraToFile.get(cameraName); File file = cycle.getImage(cameraName);
HSSFCell imageCell = row.createCell(cellnum++); HSSFCell imageCell = row.createCell(cellnum++);
try try
{ {
@ -275,7 +274,7 @@ public class DataSaving
//Put the OCR value into the sheet //Put the OCR value into the sheet
HSSFCell ocrCell = row.createCell(cellnum++); HSSFCell ocrCell = row.createCell(cellnum++);
Double ocrRead = inputMap.get(file); Double ocrRead = cycle.getValue(cameraName);
if(ocrRead.equals(Double.NEGATIVE_INFINITY)) if(ocrRead.equals(Double.NEGATIVE_INFINITY))
{ {
ocrCell.setCellValue("ERROR!"); ocrCell.setCellValue("ERROR!");

View file

@ -87,7 +87,7 @@ public class ErrorLogging
* *
* @param error Any error being thrown to be logged. * @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 errorStackTrace = ExceptionUtils.getStackTrace(error);
String errorMessage = datetime.format(LocalDateTime.now()) + " - " + errorStackTrace; String errorMessage = datetime.format(LocalDateTime.now()) + " - " + errorStackTrace;
@ -100,7 +100,7 @@ public class ErrorLogging
* *
* @param error Any information to save to the log file. * @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; String errorMessage = datetime.format(LocalDateTime.now()) + "\t- " + error;
fileOut.println(errorMessage); fileOut.println(errorMessage);

View file

@ -17,7 +17,7 @@ import com.pi4j.io.gpio.digital.PullResistance;
* Currently missing Run switch compatibility. * Currently missing Run switch compatibility.
* *
* @author Blizzard Finnegan * @author Blizzard Finnegan
* @version 3.1.0, 16 Mar. 2023 * @version 3.3.0, 20 Mar. 2023
*/ */
public class MovementFacade public class MovementFacade
{ {
@ -32,6 +32,11 @@ public class MovementFacade
*/ */
private static Thread runSwitchThread; 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. * 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..."); ErrorLogging.logError("DEBUG: Starting lock thread...");
runSwitchThread = new Thread(() -> runSwitchThread = new Thread(() ->
{ {
boolean unlock = false;
while(!exit) while(!exit)
{ {
if(runSwitch.isOn()) if(runSwitch.isOn())
{ {
ErrorLogging.logError("DEBUG: Run switch turned off!"); synchronized(lockObject)
while(!Cli.LOCK.tryLock()) {
{} while(runSwitch.isOn())
unlock = true;
}
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(100); } catch(Exception e){ErrorLogging.logError(e);}
if(runSwitch.isOff())
{
try{ Thread.sleep(100); } catch(Exception e){ErrorLogging.logError(e);}
if(runSwitch.isOff())
{
break;
}
}
}
}
}
try{ Thread.sleep(1); } catch(Exception e){ErrorLogging.logError(e);}
} }
}, "Run switch monitor."); }, "Run switch monitor.");
runSwitchThread.start(); runSwitchThread.start();
@ -255,15 +264,20 @@ public class MovementFacade
if(upperLimit.isHigh()) if(upperLimit.isHigh())
{ {
ErrorLogging.logError("DEBUG: Motor at highest point! Lowering to reset."); ErrorLogging.logError("DEBUG: Motor at highest point! Lowering to reset.");
synchronized(lockObject)
{
motorDirectionDown(); motorDirectionDown();
motorOn(); motorOn();
try{ Thread.sleep(500); } try{ Thread.sleep(500); }
catch (Exception e){ ErrorLogging.logError(e); } catch (Exception e){ ErrorLogging.logError(e); }
motorOff(); motorOff();
} }
}
ErrorLogging.logError("DEBUG: Moving motor to highest point."); ErrorLogging.logError("DEBUG: Moving motor to highest point.");
motorDirectionUp(); motorDirectionUp();
synchronized(lockObject)
{
motorOn(); motorOn();
for(counter = 0; counter < TIMEOUT; counter++) for(counter = 0; counter < TIMEOUT; counter++)
@ -276,12 +290,15 @@ public class MovementFacade
} }
} }
motorOff(); motorOff();
}
if(counter < TIMEOUT) if(counter < TIMEOUT)
{ {
ErrorLogging.logError("DEBUG: Motor returned after " + counter + " polls."); ErrorLogging.logError("DEBUG: Motor returned after " + counter + " polls.");
ErrorLogging.logError("DEBUG: --------------------------------------"); ErrorLogging.logError("DEBUG: --------------------------------------");
return counter; return counter;
} }
else else
{ {
ErrorLogging.logError("DEBUG: No motor return after 3 seconds."); ErrorLogging.logError("DEBUG: No motor return after 3 seconds.");
@ -372,6 +389,9 @@ public class MovementFacade
ErrorLogging.logError("DEBUG: Travel time: " + totalPollCount); ErrorLogging.logError("DEBUG: Travel time: " + totalPollCount);
ErrorLogging.logError("DEBUG: High speed poll count: " + highSpeedPolls); ErrorLogging.logError("DEBUG: High speed poll count: " + highSpeedPolls);
ErrorLogging.logError("DEBUG: ============================="); ErrorLogging.logError("DEBUG: =============================");
synchronized(lockObject)
{
motorOn(); motorOn();
for(int i = 0; i < highSpeedPolls; i++) for(int i = 0; i < highSpeedPolls; i++)
{ {
@ -387,6 +407,7 @@ public class MovementFacade
} }
} }
motorOff(); motorOff();
}
output = (limitSense.isOn() ? FinalState.UNSAFE : FinalState.SAFE); output = (limitSense.isOn() ? FinalState.UNSAFE : FinalState.SAFE);
@ -411,6 +432,8 @@ public class MovementFacade
* Extends the piston for 1 second, pushing the button on the DUT. * Extends the piston for 1 second, pushing the button on the DUT.
*/ */
public static void pressButton() public static void pressButton()
{
synchronized(lockObject)
{ {
ErrorLogging.logError("DEBUG: Pressing button..."); ErrorLogging.logError("DEBUG: Pressing button...");
pistonActivate.on(); pistonActivate.on();
@ -418,6 +441,7 @@ public class MovementFacade
ErrorLogging.logError("DEBUG: Releasing button..."); ErrorLogging.logError("DEBUG: Releasing button...");
pistonActivate.off(); pistonActivate.off();
} }
}
/** /**
* Closes connections to all GPIO pins. * Closes connections to all GPIO pins.

View file

@ -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<E,F>
{
/**
* 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; }
}