1a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)// Copyright 2014 The Chromium Authors. All rights reserved. 25d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 35d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// found in the LICENSE file. 45d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 55c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu#include <linux/hidraw.h> 65c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu#include <sys/ioctl.h> 75c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 8a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)#include <stdint.h> 9a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include <string> 115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 12a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch#include "base/bind.h" 135c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu#include "base/files/file_path.h" 145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "base/logging.h" 155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "base/platform_file.h" 16a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)#include "base/stl_util.h" 175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "base/strings/string_number_conversions.h" 185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "base/strings/string_piece.h" 195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "base/strings/string_split.h" 205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "device/hid/hid_connection_linux.h" 215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "device/hid/hid_device_info.h" 225c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu#include "device/hid/hid_report_descriptor.h" 235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "device/hid/hid_service_linux.h" 2446d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)#include "device/udev_linux/udev.h" 255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)namespace device { 275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)namespace { 295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)const char kHIDSubSystem[] = "hid"; 315c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liuconst char kHidrawSubsystem[] = "hidraw"; 325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)const char kHIDID[] = "HID_ID"; 335d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)const char kHIDName[] = "HID_NAME"; 345d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)const char kHIDUnique[] = "HID_UNIQ"; 355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 3646d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)} // namespace 375d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 385d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)HidServiceLinux::HidServiceLinux() { 39a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch DeviceMonitorLinux* monitor = DeviceMonitorLinux::GetInstance(); 40a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch monitor->AddObserver(this); 41a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch monitor->Enumerate( 42a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch base::Bind(&HidServiceLinux::OnDeviceAdded, base::Unretained(this))); 435d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)} 445d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 45a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)scoped_refptr<HidConnection> HidServiceLinux::Connect( 46a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) const HidDeviceId& device_id) { 47a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) HidDeviceInfo device_info; 48a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if (!GetDeviceInfo(device_id, &device_info)) 49a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return NULL; 50a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 51a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch ScopedUdevDevicePtr device = 52a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch DeviceMonitorLinux::GetInstance()->GetDeviceFromPath( 53a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch device_info.device_id); 545c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 555c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (device) { 565c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu std::string dev_node; 575c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (!FindHidrawDevNode(device.get(), &dev_node)) { 585c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu LOG(ERROR) << "Cannot open HID device as hidraw device."; 595c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return NULL; 605c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 615c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return new HidConnectionLinux(device_info, dev_node); 625c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 635c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 64a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return NULL; 65a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)} 66a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 675d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)HidServiceLinux::~HidServiceLinux() { 68a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch if (DeviceMonitorLinux::HasInstance()) 69a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch DeviceMonitorLinux::GetInstance()->RemoveObserver(this); 705d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)} 715d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 72a02191e04bc25c4935f804f2c080ae28663d096dBen Murdochvoid HidServiceLinux::OnDeviceAdded(udev_device* device) { 735d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (!device) 745d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 755d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 76a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) const char* device_path = udev_device_get_syspath(device); 77a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if (!device_path) 785d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 79a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch const char* subsystem = udev_device_get_subsystem(device); 80a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch if (!subsystem || strcmp(subsystem, kHIDSubSystem) != 0) 81a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch return; 825d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 835d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) HidDeviceInfo device_info; 84a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) device_info.device_id = device_path; 855d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 86a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) uint32_t int_property = 0; 875d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) const char* str_property = NULL; 885d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 895d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) const char* hid_id = udev_device_get_property_value(device, kHIDID); 905d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (!hid_id) 915d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 925d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 935d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) std::vector<std::string> parts; 945d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) base::SplitString(hid_id, ':', &parts); 955d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (parts.size() != 3) { 965d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 975d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 985d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 995d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (HexStringToUInt(base::StringPiece(parts[1]), &int_property)) { 1005d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) device_info.vendor_id = int_property; 1015d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1025d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1035d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (HexStringToUInt(base::StringPiece(parts[2]), &int_property)) { 1045d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) device_info.product_id = int_property; 1055d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1065d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1075d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) str_property = udev_device_get_property_value(device, kHIDUnique); 1085d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (str_property != NULL) 1095d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) device_info.serial_number = str_property; 1105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) str_property = udev_device_get_property_value(device, kHIDName); 1125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (str_property != NULL) 1135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) device_info.product_name = str_property; 1145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1155c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu std::string dev_node; 1165c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (!FindHidrawDevNode(device, &dev_node)) { 1176d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles) LOG(ERROR) << "Cannot find device node for HID device."; 1185c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return; 1195c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1205c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1215c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu int flags = base::File::FLAG_OPEN | base::File::FLAG_READ; 1225c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1235c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu base::File device_file(base::FilePath(dev_node), flags); 1245c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (!device_file.IsValid()) { 1256d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles) LOG(ERROR) << "Cannot open '" << dev_node << "': " 1266d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles) << base::File::ErrorToString(device_file.error_details()); 1275c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return; 1285c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1295c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1305c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu int desc_size = 0; 1315c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu int res = ioctl(device_file.GetPlatformFile(), HIDIOCGRDESCSIZE, &desc_size); 1325c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (res < 0) { 1335c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu LOG(ERROR) << "HIDIOCGRDESCSIZE failed."; 1345c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu device_file.Close(); 1355c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return; 1365c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1375c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1385c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu hidraw_report_descriptor rpt_desc; 1395c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu rpt_desc.size = desc_size; 1405c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1415c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu res = ioctl(device_file.GetPlatformFile(), HIDIOCGRDESC, &rpt_desc); 1425c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (res < 0) { 1435c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu LOG(ERROR) << "HIDIOCGRDESC failed."; 1445c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu device_file.Close(); 1455c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return; 1465c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1475c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1485c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu device_file.Close(); 1495c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1505c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu HidReportDescriptor report_descriptor(rpt_desc.value, rpt_desc.size); 1515c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu report_descriptor.GetTopLevelCollections(&device_info.usages); 1525c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1535d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) AddDevice(device_info); 1545d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)} 1555d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 156a02191e04bc25c4935f804f2c080ae28663d096dBen Murdochvoid HidServiceLinux::OnDeviceRemoved(udev_device* device) { 157a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch const char* device_path = udev_device_get_syspath(device);; 158a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch if (device_path) 159a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch RemoveDevice(device_path); 1605d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)} 1615d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1625c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liubool HidServiceLinux::FindHidrawDevNode(udev_device* parent, 1635c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu std::string* result) { 1645c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu udev* udev = udev_device_get_udev(parent); 1655c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (!udev) { 1665c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return false; 1675c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1685c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu ScopedUdevEnumeratePtr enumerate(udev_enumerate_new(udev)); 1695c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (!enumerate) { 1705c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return false; 1715c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1725c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (udev_enumerate_add_match_subsystem(enumerate.get(), kHidrawSubsystem)) { 1735c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return false; 1745c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1755c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (udev_enumerate_scan_devices(enumerate.get())) { 1765c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return false; 1775c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1785c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu std::string parent_path(udev_device_get_devpath(parent)); 1795c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (parent_path.length() == 0 || *parent_path.rbegin() != '/') 1805c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu parent_path += '/'; 1815c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu udev_list_entry* devices = udev_enumerate_get_list_entry(enumerate.get()); 1825c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu for (udev_list_entry* i = devices; i != NULL; 1835c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu i = udev_list_entry_get_next(i)) { 1845c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu ScopedUdevDevicePtr hid_dev( 1855c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu udev_device_new_from_syspath(udev, udev_list_entry_get_name(i))); 1865c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu const char* raw_path = udev_device_get_devnode(hid_dev.get()); 1875c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu std::string device_path = udev_device_get_devpath(hid_dev.get()); 1885c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (raw_path && 1895c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu !device_path.compare(0, parent_path.length(), parent_path)) { 1905c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu std::string sub_path = device_path.substr(parent_path.length()); 1915c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (sub_path.substr(0, sizeof(kHidrawSubsystem) - 1) == 1925c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu kHidrawSubsystem) { 1935c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu *result = raw_path; 1945c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return true; 1955c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1965c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1975c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1985c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 1995c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return false; 2005c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu} 2015c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu 20246d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)} // namespace device 203