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
pio run
TARG=tbeam
pio run -e $TARG
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
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
* 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
* DONE remote admin busted?

View File

@ -1,23 +1,60 @@
# 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
Initially supported features for no-code-IOT.
## Supported operations in the initial release
- Set 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)
- Write/read N bytes over I2C/SPI bus Y (as one atomic I2C/SPI transaction)
- Send N bytes out serial port Z
- Subscribe for notification for when regex X matches the bytes that were received on serial port Z
The procedure using the python command line tool is:
1. Connect local device via USB
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
[platformio]
;default_envs = tbeam
default_envs = tbeam
;default_envs = tbeam0.7
;default_envs = heltec
;default_envs = tlora-v1
@ -18,7 +18,7 @@
;default_envs = lora-relay-v1 # nrf board
;default_envs = eink
;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 is not currently used
@ -397,4 +397,4 @@ lib_deps =
;extends = esp32_base
;board = genieblocks_lora
;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)
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) {
// DEBUG_MSG("Plugin %s wantsPacket=%d\n", pi.name, wantsPacket);
pluginFound = true;
/// received channel (or NULL if not decoded)
@ -124,6 +126,14 @@ void MeshPlugin::callPlugins(const MeshPacket &mp)
} else {
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) {
DEBUG_MSG("Plugin %s handled and skipped other processing\n", pi.name);
break;
@ -136,7 +146,7 @@ void MeshPlugin::callPlugins(const MeshPacket &mp)
if (mp.decoded.want_response && toUs) {
if (currentReply) {
DEBUG_MSG("Sending response\n");
printPacket("Sending response", currentReply);
service.sendToMesh(currentReply);
currentReply = NULL;
} else {
@ -154,6 +164,13 @@ void MeshPlugin::callPlugins(const MeshPacket &mp)
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
* so that subclasses can (optionally) send a response back to the original sender. Implementing this method
* is optional
@ -176,7 +193,7 @@ void MeshPlugin::sendResponse(const MeshPacket &req)
void setReplyTo(MeshPacket *p, const MeshPacket &to)
{
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
// 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;
/**
* 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
* been initialized
@ -87,8 +92,12 @@ class MeshPlugin
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
* so that subclasses can (optionally) send a response back to the original sender. */
virtual MeshPacket *allocReply() { return NULL; }
* so that subclasses can (optionally) send a response back to the original sender.
*
* 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
@ -106,6 +115,7 @@ class MeshPlugin
* the RoutingPlugin to avoid sending redundant acks
*/
static MeshPacket *currentReply;
friend class ReliableRouter;
/** 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
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
bool ok = pubSub.publish(myStatus.c_str(), "online", true);
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;
r.get_channel_response = channels.getByIndex(channelIndex);
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.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:
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)
reply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp);
myReply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp);
else
handleSetChannel(r->set_channel);
break;
@ -66,7 +66,7 @@ bool AdminPlugin::handleReceivedProtobuf(const MeshPacket &mp, const AdminMessag
uint32_t i = r->get_channel_request - 1;
DEBUG_MSG("Client is getting channel %u\n", i);
if (i >= MAX_NUM_CHANNELS)
reply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp);
myReply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp);
else
handleGetChannel(mp, i);
break;
@ -141,13 +141,6 @@ void AdminPlugin::handleSetRadio(const RadioConfig &r)
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)
{
// restrict to the admin channel for rx

View File

@ -6,8 +6,6 @@
*/
class AdminPlugin : public ProtobufPlugin<AdminMessage>
{
MeshPacket *reply = NULL;
public:
/** Constructor
* name is for debugging output
@ -21,10 +19,6 @@ class AdminPlugin : public ProtobufPlugin<AdminMessage>
*/
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:
void handleSetOwner(const User &o);
void handleSetChannel(const Channel &cc);

View File

@ -10,12 +10,13 @@
// 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
// 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) {
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);
@ -24,7 +25,8 @@ static void pinModes(uint64_t mask, uint8_t mode) {
}
/// 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;
pinModes(mask, INPUT_PULLUP);
@ -40,10 +42,9 @@ static uint64_t digitalReads(uint64_t mask) {
return res;
}
RemoteHardwarePlugin::RemoteHardwarePlugin()
: ProtobufPlugin("remotehardware", PortNum_REMOTE_HARDWARE_APP, HardwareMessage_fields),
concurrency::OSThread("remotehardware")
: ProtobufPlugin("remotehardware", PortNum_REMOTE_HARDWARE_APP, HardwareMessage_fields), concurrency::OSThread(
"remotehardware")
{
}
@ -69,26 +70,26 @@ bool RemoteHardwarePlugin::handleReceivedProtobuf(const MeshPacket &req, const H
case HardwareMessage_Type_READ_GPIOS: {
// Print notification to LCD screen
if(screen)
if (screen)
screen->print("Read GPIOs\n");
uint64_t res = digitalReads(p.gpio_mask);
// Send the reply
HardwareMessage reply = HardwareMessage_init_default;
reply.typ = HardwareMessage_Type_READ_GPIOS_REPLY;
reply.gpio_value = res;
MeshPacket *p = allocDataProtobuf(reply);
HardwareMessage r = HardwareMessage_init_default;
r.typ = HardwareMessage_Type_READ_GPIOS_REPLY;
r.gpio_value = res;
MeshPacket *p = allocDataProtobuf(r);
setReplyTo(p, req);
service.sendToMesh(p);
myReply = p;
break;
}
case HardwareMessage_Type_WATCH_GPIOS: {
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)
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);
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);
break;
}
return true; // handled
return false;
}
int32_t RemoteHardwarePlugin::runOnce() {
if(watchGpios) {
int32_t RemoteHardwarePlugin::runOnce()
{
if (watchGpios) {
uint32_t now = millis();
if(now - lastWatchMsec >= WATCH_INTERVAL_MSEC) {
if (now - lastWatchMsec >= WATCH_INTERVAL_MSEC) {
uint64_t curVal = digitalReads(watchGpios);
if(curVal != previousWatch) {
if (curVal != previousWatch) {
previousWatch = curVal;
DEBUG_MSG("Broadcasting GPIOS 0x%llx changed!\n", 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);
HardwareMessage r = HardwareMessage_init_default;
r.typ = HardwareMessage_Type_GPIOS_CHANGED;
r.gpio_value = curVal;
MeshPacket *p = allocDataProtobuf(r);
service.sendToMesh(p);
}
}
}
else {
} else {
// No longer watching anything - stop using CPU
enabled = false;
}

View File

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