Search code examples
androiddriverkotlinandroid-thingsproximitysensor

Android Things UserSensor.Builder - unable to create distance sensor driver


I'm attempting to create an Android Things driver for the standard HC-SR04 ultrasonic sensor. I believe I've got the sequence of events correct: see footer, but have been unable to register it as a UserSensor.

userSensor = UserSensor.Builder()
    .setName("HC-SR04 Ultrasonic Distance Sensor")
    .setVersion(1)
    // If boolean "on face or not," should I use something linear like TYPE_LIGHT
    .setType(Sensor.TYPE_PROXIMITY) 
    .setDriver(this) // UserSensorDriver  
    .build()

at this point, what is the difference between registering the UserSensor with the UserDriverManager (done), and registering it with the SensorManager? Is there anything that is preventing it from showing up in the list of Sensors? Do I need to wait until the sensor is "ready" like with sensorManager.registerDynamicSensorCallback?

val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensorManager.registerListener(this, // SensorEventListener.onSensorChanged
    sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY),
    SensorManager.SENSOR_DELAY_NORMAL)

No matter what I try, I get "E/SensorManager: sensor or listener is null" (which is even more surprising in Kotlin because nulls aren't supposed to sneak in as much)


My sensor / also a gist:

/** Callback for when the distance changes "enough to care" */
interface SignificantDistanceChangeListener {
    fun onDistanceChanged(distanceCm: Float)
}

/**
 * User Sensor - Ultrasonic range finder
 */
class HCSR04(context: Context, val sdcl: SignificantDistanceChangeListener) : UserSensorDriver(), SensorEventListener, AutoCloseable {
    private val LOG = Logger.getLogger(this.javaClass.name)
    private val gpio = PeripheralManagerService().openGpio("BCM23")
    private val distanceReading: BlockingQueue<Float> = ArrayBlockingQueue(1)
    // Choreography of each ping
    private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
    private val userSensor: UserSensor

    init {
        userSensor = UserSensor.Builder()
                .setName("HC-SR04 Ultrasonic Distance Sensor")
                .setVersion(1)
                .setType(Sensor.TYPE_PROXIMITY) // Could this be something more linear like TYPE_LIGHT
                .setDriver(this)
                .build()
        UserDriverManager.getManager().registerSensor(userSensor)

        val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
        LOG.info("ALL Sensors: ${sensorManager.getSensorList(Sensor.TYPE_ALL)}")

        sensorManager.registerDynamicSensorCallback(object : SensorManager.DynamicSensorCallback() {
            override fun onDynamicSensorConnected(sensor: Sensor) {
                LOG.info("onDynamicSensorConnected")
                if (sensor.type == Sensor.TYPE_PROXIMITY) {
                    sensorManager.registerListener(
                            this@HCSR04,
                            sensor,
                            SensorManager.SENSOR_DELAY_NORMAL
                    )
                }
            }
        })

    }

    val gpioEdgeCallback = object : GpioCallback() {
        // Track the reply rise/fall
        private val startMs = AtomicLong()
        private val startValid = AtomicBoolean(false)

        private fun calculate() {
            val elapsed = (System.nanoTime() / 1000) - startMs.get()
            if (startValid.get() && elapsed > 0) {
                distanceReading.put(elapsed * 34000 / 2f)
            } else {
                LOG.warning("Discarding edge callback ${startMs.get()} ${startValid.get()} $elapsed")
            }
            startValid.set(false)
        }

        override fun onGpioEdge(gpio: Gpio?): Boolean {
            if (gpio != null) {
                if (gpio.value) {
                    startMs.set(System.nanoTime() / 1000)
                    startValid.set(true)
                } else {
                    calculate()
                }
                LOG.finer("GPIO input edge: ${System.nanoTime() / 1000} ${gpio.value}")
            }
            return true
        }

        override fun onGpioError(gpio: Gpio?, error: Int) = LOG.severe("$gpio Error event $error")
    }

    /** Launch a new thread to get the distance, then block until we have a result */
    override fun read(): UserSensorReading {
        distanceReading.clear()

        gpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW)
        gpio.setActiveType(Gpio.ACTIVE_HIGH)
        gpio.value = false

        scheduler.schedule({ gpio.value = true }, 1, TimeUnit.MICROSECONDS)
        scheduler.schedule({ gpio.value = false }, 11, TimeUnit.MICROSECONDS)
        scheduler.schedule({
            gpio.setDirection(Gpio.DIRECTION_IN)
            gpio.setActiveType(Gpio.ACTIVE_HIGH) // redundant?
            gpio.setEdgeTriggerType(Gpio.EDGE_BOTH)
            gpio.registerGpioCallback(gpioEdgeCallback)
        }, 12, TimeUnit.MICROSECONDS)

        val distanceCm = distanceReading.take()
        gpio.unregisterGpioCallback(gpioEdgeCallback)
        LOG.info("New distance reading: $distanceCm")
        return UserSensorReading(floatArrayOf(distanceCm))
    }

    /** from @SensorEventListener */
    override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = LOG.info("$sensor accuracy change: $accuracy")

    /**
     * from @SensorEventListener
     */
    override fun onSensorChanged(event: SensorEvent) = sdcl.onDistanceChanged(event.values[0])

    /** from @AutoCloseable */
    override fun close() {
        LOG.warning("Closing Sensor HCSR04")
        UserDriverManager.getManager().unregisterSensor(userSensor)
        gpio.close()
        scheduler.shutdownNow()
    }
}

Solution

  • One thing you might consider is changing the sensor type. TYPE_PROXIMITY is an on-change sensor, which is supported in the current preview. However it's also a wakeup sensor, which might not be fully supported yet. You could try modifying your sensor definition to use a custom type instead:

    userSensor = UserSensor.Builder()
            .setName("HC-SR04 Ultrasonic Distance Sensor")
            .setVersion(1)
            .setCustomType(Sensor.TYPE_DEVICE_PRIVATE_BASE,
                    "com.example.ultrasonic",
                    Sensor.REPORTING_MODE_CONTINUOUS)
            .setDriver(this)
            .build()
    

    at this point, what is the difference between registering the UserSensor with the UserDriverManager (done), and registering it with the SensorManager?

    You can't register a UserSensor directly with SensorManager. The Android SensorManager API exists to enable client applications to read data from sensors built into the device. The UserDriverManager API exists to enable Android Things developers to add new sensors to the system that you may want to read elsewhere in the code using the same SensorManager API.

    To put it another way, you build a UserSensor to inject your custom sensor data into the framework via UserDriverManager. You use SensorManager to extract the data provided to the framework and use it in a client app.

    Is there anything that is preventing it from showing up in the list of Sensors?

    You should be able to test this using SensorManager.getDynamicSensorList() (not the same as the getSensorList() method) after the sensor callback triggers.

    Do I need to wait until the sensor is "ready" like with sensorManager.registerDynamicSensorCallback?

    The dynamic callback tells you when the new driver has been successfully registered with the framework. You won't be able to attach a listener or query the sensor itself until after onDynamicSensorConnected() is called.