Using automation to stop a running capture

Hi,

I have an IoT device running automated power tests. It communicates with my power supply and turns it’s own power off and back on after a second if the boot was successful. Logic is connected and cpaturing I2C traffic in a cyclic buffer.

If I detect a fault situation on the device, my code (running on the device) will not power cycle but instead connect to Logic (started with “logic --automation --automationHost 0.0.0.0”) and send the “STOP_CAPTURE” command.

The connection to Logic (running on my PC at 192.168.1.100) is successful, but the STOP_CAPTURE command does not stop the capture (and I’m not getting any response).

My code is as follows:

        Log(ZONE_INFO, L"Connecting to Saleae Logic...\r\n");
        for (DWORD dwRetry = 0; dwRetry < LOGIC_CONNECT_RETRIES; dwRetry++)
        {
            if (Connect(LOGIC_IP, LOGIC_PORT))
                break;
            Wrn(L"Could not connect to Saleae Logic, retrying in %u ms... [%u]\r\n", LOGIC_CONNECT_RETRY_DELAY, dwRetry + 1);
            Sleep(LOGIC_CONNECT_RETRY_DELAY);
        }
        if (LOGIC_CONNECT_RETRIES == dwRetry)
        {
            dwError = GetLastError();
            Err(L"Could not connect to Saleae Logic!\r\n");
            break;
        }

        if (!SendCommand(LOGIC_COMMAND))
        {
            dwError = GetLastError();
            Err(L"Could not send %S command to Saleae Logic, error %d\r\n", LOGIC_COMMAND, dwError);
        }

        char szResponse[100];
        if (!ReadResponse(szResponse, sizeof(szResponse)))
        {
            dwError = GetLastError();
            Err(L"Could not read response from Saleae Logic, error %d\r\n", dwError);
        }

        LPCSTR pszPtr = strchr(szResponse, 'A');
        if (!pszPtr || (0 != strcmp(pszPtr, "ACK")))
        {
            dwError = GetLastError();
            Err(L"No ACK response from Saleae Logic, error %d\r\n", dwError);
        }

The connection succeeds, sending the command succeeds, the response is an empty string.

What am I missing?

SendCommand:

// Send a command to the TCP Server
BOOL SendCommand(LPCSTR szCmd)
{
    FUNC_ENTRY(L"szCmd=%S", szCmd);

    BOOL bRet = FALSE;

    // We use a loop for easy break out and error handling
    for (;;)
    {   // Sanity checks
        if (!IsConnected())
        {
            Err(L"Not connected\r\n");
            break;
        }

        if (!szCmd)
        {
            Err(L"Invalid parameter\r\n");
            break;
        }

        // Send the entire packet in one go
        if (SOCKET_ERROR == send(s_sck, szCmd, strlen(szCmd) + 1, 0))
        {
            Err(L"Error %d sending data over socket\r\n", WSAGetLastError());
            break;
        }

        bRet = TRUE;
        // Always break out of the loop
        break;
    }

    FUNC_EXIT(L"%s", bRet ? L"TRUE" : L"FALSE");
    return bRet;
}

ReadResponse:

// Read a response from the TCP Server
BOOL ReadResponse(LPSTR pszResponse, size_t stCount)
{
    FUNC_ENTRY(L"pszResponse=%S", pszResponse);

    BOOL bRet = FALSE;

    // We use a loop for easy break out and error handling
    for (;;)
    {   // Sanity checks
        if (!IsConnected())
        {
            Err(L"Not connected\r\n");
            break;
        }

        if (!pszResponse || !stCount)
        {
            Err(L"Invalid parameter\r\n");
            break;
        }


        // Read the entire response in one go
        if (SOCKET_ERROR == recv(s_sck, pszResponse, stCount, 0))
        {
            Err(L"Error %d receiving data over socket\r\n", WSAGetLastError());
            break;
        }

        bRet = TRUE;
        // Always break out of the loop
        break;
    }

    FUNC_EXIT(L"%s", bRet ? L"TRUE" : L"FALSE");
    return bRet;
}

Ok, well, looks like I was missing a lot. Logic is now using gRPC, which of course is not available on many IoT devices. Not a very well thought out move Saleae! Why can’t you keep the simple socket server operational as it was in v1? This is super simple to connect to from ANY device. gRPC is great if you have the protocol support, but if not it is a disaster!

Anyway, I tried handcrafting a gRPC command, but I had to make assumptions (because the low level command structure is not specified nor can be found in any code either, very annoying).

Here’s my updated code:

// Send a command to the TCP Server
BOOL SendCommand(const PUINT8 pu8Data, DWORD dwSize)
{
    FUNC_ENTRY(L"pu8Data=0x%08X, dwSize=%u", pu8Data, dwSize);

    BOOL bRet = FALSE;

    // We use a loop for easy break out and error handling
    for (;;)
    {   // Sanity checks
        if (!IsConnected())
        {
            Err(L"Not connected\r\n");
            break;
        }

        if (!pu8Data || !dwSize)
        {
            Err(L"Invalid parameter\r\n");
            break;
        }

        // Send the entire packet in one go
        if (SOCKET_ERROR == send(s_sck, (const char*)pu8Data, dwSize, 0))
        {
            Err(L"Error %d sending data over socket\r\n", WSAGetLastError());
            break;
        }

        bRet = TRUE;
        // Always break out of the loop
        break;
    }

    FUNC_EXIT(L"%s", bRet ? L"TRUE" : L"FALSE");
    return bRet;
}

// Read a response from the TCP Server
BOOL ReadResponse(PUINT8 pu8Data, LPDWORD pdwSize)
{
    FUNC_ENTRY(L"pu8Data=0x%08X, pdwSize=0x%08X", pu8Data, pdwSize);

    BOOL bRet = FALSE;

    // We use a loop for easy break out and error handling
    for (;;)
    {   // Sanity checks
        if (!IsConnected())
        {
            Err(L"Not connected\r\n");
            break;
        }

        if (!pu8Data || !pdwSize || !*pdwSize)
        {
            Err(L"Invalid parameter\r\n");
            break;
        }


        // Read the entire response in one go
        DWORD dwSize = recv(s_sck, (char*)pu8Data, *pdwSize, 0);
        if (SOCKET_ERROR == dwSize)
        {
            Err(L"Error %d receiving data over socket\r\n", WSAGetLastError());
            break;
        }
        *pdwSize = dwSize;

        bRet = TRUE;
        // Always break out of the loop
        break;
    }

    FUNC_EXIT(L"%s", bRet ? L"TRUE" : L"FALSE");
    return bRet;
}

And the code crafting the gRPC:

        Log(ZONE_INFO, L"Connecting to Saleae Logic...\r\n");
        for (DWORD dwRetry = 0; dwRetry < LOGIC_CONNECT_RETRIES; dwRetry++)
        {
            if (Connect(LOGIC_IP, LOGIC_PORT))
                break;
            Wrn(L"Could not connect to Saleae Logic, retrying in %u ms... [%u]\r\n", LOGIC_CONNECT_RETRY_DELAY, dwRetry + 1);
            Sleep(LOGIC_CONNECT_RETRY_DELAY);
        }
        if (LOGIC_CONNECT_RETRIES == dwRetry)
        {
            dwError = GetLastError();
            Err(L"Could not connect to Saleae Logic!\r\n");
            break;
        }

        // Send connection preface
        LPCSTR szConnectionPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
        if (!SendCommand((const PUINT8)szConnectionPreface, sizeof(szConnectionPreface)))
        {
            dwError = GetLastError();
            Err(L"Could not send connection preface to Saleae Logic, error %d\r\n", dwError);
        }

        // Send SETTINGS frame
        const UINT8 u8SettingsFrame[] = {0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00};
        if (!SendCommand((const PUINT8)u8SettingsFrame, sizeof(u8SettingsFrame)))
        {
            dwError = GetLastError();
            Err(L"Could not send settings frame to Saleae Logic, error %d\r\n", dwError);
        }

        // Send HEADERS frame header and payload
        const UINT8 u8HeadersFrameHeader[] = {0x00, 0x00, 0x58, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01};
        const UINT8 u8HpackHeaders[] = {
            0x42, 0xa3, 0x19, '/', 'L', 'o', 'g', 'i', 'c', 'M', 'a', 'n', 'a', 'g', 'e', 'r', '/', 'S', 't', 'o', 'p', 'C', 'a', 'p', 't', 'u', 'r', 'e',
            0x85, 0xa0, 0x0f, 'l', 'o', 'c', 'a', 'l', 'h', 'o', 's', 't', ':', '1', '0', '4', '3', '0',
            0x9b, 'c', 'o', 'n', 't', 'e', 'n', 't', '-', 't', 'y', 'p', 'e', 0x10, 'a', 'p', 'p', 'l', 'i', 'c', 'a', 't', 'i', 'o', 'n', '/', 'g', 'r', 'p', 'c',
            0x81, 't', 'e', 0x08, 't', 'r', 'a', 'i', 'l', 'e', 'r', 's'
        };
        if (!SendCommand((const PUINT8)u8HeadersFrameHeader, sizeof(u8HeadersFrameHeader)))
        {
            dwError = GetLastError();
            Err(L"Could not send headers frame to Saleae Logic, error %d\r\n", dwError);
        }
        if (!SendCommand((const PUINT8)u8HpackHeaders, sizeof(u8HpackHeaders)))
        {
            dwError = GetLastError();
            Err(L"Could not send Hpack header to Saleae Logic, error %d\r\n", dwError);
        }

        // Send DATA frame header
        const UINT8 u8DataFrameHeader[] = {0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01};
        if (!SendCommand((const PUINT8)u8DataFrameHeader, sizeof(u8DataFrameHeader)))
        {
            dwError = GetLastError();
            Err(L"Could not send headers frame to Saleae Logic, error %d\r\n", dwError);
        }

        Sleep(3000);

        char szResponse[100];
        DWORD dwSize = sizeof(szResponse);
        if (!ReadResponse((PUINT8)szResponse, &dwSize))
        {
            dwError = GetLastError();
            Err(L"Could not read response from Saleae Logic, error %d\r\n", dwError);
        }

The headers are encoded without Huffman encoding for simplicity, but the server might expect Huffman encoding for efficiency. Does Logic expect Huffman encoded packets?

The code above does not result in the capture stopping in Logic. It does give me a 26 byte response though, but mostly zeroes. Nothing human readable.

Any and all help / guidance would be appreciated!

@mverhagen Sorry for the trouble with this. With regards to your first post, you are correct in that our older Socket API commands documented here are no longer compatible with our Logic 2 software.

As you’ve discovered, we do in fact provide a new Python-based Automation API specifically designed for the Logic 2 software which does in fact utilize gRPC.

The repository for it below (along with example test scenarios) includes the (1) Python library around the gRPC API and (2) the gRPC .proto definition file.

Having said that, we unfortunately have not documented a way to handcraft gRPC commands, and we did not design our Automation API to be quickly used in this manner. I’ll forward this over to the rest of our engineering team in case they have any insights to share, though, we may not be able to prioritize looking into this due to other priority projects we are working on at the moment.

So, you have no C/C++ example code showing how to control the Logic app through automation whatsoever?

Why the move to gRPC? It limits so much what can be done. The need for Python, really? A simple command server, like that in use by power supplies for instance, is so easy to control from whatever device, using whatever programming language. Sounds to me like gRPC was a bad decision, but really looking forward to hear the reasoning for this change.

Is there no way you can just run both gRPC and the simple command server next to each other? Run gRPC on port 10430 and the simple TCP server on port 10429 for instance. It should be quite easy to put that v1 code back, right? If it runs on its own separate port it won’t bite your gRPC server.

I am totally stuck on this now. No way to control Logic from an IoT device being captured…

Some ideas:

  1. You could sniff the gRPC packets with wireshark:
    Analyzing gRPC messages using Wireshark | gRPC

… using Saleae’s .proto file

  1. Write a Logic 1 to Logic 2 automation server / API (in python) and use it to bridge your IoT device commands to Saleae logic (or write your own simplified socket API)

  2. Use gRPC to generate a full C++ binding/API for Saleae’s protobuf above

  3. Run old Logic 1 software and use old automation API (if your hardware is compatible)

It depends on how many automation commands you need, and what your goals are.

A wireshark capture of just the stop capture command would be very useful, but that means I need to install all the python stuff and get up to speed with that. Hoping someone in the community could capture the wireshark packet for me with the stop-capture command…

I’ve looked at generating the C++ binding/API using gRPC, but it’s quite a steep learning curve with cmake, vcpkg and/or other methods. Not easy. Oh how I wish Logic2 would still have the simple command server… Why oh why gRPC. I just don’t get it!

So, the only command you need to implement is the equivalent of Logic 1 socket API stop_capture command?

Or you need more than that?

Nope, that’s all I need!

I also tried Logic1, but that doesn’t even start up anymore on my Windows 11 x64 OS. It started once, but then never again, so that’s not an option either.

I’m currently going through the cmake, vcpkg, protoc mill to try and create a C++ API, but if you can capture just the stop_capture packet on Wireshark that would help me a lot.

Thanks!!

I’m not sure that the new API will work for your use case, or at least not your current way of doing it.

In the new Logic 2 Automation API, it has a separate session dedicated to the automation script that is separate from your own running session. In other words, you cannot automate the existing UI, rather it is more for a ‘headless’ run from start to finish.

Thus, I’m pretty sure you need to:

  • Initialize the new connection (with the Manager class)
    • using the Manager.launch() or Manager.connect() method)
    • Invokes gRPC GetAppInfoRequest procedure?
  • Start the capture (with the Manager.start_capture() method)
    • Invokes gRPC StartCaptureRequest procedure?
  • Wait or stop the capture (with the Capture.stop() method)
    • Invokes gRPC StopCaptureRequest procedure?
  • Close the connection (with the Manager.close() method)
    • Invokes gRPC CloseCaptureRequest procedure?

As far as I know, you can’t just jump straight to Capture.stop() (or gRPC StopCaptureRequest) without first having a Manager.start_capture() request (or gRPC StartCaptureRequest). However, would need a Saleae support person to elaborate on the gRPC usage restrictions if trying to access underlying gRPC calls directly (vs. using the python API).

Note: above are all based on the python API, defined in the Automation API documentation. The underlying gRPC protobuf for this is on the GitHub site (linked previously). I’m not exactly sure how the above python APIs are mapped to the gRPC remote procedure calls, but above is my initial guess.

For reference (in case it helps), attached are two wireshark captures:
stop_capture_tests.zip (6.1 KB)

In the zip file:

  • Test a manual capture/stop after 1s
    • Python script: stop_capture_1s.py
    • Wireshark capture: stop_capture_1s_log.pcapng
  • Test a manual capture/stop after 10s
    • Python script: stop_capture_10s.py
    • Wireshark capture: stop_capture_10s_log.pcapng

Finally, this was all done on a Logic 8 – not sure how it may be different on other (logic 8/16 Pro) hardware.

I’m with you – gRPC is not very ‘low-level access’ friendly for trying to use directly with vanilla TCP/IP sockets.

Oh my… This all seems to be very limiting. I have a device here that very occasionally has a problem reading the RTC right after boot. This also means I can’t start a capture (by the time the device has started, the fault situation that I am trying to capture has already happened).

This means I need to have a capture running at all times.

My intended setup is:

  1. Connect the Logic8 channel 1/2 probes to the I2C bus on the device.
  2. Start capturing the I2C data in a cyclic buffer in Logic2.
  3. Power the device
  4. At boot, my code running on the device checks the date. Is the year 2025 then the problem did not occur. My code will now send GDB commands to my power supply to turn the power off and back on after 1 second (so a simple power cycle of the device).
    If the year is found to be not 2025, it means the problem has occured and I need to send a command to Logic2 to stop the capture.

It seems this is all not possible. Saleae seems to have totally destroyed being able to do this. Again, WHY ON EARTH gRPC?! This is just not at all user-friendly. A simple TCP server with SIMPLE commands (like my GWInstek GPP-4323 power supply has, as do many other power supplies following the SCPI standard) is all you need. Stop being fancy and just create something that works. KISS, remember!?

Anyway, thank you very much for the captures. Unfortunately it sounds like it won’t work for my situation.

Saleae, is there anyway I can stop the capture by bringing one of the channels high, for instance? I could connect one channel to a GPIO and bring that GPIO high when I encounter the fault situation and want to stop the capture.

PS. I’m still trying to create the gRPC C++ bindings, but holy hell, many, many gigabytes are needed to compile all the required libraries. I’m running this inside a VM but keep running out of GBs on the virtual disk. Incredible how elaborate all of this is.

You can use Trigger Mode, as per: Capture Modes | Saleae Support
(e.g., rising or falling edge, or a pulse will stop the capture)

As far as why gRPC, I’ll defer to Saleae for an official response. However, my guess is that it provides an ‘easy’ way to have a cross-platform & multi-language friendly API that doesn’t require low-level socket programming to access. The APIs are auto-generated for each language from a single/common protobuf definition. I think ‘easy’ is based on already being tooled up to support it at the higher level of API abstraction and on a full powered development PC, not at the bare bones IoT / lower-level networking socket layer creating packets directly.

Note: The gRPC site does list a lot of options: Supported languages | gRPC
… Saleae seems to only officially use python, but has talked about .NET and C# in the past. Another benefit of gRPC is an ambitious developer could create their own support on their own preferred OS/language without waiting for Saleae to do it for them. A higher barrier for entry, but more powerful once you get there (if you need those extra powers).

Perhaps when they add back UI automation API, it can be done more like you requested (e.g., simple socket-level control, maybe VISA and/or SCPI based vs. gRPC). Even better for some might be to restore the previous socket API and extend with new commands, if it fits in their future product roadmap.

Yep, I’ll use the GPIO method to stop the capture. It’s the only way…

And yes, you have to be a very ambitious developer to want to create your own support for gRPC in the language and on the device of your choice. I started it, but am now stuck on CMAKE, another hurdle to get to grips with. You need 46.2 GB to get to the point of simply generating the .cc files describing the proto buffer. Honestly, Logic2 is a measurement “device”. It is screaming for SCPI support. gRPC was a very bad choice IMHO.

Here are the instructions (in case someone wants to create C++ bindings for Logic2):

[Instructions for Windows 11 x64]

  1. Download and install git
  2. Download and install CMake
  3. Open a command prompt
  4. cd\
  5. git clone https://github.com/microsoft/vcpkg.git
  6. cd vcpkg
  7. bootstrap-vcpkg.bat
  8. set VCPKG_ROOT=C:\vcpkg
  9. set path=%VCPKG_ROOT%;%PATH%
  10. vcpkg install grpc:x64-windows
  11. vcpkg install protobuf[zlib] protobuf[zlib]:x64-windows
  12. vcpkg integrate install
  13. set path=%VCPKG_ROOT%\packages\protobuf_x64-windows\tools\protobuf;%PATH%
  14. Create a folder c:\logic2 and download the saleae proto file into that folder (logic2-automation/proto/saleae/grpc/saleae.proto at develop · saleae/logic2-automation · GitHub)
  15. protoc -I=c:\logic2 --cpp_out=c:\logic2 c:\logic2\saleae.proto
  16. protoc -I=c:\logic2 --grpc_out=c:\logic2 --plugin=protoc-gen-grpc=“%VCPKG_ROOT%\packages\grpc_x64-windows\tools\grpc\grpc_cpp_plugin.exe” c:\logic2\saleae.proto
  17. cmake -G “Visual Studio 16 2019” -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake ./

I’m stuck on that last step. It needs a CMakeLists.txt so I’m just reading up on the format of that (if anyone in the community can help with this, it would be much appreciated).

The above procedure will take quite a long time to build everything and in the end will take up a whopping 46.2 GB, and even then there’s very little chance you will find the right libraries to run on a resource limited IoT device that may run FreeRTOS, QNX, Windows CE, etc.

I’ve posted a feature request for Logic2 to implement either a super simple proprietary TCP command server, or better, implement SCPI so it’s all more standardized (and you can even control the Logic2 GUI from eg LabView or a million other apps that support SCPI): Add easy automation, no gRPC! - Logic 2 - Ideas and Feature Requests - Saleae. SCPI is also very easy to implement in any language; just a simple TCP socket, send some ASCII characters and Bob’s your uncle. See above code.

I remain at the opinion that adding gRPC to Logic2 was a really, really, really bad design decision.