andrewrapp / xbee-arduino

Arduino library for communicating with XBee radios in API mode
GNU General Public License v2.0
340 stars 163 forks source link

Various API improvements #5

Closed matthijskooijman closed 8 years ago

matthijskooijman commented 9 years ago

I am currently in the process of writing a book about Arduino and XBee, which will make use of this library. First off, compliments on the library: it is well designed and implemented, powerful but also easy to use.

While writing some examples for my book, I did notice that I still needed quite some boilerplate code, wrt checking for available packets, converting to the right type, etc. I think that, by expanding on the API that the library provides, a lot of this boilerplate code could be removed (without adding much of a performance overhead, or breaking backward compatibility). I also noticed some other improvements that could make things easier.

I'm willing to implement all of this, if you agree these are good additions and expect you can merge the changes without too much delay. I'd like to update the examples in my book using these improvements, but since I cannot publish before I'm sure these changes are actually merged, some timeliness would be required (e.g. merging this within the next one or two months).

In particular, these are the changes I have in mind:

Let me know if any or all of these changes make sense, or if you have other ideas or suggestions!

andrewrapp commented 9 years ago

These are excellent ideas. I never liked the getXxxResponse() methods as they are awkward. However, I didn't understand C++ enough to implement it differently. Another design choice I regret is the api allocates an array for the largest packet anticipated. This can be changed in xbee.h but I feel a better approach would be to allow the api user to provide the array. For example if you are only sending a few bytes per packet, then a lot of memory is wasted as it is now. I'm fine with other changes to the extent that it is easy to use for the vast majority of Arduino users, doesn't add a lot of complexity (e.g dynamic memory), and should be backwards compatible. Also it needs to work with softserial.

matthijskooijman commented 9 years ago

(woops, submitted comment too soon. Here's the full comment)

I never liked the getXxxResponse() methods as they are awkward. However, I didn't understand C++ enough to implement it differently.

It's tricky to do this without a bunch of these methods (you could template them, but that only changes syntax, not the net effect), since you really need different types. So I guess changing the methods to return the object instead of passing it as a parameter would be the best approach. Another design choice I regret is the api allocates an array for the largest packet anticipated. This can be changed in xbee.h but I feel a better approach would be to allow the api user to provide the array.

Another design choice I regret is the api allocates an array for the largest packet anticipated. This can be changed in xbee.h but I feel a better approach would be to allow the api user to provide the array.

Ah, agreed. It's a bit cumbersome for a sketch to allocate an array and pass it to the class though, the current method is more userfriendly. Using a template argument could make it easier, but template arguments can lead to complicated error messages and aren't really common in the Arduino APIs.

In any case, changing this without breaking compatibility is tricky. The only way I can think of right now is to rename XBee to XBeeLight and add a new XBee class as a subclass. The latter subclass would allocate the data array and pass it to setFrameData(), removing the current global data array.

This is a bit tricky to combine with my XBeeCallbacks subclass above, since that should then either pick the light or normal version. So, probably just leave this as is?

I'm fine with other changes to the extent that it is easy to use for the vast majority of Arduino users, doesn't add a lot of complexity (e.g dynamic memory), and should be backwards compatible. Also it needs to work with softserial.

I think all of my intended changes would adhere to those requirements (and I agree that they should).

I'll try to implement this stuff soon, I'll probably start somewhere next week. I'll keep you posted on any progress. Thanks!

JChristensen commented 9 years ago

Hope you guys don't mind another butt-in comment but I've been involved in a similar situation and I just want to say that some careful thought should be given beforehand, both from the standpoint of the library author and the book author.

Linking a library to a book can cause issues down the road that may be unanticipated. Once a book goes to press, it's pretty much set in stone and that implies that the library is also, at least in certain ways. Changes or improvements to the library can break examples in the book. The library will need to remain backwards compatible with the book for some relatively long period of time. This may stifle development or lead to sub-optimal improvements.

In my case we ended up freezing the master branch; development and improvements continued on another branch. I don't know whether that's the best solution or even a good solution. It does make the improved code harder to find for those that use the library but not the book. The problems we experienced were due at least partly to the fact that the book's author did not own the repository.

I'm a huge fan of this library, so I just wanted to ensure that a thought process was in place beyond the technical aspects. Best wishes to both of you and your respective projects.

andrewrapp commented 9 years ago

I'd rather not add an XBeeLight class. This just has the potential to confuse users. I think the best approach is to simply fork the project, as many have done, and we can take a look at merging as long as there are no breaking API changes. As with any changes, improvements, or bug fixes, there needs to be sufficient QA.

andrewrapp commented 9 years ago

@JChristensen that's sort of the reality for any software book. Software will evolve and advance over time. As long as good version control practices are in place, book users should always be able to obtain the same version of software that the book references. The O'Reilly XBee book by Rob Faludi references this project https://www.safaribooksonline.com/library/view/building-wireless-sensor/780596807757/libraries.html The only issue was I would get questions about the book examples that I couldn't help with since I didn't have the book.

matthijskooijman commented 9 years ago

@JChristensen, I realize the catches, but I can't really see any alternatives. I'll make sure to explicitely reference the version of the library used and probably included it with the book, so readers can at least use all examples as-is.

I'd like to prevent forking, since then I'll have examples in the book that will certainly not work with newer versions of this library. Of course, future changes might still break my examples, but that's ok. I won't ask for any unreasonable freezing of the developments, just a sensible approach to backward compatibility, but from what I've seen, this is fine.

The only issue was I would get questions about the book examples that I couldn't help with since I didn't have the book.

Once the book is done, I'll make sure to send you one. Feel free to remind me if I forget :-)

I'd rather not add an XBeeLight class. This just has the potential to confuse users.

Yeah, agreed. I can't see another way to allow a smaller buffer-size in a compatible way, though (unless the IDE will at some point support setting defines through -D compiler options, but that doesn't seem likely on the short term). In any case, I don't really need the configurable buffer size, I just agreed it would be nice to have :-)

Things are a bit more busy than I anticipated, so I haven't started implementing anything yet. I'll keep you posted on any progress.

JChristensen commented 9 years ago

Referencing the version of the library used in the book sounds like a great way to go. I might also include a note to the effect that the library will likely evolve, and also provide a link to the current version. I do have Faludi's book and I like it. However I haven't cracked it in a while and I don't remember whether it references a particular version. I also don't remember how deeply it gets into API mode. If there are just a few introductory examples, then they might be the type of thing that would be fairly tolerant to changes in the library. Thanks again, hope I didn't get things off into the weeds too much.

matthijskooijman commented 9 years ago

I have crossed out one proposal above, since I've formulated a new and conflicting one (and already implmented it):

matthijskooijman commented 9 years ago

And another lengthy comment from my side. While working on the implementation of the callback stuff, I worked with the new response API (as proposed in the previous comment, which I implemented today) a bit more, and I'm not so sure I like it so much (it's #4 in the list below, look there for downsides).

To get a feeling for how we want this API to look, I made an example using a few styles that have been discussed or I just thought of.

  1. This is the current style. It can be improved by narrowing down the argument types to getXxxResponse(), which improves type safety without changing the way the API is used.

    switch (xbee.getResonse().getApiId() {
       case MODEM_STATUS_RESPONSE:
       {
           ModemStatusResponse r;
           xbee.getResponse().getModemStatusResponse(r);
           // Do things with r
           break;
       }
       case ZB_RX_RESPONSE:
       {
           ZBRxResponse r;
           xbee.getResponse().getModemStatusResponse(r);
           // Do things with r
           break;
       }
    }
  2. This is what I originally proposed, let the getXxxResponse() methods return an object instead of passing it as an argument.

    switch (xbee.getResonse().getApiId() {
       case MODEM_STATUS_RESPONSE:
       {
           ModemStatusResponse r = xbee.getResponse().getModemStatusResponse();
           // Do things with r
           break;
       }
       case ZB_RX_RESPONSE:
       {
           ZBRxResponse r = xbee.getResponse().getModemStatusResponse();
           // Do things with r
           break;
       }
    }
  3. This is what I later proposed (and implemented), essentially just #1 with the methods renamed:

    switch (xbee.getResonse().getApiId() {
       case MODEM_STATUS_RESPONSE:
       {
           ModemStatusResponse r;
           xbee.getResponse().convertTo(r);
           // Do things with r
           break;
       }
       case ZB_RX_RESPONSE:
       {
           ZBRxResponse r;
           xbee.getResponse().convertTo(r);
           // Do things with r
           break;
       }
    }
  4. This is what an extension I proposed to #3, by letting convertTo return a bool. I like this style because it forbids mismatches between the API ID check and the conversion and is concise. I dislike this approach for forcing to declare all possible responsetypes beforehand. This is potentially inefficient and doesn't read nicely.

    AFAIK you cannot declare a variable inside an if condition, so I don't see a way to elegantly fix this (adding {} around each if to limit scope is ugly and prevents using else if).

    Also, I suspect that this will cause the compiler to allocate a lot of space on the stack and probably even call each constructor. This might be alleviated when the constructors and convertTo methods can be inlined, but I'm not sure the compiler is smart enough for that.

    ModemStatusResponse modem_status;
    ZBRxResponse zb_rx;
    if (xbee.getResponse().convertTo(modem_status)) {
       // Do things with modem_status
       break;
    } else if (xbee.getResponse().convertTo(zb_rx)) {
       // Do things with zb_rx
       break;
    }
  5. This is a variation on #2 I just realized, by overloading operator=. Essentially it just allows you to assign an XBeeResponse to any other subclass and it will do the right thing.

    switch (xbee.getResonse().getApiId() {
       case MODEM_STATUS_RESPONSE:
       {
           ModemStatusResponse r = xbee.getResponse();
           // Do things with r
           break;
       }
       case ZB_RX_RESPONSE:
       {
           ZBRxResponse r = xbee.getResponse();
           // Do things with r
           break;
       }
    }

I'd be happy if people could comment on these alternatives and perhaps even suggest alternatives. Ideally, the API should:

All of the styles above implement just one of these points, not both. Looking at them now, I actually think I like #5 best - it looks rather clean and tight to me, even though it allows mismatching the types.

To some degree, the callback style would actually satisfy both of these. Since for my book I'll be using the callback style exclusively, I might end up implementing that first, on top of the current API so we can get that merged, and then improve the regular API (which I think still has uses) later. To get an idea of this callback style, here's how the above example would look using that:

void onModemStatus(ModemStatusResponse& r, void*) {
    // Do things with r
}

void onZBRx(ZBRxResponse& r, void*) {
    // Do things with r
}

setup() {
    ...
    xbee.onModemStatusResponse(onModemStatus);
    xbee.onZBRxResponse(onZBRx);
}

loop() {
    ...
    xbee.loop();
}

The void* arguments are just passed as NULL by default, but they could be used to pass arbitrary data into the callbacks (defined by the sketch). This could allow doing something like:

xbee.onPacketError(printError, &Serial);

where &Serial is the void* passed to the printError callback.

matthijskooijman commented 9 years ago

The callbacks are pretty much working now. I need to add an example and then they're ready to push (I already have an example that uses ZDO requests to scan the network and devices for supported endpoints and clusters, but that's a bit too complicated to serve as a basic callback example).

On top of the callbacks, I've also implemented some "wait" methods, that allow waiting for a status response or reply packet in a concise way. E.g., you can write:

    uint8_t value = 1;
    AtCommandRequest req((uint8_t*)"AO", &value, sizeof(value));
    req.setFrameId(xbee.getNextFrameId());
    uint8_t status = xbee.sendAndWait(req, 150);
    if (status == 0)
      Serial.println(F("Set AO=1"));
    else
      Serial.println(F("Failed to set AO, expect problems"));

To wait for a AtCommandResponse (based on the frameid). Or, you can write:

    ZBExplicitTxRequest tx = buildZdoRequest(addr, cluster_id, (uint8_t*)payload, len);
    xbee.send(tx);

    uint8_t transaction_id = payload[0];
    uint8_t status = xbee.waitFor(rx, 1000, matchZdoReply, transaction_id, tx.getFrameId());
    if (status != 0)
      Serial.println("Failed to send or no reply received")

This waits for a reply packet that satisfies the criteria defined by the (user-defined) matchZdoReply() function (passing in the transaction_id to match) or a TX status failure.