From 846cbfbe8dc9e2de08f0faed781fdf9a49fb5c15 Mon Sep 17 00:00:00 2001 From: Alan Doherty Date: Tue, 30 Jul 2019 18:17:16 +0100 Subject: [PATCH] Initial work on 0.1.0 --- README.md | 61 +- SpiDriver.sln | 41 ++ img/spidriver.jpg | Bin 0 -> 38927 bytes .../Example.CommandLine.csproj | 12 + samples/Example.CommandLine/Program.cs | 164 ++++++ src/SpiDriver/DataStream.cs | 128 +++++ src/SpiDriver/Device.cs | 543 ++++++++++++++++++ src/SpiDriver/DeviceStatus.cs | 62 ++ src/SpiDriver/Output.cs | 27 + src/SpiDriver/SpiDriver.csproj | 41 ++ 10 files changed, 1077 insertions(+), 2 deletions(-) create mode 100644 SpiDriver.sln create mode 100644 img/spidriver.jpg create mode 100644 samples/Example.CommandLine/Example.CommandLine.csproj create mode 100644 samples/Example.CommandLine/Program.cs create mode 100644 src/SpiDriver/DataStream.cs create mode 100644 src/SpiDriver/Device.cs create mode 100644 src/SpiDriver/DeviceStatus.cs create mode 100644 src/SpiDriver/Output.cs create mode 100644 src/SpiDriver/SpiDriver.csproj diff --git a/README.md b/README.md index 59cb0af..37e3049 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -# spidriver-net -An unofficial .NET SDK for the SPIDriver debugger + + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/alandoherty/spidriver-net/master/LICENSE) +[![GitHub issues](https://img.shields.io/github/issues/alandoherty/spidriver-net.svg?style=flat-square)](https://github.com/alandoherty/spidriver-net/issues) +[![GitHub stars](https://img.shields.io/github/stars/alandoherty/spidriver-net.svg?style=flat-square)](https://github.com/alandoherty/spidriver-net/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/alandoherty/spidriver-net.svg?style=flat-square)](https://github.com/alandoherty/spidriver-net/network) +[![GitHub forks](https://img.shields.io/nuget/dt/SPIDriver.svg?style=flat-square)](https://www.nuget.org/packages/SPIDriver/) + + + +# spidriver + +An unofficial .NET library for [SPIDriver](https://spidriver.com/), allows for synchronous/asynchronous control of the device via the serial port. Open permissive MIT license with a dual-compliation for .NET Standard 2.0 and .NET Framework 4.6.1. + +![SPIDriver PCB](img/spidriver.jpg) + +## Getting Started + +[![NuGet Status](https://img.shields.io/nuget/v/SPIDriver.svg?style=flat-square)](https://www.nuget.org/packages/SPIDriver/) + +You can install the package using either the CLI: + +``` +dotnet add package SPIDriver +``` + +or from the NuGet package manager: + +``` +Install-Package SPIDriver +``` + +For assistance with the SPIDriver product itself you can view the [user guide](https://spidriver.com/spidriver.pdf) hosted on the official website. + +### Example + +You can find the example project `Example.CommandLine`, which demonstrates a CLI which implements all the features of the library. + +``` +> connect +Connected successfully +> status +Model: spidriver1 +Serial Number: DO01JHMO +Uptime: 01:56:22 +Voltage: 5.203V +Current: 0A +Temperature: 32.6°C +A: True +B: True +CS: True +CRC: 0x0001 +> writef airline.mp4 +Wrote 1510004 bytes in 33.294s +``` + +## Contributing + +Any pull requests or bug reports are welcome, please try and keep to the existing style conventions and comment any additions. \ No newline at end of file diff --git a/SpiDriver.sln b/SpiDriver.sln new file mode 100644 index 0000000..a05c6ad --- /dev/null +++ b/SpiDriver.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29009.5 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpiDriver", "src\SpiDriver\SpiDriver.csproj", "{DB31321A-1176-45BA-B346-3E935C461B62}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7C86F419-D0FB-4251-AF6C-2C9194EA1C05}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{EFCCC756-6E2D-4608-865D-5CE6B204C6F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.CommandLine", "samples\Example.CommandLine\Example.CommandLine.csproj", "{69010272-1995-4708-A354-CC8485F9109F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DB31321A-1176-45BA-B346-3E935C461B62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB31321A-1176-45BA-B346-3E935C461B62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB31321A-1176-45BA-B346-3E935C461B62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB31321A-1176-45BA-B346-3E935C461B62}.Release|Any CPU.Build.0 = Release|Any CPU + {69010272-1995-4708-A354-CC8485F9109F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69010272-1995-4708-A354-CC8485F9109F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69010272-1995-4708-A354-CC8485F9109F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69010272-1995-4708-A354-CC8485F9109F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {69010272-1995-4708-A354-CC8485F9109F} = {EFCCC756-6E2D-4608-865D-5CE6B204C6F8} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1AD50AD4-1933-4F12-BC76-14C291BA7587} + EndGlobalSection +EndGlobal diff --git a/img/spidriver.jpg b/img/spidriver.jpg new file mode 100644 index 0000000000000000000000000000000000000000..379ca78e8b73e13747cc30fda841de2691773e93 GIT binary patch literal 38927 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 literal 0 HcmV?d00001 diff --git a/samples/Example.CommandLine/Example.CommandLine.csproj b/samples/Example.CommandLine/Example.CommandLine.csproj new file mode 100644 index 0000000..6c0c373 --- /dev/null +++ b/samples/Example.CommandLine/Example.CommandLine.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp2.2 + + + + + + + diff --git a/samples/Example.CommandLine/Program.cs b/samples/Example.CommandLine/Program.cs new file mode 100644 index 0000000..9f5a2f2 --- /dev/null +++ b/samples/Example.CommandLine/Program.cs @@ -0,0 +1,164 @@ +using SpiDriver; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace Example.CommandLine +{ + class Program + { + static void Main(string[] argv) { + // the device + Device device = null; + + // loop through accepting commands + string line; + + while (true) { + // read next line + Console.Write("> "); + line = Console.ReadLine().Trim(); + + // parse the line + string[] components = line.Split(' '); + string[] args = components.Length == 0 ? new string[0] : new string[components.Length - 1]; + string cmd = components.Length == 0 ? null : components[0]; + + if (args.Length > 0) + Array.Copy(components, 1, args, 0, args.Length); + + // ignore empty lines + if (cmd == null) + continue; + + // process command + if (cmd.Equals("connect", StringComparison.CurrentCultureIgnoreCase)) { + if (args.Length == 0) { + Console.WriteLine("connect "); + continue; + } + + try { + device = new Device(args[0]); + device.Connect(); + Console.WriteLine("Connected successfully"); + } catch (Exception ex) { + Console.Error.WriteLine(ex.Message); + } + } else if (cmd.Equals("a", StringComparison.CurrentCultureIgnoreCase) || cmd.Equals("b", StringComparison.CurrentCultureIgnoreCase) + || cmd.Equals("cs", StringComparison.CurrentCultureIgnoreCase)) { + // check device is connected + if (device == null) { + Console.WriteLine("Not connected to device"); + continue; + } + + // determine output + Output output = Output.A; + + if (cmd.Equals("b", StringComparison.CurrentCultureIgnoreCase)) + output = Output.B; + else if (cmd.Equals("cs", StringComparison.CurrentCultureIgnoreCase)) + output = Output.CS; + + try { + if (args.Length == 0) { + bool value = device.GetOutput(output); + device.SetOutput(output, !value); + Console.WriteLine($"{output}: {!value}"); + } else { + if (args[0].Equals("yes", StringComparison.CurrentCultureIgnoreCase) || args[0].Equals("on", StringComparison.CurrentCultureIgnoreCase)) { + device.SetOutput(output, true); + } else { + device.SetOutput(output, false); + } + } + } catch (Exception ex) { + Console.Error.WriteLine(ex.Message); + } + } else if (cmd.Equals("status", StringComparison.CurrentCultureIgnoreCase)) { + if (device == null) { + Console.WriteLine("Not connected to device"); + continue; + } + + try { + DeviceStatus status = device.GetStatus(); + + Console.WriteLine($"Model: {status.Model}"); + Console.WriteLine($"Serial Number: {status.Serial}"); + Console.WriteLine($"Uptime: {status.Uptime}"); + Console.WriteLine($"Voltage: {status.Voltage}V"); + Console.WriteLine($"Current: {status.Current}A"); + Console.WriteLine($"Temperature: {status.Temperature}°C"); + Console.WriteLine($"A: {status.A}"); + Console.WriteLine($"B: {status.B}"); + Console.WriteLine($"CS: {status.ChipSelect}"); + Console.WriteLine($"CRC: 0x{status.Crc.ToString("x4")}"); + } catch (Exception ex) { + Console.Error.WriteLine(ex.Message); + } + } else if (cmd.Equals("writef", StringComparison.CurrentCultureIgnoreCase)) { + string path = string.Join(' ', args); + + if (device == null) { + Console.WriteLine("Not connected to device"); + continue; + } else if (args.Length == 0) { + Console.WriteLine("No data in arguments"); + continue; + } else if (!File.Exists(path)) { + Console.WriteLine("File does not exist"); + continue; + } + + // create stopwatch to time transfer + Stopwatch stopwatch = new Stopwatch(); + + try { + byte[] fb = File.ReadAllBytes(path); + + stopwatch.Start(); + device.Write(fb, 0, fb.Length); + + Console.WriteLine($"Wrote {fb.Length} bytes in {Math.Round(stopwatch.Elapsed.TotalSeconds, 3)}s"); + } catch (Exception ex) { + Console.Error.WriteLine(ex.Message); + } + } else if (cmd.Equals("read", StringComparison.CurrentCultureIgnoreCase)) { + int count = 0; + + if (device == null) { + Console.WriteLine("Not connected to device"); + continue; + } else if (args.Length == 0) { + Console.WriteLine("No data in arguments"); + continue; + } else if (!int.TryParse(args[0], out count)) { + Console.WriteLine("Invalid count to read"); + continue; + } + + try { + byte[] data = new byte[count]; + device.Read(data, 0, data.Length); + + Console.WriteLine(BitConverter.ToString(data)); + } catch (Exception ex) { + Console.Error.WriteLine(ex.Message); + } + } else if (cmd.Equals("help", StringComparison.CurrentCultureIgnoreCase)) { + Console.WriteLine("connect - connect to serial port"); + Console.WriteLine("a [on/off] - toggle or set A output"); + Console.WriteLine("b [on/off] - toggle or set B output"); + Console.WriteLine("cs [on/off] - toggle or set CS output"); + Console.WriteLine("status - get device status"); + Console.WriteLine("writef - write file to SPI"); + } else if (line != "quit" && line != "q" && line != "exit") { + return; + } + } + } + } +} diff --git a/src/SpiDriver/DataStream.cs b/src/SpiDriver/DataStream.cs new file mode 100644 index 0000000..2d0a77e --- /dev/null +++ b/src/SpiDriver/DataStream.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SpiDriver +{ + /// + /// Provides functionality to write/read data to the SPI component as a stream. + /// + public class DataStream : Stream + { + private Device _device; + + /// + /// Gets if this stream can read data. + /// + public override bool CanRead { + get { + return true; + } + } + + /// + /// Gets if this stream can seek. + /// + public override bool CanSeek { + get { + return false; + } + } + + /// + /// Gets if this stream can write data. + /// + public override bool CanWrite { + get { + return true; + } + } + + /// + /// Not supported. + /// + public override long Length { + get { + return 0; + } + } + + /// + /// Not supported. + /// + public override long Position { + get { + throw new NotImplementedException(); + } set { + throw new NotSupportedException(); + } + } + + /// + /// Flushes the stream. + /// + public override void Flush() { + } + + /// + /// Reads data from the SPI device. + /// + /// The byte array. + /// The offset in the byte array to begin reading. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) { + return _device.Read(buffer, offset, count); + } + + /// + /// Not supported. + /// + /// + /// + /// + public override long Seek(long offset, SeekOrigin origin) { + throw new NotSupportedException(); + } + + /// + /// Not supported. + /// + /// + public override void SetLength(long value) { + throw new NotSupportedException(); + } + + /// + /// Writes the byte array to the SPI device. + /// + /// The buffer. + /// The offset. + /// The count. + public override void Write(byte[] buffer, int offset, int count) { + _device.Write(buffer, offset, count); + } + + /// + /// Reads and writes data to and from the SPI device. + /// + /// The input byte array. + /// The offset in the byte array to begin reading. + /// The output byte array. + /// The offset in the byte array to begin writing. + /// The number of bytes to read. + /// The number of bytes written and read. + public int ReadWrite(byte[] inBytes, int inOffset, byte[] outBytes, int outOffset, int count) { + return _device.ReadWrite(inBytes, inOffset, outBytes, outOffset, count); + } + + /// + /// Creates a new data stream. + /// + /// The device. + internal DataStream(Device device) { + _device = device; + } + } +} diff --git a/src/SpiDriver/Device.cs b/src/SpiDriver/Device.cs new file mode 100644 index 0000000..8237bf0 --- /dev/null +++ b/src/SpiDriver/Device.cs @@ -0,0 +1,543 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Ports; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SpiDriver +{ + /// + /// Provides functionality to communicate with SPIDriver devices. + /// + public class Device + { + internal SerialPort _port; + private SemaphoreSlim _portSemaphore = new SemaphoreSlim(1, 1); + + /// + /// Gets or sets the read timeout, the default is infinite. + /// + public TimeSpan ReadTimeout { + get { + return TimeSpan.FromMilliseconds(_port.ReadTimeout); + } set { + _port.ReadTimeout = (int)value.TotalMilliseconds; + } + } + + /// + /// Gets or sets the write timeout, the default is infinite. + /// + public TimeSpan WriteTimeout { + get { + return TimeSpan.FromMilliseconds(_port.WriteTimeout); + } + set { + _port.WriteTimeout = (int)value.TotalMilliseconds; + } + } + + /// + /// Gets the serial port, direct access is not thread safe. + /// + public SerialPort Port { + get { + return _port; + } + } + + /// + /// Gets if the connection to the device is open. + /// + public bool IsOpen { + get { + return _port.IsOpen; + } + } + + private void ReadWire(byte[] buffer, int offset, int count) { + int dataRemaining = count; + + while (dataRemaining > 0) { + int dataRead = _port.Read(buffer, count - dataRemaining, dataRemaining); + dataRemaining -= dataRead; + } + } + + /// + /// Closes the device. + /// + public void Close() { + _port.Close(); + } + + /// + /// Gets a which provides read/write access to the underlying SPI data stream. + /// + /// + public Stream GetStream() { + return new DataStream(this); + } + + /// + /// Gets the current output state by querying the device status. + /// + /// The output. + /// The output value. + public bool GetOutput(Output output) { + DeviceStatus status = GetStatus(); + + if (output == Output.A) + return status.A; + else if (output == Output.B) + return status.B; + else if (output == Output.CS) + return status.ChipSelect; + else + throw new NotImplementedException(); + } + + /// + /// Gets the current output state by querying the device status asynchronously. + /// + /// The output. + /// The output value. + public async Task GetOutputAsync(Output output) { + DeviceStatus status = await GetStatusAsync().ConfigureAwait(false); + + if (output == Output.A) + return status.A; + else if (output == Output.B) + return status.B; + else if (output == Output.CS) + return status.ChipSelect; + else + throw new NotImplementedException(); + } + + /// + /// Gets the status of the device. + /// + /// The status. + public DeviceStatus GetStatus() { + if (!_port.IsOpen) + throw new InvalidOperationException("The serial port is not open"); + + _portSemaphore.Wait(); + + try { + // send request + _port.Write(new byte[] { (byte)'?' }, 0, 1); + + // get response + byte[] data = new byte[80]; + ReadWire(data, 0, 80); + + // convert to string + string dataStr = Encoding.ASCII.GetString(data) + .TrimStart('[') + .TrimEnd(']'); + + // split components + string[] dataComponents = dataStr.Split(' '); + + // fill status object + DeviceStatus status = new DeviceStatus(); + status.Model = dataComponents[0]; + status.Serial = dataComponents[1]; + status.Uptime = TimeSpan.FromSeconds((double)ulong.Parse(dataComponents[2])); + status.Voltage = float.Parse(dataComponents[3]); + status.Current = float.Parse(dataComponents[4]); + status.Temperature = float.Parse(dataComponents[5]); + status.A = int.Parse(dataComponents[6]) == 1; + status.B = int.Parse(dataComponents[7]) == 1; + status.ChipSelect = int.Parse(dataComponents[8]) == 1; + status.Crc = ushort.Parse(dataComponents[6], NumberStyles.HexNumber); + + return status; + } finally { + _portSemaphore.Release(); + } + } + + /// + /// Gets the status of the device asynchronously. + /// + /// + public async Task GetStatusAsync() { + if (!_port.IsOpen) + throw new InvalidOperationException("The serial port is not open"); + + await _portSemaphore.WaitAsync().ConfigureAwait(false); + + try { + return await Task.Run(() => { + // send request + _port.Write(new byte[] { (byte)'?' }, 0, 1); + + // get response + byte[] data = new byte[80]; + ReadWire(data, 0, 80); + + // convert to string + string dataStr = Encoding.ASCII.GetString(data) + .TrimStart('[') + .TrimEnd(']'); + + // split components + string[] dataComponents = dataStr.Split(' '); + + // fill status object + DeviceStatus status = new DeviceStatus(); + status.Model = dataComponents[0]; + status.Serial = dataComponents[1]; + status.Uptime = TimeSpan.FromSeconds((double)ulong.Parse(dataComponents[2])); + status.Voltage = float.Parse(dataComponents[3]); + status.Current = float.Parse(dataComponents[4]); + status.Temperature = float.Parse(dataComponents[5]); + status.A = int.Parse(dataComponents[6]) == 1; + status.B = int.Parse(dataComponents[7]) == 1; + status.ChipSelect = int.Parse(dataComponents[8]) == 1; + status.Crc = ushort.Parse(dataComponents[6], NumberStyles.HexNumber); + + return status; + }).ConfigureAwait(false); + } finally { + _portSemaphore.Release(); + } + } + + /// + /// Set the specified output pin on the device. + /// + /// The output pin. + /// The enable. + public void SetOutput(Output output, bool enable) { + if (!_port.IsOpen) + throw new InvalidOperationException("The serial port is not open"); + + _portSemaphore.Wait(); + + try { + if (output == Output.A) { + _port.Write(new byte[] { (byte)'a', enable ? (byte)1 : (byte)0 }, 0, 2); + } else if (output == Output.B) { + _port.Write(new byte[] { (byte)'b', enable ? (byte)1 : (byte)0 }, 0, 2); + } else if (output == Output.CS) { + _port.Write(new byte[] { enable ? (byte)'s' : (byte)'u'}, 0, 2); + } else { + throw new NotImplementedException(); + } + } finally { + _portSemaphore.Release(); + } + } + + /// + /// Set the specified output pin on the device asynchronously. + /// + /// The output pin. + /// The enable. + public async Task SetOutputAsync(Output output, bool enable) { + if (!_port.IsOpen) + throw new InvalidOperationException("The serial port is not open"); + + await _portSemaphore.WaitAsync().ConfigureAwait(false); + + try { + await Task.Run(() => { + if (output == Output.A) { + _port.Write(new byte[] { (byte)'a', enable ? (byte)1 : (byte)0 }, 0, 2); + } else if (output == Output.B) { + _port.Write(new byte[] { (byte)'b', enable ? (byte)1 : (byte)0 }, 0, 2); + } else if (output == Output.CS) { + _port.Write(new byte[] { enable ? (byte)'s' : (byte)'u' }, 0, 2); + } else { + throw new NotImplementedException(); + } + }).ConfigureAwait(false); + } finally { + _portSemaphore.Release(); + } + } + + /// + /// Connects to the serial port and performs connection tests. + /// + public void Connect() { + _portSemaphore.Wait(); + + try { + if (!_port.IsOpen) + _port.Open(); + + // write init message + _port.Write(Encoding.ASCII.GetBytes(new string('@', 64)), 0, 64); + + // write tests + byte[] tests = new byte[] { (byte)'A', (byte)'\r', (byte)'\n', 0xFF }; + byte[] writeCmd = new byte[] { (byte)'e', 0 }; + byte[] readCmd = new byte[] { 0 }; + + for (int i = 0; i < 4; i++) { + writeCmd[1] = tests[i]; + + // write test byte + _port.Write(writeCmd, 0, writeCmd.Length); + + // read test byte + ReadWire(readCmd, 0, 1); + + if (readCmd[0] != tests[i]) + throw new Exception("Response invalid during connection test"); + } + } finally { + _portSemaphore.Release(); + } + } + + /// + /// Connects to the serial port and performs connection tests asynchronously. + /// + public async Task ConnectAsync() { + await _portSemaphore.WaitAsync().ConfigureAwait(false); + + try { + await Task.Run(() => { + if (!_port.IsOpen) + _port.Open(); + + // write init message + _port.Write(Encoding.ASCII.GetBytes(new string('@', 64)), 0, 64); + + // write tests + byte[] tests = new byte[] { (byte)'A', (byte)'\r', (byte)'\n', 0xFF }; + byte[] writeCmd = new byte[] { (byte)'e', 0 }; + byte[] readCmd = new byte[] { 0 }; + + for (int i = 0; i < 4; i++) { + writeCmd[1] = tests[i]; + + // write test byte + _port.Write(writeCmd, 0, writeCmd.Length); + + // read test byte + ReadWire(readCmd, 0, 1); + + if (readCmd[0] != tests[i]) + throw new Exception("Response invalid during connection test"); + } + }).ConfigureAwait(false); + } finally { + _portSemaphore.Release(); + } + } + + /// + /// Writes data to the SPI device. + /// + /// The byte array. + /// The offset in the byte array to begin writing. + /// The number of bytes to write. + public void Write(byte[] bytes, int offset, int count) { + if (count == 0) + return; + + _portSemaphore.Wait(); + + try { + byte[] cmd = new byte[65]; + + for (int i = 0; i < count; i += 64) { + int len = ((count - i) < 64) ? (count - i) : 64; + + for (int j = 0; j < cmd.Length; j++) { + cmd[j] = (byte)(0xC0 + len - 1); + } + + Buffer.BlockCopy(bytes, offset + i, cmd, 1, len); + _port.Write(cmd, 0, 1 + len); + } + } finally { + _portSemaphore.Release(); + } + } + + /// + /// Writes data to the SPI device asynchronously. + /// + /// The byte array. + /// The offset in the byte array to begin writing. + /// The number of bytes to write. + public async Task WriteAsync(byte[] bytes, int offset, int count) { + if (count == 0) + return; + + await _portSemaphore.WaitAsync().ConfigureAwait(false); + + try { + await Task.Run(() => { + byte[] cmd = new byte[65]; + + for (int i = 0; i < count; i += 64) { + int len = ((count - i) < 64) ? (count - i) : 64; + + for (int j = 0; j < cmd.Length; j++) { + cmd[j] = (byte)(0xC0 + len - 1); + } + + Buffer.BlockCopy(bytes, offset + i, cmd, 1, len); + _port.Write(cmd, 0, 1 + len); + } + }).ConfigureAwait(false); + } finally { + _portSemaphore.Release(); + } + } + + /// + /// Reads data from the SPI device. + /// + /// The byte array. + /// The offset in the byte array to begin reading. + /// The number of bytes to read. + /// The number of bytes read. + public int Read(byte[] bytes, int offset, int count) { + if (count == 0) + return 0; + + _portSemaphore.Wait(); + + try { + byte[] cmd = new byte[65]; + + for (int i = 0; i < count; i += 64) { + int len = ((count - i) < 64) ? (count - i) : 64; + cmd[0] = (byte)(0x80 + len - 1); + _port.Write(cmd, 0, len + 1); + ReadWire(bytes, i + offset, len); + } + } finally { + _portSemaphore.Release(); + } + + return count; + } + + /// + /// Reads data from the SPI device. + /// + /// The byte array. + /// The offset in the byte array to begin reading. + /// The number of bytes to read. + /// The number of bytes read. + public async Task ReadAsync(byte[] bytes, int offset, int count) { + if (count == 0) + return 0; + + await _portSemaphore.WaitAsync().ConfigureAwait(false); + + try { + await Task.Run(() => { + byte[] cmd = new byte[65]; + + for (int i = 0; i < count; i += 64) { + int len = ((count - i) < 64) ? (count - i) : 64; + cmd[0] = (byte)(0x80 + len - 1); + _port.Write(cmd, 0, len + 1); + ReadWire(bytes, i + offset, len); + } + }).ConfigureAwait(false); + } finally { + _portSemaphore.Release(); + } + + return count; + } + + /// + /// Reads and writes data to and from the SPI device. + /// + /// The input byte array. + /// The offset in the byte array to begin reading. + /// The output byte array. + /// The offset in the byte array to begin writing. + /// The number of bytes to read. + /// The number of bytes written and read. + public int ReadWrite(byte[] inBytes, int inOffset, byte[] outBytes, int outOffset, int count) { + if (count == 0) + return 0; + + _portSemaphore.Wait(); + + try { + byte[] cmd = new byte[65]; + + for (int i = 0; i < count; i += 64) { + int len = ((count - i) < 64) ? (count - i) : 64; + cmd[0] = (byte)(0x80 + len - 1); + Buffer.BlockCopy(outBytes, outOffset + i, cmd, 1, len); + _port.Write(cmd, 0, len + 1); + ReadWire(inBytes, i + inOffset, len); + } + } finally { + _portSemaphore.Release(); + } + + return count; + } + + /// + /// Reads and writes data to and from the SPI device. + /// + /// The input byte array. + /// The offset in the byte array to begin reading. + /// The output byte array. + /// The offset in the byte array to begin writing. + /// The number of bytes to read. + /// The number of bytes written and read. + public async Task ReadWriteAsync(byte[] inBytes, int inOffset, byte[] outBytes, int outOffset, int count) { + if (count == 0) + return 0; + + await _portSemaphore.WaitAsync().ConfigureAwait(false); + + try { + await Task.Run(() => { + byte[] cmd = new byte[65]; + + for (int i = 0; i < count; i += 64) { + int len = ((count - i) < 64) ? (count - i) : 64; + cmd[0] = (byte)(0x80 + len - 1); + Buffer.BlockCopy(outBytes, outOffset + i, cmd, 1, len); + _port.Write(cmd, 0, len + 1); + ReadWire(inBytes, i + inOffset, len); + } + }).ConfigureAwait(false); + } finally { + _portSemaphore.Release(); + } + + return count; + } + + /// + /// Creates a new device with the specified serial port. + /// + /// The port. + public Device(SerialPort port) { + _port = port; + } + + /// + /// Creates a new device with the specified serial port. + /// + /// The port name. + public Device(string portName) { + _port = new SerialPort(portName, 460800, Parity.None, 8, StopBits.One); + } + } +} diff --git a/src/SpiDriver/DeviceStatus.cs b/src/SpiDriver/DeviceStatus.cs new file mode 100644 index 0000000..0926ac5 --- /dev/null +++ b/src/SpiDriver/DeviceStatus.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SpiDriver +{ + /// + /// Represents status information for the driver. + /// + public class DeviceStatus + { + /// + /// Gets the model. + /// + public string Model { get; internal set; } + + /// + /// Gets the serial number. + /// + public string Serial { get; internal set; } + + /// + /// Gets the uptime. + /// + public TimeSpan Uptime { get; internal set; } + + /// + /// Gets the voltage of the USB line. + /// + public float Voltage { get; internal set; } + + /// + /// Gets the current used by the target SPI device. + /// + public float Current { get; internal set; } + + /// + /// Gets the device temperature. + /// + public float Temperature { get; internal set; } + + /// + /// Gets the A state. + /// + public bool A { get; internal set; } + + /// + /// Gets the B state. + /// + public bool B { get; internal set; } + + /// + /// Gets the chip select state. + /// + public bool ChipSelect { get; internal set; } + + /// + /// Gets thge current CRC value. + /// + public ushort Crc { get; internal set; } + } +} diff --git a/src/SpiDriver/Output.cs b/src/SpiDriver/Output.cs new file mode 100644 index 0000000..d4032e9 --- /dev/null +++ b/src/SpiDriver/Output.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SpiDriver +{ + /// + /// Defines the output pins on the device. + /// + public enum Output + { + /// + /// The A output. + /// + A, + + /// + /// The B output. + /// + B, + + /// + /// The chip select output. + /// + CS + } +} diff --git a/src/SpiDriver/SpiDriver.csproj b/src/SpiDriver/SpiDriver.csproj new file mode 100644 index 0000000..25ac155 --- /dev/null +++ b/src/SpiDriver/SpiDriver.csproj @@ -0,0 +1,41 @@ + + + + netstandard2.0 + LICENSE + 0.1.0 + Alan Doherty + Alan Doherty + SPIDriver + SPIDriver + An unofficial library for SPIDriver allows for synchronous/asynchronous control of the device via the serial port + https://spidriver.com + https://github.com/alandoherty/spidriver-net + git + 0.1.0.0 + 0.1.0.0 + true + Alan Doherty (C) 2019 + https://s3-eu-west-1.amazonaws.com/assets.alandoherty.co.uk/github/spidriver-net-nuget.png + + + + bin\Debug\netstandard2.0\SPIDriver.xml + + + + bin\Release\netstandard2.0\SPIDriver.xml + + + + + + + + + True + + + + +