I made a simple C# library which detects incoming data from 1 or 2 scales plugged into a computer's usb.
My main concerns:
- I feel like I should potentially be using events/delegates to process the data, but I'm not sure how to approach this so would appreciate any advice
- My error-handling can probably be improved
- The application is not s flexible as it could be (e.g. I can't customize the serial port initialisation, or acocunt for different formats of data or number of scales attached to the machine)
- I am returning the read weights as a
Microsoft.VisualBasic.Collection
, as the library is currently being called by a macro in MS Access. However, if this was to be used by any other application, perhaps it would be better to return aSystem.Collections.Generic.IEnumerable
ScaleReader.cs
The main class to be instantiated and called. It initialises ports in the machine and then we can call the ReadAllWeights
method to get the data from any scales that are plugged in
public class ScaleReader
{
private List<Port> ports;
private bool multiScale;
/// <summary>
/// The constructor simply finds all COM ports which are in use and initialises
/// each of them with InitialisePort(string portName).
/// </summary>
public ScaleReader()
{
ports = PortHelper.GetPorts();
multiScale = (ports.Count > 1);
}
/// <summary>
/// Iterates through all COM ports stored in the Ports List field, and
/// calls ReadWeight(SerialPort port) for each one. The values output
/// from ReadWeight are returned as a Microsoft.VisualBasic.Collection.
/// </summary>
/// <remarks>
/// The reason why this method returns a VisualBasic collection rather
/// than a list or anything else is so that the items can be retrieved
/// from inside an MS-Access macro.
/// </remarks>
/// <returns></returns>
public Collection ReadAllWeights()
{
var weights = new Collection();
ports.ForEach(port
=> weights.Add(port.ReadWeight(multiScale)));
// If there is a mismatch between our weights and the ports, add some
// 0 weights as something has gone wrong (add 2 in case of multiweight)
if (weights.Count != ports.Count)
{
weights.Add(0);
weights.Add(0);
}
return weights;
}
}
PortHelper.cs
Contains some static methods to assist in dealing with the computer's ports - GetPortNames
(self-explanatory) and GetPorts
, which uses the port names to return a list of Port
objects
class PortHelper
{
/// <summary>
/// Returns a list of port names retrieved from SerialPort.GetPortNames()
/// </summary>
/// <returns></returns>
private static List<string> GetPortNames()
{
var portNameList = new List<string>();
try
{
var portNames = SerialPort.GetPortNames();
portNameList = portNames.ToList();
}
catch (System.ComponentModel.Win32Exception w32Ex)
{
var error = new Error();
error.Log(w32Ex, "Could not enumerate ports",
"Failed to call SerialPort.GetPortNames() in PortHelper.cs");
}
catch (ArgumentNullException nEx)
{
var error = new Error();
error.Log(nEx, "Could not convert ports to list",
"Failed to call ToList() on SerialPort.GetPortNames() in PortHelper.cs");
}
return portNameList;
}
/// <summary>
/// Gets a list of ports from GetPortNames() and then returns
/// a list of ports based on those names
/// </summary>
/// <param name="portNames"></param>
/// <returns></returns>
internal static List<Port> GetPorts()
{
var portNames = GetPortNames();
var ports = new List<Port>();
if (portNames.Count < 1)
{
MessageBox.Show("Could not find any connected scales.");
}
else
{
portNames.ForEach(portName =>
ports.Add(new Port(portName)));
}
return ports;
}
}
port.cs
This represents an instance of a SerialPort
, and contains some methods used by the scale reader, e.g. ReadWeight
- which attempts to process the incoming string data. There is some concern that this depends on the configuration of the scale/port, but I'm not sure what the best way would be to handle this. The Multiscale
option specified whether or not the computer is expected to have more than 1 scale plugged in
class Port
{
public SerialPort COMPort;
public readonly string Name;
const decimal ErrorValue = -999;
public Port(string portName)
{
Name = portName;
COMPort = new SerialPort()
{
PortName = Name,
BaudRate = 9600,
Parity = Parity.None,
DataBits = 8,
StopBits = StopBits.One,
Encoding = Encoding.ASCII,
Handshake = Handshake.None,
RtsEnable = true,
ReceivedBytesThreshold = 1,
ReadTimeout = 3000
};
}
/// <summary>
/// Open the given port and get the weight output, returning it as
/// a decimal.
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public decimal ReadWeight(bool multiScale)
{
var scaleDecimal = ErrorValue;
var giveup = 0;
Open();
if (COMPort.IsOpen)
{
using (COMPort)
{
while (scaleDecimal == ErrorValue && giveup < 20)
{
var scaleStringData = ReadData(multiScale);
scaleDecimal = ConvertStringToDecimal(scaleStringData);
giveup++;
}
}
}
else
{
var error = new Error();
error.Log(new Exception("the port wa s not open when checking it to read"),
"Port is not open", $"{COMPort.PortName}");
}
if (COMPort.IsOpen)
{
Close();
}
return scaleDecimal;
}
private decimal ConvertStringToDecimal(string scaleStringData)
{
if (!Decimal.TryParse(scaleStringData, out decimal outDecimal))
outDecimal = 0;
return outDecimal;
}
/// <summary>
/// Opens the current Port's SerialPort (COMPort)
/// </summary>
private void Open()
{
try
{
COMPort.Open();
}
catch (InvalidOperationException) // The port is already open
{
return;
}
catch (ArgumentException argEx)
{
var error = new Error();
error.Log(argEx,
$"Could not open port {COMPort.PortName}",
$"Port name: {Name} COMPort name: {COMPort.PortName}");
}
catch (Exception ex)
{
var error = new Error();
error.Log(ex, $"Could not open port {COMPort.PortName}",
$"IsOpen: {COMPort.IsOpen}");
}
}
private void Close()
{
try
{
COMPort.Close();
}
catch (Exception ex)
{
var error = new Error();
error.Log(ex, $"Could not close port {COMPort.PortName}",
$"IsOpen?: {COMPort.IsOpen}");
}
}
/// <summary>
/// Reads the output data of a given port and processes it to retrieve the
/// latest reading, returning it as a string.
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
private string ReadData(bool multiScale)
{
var scaleData = string.Empty;
if (multiScale)
{
scaleData = ReadMultiPortData();
}
else
{
scaleData = ReadSinglePortData();
}
return CleanString(scaleData);
}
private string ReadSinglePortData()
{
var scaleData = COMPort.ReadLine();
var giveup = 0;
while (string.IsNullOrEmpty(scaleData) & giveup < 1000)
{
scaleData = COMPort.ReadLine();
giveup++;
}
return scaleData;
}
/// <summary>
/// Data from the multi-scale machines looks different to that
/// sent from a single-scale machine
/// </summary>
/// <returns></returns>
private string ReadMultiPortData()
{
//var scaleData = COMPort.ReadExisting();
var scaleData = string.Empty;
var giveup = 0;
while (string.IsNullOrEmpty(scaleData) && giveup < 10000)
{
if (COMPort.BytesToRead > 0)
{
scaleData = COMPort.ReadExisting();
}
giveup++;
}
return ProcessMultiPortData(scaleData);
}
private string ProcessMultiPortData(string scaleData)
{
try
{
// TEST
var scaleDataArray = scaleData.Split('\r');
scaleData = scaleDataArray[scaleDataArray.Length - 2];
//// Find the penultimate \r
//var ultimateIndex = scaleData.LastIndexOf("\r");
//var penUtimateIndex =
// ultimateIndex > 0
// ? scaleData.LastIndexOf("\r", ultimateIndex - 1)
// : -1;
//// Remove everything before the penultimate /r
//scaleData = scaleData.Substring(penUtimateIndex);
//// Remove everything after the ultimate /r
//scaleData = scaleData.Substring(0, scaleData.LastIndexOf("\r"));
}
catch (Exception)
{
// This happens so often that it's probably not worth logging it ...
//var error = new Error();
//error.Log(ex, "Failed to process multiscale output data. Now returning scaleData as is", $"scaleData: {scaleData}");
return scaleData;
}
return scaleData;
}
/// <summary>
/// Calls a regex on the given string to return the string with everything
/// but +, - and [0-9]s removed.
/// </summary>
/// <param name="scaleData"></param>
/// <returns></returns>
private string CleanString(string scaleData)
{
var pattern = "[^0-9.+-]";
if (!string.IsNullOrEmpty(scaleData))
{
try
{
scaleData = Regex.Replace(scaleData, pattern, "");
}
catch (ArgumentException aEx)
{
var error = new Error();
error.Log(aEx, $"Error when calling Regex.Replace on stringstring {scaleData}",
$"Regex: {pattern}, scaleData: {scaleData}");
}
return scaleData;
}
else
{
return "0";
}
}
}