LJMUAstroecology / flirpy

Python library to interact with FLIR camera cores
Other
191 stars 54 forks source link

Multiple Boson under win32 - find_cameras returns only first #39

Closed helarsen closed 3 years ago

helarsen commented 3 years ago

I have several BOSON on Windows 10 system. Auto detection seems not to work. "find_cameras.exe" returns only one camera. Is that correctly observed?

If so, I would like to suggest that at more appropriate function would be to return a list of all devices.

jveitchmichaelis commented 3 years ago

Yes, currently the script only returns one camera: https://github.com/LJMUAstroecology/flirpy/blob/master/find_cameras/find_cameras/find_cameras.cpp

Just have it print out every id rather than just the first. Feel free to submit a pull request! Note that while it's easy to do this, it is not trivial to associate camera IDs with their correct serial ports (and this should be done properly so that you can query each camera for internal info/control settings).

This is a duplicate issue and therefore I'm closing this one: https://github.com/LJMUAstroecology/flirpy/issues/24

helarsen commented 3 years ago

thanks. And sorry I should have seen #24. I will investigate if I can come up with a solution. The enum com devices is easy - done that already. The cross linking to video device is the hard part.

jveitchmichaelis commented 3 years ago

Yeah, I would suggest just returning a vector of IDs and then printing them comma separated (or something similar). Unfortunately I no longer have multiple Bosons to test with otherwise I'd have a go - though I can update the code and if you're willing to test it we can work together on it. The difficult bit is then matching the serials, but I think that should be possible.

helarsen commented 3 years ago

I am not much familier with this workflow. The code below is tested and will return a list of COM ports and the Boson serial number. The list could also be returned as [["COM35", 12456], ["COM8",23456]] to keep items as a camera property. Anyways - this code does not break much ground. I would be happy to assist and can test with at least 2 Bosons. henning

@classmethod def get_serial_device_list(self): """ Attempts to find the serial ports that has a Boson connection and returns a list of COM port and serial number Returns

list of string serial port name list of int Boson serial number """ device_list = list_ports.comports() device_list = sorted(device_list, key=lambda dev: dev.device) # in case os uses random order. VID = 0x09CB PID = 0x4007 ports = [ device.device for device in device_list if device.vid == VID and device.pid == PID] sns = [ int(device.serial_number) for device in device_list if device.vid == VID and device.pid == PID] return ports, sns

jveitchmichaelis commented 3 years ago

So if you can extract the serial number from DirectShow as well, then that should give you the correct device. Could you check whether the serial number you can get from the device matches any of the properties listed by Windows (any unique matching parameter would do actually)? You can check this by going to Device Manager and checking the Details tab for the camera there (you should be able to see e.g. the PID, VID and friendly name, etc).

helarsen commented 3 years ago

One device seen by device manager: image

Second device seen by device manager: image

So there is a common value seen in both views. One can get this value via COM listing in python (code above with minor modification): for COM8: location = '1-1.2:x.2' or hwid='USB VID:PID=09CB:4007 SER=85786 LOCATION=1-1.2:x.2' for COM35 location = '1-1.3.2:x.2' hwid='USB VID:PID=09CB:4007 SER=53112 LOCATION=1-1.3.2:x.2'

Although a different format it is clearly the same parameter and thus a link between the two views. BUT How to get the video location information programmatically?

jveitchmichaelis commented 3 years ago

You should be able to query that using the cpp script, in the same way we check for the "friendly name" e.g.

hr = pPropBag->Read(L"Location information", &var, 0);

and then convert it to a string and return it with the id.

helarsen commented 3 years ago

Unfortunately pPropBag interface calls

hr = pPropBag->Read(L"Location Information", &var, 0); hr = pPropBag->Read(L"Location information", &var, 0); hr = pPropBag->Read(L"location", &var, 0); hr = pPropBag->Read(L"Location", &var, 0);

does not return any value to var. But

hr = pPropBag->Read(L"DevicePath", &var, 0);

returns

var= BSTR = 0x01203734 L"\\?\usb#vid_09cb&pid_4007&mi_00#d&2b4821fa&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global"

and for the second camera

var= BSTR = 0x01203734 L"\\?\usb#vid_09cb&pid_4007&mi_00#3&26014a8a&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global"

As seen slightly different but does not correlate well to what serial port enumeration gives. So back to the guesswork - I wish I could find documentation for this horrible pPropBag interface

jveitchmichaelis commented 3 years ago

Yeah, I've not been able to find a way of enumerating valid keys for an IPropertyBag. This might be one for Stack Overflow to answer. I'm not super familiar with COM programming.

helarsen commented 3 years ago

https://docs.microsoft.com/en-us/windows/win32/directshow/selecting-a-capture-device says

  1. The "FriendlyName" and "Description" properties are suitable for displaying in a UI. The "FriendlyName" property is available for every device. It contains a human-readable name for the device.
  2. The "Description" property is available only for DV and D-VHS/MPEG camcorder devices. For more information, see MSDV Driver and MSTape Driver. If available, it contains a description of the device which is more specific than the "FriendlyName" property. Typically it includes the vendor name.
  3. The "DevicePath" property is not a human-readable string, but is guaranteed to be unique for each video capture device on the system. You can use this property to distinguish between two or more instances of the same model of device.

so basically there are only Firendlyname and devicePath to read that way. DevicePath is unique (example is in my previous post) but it does not help here due to lack of visibility via the serial port.

Tried to switch the device into ramp mode and hoped that way to generate an image which is easy to identify. This has for the moment stranded on the fact that the test ramps I get are not fixed patterns and are not reproducible which makes it hard to use. I am however confident that it can be done that way. Problem with test ramp is that it is (also) poorly documented and there are a surprisingly many types of ramps not only patterns but also the location they are at. Does anyone reading here have experience with setting test ramps?

jveitchmichaelis commented 3 years ago

This example compiles fine for me on Windows 10 and does print out the location correctly. https://stackoverflow.com/questions/3438366/setupdigetdeviceproperty-usage-example

We should be able to do it by matching up the device paths, which is a bit messy but I can't see another obvious way.

Here is a complete example. I split the Device Path to get the unique ID for the camera, and then query the device list to get the location string. You should then be able to match that up to the COM port in PySerial.

// find_cameras.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include <dshow.h>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>
#include <cctype>
#include <algorithm>

#pragma comment(lib, "strmiids")

#include <devguid.h>    // for GUID_DEVCLASS_CDROM etc
#include <setupapi.h>
#include <cfgmgr32.h>   // for MAX_DEVICE_ID_LEN, CM_Get_Parent and CM_Get_Device_ID
#define INITGUID
#include <tchar.h>
#include <stdio.h>

//#include "c:\WinDDK\7600.16385.1\inc\api\devpkey.h"

// include DEVPKEY_Device_BusReportedDeviceDesc from WinDDK\7600.16385.1\inc\api\devpropdef.h
#ifdef DEFINE_DEVPROPKEY
#undef DEFINE_DEVPROPKEY
#endif
#ifdef INITGUID
#define DEFINE_DEVPROPKEY(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8, pid) EXTERN_C const DEVPROPKEY DECLSPEC_SELECTANY name = { { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }, pid }
#else
#define DEFINE_DEVPROPKEY(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8, pid) EXTERN_C const DEVPROPKEY name
#endif // INITGUID

// include DEVPKEY_Device_BusReportedDeviceDesc from WinDDK\7600.16385.1\inc\api\devpkey.h
DEFINE_DEVPROPKEY(DEVPKEY_Device_BusReportedDeviceDesc, 0x540b947e, 0x8b40, 0x45bc, 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2, 4);     // DEVPROP_TYPE_STRING
DEFINE_DEVPROPKEY(DEVPKEY_Device_ContainerId, 0x8c7ed206, 0x3f8a, 0x4827, 0xb3, 0xab, 0xae, 0x9e, 0x1f, 0xae, 0xfc, 0x6c, 2);     // DEVPROP_TYPE_GUID
DEFINE_DEVPROPKEY(DEVPKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);    // DEVPROP_TYPE_STRING
DEFINE_DEVPROPKEY(DEVPKEY_DeviceDisplay_Category, 0x78c34fc8, 0x104a, 0x4aca, 0x9e, 0xa4, 0x52, 0x4d, 0x52, 0x99, 0x6e, 0x57, 0x5a);  // DEVPROP_TYPE_STRING_LIST
DEFINE_DEVPROPKEY(DEVPKEY_Device_LocationInfo, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 15);    // DEVPROP_TYPE_STRING
DEFINE_DEVPROPKEY(DEVPKEY_Device_Manufacturer, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 13);    // DEVPROP_TYPE_STRING
DEFINE_DEVPROPKEY(DEVPKEY_Device_SecuritySDS, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 26);    // DEVPROP_TYPE_SECURITY_DESCRIPTOR_STRING

#define ARRAY_SIZE(arr)     (sizeof(arr)/sizeof(arr[0]))

#pragma comment (lib, "setupapi.lib")

typedef BOOL(WINAPI* FN_SetupDiGetDevicePropertyW)(
    __in       HDEVINFO DeviceInfoSet,
    __in       PSP_DEVINFO_DATA DeviceInfoData,
    __in       const DEVPROPKEY* PropertyKey,
    __out      DEVPROPTYPE* PropertyType,
    __out_opt  PBYTE PropertyBuffer,
    __in       DWORD PropertyBufferSize,
    __out_opt  PDWORD RequiredSize,
    __in       DWORD Flags
    );

// List all USB devices with some additional information
void ListDeviceLocation(CONST GUID* pClassGuid, LPCTSTR pszEnumerator, std::wstring devicePath)
{
    unsigned i;
    DWORD dwSize;
    DEVPROPTYPE ulPropertyType;
    CONFIGRET status;
    HDEVINFO hDevInfo;
    SP_DEVINFO_DATA DeviceInfoData;
    const static LPCTSTR arPrefix[3] = { TEXT("VID_"), TEXT("PID_"), TEXT("MI_") };
    TCHAR szDeviceInstanceID[MAX_DEVICE_ID_LEN];
    WCHAR szBuffer[4096];
    FN_SetupDiGetDevicePropertyW fn_SetupDiGetDevicePropertyW = (FN_SetupDiGetDevicePropertyW)
        GetProcAddress(GetModuleHandle(TEXT("Setupapi.dll")), "SetupDiGetDevicePropertyW");

    // List all connected USB devices
    hDevInfo = SetupDiGetClassDevs(&GUID_DEVCLASS_CAMERA, pszEnumerator, NULL, DIGCF_PRESENT);
    if (hDevInfo == INVALID_HANDLE_VALUE) {
        std::cout << "Invalid handle";
        return;
    }

    // Find the ones that are driverless
    for (i = 0; ; i++) {
        DeviceInfoData.cbSize = sizeof(DeviceInfoData);
        if (!SetupDiEnumDeviceInfo(hDevInfo, i, &DeviceInfoData)) {
            break;
        }

        status = CM_Get_Device_ID(DeviceInfoData.DevInst, szDeviceInstanceID, MAX_PATH, 0);
        if (status != CR_SUCCESS) {
            std::cout << "Failed to get Device ID";
            continue;
        }

        // Check for correct device
        std::size_t found = std::wstring(szDeviceInstanceID).find(devicePath);

        if (found == std::string::npos) {
            continue;
        }

        // Retreive the device description as reported by the device itself
        // On Vista and earlier, we can use only SPDRP_DEVICEDESC
        // On Windows 7, the information we want ("Bus reported device description") is
        // accessed through DEVPKEY_Device_BusReportedDeviceDesc
        if (fn_SetupDiGetDevicePropertyW && fn_SetupDiGetDevicePropertyW(hDevInfo, &DeviceInfoData, &DEVPKEY_Device_BusReportedDeviceDesc,
            &ulPropertyType, (BYTE*)szBuffer, sizeof(szBuffer), &dwSize, 0)) {

            if (fn_SetupDiGetDevicePropertyW(hDevInfo, &DeviceInfoData, &DEVPKEY_Device_LocationInfo,
                &ulPropertyType, (BYTE*)szBuffer, sizeof(szBuffer), &dwSize, 0)) {
                _tprintf(TEXT("Location: \"%ls\"\n"), szBuffer);
            }
        }
    }

    return;
}

inline std::wstring convert(const std::string& as)
{
    // deal with trivial case of empty string
    if (as.empty())    return std::wstring();

    // determine required length of new string
    size_t reqLength = ::MultiByteToWideChar(CP_UTF8, 0, as.c_str(), (int)as.length(), 0, 0);

    // construct new string of required length
    std::wstring ret(reqLength, L'\0');

    // convert old string to new string
    ::MultiByteToWideChar(CP_UTF8, 0, as.c_str(), (int)as.length(), &ret[0], (int)ret.length());

    // return new string ( compiler should optimize this away )
    return ret;
}

HRESULT EnumerateDevices(REFGUID category, IEnumMoniker **ppEnum)
{
    // Create the System Device Enumerator.
    ICreateDevEnum *pDevEnum;
    HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL,
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pDevEnum));

    if (SUCCEEDED(hr))
    {
        // Create an enumerator for the category.
        hr = pDevEnum->CreateClassEnumerator(category, ppEnum, 0);
        if (hr == S_FALSE)
        {
            hr = VFW_E_NOT_FOUND;  // The category is empty. Treat as an error.
        }
        pDevEnum->Release();
    }
    return hr;
}

std::vector<std::wstring> split(std::wstring input, wchar_t delim) {
    // Vector of string to save tokens 
    std::vector <std::wstring> tokens;

    std::wstring intermediate;
    std::wstringstream stream;
    stream << input;

    // Tokenizing w.r.t. hash
    while (std::getline(stream, intermediate, delim))
    {
        std::transform(intermediate.begin(), intermediate.end(), intermediate.begin(), ::toupper);
        tokens.push_back(intermediate);
    }

    return tokens;
}

int get_id(IEnumMoniker *pEnum, std::string friendly_name)
{
    IMoniker *pMoniker = NULL;
    int i = 0;

    while (pEnum->Next(1, &pMoniker, NULL) == S_OK)
    {
        IPropertyBag *pPropBag;
        HRESULT hr = pMoniker->BindToStorage(0, 0, IID_PPV_ARGS(&pPropBag));
        if (FAILED(hr))
        {
            pMoniker->Release();
            continue;
        }

        VARIANT var;
        VariantInit(&var);

        // Get description or friendly name.
        hr = pPropBag->Read(L"Description", &var, 0);
        if (FAILED(hr))
        {
            hr = pPropBag->Read(L"FriendlyName", &var, 0);
        }
        if (SUCCEEDED(hr))
        {
            std::wstring str(var.bstrVal);

            std::size_t found = str.find(convert(friendly_name));

            if (found != std::string::npos) {
                hr = pPropBag->Read(L"DevicePath", &var, 0);
                if (SUCCEEDED(hr)){
                    auto path_tokens = split(std::wstring(var.bstrVal), L'#');
                    ListDeviceLocation(&GUID_DEVCLASS_CAMERA, NULL, path_tokens.at(2));
                }
                return i;
            }
            VariantClear(&var);
        }

        i++;

        pPropBag->Release();
        pMoniker->Release();
    }

    return -1;
}

int find_device(std::string friendly_name) {
    HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    int id = -1;
    if (SUCCEEDED(hr))
    {
        IEnumMoniker *pEnum;

        hr = EnumerateDevices(CLSID_VideoInputDeviceCategory, &pEnum);
        if (SUCCEEDED(hr))
        {
            id = get_id(pEnum, friendly_name);
            pEnum->Release();
        }
        CoUninitialize();
    }
    return id;
}

int main(int argc, char** argv)
{
    if (argc >= 2) {
        int devid =  find_device(argv[1]);
        std::cout << devid;
    }

    return 0;
}

Example output:

(base) PS C:\Users\Josh\code\flirpy\find_cameras\x64\Debug> .\find_cameras.exe "Integrated Camera"
Location: "0000.0014.0000.008.000.000.000.000.000"
0
helarsen commented 3 years ago

Great work! I think its almost in the PropBag!

This is what I get looping for all flir: image

and it matches what device manager show. Remains to correlate 0009.0000.0000.001.002.000.000.000.000 with location = '1-1.2:x.2' 0009.0000.0000.001.003.002.000.000.000 with location = '1-1.3.2:x.2' because the COM query gives for COM8: location = '1-1.2:x.2' or hwid='USB VID:PID=09CB:4007 SER=85786 LOCATION=1-1.2:x.2' for COM35 location = '1-1.3.2:x.2' hwid='USB VID:PID=09CB:4007 SER=53112 LOCATION=1-1.3.2:x.2'

Fortunately this can and should be done in python. Guessing that it is a matter of comparing the fields in bold and setting .000 = "" and hoping the format is more or less fixed.

helarsen commented 3 years ago

Did you try removing spaces with the query string? The following works for me with an integrated camera:

hr = pPropBag->Read(L"LocationInformation", &var, 0);

no did not work in this context

jveitchmichaelis commented 3 years ago

Did you try removing spaces with the query string? The following works for me with an integrated camera: hr = pPropBag->Read(L"LocationInformation", &var, 0);

no did not work in this context

Yeah I wondered if that was an easy fix, but then I realised I hadn't checked the output was actually a success and I was getting random strings every time.

jveitchmichaelis commented 3 years ago

For now let's assume that it's those three fixed fields. If someone else has an issue with it, then we can worry about it later!

helarsen commented 3 years ago

I have implemented the algorithms to query the video and the serial port and match up giving a list of cameras:

[{"vid":1, "port":"COM3", "sn":1234}, {"vid":2, "port":"COM45", "sn":1256} ], is_race (note 1)

There remain a rather important work incorporating this into the original class. This require restructuring of the Boson class. This is a work which is best done by the main author as it affects the architecture.

One odd thing I see (I may say an asymmetry) with the current architecture is that when the class is created, only the com port is opened and not the associated video port. I think they should both be opened in the constructor so that the camera is ready for any type of operation (cap is initialized). With the new feature this should be possible to do in a consistent manner.

Also one should decide how to identify a camera to the constructor: By the vid, the port or maybe by the serial number. Again a fundamental change to the architecture.

(note 1) One note about the list of cameras: If there is a PnP event between the two type of device queries, the procedure will fail. The reason is that the vid always starts at the lowest value, so if we have vid 1 and 2 and 1 gets unplugged, the remaining will change to vid=1 To allow handling of this, the code is checking that the two queries have the same number of devices and if not the second argument returned is set to True meaning "RaceCondition"

Let me know any thoughts about further progress and suggestions.

jveitchmichaelis commented 3 years ago

I'll take a look at this properly shortly, but I would suggest that rather returning a flag for a race condition, just make the whole thing error and ask the user to try again. This seems like a very narrow edge case (if you pull a camera during enumeration?) and it would be simpler to just leave it as an assert somewhere.

helarsen commented 3 years ago

ok I see 4 options to signal a pnp event during the execution of the two device enumerations which causes inconsistency.

  1. ignore and return whatever camera configuration was found. Not good idea as it masks the error.
  2. return None
  3. return a flag (race=True) like i describe and let the caller deal with it
  4. throw an error of some kind inside the enumerator

I really have no præference of the 3

jveitchmichaelis commented 3 years ago

There is also option 5 which is to repeat the check until (for say 3 retries) the results of the enumeration are consistent?