Dart on Pi - Tracking Changes



Tracking Raspberry Pi GPIO values

One application of the RPi is to track sensor changes over time. For example, a RPi could have various sensors for detecting water in places on the floor. The Dart program running on the RPi could detect the sensor change and send an email (see mailer pub package for sending email using Dart) reporting the problem. We can read the GPIO pin values as discussed in the previous post, but how to track them over time? Two possibilities include:

  • Polling
  • Interrupts

Polling

Using the rpi_gpio pub package mentioned in the previous post, one approach for detecting a sensor change is to periodically check the value and compare it with the previous value. If the value has changed since the last time it was read, then we can react to that change. To start, create a sensor class for tracking the value of a GPIO pin. Periodically call the sensor's hasChanged method to determine if the pin has changed state.

class Sensor {
 final Pin pin;
 int value;

 Sensor(this.pin) {
   value = pin.value;
 }

 bool hasChanged() {
   var oldValue = value;
   value = pin.value;
   return value != oldValue;
 }

 toString() => '${value} => ${pin.description}';
}

For testing on platforms other than the RPi, create a MockHardware class to simulate some pins changing state. The elapsedMillisecondsSinceTrigger method returns the number of milliseconds since the pin value changed and is used to report the accuracy of the event.

class MockHardware implements GpioHardware {
 static var triggerTime = now;

 static get elapsedMillisecondsSinceTrigger =>
     now.difference(triggerTime).inMilliseconds;

 static DateTime get now => new DateTime.now();

 List<int> values = [0, 0, 0, 0, 0, 0, 0, 0];

 MockHardware() {
   new Timer(new Duration(milliseconds: 17), () => _changed(3, 1));
   new Timer(new Duration(milliseconds: 24), () => _changed(1, 1));
   new Timer(new Duration(milliseconds: 32), () => _changed(2, 1));
   new Timer(new Duration(milliseconds: 59), () => _changed(6, 1));
 }

 int digitalRead(int pinNum) => values[pinNum];

 noSuchMethod(Invocation invocation) =>
super.noSuchMethod(invocation);
 void pinMode(int pinNum, int mode) {}

 int _changed(int pinNum, int value) {
   values[pinNum] = value;
   triggerTime = now;
   return value;
 }
}

Now tie it all together with a main method that calls the GPIO pins when running on the RPi, but uses the MockHardware when running on other platforms.

import 'dart:async';

import 'package:rpi_gpio/rpi_gpio.dart';
import 'package:rpi_gpio/rpi_hardware.dart' deferred as rpi;

/// Monitor current values for pins 0 - 7 using polling
main() {
 if (isRaspberryPi) {
   // Initialize the underlying hardware library
   rpi.loadLibrary();
   Gpio.hardware = new rpi.RpiHardware();
 } else {
   // Mock the hardware when testing
   Gpio.hardware = new MockHardware();
 }
 var stopwatch = new Stopwatch()..start();

 var gpio = Gpio.instance;
 var sensors = [];
 for (int pinNum = 0; pinNum < 8; ++pinNum) {
   var sensor = new Sensor(gpio.pin(pinNum, input));
   sensors.add(sensor);
   print(sensor);
 }

 print('=== polling for changes');
 int count = 0;
 new (new Duration(milliseconds: 5), (Timer timer) {
   sensors.forEach((Sensor sensor) {
     if (sensor.hasChanged()) {
       print('${stopwatch.elapsedMilliseconds}, '
      '${MockHardware.elapsedMillisecondsSinceTrigger} : $sensor');
     }
   });
   ++count;
   if (count >= 20) timer.cancel();
 });
}

Running this example on non-RPi, generates something like this.

0 => Pin 0  (BMC_GPIO 17, Phys 11)
0 => Pin 1  (BMC_GPIO 18, Phys 12, PMW)
0 => Pin 2  (BMC_GPIO 27, Phys 13)
0 => Pin 3  (BMC_GPIO 22, Phys 15)
0 => Pin 4  (BMC_GPIO 23, Phys 16)
0 => Pin 5  (BMC_GPIO 24, Phys 18)
0 => Pin 6  (BMC_GPIO 25, Phys 22)
0 => Pin 7  (BMC_GPIO 4,  Phys 7,  Clock)
=== polling for changes
18, 3 : 1 => Pin 3  (BMC_GPIO 22, Phys 15)
28, 4 : 1 => Pin 1  (BMC_GPIO 18, Phys 12, PMW)
33, 0 : 1 => Pin 2  (BMC_GPIO 27, Phys 13)
63, 2 : 1 => Pin 6  (BMC_GPIO 25, Phys 22)

Note that the accuracy of the events (3, 4, 0, and 2 respectively) is between 0 and 4 milliseconds. To increase the accuracy we could increase polling frequency, but that would increase the load on the CPU.

Interrupts

An alternative approach is to use interrupts and let the underlying operating system notify your application when a change has occurred. The rpi_gpio pub package surfaces a stream of pin value change events via the Pin events method. For testing on platforms other than the RPi, create a MockHardware class to simulate some pins changing state. As in the polling example above, the elapsedMillisecondsSinceTrigger method returns the number of milliseconds since the pin value changed and is used to report the accuracy of the event.

class MockHardware implements GpioHardware {
 static var triggerTime = now;

 static get elapsedMillisecondsSinceTrigger =>
     now.difference(triggerTime).inMilliseconds;

 static DateTime get now => new DateTime.now();

 List<int> values = [0, 0, 0, 0, 0, 0, 0, 0];

 SendPort interruptPort;

 MockHardware() {
   new Timer(new Duration(milliseconds: 17), () => _changed(3, 1));
   new Timer(new Duration(milliseconds: 24), () => _changed(1, 1));
   new Timer(new Duration(milliseconds: 32), () => _changed(2, 1));
   new Timer(new Duration(milliseconds: 59), () => _changed(6, 1));
 }

 int digitalRead(int pinNum) => values[pinNum];

 @override
 int enableInterrupt(int pinNum) => -1;

 @override
 void initInterrupts(SendPort port) {
   interruptPort = port;
 }

 noSuchMethod(Invocation invocation) =>
super.noSuchMethod(invocation);

 void pinMode(int pinNum, int mode) {}

 int _changed(int pinNum, int value) {
   values[pinNum] = value;
   triggerTime = now;
   interruptPort.send(pinNum | (value != 0 ? 0x80 : 0));
   return value;
 }
}

As before, tie it all together with a main method that calls the GPIO pins when running on the RPi, but uses the MockHardware when running on other platforms.

main() {
 if (isRaspberryPi) {
   // Initialize the underlying hardware library
   rpi.loadLibrary();
   Gpio.hardware = new rpi.RpiHardware();
 } else {
   // Mock the hardware when testing
   Gpio.hardware = new MockHardware();
 }
 var stopwatch = new Stopwatch()..start();

 var gpio = Gpio.instance;
 var subscriptions = [];
 for (int pinNum = 0; pinNum < 8; ++pinNum) {
   var pin = gpio.pin(pinNum, input);
   subscriptions.add(pin.events.listen((PinEvent event) {
     print('${stopwatch.elapsedMilliseconds}, '
         '${MockHardware.elapsedMillisecondsSinceTrigger} : '
         '${event.value} => ${event.pin}');
   }));
   print('${pin.value} => ${pin}');
 }

 print('=== wait for changes');
 new Timer(new Duration(milliseconds: 100), () {
   for (var s in subscriptions) {
     s.cancel();
   }
 });
}

Running this example on non-RPi, generates something like this.

Observatory listening on http://127.0.0.1:54884
0 => Pin 0  (BMC_GPIO 17, Phys 11) PinMode.input
0 => Pin 1  (BMC_GPIO 18, Phys 12, PMW) PinMode.input
0 => Pin 2  (BMC_GPIO 27, Phys 13) PinMode.input
0 => Pin 3  (BMC_GPIO 22, Phys 15) PinMode.input
0 => Pin 4  (BMC_GPIO 23, Phys 16) PinMode.input
0 => Pin 5  (BMC_GPIO 24, Phys 18) PinMode.input
0 => Pin 6  (BMC_GPIO 25, Phys 22) PinMode.input
0 => Pin 7  (BMC_GPIO 4,  Phys 7,  Clock) PinMode.input
=== wait for changes
25, 4 : 1 => Pin 3  (BMC_GPIO 22, Phys 15) PinMode.input
25, 0 : 1 => Pin 1  (BMC_GPIO 18, Phys 12, PMW) PinMode.input
33, 0 : 1 => Pin 2  (BMC_GPIO 27, Phys 13) PinMode.input
62, 0 : 1 => Pin 6  (BMC_GPIO 25, Phys 22) PinMode.input

Note that after some initial setup cost, the accuracy of the interrupt events (4, 0, 0, and 0 respectively) is much higher than the corresponding polling event without the increased load on the CPU.

Comments

Popular posts from this blog

Dart on Pi - Getting Started

Dartino - Getting Started