Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions include/droid_persistence.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ DroidStore load();
void save(const DroidStore &store);

} // namespace DroidPersistence

// Note on the *currently selected* PAN ID (as opposed to the list above):
// there's deliberately no persistence for it here. Once XbeeControl::
// setPanId() commits a PAN ID via "WR", the XBee module remembers it in
// its own flash across power cycles on its own — it never needs to be
// re-applied at boot, and never changes except when a Switch Droid action
// explicitly calls setPanId() again. SnipsController.ino derives the
// display name for it at boot by querying the module's current PAN ID
// (XbeeControl::queryPanId()) and matching it against the list above.
10 changes: 7 additions & 3 deletions include/menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,14 @@ class MenuController {
const TextEntryWidget &nameEntry() const { return nameEntry_; }
const TextEntryWidget &panIdEntry() const { return panIdEntry_; }

// The most recently successfully-switched-to droid's name, for the
// complications system's "Droid Name" source — "(none)" until a switch
// has actually succeeded this session (not persisted; resets on boot).
// The currently-active droid's name, for the complications system's
// "Droid Name" source — "(none)" until either a switch succeeds or
// setCurrentDroidName() seeds it. SnipsController.ino calls the setter
// once at boot, having derived the name by querying the XBee module's
// current PAN ID (which it remembers on its own — see
// XbeeControl::queryPanId()) and matching it against the droid list.
const char *currentDroidName() const { return currentDroidName_; }
void setCurrentDroidName(const char *name);

// Display Config edits an externally-owned ComplicationRegistry rather
// than duplicating its slot-assignment state here — set once at boot.
Expand Down
9 changes: 9 additions & 0 deletions include/xbee_control.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ class XbeeControl : public XbeeTransport {
// query failure, leaving outHex untouched.
bool querySerialLow(char *outHex, size_t outHexCapacity);

// Queries the module's current PAN ID ("ID"). Once setPanId() commits
// one via "WR", the module remembers it in its own flash across power
// cycles on its own — this exists so SnipsController.ino can derive
// which saved droid (by name) that PAN ID corresponds to at boot,
// rather than to re-apply anything. Writes up to 16 hex chars + a null
// terminator into outHex (needs a 17-byte buffer). Returns false on
// query failure, leaving outHex untouched.
bool queryPanId(char *outHex, size_t outHexCapacity);

// Queries the local module's own last-hop received signal strength
// ("DB" AT command — a single byte, the RSSI magnitude in dBm, e.g. a
// response of 0x2A means -42dBm). Purely local: no round trip to
Expand Down
19 changes: 19 additions & 0 deletions src/SnipsController.ino
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,25 @@ void setup() {
xbeeControl.begin();
menuController.setXbeeTransport(&xbeeControl);

// The XBee module remembers its own PAN ID across power cycles once
// XbeeControl::setPanId() commits one via "WR" — it never needs to be
// re-applied here. This just derives which saved droid (by name) that
// PAN ID corresponds to, for the "Droid Name" complication; no match
// (e.g. first boot, or after a Factory Reset clears the list) leaves
// the "(none)" default in place.
char currentPanId[17];
if (xbeeControl.queryPanId(currentPanId, sizeof(currentPanId))) {
const DroidStore &droidStore = menuController.droidStore();
for (size_t i = 0; i < droidStore.count(); ++i) {
if (std::strcmp(droidStore.at(i).panId, currentPanId) == 0) {
menuController.setCurrentDroidName(droidStore.at(i).name);
break;
}
}
} else {
Serial.println("XBee PAN ID query failed at boot.");
}

// The module's SL is fixed hardware, so querying it once at boot (for
// the Device Info screen) is enough — no need to re-query per menu
// visit. deviceSerialLowBuf must outlive setup() since MenuController
Expand Down
30 changes: 27 additions & 3 deletions src/menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
#include <cstdio>
#include <cstring>

namespace {
// A 64-bit all-zero PAN ID — Digi's convention for "unconfigured," used
// by Factory Reset to make sure the radio doesn't quietly stay
// associated with whatever droid it was last on. See the
// kFactoryResetConfirm case in MenuController::onEnter().
constexpr const char *kClearedPanId = "0000000000000000";
} // namespace

const char *mainMenuItemLabel(MainMenuItem item) {
switch (item) {
case MainMenuItem::kSwitchDroid: return "Switch Droid";
Expand All @@ -20,6 +28,11 @@ MainMenuItem MenuController::selectedMainMenuItem() const {
return static_cast<MainMenuItem>(mainMenuIndex_);
}

void MenuController::setCurrentDroidName(const char *name) {
std::strncpy(currentDroidName_, name, sizeof(currentDroidName_) - 1);
currentDroidName_[sizeof(currentDroidName_) - 1] = '\0';
}

int MenuController::wrapIndex(int index, int count) {
if (count <= 0) return 0;
return (index % count + count) % count;
Expand Down Expand Up @@ -198,9 +211,7 @@ void MenuController::onEnter(int rawTrigger, int rawStickX, int rawStickY) {
const DroidEntry &target = droidStore_.at(droidListIndex_);
lastSwitchResult_ = DroidSwitcher::switchTo(target.panId, xbeeTransport_);
if (lastSwitchResult_ == DroidSwitchResult::kSuccess) {
std::strncpy(currentDroidName_, target.name,
sizeof(currentDroidName_) - 1);
currentDroidName_[sizeof(currentDroidName_) - 1] = '\0';
setCurrentDroidName(target.name);
}
screen_ = MenuScreen::kSwitchDroidResult;
}
Expand Down Expand Up @@ -292,6 +303,19 @@ void MenuController::onEnter(int rawTrigger, int rawStickX, int rawStickY) {
factoryResetConfirmed_ = true;
droidStore_ = DroidStore();
droidStoreChanged_ = true;
// A factory reset should genuinely disconnect from whatever droid
// this controller was last on — leaving it silently still joined
// post-reset would be confusing. Clears the PAN ID to a neutral,
// unconfigured value and forces an immediate leave — deliberately
// no rejoinNetwork() call after, unlike a normal Switch Droid: it
// should sit disconnected until the user explicitly picks a new
// droid, not auto-associate with whatever it can find. Everything
// else about the radio (SL, etc.) is untouched.
if (xbeeTransport_ != nullptr) {
xbeeTransport_->leaveNetwork();
xbeeTransport_->setPanId(kClearedPanId);
}
setCurrentDroidName("(none)");
screen_ = MenuScreen::kMainMenu;
break;

Expand Down
26 changes: 24 additions & 2 deletions src/xbee_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@ bool XbeeControl::setPanId(const char *panId) {
if (!hexStringToBytes(panId, panIdBytes, sizeof(panIdBytes))) {
return false;
}
return spi_.sendAtCommand("ID", panIdBytes, sizeof(panIdBytes), nullptr, 0,
nullptr);
if (!spi_.sendAtCommand("ID", panIdBytes, sizeof(panIdBytes), nullptr, 0,
nullptr)) {
return false;
}
// "WR": commit to the module's own flash. Once written, the module
// remembers this PAN ID across power cycles on its own — nothing needs
// to re-apply it at boot, and it never changes again until this method
// is called again for a different droid.
return spi_.sendAtCommand("WR", nullptr, 0, nullptr, 0, nullptr);
}

bool XbeeControl::rejoinNetwork() {
Expand All @@ -71,6 +78,21 @@ bool XbeeControl::querySerialLow(char *outHex, size_t outHexCapacity) {
return true;
}

bool XbeeControl::queryPanId(char *outHex, size_t outHexCapacity) {
if (outHexCapacity < 17) {
return false; // 16 hex chars + null
}
uint8_t value[8];
uint8_t valueLength = 0;
if (!spi_.sendAtCommand("ID", nullptr, 0, value, sizeof(value),
&valueLength) ||
valueLength != sizeof(value)) {
return false;
}
bytesToHexString(value, sizeof(value), outHex);
return true;
}

bool XbeeControl::queryLocalRssiDbm(int *outDbm) {
uint8_t value[1];
uint8_t valueLength = 0;
Expand Down
52 changes: 47 additions & 5 deletions test/test_menu/test_menu.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <unity.h>
#include <cstring>
#include <string>

#include "menu.h"

Expand Down Expand Up @@ -31,9 +32,22 @@ DroidStore twoDroidStore() {

class FakeTransport : public XbeeTransport {
public:
bool leaveNetwork() override { return true; }
bool setPanId(const char * /*panId*/) override { return true; }
bool rejoinNetwork() override { return true; }
bool leaveCalled = false;
bool rejoinCalled = false;
std::string lastPanId;

bool leaveNetwork() override {
leaveCalled = true;
return true;
}
bool setPanId(const char *panId) override {
lastPanId = panId;
return true;
}
bool rejoinNetwork() override {
rejoinCalled = true;
return true;
}
};

} // namespace
Expand Down Expand Up @@ -156,11 +170,11 @@ void test_enter_factory_reset_confirm_then_back_cancels() {
}

void test_factory_reset_confirmed_via_enter() {
MenuController menu;
MenuController menu; // no transport set — exercises the null guard
menu.setDroidStore(twoDroidStore());
selectMainMenuItem(&menu, MainMenuItem::kFactoryReset);
menu.onEnter(0, 0, 0); // -> confirm screen
menu.onEnter(0, 0, 0); // confirms
menu.onEnter(0, 0, 0); // confirms — should not crash without a transport

TEST_ASSERT_TRUE(MenuScreen::kMainMenu == menu.currentScreen());
TEST_ASSERT_TRUE(menu.consumeFactoryResetConfirmed());
Expand All @@ -170,6 +184,24 @@ void test_factory_reset_confirmed_via_enter() {
// Factory reset also clears the droid list, and reports that change.
TEST_ASSERT_EQUAL_INT(0, menu.droidStore().count());
TEST_ASSERT_TRUE(menu.consumeDroidStoreChanged());

// And resets the current-droid display.
TEST_ASSERT_EQUAL_STRING("(none)", menu.currentDroidName());
}

void test_factory_reset_disconnects_the_radio() {
MenuController menu;
FakeTransport transport;
menu.setXbeeTransport(&transport);
selectMainMenuItem(&menu, MainMenuItem::kFactoryReset);
menu.onEnter(0, 0, 0); // -> confirm screen
menu.onEnter(0, 0, 0); // confirms

TEST_ASSERT_TRUE(transport.leaveCalled);
TEST_ASSERT_EQUAL_STRING("0000000000000000", transport.lastPanId.c_str());
// Deliberately does not rejoin — should sit disconnected until the
// user explicitly picks a new droid via Switch Droid.
TEST_ASSERT_FALSE(transport.rejoinCalled);
}

// ---- trigger calibration ---------------------------------------------------
Expand Down Expand Up @@ -524,6 +556,14 @@ void test_current_droid_name_unchanged_on_failed_switch() {
TEST_ASSERT_EQUAL_STRING("(none)", menu.currentDroidName());
}

void test_set_current_droid_name_seeds_it_directly() {
MenuController menu; // simulates SnipsController.ino restoring a
// persisted selection at boot, before any switch
menu.setCurrentDroidName("BB-8");
TEST_ASSERT_EQUAL_STRING("BB-8", menu.currentDroidName());
}


// ---- labels ------------------------------------------------------------------

void test_main_menu_item_labels() {
Expand Down Expand Up @@ -774,6 +814,7 @@ int main(int argc, char **argv) {
RUN_TEST(test_device_info_enter_or_back_returns_to_main_menu);
RUN_TEST(test_enter_factory_reset_confirm_then_back_cancels);
RUN_TEST(test_factory_reset_confirmed_via_enter);
RUN_TEST(test_factory_reset_disconnects_the_radio);
RUN_TEST(test_trigger_calibration_full_flow);
RUN_TEST(test_back_during_trigger_calibration_cancels_and_resets);
RUN_TEST(test_stick_calibration_full_flow);
Expand All @@ -797,6 +838,7 @@ int main(int argc, char **argv) {
RUN_TEST(test_current_droid_name_defaults_to_none);
RUN_TEST(test_current_droid_name_set_on_successful_switch);
RUN_TEST(test_current_droid_name_unchanged_on_failed_switch);
RUN_TEST(test_set_current_droid_name_seeds_it_directly);
RUN_TEST(test_main_menu_item_labels);
RUN_TEST(test_render_inactive_leaves_screen_blank);
RUN_TEST(test_render_main_menu_marks_selected_item);
Expand Down
Loading