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

в 6:44, , рубрики: .net, HID, scale, usb, Visual Basic .NET, метки: , , ,

Не так давно выиграл проект на 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

Автор: itsplus

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js