Merge pull request #780 from geeksville/mqtt

1.2.23 - fix gpio access
This commit is contained in:
Kevin Hester 2021-04-06 11:09:03 +08:00 committed by GitHub
commit 50ec03229f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 123 additions and 74 deletions

View File

@ -1,11 +1,13 @@
set -e set -e
pio run TARG=tbeam
pio run -e $TARG
echo uploading to usb1 echo uploading to usb1
pio run --upload-port /dev/ttyUSB1 -t upload & pio run --upload-port /dev/ttyUSB1 -t upload -e $TARG &
echo uploading to usb0 echo uploading to usb0
pio run --upload-port /dev/ttyUSB0 -t upload & pio run --upload-port /dev/ttyUSB0 -t upload -e $TARG &
wait wait

View File

@ -4,7 +4,7 @@ You probably don't care about this section - skip to the next one.
## before next release ## before next release
* android speed settings https://github.com/meshtastic/Meshtastic-Android/issues/271 * DONE android speed settings https://github.com/meshtastic/Meshtastic-Android/issues/271
* fix heltec battery scaling * fix heltec battery scaling
* DONE remote admin busted? * DONE remote admin busted?

View File

@ -1,23 +1,60 @@
# Remote Hardware Service # Remote Hardware Service
FIXME - the following are a collection of notes moved from elsewhere. We need to refactor these notes into actual documentation on the remote-hardware/gpio service. These are 'programmer focused' notes on using the "remote hardware" feature.
### 1.7.2. New 'no-code-IOT' mini-app Note: This feature uses a preinstalled plugin in the device code and associated commandline flags/classes in the python code. You'll need to be running at least version 1.2.23 (or later) of the python and device code to use this feature.
Add a new 'remote GPIO/serial port/SPI/I2C access' mini-app. This new standard app would use the MQTT messaging layer to let users (developers that don't need to write device code) do basic (potentially dangerous) operations remotely. You can get the latest python tool/library with "pip3 install --upgrade meshtastic" on Windows/Linux/OS-X.
#### 1.7.2.1. Supported operations in the initial release ## Supported operations in the initial release
Initially supported features for no-code-IOT.
- Set any GPIO - Set any GPIO
- Read any GPIO - Read any GPIO
- Receive notification of changes in any GPIO.
#### 1.7.2.2. Supported operations eventually ## Setup
General ideas for no-code IOT. GPIO access is fundamentally 'dangerous' because invalid options can physically burn-up hardware. To prevent access from untrusted users you must first make a "gpio" channel that is used for authenticated access to this feature. You'll need to install this channel on both the local and remote node.
- Subscribe for notification of GPIO input status change (i.e. when pin goes low, send my app a message) The procedure using the python command line tool is:
- Write/read N bytes over I2C/SPI bus Y (as one atomic I2C/SPI transaction)
- Send N bytes out serial port Z 1. Connect local device via USB
- Subscribe for notification for when regex X matches the bytes that were received on serial port Z 2. "meshtastic --ch-add admin; meshtastic --info" thn copy the (long) "Complete URL" that info printed
3. Connect remote device via USB (or use the remote admin feature to reach it through the mesh, but that's beyond the scope of this tutorial)
4. "meshtastic --seturl theurlyoucopiedinstep2"
Now both devices can talk over the "gpio" channel.
## Doing GPIO operations
Here's some examples using the command line tool.
## Using GPIOs from python
You can programmatically do operations from your own python code by using the meshtastic "RemoteHardwareClient" class - see the python documentation for more details.
Writing a GPIO
```
meshtastic --port /dev/ttyUSB0 --gpio-wrb 4 1 --dest \!28979058
Connected to radio
Writing GPIO mask 0x10 with value 0x10 to !28979058
```
Reading a GPIO
```
meshtastic --port /dev/ttyUSB0 --gpio-rd 0x10 --dest \!28979058
Connected to radio
Reading GPIO mask 0x10 from !28979058
GPIO read response gpio_value=16
```
Watching for GPIO changes:
```
meshtastic --port /dev/ttyUSB0 --gpio-watch 0x10 --dest \!28979058
Connected to radio
Watching GPIO mask 0x10 from !28979058
Received RemoteHardware typ=GPIOS_CHANGED, gpio_value=16
Received RemoteHardware typ=GPIOS_CHANGED, gpio_value=0
Received RemoteHardware typ=GPIOS_CHANGED, gpio_value=16
< press ctrl-c to exit >
```

View File

@ -9,7 +9,7 @@
; https://docs.platformio.org/page/projectconf.html ; https://docs.platformio.org/page/projectconf.html
[platformio] [platformio]
;default_envs = tbeam default_envs = tbeam
;default_envs = tbeam0.7 ;default_envs = tbeam0.7
;default_envs = heltec ;default_envs = heltec
;default_envs = tlora-v1 ;default_envs = tlora-v1
@ -18,7 +18,7 @@
;default_envs = lora-relay-v1 # nrf board ;default_envs = lora-relay-v1 # nrf board
;default_envs = eink ;default_envs = eink
;default_envs = nrf52840dk-geeksville ;default_envs = nrf52840dk-geeksville
default_envs = native # lora-relay-v1 # nrf52840dk-geeksville # linux # or if you'd like to change the default to something like lora-relay-v1 put that here ;default_envs = native # lora-relay-v1 # nrf52840dk-geeksville # linux # or if you'd like to change the default to something like lora-relay-v1 put that here
[common] [common]
; common is not currently used ; common is not currently used
@ -397,4 +397,4 @@ lib_deps =
;extends = esp32_base ;extends = esp32_base
;board = genieblocks_lora ;board = genieblocks_lora
;build_flags = ;build_flags =
; ${esp32_base.build_flags} -D GENIEBLOCKS ; ${esp32_base.build_flags} -D GENIEBLOCKS

View File

@ -86,8 +86,10 @@ void MeshPlugin::callPlugins(const MeshPacket &mp)
/// We only call plugins that are interested in the packet (and the message is destined to us or we are promiscious) /// We only call plugins that are interested in the packet (and the message is destined to us or we are promiscious)
bool wantsPacket = (isDecoded || pi.encryptedOk) && (pi.isPromiscuous || toUs) && pi.wantPacket(&mp); bool wantsPacket = (isDecoded || pi.encryptedOk) && (pi.isPromiscuous || toUs) && pi.wantPacket(&mp);
DEBUG_MSG("Plugin %s wantsPacket=%d\n", pi.name, wantsPacket);
assert(!pi.myReply); // If it is !null it means we have a bug, because it should have been sent the previous time
if (wantsPacket) { if (wantsPacket) {
// DEBUG_MSG("Plugin %s wantsPacket=%d\n", pi.name, wantsPacket);
pluginFound = true; pluginFound = true;
/// received channel (or NULL if not decoded) /// received channel (or NULL if not decoded)
@ -124,6 +126,14 @@ void MeshPlugin::callPlugins(const MeshPacket &mp)
} else { } else {
DEBUG_MSG("Plugin %s considered\n", pi.name); DEBUG_MSG("Plugin %s considered\n", pi.name);
} }
// If the requester didn't ask for a response we might need to discard unused replies to prevent memory leaks
if (pi.myReply) {
DEBUG_MSG("Discarding an unneeded response\n");
packetPool.release(pi.myReply);
pi.myReply = NULL;
}
if (handled) { if (handled) {
DEBUG_MSG("Plugin %s handled and skipped other processing\n", pi.name); DEBUG_MSG("Plugin %s handled and skipped other processing\n", pi.name);
break; break;
@ -136,7 +146,7 @@ void MeshPlugin::callPlugins(const MeshPacket &mp)
if (mp.decoded.want_response && toUs) { if (mp.decoded.want_response && toUs) {
if (currentReply) { if (currentReply) {
DEBUG_MSG("Sending response\n"); printPacket("Sending response", currentReply);
service.sendToMesh(currentReply); service.sendToMesh(currentReply);
currentReply = NULL; currentReply = NULL;
} else { } else {
@ -154,6 +164,13 @@ void MeshPlugin::callPlugins(const MeshPacket &mp)
DEBUG_MSG("No plugins interested in portnum=%d\n", mp.decoded.portnum); DEBUG_MSG("No plugins interested in portnum=%d\n", mp.decoded.portnum);
} }
MeshPacket *MeshPlugin::allocReply()
{
auto r = myReply;
myReply = NULL; // Only use each reply once
return r;
}
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked /** Messages can be received that have the want_response bit set. If set, this callback will be invoked
* so that subclasses can (optionally) send a response back to the original sender. Implementing this method * so that subclasses can (optionally) send a response back to the original sender. Implementing this method
* is optional * is optional
@ -176,7 +193,7 @@ void MeshPlugin::sendResponse(const MeshPacket &req)
void setReplyTo(MeshPacket *p, const MeshPacket &to) void setReplyTo(MeshPacket *p, const MeshPacket &to)
{ {
assert(p->which_payloadVariant == MeshPacket_decoded_tag); // Should already be set by now assert(p->which_payloadVariant == MeshPacket_decoded_tag); // Should already be set by now
p->to = getFrom(&to); p->to = getFrom(&to); // Make sure that if we are sending to the local node, we use our local node addr, not 0
p->channel = to.channel; // Use the same channel that the request came in on p->channel = to.channel; // Use the same channel that the request came in on
// No need for an ack if we are just delivering locally (it just generates an ignored ack) // No need for an ack if we are just delivering locally (it just generates an ignored ack)

View File

@ -69,6 +69,11 @@ class MeshPlugin
*/ */
static const MeshPacket *currentRequest; static const MeshPacket *currentRequest;
/**
* If your handler wants to send a response, simply set currentReply and it will be sent at the end of response handling.
*/
MeshPacket *myReply = NULL;
/** /**
* Initialize your plugin. This setup function is called once after all hardware and mesh protocol layers have * Initialize your plugin. This setup function is called once after all hardware and mesh protocol layers have
* been initialized * been initialized
@ -87,8 +92,12 @@ class MeshPlugin
virtual bool handleReceived(const MeshPacket &mp) { return false; } virtual bool handleReceived(const MeshPacket &mp) { return false; }
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked /** Messages can be received that have the want_response bit set. If set, this callback will be invoked
* so that subclasses can (optionally) send a response back to the original sender. */ * so that subclasses can (optionally) send a response back to the original sender.
virtual MeshPacket *allocReply() { return NULL; } *
* Note: most implementers don't need to override this, instead: If while handling a request you have a reply, just set
* the protected reply field in this instance.
* */
virtual MeshPacket *allocReply();
/*** /***
* @return true if you want to be alloced a UI screen frame * @return true if you want to be alloced a UI screen frame
@ -106,6 +115,7 @@ class MeshPlugin
* the RoutingPlugin to avoid sending redundant acks * the RoutingPlugin to avoid sending redundant acks
*/ */
static MeshPacket *currentReply; static MeshPacket *currentReply;
friend class ReliableRouter; friend class ReliableRouter;
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked /** Messages can be received that have the want_response bit set. If set, this callback will be invoked

View File

@ -75,11 +75,6 @@ void MQTT::reconnect()
enabled = true; // Start running background process again enabled = true; // Start running background process again
runASAP = true; runASAP = true;
static char subsStr[64]; /* We keep this static because the mqtt lib
might not be copying it */
// snprintf(subsStr, sizeof(subsStr), "/ezd/todev/%s/#", clientId);
// pubSub.subscribe(subsStr, 1); // we use qos 1 because we don't want to miss messages
/// FIXME, include more information in the status text /// FIXME, include more information in the status text
bool ok = pubSub.publish(myStatus.c_str(), "online", true); bool ok = pubSub.publish(myStatus.c_str(), "online", true);
DEBUG_MSG("published %d\n", ok); DEBUG_MSG("published %d\n", ok);

View File

@ -19,7 +19,7 @@ void AdminPlugin::handleGetChannel(const MeshPacket &req, uint32_t channelIndex)
AdminMessage r = AdminMessage_init_default; AdminMessage r = AdminMessage_init_default;
r.get_channel_response = channels.getByIndex(channelIndex); r.get_channel_response = channels.getByIndex(channelIndex);
r.which_variant = AdminMessage_get_channel_response_tag; r.which_variant = AdminMessage_get_channel_response_tag;
reply = allocDataProtobuf(r); myReply = allocDataProtobuf(r);
} }
} }
@ -36,7 +36,7 @@ void AdminPlugin::handleGetRadio(const MeshPacket &req)
r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); r.get_radio_response.preferences.ls_secs = getPref_ls_secs();
r.which_variant = AdminMessage_get_radio_response_tag; r.which_variant = AdminMessage_get_radio_response_tag;
reply = allocDataProtobuf(r); myReply = allocDataProtobuf(r);
} }
} }
@ -57,7 +57,7 @@ bool AdminPlugin::handleReceivedProtobuf(const MeshPacket &mp, const AdminMessag
case AdminMessage_set_channel_tag: case AdminMessage_set_channel_tag:
DEBUG_MSG("Client is setting channel %d\n", r->set_channel.index); DEBUG_MSG("Client is setting channel %d\n", r->set_channel.index);
if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS) if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS)
reply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp); myReply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp);
else else
handleSetChannel(r->set_channel); handleSetChannel(r->set_channel);
break; break;
@ -66,7 +66,7 @@ bool AdminPlugin::handleReceivedProtobuf(const MeshPacket &mp, const AdminMessag
uint32_t i = r->get_channel_request - 1; uint32_t i = r->get_channel_request - 1;
DEBUG_MSG("Client is getting channel %u\n", i); DEBUG_MSG("Client is getting channel %u\n", i);
if (i >= MAX_NUM_CHANNELS) if (i >= MAX_NUM_CHANNELS)
reply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp); myReply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp);
else else
handleGetChannel(mp, i); handleGetChannel(mp, i);
break; break;
@ -141,13 +141,6 @@ void AdminPlugin::handleSetRadio(const RadioConfig &r)
service.reloadConfig(); service.reloadConfig();
} }
MeshPacket *AdminPlugin::allocReply()
{
auto r = reply;
reply = NULL; // Only use each reply once
return r;
}
AdminPlugin::AdminPlugin() : ProtobufPlugin("Admin", PortNum_ADMIN_APP, AdminMessage_fields) AdminPlugin::AdminPlugin() : ProtobufPlugin("Admin", PortNum_ADMIN_APP, AdminMessage_fields)
{ {
// restrict to the admin channel for rx // restrict to the admin channel for rx

View File

@ -6,8 +6,6 @@
*/ */
class AdminPlugin : public ProtobufPlugin<AdminMessage> class AdminPlugin : public ProtobufPlugin<AdminMessage>
{ {
MeshPacket *reply = NULL;
public: public:
/** Constructor /** Constructor
* name is for debugging output * name is for debugging output
@ -21,10 +19,6 @@ class AdminPlugin : public ProtobufPlugin<AdminMessage>
*/ */
virtual bool handleReceivedProtobuf(const MeshPacket &mp, const AdminMessage *p); virtual bool handleReceivedProtobuf(const MeshPacket &mp, const AdminMessage *p);
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked
* so that subclasses can (optionally) send a response back to the original sender. */
virtual MeshPacket *allocReply();
private: private:
void handleSetOwner(const User &o); void handleSetOwner(const User &o);
void handleSetChannel(const Channel &cc); void handleSetChannel(const Channel &cc);

View File

@ -10,12 +10,13 @@
// Because (FIXME) we currently don't tell API clients status on sent messages // 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 // 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 // don't generate more messages than the net can send. So we limit watch messages to
// a max of one change per 30 seconds // a max of one change per 30 seconds
#define WATCH_INTERVAL_MSEC (30 * 1000) #define WATCH_INTERVAL_MSEC (30 * 1000)
/// Set pin modes for every set bit in a mask /// Set pin modes for every set bit in a mask
static void pinModes(uint64_t mask, uint8_t mode) { static void pinModes(uint64_t mask, uint8_t mode)
{
for (uint8_t i = 0; i < NUM_GPIOS; i++) { for (uint8_t i = 0; i < NUM_GPIOS; i++) {
if (mask & (1 << i)) { if (mask & (1 << i)) {
pinMode(i, mode); pinMode(i, mode);
@ -24,7 +25,8 @@ static void pinModes(uint64_t mask, uint8_t mode) {
} }
/// Read all the pins mentioned in a mask /// Read all the pins mentioned in a mask
static uint64_t digitalReads(uint64_t mask) { static uint64_t digitalReads(uint64_t mask)
{
uint64_t res = 0; uint64_t res = 0;
pinModes(mask, INPUT_PULLUP); pinModes(mask, INPUT_PULLUP);
@ -40,10 +42,9 @@ static uint64_t digitalReads(uint64_t mask) {
return res; return res;
} }
RemoteHardwarePlugin::RemoteHardwarePlugin() RemoteHardwarePlugin::RemoteHardwarePlugin()
: ProtobufPlugin("remotehardware", PortNum_REMOTE_HARDWARE_APP, HardwareMessage_fields), : ProtobufPlugin("remotehardware", PortNum_REMOTE_HARDWARE_APP, HardwareMessage_fields), concurrency::OSThread(
concurrency::OSThread("remotehardware") "remotehardware")
{ {
} }
@ -69,26 +70,26 @@ bool RemoteHardwarePlugin::handleReceivedProtobuf(const MeshPacket &req, const H
case HardwareMessage_Type_READ_GPIOS: { case HardwareMessage_Type_READ_GPIOS: {
// Print notification to LCD screen // Print notification to LCD screen
if(screen) if (screen)
screen->print("Read GPIOs\n"); screen->print("Read GPIOs\n");
uint64_t res = digitalReads(p.gpio_mask); uint64_t res = digitalReads(p.gpio_mask);
// Send the reply // Send the reply
HardwareMessage reply = HardwareMessage_init_default; HardwareMessage r = HardwareMessage_init_default;
reply.typ = HardwareMessage_Type_READ_GPIOS_REPLY; r.typ = HardwareMessage_Type_READ_GPIOS_REPLY;
reply.gpio_value = res; r.gpio_value = res;
MeshPacket *p = allocDataProtobuf(reply); MeshPacket *p = allocDataProtobuf(r);
setReplyTo(p, req); setReplyTo(p, req);
service.sendToMesh(p); myReply = p;
break; break;
} }
case HardwareMessage_Type_WATCH_GPIOS: { case HardwareMessage_Type_WATCH_GPIOS: {
watchGpios = p.gpio_mask; watchGpios = p.gpio_mask;
lastWatchMsec = 0; // Force a new publish soon lastWatchMsec = 0; // Force a new publish soon
previousWatch = ~watchGpios; // generate a 'previous' value which is guaranteed to not match (to force an initial publish) 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 enabled = true; // Let our thread run at least once
DEBUG_MSG("Now watching GPIOs 0x%llx\n", watchGpios); DEBUG_MSG("Now watching GPIOs 0x%llx\n", watchGpios);
break; break;
} }
@ -101,31 +102,31 @@ bool RemoteHardwarePlugin::handleReceivedProtobuf(const MeshPacket &req, const H
DEBUG_MSG("Hardware operation %d not yet implemented! FIXME\n", p.typ); DEBUG_MSG("Hardware operation %d not yet implemented! FIXME\n", p.typ);
break; break;
} }
return true; // handled return false;
} }
int32_t RemoteHardwarePlugin::runOnce() { int32_t RemoteHardwarePlugin::runOnce()
if(watchGpios) { {
if (watchGpios) {
uint32_t now = millis(); uint32_t now = millis();
if(now - lastWatchMsec >= WATCH_INTERVAL_MSEC) { if (now - lastWatchMsec >= WATCH_INTERVAL_MSEC) {
uint64_t curVal = digitalReads(watchGpios); uint64_t curVal = digitalReads(watchGpios);
if(curVal != previousWatch) { if (curVal != previousWatch) {
previousWatch = curVal; previousWatch = curVal;
DEBUG_MSG("Broadcasting GPIOS 0x%llx changed!\n", curVal); DEBUG_MSG("Broadcasting GPIOS 0x%llx changed!\n", curVal);
// Something changed! Tell the world with a broadcast message // Something changed! Tell the world with a broadcast message
HardwareMessage reply = HardwareMessage_init_default; HardwareMessage r = HardwareMessage_init_default;
reply.typ = HardwareMessage_Type_GPIOS_CHANGED; r.typ = HardwareMessage_Type_GPIOS_CHANGED;
reply.gpio_value = curVal; r.gpio_value = curVal;
MeshPacket *p = allocDataProtobuf(reply); MeshPacket *p = allocDataProtobuf(r);
service.sendToMesh(p); service.sendToMesh(p);
} }
} }
} } else {
else {
// No longer watching anything - stop using CPU // No longer watching anything - stop using CPU
enabled = false; enabled = false;
} }

View File

@ -1,4 +1,4 @@
[VERSION] [VERSION]
major = 1 major = 1
minor = 2 minor = 2
build = 20 build = 23