Added Latest TS.NET and removed legacy software #239
|
@ -0,0 +1,2 @@
|
|||
version=$(cat ../source/TS.NET.UI.Avalonia/TS.NET.UI.Avalonia.csproj | grep -oPm1 "(?<=<Version>)[^<]+")
|
||||
dotnet publish ../source/TS.NET.UI.Avalonia/TS.NET.UI.Avalonia.csproj -r linux-x64 -c Release --self-contained /p:PublishSingleFile=true /p:PublishTrimmed=true /p:IncludeNativeLibrariesForSelfExtract=true --output ../builds/linux-x64/TS.NET.UI.Avalonia/$version
|
|
@ -0,0 +1,2 @@
|
|||
version=$(cat ../source/TS.NET.UI.Avalonia/TS.NET.UI.Avalonia.csproj | grep -oPm1 "(?<=<Version>)[^<]+")
|
||||
dotnet publish ../source/TS.NET.Engine/TS.NET.Engine.csproj -r linux-x64 -c Release --self-contained /p:PublishSingleFile=true /p:PublishTrimmed=true /p:IncludeNativeLibrariesForSelfExtract=true --output ../builds/linux-x64/TS.NET.Engine/$version
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
public abstract record HardwareRequestDto();
|
||||
public record HardwareStartRequest() : HardwareRequestDto;
|
||||
public record HardwareStopRequest() : HardwareRequestDto;
|
||||
|
||||
public abstract record HardwareConfigureChannelDto(int Channel): HardwareRequestDto;
|
||||
public record HardwareSetEnabledRequest(int Channel, bool Enabled) : HardwareConfigureChannelDto(Channel);
|
||||
public record HardwareSetOffsetRequest(int Channel, double Offset) : HardwareConfigureChannelDto(Channel);
|
||||
public record HardwareSetVdivRequest(int Channel, int VoltsDiv) : HardwareConfigureChannelDto(Channel);
|
||||
public record HardwareSetBandwidthRequest(int Channel, int Bandwidth) : HardwareConfigureChannelDto(Channel);
|
||||
public record HardwareSetCouplingRequest(int Channel, ThunderscopeCoupling Coupling) : HardwareConfigureChannelDto(Channel);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using System;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
public record HardwareResponseDto(HardwareRequestDto Request);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using System;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
public record InputDataDto(ThunderscopeConfiguration Configuration, ThunderscopeMemory Memory);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
public abstract record ProcessingRequestDto();
|
||||
|
||||
public record ProcessingStartTriggerDto(bool ForceTrigger, bool OneShot) : ProcessingRequestDto;
|
||||
public record ProcessingStopTriggerDto() : ProcessingRequestDto;
|
||||
|
||||
public record ProcessingSetDepthDto(long Samples) : ProcessingRequestDto;
|
||||
public record ProcessingSetRateDto(long SamplingHz) : ProcessingRequestDto;
|
||||
|
||||
public record ProcessingSetTriggerSourceDto(TriggerChannel Channel) : ProcessingRequestDto;
|
||||
public record ProcessingSetTriggerDelayDto(long Femtoseconds) : ProcessingRequestDto;
|
||||
public record ProcessingSetTriggerLevelDto(double Level) : ProcessingRequestDto;
|
||||
public record ProcessingSetTriggerEdgeDirectionDto() : ProcessingRequestDto;
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using System;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
public record ProcessingResponseDto(ProcessingRequestDto Command);
|
||||
}
|
|
@ -3,26 +3,54 @@ using System.Diagnostics;
|
|||
using TS.NET;
|
||||
using TS.NET.Engine;
|
||||
|
||||
// The aim is to have a thread-safe lock-free dataflow architecture (to prevent various classes of bugs).
|
||||
// The use of async/await for processing is avoided as the task thread pool is of little use here.
|
||||
// Fire up threads to handle specific loops with extremely high utilisation. These threads are created once only, so the overhead of thread creation isn't important (one of the design goals of async/await).
|
||||
// Future work might pin CPU cores to exclusively process a particular thread, perhaps with high/rt priority.
|
||||
// Task.Factory.StartNew(() => Loop(...TaskCreationOptions.LongRunning) is just a shorthand for creating a new Thread to process a loop, the task thread pool isn't used.
|
||||
// The use of hardwareRequestChannel is to prevent 2 classes of bug: locking and thread safety.
|
||||
// By serialising the config-update/data-read it also allows for specific behaviours (like pausing acquisition on certain config updates) and ensuring a perfect match between sample-block & hardware configuration that created it.
|
||||
|
||||
Console.WriteLine("Starting...");
|
||||
|
||||
Console.Title = "Engine";
|
||||
using (Process p = Process.GetCurrentProcess())
|
||||
p.PriorityClass = ProcessPriorityClass.High;
|
||||
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(options => { options.SingleLine = true; options.TimestampFormat = "HH:mm:ss "; }).AddFilter(level => level >= LogLevel.Debug));
|
||||
|
||||
BlockingChannel<ThunderscopeMemory> memoryPool = new();
|
||||
for (int i = 0; i < 120; i++) // 120 = about 1 seconds worth of samples at 1GSPS
|
||||
memoryPool.Writer.Write(new ThunderscopeMemory());
|
||||
// Instantiate dataflow channels
|
||||
const int bufferLength = 120; // 120 = about 1 seconds worth of samples at 1GSPS
|
||||
BlockingChannel<ThunderscopeMemory> inputChannel = new(bufferLength);
|
||||
for (int i = 0; i < bufferLength; i++)
|
||||
inputChannel.Writer.Write(new ThunderscopeMemory());
|
||||
BlockingChannel<InputDataDto> processingChannel = new();
|
||||
BlockingChannel<HardwareRequestDto> hardwareRequestChannel = new();
|
||||
BlockingChannel<HardwareResponseDto> hardwareResponseChannel = new();
|
||||
BlockingChannel<ProcessingRequestDto> processingRequestChannel = new();
|
||||
BlockingChannel<ProcessingResponseDto> processingResponseChannel = new();
|
||||
|
||||
Thread.Sleep(1000);
|
||||
|
||||
BlockingChannel<ThunderscopeMemory> processingPool = new();
|
||||
// Find thunderscope
|
||||
var devices = Thunderscope.IterateDevices();
|
||||
if (devices.Count == 0)
|
||||
throw new Exception("No thunderscopes found");
|
||||
|
||||
// Start threads
|
||||
ProcessingTask processingTask = new();
|
||||
processingTask.Start(loggerFactory, processingPool.Reader, memoryPool.Writer);
|
||||
processingTask.Start(loggerFactory, processingChannel.Reader, inputChannel.Writer, processingRequestChannel.Reader, processingResponseChannel.Writer);
|
||||
InputTask inputTask = new();
|
||||
inputTask.Start(loggerFactory, memoryPool.Reader, processingPool.Writer);
|
||||
inputTask.Start(loggerFactory, devices[0], inputChannel.Reader, processingChannel.Writer, hardwareRequestChannel.Reader, hardwareResponseChannel.Writer);
|
||||
SocketTask socketTask = new();
|
||||
socketTask.Start(loggerFactory, processingRequestChannel.Writer);
|
||||
SCPITask scpiTask = new();
|
||||
scpiTask.Start(loggerFactory, hardwareRequestChannel.Writer, hardwareResponseChannel.Reader, processingRequestChannel.Writer, processingResponseChannel.Reader);
|
||||
|
||||
Console.WriteLine("Running... press any key to stop");
|
||||
Console.ReadKey();
|
||||
|
||||
processingTask.Stop();
|
||||
inputTask.Stop();
|
||||
inputTask.Stop();
|
||||
socketTask.Stop();
|
||||
scpiTask.Stop();
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
|
@ -9,11 +10,17 @@ namespace TS.NET.Engine
|
|||
private CancellationTokenSource? cancelTokenSource;
|
||||
private Task? taskLoop;
|
||||
|
||||
public void Start(ILoggerFactory loggerFactory, BlockingChannelReader<ThunderscopeMemory> memoryPool, BlockingChannelWriter<ThunderscopeMemory> processingPool)
|
||||
public void Start(
|
||||
ILoggerFactory loggerFactory,
|
||||
ThunderscopeDevice thunderscopeDevice,
|
||||
BlockingChannelReader<ThunderscopeMemory> inputChannel,
|
||||
BlockingChannelWriter<InputDataDto> processingChannel,
|
||||
BlockingChannelReader<HardwareRequestDto> hardwareRequestChannel,
|
||||
BlockingChannelWriter<HardwareResponseDto> hardwareResponseChannel)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger("InputTask");
|
||||
cancelTokenSource = new CancellationTokenSource();
|
||||
taskLoop = Task.Factory.StartNew(() => Loop(logger, memoryPool, processingPool, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
|
||||
taskLoop = Task.Factory.StartNew(() => Loop(logger, thunderscopeDevice, inputChannel, processingChannel, hardwareRequestChannel, hardwareResponseChannel, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
|
@ -22,48 +29,161 @@ namespace TS.NET.Engine
|
|||
taskLoop?.Wait();
|
||||
}
|
||||
|
||||
private static void Loop(ILogger logger, BlockingChannelReader<ThunderscopeMemory> memoryPool, BlockingChannelWriter<ThunderscopeMemory> processingPool, CancellationToken cancelToken)
|
||||
private static void Loop(
|
||||
ILogger logger,
|
||||
ThunderscopeDevice thunderscopeDevice,
|
||||
BlockingChannelReader<ThunderscopeMemory> inputChannel,
|
||||
BlockingChannelWriter<InputDataDto> processingChannel,
|
||||
BlockingChannelReader<HardwareRequestDto> hardwareRequestChannel,
|
||||
BlockingChannelWriter<HardwareResponseDto> hardwareResponseChannel,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
Thread.CurrentThread.Name = "TS.NET Input";
|
||||
Thread.CurrentThread.Priority = ThreadPriority.Highest;
|
||||
Thunderscope thunderscope = new();
|
||||
try
|
||||
{
|
||||
Thread.CurrentThread.Name = "TS.NET Input";
|
||||
Thread.CurrentThread.Priority = ThreadPriority.Highest;
|
||||
|
||||
var devices = Thunderscope.IterateDevices();
|
||||
if (devices.Count == 0)
|
||||
throw new Exception("No thunderscopes found");
|
||||
Thunderscope thunderscope = new Thunderscope();
|
||||
thunderscope.Open(devices[0]);
|
||||
thunderscope.EnableChannel(0);
|
||||
thunderscope.EnableChannel(1);
|
||||
thunderscope.EnableChannel(2);
|
||||
thunderscope.EnableChannel(3);
|
||||
thunderscope.Open(thunderscopeDevice);
|
||||
ThunderscopeConfiguration configuration = DoInitialConfiguration(thunderscope);
|
||||
thunderscope.Start();
|
||||
|
||||
Stopwatch oneSecond = Stopwatch.StartNew();
|
||||
uint oneSecondEnqueueCount = 0;
|
||||
uint enqueueCounter = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
var memory = memoryPool.Read();
|
||||
try
|
||||
|
||||
// Check for configuration requests
|
||||
if (hardwareRequestChannel.PeekAvailable() != 0)
|
||||
{
|
||||
thunderscope.Read(memory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message == "ReadFile - failed (1359)")
|
||||
logger.LogDebug("Stop acquisition and process commands...");
|
||||
thunderscope.Stop();
|
||||
|
||||
while (hardwareRequestChannel.TryRead(out var request))
|
||||
{
|
||||
logger.LogError(ex, $"{nameof(InputTask)} error");
|
||||
// Do configuration update, pausing acquisition if necessary
|
||||
if (request is HardwareStartRequest)
|
||||
{
|
||||
logger.LogDebug("Start request (ignore)");
|
||||
}
|
||||
else if (request is HardwareStopRequest)
|
||||
{
|
||||
logger.LogDebug("Stop request (ignore)");
|
||||
}
|
||||
else if (request is HardwareConfigureChannelDto)
|
||||
{
|
||||
var chNum = ((HardwareConfigureChannelDto)request).Channel;
|
||||
ThunderscopeChannel ch = configuration.GetChannel(chNum);
|
||||
|
||||
if (request is HardwareSetOffsetRequest)
|
||||
{
|
||||
var voltage = ((HardwareSetOffsetRequest)request).Offset;
|
||||
logger.LogDebug($"Set offset request: ch {chNum} voltage {voltage}");
|
||||
ch.VoltsOffset = voltage;
|
||||
}
|
||||
else if (request is HardwareSetVdivRequest)
|
||||
{
|
||||
var vdiv = ((HardwareSetVdivRequest)request).VoltsDiv;
|
||||
logger.LogDebug($"Set vdiv request: ch {chNum} div {vdiv}");
|
||||
ch.VoltsDiv = vdiv;
|
||||
}
|
||||
else if (request is HardwareSetBandwidthRequest)
|
||||
{
|
||||
var bw = ((HardwareSetBandwidthRequest)request).Bandwidth;
|
||||
logger.LogDebug($"Set bw request: ch {chNum} bw {bw}");
|
||||
ch.Bandwidth = bw;
|
||||
}
|
||||
else if (request is HardwareSetCouplingRequest)
|
||||
{
|
||||
var coup = ((HardwareSetCouplingRequest)request).Coupling;
|
||||
logger.LogDebug($"Set coup request: ch {chNum} coup {coup}");
|
||||
ch.Coupling = coup;
|
||||
}
|
||||
else if (request is HardwareSetEnabledRequest)
|
||||
{
|
||||
var enabled = ((HardwareSetEnabledRequest)request).Enabled;
|
||||
logger.LogDebug($"Set enabled request: ch {chNum} enabled {enabled}");
|
||||
ch.Enabled = enabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning($"Unknown HardwareConfigureChannelDto: {request}");
|
||||
}
|
||||
|
||||
configuration.SetChannel(chNum, ch);
|
||||
ConfigureFromObject(thunderscope, configuration);
|
||||
thunderscope.EnableChannel(chNum);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning($"Unknown HardwareRequestDto: {request}");
|
||||
}
|
||||
|
||||
// Signal back to the sender that config update happened.
|
||||
// hardwareResponseChannel.TryWrite(new HardwareResponseDto(request));
|
||||
|
||||
if (hardwareRequestChannel.PeekAvailable() == 0)
|
||||
Thread.Sleep(150);
|
||||
}
|
||||
|
||||
logger.LogDebug("Start again");
|
||||
thunderscope.Start();
|
||||
}
|
||||
|
||||
var memory = inputChannel.Read();
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
thunderscope.Read(memory);
|
||||
break;
|
||||
}
|
||||
catch (ThunderscopeMemoryOutOfMemoryException ex)
|
||||
{
|
||||
logger.LogWarning("Scope ran out of memory - reset buffer pointers and continue");
|
||||
thunderscope.ResetBuffer();
|
||||
continue;
|
||||
}
|
||||
throw;
|
||||
catch (ThunderscopeFIFOOverflowException ex)
|
||||
{
|
||||
logger.LogWarning("Scope had FIFO overflow - ignore and continue");
|
||||
continue;
|
||||
}
|
||||
catch (ThunderscopeNotRunningException ex)
|
||||
{
|
||||
// logger.LogWarning("Tried to read from stopped scope");
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message == "ReadFile - failed (1359)")
|
||||
{
|
||||
logger.LogError(ex, $"{nameof(InputTask)} error");
|
||||
continue;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
oneSecondEnqueueCount++;
|
||||
enqueueCounter++;
|
||||
|
||||
processingChannel.Write(new InputDataDto(configuration, memory), cancelToken);
|
||||
|
||||
if (oneSecond.ElapsedMilliseconds >= 10000)
|
||||
{
|
||||
logger.LogDebug($"Enqueues/sec: {oneSecondEnqueueCount / (oneSecond.ElapsedMilliseconds * 0.001):F2}, enqueue count: {enqueueCounter}");
|
||||
oneSecond.Restart();
|
||||
oneSecondEnqueueCount = 0;
|
||||
}
|
||||
processingPool.Write(memory);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogDebug($"{nameof(InputTask)} stopping");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -72,8 +192,66 @@ namespace TS.NET.Engine
|
|||
}
|
||||
finally
|
||||
{
|
||||
thunderscope.Stop();
|
||||
logger.LogDebug($"{nameof(InputTask)} stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private static ThunderscopeConfiguration DoInitialConfiguration(Thunderscope thunderscope)
|
||||
{
|
||||
ThunderscopeConfiguration configuration = new()
|
||||
{
|
||||
AdcChannels = AdcChannels.Four,
|
||||
Channel0 = new ThunderscopeChannel()
|
||||
{
|
||||
Enabled = true,
|
||||
VoltsOffset = 0,
|
||||
VoltsDiv = 100,
|
||||
Bandwidth = 350,
|
||||
Coupling = ThunderscopeCoupling.DC
|
||||
},
|
||||
Channel1 = new ThunderscopeChannel()
|
||||
{
|
||||
Enabled = true,
|
||||
VoltsOffset = 0,
|
||||
VoltsDiv = 100,
|
||||
Bandwidth = 350,
|
||||
Coupling = ThunderscopeCoupling.DC
|
||||
},
|
||||
Channel2 = new ThunderscopeChannel()
|
||||
{
|
||||
Enabled = true,
|
||||
VoltsOffset = 0,
|
||||
VoltsDiv = 100,
|
||||
Bandwidth = 350,
|
||||
Coupling = ThunderscopeCoupling.DC
|
||||
},
|
||||
Channel3 = new ThunderscopeChannel()
|
||||
{
|
||||
Enabled = true,
|
||||
VoltsOffset = 0,
|
||||
VoltsDiv = 100,
|
||||
Bandwidth = 350,
|
||||
Coupling = ThunderscopeCoupling.DC
|
||||
},
|
||||
};
|
||||
|
||||
ConfigureFromObject(thunderscope, configuration);
|
||||
|
||||
thunderscope.EnableChannel(0);
|
||||
thunderscope.EnableChannel(1);
|
||||
thunderscope.EnableChannel(2);
|
||||
thunderscope.EnableChannel(3);
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private static void ConfigureFromObject(Thunderscope thunderscope, ThunderscopeConfiguration configuration)
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
thunderscope.Channels[i] = configuration.GetChannel(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
|
@ -10,16 +9,20 @@ namespace TS.NET.Engine
|
|||
private CancellationTokenSource? cancelTokenSource;
|
||||
private Task? taskLoop;
|
||||
|
||||
//, Action<Memory<double>> action
|
||||
public void Start(ILoggerFactory loggerFactory, BlockingChannelReader<ThunderscopeMemory> processingPool, BlockingChannelWriter<ThunderscopeMemory> memoryPool)
|
||||
public void Start(
|
||||
ILoggerFactory loggerFactory,
|
||||
BlockingChannelReader<InputDataDto> processingChannel,
|
||||
BlockingChannelWriter<ThunderscopeMemory> inputChannel,
|
||||
BlockingChannelReader<ProcessingRequestDto> processingRequestChannel,
|
||||
BlockingChannelWriter<ProcessingResponseDto> processingResponseChannel)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger("ProcessingTask");
|
||||
cancelTokenSource = new CancellationTokenSource();
|
||||
ulong capacityBytes = 4 * 100 * 1000 * 1000; // Maximum capacity = 100M samples per channel
|
||||
ulong dataCapacityBytes = 4 * 100 * 1000 * 1000; // Maximum capacity = 100M samples per channel
|
||||
// Bridge is cross-process shared memory for the UI to read triggered acquisitions
|
||||
// The trigger point is _always_ in the middle of the channel block, and when the UI sets positive/negative trigger point, it's just moving the UI viewport
|
||||
ThunderscopeBridgeWriter bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", capacityBytes), loggerFactory);
|
||||
taskLoop = Task.Factory.StartNew(() => Loop(logger, processingPool, memoryPool, bridge, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
|
||||
ThunderscopeBridgeWriter bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", dataCapacityBytes), loggerFactory);
|
||||
taskLoop = Task.Factory.StartNew(() => Loop(logger, bridge, processingChannel, inputChannel, processingRequestChannel, processingResponseChannel, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
|
@ -29,30 +32,31 @@ namespace TS.NET.Engine
|
|||
}
|
||||
|
||||
// The job of this task - pull data from scope driver/simulator, shuffle if 2/4 channels, horizontal sum, trigger, and produce window segments.
|
||||
private static void Loop(ILogger logger, BlockingChannelReader<ThunderscopeMemory> processingPool, BlockingChannelWriter<ThunderscopeMemory> memoryPool, ThunderscopeBridgeWriter bridge, CancellationToken cancelToken)
|
||||
private static void Loop(
|
||||
ILogger logger,
|
||||
ThunderscopeBridgeWriter bridge,
|
||||
BlockingChannelReader<InputDataDto> processingChannel,
|
||||
BlockingChannelWriter<ThunderscopeMemory> inputChannel,
|
||||
BlockingChannelReader<ProcessingRequestDto> processingRequestChannel,
|
||||
BlockingChannelWriter<ProcessingResponseDto> processingResponseChannel,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
const int initialMaxChannelLength = 10 * 1000000;
|
||||
|
||||
Thread.CurrentThread.Name = "TS.NET Processing";
|
||||
|
||||
// Configuration values to be updated during runtime... conveiniently all on ThunderscopeMemoryBridgeHeader
|
||||
ThunderscopeConfiguration config = new()
|
||||
ThunderscopeProcessing processingConfig = new()
|
||||
{
|
||||
Channels = Channels.Four,
|
||||
ChannelLength = 10 * 1000000,//(ulong)ChannelLength.OneHundredM,
|
||||
ChannelLength = initialMaxChannelLength,
|
||||
HorizontalSumLength = HorizontalSumLength.None,
|
||||
TriggerChannel = TriggerChannel.One,
|
||||
TriggerMode = TriggerMode.Normal
|
||||
TriggerMode = TriggerMode.Normal,
|
||||
ChannelDataType = ThunderscopeChannelDataType.Byte
|
||||
};
|
||||
bridge.Configuration = config;
|
||||
|
||||
ThunderscopeMonitoring monitoring = new()
|
||||
{
|
||||
TotalAcquisitions = 0,
|
||||
MissedAcquisitions = 0
|
||||
};
|
||||
bridge.Monitoring = monitoring;
|
||||
var bridgeWriterSemaphore = bridge.GetWriterSemaphore();
|
||||
bridge.Processing = processingConfig;
|
||||
bridge.MonitoringReset();
|
||||
|
||||
// Various buffers allocated once and reused forevermore.
|
||||
//Memory<byte> hardwareBuffer = new byte[ThunderscopeMemory.Length];
|
||||
|
@ -71,28 +75,90 @@ namespace TS.NET.Engine
|
|||
|
||||
Span<uint> triggerIndices = new uint[ThunderscopeMemory.Length / 1000]; // 1000 samples is the minimum holdoff
|
||||
Span<uint> holdoffEndIndices = new uint[ThunderscopeMemory.Length / 1000]; // 1000 samples is the minimum holdoff
|
||||
RisingEdgeTriggerAlt trigger = new(200, 190, (ulong)(config.ChannelLength/2));
|
||||
RisingEdgeTriggerAlt trigger = new(200, 190, (ulong)(processingConfig.ChannelLength / 2));
|
||||
|
||||
DateTimeOffset startTime = DateTimeOffset.UtcNow;
|
||||
uint dequeueCounter = 0;
|
||||
uint oneSecondHoldoffCount = 0;
|
||||
uint oneSecondDequeueCount = 0;
|
||||
// HorizontalSumUtility.ToDivisor(horizontalSumLength)
|
||||
Stopwatch oneSecond = Stopwatch.StartNew();
|
||||
|
||||
var circularBuffer1 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length);
|
||||
var circularBuffer2 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length);
|
||||
var circularBuffer3 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length);
|
||||
var circularBuffer4 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length);
|
||||
var circularBuffer1 = new ChannelCircularAlignedBuffer((uint)processingConfig.ChannelLength + ThunderscopeMemory.Length);
|
||||
var circularBuffer2 = new ChannelCircularAlignedBuffer((uint)processingConfig.ChannelLength + ThunderscopeMemory.Length);
|
||||
var circularBuffer3 = new ChannelCircularAlignedBuffer((uint)processingConfig.ChannelLength + ThunderscopeMemory.Length);
|
||||
var circularBuffer4 = new ChannelCircularAlignedBuffer((uint)processingConfig.ChannelLength + ThunderscopeMemory.Length);
|
||||
|
||||
bool forceTrigger = false;
|
||||
bool oneShotTrigger = false;
|
||||
bool triggerRunning = false;
|
||||
|
||||
uint clientRequestedDepth = (uint)processingConfig.ChannelLength;
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
var memory = processingPool.Read(cancelToken);
|
||||
// Add a zero-wait mechanism here that allows for configuration values to be updated
|
||||
// (which will require updating many of the intermediate variables/buffers)
|
||||
|
||||
// Check for processing requests
|
||||
if (processingRequestChannel.TryRead(out var request))
|
||||
{
|
||||
if (request is ProcessingStartTriggerDto)
|
||||
{
|
||||
triggerRunning = true;
|
||||
oneShotTrigger = ((ProcessingStartTriggerDto)request).OneShot;
|
||||
forceTrigger = ((ProcessingStartTriggerDto)request).ForceTrigger;
|
||||
|
||||
logger.LogDebug($"Start: triggerRunning={triggerRunning}, oneShotTrigger={oneShotTrigger}, forceTrigger={forceTrigger}");
|
||||
}
|
||||
else if (request is ProcessingStopTriggerDto)
|
||||
{
|
||||
triggerRunning = false;
|
||||
logger.LogDebug("Stop");
|
||||
}
|
||||
else if (request is ProcessingSetDepthDto)
|
||||
{
|
||||
var depth = ((ProcessingSetDepthDto)request).Samples;
|
||||
depth = Math.Min(depth, initialMaxChannelLength);
|
||||
processingConfig.ChannelLength = (int)depth;
|
||||
// TODO: This races with a reader since there are two regions and only one processingConfig
|
||||
// TODO: Does not resize buffers above, so cannot increase from initial
|
||||
}
|
||||
else if (request is ProcessingSetRateDto)
|
||||
{
|
||||
var rate = ((ProcessingSetRateDto)request).SamplingHz;
|
||||
}
|
||||
else if (request is ProcessingSetTriggerSourceDto)
|
||||
{
|
||||
var channel = ((ProcessingSetTriggerSourceDto)request).Channel;
|
||||
processingConfig.TriggerChannel = channel;
|
||||
}
|
||||
else if (request is ProcessingSetTriggerDelayDto)
|
||||
{
|
||||
var fs = ((ProcessingSetTriggerDelayDto)request).Femtoseconds;
|
||||
}
|
||||
else if (request is ProcessingSetTriggerLevelDto)
|
||||
{
|
||||
var level = ((ProcessingSetTriggerLevelDto)request).Level;
|
||||
}
|
||||
else if (request is ProcessingSetTriggerEdgeDirectionDto)
|
||||
{
|
||||
// var edges = ((ProcessingSetTriggerEdgeDirectionDto)request).Edges;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning($"Unknown ProcessingRequestDto: {request}");
|
||||
}
|
||||
|
||||
bridge.Processing = processingConfig;
|
||||
}
|
||||
|
||||
InputDataDto processingDto = processingChannel.Read(cancelToken);
|
||||
bridge.Configuration = processingDto.Configuration;
|
||||
dequeueCounter++;
|
||||
int channelLength = config.ChannelLength;
|
||||
switch (config.Channels)
|
||||
oneSecondDequeueCount++;
|
||||
|
||||
int channelLength = processingConfig.ChannelLength;
|
||||
switch (processingDto.Configuration.AdcChannels)
|
||||
{
|
||||
// Processing pipeline:
|
||||
// Shuffle (if needed)
|
||||
|
@ -100,32 +166,32 @@ namespace TS.NET.Engine
|
|||
// Write to circular buffer
|
||||
// Trigger
|
||||
// Data segment on trigger (if needed)
|
||||
case Channels.None:
|
||||
case AdcChannels.None:
|
||||
break;
|
||||
case Channels.One:
|
||||
case AdcChannels.One:
|
||||
// Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
|
||||
//if (config.HorizontalSumLength != HorizontalSumLength.None)
|
||||
// throw new NotImplementedException();
|
||||
// Write to circular buffer
|
||||
circularBuffer1.Write(memory.Span);
|
||||
circularBuffer1.Write(processingDto.Memory.Span);
|
||||
// Trigger
|
||||
if (config.TriggerChannel != TriggerChannel.None)
|
||||
if (processingConfig.TriggerChannel != TriggerChannel.None)
|
||||
{
|
||||
var triggerChannelBuffer = config.TriggerChannel switch
|
||||
var triggerChannelBuffer = processingConfig.TriggerChannel switch
|
||||
{
|
||||
TriggerChannel.One => memory.Span,
|
||||
TriggerChannel.One => processingDto.Memory.Span,
|
||||
_ => throw new ArgumentException("Invalid TriggerChannel value")
|
||||
};
|
||||
trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
|
||||
}
|
||||
// Finished with the memory, return it
|
||||
memoryPool.Write(memory);
|
||||
inputChannel.Write(processingDto.Memory);
|
||||
break;
|
||||
case Channels.Two:
|
||||
case AdcChannels.Two:
|
||||
// Shuffle
|
||||
Shuffle.TwoChannels(input: memory.Span, output: shuffleBuffer);
|
||||
Shuffle.TwoChannels(input: processingDto.Memory.Span, output: shuffleBuffer);
|
||||
// Finished with the memory, return it
|
||||
memoryPool.Write(memory);
|
||||
inputChannel.Write(processingDto.Memory);
|
||||
// Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
|
||||
//if (config.HorizontalSumLength != HorizontalSumLength.None)
|
||||
// throw new NotImplementedException();
|
||||
|
@ -133,9 +199,9 @@ namespace TS.NET.Engine
|
|||
circularBuffer1.Write(postShuffleCh1_2);
|
||||
circularBuffer2.Write(postShuffleCh2_2);
|
||||
// Trigger
|
||||
if (config.TriggerChannel != TriggerChannel.None)
|
||||
if (processingConfig.TriggerChannel != TriggerChannel.None)
|
||||
{
|
||||
var triggerChannelBuffer = config.TriggerChannel switch
|
||||
var triggerChannelBuffer = processingConfig.TriggerChannel switch
|
||||
{
|
||||
TriggerChannel.One => postShuffleCh1_2,
|
||||
TriggerChannel.Two => postShuffleCh2_2,
|
||||
|
@ -144,11 +210,11 @@ namespace TS.NET.Engine
|
|||
trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
|
||||
}
|
||||
break;
|
||||
case Channels.Four:
|
||||
case AdcChannels.Four:
|
||||
// Shuffle
|
||||
Shuffle.FourChannels(input: memory.Span, output: shuffleBuffer);
|
||||
Shuffle.FourChannels(input: processingDto.Memory.Span, output: shuffleBuffer);
|
||||
// Finished with the memory, return it
|
||||
memoryPool.Write(memory);
|
||||
inputChannel.Write(processingDto.Memory);
|
||||
// Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
|
||||
//if (config.HorizontalSumLength != HorizontalSumLength.None)
|
||||
// throw new NotImplementedException();
|
||||
|
@ -158,9 +224,9 @@ namespace TS.NET.Engine
|
|||
circularBuffer3.Write(postShuffleCh3_4);
|
||||
circularBuffer4.Write(postShuffleCh4_4);
|
||||
// Trigger
|
||||
if (config.TriggerChannel != TriggerChannel.None)
|
||||
if (triggerRunning && processingConfig.TriggerChannel != TriggerChannel.None)
|
||||
{
|
||||
var triggerChannelBuffer = config.TriggerChannel switch
|
||||
var triggerChannelBuffer = processingConfig.TriggerChannel switch
|
||||
{
|
||||
TriggerChannel.One => postShuffleCh1_4,
|
||||
TriggerChannel.Two => postShuffleCh2_4,
|
||||
|
@ -169,47 +235,60 @@ namespace TS.NET.Engine
|
|||
_ => throw new ArgumentException("Invalid TriggerChannel value")
|
||||
};
|
||||
trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
|
||||
monitoring.TotalAcquisitions += holdoffEndCount;
|
||||
oneSecondHoldoffCount += holdoffEndCount;
|
||||
if (holdoffEndCount > 0)
|
||||
{
|
||||
// logger.LogDebug("Trigger Fired");
|
||||
for (int i = 0; i < holdoffEndCount; i++)
|
||||
{
|
||||
if (bridge.IsReadyToWrite)
|
||||
{
|
||||
bridge.Monitoring = monitoring;
|
||||
var bridgeSpan = bridge.Span;
|
||||
uint holdoffEndIndex = (uint)postShuffleCh1_4.Length - holdoffEndIndices[i];
|
||||
circularBuffer1.Read(bridgeSpan.Slice(0, channelLength), holdoffEndIndex);
|
||||
circularBuffer2.Read(bridgeSpan.Slice(channelLength, channelLength), holdoffEndIndex);
|
||||
circularBuffer3.Read(bridgeSpan.Slice(channelLength + channelLength, channelLength), holdoffEndIndex);
|
||||
circularBuffer4.Read(bridgeSpan.Slice(channelLength + channelLength + channelLength, channelLength), holdoffEndIndex);
|
||||
bridge.DataWritten();
|
||||
bridgeWriterSemaphore.Release(); // Signal to the reader that data is available
|
||||
}
|
||||
else
|
||||
{
|
||||
monitoring.MissedAcquisitions++;
|
||||
}
|
||||
var bridgeSpan = bridge.AcquiringRegion;
|
||||
uint holdoffEndIndex = (uint)postShuffleCh1_4.Length - holdoffEndIndices[i];
|
||||
circularBuffer1.Read(bridgeSpan.Slice(0, channelLength), holdoffEndIndex);
|
||||
circularBuffer2.Read(bridgeSpan.Slice(channelLength, channelLength), holdoffEndIndex);
|
||||
circularBuffer3.Read(bridgeSpan.Slice(channelLength + channelLength, channelLength), holdoffEndIndex);
|
||||
circularBuffer4.Read(bridgeSpan.Slice(channelLength + channelLength + channelLength, channelLength), holdoffEndIndex);
|
||||
bridge.DataWritten();
|
||||
bridge.SwitchRegionIfNeeded();
|
||||
}
|
||||
forceTrigger = false; // Ignore the force trigger request, a normal trigger happened
|
||||
if (oneShotTrigger) triggerRunning = false;
|
||||
}
|
||||
else if (forceTrigger)
|
||||
{
|
||||
// logger.LogDebug("Force Trigger fired");
|
||||
var bridgeSpan = bridge.AcquiringRegion;
|
||||
circularBuffer1.Read(bridgeSpan.Slice(0, channelLength), 0);
|
||||
circularBuffer2.Read(bridgeSpan.Slice(channelLength, channelLength), 0);
|
||||
circularBuffer3.Read(bridgeSpan.Slice(channelLength + channelLength, channelLength), 0);
|
||||
circularBuffer4.Read(bridgeSpan.Slice(channelLength + channelLength + channelLength, channelLength), 0);
|
||||
bridge.DataWritten();
|
||||
bridge.SwitchRegionIfNeeded();
|
||||
forceTrigger = false;
|
||||
if (oneShotTrigger) triggerRunning = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
bridge.SwitchRegionIfNeeded();
|
||||
}
|
||||
|
||||
}
|
||||
//logger.LogInformation($"Dequeue #{dequeueCounter++}, Ch1 triggers: {triggerCount1}, Ch2 triggers: {triggerCount2}, Ch3 triggers: {triggerCount3}, Ch4 triggers: {triggerCount4} ");
|
||||
break;
|
||||
}
|
||||
|
||||
if (oneSecond.ElapsedMilliseconds >= 1000)
|
||||
if (oneSecond.ElapsedMilliseconds >= 10000)
|
||||
{
|
||||
logger.LogDebug($"Triggers/sec: {oneSecondHoldoffCount / (oneSecond.ElapsedMilliseconds * 0.001):F2}, dequeue count: {dequeueCounter}, trigger count: {monitoring.TotalAcquisitions}, UI displayed triggers: {monitoring.TotalAcquisitions - monitoring.MissedAcquisitions}, UI dropped triggers: {monitoring.MissedAcquisitions}");
|
||||
logger.LogDebug($"Outstanding frames: {processingChannel.PeekAvailable()}, dequeues/sec: {oneSecondDequeueCount / (oneSecond.ElapsedMilliseconds * 0.001):F2}, dequeue count: {dequeueCounter}");
|
||||
logger.LogDebug($"Triggers/sec: {oneSecondHoldoffCount / (oneSecond.ElapsedMilliseconds * 0.001):F2}, trigger count: {bridge.Monitoring.TotalAcquisitions}, UI displayed triggers: {bridge.Monitoring.TotalAcquisitions - bridge.Monitoring.MissedAcquisitions}, UI dropped triggers: {bridge.Monitoring.MissedAcquisitions}");
|
||||
oneSecond.Restart();
|
||||
oneSecondHoldoffCount = 0;
|
||||
oneSecondDequeueCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogDebug($"{nameof(ProcessingTask)} stopping");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -221,5 +300,15 @@ namespace TS.NET.Engine
|
|||
logger.LogDebug($"{nameof(ProcessingTask)} stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlushProcessingQueue(
|
||||
BlockingChannelReader<InputDataDto> processingChannel,
|
||||
BlockingChannelWriter<ThunderscopeMemory> inputChannel)
|
||||
{
|
||||
while (processingChannel.TryRead(out var m))
|
||||
{
|
||||
inputChannel.Write(m.Memory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,379 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
internal class SCPITask
|
||||
{
|
||||
private CancellationTokenSource? cancelTokenSource;
|
||||
private Task? taskLoop;
|
||||
private Socket listener;
|
||||
|
||||
public void Start(
|
||||
ILoggerFactory loggerFactory,
|
||||
BlockingChannelWriter<HardwareRequestDto> configRequestChannel,
|
||||
BlockingChannelReader<HardwareResponseDto> configResponseChannel,
|
||||
BlockingChannelWriter<ProcessingRequestDto> processingRequestChannel,
|
||||
BlockingChannelReader<ProcessingResponseDto> processingResponseChannel)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger("SCPITask");
|
||||
cancelTokenSource = new CancellationTokenSource();
|
||||
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 5025);
|
||||
listener = new Socket(IPAddress.Any.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
listener.LingerState = new LingerOption(true, 1);
|
||||
listener.Bind(localEndPoint);
|
||||
taskLoop = Task.Factory.StartNew(() => Loop(logger, listener, configRequestChannel, configResponseChannel, processingRequestChannel, processingResponseChannel, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
cancelTokenSource?.Cancel();
|
||||
listener.Close();
|
||||
taskLoop?.Wait();
|
||||
}
|
||||
|
||||
private static void Loop(
|
||||
ILogger logger,
|
||||
Socket listener,
|
||||
BlockingChannelWriter<HardwareRequestDto> configRequestChannel,
|
||||
BlockingChannelReader<HardwareResponseDto> configResponseChannel,
|
||||
BlockingChannelWriter<ProcessingRequestDto> processingRequestChannel,
|
||||
BlockingChannelReader<ProcessingResponseDto> processingResponseChannel,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
Thread.CurrentThread.Name = "TS.NET SCPI";
|
||||
Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
|
||||
|
||||
logger.LogDebug($"Thread ID: {Thread.CurrentThread.ManagedThreadId}");
|
||||
Socket clientSocket = null;
|
||||
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Starting control plane socket server at :5025");
|
||||
listener.Listen(10);
|
||||
clientSocket = listener.Accept();
|
||||
clientSocket.NoDelay = true;
|
||||
logger.LogInformation("Client connected to control plane");
|
||||
uint seqnum = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
byte[] bytes = new byte[1];
|
||||
string command = "";
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!clientSocket.Poll(10_000, SelectMode.SelectRead)) continue;
|
||||
|
||||
int numByte = clientSocket.Receive(bytes);
|
||||
|
||||
if (numByte == 0) continue;
|
||||
|
||||
string c = Encoding.UTF8.GetString(bytes, 0, 1);
|
||||
|
||||
if (c == "\n") break;
|
||||
else command += c;
|
||||
}
|
||||
|
||||
// logger.LogDebug("SCPI command: '{String}'", command);
|
||||
|
||||
string? r = ProcessSCPICommand(logger, configRequestChannel, configResponseChannel, processingRequestChannel, processingResponseChannel, command, cancelToken);
|
||||
|
||||
if (r != null)
|
||||
{
|
||||
logger.LogDebug(" -> SCPI reply: '{String}'", r);
|
||||
clientSocket.Send(Encoding.UTF8.GetBytes(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogDebug($"{nameof(SCPITask)} stopping");
|
||||
// throw;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
if (!ex.Message.Contains("WSACancelBlockingCall")) // On Windows; can use this string to ignore the SocketException thrown when listener.Close() called
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogCritical(ex, $"{nameof(SCPITask)} error");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
clientSocket?.Shutdown(SocketShutdown.Both);
|
||||
clientSocket?.Close();
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
logger.LogDebug($"{nameof(SCPITask)} stopped");
|
||||
}
|
||||
}
|
||||
|
||||
public static string? ProcessSCPICommand(
|
||||
ILogger logger,
|
||||
BlockingChannelWriter<HardwareRequestDto> hardwareRequestChannel,
|
||||
BlockingChannelReader<HardwareResponseDto> hardwareResponseChannel,
|
||||
BlockingChannelWriter<ProcessingRequestDto> processingRequestChannel,
|
||||
BlockingChannelReader<ProcessingResponseDto> processingResponseChannel,
|
||||
string fullCommand,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
string? argument = null;
|
||||
string? subject = null;
|
||||
string command = fullCommand; ;
|
||||
bool isQuery = false;
|
||||
|
||||
if (fullCommand.Contains(" "))
|
||||
{
|
||||
int index = fullCommand.IndexOf(" ");
|
||||
argument = fullCommand.Substring(index + 1);
|
||||
command = fullCommand.Substring(0, index);
|
||||
}
|
||||
else if (command.Contains("?"))
|
||||
{
|
||||
isQuery = true;
|
||||
command = fullCommand.Substring(0, fullCommand.Length - 1);
|
||||
}
|
||||
|
||||
if (command.StartsWith(":"))
|
||||
{
|
||||
command = command.Substring(1);
|
||||
}
|
||||
|
||||
if (command.Contains(":"))
|
||||
{
|
||||
int index = command.IndexOf(":");
|
||||
subject = command.Substring(0, index);
|
||||
command = command.Substring(index + 1);
|
||||
}
|
||||
|
||||
bool hasArg = argument != null;
|
||||
|
||||
// logger.LogDebug("o:'{String}', q:{bool}, s:'{String?}', c:'{String?}', a:'{String?}'", fullCommand, isQuery, subject, command, argument);
|
||||
|
||||
if (!isQuery)
|
||||
{
|
||||
if (subject == null)
|
||||
{
|
||||
if (command == "START")
|
||||
{
|
||||
// Start
|
||||
logger.LogDebug("Start acquisition");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingStartTriggerDto(false, false));
|
||||
// hardwareResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "STOP")
|
||||
{
|
||||
// Stop
|
||||
logger.LogDebug("Stop acquisition");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingStopTriggerDto());
|
||||
// hardwareResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "SINGLE")
|
||||
{
|
||||
// Single capture
|
||||
logger.LogDebug("Single acquisition");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingStartTriggerDto(false, true));
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "FORCE")
|
||||
{
|
||||
// force capture
|
||||
logger.LogDebug("Force acquisition");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingStartTriggerDto(true, true));
|
||||
// processingResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "DEPTH" && hasArg)
|
||||
{
|
||||
long depth = Convert.ToInt64(argument);
|
||||
// Set depth
|
||||
logger.LogDebug($"Set depth to {depth}S");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingSetDepthDto(depth));
|
||||
// processingResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "RATE" && hasArg)
|
||||
{
|
||||
long rate = Convert.ToInt64(argument);
|
||||
// Set rate
|
||||
logger.LogDebug($"Set rate to {rate}Hz");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingSetRateDto(rate));
|
||||
// processingResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (subject == "TRIG")
|
||||
{
|
||||
if (command == "LEV" && hasArg)
|
||||
{
|
||||
double level = Convert.ToDouble(argument);
|
||||
// Set trig level
|
||||
logger.LogDebug($"Set trigger level to {level}V");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingSetTriggerLevelDto(level));
|
||||
// processingResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "SOU" && hasArg)
|
||||
{
|
||||
int source = Convert.ToInt32(argument);
|
||||
|
||||
if (source < 0 || source > 3)
|
||||
source = 0;
|
||||
|
||||
// Set trig channel
|
||||
logger.LogDebug($"Set trigger source to ch {source}");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingSetTriggerSourceDto((TriggerChannel)(source+1)));
|
||||
// processingResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "DELAY" && hasArg)
|
||||
{
|
||||
long delay = Convert.ToInt64(argument);
|
||||
// Set trig delay
|
||||
logger.LogDebug($"Set trigger delay to {delay}fs");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingSetTriggerDelayDto(delay));
|
||||
// processingResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "EDGE:DIR" && hasArg)
|
||||
{
|
||||
String dir = argument;
|
||||
// Set direction
|
||||
logger.LogDebug($"Set [edge] trigger direction to {dir}");
|
||||
|
||||
processingRequestChannel.Write(new ProcessingSetTriggerEdgeDirectionDto(/*dir*/));
|
||||
// processingResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (subject.Length == 1 && Char.IsDigit(subject[0]))
|
||||
{
|
||||
int chNum = subject[0] - '0';
|
||||
|
||||
if (command == "ON" || command == "OFF")
|
||||
{
|
||||
// Turn on/off
|
||||
logger.LogDebug($"Set ch {chNum} enabled {command=="ON"}");
|
||||
|
||||
hardwareRequestChannel.Write(new HardwareSetEnabledRequest(chNum, command=="ON"));
|
||||
// hardwareResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "COUP" && hasArg)
|
||||
{
|
||||
String coup = argument;
|
||||
// Set coupling
|
||||
logger.LogDebug($"Set ch {chNum} coupling to {coup}");
|
||||
|
||||
hardwareRequestChannel.Write(new HardwareSetCouplingRequest(chNum, (coup=="DC1M"?ThunderscopeCoupling.DC:ThunderscopeCoupling.AC)));
|
||||
// hardwareResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "OFFS" && hasArg)
|
||||
{
|
||||
double offset = Convert.ToDouble(argument);
|
||||
// Set offset
|
||||
logger.LogDebug($"Set ch {chNum} offset to {offset}V");
|
||||
|
||||
offset = Math.Clamp(offset, -0.5, 0.5);
|
||||
|
||||
hardwareRequestChannel.Write(new HardwareSetOffsetRequest(chNum, offset));
|
||||
// hardwareResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
else if (command == "RANGE" && hasArg)
|
||||
{
|
||||
double range = Convert.ToDouble(argument);
|
||||
// Set range
|
||||
|
||||
int[] available_mv = { 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000 };
|
||||
|
||||
int range_mv = (int)((range * 1000d) / 10d);
|
||||
int computedRange = available_mv[0];
|
||||
|
||||
for (int i = available_mv.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (available_mv[i] > computedRange && available_mv[i] <= range_mv)
|
||||
{
|
||||
computedRange = available_mv[i];
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug($"Set ch {chNum} range to {range}V -> {range_mv}mV -> {computedRange} computed mV");
|
||||
|
||||
hardwareRequestChannel.Write(new HardwareSetVdivRequest(chNum, computedRange));
|
||||
// hardwareResponseChannel.Read(cancelToken); // Maybe need some kind of UID to know this is the correct response? Bodge for now.
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (subject == null)
|
||||
{
|
||||
if (command == "*IDN")
|
||||
{
|
||||
logger.LogDebug("Reply to *IDN? query");
|
||||
return "ThunderScope,(Bridge),NOSERIAL,NOVERSION\n";
|
||||
}
|
||||
else if (command == "DEPTHS")
|
||||
{
|
||||
logger.LogDebug("Reply to DEPTHS? query");
|
||||
var s = "";
|
||||
for (int mul = 1000; mul <= 10 * 1000000; mul *= 10) // TODO: Single-source-of-truth for top end
|
||||
{
|
||||
s += mul + "," + (mul*2.5) + "," + (mul*5) + ",";
|
||||
}
|
||||
return s + "\n";
|
||||
}
|
||||
else if (command == "RATES")
|
||||
{
|
||||
logger.LogDebug("Reply to RATES? query");
|
||||
return "" + (1000000 * 4) + ",\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogWarning("Unknown SCPI Operation: {String}", fullCommand);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace TS.NET.Engine
|
||||
{
|
||||
internal class SocketTask
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
internal struct WaveformHeader
|
||||
{
|
||||
internal uint seqnum;
|
||||
internal ushort numChannels;
|
||||
internal ulong fsPerSample;
|
||||
internal ulong triggerFs;
|
||||
internal double hwWaveformsPerSec;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
internal struct ChannelHeader
|
||||
{
|
||||
internal byte chNum;
|
||||
internal ulong depth;
|
||||
internal float scale;
|
||||
internal float offset;
|
||||
internal float trigphase;
|
||||
internal byte clipping;
|
||||
}
|
||||
|
||||
private CancellationTokenSource? cancelTokenSource;
|
||||
private Task? taskLoop;
|
||||
private Socket listener;
|
||||
|
||||
public void Start(ILoggerFactory loggerFactory, BlockingChannelWriter<ProcessingRequestDto> processingRequestChannel)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger("SocketTask");
|
||||
cancelTokenSource = new CancellationTokenSource();
|
||||
ulong dataCapacityBytes = 4 * 100 * 1000 * 1000; // Maximum capacity = 100M samples per channel
|
||||
ThunderscopeBridgeReader bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", dataCapacityBytes), loggerFactory);
|
||||
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 5026);
|
||||
listener = new Socket(IPAddress.Any.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
listener.LingerState = new LingerOption(true, 1);
|
||||
listener.Bind(localEndPoint);
|
||||
taskLoop = Task.Factory.StartNew(() => Loop(logger, bridge, listener, processingRequestChannel, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
cancelTokenSource?.Cancel();
|
||||
listener.Close();
|
||||
taskLoop?.Wait();
|
||||
}
|
||||
|
||||
private static void Loop(
|
||||
ILogger logger,
|
||||
ThunderscopeBridgeReader bridge,
|
||||
Socket listener,
|
||||
BlockingChannelWriter<ProcessingRequestDto> processingRequestChannel,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
Thread.CurrentThread.Name = "TS.NET Socket";
|
||||
logger.LogDebug($"Thread ID: {Thread.CurrentThread.ManagedThreadId}");
|
||||
Socket clientSocket = null;
|
||||
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Starting data plane socket server at :5026");
|
||||
listener.Listen(10);
|
||||
clientSocket = listener.Accept();
|
||||
clientSocket.NoDelay = true;
|
||||
logger.LogInformation("Client connected to data plane");
|
||||
|
||||
uint seqnum = 0;
|
||||
|
||||
clientSocket.NoDelay = true;
|
||||
|
||||
while (true)
|
||||
{
|
||||
byte[] bytes = new byte[1];
|
||||
|
||||
// Wait for flow control 'K'
|
||||
while (true)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
if (!clientSocket.Poll(10_000, SelectMode.SelectRead)) continue;
|
||||
int numByte = clientSocket.Receive(bytes);
|
||||
if (numByte != 0) break;
|
||||
}
|
||||
|
||||
// logger.LogDebug("Got request for waveform...");
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (bridge.RequestAndWaitForData(500))
|
||||
{
|
||||
// logger.LogDebug("Send waveform...");
|
||||
var cfg = bridge.Configuration;
|
||||
var data = bridge.AcquiredRegion;
|
||||
|
||||
var processingCfg = bridge.Processing;//.GetConfiguration();
|
||||
ulong channelLength = (ulong)processingCfg.ChannelLength;
|
||||
|
||||
WaveformHeader header = new()
|
||||
{
|
||||
seqnum = seqnum,
|
||||
numChannels = 4,
|
||||
fsPerSample = 1000000 * 4, // 1GS / 4 channels (?)
|
||||
triggerFs = 0,
|
||||
hwWaveformsPerSec = 1
|
||||
};
|
||||
|
||||
ChannelHeader chHeader = new()
|
||||
{
|
||||
chNum = 0,
|
||||
depth = channelLength,
|
||||
scale = 1,
|
||||
offset = 0,
|
||||
trigphase = 0,
|
||||
clipping = 0
|
||||
};
|
||||
|
||||
unsafe
|
||||
{
|
||||
clientSocket.Send(new ReadOnlySpan<byte>(&header, sizeof(WaveformHeader)));
|
||||
|
||||
for (byte ch = 0; ch < 4; ch++)
|
||||
{
|
||||
ThunderscopeChannel tChannel = cfg.GetChannel(ch);
|
||||
|
||||
float full_scale = ((float)tChannel.VoltsDiv / 1000f) * 5f; // 5 instead of 10 for signed
|
||||
|
||||
chHeader.chNum = ch;
|
||||
chHeader.scale = full_scale / 127f; // 127 instead of 255 for signed
|
||||
chHeader.offset = -((float)tChannel.VoltsOffset); // needs chHeader.scale * 0x80 for signed
|
||||
|
||||
// TODO: What is up with samples in the 245-255 range that seem to be spurious or maybe a representation of negative voltages?
|
||||
|
||||
// if (ch == 0)
|
||||
// logger.LogDebug($"ch {ch}: VoltsDiv={tChannel.VoltsDiv} -> .scale={chHeader.scale}, VoltsOffset={tChannel.VoltsOffset} -> .offset = {chHeader.offset}, Coupling={tChannel.Coupling}");
|
||||
|
||||
// Length of this channel as 'depth'
|
||||
clientSocket.Send(new ReadOnlySpan<byte>(&chHeader, sizeof(ChannelHeader)));
|
||||
clientSocket.Send(data.Slice(ch * (int)channelLength, (int)channelLength));
|
||||
}
|
||||
}
|
||||
|
||||
seqnum++;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (false)
|
||||
{
|
||||
logger.LogDebug("Remote wanted waveform but not ready -- forcing trigger");
|
||||
processingRequestChannel.Write(new ProcessingStartTriggerDto(true, true));
|
||||
// TODO: This doesn't seem like the behavior we want, unless in "AUTO" triggering mode.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogDebug($"{nameof(SocketTask)} stopping");
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
if (!ex.Message.Contains("WSACancelBlockingCall")) // On Windows; can use this string to ignore the SocketException thrown when listener.Close() called
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogCritical(ex, $"{nameof(SocketTask)} error");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
clientSocket?.Shutdown(SocketShutdown.Both);
|
||||
clientSocket?.Close();
|
||||
}
|
||||
catch (Exception ex) { }
|
||||
|
||||
logger.LogDebug($"{nameof(SocketTask)} stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -89,7 +89,6 @@ namespace TS.NET.UI.Avalonia
|
|||
{
|
||||
uint bufferLength = 4 * 100 * 1000 * 1000; //Maximum record length = 100M samples per channel
|
||||
ThunderscopeBridgeReader bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", bufferLength), loggerFactory);
|
||||
var bridgeReadSemaphore = bridge.GetReaderSemaphore();
|
||||
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
|
||||
|
@ -97,9 +96,9 @@ namespace TS.NET.UI.Avalonia
|
|||
while (true)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
if (bridgeReadSemaphore.Wait(500))
|
||||
if (bridge.RequestAndWaitForData(500))
|
||||
{
|
||||
ulong channelLength = (ulong)bridge.Configuration.ChannelLength;
|
||||
ulong channelLength = (ulong)bridge.Processing.ChannelLength;
|
||||
//uint viewportLength = (uint)bridge.Configuration.ChannelLength;//1000;
|
||||
uint viewportLength = 1000000;// (uint)upDownIndex.Value;
|
||||
if (viewportLength < 100)
|
||||
|
@ -130,13 +129,12 @@ namespace TS.NET.UI.Avalonia
|
|||
|
||||
var cfg = bridge.Configuration;
|
||||
var status = $"[Horizontal] Displaying {AddPrefix(viewportLength)} samples of {AddPrefix(channelLength)} [Acquisitions] displayed: {bridge.Monitoring.TotalAcquisitions - bridge.Monitoring.MissedAcquisitions}, missed: {bridge.Monitoring.MissedAcquisitions}, total: {bridge.Monitoring.TotalAcquisitions}";
|
||||
var data = bridge.Span;
|
||||
var data = bridge.AcquiredRegion;
|
||||
int offset = (int)((channelLength / 2) - (viewportLength / 2));
|
||||
data.Slice(offset, (int)viewportLength).ToDoubleArray(channel1); offset += (int)channelLength;
|
||||
data.Slice(offset, (int)viewportLength).ToDoubleArray(channel2); offset += (int)channelLength;
|
||||
data.Slice(offset, (int)viewportLength).ToDoubleArray(channel3); offset += (int)channelLength;
|
||||
data.Slice(offset, (int)viewportLength).ToDoubleArray(channel4);
|
||||
bridge.DataRead();
|
||||
|
||||
//var reading = bridge.Span[(int)upDownIndex.Value];
|
||||
count++;
|
||||
|
|
|
@ -1,51 +1,16 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TS.NET.Interop;
|
||||
|
||||
namespace TS.NET
|
||||
{
|
||||
public record ThunderscopeDevice
|
||||
{
|
||||
public string DevicePath { get; set; }
|
||||
}
|
||||
|
||||
public record ThunderscopeChannel
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public double VoltsOffset { get; set; } = 0;
|
||||
public int VoltsDiv { get; set; } = 100;
|
||||
public int Bandwidth { get; set; } = 350;
|
||||
public ThunderscopeCoupling Coupling { get; set; }
|
||||
}
|
||||
|
||||
public enum ThunderscopeCoupling
|
||||
{
|
||||
DC,
|
||||
AC
|
||||
}
|
||||
public record ThunderscopeDevice(string DevicePath);
|
||||
|
||||
public class Thunderscope
|
||||
{
|
||||
private static Guid deviceGuid = new(0x74c7e4a9, 0x6d5d, 0x4a70, 0xbc, 0x0d, 0x20, 0x69, 0x1d, 0xff, 0x9e, 0x9d);
|
||||
private static IntPtr NULL = IntPtr.Zero;
|
||||
private const int INVALID_HANDLE_VALUE = -1;
|
||||
private const int ERROR_INSUFFICIENT_BUFFER = 122;
|
||||
private const int ERROR_NO_MORE_ITEMS = 259;
|
||||
private const int FILE_BEGIN = 0;
|
||||
private const string USER_DEVICE_PATH = "user";
|
||||
private const string C2H_0_DEVICE_PATH = "c2h_0";
|
||||
|
||||
//private MemoryMappedFile userFile;
|
||||
//private MemoryMappedViewAccessor userMap;
|
||||
//private BinaryWriter controllerToHostWriter;
|
||||
private IntPtr userFilePointer;
|
||||
private IntPtr controllerToHostFilePointer;
|
||||
private ThunderscopeInterop interop;
|
||||
|
||||
private bool open = false;
|
||||
private ThunderscopeHardwareState hardwareState = new();
|
||||
|
@ -54,48 +19,16 @@ namespace TS.NET
|
|||
|
||||
public static List<ThunderscopeDevice> IterateDevices()
|
||||
{
|
||||
List<ThunderscopeDevice> devices = new();
|
||||
|
||||
var deviceInfo = Interop.SetupDiGetClassDevs(ref deviceGuid, NULL, NULL, DiGetClassFlags.DIGCF_PRESENT | DiGetClassFlags.DIGCF_DEVICEINTERFACE);
|
||||
if ((IntPtr.Size == 4 && deviceInfo.ToInt32() == INVALID_HANDLE_VALUE) || (IntPtr.Size == 8 && deviceInfo.ToInt64() == INVALID_HANDLE_VALUE))
|
||||
throw new Exception("SetupDiGetClassDevs - failed with INVALID_HANDLE_VALUE");
|
||||
|
||||
SP_DEVICE_INTERFACE_DATA deviceInterface = new();
|
||||
unsafe
|
||||
{
|
||||
deviceInterface.CbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
|
||||
for (uint i = 0; Interop.SetupDiEnumDeviceInterfaces(deviceInfo, NULL, ref deviceGuid, i, ref deviceInterface); ++i) //Marshal.GetLastWin32Error() == ERROR_NO_MORE_ITEMS
|
||||
{
|
||||
uint detailLength = 0;
|
||||
if (!Interop.SetupDiGetDeviceInterfaceDetail(deviceInfo, ref deviceInterface, NULL, 0, ref detailLength, NULL) && Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER)
|
||||
throw new Exception("SetupDiGetDeviceInterfaceDetail - failed getting length with ERROR_INSUFFICIENT_BUFFER");
|
||||
if (detailLength > 255)
|
||||
throw new Exception("SetupDiGetDeviceInterfaceDetail - failed getting length by returning a length greater than 255 which won't fit into fixed length string field");
|
||||
|
||||
SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetail = new();
|
||||
deviceInterfaceDetail.CbSize = IntPtr.Size == 8 ? 8 : 6; // 6 bytes for x86, 8 bytes for x64
|
||||
// Could use Marshal.AllocHGlobal and Marshal.FreeHGlobal, inside Try/Finally, but might as well use the Marshalling syntax sugar
|
||||
if (!Interop.SetupDiGetDeviceInterfaceDetail(deviceInfo, ref deviceInterface, ref deviceInterfaceDetail, detailLength, NULL, NULL))
|
||||
throw new Exception("SetupDiGetDeviceInterfaceDetail - failed");
|
||||
|
||||
devices.Add(new ThunderscopeDevice() { DevicePath = deviceInterfaceDetail.DevicePath });
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
return ThunderscopeInterop.IterateDevices();
|
||||
}
|
||||
|
||||
public void Open(ThunderscopeDevice device)
|
||||
{
|
||||
if (open)
|
||||
Close();
|
||||
//File.OpenHandle(device.DevicePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, FileOptions.None);
|
||||
//userWriter = new BinaryWriter(File.Open($"{device.DevicePath}\\{USER_DEVICE_PATH}", FileMode.Open));
|
||||
//controllerToHostWriter = new BinaryWriter(File.Open($"{device.DevicePath}\\{C2H_0_DEVICE_PATH}", FileMode.Open));
|
||||
//userFile = MemoryMappedFile.CreateFromFile($"{device.DevicePath}\\{USER_DEVICE_PATH}", FileMode.Open);
|
||||
//userMap = userFile.CreateViewAccessor(0, 0);
|
||||
userFilePointer = Interop.CreateFile($"{device.DevicePath}\\{USER_DEVICE_PATH}", FileAccess.ReadWrite, FileShare.None, NULL, FileMode.Open, FileAttributes.Normal, NULL);
|
||||
controllerToHostFilePointer = Interop.CreateFile($"{device.DevicePath}\\{C2H_0_DEVICE_PATH}", FileAccess.ReadWrite, FileShare.None, NULL, FileMode.Open, FileAttributes.Normal, NULL);
|
||||
|
||||
interop = ThunderscopeInterop.CreateInterop(device);
|
||||
|
||||
Initialise();
|
||||
open = true;
|
||||
}
|
||||
|
@ -133,12 +66,12 @@ namespace TS.NET
|
|||
ConfigureDatamover(hardwareState);
|
||||
}
|
||||
|
||||
public void Read(ThunderscopeMemory data) //ThunderscopeMemoryBlock ensures memory is aligned on 4k boundary
|
||||
public void Read(ThunderscopeMemory data) //ThunderscopeMemory ensures memory is aligned on 4k boundary
|
||||
{
|
||||
if (!open)
|
||||
throw new Exception("Thunderscope not open");
|
||||
if (!hardwareState.DatamoverEnabled)
|
||||
throw new Exception("Thunderscope not started");
|
||||
throw new ThunderscopeNotRunningException("Thunderscope not started");
|
||||
|
||||
// Buffer data must be aligned to 4096
|
||||
//if (0xFFF & (ptrdiff_t)data)
|
||||
|
@ -171,7 +104,7 @@ namespace TS.NET
|
|||
if (pages_to_read > hardwareState.RamSizePages - buffer_read_pos) pages_to_read = hardwareState.RamSizePages - buffer_read_pos;
|
||||
if (pages_to_read > hardwareState.RamSizePages / 4) pages_to_read = hardwareState.RamSizePages / 4;
|
||||
|
||||
Read(controllerToHostFilePointer, data, dataIndex, buffer_read_pos << 12, pages_to_read << 12);
|
||||
interop.ReadC2H(data, dataIndex, buffer_read_pos << 12, pages_to_read << 12);
|
||||
//read_handle(ts, ts->c2h0_handle, dataPtr, buffer_read_pos << 12, pages_to_read << 12);
|
||||
|
||||
dataIndex += pages_to_read << 12;
|
||||
|
@ -198,7 +131,7 @@ namespace TS.NET
|
|||
private uint Read32(BarRegister register)
|
||||
{
|
||||
Span<byte> bytes = new byte[4];
|
||||
Read(userFilePointer, bytes, (ulong)register);
|
||||
interop.ReadUser(bytes, (ulong)register);
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
}
|
||||
|
||||
|
@ -206,7 +139,7 @@ namespace TS.NET
|
|||
{
|
||||
Span<byte> bytes = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
Write(userFilePointer, bytes, (ulong)register);
|
||||
interop.WriteUser(bytes, (ulong)register);
|
||||
}
|
||||
|
||||
private void WriteFifo(ReadOnlySpan<byte> data)
|
||||
|
@ -224,7 +157,7 @@ namespace TS.NET
|
|||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
// TODO: Replace with write32
|
||||
Write(userFilePointer, data.Slice(i, 1), (ulong)BarRegister.SERIAL_FIFO_DATA_WRITE_REG);
|
||||
interop.WriteUser(data.Slice(i, 1), (ulong)BarRegister.SERIAL_FIFO_DATA_WRITE_REG);
|
||||
}
|
||||
// read TDFV (vacancy byte)
|
||||
Read32(BarRegister.SERIAL_FIFO_TDFV_ADDRESS);
|
||||
|
@ -363,6 +296,13 @@ namespace TS.NET
|
|||
ConfigureChannel(channel);
|
||||
}
|
||||
|
||||
public void ResetBuffer()
|
||||
{
|
||||
hardwareState.BufferHead = 0;
|
||||
hardwareState.BufferTail = 0;
|
||||
ConfigureDatamover(hardwareState);
|
||||
}
|
||||
|
||||
private void ConfigureChannel(int channel)
|
||||
{
|
||||
ConfigureChannels();
|
||||
|
@ -492,7 +432,7 @@ namespace TS.NET
|
|||
throw new Exception("Thunderscope - datamover error");
|
||||
|
||||
if ((error_code & 1) > 0)
|
||||
throw new Exception("Thunderscope - FIFO overflow");
|
||||
throw new ThunderscopeFIFOOverflowException("Thunderscope - FIFO overflow");
|
||||
|
||||
uint overflow_cycles = (transfer_counter >> 16) & 0x3FFF;
|
||||
if (overflow_cycles > 0)
|
||||
|
@ -507,56 +447,7 @@ namespace TS.NET
|
|||
|
||||
ulong pages_available = hardwareState.BufferHead - hardwareState.BufferTail;
|
||||
if (pages_available >= hardwareState.RamSizePages)
|
||||
throw new Exception("Thunderscope - memory full");
|
||||
}
|
||||
|
||||
private void Write(IntPtr fileHandle, ReadOnlySpan<byte> data, ulong addr)
|
||||
{
|
||||
if (!Interop.SetFilePointerEx(fileHandle, addr, NULL, FILE_BEGIN))
|
||||
throw new Exception($"SetFilePointerEx - failed ({Marshal.GetLastWin32Error()})");
|
||||
|
||||
// write from buffer to device
|
||||
//DWORD bytesWritten;
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
if (!Interop.WriteFile(fileHandle, dataPtr, (uint)data.Length, out uint bytesWritten, NULL))
|
||||
throw new Exception($"WriteFile - failed ({Marshal.GetLastWin32Error()})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private void Read(IntPtr fileHandle, Span<byte> data, ulong addr)
|
||||
{
|
||||
if (!Interop.SetFilePointerEx(fileHandle, addr, NULL, FILE_BEGIN))
|
||||
throw new Exception($"SetFilePointerEx - failed ({Marshal.GetLastWin32Error()})");
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
if (!Interop.ReadFile(fileHandle, dataPtr, (uint)data.Length, out uint bytesRead, NULL))
|
||||
throw new Exception($"ReadFile - failed ({Marshal.GetLastWin32Error()})");
|
||||
if (bytesRead != data.Length)
|
||||
throw new Exception("ReadFile - failed to read all bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private void Read(IntPtr fileHandle, ThunderscopeMemory data, ulong offset, ulong addr, ulong length)
|
||||
{
|
||||
if (!Interop.SetFilePointerEx(fileHandle, addr, NULL, FILE_BEGIN))
|
||||
throw new Exception($"SetFilePointerEx - failed ({Marshal.GetLastWin32Error()})");
|
||||
unsafe
|
||||
{
|
||||
if (!Interop.ReadFile(fileHandle, data.Pointer + offset, (uint)length, out uint bytesRead, NULL))
|
||||
throw new Exception($"ReadFile - failed ({Marshal.GetLastWin32Error()})");
|
||||
if (bytesRead != length)
|
||||
throw new Exception("ReadFile - failed to read all bytes");
|
||||
}
|
||||
throw new ThunderscopeMemoryOutOfMemoryException("Thunderscope - memory full");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
namespace TS.NET
|
||||
{
|
||||
public class ThunderscopeException : Exception
|
||||
{
|
||||
public ThunderscopeException(string v) : base(v) { }
|
||||
}
|
||||
|
||||
public class ThunderscopeNotRunningException : ThunderscopeException
|
||||
{
|
||||
public ThunderscopeNotRunningException(string v) : base(v) { }
|
||||
}
|
||||
|
||||
public class ThunderscopeRecoverableOverflowException : ThunderscopeException
|
||||
{
|
||||
public ThunderscopeRecoverableOverflowException(string v) : base(v) { }
|
||||
}
|
||||
|
||||
public class ThunderscopeMemoryOutOfMemoryException : ThunderscopeRecoverableOverflowException
|
||||
{
|
||||
public ThunderscopeMemoryOutOfMemoryException(string v) : base(v) { }
|
||||
}
|
||||
|
||||
public class ThunderscopeFIFOOverflowException : ThunderscopeRecoverableOverflowException
|
||||
{
|
||||
public ThunderscopeFIFOOverflowException(string v) : base(v) { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
// [assembly: DisableRuntimeMarshalling] // Coming in .NET 7 with a source analyzer. This will guarantee interop has zero performance penalty.
|
||||
namespace TS.NET.Interop.Linux
|
||||
{
|
||||
[Flags]
|
||||
internal enum OpenFlags : uint
|
||||
{
|
||||
O_RDONLY = 0,
|
||||
O_WRONLY = 1,
|
||||
O_RDWR = 2,
|
||||
}
|
||||
|
||||
internal static class Interop
|
||||
{
|
||||
[DllImport("libc.so.6", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
|
||||
public static extern Int32 open(
|
||||
[MarshalAs(UnmanagedType.LPStr)] string pathname,
|
||||
Int32 flags);
|
||||
|
||||
[DllImport("libc.so.6", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
|
||||
public static unsafe extern Int32 pread(
|
||||
Int32 fildes,
|
||||
byte* buf,
|
||||
Int32 nbyte,
|
||||
Int32 offset);
|
||||
|
||||
[DllImport("libc.so.6", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
|
||||
public static unsafe extern Int32 pwrite(
|
||||
Int32 fildes,
|
||||
byte* buf,
|
||||
Int32 nbyte,
|
||||
Int32 offset);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TS.NET.Interop;
|
||||
|
||||
namespace TS.NET.Interop.Linux
|
||||
{
|
||||
internal class ThunderscopeInteropLinux : ThunderscopeInterop
|
||||
{
|
||||
private const string USER_DEVICE_PATH = "user";
|
||||
private const string C2H_0_DEVICE_PATH = "c2h_0";
|
||||
|
||||
public static List<ThunderscopeDevice> PlatIterateDevices()
|
||||
{
|
||||
List<ThunderscopeDevice> devices = new();
|
||||
|
||||
devices.Add(new ThunderscopeDevice(DevicePath: "/dev/xdma0"));
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
private Int32 userFilePointer;
|
||||
private Int32 controllerToHostFilePointer;
|
||||
|
||||
public ThunderscopeInteropLinux(ThunderscopeDevice device)
|
||||
{
|
||||
userFilePointer = Interop.open($"{device.DevicePath}_{USER_DEVICE_PATH}", (Int32)OpenFlags.O_RDWR);
|
||||
|
||||
if (userFilePointer < 0)
|
||||
throw new Exception($"open '{device.DevicePath}_{USER_DEVICE_PATH}' failed -> ret={userFilePointer} / errno={Marshal.GetLastWin32Error()}");
|
||||
|
||||
controllerToHostFilePointer = Interop.open($"{device.DevicePath}_{C2H_0_DEVICE_PATH}", (Int32)OpenFlags.O_RDWR);
|
||||
|
||||
if (controllerToHostFilePointer < 0)
|
||||
throw new Exception($"open '{device.DevicePath}_{C2H_0_DEVICE_PATH}' failed -> ret={controllerToHostFilePointer} / errno={Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
|
||||
public override void WriteUser(ReadOnlySpan<byte> data, ulong addr)
|
||||
{
|
||||
// write from buffer to device
|
||||
//DWORD bytesWritten;
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
Int32 bytesWritten = Interop.pwrite(userFilePointer, dataPtr, (Int32)data.Length, (Int32)addr);
|
||||
|
||||
if (bytesWritten != data.Length)
|
||||
throw new Exception($"pwrite user - failed -> toWrite={data.Length}, written={bytesWritten}, errno={Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public override void ReadUser(Span<byte> data, ulong addr)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
Int32 bytesRead = Interop.pread(userFilePointer, dataPtr, (Int32)data.Length, (Int32)addr);
|
||||
|
||||
if (bytesRead != data.Length)
|
||||
throw new Exception($"pread user - failed -> toRead={data.Length}, read={bytesRead}, errno={Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public override void ReadC2H(ThunderscopeMemory data, ulong offset, ulong addr, ulong length)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
Int32 bytesRead = Interop.pread(controllerToHostFilePointer, data.Pointer + offset, (Int32)length, (Int32)addr);
|
||||
|
||||
if (bytesRead != (Int32)length)
|
||||
throw new Exception($"pread c2h - failed -> toRead={length}, read={bytesRead}, errno={Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TS.NET.Interop;
|
||||
using TS.NET.Interop.Windows;
|
||||
using TS.NET.Interop.Linux;
|
||||
|
||||
namespace TS.NET.Interop
|
||||
{
|
||||
public abstract class ThunderscopeInterop : IDisposable
|
||||
{
|
||||
public static List<ThunderscopeDevice> IterateDevices() {
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
|
||||
ThunderscopeInteropWindows.PlatIterateDevices() : ThunderscopeInteropLinux.PlatIterateDevices();
|
||||
}
|
||||
|
||||
public static ThunderscopeInterop CreateInterop(ThunderscopeDevice dev) {
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
|
||||
new ThunderscopeInteropWindows(dev) : new ThunderscopeInteropLinux(dev);
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public abstract void WriteUser(ReadOnlySpan<byte> data, ulong addr);
|
||||
|
||||
public abstract void ReadUser(Span<byte> data, ulong addr);
|
||||
|
||||
public abstract void ReadC2H(ThunderscopeMemory data, ulong offset, ulong addr, ulong length);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
// [assembly: DisableRuntimeMarshalling] // Coming in .NET 7 with a source analyzer. This will guarantee interop has zero performance penalty.
|
||||
namespace TS.NET
|
||||
namespace TS.NET.Interop.Windows
|
||||
{
|
||||
// https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices#blittable-types
|
||||
// CharSet = CharSet.Unicode helps ensure blitability
|
|
@ -0,0 +1,121 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TS.NET.Interop;
|
||||
|
||||
namespace TS.NET.Interop.Windows
|
||||
{
|
||||
internal class ThunderscopeInteropWindows : ThunderscopeInterop
|
||||
{
|
||||
private const string USER_DEVICE_PATH = "user";
|
||||
private const string C2H_0_DEVICE_PATH = "c2h_0";
|
||||
private const int INVALID_HANDLE_VALUE = -1;
|
||||
private const int ERROR_INSUFFICIENT_BUFFER = 122;
|
||||
private const int ERROR_NO_MORE_ITEMS = 259;
|
||||
private const int FILE_BEGIN = 0;
|
||||
private static Guid deviceGuid = new(0x74c7e4a9, 0x6d5d, 0x4a70, 0xbc, 0x0d, 0x20, 0x69, 0x1d, 0xff, 0x9e, 0x9d);
|
||||
private static IntPtr NULL = IntPtr.Zero;
|
||||
|
||||
public static List<ThunderscopeDevice> PlatIterateDevices()
|
||||
{
|
||||
List<ThunderscopeDevice> devices = new();
|
||||
|
||||
var deviceInfo = Interop.SetupDiGetClassDevs(ref deviceGuid, NULL, NULL, DiGetClassFlags.DIGCF_PRESENT | DiGetClassFlags.DIGCF_DEVICEINTERFACE);
|
||||
if ((IntPtr.Size == 4 && deviceInfo.ToInt32() == INVALID_HANDLE_VALUE) || (IntPtr.Size == 8 && deviceInfo.ToInt64() == INVALID_HANDLE_VALUE))
|
||||
throw new Exception("SetupDiGetClassDevs - failed with INVALID_HANDLE_VALUE");
|
||||
|
||||
SP_DEVICE_INTERFACE_DATA deviceInterface = new();
|
||||
unsafe
|
||||
{
|
||||
deviceInterface.CbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
|
||||
for (uint i = 0; Interop.SetupDiEnumDeviceInterfaces(deviceInfo, NULL, ref deviceGuid, i, ref deviceInterface); ++i) //Marshal.GetLastWin32Error() == ERROR_NO_MORE_ITEMS
|
||||
{
|
||||
uint detailLength = 0;
|
||||
if (!Interop.SetupDiGetDeviceInterfaceDetail(deviceInfo, ref deviceInterface, NULL, 0, ref detailLength, NULL) && Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER)
|
||||
throw new Exception("SetupDiGetDeviceInterfaceDetail - failed getting length with ERROR_INSUFFICIENT_BUFFER");
|
||||
if (detailLength > 255)
|
||||
throw new Exception("SetupDiGetDeviceInterfaceDetail - failed getting length by returning a length greater than 255 which won't fit into fixed length string field");
|
||||
|
||||
SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetail = new();
|
||||
deviceInterfaceDetail.CbSize = IntPtr.Size == 8 ? 8 : 6; // 6 bytes for x86, 8 bytes for x64
|
||||
// Could use Marshal.AllocHGlobal and Marshal.FreeHGlobal, inside Try/Finally, but might as well use the Marshalling syntax sugar
|
||||
if (!Interop.SetupDiGetDeviceInterfaceDetail(deviceInfo, ref deviceInterface, ref deviceInterfaceDetail, detailLength, NULL, NULL))
|
||||
throw new Exception("SetupDiGetDeviceInterfaceDetail - failed");
|
||||
|
||||
devices.Add(new ThunderscopeDevice(DevicePath: deviceInterfaceDetail.DevicePath));
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
private IntPtr userFilePointer;
|
||||
private IntPtr controllerToHostFilePointer;
|
||||
|
||||
public ThunderscopeInteropWindows(ThunderscopeDevice device)
|
||||
{
|
||||
userFilePointer = Interop.CreateFile($"{device.DevicePath}\\{USER_DEVICE_PATH}", FileAccess.ReadWrite, FileShare.None, NULL, FileMode.Open, FileAttributes.Normal, NULL);
|
||||
controllerToHostFilePointer = Interop.CreateFile($"{device.DevicePath}\\{C2H_0_DEVICE_PATH}", FileAccess.ReadWrite, FileShare.None, NULL, FileMode.Open, FileAttributes.Normal, NULL);
|
||||
}
|
||||
|
||||
public override void WriteUser(ReadOnlySpan<byte> data, ulong addr)
|
||||
{
|
||||
if (!Interop.SetFilePointerEx(userFilePointer, addr, NULL, FILE_BEGIN))
|
||||
throw new Exception($"SetFilePointerEx - failed ({Marshal.GetLastWin32Error()})");
|
||||
|
||||
// write from buffer to device
|
||||
//DWORD bytesWritten;
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
if (!Interop.WriteFile(userFilePointer, dataPtr, (uint)data.Length, out uint bytesWritten, NULL))
|
||||
throw new Exception($"WriteFile - failed ({Marshal.GetLastWin32Error()})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public override void ReadUser(Span<byte> data, ulong addr)
|
||||
{
|
||||
if (!Interop.SetFilePointerEx(userFilePointer, addr, NULL, FILE_BEGIN))
|
||||
throw new Exception($"SetFilePointerEx - failed ({Marshal.GetLastWin32Error()})");
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
if (!Interop.ReadFile(userFilePointer, dataPtr, (uint)data.Length, out uint bytesRead, NULL))
|
||||
throw new Exception($"ReadFile - failed ({Marshal.GetLastWin32Error()})");
|
||||
if (bytesRead != data.Length)
|
||||
throw new Exception("ReadFile - failed to read all bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public override void ReadC2H(ThunderscopeMemory data, ulong offset, ulong addr, ulong length)
|
||||
{
|
||||
if (!Interop.SetFilePointerEx(controllerToHostFilePointer, addr, NULL, FILE_BEGIN))
|
||||
throw new Exception($"SetFilePointerEx - failed ({Marshal.GetLastWin32Error()})");
|
||||
unsafe
|
||||
{
|
||||
if (!Interop.ReadFile(controllerToHostFilePointer, data.Pointer + offset, (uint)length, out uint bytesRead, NULL))
|
||||
throw new Exception($"ReadFile - failed ({Marshal.GetLastWin32Error()})");
|
||||
if (bytesRead != length)
|
||||
throw new Exception("ReadFile - failed to read all bytes");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,23 +5,68 @@ namespace TS.NET
|
|||
// Ensure this is blitable (i.e. don't use bool)
|
||||
// Pack of 1 = No padding.
|
||||
// There might be some benefit later to setting a fixed size (64?) for memory alignment if an aligned memorymappedfile can be created.
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
internal struct ThunderscopeBridgeHeader
|
||||
{
|
||||
// Version + DataCapacity is enough data for the UI to know how big a memorymappedfile to open
|
||||
internal byte Version; // Allows UI to know which ThunderscopeMemoryBridgeHeader version to use, hence the size of the header.
|
||||
internal ulong DataCapacityBytes; // Maximum size of the data array in bridge. Example: 400M, set from configuration file?
|
||||
|
||||
internal ThunderscopeMemoryBridgeState State;
|
||||
internal ThunderscopeConfiguration Configuration;
|
||||
internal ThunderscopeMonitoring Monitoring;
|
||||
internal ThunderscopeMemoryAcquiringRegion AcquiringRegion; // Therefore 'AcquiredRegion' (to be used by UI) is the opposite
|
||||
internal ThunderscopeConfiguration Configuration; // Read only from UI perspective, UI uses SCPI interface to change configuration
|
||||
internal ThunderscopeProcessing Processing; // Read only from UI perspective, UI displays these values
|
||||
internal ThunderscopeMonitoring Monitoring; // Read only from UI perspective, UI displays these values
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct ThunderscopeConfiguration
|
||||
public struct ThunderscopeConfiguration // Idempotent so that UI doesn't have to store state and removes the possibility of config mismatch with multiple actors changing config (e.g. SCPI and Web UI)
|
||||
{
|
||||
public Channels Channels;
|
||||
public int ChannelLength; // Example: 4 channels with max length = 100M, can easily be 1k for high update rate. Max: Capacity/4, Min: 1k.
|
||||
public AdcChannels AdcChannels; // The number of channels enabled on ADC. ADC has input mux, e.g. Channel1.Enabled and Channel4.Enabled could have AdcChannels of Two. Useful for UI to know this, in order to clamp maximum sample rate.
|
||||
//[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
|
||||
//public ThunderscopeChannel* Channels; // Commented out as requires unsafe context but maybe switch to it later?
|
||||
public ThunderscopeChannel Channel0;
|
||||
public ThunderscopeChannel Channel1;
|
||||
public ThunderscopeChannel Channel2;
|
||||
public ThunderscopeChannel Channel3;
|
||||
|
||||
public ThunderscopeChannel GetChannel(int channel)
|
||||
{
|
||||
return channel switch
|
||||
{
|
||||
0 => Channel0,
|
||||
1 => Channel1,
|
||||
2 => Channel2,
|
||||
3 => Channel3,
|
||||
_ => throw new ArgumentException("channel out of range")
|
||||
};
|
||||
}
|
||||
|
||||
public void SetChannel(int channel, ThunderscopeChannel ch)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case 0:
|
||||
Channel0 = ch;
|
||||
break;
|
||||
case 1:
|
||||
Channel1 = ch;
|
||||
break;
|
||||
case 2:
|
||||
Channel2 = ch;
|
||||
break;
|
||||
case 3:
|
||||
Channel3 = ch;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("channel out of range");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct ThunderscopeProcessing // Idempotent so that UI doesn't have to store state and removes the possibility of config mismatch with multiple actors changing config (e.g. SCPI and Web UI)
|
||||
{
|
||||
public int ChannelLength; // Example: 4 channels with max length = 100M, can easily be 1k for high update rate. Max: Capacity/4, Min: 1k.
|
||||
public HorizontalSumLength HorizontalSumLength;
|
||||
public TriggerChannel TriggerChannel;
|
||||
public TriggerMode TriggerMode;
|
||||
|
@ -31,14 +76,14 @@ namespace TS.NET
|
|||
// Monitoring variables that reset when configuration variables change
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct ThunderscopeMonitoring
|
||||
{
|
||||
{
|
||||
public ulong TotalAcquisitions; // All triggers
|
||||
public ulong MissedAcquisitions; // Triggers that weren't displayed
|
||||
}
|
||||
|
||||
public enum ThunderscopeMemoryBridgeState : byte
|
||||
public enum ThunderscopeMemoryAcquiringRegion : byte
|
||||
{
|
||||
Empty = 1, // Writing is allowed
|
||||
Full = 2, // Writing is blocked, waiting for reader to set back to Unset
|
||||
RegionA = 1,
|
||||
RegionB = 2
|
||||
}
|
||||
}
|
|
@ -21,8 +21,8 @@ namespace TS.NET
|
|||
|
||||
MemoryName = memoryName;
|
||||
Path = path;
|
||||
DataCapacityBytes = dataCapacityBytes;
|
||||
BridgeCapacityBytes = (ulong)sizeof(ThunderscopeBridgeHeader) + dataCapacityBytes;
|
||||
DataCapacityBytes = dataCapacityBytes * 2; // *2 as there are 2 regions used in tick-tock fashion
|
||||
BridgeCapacityBytes = (ulong)sizeof(ThunderscopeBridgeHeader) + DataCapacityBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,21 +12,23 @@ namespace TS.NET
|
|||
public class ThunderscopeBridgeReader : IDisposable
|
||||
{
|
||||
private readonly ThunderscopeBridgeOptions options;
|
||||
private readonly ulong dataBytesCapacity;
|
||||
private readonly ulong dataCapacityInBytes;
|
||||
private readonly IMemoryFile file;
|
||||
private readonly MemoryMappedViewAccessor view;
|
||||
private unsafe byte* basePointer;
|
||||
private unsafe byte* dataPointer { get; }
|
||||
private ThunderscopeBridgeHeader header;
|
||||
|
||||
public IntPtr DataPointer { get { unsafe { return (IntPtr)dataPointer; } } }
|
||||
public Span<byte> Span { get { unsafe { return new Span<byte>(dataPointer, (int)dataBytesCapacity); } } }
|
||||
private bool IsHeaderSet { get { GetHeader(); return header.Version != 0; } }
|
||||
private readonly IInterprocessSemaphoreReleaser dataRequestSemaphore;
|
||||
private readonly IInterprocessSemaphoreWaiter dataReadySemaphore;
|
||||
private bool hasSignaledRequest = false;
|
||||
|
||||
public ReadOnlySpan<byte> AcquiredRegion { get { return GetAcquiredRegion(); } }
|
||||
|
||||
public unsafe ThunderscopeBridgeReader(ThunderscopeBridgeOptions options, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this.options = options;
|
||||
dataBytesCapacity = options.BridgeCapacityBytes - (uint)sizeof(ThunderscopeBridgeHeader);
|
||||
dataCapacityInBytes = options.BridgeCapacityBytes - (uint)sizeof(ThunderscopeBridgeHeader);
|
||||
file = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? new MemoryFileWindows(options)
|
||||
: new MemoryFileUnix(options, loggerFactory);
|
||||
|
@ -37,7 +39,7 @@ namespace TS.NET
|
|||
|
||||
try
|
||||
{
|
||||
basePointer = AcquirePointer();
|
||||
basePointer = GetPointer();
|
||||
dataPointer = basePointer + sizeof(ThunderscopeBridgeHeader);
|
||||
|
||||
while (!IsHeaderSet)
|
||||
|
@ -48,6 +50,8 @@ namespace TS.NET
|
|||
GetHeader();
|
||||
if (header.DataCapacityBytes != options.DataCapacityBytes)
|
||||
throw new Exception($"Mismatch in data capacity, options: {options.DataCapacityBytes}, bridge: {header.DataCapacityBytes}");
|
||||
dataRequestSemaphore = InterprocessSemaphore.CreateReleaser(options.MemoryName + "DataRequest");
|
||||
dataReadySemaphore = InterprocessSemaphore.CreateWaiter(options.MemoryName + "DataReady");
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -70,11 +74,6 @@ namespace TS.NET
|
|||
file.Dispose();
|
||||
}
|
||||
|
||||
public IInterprocessSemaphoreWaiter GetReaderSemaphore()
|
||||
{
|
||||
return InterprocessSemaphore.CreateWaiter(options.MemoryName);
|
||||
}
|
||||
|
||||
public ThunderscopeConfiguration Configuration
|
||||
{
|
||||
get
|
||||
|
@ -84,6 +83,15 @@ namespace TS.NET
|
|||
}
|
||||
}
|
||||
|
||||
public ThunderscopeProcessing Processing
|
||||
{
|
||||
get
|
||||
{
|
||||
GetHeader();
|
||||
return header.Processing;
|
||||
}
|
||||
}
|
||||
|
||||
public ThunderscopeMonitoring Monitoring
|
||||
{
|
||||
get
|
||||
|
@ -93,22 +101,25 @@ namespace TS.NET
|
|||
}
|
||||
}
|
||||
|
||||
public bool IsReadyToRead
|
||||
public bool RequestAndWaitForData(int millisecondsTimeout)
|
||||
{
|
||||
get
|
||||
if (!hasSignaledRequest)
|
||||
{
|
||||
GetHeader();
|
||||
return header.State == ThunderscopeMemoryBridgeState.Full;
|
||||
// Only signal request once, or we will run up semaphore counter
|
||||
dataRequestSemaphore.Release();
|
||||
hasSignaledRequest = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void DataRead()
|
||||
{
|
||||
unsafe
|
||||
bool wasReady = dataReadySemaphore.Wait(millisecondsTimeout);
|
||||
|
||||
if (wasReady)
|
||||
{
|
||||
header.State = ThunderscopeMemoryBridgeState.Empty;
|
||||
SetHeader();
|
||||
// Now that the bridge has tick-tocked, the next request will be 'real'
|
||||
// TODO: Should this be a separate method, or part of GetPointer() ?
|
||||
hasSignaledRequest = false;
|
||||
}
|
||||
|
||||
return wasReady;
|
||||
}
|
||||
|
||||
private void GetHeader()
|
||||
|
@ -116,12 +127,12 @@ namespace TS.NET
|
|||
unsafe { Unsafe.Copy(ref header, basePointer); }
|
||||
}
|
||||
|
||||
private void SetHeader()
|
||||
{
|
||||
unsafe { Unsafe.Copy(basePointer, ref header); }
|
||||
}
|
||||
//private void SetHeader()
|
||||
//{
|
||||
// unsafe { Unsafe.Copy(basePointer, ref header); }
|
||||
//}
|
||||
|
||||
private unsafe byte* AcquirePointer()
|
||||
private unsafe byte* GetPointer()
|
||||
{
|
||||
byte* ptr = null;
|
||||
view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
|
||||
|
@ -130,5 +141,16 @@ namespace TS.NET
|
|||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
private unsafe ReadOnlySpan<byte> GetAcquiredRegion()
|
||||
{
|
||||
int regionLength = (int)dataCapacityInBytes / 2;
|
||||
return header.AcquiringRegion switch
|
||||
{
|
||||
ThunderscopeMemoryAcquiringRegion.RegionA => new ReadOnlySpan<byte>(dataPointer + regionLength, regionLength), // If acquiring region is Region A, return Region B
|
||||
ThunderscopeMemoryAcquiringRegion.RegionB => new ReadOnlySpan<byte>(dataPointer, regionLength), // If acquiring region is Region B, return Region A
|
||||
_ => throw new InvalidDataException("Enum value not handled, add enum value to switch")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,22 +11,28 @@ using System.Runtime.CompilerServices;
|
|||
namespace TS.NET
|
||||
{
|
||||
// This is a shared memory-mapped file between processes, with only a single writer and a single reader with a header struct
|
||||
// Not thread safe
|
||||
public class ThunderscopeBridgeWriter : IDisposable
|
||||
{
|
||||
private readonly ThunderscopeBridgeOptions options;
|
||||
private readonly ulong dataCapacityBytes;
|
||||
private readonly ulong dataCapacityInBytes;
|
||||
private readonly IMemoryFile file;
|
||||
private readonly MemoryMappedViewAccessor view;
|
||||
private unsafe byte* basePointer;
|
||||
private unsafe byte* dataPointer { get; }
|
||||
private ThunderscopeBridgeHeader header;
|
||||
private readonly IInterprocessSemaphoreWaiter dataRequestSemaphore;
|
||||
private readonly IInterprocessSemaphoreReleaser dataReadySemaphore;
|
||||
private bool dataRequested = false;
|
||||
private bool acquiringRegionFilled = false;
|
||||
|
||||
public Span<byte> Span { get { unsafe { return new Span<byte>(dataPointer, (int)dataCapacityBytes); } } }
|
||||
public Span<byte> AcquiringRegion { get { return GetAcquiringRegion(); } }
|
||||
public ThunderscopeMonitoring Monitoring { get { return header.Monitoring; } }
|
||||
|
||||
public unsafe ThunderscopeBridgeWriter(ThunderscopeBridgeOptions options, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this.options = options;
|
||||
dataCapacityBytes = options.BridgeCapacityBytes - (uint)sizeof(ThunderscopeBridgeHeader);
|
||||
dataCapacityInBytes = options.BridgeCapacityBytes - (uint)sizeof(ThunderscopeBridgeHeader);
|
||||
file = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? new MemoryFileWindows(options)
|
||||
: new MemoryFileUnix(options, loggerFactory);
|
||||
|
@ -37,14 +43,16 @@ namespace TS.NET
|
|||
|
||||
try
|
||||
{
|
||||
basePointer = AcquirePointer();
|
||||
basePointer = GetPointer();
|
||||
dataPointer = basePointer + sizeof(ThunderscopeBridgeHeader);
|
||||
|
||||
// Writer sets initial state of header
|
||||
header.State = ThunderscopeMemoryBridgeState.Empty;
|
||||
header.AcquiringRegion = ThunderscopeMemoryAcquiringRegion.RegionA;
|
||||
header.Version = 1;
|
||||
header.DataCapacityBytes = dataCapacityBytes;
|
||||
header.DataCapacityBytes = dataCapacityInBytes;
|
||||
SetHeader();
|
||||
dataRequestSemaphore = InterprocessSemaphore.CreateWaiter(options.MemoryName + "DataRequest");
|
||||
dataReadySemaphore = InterprocessSemaphore.CreateReleaser(options.MemoryName + "DataReady");
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -67,11 +75,6 @@ namespace TS.NET
|
|||
file.Dispose();
|
||||
}
|
||||
|
||||
public IInterprocessSemaphoreReleaser GetWriterSemaphore()
|
||||
{
|
||||
return InterprocessSemaphore.CreateReleaser(options.MemoryName);
|
||||
}
|
||||
|
||||
public ThunderscopeConfiguration Configuration
|
||||
{
|
||||
set
|
||||
|
@ -82,45 +85,67 @@ namespace TS.NET
|
|||
}
|
||||
}
|
||||
|
||||
public ThunderscopeMonitoring Monitoring
|
||||
public ThunderscopeProcessing Processing
|
||||
{
|
||||
set
|
||||
{
|
||||
// This is a shallow copy, but considering the struct should be 100% blitable (i.e. no reference types), this is effectively a full copy
|
||||
header.Monitoring = value;
|
||||
header.Processing = value;
|
||||
SetHeader();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsReadyToWrite
|
||||
public void MonitoringReset()
|
||||
{
|
||||
get
|
||||
header.Monitoring.TotalAcquisitions = 0;
|
||||
header.Monitoring.MissedAcquisitions = 0;
|
||||
SetHeader();
|
||||
}
|
||||
|
||||
public void SwitchRegionIfNeeded()
|
||||
{
|
||||
if (!dataRequested)
|
||||
dataRequested = dataRequestSemaphore.Wait(0); // Only wait on the semaphore once and cache the result, clearing when needed later
|
||||
if (dataRequested && acquiringRegionFilled) // UI has requested data and there is data available to be read...
|
||||
{
|
||||
GetHeader();
|
||||
return header.State == ThunderscopeMemoryBridgeState.Empty;
|
||||
dataRequested = false;
|
||||
acquiringRegionFilled = false;
|
||||
header.AcquiringRegion = header.AcquiringRegion switch
|
||||
{
|
||||
ThunderscopeMemoryAcquiringRegion.RegionA => ThunderscopeMemoryAcquiringRegion.RegionB,
|
||||
ThunderscopeMemoryAcquiringRegion.RegionB => ThunderscopeMemoryAcquiringRegion.RegionA,
|
||||
_ => throw new InvalidDataException("Enum value not handled, add enum value to switch")
|
||||
};
|
||||
SetHeader();
|
||||
// Console.WriteLine("[BW] SwitchRegionIfNeeded -> switching!");
|
||||
dataReadySemaphore.Release(); // Allow UI to use the acquired region
|
||||
}
|
||||
else
|
||||
{
|
||||
// Console.WriteLine("[BW] SwitchRegionIfNeeded -> NOT switching");
|
||||
}
|
||||
}
|
||||
|
||||
public void DataWritten()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
header.State = ThunderscopeMemoryBridgeState.Full;
|
||||
SetHeader();
|
||||
}
|
||||
header.Monitoring.TotalAcquisitions++;
|
||||
if (acquiringRegionFilled)
|
||||
header.Monitoring.MissedAcquisitions++;
|
||||
acquiringRegionFilled = true;
|
||||
SetHeader();
|
||||
}
|
||||
|
||||
private void GetHeader()
|
||||
{
|
||||
unsafe { Unsafe.Copy(ref header, basePointer); }
|
||||
}
|
||||
//private void GetHeader()
|
||||
//{
|
||||
// unsafe { Unsafe.Copy(ref header, basePointer); }
|
||||
//}
|
||||
|
||||
private void SetHeader()
|
||||
{
|
||||
unsafe { Unsafe.Copy(basePointer, ref header); }
|
||||
}
|
||||
|
||||
private unsafe byte* AcquirePointer()
|
||||
private unsafe byte* GetPointer()
|
||||
{
|
||||
byte* ptr = null;
|
||||
view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
|
||||
|
@ -129,5 +154,16 @@ namespace TS.NET
|
|||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
private unsafe Span<byte> GetAcquiringRegion()
|
||||
{
|
||||
int regionLength = (int)dataCapacityInBytes / 2;
|
||||
return header.AcquiringRegion switch
|
||||
{
|
||||
ThunderscopeMemoryAcquiringRegion.RegionA => new Span<byte>(dataPointer, regionLength),
|
||||
ThunderscopeMemoryAcquiringRegion.RegionB => new Span<byte>(dataPointer + regionLength, regionLength),
|
||||
_ => throw new InvalidDataException("Enum value not handled, add enum value to switch")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,13 @@ namespace TS.NET.Semaphore.Linux
|
|||
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "There is a bug in the rule!")]
|
||||
internal static class Interop
|
||||
{
|
||||
private const string Lib = "librt";
|
||||
private const string Lib = "librt.so.1";
|
||||
private const uint SEMVALUEMAX = 32767;
|
||||
private const int OCREAT = 0x040; // create the semaphore if it does not exist
|
||||
|
||||
private const int ENOENT = 2; // The named semaphore does not exist.
|
||||
private const int EINTR = 4; // Semaphore operation was interrupted by a signal.
|
||||
private const int EAGAIN = 11; // Couldn't be acquired (sem_trywait)
|
||||
private const int ENOMEM = 12; // Out of memory
|
||||
private const int EACCES = 13; // Semaphore exists, but the caller does not have permission to open it.
|
||||
private const int EEXIST = 17; // O_CREAT and O_EXCL were specified and the semaphore exists.
|
||||
|
@ -37,6 +38,9 @@ namespace TS.NET.Semaphore.Linux
|
|||
[DllImport(Lib, SetLastError = true)]
|
||||
private static extern int sem_wait(IntPtr handle);
|
||||
|
||||
[DllImport(Lib, SetLastError = true)]
|
||||
private static extern int sem_trywait(IntPtr handle);
|
||||
|
||||
[DllImport(Lib, SetLastError = true)]
|
||||
private static extern int sem_timedwait(IntPtr handle, ref PosixTimespec abs_timeout);
|
||||
|
||||
|
@ -86,6 +90,19 @@ namespace TS.NET.Semaphore.Linux
|
|||
Wait(handle);
|
||||
return true;
|
||||
}
|
||||
else if (millisecondsTimeout == 0)
|
||||
{
|
||||
if (sem_trywait(handle) == 0)
|
||||
return true;
|
||||
|
||||
return Error switch
|
||||
{
|
||||
EAGAIN => false,
|
||||
EINVAL => throw new InvalidPosixSempahoreException(),
|
||||
EINTR => throw new OperationCanceledException(),
|
||||
_ => throw new PosixSempahoreException(Error),
|
||||
};
|
||||
}
|
||||
|
||||
var timeout = DateTimeOffset.UtcNow.AddMilliseconds(millisecondsTimeout);
|
||||
return Wait(handle, timeout);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue