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>
<artifactId>ocr</artifactId>
<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>
<organization>
<name>Baxter International</name>
@ -95,9 +95,6 @@
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
<configuration>
<show>private</show>
</configuration>
</plugin>
</plugins>
</reporting>

View file

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

View file

@ -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

View file

@ -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<File,Double> resultMap = new HashMap<>();
Map<String,File> cameraToFile = new HashMap<>();
//Initialise cameraToFile, so keys don't shuffle.
for(String cameraName : cameraList)
{
cameraToFile.put(cameraName,new File("/dev/null"));
}
final Map<String,String> 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<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++)
{
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!");
}

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.
*
* @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<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;
int cellnum = 0;
int startingRow = outputSheet.getLastRowNum();
HSSFRow row = (cycle == 1) ? outputSheet.getRow(startingRow) : outputSheet.createRow(++startingRow);
List<String> cameraNames = new ArrayList<>(cameraToFile.keySet());
cycle++;
HSSFRow row = (cycle.getcycleNumber() == 1) ? outputSheet.getRow(startingRow) : outputSheet.createRow(++startingRow);
List<String> 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!");

View file

@ -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);

View file

@ -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();
}
}
/**

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