Чтение данных с весов Mettler Toledo PS60

от автора

Не так давно выиграл проект на Elance — сделать простое WinForms приложение на Visual Basic, которое будет отображать данные с весов Mettler Toledo PS60.
К счастью, данные весы являются HID-устройством, подключаемом по USB.
В этом посте я опишу как работать с подобными HID устройствами в Visual Basic (да и вообще в .Net)

Поискал немного гуглом, нашел несколько интересных ссылок.
В основном, рекомендуют использовать библиотеку «Mike O’Brien’s USB HID library».
Вот статья, в которой с использованием этой библиотеки читают данные с похожих весов:
nicholas.piasecki.name/blog/2008/11/reading-a-stamps-com-usb-scale-from-c-sharp/
Что мне не понравилось — это угадывание формата данных. К тому же, поделившись ссылкой с заказчиком, получил ответ, что вся библиотека ему не нужна и вообще он бы предпочел чтобы я решил проблему самостоятельно.
Хорошо, вооружаемся MSDN а также спецификацией на весы:
«64067860 PS scales Operation and Technical Manual.pdf» — легко ищется гуглом.

Чтение данных с HID устройства, если не требуются какие-то особенные сложности (попробую позже написать про чтение ACS NFC SmartCard Reader), довольно просто:
1) нужно получить DevicePath нужного нам устройства, вот такого вида:
(\?\usb#vid_04a9&pid_1097#207946#{28d78fad-5a12-11d1-ae5b-0000f803a8c2})
2) открываем этот DevicePath с помощью самой обычной функции CreateFile с доступом GENERIC_READ

NativeMethods.CreateFile(DeviceInterfaceDetailData.DevicePath, NativeMethods.GENERIC_READ, NativeMethods.FILE_SHARE_READ + NativeMethods.FILE_SHARE_WRITE, security, NativeMethods.OPEN_EXISTING, 0, 0) 

3) Читаем с помощью ReadFile

res = NativeMethods.ReadFile(ioHandle, bufPtr, 10, bytesRead, IntPtr.Zero) 

Как получить DevicePath. Задача несложная. Нужно получить список всех устройств, найти весы, и считать структуру HIDD_ATTRIBUTES при помощи функции HidD_GetAttributes(hidHandle, deviceAttributes)
По шагам:
1) Получаем Guid класса устройств

NativeMethods.HidD_GetHidGuid(hidClass) 

2) Создаем Enumerator для класса устройств

DeviceInfoSet = NativeMethods.SetupDiGetClassDevs(hidClass, IntPtr.Zero, 0, NativeMethods.DIGCF_PRESENT + NativeMethods.DIGCF_DEVICEINTERFACE) 

3) Идем по списку устройств

Do While NativeMethods.SetupDiEnumDeviceInfo(DeviceInfoSet, deviceIndex, DeviceInfoData) 

4) Вложенным циклом идем по списку интерфейсов устройства

Do While NativeMethods.SetupDiEnumDeviceInterfaces(DeviceInfoSet, DeviceInfoData, hidClass, deviceIfaceIndex, DeviceInterfaceData) 

5) Получаем DevicePath

                success = NativeMethods.SetupDiGetDeviceInterfaceDetailBuffer(DeviceInfoSet, DeviceInterfaceData, IntPtr.Zero, 0, RequiredSize, IntPtr.Zero) ' Obtain buffer size                 success = NativeMethods.SetupDiGetDeviceInterfaceDetail(DeviceInfoSet, DeviceInterfaceData, DeviceInterfaceDetailData, RequiredSize, RequiredSize, DeviceInfoData) ' Get device information using previously recieved buffer size 

Здесь небольшое ухищрение с передачей null для того, чтобы получить правильный размер буфера для данных. Половина задачи сделана — у нас есть DevicePath.
6) Теперь нужно понять, то ли это устройство.

NativeMethods.CreateFile(DeviceInterfaceDetailData.DevicePath, NativeMethods.ACCESS_NONE, NativeMethods.FILE_SHARE_READ + NativeMethods.FILE_SHARE_WRITE, security, NativeMethods.OPEN_EXISTING, 0, 0) 

Открываем устройство с ACCESS_NONE (нам нужно только pid&vid, для этого нет нужды открывать устройство на чтение, большинство устройств нам этого не позволят и здесь будет исключение)
7) И читаем атрибуты

                    Dim deviceAttributes As NativeMethods.HIDD_ATTRIBUTES                     deviceAttributes.cbSize = Marshal.SizeOf(deviceAttributes)                      NativeMethods.HidD_GetAttributes(hidHandle, deviceAttributes) 

8) Теперь только сравним deviceAttributes.VendorID и deviceAttributes.ProductID с константами и если это то что нужно — можно выходить из циклов

Теперь собственно к весам. При чтении данных они выдают нам 6 байт, с которыми нужно разобраться.
Согласно спецификации, первый байт посылки — это report id.
Второй — статус измерения. Бывает: ошибка, стабильный вес, меньше нуля, колебания, и т.п. Полный список — в коде и в спецификации.
Третий — единицы измерения. Это понятно — милиграммы, граммы, килограммы, и т.д. Хоть тройская унция.
Три следующих байта — это собственно вес.
Первый байт веса — степень десятки, следующие два — собственно значение.
Таким образом, чтобы получить значение веса, нужно сделать нехитрую операцию:
(b[5]*256+b[4])*10^b[3]

Вот так — все довольно просто.

Исходный код:

NativeMethods.vb

Public Class NativeMethods     Public Const DIGCF_PRESENT = &H2     Public Const DIGCF_DEVICEINTERFACE = &H10      Public Const FILE_FLAG_OVERLAPPED = &H40000000     Public Const FILE_SHARE_READ = 1     Public Const FILE_SHARE_WRITE = 2     Public Const GENERIC_READ = &H80000000     Public Const GENERIC_WRITE = &H40000000     Public Const ACCESS_NONE = 0     Public Const INVALID_HANDLE_VALUE = -1 	Public Const OPEN_EXISTING = 3      <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _     Public Structure SP_DEVICE_INTERFACE_DETAIL_DATA         Public cbSize As UInt32         <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=256)> _         Public DevicePath As String     End Structure      Public Structure SP_DEVICE_INTERFACE_DATA         Public cbSize As Integer         Public InterfaceClassGuid As System.Guid         Public Flags As Integer         Public Reserved As UIntPtr     End Structure      Public Structure SP_DEVINFO_DATA         Public cbSize As Integer         Public ClassGuid As System.Guid         Public DevInst As Integer         Public Reserved As UIntPtr     End Structure      Public Const HIDP_INPUT = 0     Public Const HIDP_OUTPUT = 1     Public Const HIDP_FEATURE = 2      Public Structure HIDD_ATTRIBUTES         Public cbSize As Integer         Public VendorID As UShort         Public ProductID As UShort         Public VersionNumber As Short     End Structure      Public Structure SECURITY_ATTRIBUTES         Public nLength As Integer         Public lpSecurityDescriptor As IntPtr         Public bInheritHandle As Boolean     End Structure      Public Declare Auto Function CreateFile Lib "kernel32.dll" (lpFileName As String, dwDesiredAccess As Integer, dwShareMode As Integer, ByRef lpSecurityAttributes As SECURITY_ATTRIBUTES, dwCreationDisposition As Integer, dwFlagsAndAttributes As Integer, hTemplateFile As Integer) As IntPtr     Public Declare Auto Function ReadFile Lib "kernel32.dll" (ByVal hFile As IntPtr, ByVal Buffer As IntPtr, ByVal nNumberOfBytesToRead As Integer, ByRef lpNumberOfBytesRead As Integer, ByVal Overlapped As IntPtr) As Integer     Public Declare Auto Function CloseHandle Lib "kernel32.dll" (hObject As IntPtr) As Boolean      Public Declare Auto Function SetupDiGetClassDevs Lib "setupapi.dll" (ByRef ClassGuid As System.Guid, ByVal Enumerator As Integer, ByVal hwndParent As IntPtr, ByVal Flags As Integer) As IntPtr     Public Declare Auto Function SetupDiDestroyDeviceInfoList Lib "setupapi.dll" (deviceInfoSet As IntPtr) As Boolean      Public Declare Auto Function SetupDiEnumDeviceInfo Lib "setupapi.dll" (ByVal DeviceInfoSet As Integer, ByVal MemberIndex As Integer, ByRef DeviceInfoData As SP_DEVINFO_DATA) As Boolean     Public Declare Auto Function SetupDiEnumDeviceInterfaces Lib "setupapi.dll" (ByVal DeviceInfoSet As IntPtr, ByRef DeviceInfoData As SP_DEVINFO_DATA, ByRef InterfaceClassGuid As System.Guid, ByVal MemberIndex As UInteger, ByRef DeviceInterfaceData As SP_DEVICE_INTERFACE_DATA) As Boolean      Public Declare Auto Function SetupDiGetDeviceInterfaceDetailBuffer Lib "setupapi.dll" Alias "SetupDiGetDeviceInterfaceDetail" (ByVal DeviceInfoSet As IntPtr, ByRef DeviceInterfaceData As SP_DEVICE_INTERFACE_DATA, ByVal DeviceInterfaceDetailData As IntPtr, ByVal DeviceInterfaceDetailDataSize As Integer, ByRef RequiredSize As Integer, ByRef DeviceInfoData As IntPtr) As Boolean     Public Declare Auto Function SetupDiGetDeviceInterfaceDetail Lib "setupapi.dll" (ByVal DeviceInfoSet As IntPtr, ByRef DeviceInterfaceData As SP_DEVICE_INTERFACE_DATA, ByRef DeviceInterfaceDetailData As SP_DEVICE_INTERFACE_DETAIL_DATA, ByVal DeviceInterfaceDetailDataSize As Integer, ByRef RequiredSize As Integer, ByRef DeviceInfoData As SP_DEVINFO_DATA) As Boolean       Public Declare Auto Sub HidD_GetHidGuid Lib "hid.dll" Alias "HidD_GetHidGuid" (ByRef hidGuid As Guid)     Public Declare Auto Function HidD_GetAttributes Lib "hid.dll" (hidDeviceObject As IntPtr, ByRef attributes As HIDD_ATTRIBUTES) As Boolean  End Class 

ScaleReader.vb

Public Class ScaleReader     Private Const VendorId = &HEB8      ' 0EB8 = Toledo, see http://usb-ids.gowdy.us/read/UD/     Private Const ProductId = &HF000    ' F000 = PS60      ' Scale status enumeration     Public Enum ScaleStatus         Fault         StableAtZero         InMotion         WeightStable         UnderZero         OverWeight         RequiresCalibration         RequiresRezeroing         RequiresGEO         Unknown     End Enum      ' Scale weighing unit     Public Enum WeightUnit         UnitMilligram         UnitGram         UnitKilogram         UnitCarats         UnitTaels         UnitGrains         UnitPennyweights         UnitMetricTon         UnitAvoirTon         UnitTroyOunce         UnitOunce         UnitPound         UnitUnknown     End Enum      ' Scale measure report     Public Structure ScaleReport         Public ReportId As UShort       ' Scale report id         Public Status As ScaleStatus    ' Scale status         Public Unit As WeightUnit       ' Weighing unit         Public Scaling As SByte          ' Scaling, power of 10         Public WeightLsb As UShort      ' Least-significant byte of weight value         Public WeightMsb As UShort      ' Most-significant byte of weight value         Public ErrorCode As Integer     ' Error code          ' Calculates weight from LSB, MSB and scaling         Public Function GetWeight() As Double             GetWeight = (WeightMsb * 256 + WeightLsb) * (10 ^ Scaling)         End Function      End Structure      Private ioHandle As IntPtr  ' handle to read from device      ' Opens device with desired access rights     Private Function OpenDeviceIO(devicePath As String, deviceAccess As Integer) As IntPtr         Dim security As NativeMethods.SECURITY_ATTRIBUTES          security.lpSecurityDescriptor = IntPtr.Zero         security.bInheritHandle = True         security.nLength = Marshal.SizeOf(security)          OpenDeviceIO = NativeMethods.CreateFile(devicePath, deviceAccess, NativeMethods.FILE_SHARE_READ + NativeMethods.FILE_SHARE_WRITE, security, NativeMethods.OPEN_EXISTING, 0, 0)     End Function      ' Close previously opened device     Private Sub CloseDeviceIO(handle As IntPtr)         NativeMethods.CloseHandle(handle)     End Sub      ' Disconnect from scale     Public Sub Disconnect()         CloseDeviceIO(ioHandle)     End Sub      ' Find Toledo PS60 scale and open to read weight values     Public Function Connect() As Boolean         Dim hidClass As Guid         NativeMethods.HidD_GetHidGuid(hidClass) ' Obtain hid device class Guid to enumerate all hid devices          Dim DeviceInfoSet As IntPtr         Dim DeviceInfoData As NativeMethods.SP_DEVINFO_DATA         Dim DeviceInterfaceData As NativeMethods.SP_DEVICE_INTERFACE_DATA         Dim DeviceInterfaceDetailData As NativeMethods.SP_DEVICE_INTERFACE_DETAIL_DATA = Nothing         Dim RequiredSize As Integer         Dim success As Boolean          DeviceInfoSet = NativeMethods.SetupDiGetClassDevs(hidClass, IntPtr.Zero, 0, NativeMethods.DIGCF_PRESENT + NativeMethods.DIGCF_DEVICEINTERFACE) ' Open hid device enumeration          DeviceInterfaceData.cbSize = Marshal.SizeOf(DeviceInterfaceData)         DeviceInterfaceDetailData.cbSize = 6         DeviceInfoData.cbSize = Marshal.SizeOf(DeviceInfoData)          Dim deviceIndex As Integer ' Current deviec index         deviceIndex = 0          Do While NativeMethods.SetupDiEnumDeviceInfo(DeviceInfoSet, deviceIndex, DeviceInfoData) ' Loop through all hid devices             Dim deviceIfaceIndex As Integer ' Device interface index             deviceIfaceIndex = 0             Do While NativeMethods.SetupDiEnumDeviceInterfaces(DeviceInfoSet, DeviceInfoData, hidClass, deviceIfaceIndex, DeviceInterfaceData) ' Loop through all interfaces of current device                 success = NativeMethods.SetupDiGetDeviceInterfaceDetailBuffer(DeviceInfoSet, DeviceInterfaceData, IntPtr.Zero, 0, RequiredSize, IntPtr.Zero) ' Obtain buffer size                 success = NativeMethods.SetupDiGetDeviceInterfaceDetail(DeviceInfoSet, DeviceInterfaceData, DeviceInterfaceDetailData, RequiredSize, RequiredSize, DeviceInfoData) ' Get device information using previously recieved buffer size                  Dim hidHandle As IntPtr                 hidHandle = OpenDeviceIO(DeviceInterfaceDetailData.DevicePath, NativeMethods.ACCESS_NONE) ' Open device with no access rights to get pid&vid                  If hidHandle <> NativeMethods.INVALID_HANDLE_VALUE Then                      Dim deviceAttributes As NativeMethods.HIDD_ATTRIBUTES                     deviceAttributes.cbSize = Marshal.SizeOf(deviceAttributes)                      success = NativeMethods.HidD_GetAttributes(hidHandle, deviceAttributes) ' Read device attributes, including PID, VID and Version                      If success And deviceAttributes.VendorID = VendorId And deviceAttributes.ProductID = ProductId Then ' If it matches Toledo PS60                         CloseDeviceIO(hidHandle)    ' Close device                         ioHandle = OpenDeviceIO(DeviceInterfaceDetailData.DevicePath, NativeMethods.GENERIC_READ) ' And reopen with access rights to read reports                         NativeMethods.SetupDiDestroyDeviceInfoList(DeviceInfoSet) ' Close enumeration                         Connect = True                         Exit Function                     End If                      CloseDeviceIO(hidHandle)                 End If                 deviceIfaceIndex = deviceIfaceIndex + 1             Loop              deviceIndex = deviceIndex + 1         Loop         NativeMethods.SetupDiDestroyDeviceInfoList(DeviceInfoSet) ' Close enumeration          Connect = False     End Function      ' Reads current weight from scale     Public Function ReadValue() As ScaleReport         Dim bytesRead As Integer         Dim buffer(10) As Byte         Dim bufPtr As IntPtr          bufPtr = Marshal.AllocHGlobal(10) ' Allocate 10 bytes for report          ReadValue = Nothing          Dim res As Integer         res = NativeMethods.ReadFile(ioHandle, bufPtr, 10, bytesRead, IntPtr.Zero) ' Read 10 bytes from scale          If res > 0 Then ' 0=Failure, any positive is success             Marshal.Copy(bufPtr, buffer, 0, 10) ' Copy unmamanged buffer to managed byte array             If bytesRead < 6 Then ' Report must be 6 bytes or greater (for compatibility)                 ReadValue.Status = ScaleStatus.Fault                 Marshal.FreeHGlobal(bufPtr)                 Exit Function             End If              Dim rep As ScaleReport              rep.ReportId = buffer(0) ' byte #0 is report id             Select Case buffer(1)    ' byte #1 is scale status                 Case &H1                     rep.Status = ScaleStatus.Fault                 Case &H2                     rep.Status = ScaleStatus.StableAtZero                 Case &H3                     rep.Status = ScaleStatus.InMotion                 Case &H4                     rep.Status = ScaleStatus.WeightStable                 Case &H5                     rep.Status = ScaleStatus.UnderZero                 Case &H6                     rep.Status = ScaleStatus.OverWeight                 Case &H7                     rep.Status = ScaleStatus.RequiresCalibration                 Case &H8                     rep.Status = ScaleStatus.RequiresRezeroing                 Case &H9                     rep.Status = ScaleStatus.RequiresGEO                 Case Else                     rep.Status = ScaleStatus.Unknown             End Select              Select Case buffer(2)       ' byte #2 is scale unit                 Case &H1                     rep.Unit = WeightUnit.UnitMilligram                 Case &H2                     rep.Unit = WeightUnit.UnitGram                 Case &H3                     rep.Unit = WeightUnit.UnitKilogram                 Case &H4                     rep.Unit = WeightUnit.UnitCarats                 Case &H5                     rep.Unit = WeightUnit.UnitTaels                 Case &H6                     rep.Unit = WeightUnit.UnitGrains                 Case &H7                     rep.Unit = WeightUnit.UnitPennyweights                 Case &H8                     rep.Unit = WeightUnit.UnitMetricTon                 Case &H9                     rep.Unit = WeightUnit.UnitAvoirTon                 Case &HA                     rep.Unit = WeightUnit.UnitTroyOunce                 Case &HB                     rep.Unit = WeightUnit.UnitOunce                 Case &HC                     rep.Unit = WeightUnit.UnitPound                 Case Else                     rep.Unit = WeightUnit.UnitUnknown             End Select              rep.Scaling = IIf(buffer(3) < 128, buffer(3), buffer(3) - 256)     ' byte #3 is scaling             rep.WeightLsb = buffer(4)   ' byte #4 is LSB             rep.WeightMsb = buffer(5)   ' byte #5 is MSB              ReadValue = rep         Else             Dim err = Marshal.GetLastWin32Error             ReadValue.Status = ScaleStatus.Fault             ReadValue.ErrorCode = err         End If         Marshal.FreeHGlobal(bufPtr)     End Function   End Class 

ссылка на оригинал статьи http://habrahabr.ru/post/205462/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *