androidkotlinjava-native-interfacesysfsdragonboard

how to use poll(2) or select(2) service call to watch a pseudo file for changes with Kotlin


I am working with a DragonBoard 410C using Android 5.1 and Kotlin to experiment with the GPIO pins on the 40 pin low power connector. The library I'm using is using the sysfs interface for interacting with the GPIO pins which requires opening various pseudo files in /sys/class/gpio/ directory tree and reading and writing values to those files, see accessing GPIO low power connector on DragonBoard 410C running Android

My understanding is that I can provision a GPIO pin as Input and Edge triggered which would allow me to wire up a simple circuit with a momentary contact switch and be able to detect when the switch is pressed.

However the documentation I've found indicates that I need to use the poll(2) system service or the select(2) system service on a file descriptor for the /value pseudo file of the GPIO pin I am using in order to detect when the Edge is detected, e.g. /sys/class/gpio/gpio910/value.

How do I use the poll(2) or select(2) system services with a file descriptor in Kotlin? Is poll(2) the same as ready() method of FileReader?

Perhaps something similar to the Java WatchService functionality is needed? http://www.java2s.com/Tutorials/Java/java.nio.file/WatchService/0060__WatchService.poll_.htm

What I am planning, unless this is the wrong approach, is to have a utility function, something like:

// pollPseudoFile() polls the specified GPIO pin using the sysfs interface in order
// to wait for a pin value due to an external event such as a momentary switch press
//
// Returns:
//   - 0  -> an error of some kind
//   - 1  -> time out expired with no value
//   - 2  -> value change detected
public fun pollPseudoFile (pinPathFull : String, timeOut : Int) : Int {
    println("    pollPseudoFile - String")
    var iStatus : Int = 0     // status return indicating if poll() completed or not.
    try {
        val br = FileReader(pinPathFull)
        br.poll(timeOut)     // wait for data to be available or time out. how to do poll?
        iStatus = 2          // indicate value change unless the poll() timed out
        if (br.pollExpired) iStatus = 1     // poll timed out. how to check for poll status?
        br.close()
    } catch (e: Exception) {
        println("Error: " + e.message)
    }

    return iStatus;
}

public fun pollGetValue (pinPathFull : String) : Int {
    println("    pollGetValue - String")
    var line = ""
    try {
        val br = BufferedReader(FileReader(pinPathFull))
        line = br.readLine()
        br.close()
    } catch (e: Exception) {
        println("Error: " + e.message)
    }

    return line.toInt()
}

https://www.kernel.org/doc/Documentation/gpio/sysfs.txt

"value" ... reads as either 0 (low) or 1 (high). If the GPIO is configured as an output, this value may be written; any nonzero value is treated as high.

If the pin can be configured as interrupt-generating interrupt and if it has been configured to generate interrupts (see the description of "edge"), you can poll(2) on that file and poll(2) will return whenever the interrupt was triggered. If you use poll(2), set the events POLLPRI and POLLERR. If you use select(2), set the file descriptor in exceptfds. After poll(2) returns, either lseek(2) to the beginning of the sysfs file and read the new value or close the file and re-open it to read the value.

"edge" ... reads as either "none", "rising", "falling", or "both". Write these strings to select the signal edge(s) that will make poll(2) on the "value" file return.

This file exists only if the pin can be configured as an interrupt generating input pin.

Additional notes

Note 1: Using the adb utility I was able to shell into my DragonBoard 410C and tested configuring physical pin 26, GPIO971, with direction set to in and edge set to rising. Using a simple LED circuit on a breadboard that was tied to physical pin 23, GPIO938, and adding a wire from physical pin 26 to the LED managed by physical pin 23, I was able to turn on the LED with echo 1 > gpio938/value and then cat gpio971/value to see that the value for physical pin 26 had gone high and was reading as 1. I then turned the LED connected to physical pin 23 off with echo 0 > gpio938/value and then cat gpio971/value returned a value of 0 as expected.

However this experiment does not tell me if a poll(2) would indicate a change on gpio971/value when the LED was turned on and turned off or not.

Note 1a: I have a first version of a Native C++ JNI function to implement the poll(2) service call and have been testing it with my DragonBoard 410C. What I am seeing is that the poll(2) function is immediately returning with both POLLIN and POLLERR set in the revents member of the struct pollfd array.

The test is using physical pin 26 connected to a breadboard row with one leg of an LED connected to physical pin 23 which I am able to turn on and off. When I attempt to turn on polling with a 10000 ms time out, the call returns immediately whether the LED is lit (pin 26 value is 1) or not lit (pin 26 value is 0) with both indicators set.

My expectation is that since I have edge set to rising, I should see the poll(2) return only when the LED is unlit and I then turn it on or 10 seconds have elapsed.

Am continuing my investigation as it strikes me there may a problem with how I'm using the Native C++ function I wrote in the Kotlin side of the app.

Note 2: I attempted to use WatchService with my Kotlin application and ran into an error that WatchService required an API level 26 and my minimum target in Android Studio is API level 22. It looks like WatchService requires Android 8.0 (Oreo) while the DragonBoard is at Android 5.1 (Lollipop) so using WatchService to monitor file status is not available to me.


Solution

  • The approach I am taking is to create a native C++ JNI function to provide a way to implement the poll(2) Linux service call.

    One interesting issue I ran into during development and testing was the poll() returning immediately rather than waiting on either a time out or a voltage to the GPIO input pin. After posting on the 96Boards.org forum for the DragonBoard 410C, How to use poll() with sysfs interface to input GPIO pin to handle a switch press event, someone proposed a possible solution which worked, to read the pseudo file before starting the poll(2).

    In order to use this function, I need to have some kind of a Kotlin coroutine or side thread so that when the main UI is processing a button click which starts the polling of the GPIO input pin, the main UI thread is not blocked until the function returns with either a GPIO event or a time out.

    I have not yet been able to discern how to do such a coroutine so this is still a work in progress. After some thinking, it appears that some kind of an event listener architecture would be the most appropriate approach.

    However testing indicates that the function pollPseudoFile() is working properly by either doing a time out or returning with a value from /value when a voltage is applied by hand using a wire from the 1.8v power (pin 38) to the GPIO input pin that is set with either a rising or falling setting in the /edge pseudo file.

    The source code for the Native C++ JNI function is below. I am using it with the following Kotlin source code.

    First of all in my MainActivity.kt source file, I make the Native C++ library available with the following source:

    // See the StackOverFlow question with answer at URL:
    //    https://stackoverflow.com/questions/36932662/android-how-to-call-ndk-function-from-kotlin
    init {
        System.loadLibrary("pollfileservice")
    }
    
    external fun pollFileWithTimeOut(pathPseudo : String, timeOutMs : Int): Int
    external fun pollGetLastRevents() : Int
    

    Next I'm using this function in the Kotlin source file Gpio.kt to actually perform the poll() service call on the pseudo file.

    class Gpio(pin: Int)  {
        private val pin : Int
        private val pinGpio : GpioFile = GpioFile()
    
        /*
         *  The GPIO pins are represented by folders in the Linux file system
         *  within the folder /sys/class/gpio. Each pin is represented by a folder
         *  whose name is the prefix "gpio" followed by the pin number.
         *  Within the folder representing the pin are two files, "value" used to
         *  set or get the value of the pin and "direction" used to set or get
         *  the direction of the pin.
         *
         *  This function creates the path to the Linux file which represents a particular
         *  GPIO pin function, "value" or "direction".
         */
        private fun MakeFileName(pin: Int, op: String): String {
            return "/sys/class/gpio/gpio$pin$op"
        }
    
    
        //    ....... other source code in the Kotlin class Gpio
    
    
        fun pinPoll (timeMs: Int) : Int {
            val iStatus : Int = pinGpio.pollPseudoFile (MakeFileName(pin,  "/value"), timeMs)
            return iStatus
        }
    

    The above Gpio class is used in the actual UI button click listener as follows:

                val gpioProcessor = GpioProcessor()
                // Get reference of GPIO23.
                val gpioPin26 = gpioProcessor.pin26
    
                // Set GPIO26 as input.
                gpioPin26.pinIn()
                gpioPin26.pinEdgeRising()
    
                var xStatus: Int = gpioPin26.pinPoll(10000)
                val xvalue = gpioPin26.value
    

    PollFileService.h

    //
    // Created by rchamber on 9/24/2020.
    //
    
    #ifndef MY_APPLICATION_POLLFILESERVICE_H
    #define MY_APPLICATION_POLLFILESERVICE_H
    
    
    class PollFileService {
    private:
        int iValue;
        int fd;         /* file descriptor */
    
    public:
        // See poll(2) man page at https://linux.die.net/man/2/poll
        static const int PollSuccess = 0;
        static const int PollTimeOut = 1;
        static const int PollErrorEFAULT = -1;
        static const int PollErrorEINTR  = -2;
        static const int PollErrorEINVAL = -3;
        static const int PollErrorENOMEM = -4;
        static const int PollErrorPOLLERR = -5;
        static const int PollErrorPOLLNVAL = -6;
        static const int PollErrorPOLLERRNVAL = -7;
        static const int PollErrorPOLLHUP = -8;
        static const int PollErrorPOLLERRDEFLT = -9;
    
        static const int PollErrorUNKNOWN = -100;
    
        static int iPollStatus;
        static int iPollRet;
        static int iPollRevents;
    
        PollFileService(const char *pathName = nullptr, int timeMilliSec = -1);
        ~PollFileService();
    
        int PollFileCheck (const char *pathName, int timeMilliSec = -1);
        int PollFileRead (const char *pathName = nullptr);
    };
    
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_example_myapplication_MainActivity_pollFileWithTimeOut (JNIEnv* pEnv, jobject pThis, jstring pKey, jint timeMS);
    
    #endif //MY_APPLICATION_POLLFILESERVICE_H
    

    PollFileService.cpp

    //
    // Created by rchamber on 9/24/2020.
    //
    
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <math.h>
    #include <errno.h>
    #include <poll.h>
    
    #include <jni.h>
    
    #include "PollFileService.h"
    
    int PollFileService::iPollStatus = 0;
    int PollFileService::iPollRet = 0;
    int PollFileService::iPollRevents = 0;
    
    PollFileService::PollFileService(const char *pathName /* = nullptr */, int timeMilliSec /* = -1 */) : iValue(23), fd(-1)
    {
        iPollStatus = 0;
        if (pathName) {
            fd = open (pathName, O_RDONLY);
        }
    }
    
    PollFileService::~PollFileService()
    {
        if (fd >= 0) {
            close (fd);
            fd = -1;
        }
    }
    
    int PollFileService::PollFileCheck(const char *pathName, int timeMilliSec /* = -1 */)
    {
        struct pollfd fdList[] = {
                {fd, POLLPRI | POLLERR, 0},
                {0}
            };
        nfds_t nfds = 1;
        unsigned char tempbuff[256] = {0};
    
        if (fd < 0 && pathName) {
            fd = open (pathName, O_RDONLY);
            fdList[0].fd = fd;
        }
    
        // with a edge triggered GPIO that we are going to use the poll(2)
        // function to wait on an event, we need to read from the
        // pin before we do the poll(2). If the read is not done then
        // the poll(2) returns with both POLLPRI and POLLERR set in the
        // revents member. however if we read first then do the poll2()
        // the poll(2) will wait for the event, input voltage change with
        // either a rising edge or a falling edge, depending on the setting
        // in the /edge pseudo file.
        ssize_t iCount = read (fdList[0].fd, tempbuff, 255);
    
        iPollStatus = PollErrorUNKNOWN;
        int iRet = poll(fdList, nfds, timeMilliSec);
    
        if (iRet == 0) {
            iPollStatus = PollTimeOut;
        } else if (iRet < 0) {
            switch (errno) {
                case EFAULT:
                    iPollStatus = PollErrorEFAULT;
                    break;
                case EINTR:
                    iPollStatus = PollErrorEINTR;
                    break;
                case EINVAL:
                    iPollStatus = PollErrorEINVAL;
                    break;
                case ENOMEM:
                    iPollStatus = PollErrorENOMEM;
                    break;
                default:
                    iPollStatus = PollErrorUNKNOWN;
                    break;
            }
        } else if (iRet > 0) {
            // successful call now determine what we should return.
            iPollRevents = fdList[0].revents; /* & (POLLIN | POLLPRI | POLLERR); */
            switch (fdList[0].revents & (POLLIN | POLLPRI | POLLERR /* | POLLNVAL | POLLHUP*/)) {
                case (POLLIN):                // value of 1, There is data to read.
                case (POLLPRI):               // value of 2, There is urgent data to read
                case (POLLOUT):               // , Writing now will not block.
                case (POLLIN | POLLPRI):      // value of 3
                    iPollStatus = PollSuccess;
                    break;
    
                // testing with a DragonBoard 410C indicates that we may
                // see the POLLERR indicator set in revents along with
                // the POLLIN and/or POLLPRI indicator set indicating there
                // is data to be read.
                // see as well poll(2) man page which states:
                //    POLLERR  Error condition (output only).
                case (POLLIN | POLLERR):                 // value of 9
                case (POLLPRI | POLLERR):                // value of 10
                case (POLLIN | POLLPRI | POLLERR):       // value of 11
                    iPollStatus = PollSuccess;
                    break;
    
                case (POLLHUP):               // , Hang up (output only).
                    iPollStatus = PollErrorPOLLHUP;
                    break;
    
                case (POLLERR):               // value of 8, Error condition (output only).
                    iPollStatus = PollErrorPOLLERR;
                    break;
                case (POLLNVAL):              // , Invalid request: fd not open (output only).
                    iPollStatus = PollErrorPOLLNVAL;
                    break;
                case (POLLERR | POLLNVAL):
                    iPollStatus = PollErrorPOLLERRNVAL;
                    break;
    
                default:
                    iPollStatus = PollErrorPOLLERRDEFLT;
                    break;
            }
        }
    
        return iPollStatus;
    }
    
    int PollFileService::PollFileRead (const char *pathName /* = nullptr */)
    {
        char  buffer[12] = {0};
        int iRet = -1;
    
        if (fd < 0 && pathName) {
            fd = open (pathName, O_RDONLY);
        }
        int nCount = read (fd, buffer, 10);
        if (nCount > 0) {
            iRet = atoi (buffer);
        }
    
        return iRet;
    }
    
    // Check the specified file using the poll(2) service and
    // return a status as follows:
    //  -    0  -> poll(2) success indicating something is available
    //  -    1  -> poll(2) failed with time out before anything available
    //  -   -1  -> poll(2) error - EFAULT
    //  -   -2  -> poll(2) error - EINTR
    //  -   -3  -> poll(2) error - EINVAL
    //  -   -4  -> poll(2) error - ENOMEM
    //  -   -5  -> poll(2) error - POLLERR
    //  -   -6  -> poll(2) error - POLLNVAL
    //  -   -7  -> poll(2) error - POLLERR | POLLNVAL
    //  -   -8  -> poll(2) error - POLLHUP
    //  -   -9  -> poll(2) error - poll(2) revent indicator Unknown
    //  - -100 -> poll(2) error - Unknown error
    //
    static int lastRevents = 0;
    
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_example_myapplication_MainActivity_pollFileWithTimeOut (JNIEnv* pEnv, jobject pThis, jstring pKey, jint timeMS)
    {
        char *pathName;
        int timeMilliSec;
        PollFileService  myPoll;
    
        const char *str = pEnv->GetStringUTFChars(pKey, 0);
        int  timeMSint = 10000; // timeMS;
    
    #if 1
        int iStatus = myPoll.PollFileCheck(str, timeMSint);
    #else
        int iStatus = myPoll.PollFileRead(str);
    #endif
    
        pEnv->ReleaseStringUTFChars(pKey, str);
    
        lastRevents = myPoll.iPollRevents;
    
        return iStatus;
    }
    
    #if 0
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_example_myapplication_MainActivity_pollGetLastStatus (JNIEnv* pEnv, jobject pThis) {
        return PollFileService::iPollStatus;
    }
    #endif
    
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_example_myapplication_MainActivity_pollGetLastRevents (JNIEnv* pEnv, jobject pThis)
    {
        return lastRevents;
    }