How to read without blocking

How can I read from an input device on Linux in a non-blocking way, perhaps with a timeout?

I have this function:

function read_event(js::JSDevice)
    event = read(js.device, sizeof(JSEvent); all = false)
    if sizeof(event) != sizeof(JSEvent)
        return nothing
    end
    reinterpret(JSEvent, event)[]
end

It never returns nothing, instead it blocks until the correct amount of bytes are available.

Is there a way to check how many bytes are available for reading before reading?

Full code: Joysticks.jl/Joysticks.jl at main · ufechner7/Joysticks.jl · GitHub

In the documentation of the C api it says:

3. Reading
~~~~~~~~~~

If you open the device in blocking mode, a read will block (that is,
wait) forever until an event is generated and effectively read. There
are two alternatives if you can't afford to wait forever (which is,
admittedly, a long time;)

	a) use select to wait until there's data to be read on fd, or
	   until it timeouts. There's a good example on the select(2)
	   man page.

	b) open the device in non-blocking mode (O_NONBLOCK)

But how can I open the device in non-blocking mode?
My current code:

            file = open(filename, "r+")
            device = JSDevice(file, fd(file), 0, 0)

For the open function of Julia I could not find any option O_NONBLOCK …

Normally in Julia you would use an @async task (a green thread). The task will wake up when bytes are available to read.

1 Like

Well, I tried, but it does not work, because while the read command is blocking no other Julia code is being executed.

What I need is the Julia equivalent or translation of this C code:

js = open(device, O_RDONLY | O_NONBLOCK);

This does not work:

file = open(filename, "r+")

Found this open issue in Julia: Review of IO blocking behaviour · Issue #24526 · JuliaLang/julia · GitHub
Anybody around who knows how to translate C code to Julia?

You can use ccall() to directly call the underlying functions yourself. Since (I guess) you only need to open, close and read from the joystick device file something like this would do it:

O_RDONLY = 0
O_NONBLOCK = 2048

jfd = ccall(:open, Cint, (Cstring, Cint), "/dev/input/js0", O_RDONLY|O_NONBLOCK)
println("fd = $(jfd)")

N = 100
buf = Vector{UInt8}(undef, N)

# Loop until we read succesfully 5 times
count = 0
while count < 5

    # ssize_t read(int fd, void *buf, size_t count)
    res = ccall(:read, Cssize_t, (Cint, Ptr{Cuchar}, Csize_t), jfd, buf, N)
    
    if res != -1        
        println("read() returned $(res) bytes")
        println(buf[1:res])
        global count += 1
    end
    
end

ccall(:close, Cint, (Cint,), jfd)

With a gamepad attached to my system I get output like this:

melis@juggle 15:06:~$ julia t.jl
fd = 18
# Initial joystick data returned
read() returned 96 bytes
UInt8[0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x00, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x01, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x02, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x03, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x04, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x05, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x06, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x07, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x08, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x09, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x81, 0x0a, 0xcb, 0x6d, 0xc0, 0x55, 0x00, 0x00, 0x82, 0x00, 0x00]
read() returned 56 bytes
UInt8[0x43, 0x6e, 0xc0, 0x55, 0xfe, 0xff, 0x82, 0x01, 0x43, 0x6e, 0xc0, 0x55, 0x01, 0x80, 0x82, 0x02, 0x43, 0x6e, 0xc0, 0x55, 0x00, 0x00, 0x82, 0x03, 0x43, 0x6e, 0xc0, 0x55, 0xfe, 0xff, 0x82, 0x04, 0x43, 0x6e, 0xc0, 0x55, 0x01, 0x80, 0x82, 0x05, 0x43, 0x6e, 0xc0, 0x55, 0x00, 0x00, 0x82, 0x06, 0x43, 0x6e, 0xc0, 0x55, 0x00, 0x00, 0x82, 0x07, 0xcb]
# read() continuously returns -1 within the loop, until I trigger the joystick again
read() returned 8 bytes
UInt8[0x5a, 0x73, 0xc0, 0x55, 0xdd, 0xf4, 0x02, 0x00, 0x43]
read() returned 8 bytes
UInt8[0x61, 0x73, 0xc0, 0x55, 0xb0, 0xe5, 0x02, 0x00, 0x43]
read() returned 8 bytes
UInt8[0x68, 0x73, 0xc0, 0x55, 0x80, 0xd5, 0x02, 0x00, 0x43]

You would then have to map the returned bytes to the actual joystick data you need, but hopefully this is a start.

Edit: and to verify that this indeed opens the device file with the correct options:

melis@juggle 15:10:~$ strace julia t.jl 2>&1 | grep js0
openat(AT_FDCWD, "/dev/input/js0", O_RDONLY|O_NONBLOCK) = 16

Edit 2: fixed off-by-one in indexing buf, due to my Python habits :wink:

2 Likes

The following code works:

const O_RDONLY = 0
const O_NONBLOCK = 2048

function open_joystick(filename = "/dev/input/js0")
    if  Sys.islinux()
        if ispath(filename)
            jfd = ccall(:open, Cint, (Cstring, Cint), filename, O_RDONLY|O_NONBLOCK)
            device = JSDevice(filename, jfd, 0, 0)
            device.axis_count = axis_count(device) 
            device.button_count = button_count(device)
            return device
        else
            println("ERROR: The device $filename does not exist!")
            return nothing
        end
    else
        error("Currently Joystick.jl supports only Linux!")
        nothing
    end
end
function read_event(js::JSDevice)
    event = Vector{UInt8}(undef, sizeof(JSEvent))
    res = ccall(:read, Cssize_t, (Cint, Ptr{Cuchar}, Csize_t), js.fd, event, sizeof(JSEvent))
    if res == -1    
        return nothing
    end
    reinterpret(JSEvent, event)[]
end

Complete code: GitHub - ufechner7/Joysticks.jl: Joystick interface for Julia

Thank you very much! :slight_smile:

1 Like