Added Latest TS.NET and removed legacy software #239

Merged
AleksaBjelogrlic merged 1 commits from TS.NET into master 2022-11-01 01:34:07 +00:00
165 changed files with 1589 additions and 84752 deletions
Showing only changes of commit 78143d76d5 - Show all commits

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
using System;
namespace TS.NET.Engine
{
public record HardwareResponseDto(HardwareRequestDto Request);
}

View File

@ -0,0 +1,6 @@
using System;
namespace TS.NET.Engine
{
public record InputDataDto(ThunderscopeConfiguration Configuration, ThunderscopeMemory Memory);
}

View File

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

View File

@ -0,0 +1,6 @@
using System;
namespace TS.NET.Engine
{
public record ProcessingResponseDto(ProcessingRequestDto Command);
}

View File

@ -3,26 +3,54 @@ using System.Diagnostics;
using TS.NET; using TS.NET;
using TS.NET.Engine; 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"; Console.Title = "Engine";
using (Process p = Process.GetCurrentProcess()) using (Process p = Process.GetCurrentProcess())
p.PriorityClass = ProcessPriorityClass.High; 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)); 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(); // Instantiate dataflow channels
for (int i = 0; i < 120; i++) // 120 = about 1 seconds worth of samples at 1GSPS const int bufferLength = 120; // 120 = about 1 seconds worth of samples at 1GSPS
memoryPool.Writer.Write(new ThunderscopeMemory()); 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); 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 processingTask = new();
processingTask.Start(loggerFactory, processingPool.Reader, memoryPool.Writer); processingTask.Start(loggerFactory, processingChannel.Reader, inputChannel.Writer, processingRequestChannel.Reader, processingResponseChannel.Writer);
InputTask inputTask = new(); 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.WriteLine("Running... press any key to stop");
Console.ReadKey(); Console.ReadKey();
processingTask.Stop(); processingTask.Stop();
inputTask.Stop(); inputTask.Stop();
socketTask.Stop();
scpiTask.Stop();

View File

@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Diagnostics;
namespace TS.NET.Engine namespace TS.NET.Engine
{ {
@ -9,11 +10,17 @@ namespace TS.NET.Engine
private CancellationTokenSource? cancelTokenSource; private CancellationTokenSource? cancelTokenSource;
private Task? taskLoop; 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"); var logger = loggerFactory.CreateLogger("InputTask");
cancelTokenSource = new CancellationTokenSource(); 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() public void Stop()
@ -22,48 +29,161 @@ namespace TS.NET.Engine
taskLoop?.Wait(); 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 try
{ {
Thread.CurrentThread.Name = "TS.NET Input"; thunderscope.Open(thunderscopeDevice);
Thread.CurrentThread.Priority = ThreadPriority.Highest; ThunderscopeConfiguration configuration = DoInitialConfiguration(thunderscope);
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.Start(); thunderscope.Start();
Stopwatch oneSecond = Stopwatch.StartNew();
uint oneSecondEnqueueCount = 0;
uint enqueueCounter = 0;
while (true) while (true)
{ {
cancelToken.ThrowIfCancellationRequested(); cancelToken.ThrowIfCancellationRequested();
var memory = memoryPool.Read();
try // Check for configuration requests
if (hardwareRequestChannel.PeekAvailable() != 0)
{ {
thunderscope.Read(memory); logger.LogDebug("Stop acquisition and process commands...");
} thunderscope.Stop();
catch (Exception ex)
{ while (hardwareRequestChannel.TryRead(out var request))
if (ex.Message == "ReadFile - failed (1359)")
{ {
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; 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) catch (OperationCanceledException)
{ {
logger.LogDebug($"{nameof(InputTask)} stopping"); logger.LogDebug($"{nameof(InputTask)} stopping");
throw;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -72,8 +192,66 @@ namespace TS.NET.Engine
} }
finally finally
{ {
thunderscope.Stop();
logger.LogDebug($"{nameof(InputTask)} stopped"); 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);
}
}
} }
} }

View File

@ -1,7 +1,6 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.Intrinsics.X86;
namespace TS.NET.Engine namespace TS.NET.Engine
{ {
@ -10,16 +9,20 @@ namespace TS.NET.Engine
private CancellationTokenSource? cancelTokenSource; private CancellationTokenSource? cancelTokenSource;
private Task? taskLoop; private Task? taskLoop;
//, Action<Memory<double>> action public void Start(
public void Start(ILoggerFactory loggerFactory, BlockingChannelReader<ThunderscopeMemory> processingPool, BlockingChannelWriter<ThunderscopeMemory> memoryPool) ILoggerFactory loggerFactory,
BlockingChannelReader<InputDataDto> processingChannel,
BlockingChannelWriter<ThunderscopeMemory> inputChannel,
BlockingChannelReader<ProcessingRequestDto> processingRequestChannel,
BlockingChannelWriter<ProcessingResponseDto> processingResponseChannel)
{ {
var logger = loggerFactory.CreateLogger("ProcessingTask"); var logger = loggerFactory.CreateLogger("ProcessingTask");
cancelTokenSource = new CancellationTokenSource(); 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 // 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 // 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); ThunderscopeBridgeWriter bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", dataCapacityBytes), loggerFactory);
taskLoop = Task.Factory.StartNew(() => Loop(logger, processingPool, memoryPool, bridge, cancelTokenSource.Token), TaskCreationOptions.LongRunning); taskLoop = Task.Factory.StartNew(() => Loop(logger, bridge, processingChannel, inputChannel, processingRequestChannel, processingResponseChannel, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
} }
public void Stop() 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. // 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 try
{ {
const int initialMaxChannelLength = 10 * 1000000;
Thread.CurrentThread.Name = "TS.NET Processing"; Thread.CurrentThread.Name = "TS.NET Processing";
// Configuration values to be updated during runtime... conveiniently all on ThunderscopeMemoryBridgeHeader ThunderscopeProcessing processingConfig = new()
ThunderscopeConfiguration config = new()
{ {
Channels = Channels.Four, ChannelLength = initialMaxChannelLength,
ChannelLength = 10 * 1000000,//(ulong)ChannelLength.OneHundredM,
HorizontalSumLength = HorizontalSumLength.None, HorizontalSumLength = HorizontalSumLength.None,
TriggerChannel = TriggerChannel.One, TriggerChannel = TriggerChannel.One,
TriggerMode = TriggerMode.Normal TriggerMode = TriggerMode.Normal,
ChannelDataType = ThunderscopeChannelDataType.Byte
}; };
bridge.Configuration = config; bridge.Processing = processingConfig;
bridge.MonitoringReset();
ThunderscopeMonitoring monitoring = new()
{
TotalAcquisitions = 0,
MissedAcquisitions = 0
};
bridge.Monitoring = monitoring;
var bridgeWriterSemaphore = bridge.GetWriterSemaphore();
// Various buffers allocated once and reused forevermore. // Various buffers allocated once and reused forevermore.
//Memory<byte> hardwareBuffer = new byte[ThunderscopeMemory.Length]; //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> 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 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; DateTimeOffset startTime = DateTimeOffset.UtcNow;
uint dequeueCounter = 0; uint dequeueCounter = 0;
uint oneSecondHoldoffCount = 0; uint oneSecondHoldoffCount = 0;
uint oneSecondDequeueCount = 0;
// HorizontalSumUtility.ToDivisor(horizontalSumLength) // HorizontalSumUtility.ToDivisor(horizontalSumLength)
Stopwatch oneSecond = Stopwatch.StartNew(); Stopwatch oneSecond = Stopwatch.StartNew();
var circularBuffer1 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length); var circularBuffer1 = new ChannelCircularAlignedBuffer((uint)processingConfig.ChannelLength + ThunderscopeMemory.Length);
var circularBuffer2 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length); var circularBuffer2 = new ChannelCircularAlignedBuffer((uint)processingConfig.ChannelLength + ThunderscopeMemory.Length);
var circularBuffer3 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length); var circularBuffer3 = new ChannelCircularAlignedBuffer((uint)processingConfig.ChannelLength + ThunderscopeMemory.Length);
var circularBuffer4 = new ChannelCircularAlignedBuffer((uint)config.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) while (true)
{ {
cancelToken.ThrowIfCancellationRequested(); cancelToken.ThrowIfCancellationRequested();
var memory = processingPool.Read(cancelToken);
// Add a zero-wait mechanism here that allows for configuration values to be updated // Check for processing requests
// (which will require updating many of the intermediate variables/buffers) 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++; dequeueCounter++;
int channelLength = config.ChannelLength; oneSecondDequeueCount++;
switch (config.Channels)
int channelLength = processingConfig.ChannelLength;
switch (processingDto.Configuration.AdcChannels)
{ {
// Processing pipeline: // Processing pipeline:
// Shuffle (if needed) // Shuffle (if needed)
@ -100,32 +166,32 @@ namespace TS.NET.Engine
// Write to circular buffer // Write to circular buffer
// Trigger // Trigger
// Data segment on trigger (if needed) // Data segment on trigger (if needed)
case Channels.None: case AdcChannels.None:
break; break;
case Channels.One: case AdcChannels.One:
// Horizontal sum (EDIT: triggering should happen _before_ horizontal sum) // Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
//if (config.HorizontalSumLength != HorizontalSumLength.None) //if (config.HorizontalSumLength != HorizontalSumLength.None)
// throw new NotImplementedException(); // throw new NotImplementedException();
// Write to circular buffer // Write to circular buffer
circularBuffer1.Write(memory.Span); circularBuffer1.Write(processingDto.Memory.Span);
// Trigger // 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") _ => throw new ArgumentException("Invalid TriggerChannel value")
}; };
trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount); trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
} }
// Finished with the memory, return it // Finished with the memory, return it
memoryPool.Write(memory); inputChannel.Write(processingDto.Memory);
break; break;
case Channels.Two: case AdcChannels.Two:
// Shuffle // Shuffle
Shuffle.TwoChannels(input: memory.Span, output: shuffleBuffer); Shuffle.TwoChannels(input: processingDto.Memory.Span, output: shuffleBuffer);
// Finished with the memory, return it // Finished with the memory, return it
memoryPool.Write(memory); inputChannel.Write(processingDto.Memory);
// Horizontal sum (EDIT: triggering should happen _before_ horizontal sum) // Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
//if (config.HorizontalSumLength != HorizontalSumLength.None) //if (config.HorizontalSumLength != HorizontalSumLength.None)
// throw new NotImplementedException(); // throw new NotImplementedException();
@ -133,9 +199,9 @@ namespace TS.NET.Engine
circularBuffer1.Write(postShuffleCh1_2); circularBuffer1.Write(postShuffleCh1_2);
circularBuffer2.Write(postShuffleCh2_2); circularBuffer2.Write(postShuffleCh2_2);
// Trigger // 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.One => postShuffleCh1_2,
TriggerChannel.Two => postShuffleCh2_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); trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
} }
break; break;
case Channels.Four: case AdcChannels.Four:
// Shuffle // Shuffle
Shuffle.FourChannels(input: memory.Span, output: shuffleBuffer); Shuffle.FourChannels(input: processingDto.Memory.Span, output: shuffleBuffer);
// Finished with the memory, return it // Finished with the memory, return it
memoryPool.Write(memory); inputChannel.Write(processingDto.Memory);
// Horizontal sum (EDIT: triggering should happen _before_ horizontal sum) // Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
//if (config.HorizontalSumLength != HorizontalSumLength.None) //if (config.HorizontalSumLength != HorizontalSumLength.None)
// throw new NotImplementedException(); // throw new NotImplementedException();
@ -158,9 +224,9 @@ namespace TS.NET.Engine
circularBuffer3.Write(postShuffleCh3_4); circularBuffer3.Write(postShuffleCh3_4);
circularBuffer4.Write(postShuffleCh4_4); circularBuffer4.Write(postShuffleCh4_4);
// Trigger // 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.One => postShuffleCh1_4,
TriggerChannel.Two => postShuffleCh2_4, TriggerChannel.Two => postShuffleCh2_4,
@ -169,47 +235,60 @@ namespace TS.NET.Engine
_ => throw new ArgumentException("Invalid TriggerChannel value") _ => throw new ArgumentException("Invalid TriggerChannel value")
}; };
trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount); trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
monitoring.TotalAcquisitions += holdoffEndCount;
oneSecondHoldoffCount += holdoffEndCount; oneSecondHoldoffCount += holdoffEndCount;
if (holdoffEndCount > 0) if (holdoffEndCount > 0)
{ {
// logger.LogDebug("Trigger Fired");
for (int i = 0; i < holdoffEndCount; i++) for (int i = 0; i < holdoffEndCount; i++)
{ {
if (bridge.IsReadyToWrite) var bridgeSpan = bridge.AcquiringRegion;
{ uint holdoffEndIndex = (uint)postShuffleCh1_4.Length - holdoffEndIndices[i];
bridge.Monitoring = monitoring; circularBuffer1.Read(bridgeSpan.Slice(0, channelLength), holdoffEndIndex);
var bridgeSpan = bridge.Span; circularBuffer2.Read(bridgeSpan.Slice(channelLength, channelLength), holdoffEndIndex);
uint holdoffEndIndex = (uint)postShuffleCh1_4.Length - holdoffEndIndices[i]; circularBuffer3.Read(bridgeSpan.Slice(channelLength + channelLength, channelLength), holdoffEndIndex);
circularBuffer1.Read(bridgeSpan.Slice(0, channelLength), holdoffEndIndex); circularBuffer4.Read(bridgeSpan.Slice(channelLength + channelLength + channelLength, channelLength), holdoffEndIndex);
circularBuffer2.Read(bridgeSpan.Slice(channelLength, channelLength), holdoffEndIndex); bridge.DataWritten();
circularBuffer3.Read(bridgeSpan.Slice(channelLength + channelLength, channelLength), holdoffEndIndex); bridge.SwitchRegionIfNeeded();
circularBuffer4.Read(bridgeSpan.Slice(channelLength + channelLength + channelLength, channelLength), holdoffEndIndex);
bridge.DataWritten();
bridgeWriterSemaphore.Release(); // Signal to the reader that data is available
}
else
{
monitoring.MissedAcquisitions++;
}
} }
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} "); //logger.LogInformation($"Dequeue #{dequeueCounter++}, Ch1 triggers: {triggerCount1}, Ch2 triggers: {triggerCount2}, Ch3 triggers: {triggerCount3}, Ch4 triggers: {triggerCount4} ");
break; 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(); oneSecond.Restart();
oneSecondHoldoffCount = 0; oneSecondHoldoffCount = 0;
oneSecondDequeueCount = 0;
} }
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
logger.LogDebug($"{nameof(ProcessingTask)} stopping"); logger.LogDebug($"{nameof(ProcessingTask)} stopping");
throw;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -221,5 +300,15 @@ namespace TS.NET.Engine
logger.LogDebug($"{nameof(ProcessingTask)} stopped"); 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);
}
}
} }
} }

View File

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

View File

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

View File

@ -89,7 +89,6 @@ namespace TS.NET.UI.Avalonia
{ {
uint bufferLength = 4 * 100 * 1000 * 1000; //Maximum record length = 100M samples per channel uint bufferLength = 4 * 100 * 1000 * 1000; //Maximum record length = 100M samples per channel
ThunderscopeBridgeReader bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", bufferLength), loggerFactory); ThunderscopeBridgeReader bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", bufferLength), loggerFactory);
var bridgeReadSemaphore = bridge.GetReaderSemaphore();
Stopwatch stopwatch = Stopwatch.StartNew(); Stopwatch stopwatch = Stopwatch.StartNew();
@ -97,9 +96,9 @@ namespace TS.NET.UI.Avalonia
while (true) while (true)
{ {
cancelToken.ThrowIfCancellationRequested(); 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 = (uint)bridge.Configuration.ChannelLength;//1000;
uint viewportLength = 1000000;// (uint)upDownIndex.Value; uint viewportLength = 1000000;// (uint)upDownIndex.Value;
if (viewportLength < 100) if (viewportLength < 100)
@ -130,13 +129,12 @@ namespace TS.NET.UI.Avalonia
var cfg = bridge.Configuration; 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 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)); int offset = (int)((channelLength / 2) - (viewportLength / 2));
data.Slice(offset, (int)viewportLength).ToDoubleArray(channel1); offset += (int)channelLength; 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(channel2); offset += (int)channelLength;
data.Slice(offset, (int)viewportLength).ToDoubleArray(channel3); offset += (int)channelLength; data.Slice(offset, (int)viewportLength).ToDoubleArray(channel3); offset += (int)channelLength;
data.Slice(offset, (int)viewportLength).ToDoubleArray(channel4); data.Slice(offset, (int)viewportLength).ToDoubleArray(channel4);
bridge.DataRead();
//var reading = bridge.Span[(int)upDownIndex.Value]; //var reading = bridge.Span[(int)upDownIndex.Value];
count++; count++;

View File

@ -1,51 +1,16 @@
using System; using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using TS.NET.Interop;
using System.Threading.Tasks;
namespace TS.NET namespace TS.NET
{ {
public record ThunderscopeDevice public record ThunderscopeDevice(string DevicePath);
{
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 class Thunderscope public class Thunderscope
{ {
private static Guid deviceGuid = new(0x74c7e4a9, 0x6d5d, 0x4a70, 0xbc, 0x0d, 0x20, 0x69, 0x1d, 0xff, 0x9e, 0x9d); private ThunderscopeInterop interop;
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 bool open = false; private bool open = false;
private ThunderscopeHardwareState hardwareState = new(); private ThunderscopeHardwareState hardwareState = new();
@ -54,48 +19,16 @@ namespace TS.NET
public static List<ThunderscopeDevice> IterateDevices() public static List<ThunderscopeDevice> IterateDevices()
{ {
List<ThunderscopeDevice> devices = new(); return ThunderscopeInterop.IterateDevices();
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;
} }
public void Open(ThunderscopeDevice device) public void Open(ThunderscopeDevice device)
{ {
if (open) if (open)
Close(); 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)); interop = ThunderscopeInterop.CreateInterop(device);
//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);
Initialise(); Initialise();
open = true; open = true;
} }
@ -133,12 +66,12 @@ namespace TS.NET
ConfigureDatamover(hardwareState); 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) if (!open)
throw new Exception("Thunderscope not open"); throw new Exception("Thunderscope not open");
if (!hardwareState.DatamoverEnabled) if (!hardwareState.DatamoverEnabled)
throw new Exception("Thunderscope not started"); throw new ThunderscopeNotRunningException("Thunderscope not started");
// Buffer data must be aligned to 4096 // Buffer data must be aligned to 4096
//if (0xFFF & (ptrdiff_t)data) //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 - buffer_read_pos) pages_to_read = hardwareState.RamSizePages - buffer_read_pos;
if (pages_to_read > hardwareState.RamSizePages / 4) pages_to_read = hardwareState.RamSizePages / 4; 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); //read_handle(ts, ts->c2h0_handle, dataPtr, buffer_read_pos << 12, pages_to_read << 12);
dataIndex += pages_to_read << 12; dataIndex += pages_to_read << 12;
@ -198,7 +131,7 @@ namespace TS.NET
private uint Read32(BarRegister register) private uint Read32(BarRegister register)
{ {
Span<byte> bytes = new byte[4]; Span<byte> bytes = new byte[4];
Read(userFilePointer, bytes, (ulong)register); interop.ReadUser(bytes, (ulong)register);
return BinaryPrimitives.ReadUInt32LittleEndian(bytes); return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
} }
@ -206,7 +139,7 @@ namespace TS.NET
{ {
Span<byte> bytes = new byte[4]; Span<byte> bytes = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
Write(userFilePointer, bytes, (ulong)register); interop.WriteUser(bytes, (ulong)register);
} }
private void WriteFifo(ReadOnlySpan<byte> data) private void WriteFifo(ReadOnlySpan<byte> data)
@ -224,7 +157,7 @@ namespace TS.NET
for (int i = 0; i < data.Length; i++) for (int i = 0; i < data.Length; i++)
{ {
// TODO: Replace with write32 // 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) // read TDFV (vacancy byte)
Read32(BarRegister.SERIAL_FIFO_TDFV_ADDRESS); Read32(BarRegister.SERIAL_FIFO_TDFV_ADDRESS);
@ -363,6 +296,13 @@ namespace TS.NET
ConfigureChannel(channel); ConfigureChannel(channel);
} }
public void ResetBuffer()
{
hardwareState.BufferHead = 0;
hardwareState.BufferTail = 0;
ConfigureDatamover(hardwareState);
}
private void ConfigureChannel(int channel) private void ConfigureChannel(int channel)
{ {
ConfigureChannels(); ConfigureChannels();
@ -492,7 +432,7 @@ namespace TS.NET
throw new Exception("Thunderscope - datamover error"); throw new Exception("Thunderscope - datamover error");
if ((error_code & 1) > 0) if ((error_code & 1) > 0)
throw new Exception("Thunderscope - FIFO overflow"); throw new ThunderscopeFIFOOverflowException("Thunderscope - FIFO overflow");
uint overflow_cycles = (transfer_counter >> 16) & 0x3FFF; uint overflow_cycles = (transfer_counter >> 16) & 0x3FFF;
if (overflow_cycles > 0) if (overflow_cycles > 0)
@ -507,56 +447,7 @@ namespace TS.NET
ulong pages_available = hardwareState.BufferHead - hardwareState.BufferTail; ulong pages_available = hardwareState.BufferHead - hardwareState.BufferTail;
if (pages_available >= hardwareState.RamSizePages) if (pages_available >= hardwareState.RamSizePages)
throw new Exception("Thunderscope - memory full"); throw new ThunderscopeMemoryOutOfMemoryException("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");
}
} }
} }
} }

View File

@ -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) { }
}
}

View File

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

View File

@ -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()
{
}
}
}

View File

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

View File

@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
// [assembly: DisableRuntimeMarshalling] // Coming in .NET 7 with a source analyzer. This will guarantee interop has zero performance penalty. // [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 // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices#blittable-types
// CharSet = CharSet.Unicode helps ensure blitability // CharSet = CharSet.Unicode helps ensure blitability

View File

@ -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()
{
}
}
}

View File

@ -5,23 +5,68 @@ namespace TS.NET
// Ensure this is blitable (i.e. don't use bool) // Ensure this is blitable (i.e. don't use bool)
// Pack of 1 = No padding. // 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. // 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 internal struct ThunderscopeBridgeHeader
{ {
// Version + DataCapacity is enough data for the UI to know how big a memorymappedfile to open // 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 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 ulong DataCapacityBytes; // Maximum size of the data array in bridge. Example: 400M, set from configuration file?
internal ThunderscopeMemoryBridgeState State; internal ThunderscopeMemoryAcquiringRegion AcquiringRegion; // Therefore 'AcquiredRegion' (to be used by UI) is the opposite
internal ThunderscopeConfiguration Configuration; internal ThunderscopeConfiguration Configuration; // Read only from UI perspective, UI uses SCPI interface to change configuration
internal ThunderscopeMonitoring Monitoring; 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)] [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 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.
public int ChannelLength; // Example: 4 channels with max length = 100M, can easily be 1k for high update rate. Max: Capacity/4, Min: 1k. //[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 HorizontalSumLength HorizontalSumLength;
public TriggerChannel TriggerChannel; public TriggerChannel TriggerChannel;
public TriggerMode TriggerMode; public TriggerMode TriggerMode;
@ -31,14 +76,14 @@ namespace TS.NET
// Monitoring variables that reset when configuration variables change // Monitoring variables that reset when configuration variables change
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ThunderscopeMonitoring public struct ThunderscopeMonitoring
{ {
public ulong TotalAcquisitions; // All triggers public ulong TotalAcquisitions; // All triggers
public ulong MissedAcquisitions; // Triggers that weren't displayed public ulong MissedAcquisitions; // Triggers that weren't displayed
} }
public enum ThunderscopeMemoryBridgeState : byte public enum ThunderscopeMemoryAcquiringRegion : byte
{ {
Empty = 1, // Writing is allowed RegionA = 1,
Full = 2, // Writing is blocked, waiting for reader to set back to Unset RegionB = 2
} }
} }

View File

@ -21,8 +21,8 @@ namespace TS.NET
MemoryName = memoryName; MemoryName = memoryName;
Path = path; Path = path;
DataCapacityBytes = dataCapacityBytes; DataCapacityBytes = dataCapacityBytes * 2; // *2 as there are 2 regions used in tick-tock fashion
BridgeCapacityBytes = (ulong)sizeof(ThunderscopeBridgeHeader) + dataCapacityBytes; BridgeCapacityBytes = (ulong)sizeof(ThunderscopeBridgeHeader) + DataCapacityBytes;
} }
} }
} }

View File

@ -12,21 +12,23 @@ namespace TS.NET
public class ThunderscopeBridgeReader : IDisposable public class ThunderscopeBridgeReader : IDisposable
{ {
private readonly ThunderscopeBridgeOptions options; private readonly ThunderscopeBridgeOptions options;
private readonly ulong dataBytesCapacity; private readonly ulong dataCapacityInBytes;
private readonly IMemoryFile file; private readonly IMemoryFile file;
private readonly MemoryMappedViewAccessor view; private readonly MemoryMappedViewAccessor view;
private unsafe byte* basePointer; private unsafe byte* basePointer;
private unsafe byte* dataPointer { get; } private unsafe byte* dataPointer { get; }
private ThunderscopeBridgeHeader header; 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 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) public unsafe ThunderscopeBridgeReader(ThunderscopeBridgeOptions options, ILoggerFactory loggerFactory)
{ {
this.options = options; this.options = options;
dataBytesCapacity = options.BridgeCapacityBytes - (uint)sizeof(ThunderscopeBridgeHeader); dataCapacityInBytes = options.BridgeCapacityBytes - (uint)sizeof(ThunderscopeBridgeHeader);
file = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) file = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? new MemoryFileWindows(options) ? new MemoryFileWindows(options)
: new MemoryFileUnix(options, loggerFactory); : new MemoryFileUnix(options, loggerFactory);
@ -37,7 +39,7 @@ namespace TS.NET
try try
{ {
basePointer = AcquirePointer(); basePointer = GetPointer();
dataPointer = basePointer + sizeof(ThunderscopeBridgeHeader); dataPointer = basePointer + sizeof(ThunderscopeBridgeHeader);
while (!IsHeaderSet) while (!IsHeaderSet)
@ -48,6 +50,8 @@ namespace TS.NET
GetHeader(); GetHeader();
if (header.DataCapacityBytes != options.DataCapacityBytes) if (header.DataCapacityBytes != options.DataCapacityBytes)
throw new Exception($"Mismatch in data capacity, options: {options.DataCapacityBytes}, bridge: {header.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 catch
{ {
@ -70,11 +74,6 @@ namespace TS.NET
file.Dispose(); file.Dispose();
} }
public IInterprocessSemaphoreWaiter GetReaderSemaphore()
{
return InterprocessSemaphore.CreateWaiter(options.MemoryName);
}
public ThunderscopeConfiguration Configuration public ThunderscopeConfiguration Configuration
{ {
get get
@ -84,6 +83,15 @@ namespace TS.NET
} }
} }
public ThunderscopeProcessing Processing
{
get
{
GetHeader();
return header.Processing;
}
}
public ThunderscopeMonitoring Monitoring public ThunderscopeMonitoring Monitoring
{ {
get get
@ -93,22 +101,25 @@ namespace TS.NET
} }
} }
public bool IsReadyToRead public bool RequestAndWaitForData(int millisecondsTimeout)
{ {
get if (!hasSignaledRequest)
{ {
GetHeader(); // Only signal request once, or we will run up semaphore counter
return header.State == ThunderscopeMemoryBridgeState.Full; dataRequestSemaphore.Release();
hasSignaledRequest = true;
} }
}
public void DataRead() bool wasReady = dataReadySemaphore.Wait(millisecondsTimeout);
{
unsafe if (wasReady)
{ {
header.State = ThunderscopeMemoryBridgeState.Empty; // Now that the bridge has tick-tocked, the next request will be 'real'
SetHeader(); // TODO: Should this be a separate method, or part of GetPointer() ?
hasSignaledRequest = false;
} }
return wasReady;
} }
private void GetHeader() private void GetHeader()
@ -116,12 +127,12 @@ namespace TS.NET
unsafe { Unsafe.Copy(ref header, basePointer); } unsafe { Unsafe.Copy(ref header, basePointer); }
} }
private void SetHeader() //private void SetHeader()
{ //{
unsafe { Unsafe.Copy(basePointer, ref header); } // unsafe { Unsafe.Copy(basePointer, ref header); }
} //}
private unsafe byte* AcquirePointer() private unsafe byte* GetPointer()
{ {
byte* ptr = null; byte* ptr = null;
view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
@ -130,5 +141,16 @@ namespace TS.NET
return ptr; 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")
};
}
} }
} }

View File

@ -11,22 +11,28 @@ using System.Runtime.CompilerServices;
namespace TS.NET 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 // 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 public class ThunderscopeBridgeWriter : IDisposable
{ {
private readonly ThunderscopeBridgeOptions options; private readonly ThunderscopeBridgeOptions options;
private readonly ulong dataCapacityBytes; private readonly ulong dataCapacityInBytes;
private readonly IMemoryFile file; private readonly IMemoryFile file;
private readonly MemoryMappedViewAccessor view; private readonly MemoryMappedViewAccessor view;
private unsafe byte* basePointer; private unsafe byte* basePointer;
private unsafe byte* dataPointer { get; } private unsafe byte* dataPointer { get; }
private ThunderscopeBridgeHeader header; 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) public unsafe ThunderscopeBridgeWriter(ThunderscopeBridgeOptions options, ILoggerFactory loggerFactory)
{ {
this.options = options; this.options = options;
dataCapacityBytes = options.BridgeCapacityBytes - (uint)sizeof(ThunderscopeBridgeHeader); dataCapacityInBytes = options.BridgeCapacityBytes - (uint)sizeof(ThunderscopeBridgeHeader);
file = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) file = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? new MemoryFileWindows(options) ? new MemoryFileWindows(options)
: new MemoryFileUnix(options, loggerFactory); : new MemoryFileUnix(options, loggerFactory);
@ -37,14 +43,16 @@ namespace TS.NET
try try
{ {
basePointer = AcquirePointer(); basePointer = GetPointer();
dataPointer = basePointer + sizeof(ThunderscopeBridgeHeader); dataPointer = basePointer + sizeof(ThunderscopeBridgeHeader);
// Writer sets initial state of header // Writer sets initial state of header
header.State = ThunderscopeMemoryBridgeState.Empty; header.AcquiringRegion = ThunderscopeMemoryAcquiringRegion.RegionA;
header.Version = 1; header.Version = 1;
header.DataCapacityBytes = dataCapacityBytes; header.DataCapacityBytes = dataCapacityInBytes;
SetHeader(); SetHeader();
dataRequestSemaphore = InterprocessSemaphore.CreateWaiter(options.MemoryName + "DataRequest");
dataReadySemaphore = InterprocessSemaphore.CreateReleaser(options.MemoryName + "DataReady");
} }
catch catch
{ {
@ -67,11 +75,6 @@ namespace TS.NET
file.Dispose(); file.Dispose();
} }
public IInterprocessSemaphoreReleaser GetWriterSemaphore()
{
return InterprocessSemaphore.CreateReleaser(options.MemoryName);
}
public ThunderscopeConfiguration Configuration public ThunderscopeConfiguration Configuration
{ {
set set
@ -82,45 +85,67 @@ namespace TS.NET
} }
} }
public ThunderscopeMonitoring Monitoring public ThunderscopeProcessing Processing
{ {
set 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 // 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(); 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(); dataRequested = false;
return header.State == ThunderscopeMemoryBridgeState.Empty; 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() public void DataWritten()
{ {
unsafe header.Monitoring.TotalAcquisitions++;
{ if (acquiringRegionFilled)
header.State = ThunderscopeMemoryBridgeState.Full; header.Monitoring.MissedAcquisitions++;
SetHeader(); acquiringRegionFilled = true;
} SetHeader();
} }
private void GetHeader() //private void GetHeader()
{ //{
unsafe { Unsafe.Copy(ref header, basePointer); } // unsafe { Unsafe.Copy(ref header, basePointer); }
} //}
private void SetHeader() private void SetHeader()
{ {
unsafe { Unsafe.Copy(basePointer, ref header); } unsafe { Unsafe.Copy(basePointer, ref header); }
} }
private unsafe byte* AcquirePointer() private unsafe byte* GetPointer()
{ {
byte* ptr = null; byte* ptr = null;
view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
@ -129,5 +154,16 @@ namespace TS.NET
return ptr; 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")
};
}
} }
} }

View File

@ -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!")] [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 internal static class Interop
{ {
private const string Lib = "librt"; private const string Lib = "librt.so.1";
private const uint SEMVALUEMAX = 32767; private const uint SEMVALUEMAX = 32767;
private const int OCREAT = 0x040; // create the semaphore if it does not exist 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 ENOENT = 2; // The named semaphore does not exist.
private const int EINTR = 4; // Semaphore operation was interrupted by a signal. 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 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 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. 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)] [DllImport(Lib, SetLastError = true)]
private static extern int sem_wait(IntPtr handle); private static extern int sem_wait(IntPtr handle);
[DllImport(Lib, SetLastError = true)]
private static extern int sem_trywait(IntPtr handle);
[DllImport(Lib, SetLastError = true)] [DllImport(Lib, SetLastError = true)]
private static extern int sem_timedwait(IntPtr handle, ref PosixTimespec abs_timeout); private static extern int sem_timedwait(IntPtr handle, ref PosixTimespec abs_timeout);
@ -86,6 +90,19 @@ namespace TS.NET.Semaphore.Linux
Wait(handle); Wait(handle);
return true; 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); var timeout = DateTimeOffset.UtcNow.AddMilliseconds(millisecondsTimeout);
return Wait(handle, timeout); return Wait(handle, timeout);

Some files were not shown because too many files have changed in this diff Show More