logicanalyzer

24 channel, 100Msps logic analyzer hardware and software

using CLCapture;
using CommandLine;
using Newtonsoft.Json;
using SharedDriver;
using System.IO.Ports;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Channels;

Regex regAddressPort = new Regex("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\\:[0-9]+");
Regex regAddress = new Regex("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+");
LogicAnalyzerDriver? driver = null;

TaskCompletionSource<CaptureEventArgs> captureCompletedTask;

Console.CancelKeyPress += Console_CancelKeyPress;

return await Parser.Default.ParseArguments<CLCaptureOptions, CLNetworkOptions>(args)
        .MapResult(
            async (CLCaptureOptions opts) => await Capture(opts),
            async (CLNetworkOptions opts) => Configure(opts),
            errs => Task.FromResult(-1)
            );

async Task<int> Capture(CLCaptureOptions opts)
{
    bool isNetworkAddress = regAddressPort.IsMatch(opts.AddressPort);
    var ports = SerialPort.GetPortNames();

    if (!isNetworkAddress && !ports.Any(p => p.ToLower() == opts.AddressPort.ToLower()))
    {
        Console.WriteLine("Cannot find specified serial port or address has an incorrect format.");
        return -1;
    }

    if(string.IsNullOrWhiteSpace(opts.OutputFile))
    {
        Console.WriteLine("Output file not specified.");
        return -1;
    }

    string ext = Path.GetExtension(opts.OutputFile).ToLower();

    if (ext != ".csv" || ext != ".lac")
    {
        Console.WriteLine("Unsupported output file type. Must be .csv or .lac.");
        return -1;
    }

    CLChannel[]? channels;

    try
    {

        channels = opts.Channels?.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(c => new CLChannel(c)).ToArray();

        if (channels == null || channels.Any(c => c.ChannelNumber < 1 || c.ChannelNumber > 24))
        {
            Console.WriteLine("Specified capture channels out of range.");
            return -1;
        }

    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return -1;
    }

    if(opts.Trigger == null || opts.Trigger.Value == null)
    {
        Console.WriteLine("Invalid trigger definition.");
        return -1;
    }

    switch (opts.Trigger.TriggerType)
    {
        case TriggerType.Edge:

            if (opts.Trigger.Channel < 1 || opts.Trigger.Channel > 24)
            {
                Console.WriteLine("Trigger channel out of range.");
                return -1;
            }

            break;

        case TriggerType.Fast:

            if (opts.Trigger.Value.Length > 5)
            {
                Console.WriteLine("Fast trigger only supports up to 5 channels.");
                return -1;
            }

            if (opts.Trigger.Value.Length + opts.Trigger.Channel > 17)
            {
                Console.WriteLine("Fast trigger can only be used with the first 16 channels.");
                return -1;
            }

            break;

        case TriggerType.Complex:

            if (opts.Trigger.Value.Length > 16)
            {
                Console.WriteLine("Complex trigger only supports up to 16 channels.");
                return -1;
            }

            if (opts.Trigger.Value.Length + opts.Trigger.Channel > 17)
            {
                Console.WriteLine("Complex trigger can only be used with the first 16 channels.");
                return -1;
            }

            break;
    }

    CaptureSession session = new CaptureSession();
    session.Frequency = opts.SamplingFrequency;
    session.PreTriggerSamples = opts.PreSamples;
    session.PostTriggerSamples = opts.PostSamples;
    session.CaptureChannels = channels.OrderBy(c => c.ChannelNumber).Select(c => new AnalyzerChannel { ChannelNumber = c.ChannelNumber - 1, ChannelName = c.ChannelName }).ToArray();
    session.LoopCount = opts.BurstCount > 1 ? opts.BurstCount - 1 : 0;
    session.MeasureBursts = opts.MeasureBurst;
    session.TriggerType = opts.Trigger.TriggerType;
    session.TriggerChannel = opts.Trigger.Channel - 1;

    if (session.TriggerType == TriggerType.Edge)
    {
        session.TriggerInverted = opts.Trigger.Value == "0";
    }
    else
    {
        session.TriggerBitCount = opts.Trigger.Value.Length;
        session.TriggerPattern = 0;

        for (int buc = 0; buc < opts.Trigger.Value.Length; buc++)
        {
            if (opts.Trigger.Value[buc] == '1')
                session.TriggerPattern |= (UInt16)(1 << buc);
        }
    }

    Console.WriteLine($"Opening logic analyzer in {opts.AddressPort}...");

    try
    {
        driver = new LogicAnalyzerDriver(opts.AddressPort);
    }
    catch
    {
        Console.WriteLine($"Error detecting Logic Analyzer in port/address {opts.AddressPort}");
        return -1;
    }

    Console.WriteLine($"Connected to device {driver.DeviceVersion} in port/address {opts.AddressPort}");
    Console.WriteLine($"Device max. frequency: {driver.MaxFrequency}");
    Console.WriteLine($"Device max. channels: {driver.ChannelCount}");
    Console.WriteLine($"Device buffer size: {driver.BufferSize}");

    if (opts.SamplingFrequency > driver.MaxFrequency || opts.SamplingFrequency < driver.MinFrequency)
    {
        driver.Dispose();
        Console.WriteLine($"Requested sampling frequency out of device's capabilities ({driver.MinFrequency}-{driver.MaxFrequency}).");
        return -1;
    }

    var limits = driver.GetLimits(session.CaptureChannels.Select(c => c.ChannelNumber).ToArray());

    if(session.PreTriggerSamples > limits.MaxPreSamples || session.PreTriggerSamples < limits.MinPreSamples)
    {
        driver.Dispose();
        Console.WriteLine($"Requested pre-trigger samples out of device's capabilities ({limits.MinPreSamples}-{limits.MaxPreSamples}).");
        return -1;
    }

    if (session.PostTriggerSamples > limits.MaxPostSamples || session.PostTriggerSamples < limits.MinPostSamples)
    {
        driver.Dispose();
        Console.WriteLine($"Requested post-trigger samples out of device's capabilities ({limits.MinPostSamples}-{limits.MaxPostSamples}).");
        return -1;
    }

    if(session.TotalSamples > limits.MaxTotalSamples)
    {
        driver.Dispose();
        Console.WriteLine($"Requested total samples exceed device's capabilities ({limits.MaxTotalSamples}).");
        return -1;
    }

    captureCompletedTask = new TaskCompletionSource<CaptureEventArgs>();

    Console.WriteLine("Starting capture...");
    var resStart = driver.StartCapture(session, CaptureFinished);

    if (resStart != CaptureError.None)
    {
        switch (resStart)
        {
            case CaptureError.Busy:
                Console.WriteLine("Device is busy, stop the capture before starting a new one.");
                return -1;
            case CaptureError.BadParams:
                Console.WriteLine("Specified parameters are incorrect.\r\n\r\n    -Frequency must be between 3.1Khz and 100Mhz\r\n    -PreSamples must be between 2 and 31743\r\n    -PostSamples must be between 512 and 32767\r\n    -Total samples cannot exceed 32767");
                return -1;
            case CaptureError.HardwareError:
                Console.WriteLine("Device reported error starting capture. Restart the device and try again.");
                return -1;
            case CaptureError.UnexpectedError:
                Console.WriteLine("Unexpected error. Restart the device and try again.");
                return -1;
        }
    }

    var result = await captureCompletedTask.Task;

    if (!result.Success)
    {
        Console.WriteLine("Error capturing data.");
        return -1;
    }

    driver.Dispose();

    Console.WriteLine("Capture complete, writing output file(s)...");

    await WriteOutput(session, opts.OutputFile);

    Console.WriteLine("Done.");

    return 1;
}

async Task WriteOutput(CaptureSession session, string outputFile)
{
    string ext = Path.GetExtension(outputFile).ToLower();

    if (ext == ".csv")
    {
        await WriteCSV(session, outputFile);
    }
    else if (ext == ".lac")
    {
        await WriteLAC(session, outputFile);
    }
}

async Task WriteLAC(CaptureSession session, string outputFile)
{
    var content = JsonConvert.SerializeObject(new ExportedCapture { Settings = session });
    await File.WriteAllTextAsync(outputFile, content);
}

async Task WriteCSV(CaptureSession session, string outputFile)
{
    var file = File.Create(outputFile);
    StreamWriter sw = new StreamWriter(file);

    sw.WriteLine(String.Join(',', session.CaptureChannels.Select(c => c.ChannelName).ToArray()));

    StringBuilder sb = new StringBuilder();

    for (int sample = 0; sample < session.TotalSamples; sample++)
    {
        sb.Clear();

        for (int buc = 0; buc < session.CaptureChannels.Length; buc++)
        {
            if (session.CaptureChannels[buc].Samples?[sample] == 1)
                sb.Append("1,");
            else
                sb.Append("0,");
        }
        sb.Remove(sb.Length - 1, 1);
        await sw.WriteLineAsync(sb.ToString());
    }

    sw.Close();
    sw.Dispose();
    file.Close();
    file.Dispose();

    if (session.Bursts != null && session.Bursts.Length > 0)
    {
        var outBursts = Path.Combine(Path.GetDirectoryName(outputFile) ?? "", Path.GetFileNameWithoutExtension(outputFile) + "_bursts.csv");
        file = File.Create(outBursts);

        sw = new StreamWriter(file);

        sw.WriteLine("Start,End,SampleGap,TimeGap");

        foreach (var burst in session.Bursts)
        {
            await sw.WriteLineAsync($"{burst.BurstSampleStart},{burst.BurstSampleEnd},{burst.BurstSampleGap},{burst.BurstTimeGap}");
        }

        sw.Close();
        sw.Dispose();
        file.Close();
        file.Dispose();
    }

}

int Configure(CLNetworkOptions opts)
{
    var ports = SerialPort.GetPortNames();

    if (!ports.Any(p => p.ToLower() == opts.SerialPort.ToLower()))
    {
        Console.WriteLine("Cannot find specified serial port.");
        return -1;
    }

    if (opts.AccessPoint.Length > 32)
    {
        Console.WriteLine("Invalid access point name.");
        return -1;
    }

    if (opts.Password.Length > 63)
    {
        Console.WriteLine("Invalid password.");
        return -1;
    }

    if (!regAddress.IsMatch(opts.Address))
    {
        Console.WriteLine("Invalid IP address.");
        return -1;
    }

    if (opts.Port < 1)
    {
        Console.WriteLine("Invalid TCP port.");
        return -1;
    }

    

    Console.WriteLine($"Opening logic analyzer in port {opts.SerialPort}...");

    try
    {
        driver = new LogicAnalyzerDriver(opts.SerialPort);
    }
    catch
    {
        Console.WriteLine($"Error detecting Logic Analyzer in port {opts.SerialPort}");
        return -1;
    }

    Console.WriteLine($"Connected to device {driver.DeviceVersion} in port {opts.SerialPort}");

    if (driver.DeviceVersion == null || !driver.DeviceVersion.Contains("WIFI"))
    {
        Console.WriteLine($"Device does not support WiFi. Aborting operation.");
        driver.Dispose();
        return -1;
    }

    bool result = driver.SendNetworkConfig(opts.AccessPoint, opts.Password, opts.Address, opts.Port);

    if (!result)
    {
        Console.WriteLine("Error updating the network settings, restart the device and try again.");
        driver.Dispose();
        return -1;
    }

    driver.Dispose();
    Console.WriteLine("Done.");

    return 1;
}

void CaptureFinished(CaptureEventArgs e)
{
    captureCompletedTask.SetResult(e);
}

void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
    if (driver != null)
    {
        try
        {
            driver.StopCapture();
            driver.Dispose();
        }
        catch { }
        driver = null;
    }
}

class ExportedCapture
{
    public required CaptureSession Settings { get; set; }
}