├── LICENSE ├── Project ├── hdc1080.py └── main.py └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ardy Seto 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 | -------------------------------------------------------------------------------- /Project/hdc1080.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Mike Causer 2 | # SPDX-License-Identifier: MIT 3 | 4 | """ 5 | MicroPython HDC1080 Temperature & Humidity Sensor 6 | https://github.com/mcauser/micropython-hdc1080 7 | """ 8 | 9 | __version__ = "1.0.1" 10 | 11 | from micropython import const 12 | from time import sleep_ms 13 | 14 | # registers 15 | _TEMPERATURE = const(0x00) 16 | _HUMIDITY = const(0x01) 17 | _CONFIG = const(0x02) 18 | _SERIAL_ID0 = const(0xFB) 19 | _SERIAL_ID1 = const(0xFC) 20 | _SERIAL_ID2 = const(0xFD) 21 | _MANUFACTURER_ID = const(0xFE) 22 | _DEVICE_ID = const(0xFF) 23 | 24 | 25 | class HDC1080: 26 | def __init__(self, i2c): 27 | self._i2c = i2c 28 | self._address = 0x40 # fixed I2C address 29 | self._buf1 = bytearray(1) 30 | self._buf2 = bytearray(2) 31 | self._config = 0x10 32 | 33 | def _read16(self, reg): 34 | self._buf1[0] = reg 35 | self._i2c.writeto(self._address, self._buf1) 36 | sleep_ms(20) 37 | self._i2c.readfrom_into(self._address, self._buf2) 38 | return (self._buf2[0] << 8) | self._buf2[1] 39 | 40 | def _write_config(self): 41 | self._buf2[0] = _CONFIG 42 | self._buf2[1] = self._config 43 | self._i2c.writeto(self._address, self._buf2) 44 | 45 | def _read_config(self): 46 | # shift out the first 8 reserved bits 47 | self._config = self._read16(_CONFIG) >> 8 48 | 49 | def check(self): 50 | if self._i2c.scan().count(self._address) == 0: 51 | raise OSError(f"HDC1080 not found at I2C address {self._address:#x}") 52 | return True 53 | 54 | def config( 55 | self, config=None, humid_res=None, temp_res=None, mode=None, heater=None 56 | ): 57 | if config is not None: 58 | self._config = config 59 | self._write_config() 60 | else: 61 | self._read_config() 62 | if humid_res is not None: 63 | # 00 = 14-bit, 01 = 11-bit, 10 = 8-bit 64 | if humid_res == 8: 65 | self._config |= 2 66 | self._config &= ~1 67 | elif humid_res == 11: 68 | self._config &= ~2 69 | self._config |= 1 70 | elif humid_res == 14: 71 | self._config &= ~3 72 | else: 73 | raise ValueError("humid_res must be 8, 11 or 14") 74 | if temp_res is not None: 75 | # 0 = 14-bit, 1 = 11-bit 76 | if temp_res == 11: 77 | self._config |= 4 78 | elif temp_res == 14: 79 | self._config &= ~4 80 | else: 81 | raise ValueError("temp_res must be 11 or 14") 82 | if mode is not None: 83 | # mode 0 = temp or humid acquired 84 | # mode 1 = temp and humid acquired in sequence, temp first 85 | self._config &= ~16 86 | self._config |= (mode & 1) << 4 87 | if heater is not None: 88 | self._config &= ~32 89 | self._config |= (heater & 1) << 5 90 | self._write_config() 91 | 92 | def reset(self): 93 | self._config = 128 94 | self._write_config() 95 | # sw reset bit self clears 96 | self._read_config() 97 | 98 | def battery_status(self): 99 | # returns 0 if Vcc > 2.8V 100 | # returns 1 if Vcc < 2.8V 101 | self._read_config() 102 | return (self._config >> 3) & 1 103 | 104 | def temperature(self): 105 | # temperature in celsius 106 | return (self._read16(_TEMPERATURE) / 65536) * 165 - 40 107 | 108 | def humidity(self): 109 | # relative humidity percentage 110 | return (self._read16(_HUMIDITY) / 65536) * 100 111 | 112 | def serial_number(self): 113 | # unique per device 114 | return ( 115 | (self._read16(_SERIAL_ID0) << 24) 116 | | (self._read16(_SERIAL_ID1) << 8) 117 | | (self._read16(_SERIAL_ID2) >> 8) 118 | ) 119 | 120 | def manufacturer_id(self): 121 | # fixed 21577 == 0x5449 == b'\x54\x49' == b'TI' 122 | return self._read16(_MANUFACTURER_ID) 123 | 124 | def device_id(self): 125 | # fixed 4176 == 0x1050 == b'\x10\x50' == b'\x10P' 126 | return self._read16(_DEVICE_ID) 127 | -------------------------------------------------------------------------------- /Project/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py – MicroPython ESP32 BLE UART Demo 3 | 4 | This script configures an ESP32 as a BLE (Bluetooth Low Energy) peripheral offering 5 | the Nordic UART Service (NUS). It lets a central device (e.g., smartphone app) 6 | send simple text commands to: 7 | - Toggle an onboard LED 8 | - Read temperature and humidity from an HDC1080 sensor 9 | 10 | The on-board LED also blinks when disconnected and stays solid when connected. 11 | """ 12 | 13 | from machine import Pin, Timer, SoftI2C 14 | from time import sleep_ms 15 | import ubluetooth 16 | # raw_temperature is available but not used in this example; you could use it for direct chip-temp reads 17 | from esp32 import raw_temperature 18 | from hdc1080 import HDC1080 19 | 20 | class BLE: 21 | """ 22 | BLE UART helper class. 23 | 24 | Handles: 25 | - BLE initialization & advertising 26 | - Connection / disconnection callbacks 27 | - GATT service registration (Nordic UART Service) 28 | - Reading incoming commands and sending notifications 29 | 30 | Commands supported over BLE: 31 | - 'red_led' : Toggle the red LED (GPIO2) 32 | - 'read_temp' : Read temperature in °C from HDC1080 33 | - 'read_hum' : Read relative humidity (%) from HDC1080 34 | """ 35 | def __init__(self, name): 36 | """ 37 | Initialize the BLE device. 38 | 39 | Args: 40 | name (str): The advertised device name (e.g., "ESP32"). 41 | """ 42 | self.name = name 43 | self.ble = ubluetooth.BLE() 44 | self.ble.active(True) 45 | 46 | # On-board LED pin (GPIO2 on many ESP32 boards) 47 | self.led = Pin(2, Pin.OUT) 48 | 49 | # Two timers for blinking the LED when disconnected 50 | self.timer1 = Timer(0) 51 | self.timer2 = Timer(1) 52 | 53 | # Start with disconnected blink pattern 54 | self.disconnected() 55 | 56 | # Register IRQ callback before registering services 57 | self.ble.irq(self.ble_irq) 58 | 59 | # Setup Nordic UART GATT service 60 | self.register() 61 | 62 | # Begin advertising 63 | self.advertiser() 64 | 65 | def connected(self): 66 | """ 67 | Called when a central device connects. 68 | Stops the blink timers so the LED can stay solid. 69 | """ 70 | self.timer1.deinit() 71 | self.timer2.deinit() 72 | 73 | def disconnected(self): 74 | """ 75 | Called when no central is connected. 76 | Starts two timers to blink the LED: 77 | - Timer1 sets LED on every second 78 | - Timer2 clears LED every second, offset by 200ms 79 | """ 80 | self.timer1.init(period=1000, mode=Timer.PERIODIC, 81 | callback=lambda t: self.led(1)) 82 | sleep_ms(200) 83 | self.timer2.init(period=1000, mode=Timer.PERIODIC, 84 | callback=lambda t: self.led(0)) 85 | 86 | def ble_irq(self, event, data): 87 | """ 88 | BLE interrupt request handler. 89 | 90 | Args: 91 | event (int): The BLE event code. 92 | data: Event-specific data. 93 | """ 94 | if event == 1: 95 | # _IRQ_CENTRAL_CONNECT: a central has connected 96 | self.connected() 97 | self.led(1) # solid on to indicate connection 98 | 99 | elif event == 2: 100 | # _IRQ_CENTRAL_DISCONNECT: central has disconnected 101 | self.advertiser() # resume advertising 102 | self.disconnected() # restart blink pattern 103 | 104 | elif event == 3: 105 | # _IRQ_GATTS_WRITE: client has written to RX characteristic 106 | buffer = self.ble.gatts_read(self.rx) 107 | message = buffer.decode('utf-8').strip() 108 | print("Received over BLE:", message) 109 | 110 | # Handle known commands 111 | if message == 'red_led': 112 | # toggle the LED and send status back 113 | red_led.value(not red_led.value()) 114 | print('red_led state:', red_led.value()) 115 | self.send('red_led ' + str(red_led.value())) 116 | 117 | elif message == 'read_temp': 118 | # read temp in °C (True for fast, raw data) 119 | temp = sensor.read_temperature(True) 120 | print('Temperature:', temp) 121 | self.send(str(temp)) 122 | 123 | elif message == 'read_hum': 124 | # read humidity in % 125 | hum = sensor.read_humidity() 126 | print('Humidity:', hum) 127 | self.send(str(hum)) 128 | 129 | def register(self): 130 | """ 131 | Register the Nordic UART Service (NUS) with two characteristics: 132 | - TX (notify) for sending data to central 133 | - RX (write) for receiving data from central 134 | """ 135 | # NUS base UUID and characteristic UUIDs 136 | NUS_UUID = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E' 137 | RX_UUID = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E' 138 | TX_UUID = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E' 139 | 140 | BLE_NUS = ubluetooth.UUID(NUS_UUID) 141 | BLE_RX = (ubluetooth.UUID(RX_UUID), ubluetooth.FLAG_WRITE) 142 | BLE_TX = (ubluetooth.UUID(TX_UUID), ubluetooth.FLAG_NOTIFY) 143 | 144 | # Define the service tuple 145 | BLE_UART = (BLE_NUS, (BLE_TX, BLE_RX,)) 146 | SERVICES = (BLE_UART,) 147 | 148 | # Register GATT services and save characteristic handles 149 | ((self.tx, self.rx,),) = self.ble.gatts_register_services(SERVICES) 150 | 151 | def send(self, data): 152 | """ 153 | Send a notification on the TX characteristic. 154 | 155 | Args: 156 | data (str): The string to send (a newline will be appended). 157 | """ 158 | self.ble.gatts_notify(0, self.tx, data + '\n') 159 | 160 | def advertiser(self): 161 | """ 162 | Start BLE advertising with the given device name. 163 | Uses a simple non-connectable advertisement packet every 100ms. 164 | """ 165 | name_bytes = bytes(self.name, 'utf-8') 166 | adv_payload = bytearray('\x02\x01\x02') + bytearray((len(name_bytes) + 1, 0x09)) + name_bytes 167 | self.ble.gap_advertise(100, adv_payload) 168 | 169 | 170 | # === Main / Test Code === 171 | 172 | # Initialize I2C for the HDC1080 sensor 173 | i2c = SoftI2C(scl=Pin(22), sda=Pin(21)) 174 | sensor = HDC1080(i2c) 175 | 176 | # On-board LED reused for command toggling 177 | red_led = Pin(2, Pin.OUT) 178 | 179 | # Create and start BLE UART device named "ESP32" 180 | ble = BLE("ESP32") 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroPython ESP32 BLE Demo 2 | 3 | A lightweight MicroPython demo showing how to: 4 | 5 | - Expose a Nordic UART Service (NUS) over BLE 6 | - Control the onboard LED 7 | - Read temperature & humidity from an HDC1080 sensor 8 | 9 | Ideal for prototyping sensor-to-mobile IoT applications. 10 | 11 | --- 12 | 13 | ## 📋 Prerequisites 14 | 15 | - **Hardware** 16 | - Wemos ESP32 D1 Mini (or any ESP32 board) 17 | - HDC1080 temperature & humidity sensor 18 | - Optional: external red LED on GPIO 2 19 | 20 | - **Software** 21 | - MicroPython Firmware: `esp32-idf4-20210202-v1.14.bin` 22 | - Library HDC1080: [hdc1080.py](https://github.com/mcauser/micropython-hdc1080) 23 | - Tool to upload files: [ampy](https://github.com/scientifichackers/ampy) or the built-in WebREPL 24 | - Android: [Serial Bluetooth Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal) 25 | 26 | --- 27 | 28 | ## ⚙️ Installation 29 | 30 | 1. Flash MicroPython firmware onto your ESP32: 31 | ```bash 32 | esptool.py --port /dev/ttyUSB0 erase_flash 33 | esptool.py --port /dev/ttyUSB0 write_flash -z 0x1000 esp32-idf4-20210202-v1.14.bin 34 | ``` 35 | 36 | 2. Copy files to the board: 37 | 38 | ```bash 39 | ampy --port /dev/ttyUSB0 put main.py 40 | ampy --port /dev/ttyUSB0 put hdc1080.py 41 | ``` 42 | 43 | 3. Reset the ESP32. It will start advertising as **ESP32**. 44 | 45 | --- 46 | 47 | ## 🚀 Usage 48 | 49 | 1. Open your BLE terminal app and connect to **ESP32**. 50 | 51 | 2. Send one of these commands: 52 | 53 | | Command | Action | 54 | | ----------- | ------------------------------------------ | 55 | | `red_led` | Toggle the red LED (GPIO 2) | 56 | | `read_temp` | Read temperature (°C) & get a notification | 57 | | `read_hum` | Read humidity (%) & get a notification | 58 | 59 | 3. You’ll receive responses ending with `\n`. 60 | 61 | 4. Watch the onboard LED blink when disconnected, solid when connected. 62 | 63 | --- 64 | 65 | ## 🔍 Wiring 66 | 67 | ``` 68 | ESP32 D1 Mini HDC1080 Module 69 | --------------- ---------------- 70 | GPIO 21 (SDA) ───> SDA 71 | GPIO 22 (SCL) ───> SCL 72 | GND ───> GND 73 | 3V3 ───> VCC 74 | 75 | GPIO 2 (optional) ──(LED)─> GND 76 | ``` 77 | 78 | ## 📄 License 79 | 80 | This project is licensed under the MIT License. 81 | --------------------------------------------------------------------------------