Skip to content
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

Merged
merged 13 commits into from
Sep 8, 2024
Merged

Conversation

kubaraczkowski
Copy link
Contributor

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

Copy link

google-cla bot commented Jul 19, 2024

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.

@@ -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) {
Copy link
Collaborator

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK!

@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fakeSysDevices

@@ -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
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useUSBDK, or maybe useUSBDevKit?

Copy link
Contributor Author

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
Copy link
Collaborator

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.

Copy link
Contributor Author

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 {
Copy link
Collaborator

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
}

Copy link
Collaborator

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"
Copy link
Collaborator

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))
Copy link
Collaborator

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
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

@kubaraczkowski
Copy link
Contributor Author

Thx @zagrodzki for the comments, please have a look at the update.

Copy link
Collaborator

@zagrodzki zagrodzki left a 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.

@@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lowercase "the"

@@ -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 {
Copy link
Collaborator

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())
Copy link
Collaborator

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
Copy link
Collaborator

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)
Copy link
Collaborator

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)
Copy link
Collaborator

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).

Copy link
Collaborator

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 {
Copy link
Collaborator

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop (for later PR)

@@ -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
Copy link
Collaborator

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"...

Copy link
Contributor Author

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"

Copy link
Collaborator

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 :)

Copy link
Contributor Author

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{})
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

@kubaraczkowski
Copy link
Contributor Author

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
Copy link
Collaborator

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.
Copy link
Collaborator

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
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 Show resolved Hide resolved
usb.go Show resolved Hide resolved
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
Copy link
Collaborator

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.

Copy link
Contributor Author

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 Show resolved Hide resolved
usb.go Outdated
}

dev := c.libusb.getDevice(handle)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop this empty line

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still open

Copy link
Contributor Author

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

Copy link
Collaborator

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)

usb_test.go Outdated Show resolved Hide resolved
usb_test.go Outdated Show resolved Hide resolved
libusb.go Outdated Show resolved Hide resolved
libusb.go Outdated Show resolved Hide resolved
libusb.go Outdated Show resolved Hide resolved
@kubaraczkowski
Copy link
Contributor Author

Adjusted! Thx for the suggestions.

usb_test.go Outdated Show resolved Hide resolved
@zagrodzki
Copy link
Collaborator

any updates here?

@kubaraczkowski
Copy link
Contributor Author

Just back from holidays, so it will move in a few days :)

@kubaraczkowski
Copy link
Contributor Author

Sorry for the delay, restarting work after holidays took longer than expected :)
Something more to be done? Thx for the help!

Copy link
Collaborator

@zagrodzki zagrodzki left a 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)
Copy link
Collaborator

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 Show resolved Hide resolved
usb.go Outdated
}

dev := c.libusb.getDevice(handle)

Copy link
Collaborator

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()).
Copy link
Collaborator

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 Show resolved Hide resolved
@kubaraczkowski
Copy link
Contributor Author

Sorry for missing a few points.

Thx for the explanation of Errorf and Fatalf.
I think all comments are now resolved, not 100% sure. I put 👍 on everything I've checked.

Copy link
Collaborator

@zagrodzki zagrodzki left a 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)

Copy link
Collaborator

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)

@zagrodzki
Copy link
Collaborator

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.

@zagrodzki
Copy link
Collaborator

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.

@kubaraczkowski
Copy link
Contributor Author

Any hints on how you would approach keeping it work for ubuntu 20.04 while enabling the additional functionality on 22+ ?
I'm just curious how this could be done, if required.

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.

@zagrodzki
Copy link
Collaborator

Any hints on how you would approach keeping it work for ubuntu 20.04 while enabling the additional functionality on 22+ ? I'm just curious how this could be done, if required.

I think I would go with compiler conditionals in the C code, adding a replacement definition of the libusb_init_context if the libusb doesn't provide it (#if (LIBUSB_API_VERSION < ...)). And it can inspect the options and if they are non-default, return a "not supported" error.

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.

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.

https://libusb.sourceforge.io/api-1.0/group__libusb__lib.html#gaabdf0bf6be87e12023b7399003c50470 says for libusb_init_context:

Since version 1.0.27, LIBUSB_API_VERSION >= 0x0100010A

1.0.27 came out Feb 1st this year.

@zagrodzki zagrodzki merged commit 606016a into google:master Sep 8, 2024
4 checks passed
@zagrodzki
Copy link
Collaborator

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.

@kubaraczkowski
Copy link
Contributor Author

Hey, nice!

I was looking at only libusb_wrap_sys_device() which is the base of android support.
https://libusb.sourceforge.io/api-1.0/group__libusb__dev.html#ga98f783e115ceff4eaf88a60e6439563c

OK, I'm sure somebody using gousb on an older system will complain if needed!

Thanks for the great support!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants