diff --git a/docs/software/TODO.md b/docs/software/TODO.md index 2940a5fb..46f943c0 100644 --- a/docs/software/TODO.md +++ b/docs/software/TODO.md @@ -10,7 +10,7 @@ For app cleanup: * DONE write devapi user guide * DONE update android code: https://developer.android.com/topic/libraries/view-binding/migration * only do wantReplies once per packet type, if we change network settings force it again -* make gpio watch work, use thread and setup +* test GPIO watch * DONE make hello world example service * make python ping command * DONE have python tool check max packet size before sending to device diff --git a/docs/software/plugin-api.md b/docs/software/plugin-api.md index ba544928..e0699a6b 100644 --- a/docs/software/plugin-api.md +++ b/docs/software/plugin-api.md @@ -49,6 +49,11 @@ The easiest way to get started is: * Rebuild with your new messaging goodness and install on the device * Use the [meshtastic commandline tool](https://github.com/meshtastic/Meshtastic-python) to send a packet to your board "meshtastic --dest 1234 --ping" +## Threading + +It is very common that you would like your plugin to be invoked periodically. +We use a crude/basic cooperative threading system to allow this on any of our supported platforms. Simply inherit from OSThread and implement runOnce(). See the OSThread [documentation](/src/concurrency/OSThread.h) for more details. For an example consumer of this API see RemoteHardwarePlugin::runOnce. + ## Picking a port number For any new 'apps' that run on the device or via sister apps on phones/PCs they should pick and use a unique 'portnum' for their application. diff --git a/src/plugins/RemoteHardwarePlugin.cpp b/src/plugins/RemoteHardwarePlugin.cpp index 11aaad23..dee93363 100644 --- a/src/plugins/RemoteHardwarePlugin.cpp +++ b/src/plugins/RemoteHardwarePlugin.cpp @@ -10,6 +10,44 @@ RemoteHardwarePlugin remoteHardwarePlugin; #define NUM_GPIOS 64 +// Because (FIXME) we currently don't tell API clients status on sent messages +// we need to throttle our sending, so that if a gpio is bouncing up and down we +// don't generate more messages than the net can send. So we limit watch messages to +// a max of one change per 30 seconds +#define WATCH_INTERVAL_MSEC (30 * 1000) + +/// Set pin modes for every set bit in a mask +static void pinModes(uint64_t mask, uint8_t mode) { + for (uint8_t i = 0; i < NUM_GPIOS; i++) { + if (mask & (1 << i)) { + pinMode(i, mode); + } + } +} + +/// Read all the pins mentioned in a mask +static uint64_t digitalReads(uint64_t mask) { + uint64_t res = 0; + + pinModes(mask, INPUT_PULLUP); + + for (uint8_t i = 0; i < NUM_GPIOS; i++) { + uint64_t m = 1 << i; + if (mask & m) { + if (digitalRead(i)) + res |= m; + } + } + + return res; +} + + +RemoteHardwarePlugin::RemoteHardwarePlugin() + : ProtobufPlugin("remotehardware", PortNum_REMOTE_HARDWARE_APP, HardwareMessage_fields), + concurrency::OSThread("remotehardware") +{ +} bool RemoteHardwarePlugin::handleReceivedProtobuf(const MeshPacket &req, const HardwareMessage &p) { @@ -22,24 +60,17 @@ bool RemoteHardwarePlugin::handleReceivedProtobuf(const MeshPacket &req, const H uint64_t mask = 1 << i; if (p.gpio_mask & mask) { digitalWrite(i, (p.gpio_value & mask) ? 1 : 0); - pinMode(i, OUTPUT); } } + pinModes(p.gpio_mask, OUTPUT); + break; - + case HardwareMessage_Type_READ_GPIOS: { // Print notification to LCD screen screen->print("Read GPIOs\n"); - uint64_t res = 0; - for (uint8_t i = 0; i < NUM_GPIOS; i++) { - uint64_t mask = 1 << i; - if (p.gpio_mask & mask) { - pinMode(i, INPUT_PULLUP); - if (digitalRead(i)) - res |= (1 << i); - } - } + uint64_t res = digitalReads(p.gpio_mask); // Send the reply HardwareMessage reply = HardwareMessage_init_default; @@ -51,6 +82,14 @@ bool RemoteHardwarePlugin::handleReceivedProtobuf(const MeshPacket &req, const H break; } + case HardwareMessage_Type_WATCH_GPIOS: { + watchGpios = p.gpio_mask; + lastWatchMsec = 0; // Force a new publish soon + previousWatch = ~watchGpios; // generate a 'previous' value which is guaranteed to not match (to force an initial publish) + enabled = true; // Let our thread run at least once + break; + } + case HardwareMessage_Type_READ_GPIOS_REPLY: case HardwareMessage_Type_GPIOS_CHANGED: break; // Ignore - we might see our own replies @@ -61,3 +100,30 @@ bool RemoteHardwarePlugin::handleReceivedProtobuf(const MeshPacket &req, const H } return true; // handled } + +int32_t RemoteHardwarePlugin::runOnce() { + if(watchGpios) { + uint32_t now = millis(); + + if(now - lastWatchMsec >= WATCH_INTERVAL_MSEC) { + uint64_t curVal = digitalReads(watchGpios); + + if(curVal != previousWatch) { + previousWatch = curVal; + + // Something changed! Tell the world with a broadcast message + HardwareMessage reply = HardwareMessage_init_default; + reply.typ = HardwareMessage_Type_GPIOS_CHANGED; + reply.gpio_value = curVal; + MeshPacket *p = allocDataProtobuf(reply); + service.sendToMesh(p); + } + } + } + else { + // No longer watching anything - stop using CPU + enabled = false; + } + + return 200; // Poll our GPIOs every 200ms (FIXME, make adjustable via protobuf arg) +} \ No newline at end of file diff --git a/src/plugins/RemoteHardwarePlugin.h b/src/plugins/RemoteHardwarePlugin.h index faae4894..fe9bf960 100644 --- a/src/plugins/RemoteHardwarePlugin.h +++ b/src/plugins/RemoteHardwarePlugin.h @@ -1,17 +1,26 @@ #pragma once #include "ProtobufPlugin.h" #include "remote_hardware.pb.h" +#include "concurrency/OSThread.h" /** * A plugin that provides easy low-level remote access to device hardware. */ -class RemoteHardwarePlugin : public ProtobufPlugin +class RemoteHardwarePlugin : public ProtobufPlugin, private concurrency::OSThread { + /// The current set of GPIOs we've been asked to watch for changes + uint64_t watchGpios = 0; + + /// The previously read value of watched pins + uint64_t previousWatch = 0; + + /// The timestamp of our last watch event (we throttle watches to 1 change every 30 seconds) + uint32_t lastWatchMsec = 0; public: /** Constructor * name is for debugging output */ - RemoteHardwarePlugin() : ProtobufPlugin("remotehardware", PortNum_REMOTE_HARDWARE_APP, HardwareMessage_fields) {} + RemoteHardwarePlugin(); protected: /** Called to handle a particular incoming message @@ -19,6 +28,16 @@ class RemoteHardwarePlugin : public ProtobufPlugin @return true if you've guaranteed you've handled this message and no other handlers should be considered for it */ virtual bool handleReceivedProtobuf(const MeshPacket &mp, const HardwareMessage &p); + + /** + * Periodically read the gpios we have been asked to WATCH, if they have changed, + * broadcast a message with the change information. + * + * The method that will be called each time our thread gets a chance to run + * + * Returns desired period for next invocation (or RUN_SAME for no change) + */ + virtual int32_t runOnce(); }; extern RemoteHardwarePlugin remoteHardwarePlugin; \ No newline at end of file