Skip to content

Instantly share code, notes, and snippets.

@joshnuss
Last active August 17, 2023 21:46
Show Gist options
  • Save joshnuss/52a2dae98c4037ef70954432ee798fda to your computer and use it in GitHub Desktop.
Save joshnuss/52a2dae98c4037ef70954432ee798fda to your computer and use it in GitHub Desktop.
Bluetooth Speedometer using ESP32 and Hall effect sensor
/*
* Using digital hall effect sensor SENS-M-10 (purchased at Abra)
*
* MCU Board: ESP2-WROOM-32
*/
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#define MEASURE_PIN 23
#define LED_BUILTIN 2
#define WHEEL_CIRCUMFERANCE 0.7
#define SAMPLES 10
#define SERVICE_UUID "5aad4ed0-a7dd-46b2-965b-7a5a159b07a2"
#define CHARACTERISTIC_RPM_UUID "69758442-6da8-497c-a29c-48b2c48d6ceb"
typedef struct {
uint16_t ms;
uint16_t revolutions;
} sample_t;
class RPMCallbacks : public BLECharacteristicCallbacks {
private:
double* pRps;
public:
RPMCallbacks(double* pRps) {
this->pRps = pRps;
}
void onRead(BLECharacteristic *pCharacteristic) {
double rpm = *pRps * 60;
pCharacteristic->setValue(std::to_string(rpm));
}
};
volatile uint8_t nextSample = 0;
volatile sample_t samples[SAMPLES] = {};
hw_timer_t * timer = NULL;
// temp vars for computing totals
volatile sample_t* pSample;
uint8_t n;
uint8_t totalRevolutions;
uint8_t totalSamples;
uint16_t minMs;
uint16_t maxMs;
double rps;
// interrupt handler is triggered every 100ms
void IRAM_ATTR timer_interrupt() {
// compute total revolutions and revolutions per second
totalRevolutions = 0;
totalSamples = 0;
minMs = 0;
maxMs = 0;
rps = 0;
// iterate thru samples
for (int i=0;i<SAMPLES;i++) {
n = (nextSample+i) % SAMPLES;
pSample = &samples[n];
if (pSample->ms > 0) {
// good sample
totalSamples++;
totalRevolutions += pSample->revolutions;
minMs = std::min(minMs, (uint16_t)pSample->ms);
maxMs = std::max(maxMs, (uint16_t)pSample->ms);
}
}
// compute rps
if (totalSamples > 0) {
rps = totalRevolutions * 1.0 / totalSamples;
}
// prepare for a new sample
if (nextSample >= SAMPLES-1) {
nextSample = 0;
} else {
nextSample++;
}
// clear old sample data
samples[nextSample].ms = millis();
samples[nextSample].revolutions = 0;
}
// interrupt handler is triggered when magnet enters or leaves field
void magnet_detection_changed() {
// check if magnet is nearby
if (digitalRead(MEASURE_PIN) == HIGH) {
// increase revolutions
samples[nextSample].revolutions++;
// turn on built-in LED
digitalWrite(LED_BUILTIN, LOW);
} else {
// turn off built-in LED
digitalWrite(LED_BUILTIN, HIGH);
}
}
void setup() {
Serial.begin(115200);
pinMode(MEASURE_PIN, INPUT);
pinMode(LED_BUILTIN, OUTPUT);
// prescaler 80 is for 80MHz clock. One tick of the timer is 1us
timer = timerBegin(0, 80, true);
// attach interrupt handler
timerAttachInterrupt(timer, &timer_interrupt, true);
// trigger timer every 100ms (100,000us)
timerAlarmWrite(timer, 100000, true);
// enable the timer
timerAlarmEnable(timer);
// trigger function when hall effect sensor changes (goes low->high or high->low)
attachInterrupt(MEASURE_PIN, magnet_detection_changed, CHANGE);
// setup BLE device
BLEDevice::init("Speedometer");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
// RPM characteristic
BLECharacteristic *pRPMCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_RPM_UUID,
BLECharacteristic::PROPERTY_READ
);
pRPMCharacteristic->setCallbacks(new RPMCallbacks(&rps));
pService->start();
// BLEAdvertising *pAdvertising = pServer->getAdvertising(); // this still is working for backward compatibility
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
BLEAdvertisementData advertisementData = BLEAdvertisementData();
advertisementData.setName("Speedometer");
pAdvertising->setAdvertisementData(advertisementData);
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
}
void loop() {
// output calculations for debug
Serial.print("samples=");
Serial.print(totalSamples);
Serial.print(", revolutions=");
Serial.print(totalRevolutions);
Serial.print(", rps=");
Serial.print(rps);
Serial.print(", rpm=");
Serial.print(rps * 60);
Serial.print(", speed=");
Serial.print(rps * 60 * WHEEL_CIRCUMFERANCE);
Serial.println();
delay(1000);
}
@Dim4ik1992
Copy link

Dim4ik1992 commented Aug 4, 2022

Как его подключить к телефону через ble ? На Android

@joshnuss
Copy link
Author

joshnuss commented Aug 4, 2022

@Dim4ik1992 on Android, you can test it with the app Nordic nRF Connect, or write a program (I wrote one with Flutter)

@Dim4ik1992
Copy link

@Dim4ik1992на Android вы можете протестировать его с помощью приложения Nordic nRF Connect или написать программу (я написал ее с помощью Flutter)

Так может вы просто поделитесь программой ? Я не спец в приложениях

@joshnuss
Copy link
Author

joshnuss commented Aug 6, 2022

This is nRF Connect, it is for testing Bluetooth devices:
https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp

Unfortunately, I can't share the program I wrote as it's part of a much bigger codebase, so I don't think it would help you.

You would probably need to write your own Android Bluetooth app to make use of this.

@joshnuss
Copy link
Author

joshnuss commented Aug 6, 2022

I would search for "esp32 Bluetooth flutter tutorials"
For example, maybe this would help:
https://github.com/martinloretzzz/flutter-esp32-bluetooth

@JavaMcGee
Copy link

I loaded this on my ESP-WROOM-32 and it worked to connect and read data fine (thanks for this). However it would not allow reconnecting after a disconnect without restarting the device. Found a suggestion on another forum to add a server callback for onDisconnect to start the advertising again which seems to resolve the problem:

class MyServerCallbacks: public BLEServerCallbacks {
  public: 
  
  void onConnect(BLEServer* pServer) {
    Serial.println("ble connect");
  }
  
  void onDisconnect(BLEServer* pServer) {
    Serial.println("ble disconnect");
    pServer->getAdvertising()->start();
  }
};

and register the callback just after createServer():

pServer->setCallbacks(new MyServerCallbacks());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment