-
Notifications
You must be signed in to change notification settings - Fork 129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Android support via fileDescriptor #130
Conversation
…recommended libusb context creation method
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
fakelibusb_test.go
Outdated
@@ -98,7 +100,9 @@ type fakeLibusb struct { | |||
claims map[*libusbDevice]map[uint8]bool | |||
} | |||
|
|||
func (f *fakeLibusb) init() (*libusbContext, error) { return newContextPointer(), nil } | |||
func (f *fakeLibusb) init() (*libusbContext, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revert? this is just different formatting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK!
fakelibusb_test.go
Outdated
@@ -87,6 +87,8 @@ type fakeLibusb struct { | |||
mu sync.Mutex | |||
// fakeDevices has a map of devices and their descriptors. | |||
fakeDevices map[*libusbDevice]*fakeDevice | |||
// fakeLibUsbDevices keeps the order of devices to be accessd by wrapSysDevice | |||
fakeLibUsbDevices []*libusbDevice |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fakeSysDevices
fakelibusb_test.go
Outdated
@@ -87,6 +87,8 @@ type fakeLibusb struct { | |||
mu sync.Mutex | |||
// fakeDevices has a map of devices and their descriptors. | |||
fakeDevices map[*libusbDevice]*fakeDevice | |||
// fakeLibUsbDevices keeps the order of devices to be accessd by wrapSysDevice | |||
fakeLibUsbDevices []*libusbDevice |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be better to instead have a map of uintptr->device? That would allow for more realistic pointer-like values, or at least wouldn't limit the code to use contiguous 0-indexed range of values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, good point. The FDs I randomly saw were e.g. never 0,1,2 as those are 'special'. Certainly a '0' could be messing up somewhere in a test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, correction about the 0 - it makes no difference since the FDs still need to be iterated from the list of fakeDevices, unless we'd add a 'fakeFD' entry in there?
libusb.go
Outdated
type libusbImpl struct { | ||
logLevel int | ||
disableDiscovery bool | ||
useUsbDK bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useUSBDK, or maybe useUSBDevKit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
libusb.go
Outdated
type libusbImpl struct{} | ||
type libusbImpl struct { | ||
logLevel int | ||
disableDiscovery bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doouble negatives (disable: false) are bad for readability. Use "discovery bool" here, and we'll initialize it with "true" by default.
We obviously don't want the discovery to default to false though. See my other comment in the user-facing code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good point. I wanted to keep the structure as is in the libusb options, but what you propose makes sense since gousb is supposed to be more 'go-like' rather than being just a libusb wrapper.
libusb.go
Outdated
copy(libusb_options[n_options].value[:], b) | ||
n_options += 1 | ||
} | ||
if impl.useUsbDK { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto here - I don't think this is needed for Android support? Move to a separate PR
usb.c
Outdated
@@ -24,3 +24,4 @@ void gousb_set_debug(libusb_context *ctx, int lvl) { | |||
libusb_set_debug(ctx, lvl); | |||
#endif | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revert
usb_test.go
Outdated
@@ -15,7 +15,9 @@ | |||
|
|||
package gousb | |||
|
|||
import "testing" | |||
import ( | |||
"testing" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revert this block
usb_test.go
Outdated
{0x7777, 0x0003, false}, | ||
} { | ||
// The fake keeps the order of fakeDevices so that it can be indexed by a fake "fileDescriptor" | ||
dev, err := ctx.OpenDeviceWithFileDescriptor(uintptr(ix)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this conflates the ordering of the test cases with the file descriptor values, I think it's fragile. See my other comment about a map for sys devices.
usb_test.go
Outdated
if (dev != nil) != d.exists { | ||
t.Errorf("OpenDeviceWithFileDescriptor(%d): device != nil is %v, want %v", ix, dev != nil, d.exists) | ||
} | ||
if err != nil && d.exists { // It's OK to get an error if device doesn't exist |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the behavior of OpenDeviceWithFileDescriptor creates a small divergence:
the OpenDeviceWithVIDPID returns nil, nil if the device is not found. Here the function will return an error if the device is not found. I think this is fine though - the Open...VIDPID should have returned an error for not found too.
But this change makes it necessary to change the structure of the test, you cannot use the same structure - I think you don't need to distinguish "exists" vs "not exists", instead you need to distinguish errors vs non-errors. And I think it may make sense to add a separate test case for a "want error", to simplify the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, since the fakeDevice can have an uninitialised sysDevPtr (e.g. because the system doesn't support that?) there still is a case where a device exists, but is 'not useful'/'not good'. Perhaps the word 'exists' should rather be 'good' or 'useful' ?
I will split it in two test cases, this will actually make it avoid the wording problem and indeed make the cases simpler.
Thx @zagrodzki for the comments, please have a look at the update. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also documentation please, I think this is good overall.
fakelibusb_test.go
Outdated
@@ -108,6 +110,21 @@ func (f *fakeLibusb) getDevices(*libusbContext) ([]*libusbDevice, error) { | |||
return ret, nil | |||
} | |||
|
|||
func (f *fakeLibusb) wrapSysDevice(ctx *libusbContext, systemDeviceHandle uintptr) (*libusbDevHandle, error) { | |||
if _, ok := f.fakeSysDevices[systemDeviceHandle]; !ok { | |||
return nil, fmt.Errorf("The passed file descriptor %d does not point to a valid device", systemDeviceHandle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lowercase "the"
fakelibusb_test.go
Outdated
@@ -108,6 +110,21 @@ func (f *fakeLibusb) getDevices(*libusbContext) ([]*libusbDevice, error) { | |||
return ret, nil | |||
} | |||
|
|||
func (f *fakeLibusb) wrapSysDevice(ctx *libusbContext, systemDeviceHandle uintptr) (*libusbDevHandle, error) { | |||
if _, ok := f.fakeSysDevices[systemDeviceHandle]; !ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest:
dev, ok := f.fakeSysDevices[...]
if !ok {
return nil, fmt.Errorf(...)
}
...
f.handles[h] = dev
usb.go
Outdated
func (c *Context) OpenDeviceWithFileDescriptor(fd uintptr) (*Device, error) { | ||
handle, err := c.libusb.wrapSysDevice(c.ctx, fd) | ||
if err != nil { | ||
return nil, fmt.Errorf("Error opening device from file descriptor: %d, %s", fd, err.Error()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove the word "Error" from the message, it's superfluous in a context of an error message. And "opening device from the file descriptor" seems unnecessary too, because the caller know what they just called. Better just return the error from libusb wrapper as is.
usb.go
Outdated
|
||
desc, err := c.libusb.getDeviceDesc(dev) | ||
if err != nil { | ||
return nil, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you want to annotate an error somewhere, here it would be appropriate perhaps:
fmt.Errorf("device was opened, but get device descriptor failed: %v")
usb_test.go
Outdated
t.Errorf("OpenDeviceWithFileDescriptor(%d): device == nil for a valid device", d.sysDevPtr) | ||
} | ||
if err != nil { | ||
t.Errorf("OpenDeviceWithFileDescriptor(%d): err != nil for a valid device: %v", d.sysDevPtr, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fatalf, and move before the dev == nil
check.
usb_test.go
Outdated
} { | ||
dev, err := ctx.OpenDeviceWithFileDescriptor(d.sysDevPtr) | ||
if dev == nil { | ||
t.Errorf("OpenDeviceWithFileDescriptor(%d): device == nil for a valid device", d.sysDevPtr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fatalf.
Also add a check to see if the obtained device is the one you asked for (does VID/PID match).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these two above (line 112 and 115) still need to be Fatalf.
With Fatal(f), the test is immediately interrupted. With Error(f), the test continues, but will report a failure at the end with all recorded errors. The principle is "return as much useful test results from a single run as possible".
Hence:
- if err is not nil, dev is undefined and no further testing can be done (Fatal)
- if dev is nil, there's no device, testing properties of the device makes no sense (Fatal)
- if dev is not nil and err is nil, then dev may have useful properties. Checking these properties likely doesn't affect whether further testing can be done (Error), unless the code after that check has a hard dependency on the data that the previous check validated.
For future reference: if there's enough distinct check flows that each check forms a chain of dependencies, it may make sense to split individual checks into a subtest (t.Run).
usb_test.go
Outdated
if err == nil { | ||
t.Errorf("OpenDeviceWithFileDescriptor(%d): got nil error for invalid device", sysDevPtr) | ||
} | ||
if dev != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
drop this check - it's enough to check that error was non-nil. The return value is undefined, it may be non-nil, but it shouldn't be used either way.
libusb.go
Outdated
@@ -40,6 +40,7 @@ type libusbDevice C.libusb_device | |||
type libusbDevHandle C.libusb_device_handle | |||
type libusbTransfer C.struct_libusb_transfer | |||
type libusbEndpoint C.struct_libusb_endpoint_descriptor | |||
type libusbLogLevel C.enum_libusb_log_level |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
drop (for later PR)
fakelibusb_test.go
Outdated
@@ -87,6 +87,8 @@ type fakeLibusb struct { | |||
mu sync.Mutex | |||
// fakeDevices has a map of devices and their descriptors. | |||
fakeDevices map[*libusbDevice]*fakeDevice | |||
// fakeSysDevices keeps the order of devices to be accessd by wrapSysDevice | |||
fakeSysDevices map[uintptr]*libusbDevice |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually, perhaps just "sysDevices" - this is already inside the fakeLibusb, so "fake" stutters a bit. "fakeDevices" is ok because the type is called "fakeDevice", although perhaps it should also be renamed "devices"...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're the boss, but I'd argue that either both should be changed or neither - it's much more readable to see these two named in the same way. You immediately go "ah, right, these are just some mappings of the fakes"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd go with "both" then :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand this as "change both" :)
Will update
usb.go
Outdated
@@ -168,6 +168,23 @@ func NewContext() *Context { | |||
return newContextWithImpl(libusbImpl{}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it make sense to replace this with "return ContextOptions{}.New()"? It's now becoming essentially a "context with defaults" anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree - it also matches better the 'new' libusb initializer, since the int libusb_init(libusb_context ctx)
is deprecated in favour of always calling it with options.
…based on comments
Updated! Thx for detailed and clear feedback. |
usb.go
Outdated
return ContextOptions{}.New() | ||
} | ||
|
||
// DeviceDiscovery enables a scan of the available USB devices connected to the system |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a "Defaults to enabled". Also, full sentences, end with a period.
usb.go
Outdated
@@ -165,7 +165,27 @@ func newContextWithImpl(impl libusbIntf) *Context { | |||
|
|||
// NewContext returns a new Context instance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...with default ContextOptions
usb.go
Outdated
DisableDeviceDiscovery | ||
) | ||
|
||
// ContextOptions holds optional flags checked when creating a new Context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"optional flags" is too specific, since we might add more parameters to the context creation that do not easily translate into options in libusb init.
Perhaps:
ContextOptions holds parameters for Context initialization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I always need to remind myself - gousb is not a libusb wrapper. Not only a wrapper at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anything still missing here?
usb.go
Outdated
@@ -210,6 +230,38 @@ func (c *Context) OpenDevices(opener func(desc *DeviceDesc) bool) ([]*Device, er | |||
return ret, reterr | |||
} | |||
|
|||
// OpenDeviceWithFileDescriptor takes a (Unix) file descriptor of an opened USB device | |||
// and wraps the library around it. | |||
// This is particularly useful when working on Android, where the USB device must be opened |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps "is opened" rather than "must be"? I suspect that Android NDK may offer a similar functionality, it's unlikely the "must be opened in Java SDK" is going to be true forever.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It actually sucks a bit, but unfortunately NDK is not able to open USB devices. Yet?
TBH I thought that the 'must be' would be a nudge for the users to realise that they will need the Java SDK to acquire permissions and open the device.
But you're correct, it's entirely possible that the NDK limitation will be lifted at some point and this docstring will become invalid.
I will change it to "can be opened"
usb.go
Outdated
} | ||
|
||
dev := c.libusb.getDevice(handle) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
drop this empty line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still open
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's already in place
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant remove the empty line (i.e. remove line 262 between the dev assignment and desc assignment)
Adjusted! Thx for the suggestions. |
any updates here? |
Just back from holidays, so it will move in a few days :) |
Sorry for the delay, restarting work after holidays took longer than expected :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are still some comments open
usb_test.go
Outdated
} { | ||
dev, err := ctx.OpenDeviceWithFileDescriptor(d.sysDevPtr) | ||
if dev == nil { | ||
t.Errorf("OpenDeviceWithFileDescriptor(%d): device == nil for a valid device", d.sysDevPtr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these two above (line 112 and 115) still need to be Fatalf.
With Fatal(f), the test is immediately interrupted. With Error(f), the test continues, but will report a failure at the end with all recorded errors. The principle is "return as much useful test results from a single run as possible".
Hence:
- if err is not nil, dev is undefined and no further testing can be done (Fatal)
- if dev is nil, there's no device, testing properties of the device makes no sense (Fatal)
- if dev is not nil and err is nil, then dev may have useful properties. Checking these properties likely doesn't affect whether further testing can be done (Error), unless the code after that check has a hard dependency on the data that the previous check validated.
For future reference: if there's enough distinct check flows that each check forms a chain of dependencies, it may make sense to split individual checks into a subtest (t.Run).
usb.go
Outdated
} | ||
|
||
dev := c.libusb.getDevice(handle) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still open
usb.go
Outdated
// OpenDeviceWithFileDescriptor takes a (Unix) file descriptor of an opened USB device | ||
// and wraps the library around it. | ||
// This is particularly useful when working on Android, where the USB device must be opened | ||
// by the SDK (Java), giving access to the device through the file descriptor (https://developer.android.com/reference/android/hardware/usb/UsbDeviceConnection#getFileDescriptor()). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still open
Sorry for missing a few points. Thx for the explanation of Errorf and Fatalf. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
only one minor nitpicky comment left
usb.go
Outdated
} | ||
|
||
dev := c.libusb.getDevice(handle) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant remove the empty line (i.e. remove line 262 between the dev assignment and desc assignment)
a linux build is failing due to too old libusb version (1.0.25 in the build env). I wonder if we should support the older versions, but I think it would get complicated. I'll just update the minor version at the release time. In the meantime, I need to update the build env, stay tuned. |
Upgraded Linux build env to ubuntu 22.04, rebase and we should be good. |
Any hints on how you would approach keeping it work for ubuntu 20.04 while enabling the additional functionality on 22+ ? It seems the feature came out in 2019 and should already be present in 20.04 (1.0.23+), not sure though. 20.04 is still being used, at least EOL is 2025. |
I think I would go with compiler conditionals in the C code, adding a replacement definition of the Alternatively, the libusb version could be inspected from the Go code, so perhaps the C implementation can be trivial (just to appease the compiler, always return a not supported error) and the Go code can conditionally call either old-style init (if old libusb and init called with default options), or fail (old libusb and non-default options), or libusb_init_context if supported. Both of these would keep working for the existing users, until someone tries to use ContextOptions{} to change some defaults.
https://libusb.sourceforge.io/api-1.0/group__libusb__lib.html#gaabdf0bf6be87e12023b7399003c50470 says for Since version 1.0.27, 1.0.27 came out Feb 1st this year. |
thank you for your patience during the process! I'll see if I can get any of the other pending pull requests in before I release a new version, I plan to do that in ~2wks. |
Hey, nice! I was looking at only OK, I'm sure somebody using gousb on an older system will complain if needed! Thanks for the great support! |
See base #126
Since libusb supports the peculiar Android USB handling (usb device must be opened from Java, after acquiring permission from the user to do so) it's useful to enable gousb to do the same.
Checked using a basic Android app with a USB serial device in loopback.
Documentation is not yet done, if the code changes are OK, I'll write the docstrings as well