BLE Provisioning problem

I’m new to BLE and building a remote controlled sprinkler. I have two MKR1010 boards: one runs the sprinkler web server (the server) and the other acts as a BLE provisioning client (the client) for testing.

Objectives
BLE Pairing & Provisioning
The client scans for and connects to the selected server over BLE. Then the client writes the Wi Fi SSID and password into the server’s BLE characteristics.

Wi Fi HTTP Server Setup
The server uses those credentials to join my home network, then starts its HTTP server on port 80 and obtains an IP address (for example 192.168.1.42).

BLE Confirmation
The server writes its HTTP server IP into the BLE IP characteristic. The client reads that IP over BLE and then connects to the server’s HTTP host. Both devices then disconnect and close the BLE connection.

Sprinkler Control (next steps)
Once provisioned, the client will send HTTP commands (“START”, “STOP”, “CONFIG …”) to control the sprinkler zones.

Key UUIDs for quick reference
Provisioning Service: 12345678-1234-1234-1234-1234567890ab
SSID Characteristic: 0000ABCD-0000-1000-8000-00805F9B34FB
Password Characteristic: 0000EF01-0000-1000-8000-00805F9B34FB
IP Characteristic: 0000EF02-0000-1000-8000-00805F9B34FB

What I’ve checked so far
Matching UUID strings in client and server code
Characteristic permissions set to BLERead and BLEWrite
Timing loops for connection, discovery, and write operations
BLE stack initialization order on both sides

Questions

  1. Am I advertising the service correctly and is the client discovering it properly?
  2. Is my write/read sequence valid or should I be using notifications or a different API?
  3. Are there any BLE gotchas on the MKR1010 (for example, must I call BLE.poll() or BLE.advertise() differently)?

Client (Arduino) Serial Output
=== BLE Provisioning Client ===
Scanning for BLE devices for 6 seconds…
[0] c2:a2:a2:0e:be:5b ‘(no name)’
[1] b0:b2:1c:56:b0:b6 ‘MKR1010’
… (more devices listed) …
Enter device number (0–9):
You selected device number: 1
Connecting to b0:b2:1c:56:b0:b6
Connected
Discovering attributes (services/characteristics)…
Warning: discoverAttributes() returned false
ERROR: provisioning service not found

Server (MKR1010) Serial Output
=== MKR1010 Provisioning & Sprinkler ===
Advertising BLE provisioning service…
Waiting for BLE central to connect…
BLE central connected: b0:b2:1c:56:36:fa
Waiting for SSID and password…
Timeout waiting for credentials; restarting provisioning
Advertising BLE provisioning service…
Waiting for BLE central to connect…
… (repeats)

Problem Statement
The client sees and connects to the MKR1010 but can’t discover the provisioning service (UUID 12345678-1234-1234-1234-1234567890ab). On the server side it times out waiting for characteristics to be written, then resets advertising.

Server code

#include <SPI.h>
#include <ArduinoBLE.h>
#include <WiFiNINA.h>

// ───── UUID definitions ─────────────────────────────────────────────────────────
#define PROV_SERVICE_UUID  "12345678-1234-1234-1234-1234567890ab"
#define SSID_CHAR_UUID     "0000ABCD-0000-1000-8000-00805F9B34FB"
#define PASS_CHAR_UUID     "0000EF01-0000-1000-8000-00805F9B34FB"
#define IP_CHAR_UUID       "0000EF02-0000-1000-8000-00805F9B34FB"

// ───── BLE provisioning service & characteristics ───────────────────────────────
BLEService    provService(PROV_SERVICE_UUID);
BLECharacteristic ssidChar(SSID_CHAR_UUID, BLERead | BLEWrite, 32);
BLECharacteristic passChar(PASS_CHAR_UUID, BLERead | BLEWrite, 64);
BLECharacteristic ipChar(IP_CHAR_UUID,   BLERead,               16);

// ───── HTTP server on Wi Fi ─────────────────────────────────────────────────────
const int     SERVER_PORT = 80;
WiFiServer    httpServer(SERVER_PORT);

// ───── State variables ──────────────────────────────────────────────────────────
String ssid, pass;
bool   clientPreviouslyConnected = false;

void setup() {
  Serial.begin(115200);
  while (!Serial);

  Serial.println("\n=== MKR1010 Provisioning & Sprinkler ===");

  // Initialize BLE
  if (!BLE.begin()) {
    Serial.println("ERROR: BLE.init() failed");
    while (1) delay(1000);
  }
  delay(500);

  // Set up provisioning service
  provService.addCharacteristic(ssidChar);
  provService.addCharacteristic(passChar);
  provService.addCharacteristic(ipChar);
  BLE.addService(provService);
  BLE.setLocalName("MKR1010");
  BLE.setAdvertisedService(provService);

  // Run BLE provisioning with timeout logic
  bleProvisioning();

  // Start HTTP server after successful provisioning
  httpServer.begin();
  Serial.print("HTTP server live at http://");
  Serial.print(WiFi.localIP());
  Serial.print(':');
  Serial.println(SERVER_PORT);
}

void loop() {
  // Serve sprinkler commands over HTTP
  WiFiClient client = httpServer.available();
  bool isNowConnected = (bool)client;

  if (isNowConnected && !clientPreviouslyConnected) {
    Serial.println(">> HTTP client connected");
  } else if (!isNowConnected && clientPreviouslyConnected) {
    Serial.println(">> HTTP client disconnected");
  }
  clientPreviouslyConnected = isNowConnected;
  if (!isNowConnected) return;

  String cmd = client.readStringUntil('\n');
  cmd.trim();
  Serial.print("HTTP cmd → ");
  Serial.println(cmd);
  parseCommand(cmd, client);
  client.stop();
}

// BLE provisioning: SSID/PW over BLE → Wi Fi → return IP over BLE
void bleProvisioning() {
  while (true) {
    // Advertise & wait for a central
    BLE.advertise();
    Serial.println("Advertising BLE provisioning service…");
    Serial.println("Waiting for BLE central to connect…");
    BLEDevice central;
    do {
      central = BLE.central();
      delay(100);
    } while (!central);

    Serial.print("BLE central connected: ");
    Serial.println(central.address());

    // Wait for SSID & password writes with 10s timeout
    bool gotSSID = false, gotPass = false;
    unsigned long start = millis();
    Serial.println("Waiting for SSID & password…");
    while (!(gotSSID && gotPass)) {
      if (millis() - start > 30000) {
        Serial.println("⏱  Timeout waiting for credentials; restarting provisioning");
        central.disconnect();
        BLE.stopAdvertise();
        delay(200);
        break;  // restart provisioning loop
      }
      if (!gotSSID && ssidChar.written()) {
        char buf[33] = {0};
        ssidChar.readValue((uint8_t*)buf, ssidChar.valueLength());
        ssid = String(buf);
        Serial.print(" • Got SSID: ");
        Serial.println(ssid);
        gotSSID = true;
      }
      if (!gotPass && passChar.written()) {
        char buf[65] = {0};
        passChar.readValue((uint8_t*)buf, passChar.valueLength());
        pass = String(buf);
        Serial.print(" • Got Password: ");
        Serial.println(pass);
        gotPass = true;
      }
      delay(100);
    }

    if (!(gotSSID && gotPass)) {
      // Credentials not fully received in time → retry
      continue;
    }

    // Join Wi Fi
    Serial.println("Joining Wi Fi…");
    if (!connectToWiFi()) {
      Serial.println("ERROR: Wi Fi join failed");
      while (1) delay(1000);
    }

    // Send IP back over BLE
    String ip = WiFi.localIP().toString();
    ipChar.writeValue((const uint8_t*)ip.c_str(), ip.length());
    Serial.print("Wrote IP to BLE: ");
    Serial.println(ip);

    // Keep BLE alive for client to read IP
    Serial.println("Keeping BLE alive for 5 s for IP read…");
    delay(5000);

    // Teardown and exit provisioning
    BLE.stopAdvertise();
    central.disconnect();
    Serial.println("Provisioning complete!");
    break;
  }
}

// Wi Fi connection helper
bool connectToWiFi() {
  WiFi.begin(ssid.c_str(), pass.c_str());
  int retries = 0;
  while (WiFi.status() != WL_CONNECTED && retries++ < 20) {
    delay(500);
    Serial.print('.');
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("Connected! IP: ");
    Serial.println(WiFi.localIP());
    return true;
  }
  return false;
}

// Handle sprinkler commands over HTTP
void parseCommand(const String &cmd, WiFiClient &c) {
  if (cmd.equalsIgnoreCase("START")) {
    c.println("OK: Started");
    Serial.println("Sprinkler STARTED");
  }
  else if (cmd.equalsIgnoreCase("STOP")) {
    c.println("OK: Stopped");
    Serial.println("Sprinkler STOPPED");
  }
  else if (cmd.startsWith("CONFIG ")) {
    c.println("OK: Config updated");
    Serial.print("Config → ");
    Serial.println(cmd.substring(7));
  }
  else {
    c.println("ERROR: Unknown command");
    Serial.println("Unknown HTTP command");
  }
}

Client code

#include <SPI.h>
#include <ArduinoBLE.h>

// ───── UUIDs ───────────────────────────────────────────────────────────────────
const char* PROV_SERVICE_UUID = "12345678-1234-1234-1234-1234567890ab";
const char* SSID_CHAR_UUID    = "0000ABCD-0000-1000-8000-00805F9B34FB";
const char* PASS_CHAR_UUID    = "0000EF01-0000-1000-8000-00805F9B34FB";
const char* IP_CHAR_UUID      = "0000EF02-0000-1000-8000-00805F9B34FB";

// ───── Test credentials ─────────────────────────────────────────────────────────
const char* testSsid = "my ssid";
const char* testPass = "my password";

// ───── Scan settings ───────────────────────────────────────────────────────────
const unsigned long SCAN_DURATION = 6000;  // scan for 6 seconds
const int MAX_DEVICES = 12;

void setup() {
  Serial.begin(115200);
  while (!Serial);

  Serial.println("\n=== BLE Provisioning Client ===");
  if (!BLE.begin()) {
    Serial.println("ERROR: BLE.begin() failed");
    while (1) delay(1000);
  }

  // 1) Scan for peripherals
  Serial.print("Scanning for BLE devices for ");
  Serial.print(SCAN_DURATION / 1000);
  Serial.println(" seconds...");
  BLE.scan();

  BLEDevice devices[MAX_DEVICES];
  int deviceCount = 0;
  unsigned long scanStart = millis();

  while (millis() - scanStart < SCAN_DURATION) {
    BLEDevice dev = BLE.available();
    if (dev && deviceCount < MAX_DEVICES) {
      String addr = dev.address();
      bool seen = false;
      for (int i = 0; i < deviceCount; i++) {
        if (devices[i].address() == addr) {
          seen = true;
          break;
        }
      }
      if (!seen) {
        devices[deviceCount] = dev;
        String name = dev.localName();
        if (name.length() == 0) name = "(no name)";
        Serial.print("[");
        Serial.print(deviceCount);
        Serial.print("] ");
        Serial.print(addr);
        Serial.print(" '");
        Serial.print(name);
        Serial.println("'");
        deviceCount++;
      }
    }
  }
  BLE.stopScan();

  if (deviceCount == 0) {
    Serial.println("No BLE devices found. Halting.");
    while (1) delay(1000);
  }

  // 2) Prompt for selection
  int selection = -1;
  while (selection < 0 || selection >= deviceCount) {
    Serial.print("Enter device number (0–");
    Serial.print(deviceCount - 1);
    Serial.print("): ");
    while (!Serial.available()) delay(10);
    selection = Serial.parseInt();
    Serial.read();  // consume the newline

    // Echo back the choice
    Serial.print("You selected device number: ");
    Serial.println(selection);

    if (selection < 0 || selection >= deviceCount) {
      Serial.println("Invalid selection, please try again.");
    }
  }

  BLEDevice peripheral = devices[selection];
  Serial.print("Connecting to ");
  Serial.println(peripheral.address());
  // ─── Fix: call connect() on the peripheral, not BLE
  if (!peripheral.connect()) {
    Serial.println("ERROR: Connection failed");
    while (1) delay(1000);
  }
  Serial.println("Connected");
  delay(200);  // give BLE stack a moment

  // 3) Discover attributes
  Serial.println("Discovering attributes (services/characteristics)...");
  if (!peripheral.discoverAttributes()) {
    Serial.println("Warning: discoverAttributes() returned false");
  }
  delay(200);

  // 4) Locate provisioning service
  BLEService prov = peripheral.service(PROV_SERVICE_UUID);
  unsigned long svcStart = millis();
  while (!prov && millis() - svcStart < 5000) {
    // sometimes it takes a bit to appear
    delay(200);
    prov = peripheral.service(PROV_SERVICE_UUID);
  }
  if (!prov) {
    Serial.println("ERROR: provisioning service not found");
    peripheral.disconnect();
    while (1) delay(1000);
  }

  // 5) Locate characteristics
  BLECharacteristic ssidChr = prov.characteristic(SSID_CHAR_UUID);
  BLECharacteristic passChr = prov.characteristic(PASS_CHAR_UUID);
  BLECharacteristic ipChr   = prov.characteristic(IP_CHAR_UUID);
  if (!ssidChr || !passChr || !ipChr) {
    Serial.println("ERROR: missing one or more characteristics");
    peripheral.disconnect();
    while (1) delay(1000);
  }

  // 6) Write SSID
  Serial.print("Writing SSID: ");
  Serial.println(testSsid);
  if (ssidChr.writeValue((const uint8_t*)testSsid, strlen(testSsid))) {
    Serial.println(" → SSID written");
  } else {
    Serial.println(" → SSID write FAILED");
  }

  // 7) Write Password
  Serial.print("Writing Password: ");
  Serial.println(testPass);
  if (passChr.writeValue((const uint8_t*)testPass, strlen(testPass))) {
    Serial.println(" → Password written");
  } else {
    Serial.println(" → Password write FAILED");
  }

  // 8) Wait up to 10s for the server to join Wi Fi & publish its IP
  Serial.println("Waiting up to 10 s for server to join Wi Fi & publish IP…");
  unsigned long ipStart = millis();
  String ip;
  while (millis() - ipStart < 10000) {
    int len = ipChr.valueLength();
    if (len > 0) {
      uint8_t buf[32] = {0};
      int readLen = ipChr.readValue(buf, sizeof(buf) - 1);
      if (readLen > 0) {
        buf[readLen] = '\0';
        ip = String((char*)buf);
        break;
      }
    }
    delay(200);
  }

  // 9) Report IP or timeout
  if (ip.length()) {
    Serial.print("Server IP: ");
    Serial.println(ip);
  } else {
    Serial.println("ERROR: no IP received within timeout");
  }

  // 10) Disconnect and finish
  peripheral.disconnect();
  Serial.println("Provisioning client done.");
}

void loop() {
  // nothing to do
}

Why is Bluetooth needed? Isn't this just a server /client over WiFi type of app? It would sure be simpler and probably more reliable.

The sprinkler or RC car, or whatever may not have a way to enter the credentials. Think of it like a smart outlet, no display, no ketboard, not way to set it up on your network. The use BLE to send the credentials and then use them to connect to your Wi-Fi. That is what I am trying to duplicate here.

Ok, but some other options are to Hard Code, use w WiFi Manager, config file.
I have used WiFiManager a lot, initial creds are entered via my phone connecting to the RC car AP and it then saves the creds for future use.
Not saying what you are doing won't work, I just find BT to be more trouble than what it is worth.

The whole point of this effort is to do it without hard coding.

Understood, but I wanted to list most if not all the options. Have you looked at the WiFi Manager solution? It is only a couple lines of code and I have modified many existing WiFi sketches to work with it in less than 5 minutes. It uses the Standard 192.168.4.1 address to set up the WiFi. Normally only needed once but you can easily make minor modifications to use the AP to present a menu with other options and store/retrieve from EEPROM.
I will follow so I can also learn the BT way as I may find a use for it.
Thanks for the idea and good luck. If you need any WiFiManager help, just give me a shout(message)

I have made some progress on this. Right now, the sssid and the password are passed, and the Http server is created on the peripheral side. The next step is to pass the server IP address back to the central so it can connect to the server.

I'll look at that on Monday because my brain is fried just getting this far, and tomorrow is Mother's Day.

This Bluetooth stuff has been quite challenging for me, but I'm happy with what is working so far.