Implement HID report reading.
This commit is contained in:
parent
2594d20bcf
commit
ebbfd4b336
10
README.md
Normal file
10
README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Installation
|
||||
|
||||
## Windows
|
||||
|
||||
## Linux
|
||||
|
||||
Install the following libraries:
|
||||
|
||||
- libusb-dev
|
||||
- libudev-dev
|
BIN
doc/Device Class Definition for HID.pdf
Normal file
BIN
doc/Device Class Definition for HID.pdf
Normal file
Binary file not shown.
97388
doc/HID Usage Tables.pdf
Normal file
97388
doc/HID Usage Tables.pdf
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,52 +2,23 @@
|
||||
|
||||
use hidapi::HidApi;
|
||||
|
||||
//use donten::hid;
|
||||
|
||||
struct Report {
|
||||
device:usize,
|
||||
interface:usize,
|
||||
data:Vec<u8>,
|
||||
}
|
||||
|
||||
struct Usage {
|
||||
usage_page:u16,
|
||||
usage:u16,
|
||||
|
||||
offset:usize,
|
||||
bit_length:usize,
|
||||
|
||||
minimum:u32,
|
||||
maximum:u32,
|
||||
}
|
||||
|
||||
struct ReportDescriptor {
|
||||
id:usize,
|
||||
usages:Vec<Usage>,
|
||||
data:Vec<u8>,
|
||||
}
|
||||
|
||||
struct DeviceInterface {
|
||||
reports:Vec<ReportDescriptor>,
|
||||
}
|
||||
use donten::hid;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Device {
|
||||
product:String,
|
||||
manufacturer:String,
|
||||
serial:String,
|
||||
|
||||
interfaces:Vec<DeviceInterface>,
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
let (_tx, _rx) = crossbeam::channel::bounded::<Vec<u8>>(100);
|
||||
let (tx, rx) = crossbeam::channel::bounded::<Vec<u8>>(100);
|
||||
|
||||
if let Ok(hid) = HidApi::new() {
|
||||
|
||||
let mut devices = Vec::<Device>::new();
|
||||
|
||||
println!("[Device List]");
|
||||
for device_info in hid.device_list() {
|
||||
|
||||
let product = if let Some(s) = device_info.product_string() { s } else { "" }.to_string();
|
||||
@ -72,61 +43,94 @@ fn main()
|
||||
product,
|
||||
manufacturer,
|
||||
serial,
|
||||
|
||||
interfaces:Vec::new(),
|
||||
};
|
||||
|
||||
println!(" - {} ({})",
|
||||
/*println!("{} ({})",
|
||||
device.product,
|
||||
device.manufacturer,
|
||||
);
|
||||
);*/
|
||||
|
||||
devices.push(device);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
// Add usage to device.
|
||||
let mut desc_bytes = [0u8; hidapi::MAX_REPORT_DESCRIPTOR_SIZE];
|
||||
|
||||
//println!("## {} ##", devices[index].product);
|
||||
|
||||
if let Ok(hwd) = device_info.open_device(&hid) {
|
||||
if let Ok(desc_size) = hwd.get_report_descriptor(&mut desc_bytes) {
|
||||
|
||||
if let Ok(descriptor) = hid::ReportDescriptor::parse(&desc_bytes[0..desc_size]) {
|
||||
let interface = DeviceInterface {
|
||||
reports:Vec::new(),
|
||||
};
|
||||
let mut buffer_length = 0;
|
||||
|
||||
// Push interface to device.
|
||||
let interface_index = devices[index].interfaces.len();
|
||||
devices[index].interfaces.push(interface);
|
||||
/*println!("[INPUT]");
|
||||
for id in &descriptor.inputs {
|
||||
let interface = &descriptor.interfaces[*id];
|
||||
|
||||
let (usage_page, usage) = if let Some(page) = hid::consts::USAGES.get(interface.usage_page) {
|
||||
(page.name.to_string(), if let Some(usage) = page.get(interface.usage) {
|
||||
usage.name.to_string()
|
||||
} else { format!("Unknown({})", interface.usage) })
|
||||
} else {(
|
||||
format!("Unknown({})", interface.usage_page),
|
||||
format!("Unknown({})", interface.usage)
|
||||
)};
|
||||
|
||||
println!(" > {} / {} [{:02x}:{:02x}]",
|
||||
usage_page,
|
||||
usage,
|
||||
interface.collection,
|
||||
interface.index,
|
||||
);
|
||||
}*/
|
||||
|
||||
// Listen for input reports from device.
|
||||
let _ttx = tx.clone();
|
||||
let device = devices[index].clone();
|
||||
std::thread::spawn(move || {
|
||||
let _device_index = index;
|
||||
let _interface_index = interface_index;
|
||||
let buffer_length = buffer_length;
|
||||
|
||||
let mut descriptor = descriptor;
|
||||
let hwd = hwd;
|
||||
|
||||
let mut buffer_length = 0;
|
||||
for report in &descriptor.reports {
|
||||
buffer_length = buffer_length.max(1 + (report.length / 8));
|
||||
}
|
||||
|
||||
hwd.set_blocking_mode(true).ok();
|
||||
|
||||
let mut buffer = vec![0u8; buffer_length];
|
||||
while let Ok(_size) = hwd.read(&mut buffer) {
|
||||
loop {
|
||||
match hwd.read(&mut buffer) {
|
||||
Ok(size) => {
|
||||
println!("report {}", device.product);
|
||||
|
||||
if let Ok(changes) = descriptor.read_report(&buffer[..size]) {
|
||||
for change in changes {
|
||||
println!(" - change {}", change);
|
||||
}
|
||||
} else {
|
||||
println!("Failed report read.");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("error ({}): {}", device.product, e.to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("drop {}", device.product);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
// Process input reports.
|
||||
/*while let Ok(_msg) = rx.recv() {
|
||||
while let Ok(_msg) = rx.recv() {
|
||||
|
||||
}*/
|
||||
}
|
||||
} else {
|
||||
println!("failed to init hidapi.");
|
||||
}
|
||||
|
@ -0,0 +1,668 @@
|
||||
pub struct UsageCollection {
|
||||
pages:&'static [UsagePage],
|
||||
}
|
||||
impl UsageCollection {
|
||||
pub fn get(&self, id:u16) -> Option<&'static UsagePage>
|
||||
{
|
||||
for page in self.pages {
|
||||
if page.id == id {
|
||||
return Some(page);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UsagePage {
|
||||
pub id:u16,
|
||||
pub name:&'static str,
|
||||
|
||||
usages:&'static [Usage],
|
||||
}
|
||||
impl UsagePage {
|
||||
pub fn get(&self, id:u16) -> Option<&'static Usage>
|
||||
{
|
||||
for usage in self.usages {
|
||||
if usage.id == id {
|
||||
return Some(usage);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Usage {
|
||||
pub id:u16,
|
||||
pub usage_type:UsageType,
|
||||
pub name:&'static str,
|
||||
}
|
||||
|
||||
pub enum UsageType {
|
||||
Any,
|
||||
Physical,
|
||||
Application,
|
||||
Value,
|
||||
Toggle,
|
||||
Button,
|
||||
}
|
||||
|
||||
pub const USAGES :UsageCollection = UsageCollection {
|
||||
pages:&[
|
||||
UsagePage {
|
||||
id:0x0001,
|
||||
name:"Generic Desktop",
|
||||
usages:&[
|
||||
Usage {
|
||||
id:0x0001,
|
||||
usage_type:UsageType::Physical,
|
||||
name:"Pointer",
|
||||
},
|
||||
Usage {
|
||||
id:0x0002,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Mouse",
|
||||
},
|
||||
Usage {
|
||||
id:0x0004,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Joystick",
|
||||
},
|
||||
Usage {
|
||||
id:0x0005,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Gamepad",
|
||||
},
|
||||
Usage {
|
||||
id:0x0006,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Keyboard",
|
||||
},
|
||||
Usage {
|
||||
id:0x0007,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Keypad",
|
||||
},
|
||||
Usage {
|
||||
id:0x0008,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Multi-axis Controller",
|
||||
},
|
||||
Usage {
|
||||
id:0x0009,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Tablet PC",
|
||||
},
|
||||
Usage {
|
||||
id:0x000E,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Spatial Controller",
|
||||
},
|
||||
|
||||
Usage {
|
||||
id:0x0030,
|
||||
usage_type:UsageType::Value,
|
||||
name:"X",
|
||||
},
|
||||
Usage {
|
||||
id:0x0031,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Y",
|
||||
},
|
||||
Usage {
|
||||
id:0x0032,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Z",
|
||||
},
|
||||
Usage {
|
||||
id:0x0033,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Rx",
|
||||
},
|
||||
Usage {
|
||||
id:0x0034,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Ry",
|
||||
},
|
||||
Usage {
|
||||
id:0x0035,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Rz",
|
||||
},
|
||||
Usage {
|
||||
id:0x0036,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Slider",
|
||||
},
|
||||
Usage {
|
||||
id:0x0037,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Dial",
|
||||
},
|
||||
Usage {
|
||||
id:0x0038,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Wheel",
|
||||
},
|
||||
Usage {
|
||||
id:0x0039,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Hat Switch",
|
||||
},
|
||||
Usage {
|
||||
id:0x003D,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Start",
|
||||
},
|
||||
Usage {
|
||||
id:0x003E,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Select",
|
||||
},
|
||||
Usage {
|
||||
id:0x0040,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Vx",
|
||||
},
|
||||
Usage {
|
||||
id:0x0041,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Vy",
|
||||
},
|
||||
Usage {
|
||||
id:0x0042,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Vz",
|
||||
},
|
||||
Usage {
|
||||
id:0x0043,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Vbx",
|
||||
},
|
||||
Usage {
|
||||
id:0x0044,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Vby",
|
||||
},
|
||||
Usage {
|
||||
id:0x0045,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Vbz",
|
||||
},
|
||||
Usage {
|
||||
id:0x0046,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Vno",
|
||||
},
|
||||
Usage {
|
||||
id:0x0048,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Resolution Multiplier",
|
||||
},
|
||||
Usage {
|
||||
id:0x0049,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Qx",
|
||||
},
|
||||
Usage {
|
||||
id:0x004A,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Qy",
|
||||
},
|
||||
Usage {
|
||||
id:0x004B,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Qz",
|
||||
},
|
||||
Usage {
|
||||
id:0x004C,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Qw",
|
||||
},
|
||||
Usage {
|
||||
id:0x0090,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"D-pad Up",
|
||||
},
|
||||
Usage {
|
||||
id:0x0091,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"D-pad Down",
|
||||
},
|
||||
Usage {
|
||||
id:0x0092,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"D-pad Right",
|
||||
},
|
||||
Usage {
|
||||
id:0x0093,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"D-pad Left",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
UsagePage {
|
||||
id:0x0002,
|
||||
name:"Simulation Controls",
|
||||
usages:&[
|
||||
Usage {
|
||||
id:0x0001,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Flight Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x0002,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Automobile Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x0003,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Tank Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x0004,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Spaceship Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x0005,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Submarine Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x0006,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Sailing Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x0007,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Motorcycle Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x0008,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Sports Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x0009,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Airplane Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x000A,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Helicopter Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x000B,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Magic Carpet Simulation Device",
|
||||
},
|
||||
Usage {
|
||||
id:0x000C,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Bicycle Simulation Device",
|
||||
},
|
||||
|
||||
Usage {
|
||||
id:0x0020,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Flight Control Stick",
|
||||
},
|
||||
Usage {
|
||||
id:0x0021,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Flight Stick",
|
||||
},
|
||||
Usage {
|
||||
id:0x0022,
|
||||
usage_type:UsageType::Physical,
|
||||
name:"Cyclic Control",
|
||||
},
|
||||
Usage {
|
||||
id:0x0023,
|
||||
usage_type:UsageType::Physical,
|
||||
name:"Cyclic Trim",
|
||||
},
|
||||
Usage {
|
||||
id:0x0034,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Flight Yoke",
|
||||
},
|
||||
Usage {
|
||||
id:0x0025,
|
||||
usage_type:UsageType::Physical,
|
||||
name:"Track Control",
|
||||
},
|
||||
|
||||
Usage {
|
||||
id:0x00B0,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Aileron",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B1,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Aileron Trim",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B2,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Anti-Torque Control",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B3,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Autopilot",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B4,
|
||||
usage_type:UsageType::Button,
|
||||
name:"Chaff Release",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B5,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Collective Control",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B6,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Drive Brake",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B7,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Electronic Countermeasures",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B8,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Elevator",
|
||||
},
|
||||
Usage {
|
||||
id:0x00B9,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Elevator Trim",
|
||||
},
|
||||
Usage {
|
||||
id:0x00BA,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Rudder",
|
||||
},
|
||||
Usage {
|
||||
id:0x00BB,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Throttle",
|
||||
},
|
||||
Usage {
|
||||
id:0x00BC,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Flight Communications",
|
||||
},
|
||||
Usage {
|
||||
id:0x00BD,
|
||||
usage_type:UsageType::Button,
|
||||
name:"Flare Release",
|
||||
},
|
||||
Usage {
|
||||
id:0x00BE,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Landing Gear",
|
||||
},
|
||||
Usage {
|
||||
id:0x00BF,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Toe Brake",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C0,
|
||||
usage_type:UsageType::Button,
|
||||
name:"Trigger",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C1,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Weapons Arm",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C2,
|
||||
usage_type:UsageType::Button,
|
||||
name:"Weapons Select",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C3,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Wing Flags",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C4,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Accelerator",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C5,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Brake",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C6,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Clutch",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C7,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Steering",
|
||||
},
|
||||
Usage {
|
||||
id:0x00C9,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Turret Direction",
|
||||
},
|
||||
Usage {
|
||||
id:0x00CA,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Turret Elevation",
|
||||
},
|
||||
Usage {
|
||||
id:0x00CB,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Dive Plane",
|
||||
},
|
||||
Usage {
|
||||
id:0x00CC,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Ballast",
|
||||
},
|
||||
Usage {
|
||||
id:0x00CD,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Bicycle Crank",
|
||||
},
|
||||
Usage {
|
||||
id:0x00CE,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Handle Bars",
|
||||
},
|
||||
Usage {
|
||||
id:0x00CF,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Front Brake",
|
||||
},
|
||||
Usage {
|
||||
id:0x00D0,
|
||||
usage_type:UsageType::Value,
|
||||
name:"Rear Brake",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
UsagePage {
|
||||
id:0x0003,
|
||||
name:"Virtual Reality Controls",
|
||||
usages:&[
|
||||
Usage {
|
||||
id:0x0005,
|
||||
usage_type:UsageType::Physical,
|
||||
name:"Head Tracker",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
UsagePage {
|
||||
id:0x0007,
|
||||
name:"Keyboard",
|
||||
usages:&[ ],
|
||||
},
|
||||
|
||||
UsagePage {
|
||||
id:0x0008,
|
||||
name:"LED",
|
||||
usages:&[
|
||||
Usage {
|
||||
id:0x0001,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Num Lock",
|
||||
},
|
||||
Usage {
|
||||
id:0x0002,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Caps Lock",
|
||||
},
|
||||
Usage {
|
||||
id:0x0003,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Scroll Lock",
|
||||
},
|
||||
Usage {
|
||||
id:0x0004,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Compose",
|
||||
},
|
||||
Usage {
|
||||
id:0x0005,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Kana",
|
||||
},
|
||||
Usage {
|
||||
id:0x0006,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Power",
|
||||
},
|
||||
Usage {
|
||||
id:0x0007,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Shift",
|
||||
},
|
||||
Usage {
|
||||
id:0x0008,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Do Not Disturb",
|
||||
},
|
||||
Usage {
|
||||
id:0x0009,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Mute",
|
||||
},
|
||||
Usage {
|
||||
id:0x000A,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Tone Enable",
|
||||
},
|
||||
Usage {
|
||||
id:0x000B,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"High Cut Filter",
|
||||
},
|
||||
Usage {
|
||||
id:0x000C,
|
||||
usage_type:UsageType::Toggle,
|
||||
name:"Low Cut Filter",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
UsagePage {
|
||||
id:0x0009,
|
||||
name:"Button",
|
||||
usages:&[
|
||||
Usage {
|
||||
id:0x0001,
|
||||
usage_type:UsageType::Any,
|
||||
name:"Primary",
|
||||
},
|
||||
Usage {
|
||||
id:0x0002,
|
||||
usage_type:UsageType::Any,
|
||||
name:"Secondary",
|
||||
},
|
||||
Usage {
|
||||
id:0x0003,
|
||||
usage_type:UsageType::Any,
|
||||
name:"Tertiary",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
UsagePage {
|
||||
id:0x000C,
|
||||
name:"Consumer",
|
||||
usages:&[
|
||||
Usage {
|
||||
id:0x0001,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Consumer Control",
|
||||
},
|
||||
Usage {
|
||||
id:0x0002,
|
||||
usage_type:UsageType::Any,
|
||||
name:"Numeric Key Pad",
|
||||
},
|
||||
Usage {
|
||||
id:0x0003,
|
||||
usage_type:UsageType::Any,
|
||||
name:"Programmable Buttons",
|
||||
},
|
||||
Usage {
|
||||
id:0x0004,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Microphone",
|
||||
},
|
||||
Usage {
|
||||
id:0x0005,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Headphone",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
UsagePage {
|
||||
id:0x000D,
|
||||
name:"Digitizer",
|
||||
usages:&[
|
||||
Usage {
|
||||
id:0x0001,
|
||||
usage_type:UsageType::Application,
|
||||
name:"Digitizer",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/*
|
||||
UsagePage {
|
||||
id:0x00,
|
||||
name:"",
|
||||
usages:&[
|
||||
Usage {
|
||||
id:0x00,
|
||||
usage_type:UsageType::,
|
||||
name:"",
|
||||
},
|
||||
],
|
||||
},
|
||||
*/
|
||||
],
|
||||
};
|
513
src/hid/mod.rs
513
src/hid/mod.rs
@ -1,35 +1,4 @@
|
||||
/*
|
||||
enum ByteCode {
|
||||
Report = 0x00,
|
||||
Collection = 0xA1,
|
||||
UsagePage = 0x05,
|
||||
Usage = 0x09,
|
||||
UsageMin = 0x19,
|
||||
UsageMax = 0x29,
|
||||
LogicalMin = 0x15,
|
||||
LogicalMax = 0x25,
|
||||
PhysicalMin = 0x100,
|
||||
PhysicalMax = 0x101,
|
||||
ReportSize = 0x75,
|
||||
ReportCount = 0x95,
|
||||
}
|
||||
|
||||
const F_TYPE :u16 = 0x03;
|
||||
const TYPE_INPUT :u16 = 0x01;
|
||||
const TYPE_OUTPUT :u16 = 0x02;
|
||||
const TYPE_FEATURE :u16 = 0x03;
|
||||
const F_DC :u16 = 0x40; // Data / Const
|
||||
const F_RA :u16 = 0x80; // Relative / Absolute
|
||||
|
||||
struct _Usage {
|
||||
usage_page:u16,
|
||||
usage:u16,
|
||||
offset:u16,
|
||||
size:u16,
|
||||
fields:u16,
|
||||
}
|
||||
*/
|
||||
|
||||
pub mod consts;
|
||||
|
||||
// Main Items
|
||||
const MN_INPUT :u8 = 0b1000_00_00;
|
||||
@ -65,28 +34,18 @@ const LC_STRING_MIN :u8 = 0b1000_10_00;
|
||||
const LC_STRING_MAX :u8 = 0b1001_10_00;
|
||||
const LC_DELIMITER :u8 = 0b1010_10_00;
|
||||
|
||||
struct Context {
|
||||
usage_page:u16,
|
||||
usages:Vec<u16>,
|
||||
logical_min:i32,
|
||||
logical_max:i32,
|
||||
physical_min:Option<i32>,
|
||||
physical_max:Option<i32>,
|
||||
report_count:u16,
|
||||
report_size:u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct GlobalState {
|
||||
usage_page:u32,
|
||||
logical_min:i32,
|
||||
logical_max:i32,
|
||||
physical_min:i32,
|
||||
physical_max:i32,
|
||||
unit_exponent:i32,
|
||||
unit:u32,
|
||||
report_id:u32,
|
||||
report_size:u32,
|
||||
report_count:u32,
|
||||
pub usage_page:u16,
|
||||
pub logical_min:i32,
|
||||
pub logical_max:i32,
|
||||
pub physical_min:i32,
|
||||
pub physical_max:i32,
|
||||
pub unit_exponent:i32,
|
||||
pub unit:u32,
|
||||
pub report_id:u32,
|
||||
pub report_size:u32,
|
||||
pub report_count:u32,
|
||||
}
|
||||
impl GlobalState {
|
||||
pub fn new() -> Self
|
||||
@ -106,16 +65,90 @@ impl GlobalState {
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalState {
|
||||
pub usages:Vec<u32>,
|
||||
pub usage_range:[u32;2],
|
||||
}
|
||||
impl LocalState {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
usages:Vec::new(),
|
||||
usage_range:[0, 0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Collection {
|
||||
pub usage:u32,
|
||||
pub index:u32,
|
||||
}
|
||||
|
||||
pub struct Interface {
|
||||
pub usage_page:u16,
|
||||
pub usage:u16,
|
||||
pub collection:u16,
|
||||
pub index:u16,
|
||||
|
||||
working_value:i32,
|
||||
pub value:i32,
|
||||
}
|
||||
|
||||
pub struct Field {
|
||||
container:u8,
|
||||
|
||||
is_const:bool,
|
||||
is_var:bool,
|
||||
is_rel:bool,
|
||||
is_wrap:bool,
|
||||
is_linear:bool,
|
||||
has_pref:bool,
|
||||
has_null:bool,
|
||||
is_volatile:bool,
|
||||
is_buffered:bool,
|
||||
|
||||
report_size:usize,
|
||||
report_count:usize,
|
||||
|
||||
logical_min:i32,
|
||||
logical_max:i32,
|
||||
physical_min:i32,
|
||||
physical_max:i32,
|
||||
unit_exponent:i32,
|
||||
unit:u32,
|
||||
|
||||
interfaces:Vec<usize>,
|
||||
|
||||
bit_offset:usize,
|
||||
}
|
||||
|
||||
pub struct Report {
|
||||
pub id:u32,
|
||||
pub length:usize,
|
||||
pub fields:Vec<Field>,
|
||||
}
|
||||
|
||||
pub struct ReportDescriptor {
|
||||
|
||||
pub interfaces:Vec<Interface>,
|
||||
pub inputs:Vec<usize>,
|
||||
pub outputs:Vec<usize>,
|
||||
pub features:Vec<usize>,
|
||||
pub reports:Vec<Report>,
|
||||
|
||||
has_id:bool,
|
||||
}
|
||||
impl ReportDescriptor {
|
||||
pub fn parse(bytes:&[u8]) -> Result<Self,()>
|
||||
{
|
||||
// Output
|
||||
let mut descriptor = Self {
|
||||
interfaces:Vec::new(),
|
||||
inputs:Vec::new(),
|
||||
outputs:Vec::new(),
|
||||
features:Vec::new(),
|
||||
reports:Vec::new(),
|
||||
|
||||
has_id:false,
|
||||
};
|
||||
|
||||
// Global State
|
||||
@ -123,12 +156,12 @@ impl ReportDescriptor {
|
||||
global_state.push(GlobalState::new());
|
||||
|
||||
// Local State
|
||||
let usages = Vec::<[u16;2]>::new();
|
||||
let mut local_state = LocalState::new();
|
||||
|
||||
// Tracking
|
||||
let mut collection_stack = vec![Collection { usage:0, index:0 }];
|
||||
let mut collection_index = 0;
|
||||
let mut offset = 0;
|
||||
let mut report_id = 0;
|
||||
//let mut delimiter = false;
|
||||
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
@ -141,6 +174,8 @@ impl ReportDescriptor {
|
||||
index += 1;
|
||||
|
||||
if bytes.len() - index >= sz as usize {
|
||||
let global_state_index = global_state.len() - 1;
|
||||
let collection_stack_index = collection_stack.len() - 1;
|
||||
|
||||
// Handle long tag
|
||||
if sz == 2 && tg == MN_EXTENDED {
|
||||
@ -153,8 +188,11 @@ impl ReportDescriptor {
|
||||
}
|
||||
|
||||
if sz <= 4 {
|
||||
let udata = Self::unpack(&bytes, &index, sz as usize);
|
||||
let idata = Self::unpack_signed(&bytes, &index, sz as usize);
|
||||
|
||||
match tg {
|
||||
MN_INPUT => {
|
||||
MN_INPUT | MN_OUTPUT | MN_FEATURE => {
|
||||
/*
|
||||
** 0: Data(0), Constant(1)
|
||||
** 1: Array(0), Variable(1)
|
||||
@ -168,57 +206,140 @@ impl ReportDescriptor {
|
||||
** _: reserved
|
||||
*/
|
||||
|
||||
let mut is_const = false;
|
||||
let mut is_var = false;
|
||||
let mut is_rel = false;
|
||||
let mut is_wrap = false;
|
||||
let mut is_linear = false;
|
||||
let mut is_pref = false;
|
||||
let mut has_null = false;
|
||||
let mut is_volatile = false;
|
||||
let mut is_buffered = false;
|
||||
let mut field = Field {
|
||||
container:tg,
|
||||
|
||||
if sz >= 1 {
|
||||
is_const = (bytes[index + 1] & 0x01) != 0;
|
||||
is_var = (bytes[index + 1] & 0x02) != 0;
|
||||
is_rel = (bytes[index + 1] & 0x04) != 0;
|
||||
is_const:(udata & 0x01) != 0,
|
||||
is_var:(udata & 0x02) != 0,
|
||||
is_rel:(udata & 0x04) != 0,
|
||||
is_wrap:(udata & 0x04) != 0,
|
||||
is_linear:(udata & 0x04) != 0,
|
||||
has_pref:(udata & 0x04) != 0,
|
||||
has_null:(udata & 0x04) != 0,
|
||||
is_volatile:(udata & 0x04) != 0,
|
||||
is_buffered:(udata & 0x04) != 0,
|
||||
|
||||
report_size:global_state[global_state_index].report_size as usize,
|
||||
report_count:global_state[global_state_index].report_count as usize,
|
||||
|
||||
logical_min:global_state[global_state_index].logical_min,
|
||||
logical_max:global_state[global_state_index].logical_max,
|
||||
physical_min:global_state[global_state_index].physical_min,
|
||||
physical_max:global_state[global_state_index].physical_max,
|
||||
unit_exponent:global_state[global_state_index].unit_exponent,
|
||||
unit:global_state[global_state_index].unit,
|
||||
|
||||
interfaces:Vec::new(),
|
||||
|
||||
bit_offset:0,
|
||||
};
|
||||
|
||||
// Add data interfaces to descriptor.
|
||||
if !field.is_const {
|
||||
let interface_count = if field.is_var {
|
||||
global_state[global_state_index].report_count as usize
|
||||
} else {
|
||||
if local_state.usage_range != [0; 2] {
|
||||
(local_state.usage_range[1] - local_state.usage_range[0]) as usize
|
||||
} else {
|
||||
local_state.usages.len() as usize
|
||||
}
|
||||
};
|
||||
|
||||
for i in 0..interface_count {
|
||||
|
||||
let mut usage_index = 0;
|
||||
let usage = if local_state.usage_range != [0; 2] {
|
||||
if (i as u32) <= local_state.usage_range[1] - local_state.usage_range[0] {
|
||||
local_state.usage_range[0] + i as u32
|
||||
} else {
|
||||
usage_index += 1;
|
||||
local_state.usage_range[1]
|
||||
}
|
||||
} else if i < local_state.usages.len() {
|
||||
local_state.usages[i]
|
||||
} else if local_state.usages.len() > 0 {
|
||||
let index = local_state.usages.len() - 1;
|
||||
usage_index += 1;
|
||||
local_state.usages[index]
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let usage_page = if usage <= 0xFFFF {
|
||||
global_state[global_state_index].usage_page
|
||||
} else {
|
||||
(usage >> 16) as u16
|
||||
};
|
||||
|
||||
let usage = usage as u16;
|
||||
let collection = collection_stack[collection_stack_index].index as u16;
|
||||
|
||||
// Check if interface already exists
|
||||
let mut existing_index = None;
|
||||
for i in 0..descriptor.interfaces.len() {
|
||||
if descriptor.interfaces[i].usage_page == usage_page
|
||||
&& descriptor.interfaces[i].usage == usage
|
||||
&& descriptor.interfaces[i].collection == collection
|
||||
&& descriptor.interfaces[i].index == usage_index
|
||||
{
|
||||
existing_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(existing_index) = existing_index {
|
||||
field.interfaces.push(existing_index);
|
||||
} else {
|
||||
let interface = Interface {
|
||||
usage_page,
|
||||
usage:usage,
|
||||
collection,
|
||||
index:usage_index,
|
||||
|
||||
working_value:0,
|
||||
value:0,
|
||||
};
|
||||
|
||||
descriptor.interfaces.push(interface);
|
||||
|
||||
let id = descriptor.interfaces.len() - 1;
|
||||
field.interfaces.push(id);
|
||||
|
||||
match tg {
|
||||
MN_OUTPUT => { descriptor.outputs.push(id); }
|
||||
MN_FEATURE => { descriptor.features.push(id); }
|
||||
_ => { descriptor.inputs.push(id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sz >= 2 {
|
||||
let report_index = if let Some(report_index) = descriptor.find_report(global_state[global_state_index].report_id) {
|
||||
report_index
|
||||
} else {
|
||||
descriptor.reports.push(Report {
|
||||
id:global_state[global_state_index].report_id,
|
||||
length:0,
|
||||
fields:Vec::new(),
|
||||
});
|
||||
descriptor.reports.len() - 1
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
// Add interfaces to report.
|
||||
let report = &mut descriptor.reports[report_index];
|
||||
|
||||
MN_OUTPUT => {
|
||||
/*
|
||||
** 0: Data(0), Constant(1)
|
||||
** 1: Array(0), Variable(1)
|
||||
** 2: Absolute(0), Relative(1)
|
||||
** 3: NoWrap(0), Wrap(1)
|
||||
** 4: Linear(0), NonLinear(1)
|
||||
** 5: PreferredState(0), NoPreferred(1)
|
||||
** 6: NoNull(0), NullState(1)
|
||||
** 7: NonVolatile(0), Volatile(1)
|
||||
** 8: BitField(0), BufferedBytes(1)
|
||||
** _: reserved
|
||||
*/
|
||||
let bit_offset = if let Some(field) = report.fields.last() {
|
||||
field.bit_offset + field.report_size
|
||||
} else { 0 };
|
||||
|
||||
}
|
||||
field.bit_offset = bit_offset;
|
||||
//field.bit_length = global_state[global_state_index].report_size as usize;
|
||||
|
||||
MN_FEATURE => {
|
||||
/*
|
||||
** 0: Data(0), Constant(1)
|
||||
** 1: Array(0), Variable(1)
|
||||
** 2: Absolute(0), Relative(1)
|
||||
** 3: NoWrap(0), Wrap(1)
|
||||
** 4: Linear(0), NonLinear(1)
|
||||
** 5: PreferredState(0), NoPreferred(1)
|
||||
** 6: NoNull(0), NullState(1)
|
||||
** 7: NonVolatile(0), Volatile(1)
|
||||
** 8: BitField(0), BufferedBytes(1)
|
||||
** _: reserved
|
||||
*/
|
||||
report.length += field.report_size * field.report_count;
|
||||
report.fields.push(field);
|
||||
|
||||
local_state = LocalState::new();
|
||||
}
|
||||
|
||||
MN_COLLECTION_BEGIN => {
|
||||
@ -234,70 +355,89 @@ impl ReportDescriptor {
|
||||
** 80-FF: vendor-defined
|
||||
*/
|
||||
|
||||
collection_stack.push(Collection {
|
||||
usage:udata,
|
||||
index:collection_index,
|
||||
});
|
||||
collection_index += 1;
|
||||
local_state = LocalState::new();
|
||||
}
|
||||
|
||||
MN_COLLECTION_END => {
|
||||
|
||||
collection_stack.pop();
|
||||
local_state = LocalState::new();
|
||||
}
|
||||
|
||||
GB_USAGE_PAGE => {
|
||||
|
||||
global_state[global_state_index].usage_page = udata as u16;
|
||||
}
|
||||
|
||||
GB_LOGICAL_MIN => {
|
||||
|
||||
global_state[global_state_index].logical_min = idata;
|
||||
}
|
||||
|
||||
GB_LOGICAL_MAX => {
|
||||
|
||||
global_state[global_state_index].logical_max = idata;
|
||||
}
|
||||
|
||||
GB_PHYSICAL_MIN => {
|
||||
|
||||
global_state[global_state_index].physical_min = idata;
|
||||
}
|
||||
|
||||
GB_PHYSICAL_MAX => {
|
||||
|
||||
global_state[global_state_index].physical_max = idata;
|
||||
}
|
||||
|
||||
GB_UNIT_EXPONENT => {
|
||||
|
||||
global_state[global_state_index].unit_exponent = idata;
|
||||
}
|
||||
|
||||
GB_UNIT => {
|
||||
|
||||
global_state[global_state_index].unit = udata;
|
||||
}
|
||||
|
||||
GB_REPORT_SIZE => {
|
||||
|
||||
global_state[global_state_index].report_size = udata;
|
||||
}
|
||||
|
||||
GB_REPORT_ID => {
|
||||
descriptor.has_id = true;
|
||||
global_state[global_state_index].report_id = udata;
|
||||
|
||||
// Create new report if id not found.
|
||||
if descriptor.find_report(udata).is_none() {
|
||||
descriptor.reports.push(Report {
|
||||
id:udata,
|
||||
length:0,
|
||||
fields:Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
GB_REPORT_COUNT => {
|
||||
|
||||
global_state[global_state_index].report_count = udata;
|
||||
}
|
||||
|
||||
GB_PUSH => {
|
||||
|
||||
global_state.push(global_state[global_state.len() - 1]);
|
||||
}
|
||||
|
||||
GB_POP => {
|
||||
|
||||
if global_state.len() > 1 {
|
||||
global_state.pop();
|
||||
}
|
||||
}
|
||||
|
||||
LC_USAGE => {
|
||||
|
||||
local_state.usages.push(udata);
|
||||
}
|
||||
|
||||
LC_USAGE_MIN => {
|
||||
|
||||
local_state.usage_range[0] = udata;
|
||||
}
|
||||
|
||||
LC_USAGE_MAX => {
|
||||
|
||||
local_state.usage_range[1] = udata;
|
||||
}
|
||||
|
||||
LC_DESIGNATOR_INDEX => {
|
||||
@ -338,4 +478,145 @@ impl ReportDescriptor {
|
||||
|
||||
Ok(descriptor)
|
||||
}
|
||||
|
||||
pub fn read_report(&mut self, bytes:&[u8]) -> Result<Vec<usize>,()>
|
||||
{
|
||||
let mut start = 0;
|
||||
|
||||
if bytes.len() == 0 { return Err(()); }
|
||||
|
||||
// Read report id
|
||||
let report_id = if self.has_id {
|
||||
start += 1;
|
||||
bytes[0]
|
||||
} else {
|
||||
0
|
||||
} as u32;
|
||||
|
||||
if let Some(report_index) = self.find_report(report_id) {
|
||||
let report = &self.reports[report_index];
|
||||
let mut changes = Vec::new();
|
||||
|
||||
for field in &report.fields {
|
||||
|
||||
// Set interface working values to zero.
|
||||
for inf in &field.interfaces {
|
||||
self.interfaces[*inf].working_value = 0;
|
||||
}
|
||||
|
||||
// Get report values.
|
||||
for ri in 0..field.report_count {
|
||||
let bit_offset = field.bit_offset + (ri * field.report_size);
|
||||
|
||||
let byte = bit_offset / 8;
|
||||
let bit = bit_offset % 8;
|
||||
let byte_count = ((bit_offset + field.report_size) / 8) - byte;
|
||||
|
||||
let mut value = (bytes[start + byte] as i64) >> bit;
|
||||
for i in 1..byte_count {
|
||||
value |= (bytes[start + byte + i] as i64) << ((i * 8) - bit);
|
||||
}
|
||||
|
||||
if value >= field.logical_min as i64 && value <= field.logical_max as i64 {
|
||||
if field.is_var {
|
||||
let interface = &mut self.interfaces[field.interfaces[ri]];
|
||||
|
||||
// Normalize value
|
||||
if field.physical_min != 0 || field.physical_max != 0 {
|
||||
let domain = (field.logical_max - field.logical_min) as i64;
|
||||
let range = (field.physical_max - field.physical_min) as i64;
|
||||
let basis = 1 << 24;
|
||||
|
||||
interface.working_value = field.physical_min + (((value - field.logical_min as i64) * (range * basis)) / domain) as i32;
|
||||
}
|
||||
} else {
|
||||
let interface = &mut self.interfaces[field.interfaces[value as usize]];
|
||||
interface.working_value = 1 << 24;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set interface working values to zero.
|
||||
for inf in &field.interfaces {
|
||||
if self.interfaces[*inf].working_value != self.interfaces[*inf].value {
|
||||
self.interfaces[*inf].value = self.interfaces[*inf].working_value;
|
||||
changes.push(*inf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_report(&self) -> Vec<u8>
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn find_report(&self, id:u32) -> Option<usize>
|
||||
{
|
||||
let mut ri = 0;
|
||||
while ri < self.reports.len() {
|
||||
if self.reports[ri].id == id { return Some(ri); }
|
||||
ri += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn unpack(data:&[u8], index:&usize, size:usize) -> u32
|
||||
{
|
||||
let mut result = 0u32;
|
||||
if data.len() - *index >= size {
|
||||
match size {
|
||||
1 => {
|
||||
result = data[*index] as u32;
|
||||
}
|
||||
|
||||
2 => {
|
||||
result = (data[*index] as u32)
|
||||
| ((data[*index + 1] as u32) << 8);
|
||||
}
|
||||
|
||||
4 => {
|
||||
result = (data[*index] as u32)
|
||||
| ((data[*index + 1] as u32) << 8)
|
||||
| ((data[*index + 2] as u32) << 16)
|
||||
| ((data[*index + 3] as u32) << 24);
|
||||
}
|
||||
|
||||
_ => { }
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn unpack_signed(data:&[u8], index:&usize, size:usize) -> i32
|
||||
{
|
||||
let mut result = 0i32;
|
||||
if data.len() - *index >= size {
|
||||
match size {
|
||||
1 => {
|
||||
result = data[*index] as i32;
|
||||
}
|
||||
|
||||
2 => {
|
||||
result = (data[*index] as i32)
|
||||
| ((data[*index + 1] as i32) << 8);
|
||||
}
|
||||
|
||||
4 => {
|
||||
result = (data[*index] as i32)
|
||||
| ((data[*index + 1] as i32) << 8)
|
||||
| ((data[*index + 2] as i32) << 16)
|
||||
| ((data[*index + 3] as i32) << 24);
|
||||
}
|
||||
|
||||
_ => { }
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user