├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── collect_binaries.py ├── platformio.ini └── src └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | CMakeListsPrivate.txt 3 | cmake-build-*/ 4 | binaries 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # !!! WARNING !!! AUTO-GENERATED FILE, PLEASE DO NOT MODIFY IT AND USE 2 | # https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags 3 | # 4 | # If you need to override existing CMake configuration or add extra, 5 | # please create `CMakeListsUser.txt` in the root of project. 6 | # The `CMakeListsUser.txt` will not be overwritten by PlatformIO. 7 | 8 | cmake_minimum_required(VERSION 3.13) 9 | set(CMAKE_SYSTEM_NAME Generic) 10 | set(CMAKE_C_COMPILER_WORKS 1) 11 | set(CMAKE_CXX_COMPILER_WORKS 1) 12 | 13 | project("BLEExposureNotificationBeeper" C CXX) 14 | 15 | include(CMakeListsPrivate.txt) 16 | 17 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/CMakeListsUser.txt) 18 | include(CMakeListsUser.txt) 19 | endif() 20 | 21 | add_custom_target( 22 | Production ALL 23 | COMMAND platformio -c clion run "$<$>:-e${CMAKE_BUILD_TYPE}>" 24 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 25 | ) 26 | 27 | add_custom_target( 28 | Debug ALL 29 | COMMAND platformio -c clion run --target debug "$<$>:-e${CMAKE_BUILD_TYPE}>" 30 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 31 | ) 32 | 33 | add_executable(Z_DUMMY_TARGET ${SRC_LIST}) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kaspar Metz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BLE Exposure Notification Beeper 2 | 3 | Allows you to build a simple device that beeps and flashes an LED whenever someone using a COVID-19 warning/tracing app that uses [Bluetooth LE Exposure Notifications](https://en.wikipedia.org/wiki/Exposure_Notification), like the german [Corona-Warn-App](https://www.coronawarn.app/en/), is seen nearby. 4 | 5 | Notifiers (nearby devices with a warning app) are remembered for 20 minutes so it only beeps for newly detected ones. Note that the IDs will change every 15 minutes or so for privacy reasons, triggering new beeps. 6 | 7 | ## Build your own 8 | ### You need 9 | 1. Any ESP32 board (like a WEMOS D32 or TTGO ESP32, preferrably with a LiPo battery controller). 10 | 2. A 3V piezo buzzer (a simple speaker or headphones will also work). 11 | 3. Optionally a LED. 12 | 4. Optionally a small 1-cell LiPo battery. 13 | 5. A nice case :) 14 | 15 | ### Wiring 16 | 17 | | ESP32 pin | goes to | 18 | |:----------|:--------------------| 19 | | GPIO 0 | LED (+) | 20 | | GPIO 2 | Buzzer (+) | 21 | | GND | LED (-), Buzzer (-) | 22 | 23 | - Pins can easily be changed in code (`src/main.cpp`). 24 | 25 | ### Flashing the ESP32 26 | If you don't have [PlatfomIO](https://platformio.org/platformio-ide) installed, you can flash precompiled binaries directly with [esptool.py](https://github.com/espressif/esptool). 27 | 28 | #### Using PlatfomIO 29 | - Simply open the project and upload. 30 | - Or via command line: `platformio run -t upload` 31 | - You may need to install the [arduino-esp32](https://github.com/espressif/arduino-esp32) platform and the [lbernstone/Tone](https://github.com/lbernstone/Tone) library first. 32 | 33 | #### Using esptool.py 34 | - Install esptool.py: `pip install esptool` 35 | - On Windows, you might need to [install python](https://www.python.org/downloads/windows/) (2 or 3) first. 36 | - Download the [latest binaries](https://github.com/kmetz/BLEExposureNotificationBeeper/releases). 37 | - `cd` into the directory where the (unzipped) binaries are located. 38 | - Flash using the following command: 39 | ``` 40 | esptool.py \ 41 | --chip esp32 \ 42 | --baud 460800 \ 43 | --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect \ 44 | 0x1000 bootloader_dio_40m.bin \ 45 | 0x8000 partitions.bin \ 46 | 0xe000 boot_app0.bin \ 47 | 0x10000 firmware.bin 48 | ``` 49 | - You may need to define your serial port with something like `--port "/dev/cu.usbserial-0001"` if esptool.py doesn't automatically find it. 50 | - Some ESP32 boards require you to press (and hold) the flash button until flashing begins. 51 | -------------------------------------------------------------------------------- /collect_binaries.py: -------------------------------------------------------------------------------- 1 | Import("env", "projenv") 2 | 3 | """ 4 | Copies all required binaries to /binaries. 5 | """ 6 | 7 | platform = env.PioPlatform() 8 | 9 | env.AddPostAction( 10 | "buildprog", 11 | env.VerboseAction(" ".join([ 12 | "mkdir -p binaries && cp", 13 | "/".join([platform.get_package_dir("framework-arduinoespressif32"), "tools/sdk/bin/bootloader_dio_40m.bin"]), 14 | "binaries/" 15 | ]), "Copying bootloader_dio_40m.bin to ./binaries") 16 | ) 17 | 18 | env.AddPostAction( 19 | "buildprog", 20 | env.VerboseAction(" ".join([ 21 | "mkdir -p binaries && cp", 22 | "/".join([platform.get_package_dir("framework-arduinoespressif32"), "tools/partitions/boot_app0.bin"]), 23 | "binaries/" 24 | ]), "Copying boot_app0.bin to ./binaries") 25 | ) 26 | 27 | env.AddPostAction( 28 | "$BUILD_DIR/partitions.bin", 29 | env.VerboseAction("mkdir -p binaries && cp $BUILD_DIR/partitions.bin binaries/", 30 | "Copying partitions.bin to ./binaries") 31 | ) 32 | env.AddPostAction( 33 | "$BUILD_DIR/${PROGNAME}.bin", 34 | env.VerboseAction("mkdir -p binaries && cp $BUILD_DIR/${PROGNAME}.bin binaries/", 35 | "Copying ${PROGNAME}.bin to ./binaries") 36 | ) 37 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32dev] 12 | platform = espressif32 13 | board = esp32dev 14 | framework = arduino 15 | build_flags = -std=c++11 -Wno-error=reorder 16 | lib_deps = 17 | Arduino 18 | lbernstone/Tone32 @ ^1.0.0 19 | monitor_speed = 115200 20 | monitor_filters = time, esp32_exception_decoder 21 | extra_scripts = post:collect_binaries.py 22 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "Tone32.h" 6 | 7 | 8 | // Pin where the + of an LED is connected. 9 | #define LED_PIN 0 10 | 11 | // Pin where the + of a piezo buzzer is connected. 12 | #define BUZZER_PIN 2 13 | 14 | // Tone to play. 15 | #define BEEP_NOTE NOTE_C6 16 | 17 | // Tone duration in milliseconds. 18 | #define BEEP_DURATION_MS 100 19 | 20 | // Scan update time, 5 seems to be a good value. 21 | #define SCAN_TIME_SECONDS 5 22 | 23 | // When to forget old senders. 24 | #define FORGET_AFTER_MINUTES 20 25 | 26 | 27 | BLEScan *scanner; 28 | std::map seenNotifiers; 29 | 30 | 31 | /** 32 | * Called when a new exposure notifier is seen. 33 | */ 34 | void onNewNotifierFound() { 35 | Serial.println("BEEP"); 36 | digitalWrite(LED_PIN, HIGH); 37 | tone(BUZZER_PIN, BEEP_NOTE, BEEP_DURATION_MS, 0); 38 | digitalWrite(LED_PIN, LOW); 39 | } 40 | 41 | 42 | class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { 43 | /** 44 | * Called when a BLE device is discovered. 45 | */ 46 | void onResult(BLEAdvertisedDevice advertisedDevice) override { 47 | if (!advertisedDevice.haveServiceUUID() 48 | || !advertisedDevice.getServiceUUID().equals(BLEUUID((uint16_t) 0xfd6f)) 49 | ) { 50 | return; 51 | } 52 | 53 | if (!seenNotifiers.count(advertisedDevice.getAddress().toString())) { 54 | // New notifier found. 55 | Serial.printf("+ %s \r\n", advertisedDevice.getAddress().toString().c_str()); 56 | onNewNotifierFound(); 57 | } 58 | else { 59 | // We've already seen this one. 60 | Serial.printf("... %s \r\n", advertisedDevice.getAddress().toString().c_str()); 61 | } 62 | 63 | // Remember, with time. 64 | seenNotifiers[advertisedDevice.getAddress().toString()] = millis(); 65 | } 66 | }; 67 | 68 | 69 | /** 70 | * Remove notifiers last seen over FORGET_AFTER_MINUTES ago. 71 | */ 72 | void forgetOldNotifiers() { 73 | for (auto const ¬ifier : seenNotifiers) { 74 | if (notifier.second + (FORGET_AFTER_MINUTES * 60 * 1000) < millis()) { 75 | Serial.printf("- %s \r\n", notifier.first.c_str()); 76 | seenNotifiers.erase(notifier.first); 77 | } 78 | } 79 | } 80 | 81 | 82 | void setup() { 83 | Serial.begin(115200); 84 | Serial.println("Hi."); 85 | 86 | // Initialize pins. 87 | pinMode(LED_PIN, OUTPUT); 88 | pinMode(BUZZER_PIN, OUTPUT); 89 | 90 | // Initialize scanner. 91 | BLEDevice::init("ESP"); 92 | scanner = BLEDevice::getScan(); 93 | scanner->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); 94 | scanner->setActiveScan(true); // Active scan uses more power, but gets results faster. 95 | scanner->setInterval(100); 96 | scanner->setWindow(99); 97 | 98 | Serial.println("Scanning ..."); 99 | } 100 | 101 | 102 | void loop() { 103 | Serial.println("-----"); 104 | 105 | // Scan. 106 | scanner->start(SCAN_TIME_SECONDS, false); 107 | 108 | // Cleanup. 109 | scanner->clearResults(); 110 | forgetOldNotifiers(); 111 | 112 | Serial.printf("Count: %d \r\n", seenNotifiers.size()); 113 | } 114 | --------------------------------------------------------------------------------