Do not use FTDI driver to fetch COM name, and search for MODEM devices
This commit is contained in:
parent
ba48714bcb
commit
d0aab972b6
|
@ -2,7 +2,7 @@
|
|||
* SerialPort_Windows.c
|
||||
*
|
||||
* Created on: Feb 25, 2012
|
||||
* Last Updated on: Jan 20, 2022
|
||||
* Last Updated on: Jan 21, 2022
|
||||
* Author: Will Hedgecock
|
||||
*
|
||||
* Copyright (C) 2012-2022 Fazecast, Inc.
|
||||
|
@ -32,10 +32,11 @@
|
|||
#include <windows.h>
|
||||
#include <delayimp.h>
|
||||
#include <direct.h>
|
||||
#include <ntddmodm.h>
|
||||
#include <ntddser.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <setupapi.h>
|
||||
#include <shellapi.h>
|
||||
#include <devpkey.h>
|
||||
#include <devguid.h>
|
||||
#include "ftdi/ftd2xx.h"
|
||||
|
@ -75,9 +76,6 @@ jfieldID eventFlagsField;
|
|||
// Runtime-loadable DLL functions
|
||||
typedef int (__stdcall *FT_CreateDeviceInfoListFunction)(LPDWORD);
|
||||
typedef int (__stdcall *FT_GetDeviceInfoListFunction)(FT_DEVICE_LIST_INFO_NODE*, LPDWORD);
|
||||
typedef int (__stdcall *FT_GetComPortNumberFunction)(FT_HANDLE, LPLONG);
|
||||
typedef int (__stdcall *FT_OpenFunction)(int, FT_HANDLE*);
|
||||
typedef int (__stdcall *FT_CloseFunction)(FT_HANDLE);
|
||||
|
||||
// List of available serial ports
|
||||
serialPortVector serialPorts = { NULL, 0, 0 };
|
||||
|
@ -107,7 +105,16 @@ JNIEXPORT jobjectArray JNICALL Java_com_fazecast_jSerialComm_SerialPort_getCommP
|
|||
|
||||
// Enumerate all serial ports present on the current system
|
||||
wchar_t comPort[128];
|
||||
HDEVINFO devList = SetupDiGetClassDevsW(&GUID_DEVCLASS_PORTS, NULL, NULL, DIGCF_PRESENT);
|
||||
const struct { GUID guid; DWORD flags; } setupClasses[] = {
|
||||
{ .guid = GUID_DEVCLASS_PORTS, .flags = DIGCF_PRESENT },
|
||||
{ .guid = GUID_DEVCLASS_MODEM, .flags = DIGCF_PRESENT },
|
||||
{ .guid = GUID_DEVCLASS_MULTIPORTSERIAL, .flags = DIGCF_PRESENT },
|
||||
{ .guid = GUID_DEVINTERFACE_COMPORT, .flags = DIGCF_PRESENT | DIGCF_DEVICEINTERFACE },
|
||||
{ .guid = GUID_DEVINTERFACE_MODEM, .flags = DIGCF_PRESENT | DIGCF_DEVICEINTERFACE }
|
||||
};
|
||||
for (int i = 0; i < (sizeof(setupClasses) / sizeof(setupClasses[0])); ++i)
|
||||
{
|
||||
HDEVINFO devList = SetupDiGetClassDevsW(&setupClasses[i].guid, NULL, NULL, setupClasses[i].flags);
|
||||
if (devList != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
// Iterate through all devices
|
||||
|
@ -241,6 +248,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_fazecast_jSerialComm_SerialPort_getCommP
|
|||
}
|
||||
SetupDiDestroyDeviceInfoList(devList);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to locate any FTDI-specified port descriptions
|
||||
HINSTANCE ftdiLibInstance = LoadLibrary(TEXT("ftd2xx.dll"));
|
||||
|
@ -248,10 +256,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_fazecast_jSerialComm_SerialPort_getCommP
|
|||
{
|
||||
FT_CreateDeviceInfoListFunction FT_CreateDeviceInfoList = (FT_CreateDeviceInfoListFunction)GetProcAddress(ftdiLibInstance, "FT_CreateDeviceInfoList");
|
||||
FT_GetDeviceInfoListFunction FT_GetDeviceInfoList = (FT_GetDeviceInfoListFunction)GetProcAddress(ftdiLibInstance, "FT_GetDeviceInfoList");
|
||||
FT_GetComPortNumberFunction FT_GetComPortNumber = (FT_GetComPortNumberFunction)GetProcAddress(ftdiLibInstance, "FT_GetComPortNumber");
|
||||
FT_OpenFunction FT_Open = (FT_OpenFunction)GetProcAddress(ftdiLibInstance, "FT_Open");
|
||||
FT_CloseFunction FT_Close = (FT_CloseFunction)GetProcAddress(ftdiLibInstance, "FT_Close");
|
||||
if (FT_CreateDeviceInfoList && FT_GetDeviceInfoList && FT_GetComPortNumber && FT_Open && FT_Close)
|
||||
if (FT_CreateDeviceInfoList && FT_GetDeviceInfoList)
|
||||
{
|
||||
DWORD numDevs;
|
||||
if ((FT_CreateDeviceInfoList(&numDevs) == FT_OK) && (numDevs > 0))
|
||||
|
@ -273,14 +278,9 @@ JNIEXPORT jobjectArray JNICALL Java_com_fazecast_jSerialComm_SerialPort_getCommP
|
|||
}
|
||||
|
||||
// Update the port description if not already open
|
||||
if (!isOpen)
|
||||
if (!isOpen && getPortPathFromSerial(comPort, devInfo[i].SerialNumber))
|
||||
{
|
||||
LONG comPortNumber = 0;
|
||||
if ((FT_Open(i, &devInfo[i].ftHandle) == FT_OK) && (FT_GetComPortNumber(devInfo[i].ftHandle, &comPortNumber) == FT_OK))
|
||||
{
|
||||
// Close port and check if actually connected and present in the port list
|
||||
FT_Close(devInfo[i].ftHandle);
|
||||
swprintf(comPort, sizeof(comPort) / sizeof(wchar_t), L"COM%ld", comPortNumber);
|
||||
// Check if actually connected and present in the port list
|
||||
for (int j = 0; j < serialPorts.length; ++j)
|
||||
if (wcscmp(serialPorts.ports[j]->portPath, comPort) == 0)
|
||||
{
|
||||
|
@ -299,7 +299,6 @@ JNIEXPORT jobjectArray JNICALL Java_com_fazecast_jSerialComm_SerialPort_getCommP
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
free(devInfo);
|
||||
}
|
||||
}
|
||||
|
@ -444,85 +443,8 @@ JNIEXPORT jlong JNICALL Java_com_fazecast_jSerialComm_SerialPort_openPortNative(
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Reduce the port's latency timer to minimum value of 2
|
||||
HKEY key, paramKey = 0;
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Enum\\FTDIBUS", 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD index = 0, subkeySize = 255, portNameSize = 16;
|
||||
wchar_t *subkey = (wchar_t*)malloc(subkeySize*sizeof(wchar_t)), *regPortName = (wchar_t*)malloc(portNameSize*sizeof(wchar_t));
|
||||
while (RegEnumKeyExW(key, index++, subkey, &subkeySize, NULL, NULL, NULL, NULL) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD latency = 2, oldLatency = 2, oldLatencySize = sizeof(DWORD);
|
||||
char *subkeyString = (char*)malloc(subkeySize + 2);
|
||||
memset(subkeyString, 0, subkeySize + 2);
|
||||
wcstombs(subkeyString, subkey, subkeySize + 1);
|
||||
subkeySize = 255;
|
||||
portNameSize = 16;
|
||||
memset(regPortName, 0, portNameSize * sizeof(wchar_t));
|
||||
wcscat_s(subkey, subkeySize, L"\\0000\\Device Parameters");
|
||||
if (RegOpenKeyExW(key, subkey, 0, KEY_QUERY_VALUE, ¶mKey) == ERROR_SUCCESS)
|
||||
{
|
||||
if ((RegQueryValueExW(paramKey, L"PortName", NULL, NULL, (LPBYTE)regPortName, &portNameSize) == ERROR_SUCCESS) && (wcscmp(port->portPath + 4, regPortName) == 0))
|
||||
RegQueryValueExW(paramKey, L"LatencyTimer", NULL, NULL, (LPBYTE)&oldLatency, &oldLatencySize);
|
||||
RegCloseKey(paramKey);
|
||||
}
|
||||
if (oldLatency > latency)
|
||||
{
|
||||
if (RegOpenKeyExW(key, subkey, 0, KEY_SET_VALUE, ¶mKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(paramKey, L"LatencyTimer", 0, REG_DWORD, (LPBYTE)&latency, sizeof(latency));
|
||||
RegCloseKey(paramKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create registry update file
|
||||
char *workingDirectory = _getcwd(NULL, 0);
|
||||
wchar_t *workingDirectoryWide = _wgetcwd(NULL, 0);
|
||||
int workingDirectoryLength = strlen(workingDirectory) + 1;
|
||||
char *registryFileName = (char*)malloc(workingDirectoryLength + 8);
|
||||
wchar_t *paramsString = (wchar_t*)malloc((workingDirectoryLength + 11) * sizeof(wchar_t));
|
||||
sprintf(registryFileName, "%s\\del.reg", workingDirectory);
|
||||
swprintf(paramsString, workingDirectoryLength + 11, L"/s %s\\del.reg", workingDirectoryWide);
|
||||
FILE *registryFile = fopen(registryFileName, "wb");
|
||||
if (registryFile)
|
||||
{
|
||||
fprintf(registryFile, "Windows Registry Editor Version 5.00\n\n");
|
||||
fprintf(registryFile, "[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Enum\\FTDIBUS\\%s\\0000\\Device Parameters]\n", subkeyString);
|
||||
fprintf(registryFile, "\"LatencyTimer\"=dword:00000002\n\n");
|
||||
fclose(registryFile);
|
||||
}
|
||||
|
||||
// Launch a new administrative process to update the registry value
|
||||
SHELLEXECUTEINFOW shExInfo = { 0 };
|
||||
shExInfo.cbSize = sizeof(shExInfo);
|
||||
shExInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
shExInfo.hwnd = NULL;
|
||||
shExInfo.lpVerb = L"runas";
|
||||
shExInfo.lpFile = L"C:\\Windows\\regedit.exe";
|
||||
shExInfo.lpParameters = paramsString;
|
||||
shExInfo.lpDirectory = NULL;
|
||||
shExInfo.nShow = SW_SHOW;
|
||||
shExInfo.hInstApp = 0;
|
||||
if (ShellExecuteExW(&shExInfo))
|
||||
{
|
||||
WaitForSingleObject(shExInfo.hProcess, INFINITE);
|
||||
CloseHandle(shExInfo.hProcess);
|
||||
}
|
||||
|
||||
// Delete the registry update file
|
||||
remove(registryFileName);
|
||||
free(workingDirectoryWide);
|
||||
free(workingDirectory);
|
||||
free(registryFileName);
|
||||
free(paramsString);
|
||||
}
|
||||
}
|
||||
free(subkeyString);
|
||||
}
|
||||
RegCloseKey(key);
|
||||
free(regPortName);
|
||||
free(subkey);
|
||||
}
|
||||
// Reduce the port's latency to its minimum value
|
||||
reduceLatencyToMinimum(port->portPath + 4);
|
||||
|
||||
// Try to open the serial port with read/write access
|
||||
if ((port->handle = CreateFileW(portName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_OVERLAPPED, NULL)) != INVALID_HANDLE_VALUE)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* WindowsHelperFunctions.c
|
||||
*
|
||||
* Created on: May 05, 2015
|
||||
* Last Updated on: Jan 06, 2022
|
||||
* Last Updated on: Jan 22, 2022
|
||||
* Author: Will Hedgecock
|
||||
*
|
||||
* Copyright (C) 2012-2022 Fazecast, Inc.
|
||||
|
@ -24,6 +24,13 @@
|
|||
*/
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WINVER _WIN32_WINNT_VISTA
|
||||
#define _WIN32_WINNT _WIN32_WINNT_VISTA
|
||||
#define NTDDI_VERSION NTDDI_VISTA
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <direct.h>
|
||||
#include <shellapi.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "WindowsHelperFunctions.h"
|
||||
|
@ -100,4 +107,124 @@ void removePort(serialPortVector* vector, serialPort* port)
|
|||
free(port);
|
||||
}
|
||||
|
||||
// Windows-specific functionality
|
||||
void reduceLatencyToMinimum(const wchar_t* portName)
|
||||
{
|
||||
// Search for this port in all FTDI enumerated ports
|
||||
HKEY key, paramKey = 0;
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Enum\\FTDIBUS", 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD index = 0, subkeySize = 255, portNameSize = 16;
|
||||
wchar_t *subkey = (wchar_t*)malloc(subkeySize*sizeof(wchar_t)), *regPortName = (wchar_t*)malloc(portNameSize*sizeof(wchar_t));
|
||||
while (RegEnumKeyExW(key, index++, subkey, &subkeySize, NULL, NULL, NULL, NULL) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD latency = 2, oldLatency = 2, oldLatencySize = sizeof(DWORD);
|
||||
char *subkeyString = (char*)malloc(subkeySize + 2);
|
||||
memset(subkeyString, 0, subkeySize + 2);
|
||||
wcstombs(subkeyString, subkey, subkeySize + 1);
|
||||
subkeySize = 255;
|
||||
portNameSize = 16;
|
||||
memset(regPortName, 0, portNameSize * sizeof(wchar_t));
|
||||
wcscat_s(subkey, subkeySize, L"\\0000\\Device Parameters");
|
||||
if (RegOpenKeyExW(key, subkey, 0, KEY_QUERY_VALUE, ¶mKey) == ERROR_SUCCESS)
|
||||
{
|
||||
if ((RegQueryValueExW(paramKey, L"PortName", NULL, NULL, (LPBYTE)regPortName, &portNameSize) == ERROR_SUCCESS) && (wcscmp(portName, regPortName) == 0))
|
||||
RegQueryValueExW(paramKey, L"LatencyTimer", NULL, NULL, (LPBYTE)&oldLatency, &oldLatencySize);
|
||||
RegCloseKey(paramKey);
|
||||
}
|
||||
if (oldLatency > latency)
|
||||
{
|
||||
if (RegOpenKeyExW(key, subkey, 0, KEY_SET_VALUE, ¶mKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(paramKey, L"LatencyTimer", 0, REG_DWORD, (LPBYTE)&latency, sizeof(latency));
|
||||
RegCloseKey(paramKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create registry update file
|
||||
char *workingDirectory = _getcwd(NULL, 0);
|
||||
wchar_t *workingDirectoryWide = _wgetcwd(NULL, 0);
|
||||
int workingDirectoryLength = strlen(workingDirectory) + 1;
|
||||
char *registryFileName = (char*)malloc(workingDirectoryLength + 8);
|
||||
wchar_t *paramsString = (wchar_t*)malloc((workingDirectoryLength + 11) * sizeof(wchar_t));
|
||||
sprintf(registryFileName, "%s\\del.reg", workingDirectory);
|
||||
swprintf(paramsString, workingDirectoryLength + 11, L"/s %s\\del.reg", workingDirectoryWide);
|
||||
FILE *registryFile = fopen(registryFileName, "wb");
|
||||
if (registryFile)
|
||||
{
|
||||
fprintf(registryFile, "Windows Registry Editor Version 5.00\n\n");
|
||||
fprintf(registryFile, "[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Enum\\FTDIBUS\\%s\\0000\\Device Parameters]\n", subkeyString);
|
||||
fprintf(registryFile, "\"LatencyTimer\"=dword:00000002\n\n");
|
||||
fclose(registryFile);
|
||||
}
|
||||
|
||||
// Launch a new administrative process to update the registry value
|
||||
SHELLEXECUTEINFOW shExInfo = { 0 };
|
||||
shExInfo.cbSize = sizeof(shExInfo);
|
||||
shExInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
shExInfo.hwnd = NULL;
|
||||
shExInfo.lpVerb = L"runas";
|
||||
shExInfo.lpFile = L"C:\\Windows\\regedit.exe";
|
||||
shExInfo.lpParameters = paramsString;
|
||||
shExInfo.lpDirectory = NULL;
|
||||
shExInfo.nShow = SW_SHOW;
|
||||
shExInfo.hInstApp = 0;
|
||||
if (ShellExecuteExW(&shExInfo))
|
||||
{
|
||||
WaitForSingleObject(shExInfo.hProcess, INFINITE);
|
||||
CloseHandle(shExInfo.hProcess);
|
||||
}
|
||||
|
||||
// Delete the registry update file
|
||||
remove(registryFileName);
|
||||
free(workingDirectoryWide);
|
||||
free(workingDirectory);
|
||||
free(registryFileName);
|
||||
free(paramsString);
|
||||
}
|
||||
}
|
||||
free(subkeyString);
|
||||
}
|
||||
RegCloseKey(key);
|
||||
free(regPortName);
|
||||
free(subkey);
|
||||
}
|
||||
}
|
||||
|
||||
int getPortPathFromSerial(wchar_t* portPath, const char* serialNumber)
|
||||
{
|
||||
// Search for this port in all FTDI enumerated ports
|
||||
int found = 0;
|
||||
HKEY key, paramKey = 0;
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Enum\\FTDIBUS", 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD index = 0, subkeySize = 255, portNameSize = 16;
|
||||
wchar_t *subkey = (wchar_t*)malloc(subkeySize * sizeof(wchar_t));
|
||||
while (RegEnumKeyExW(key, index++, subkey, &subkeySize, NULL, NULL, NULL, NULL) == ERROR_SUCCESS)
|
||||
{
|
||||
// Determine if this device matches the specified serial number
|
||||
char *subkeyString = (char*)malloc(subkeySize + 2);
|
||||
memset(subkeyString, 0, subkeySize + 2);
|
||||
wcstombs(subkeyString, subkey, subkeySize + 1);
|
||||
subkeySize = 255;
|
||||
if (strstr(subkeyString, serialNumber))
|
||||
{
|
||||
portNameSize = 16;
|
||||
memset(portPath, 0, portNameSize * sizeof(wchar_t));
|
||||
wcscat_s(subkey, subkeySize, L"\\0000\\Device Parameters");
|
||||
if (RegOpenKeyExW(key, subkey, 0, KEY_QUERY_VALUE, ¶mKey) == ERROR_SUCCESS)
|
||||
{
|
||||
if (RegQueryValueExW(paramKey, L"PortName", NULL, NULL, (LPBYTE)portPath, &portNameSize) == ERROR_SUCCESS)
|
||||
found = 1;
|
||||
RegCloseKey(paramKey);
|
||||
}
|
||||
}
|
||||
free(subkeyString);
|
||||
}
|
||||
RegCloseKey(key);
|
||||
free(subkey);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
* WindowsHelperFunctions.h
|
||||
*
|
||||
* Created on: May 05, 2015
|
||||
* Last Updated on: Dec 16, 2021
|
||||
* Last Updated on: Jan 21, 2022
|
||||
* Author: Will Hedgecock
|
||||
*
|
||||
* Copyright (C) 2012-2021 Fazecast, Inc.
|
||||
* Copyright (C) 2012-2022 Fazecast, Inc.
|
||||
*
|
||||
* This file is part of jSerialComm.
|
||||
*
|
||||
|
@ -50,4 +50,8 @@ serialPort* pushBack(serialPortVector* vector, const wchar_t* key, const wchar_t
|
|||
serialPort* fetchPort(serialPortVector* vector, const wchar_t* key);
|
||||
void removePort(serialPortVector* vector, serialPort* port);
|
||||
|
||||
// Windows-specific functionality
|
||||
void reduceLatencyToMinimum(const wchar_t* portName);
|
||||
int getPortPathFromSerial(wchar_t* portPath, const char* serialNumber);
|
||||
|
||||
#endif // #ifndef __WINDOWS_HELPER_FUNCTIONS_HEADER_H__
|
||||
|
|
|
@ -561,6 +561,11 @@ public final class SerialPort
|
|||
* <p>
|
||||
* All serial port parameters or timeouts can be changed at any time before or after the port has been opened.
|
||||
* <p>
|
||||
* Note on Windows using an FTDI device: The first time this method is called, you may be prompted to allow elevated privileges
|
||||
* so that the driver latency can be correctly specified. This should only be necessary the first time you use a new FTDI device.
|
||||
* Declining the elevated privileges will not affect the ability of the serial port to be accessed; however, read/write timing
|
||||
* may not be as expected.
|
||||
* <p>
|
||||
* Note that calling this method on an already opened port will simply reconfigure the port parameters.
|
||||
*
|
||||
* @param safetySleepTime The number of milliseconds to sleep before opening the port in case of frequent closing/openings.
|
||||
|
@ -631,6 +636,11 @@ public final class SerialPort
|
|||
* <p>
|
||||
* All serial port parameters or timeouts can be changed at any time before or after the port has been opened.
|
||||
* <p>
|
||||
* Note on Windows using an FTDI device: The first time this method is called, you may be prompted to allow elevated privileges
|
||||
* so that the driver latency can be correctly specified. This should only be necessary the first time you use a new FTDI device.
|
||||
* Declining the elevated privileges will not affect the ability of the serial port to be accessed; however, read/write timing
|
||||
* may not be as expected.
|
||||
* <p>
|
||||
* Note that calling this method on an already opened port will simply reconfigure the port parameters.
|
||||
*
|
||||
* @param safetySleepTime The number of milliseconds to sleep before opening the port in case of frequent closing/openings.
|
||||
|
@ -645,6 +655,11 @@ public final class SerialPort
|
|||
* <p>
|
||||
* All serial port parameters or timeouts can be changed at any time before or after the port has been opened.
|
||||
* <p>
|
||||
* Note on Windows using an FTDI device: The first time this method is called, you may be prompted to allow elevated privileges
|
||||
* so that the driver latency can be correctly specified. This should only be necessary the first time you use a new FTDI device.
|
||||
* Declining the elevated privileges will not affect the ability of the serial port to be accessed; however, read/write timing
|
||||
* may not be as expected.
|
||||
* <p>
|
||||
* Note that calling this method on an already opened port will simply reconfigure the port parameters.
|
||||
*
|
||||
* @return Whether the port was successfully opened with a valid configuration.
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue