diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47aa899..be69f57 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,17 @@ For the detailed description, please explore nested folders and corresponding CH
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.2.3] - 2026-04-02
+
+### Added
+
+- Added optional accelerometer streaming on the nRF52, enabled only when requested by the user configuration.
+
+### Fixed
+
+### Changed
+
+
## [1.2.2] - 2025-03-23
### Added
diff --git a/README.md b/README.md
index ed75403..5fb2a09 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# WULPUS v1.2.2
+# WULPUS v1.2.3
## Wearable Ultra Low-Power Ultrasound
# Introduction
diff --git a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/CHANGELOG.md b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/CHANGELOG.md
index fc6a774..d5f6cac 100644
--- a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/CHANGELOG.md
+++ b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/CHANGELOG.md
@@ -5,7 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [Unreleased]
+## [1.2.3] - 2026-04-02
+
+### Added
+
+- Optional accelerometer streaming on the nRF52, enabled only when requested by the user configuration.
+- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.c`: Added IIS2DH accelerometer driver, runtime enable/disable handling, and ACC/non-ACC frame finalization.
+- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.h`: Added IIS2DH register definitions, types, and public driver API.
+
+### Changed
+
+- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/main.c`: Added runtime accelerometer state handling and deferred mode application from the main loop.
+- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_ble.c`: Added config-packet parsing for user-requested accelerometer streaming and deferred update signaling.
+- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_spi.c`: Moved frame completion responsibility to the ACC/non-ACC post-processing path.
+- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/pca10040/s132/ses/US_probe_nRF52_firmware.emProject`: Added IIS2DH source files and TWI driver sources to the SES project.
+
## [1.1.0] - 2024-02-21
diff --git a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.c b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.c
new file mode 100644
index 0000000..9bf6869
--- /dev/null
+++ b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.c
@@ -0,0 +1,314 @@
+#include "iis2dh.h"
+#include "us_spi.h"
+#include "us_defines.h"
+
+
+/* TWI instance */
+static const nrf_drv_twi_t IIS2DH_twi = NRF_DRV_TWI_INSTANCE(TWI_INSTANCE_ID);
+
+/* Flag to know when a I2C transfer has been completed */
+static volatile bool IIS2DH_xfer_done = false;
+
+extern volatile bool flag_add_IMU;
+
+extern int buffer_content;
+extern int buffer_counter;
+extern int BLE_packet_ready;
+extern ArrayList_type m_rx_buf[NUMBER_OF_XFERS*MAX_BUFFER_NUMBER_OF_US_FRAMES];
+
+uint8_t IIS2DH_buffer[805] = {};
+uint16_t IIS2DH_buffer_index =0;
+/* Frame counter for IIS2DH over bluetooth */
+uint16_t IIS2DH_frame_number =0;
+
+
+//Event Handler
+static void twi_handler(nrf_drv_twi_evt_t const * p_event, void * p_context)
+{
+ //Check the event to see what type of event occurred
+ switch (p_event->type)
+ {
+ //If data transmission or receiving is finished
+ case NRF_DRV_TWI_EVT_DONE:
+ IIS2DH_xfer_done = true;//Set the flag
+ break;
+
+ default:
+ // do nothing
+ break;
+ }
+}
+
+
+/**
+ * @brief TWI initialization function for the MCP9808 sensor
+ *
+ */
+static void iis2dh_twi_init (void)
+{
+ ret_code_t err_code;
+
+ const nrf_drv_twi_config_t twi_config = {
+ .scl = PIN_IIS2DH_SCL,
+ .sda = PIN_IIS2DH_SDA,
+ .frequency = NRF_DRV_TWI_FREQ_100K,
+ .interrupt_priority = APP_IRQ_PRIORITY_LOWEST,
+ .clear_bus_init = false
+ };
+
+ err_code = nrf_drv_twi_init(&IIS2DH_twi, &twi_config, twi_handler, NULL);
+ APP_ERROR_CHECK(err_code);
+
+ nrf_drv_twi_enable(&IIS2DH_twi);
+}
+
+void IIS2DH_init()
+{
+ iis2dh_twi_init();
+ IIS2DH_buffer[0] = 0xFF;
+ IIS2DH_buffer[1] = 128;
+ //((uint16_t*)IIS2DH_buffer)[1] = IIS2DH_frame_number++;
+ IIS2DH_buffer[3] = 0;
+ IIS2DH_buffer[2] = IIS2DH_frame_number++;
+
+ IIS2DH_buffer_index = 4;
+ nrf_delay_ms(500);
+}
+
+bool IIS2DH_register_read(uint8_t register_address, uint8_t * rx_buffer, uint8_t number_of_bytes)
+{
+ ret_code_t err_code;
+
+ //Set the flag to false to show the receiving is not yet completed
+ IIS2DH_xfer_done = false;
+
+ // Send the register address where we want to read the data from
+ err_code = nrf_drv_twi_tx(&IIS2DH_twi, IIS2DH_ADDRESS, ®ister_address, 1, true);
+
+ //Wait for the transmission to be completed
+ while (IIS2DH_xfer_done == false){}
+
+ // If transmission was not successful, exit the function and return false
+ if (NRF_SUCCESS != err_code)
+ {
+ return false;
+ }
+
+ //reset the flag so that we can read data from the IIS2DH's internal register
+ IIS2DH_xfer_done = false;
+
+ // Receive the data from the IIS2DH
+ err_code = nrf_drv_twi_rx(&IIS2DH_twi, IIS2DH_ADDRESS, rx_buffer, number_of_bytes);
+ //wait until the transmission is completed
+ while (IIS2DH_xfer_done == false){}
+
+ // if data was successfully read, return true else return false
+ if (NRF_SUCCESS != err_code)
+ {
+ return false;
+ }
+
+ return true;
+}
+bool IIS2DH_register_write(uint8_t register_address, uint8_t * wx_buffer, uint8_t number_of_bytes)
+{
+ ret_code_t err_code;
+
+ //Set the flag to false to show the receiving is not yet completed
+ IIS2DH_xfer_done = false;
+
+ uint8_t wxbuffer_with_address[10];
+ wxbuffer_with_address[0] = register_address;
+ if (number_of_bytes>8) return false;
+ for (int i = 0; i>8;
+ }else if (mode == IIS2DH_NormalMode){
+ res = res>>6;
+ }else if (mode == IIS2DH_HighResolutionMode){
+ res = res>>4;
+ }
+ return res;
+
+}
+
+bool getAccelerationData(uint16_t* X, uint16_t* Y, uint16_t* Z, IIS2DH_OperatingModes mode, IIS2DH_FullScale range){
+ *X = getHighandLow(IIS2DH_REG_OUT_X_L, IIS2DH_REG_OUT_X_H, mode);
+ *Y = getHighandLow(IIS2DH_REG_OUT_Y_L, IIS2DH_REG_OUT_Y_H, mode);
+ *Z = getHighandLow(IIS2DH_REG_OUT_Z_L, IIS2DH_REG_OUT_Z_H, mode);
+ //*X = convert2mg(X_in, range);
+ //*Y = convert2mg(Y_in, range);
+ //*Z = convert2mg(Z_in, range);
+
+
+ return true;
+
+}
+
+static void finalizeCurrentFrame(void)
+{
+ buffer_counter++;
+ if (buffer_counter == MAX_BUFFER_NUMBER_OF_US_FRAMES)
+ {
+ buffer_counter = 0;
+ }
+
+ BLE_packet_ready = 1;
+}
+
+bool IIS2DH_set_streaming_enabled(bool enable)
+{
+ if (enable)
+ {
+ return setupAccelormeter(IIS2DH_HighResolutionMode,
+ AllModes_400Hz,
+ IIS2DH_Precision_2g);
+ }
+ else
+ {
+ // Power-down mode: CTRL_REG1 = 0
+ uint8_t ctrl_reg1 = 0x00;
+ return IIS2DH_register_write(IIS2DH_REG_CTRL_REG1, &ctrl_reg1, 1);
+ }
+}
+
+void finalizeFrameWithoutIMU(void)
+{
+ while(flag_add_IMU == false) {}
+
+ flag_add_IMU = false;
+
+ // Zero out the 6 bytes that would normally carry accel data
+ uint8_t *dst = &m_rx_buf[3 + buffer_counter * NUMBER_OF_XFERS].buffer[0] + 202 - 6;
+ dst[0] = 0;
+ dst[1] = 0;
+ dst[2] = 0;
+ dst[3] = 0;
+ dst[4] = 0;
+ dst[5] = 0;
+
+ finalizeCurrentFrame();
+}
+
+void getIIS2DHData2Buffer(){
+
+ IIS2DH_buffer_index = 0;
+ IIS2DH_register_read(IIS2DH_REG_OUT_X_H, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
+ IIS2DH_register_read(IIS2DH_REG_OUT_X_L, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
+ IIS2DH_register_read(IIS2DH_REG_OUT_Y_H, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
+ IIS2DH_register_read(IIS2DH_REG_OUT_Y_L, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
+ IIS2DH_register_read(IIS2DH_REG_OUT_Z_H, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
+ IIS2DH_register_read(IIS2DH_REG_OUT_Z_L, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
+ //NRF_LOG_INFO("Received: %u", IIS2DH_buffer_index);
+
+ while(flag_add_IMU == false) {}
+
+ flag_add_IMU = false;
+
+ memcpy(&m_rx_buf[3 + buffer_counter * NUMBER_OF_XFERS].buffer[0] + 202 - 6,
+ &IIS2DH_buffer[0],
+ 6);
+
+ finalizeCurrentFrame();
+}
diff --git a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.h b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.h
new file mode 100644
index 0000000..8142722
--- /dev/null
+++ b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.h
@@ -0,0 +1,127 @@
+#ifndef IIS2DH_Breakout
+#define IIS2DH_Breakout
+
+#include
+#include "boards.h"
+#include "app_util_platform.h"
+#include "app_error.h"
+#include "nrf_drv_twi.h"
+#include "nrf_delay.h"
+#include "us_ble.h"
+
+
+#include "nrf_log_backend_rtt.h"
+#include "nrf_log.h"
+#include "nrf_log_default_backends.h"
+#include "nrf_log_ctrl.h"
+
+#define IIS2DH_REG_WHOAMI 0x0F
+#define IIS2DH_REG_OUT_X_L 0x28
+#define IIS2DH_REG_OUT_X_H 0x29
+#define IIS2DH_REG_OUT_Y_L 0x2A
+#define IIS2DH_REG_OUT_Y_H 0x2B
+#define IIS2DH_REG_OUT_Z_L 0x2C
+#define IIS2DH_REG_OUT_Z_H 0x2D
+
+#define IIS2DH_REG_CTRL_REG1 0x20
+#define IIS2DH_REG_CTRL_REG2 0x21
+#define IIS2DH_REG_CTRL_REG3 0x22
+#define IIS2DH_REG_CTRL_REG4 0x23
+#define IIS2DH_REG_CTRL_REG5 0x24
+#define IIS2DH_REG_CTRL_REG6 0x25
+
+#define IIS2DH_REG_STATUS_REG_AUX 0x07
+#define IIS2DH_REG_OUT_TEMP_L 0x0C
+#define IIS2DH_REG_OUT_TEMP_H 0x0D
+
+#define IIS2DH_REG_INT_COUNTER_REG
+#define IIS2DH_REG_TEMP_CFG_REG 0x1F
+#define IIS2DH_REG_REFERENCE
+#define IIS2DH_REG_STATUS_REG
+
+
+//
+#define IIS2DH_ADDRESS 0x18
+
+
+#define PIN_IIS2DH_SCL 31
+#define PIN_IIS2DH_SDA 30
+
+#define PIN_IIS2DH_INT1 12
+#define PIN_IIS2DH_INT2 13
+
+// TWI instance ID
+#if TWI0_ENABLED
+#define TWI_INSTANCE_ID 0
+#elif TWI1_ENABLED
+#define TWI_INSTANCE_ID 1
+#endif
+
+
+extern uint8_t IIS2DH_buffer[805];
+extern uint16_t IIS2DH_buffer_index;
+/* Frame counter for IIS2DH over bluetooth */
+extern uint16_t IIS2DH_frame_number;
+
+
+typedef enum {
+ IIS2DH_LowPowerMode = 0,
+ IIS2DH_NormalMode = 1,
+ IIS2DH_HighResolutionMode = 2
+} IIS2DH_OperatingModes;
+
+typedef enum {
+ AllModes_1Hz = 0x10,
+ AllModes_10Hz = 0x20,
+ AllModes_25Hz = 0x30,
+ AllModes_50Hz = 0x40,
+ AllModes_100Hz = 0x50,
+ AllModes_200Hz = 0x60,
+ AllModes_400Hz = 0x70,
+ LowPower_1620Hz = 0x80,
+ LowPower_5376Hz = 0x90,
+ HighResolution_1344Hz = 0x90,
+ Normal_1344Hz = 0x90
+} IIS2DH_DataRate;
+
+typedef enum {
+ IIS2DH_Precision_2g = 0x00,
+ IIS2DH_Precision_4g = 0x10,
+ IIS2DH_Precision_8g = 0x20,
+ IIS2DH_Precision_16g = 0x30,
+}IIS2DH_FullScale;
+
+/**
+ * @brief Function for initializing the GPIO and TWI peripheral for the IIS2DH sensor
+ *
+ */
+void IIS2DH_init();
+
+/**
+ * @brief Function for reading a register from the IIS2DH sensor
+ *
+ * @param[in] register_address The address of the register to read from
+ * @param[out] rx_buffer The buffer to store the received data
+ * @param[in] number_of_bytes The number of bytes to read
+ *
+ * @retval true If the read was successful
+ * @retval false If the read was not successful
+ *
+ */
+bool IIS2DH_register_read(uint8_t register_address, uint8_t * rx_buffer, uint8_t number_of_bytes);
+
+bool IIS2DH_register_write(uint8_t register_address, uint8_t * wx_buffer, uint8_t number_of_bytes);
+
+bool setupTemp();
+int8_t getTemp();
+
+bool setupAccelormeter(IIS2DH_OperatingModes mode, IIS2DH_DataRate rate, IIS2DH_FullScale fs);
+bool getAccelerationData(uint16_t* X, uint16_t* Y, uint16_t* Z, IIS2DH_OperatingModes mode, IIS2DH_FullScale range);
+
+bool IIS2DH_set_streaming_enabled(bool enable);
+void finalizeFrameWithoutIMU(void);
+
+void getIIS2DHData2Buffer();
+#endif
+
+
diff --git a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/main.c b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/main.c
index 84d9b46..2918c2e 100644
--- a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/main.c
+++ b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/main.c
@@ -52,6 +52,8 @@
#include "us_ble.h"
#include "us_defines.h"
+#include "iis2dh.h"
+
// Buffers to store US data
ArrayList_type m_rx_buf[NUMBER_OF_XFERS*MAX_BUFFER_NUMBER_OF_US_FRAMES] = {0};
@@ -69,6 +71,14 @@ volatile bool ble_connected = false;
// A flag to signal that MSP config is received
volatile bool msp_conf_received = false;
+volatile bool do_act_reading=false;
+
+
+// Handle accelerometer as requested by GUI/User (see us_ble.c)
+volatile bool accel_stream_enabled = false;
+volatile bool accel_stream_requested = false;
+volatile bool accel_stream_update_pending = false;
+
/**@brief Function for initializing the timer module.
*/
static void timers_init(void)
@@ -132,6 +142,7 @@ void in_pin_handler(nrf_drv_gpiote_pin_t pin, nrf_gpiote_polarity_t action)
// Enable timer and counter to start the four SPI transactions
nrf_drv_timer_enable(&timer_timer);
nrf_drv_timer_enable(&timer_counter);
+ do_act_reading =true;
}
}
@@ -167,6 +178,19 @@ static void gpio_init(void)
}
+
+static void apply_accel_mode_if_pending(void)
+{
+ if (!accel_stream_update_pending)
+ return;
+
+ accel_stream_enabled = accel_stream_requested;
+ IIS2DH_set_streaming_enabled(accel_stream_enabled);
+ accel_stream_update_pending = false;
+}
+
+
+
/**@brief Application main function.
*/
int main(void)
@@ -178,11 +202,13 @@ int main(void)
us_ble_init();
gpio_init();
us_spi_init();
+ IIS2DH_init();
// Tell MSP430 that the BLE connection is not ready yet
nrf_drv_gpiote_out_clear(PIN_BLE_CONN_READY);
+
// Start BLE advertising
advertising_start();
@@ -197,16 +223,35 @@ int main(void)
while(msp_conf_received==false)
{
- // Wait for MSP430 config to be received from host PC
+ idle_state_handle();
}
- //msp_conf_received = false;
+ msp_conf_received = false;
+
+ apply_accel_mode_if_pending();
// Now the BLE connection is ready to send US data
nrf_drv_gpiote_out_set(PIN_BLE_CONN_READY);
// Enter main loop.
while(1)
- {
+ {
+ // Enable/disable accelerometer as requested by user-config
+ apply_accel_mode_if_pending();
+
+ if (do_act_reading)
+ {
+ if (accel_stream_enabled)
+ {
+ getIIS2DHData2Buffer();
+ }
+ else
+ {
+ finalizeFrameWithoutIMU();
+ }
+
+ do_act_reading = false;
+ }
+
send_pending_frames();
idle_state_handle();
}
diff --git a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/pca10040/s132/ses/US_probe_nRF52_firmware.emProject b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/pca10040/s132/ses/US_probe_nRF52_firmware.emProject
index 3199725..2506b6f 100644
--- a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/pca10040/s132/ses/US_probe_nRF52_firmware.emProject
+++ b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/pca10040/s132/ses/US_probe_nRF52_firmware.emProject
@@ -99,6 +99,11 @@
+
+
+
+
+
@@ -112,6 +117,8 @@
+
+
diff --git a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_ble.c b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_ble.c
index 49bf1ed..cc5e639 100644
--- a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_ble.c
+++ b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_ble.c
@@ -73,6 +73,7 @@
#include "nrf_delay.h"
#include "us_ble.h"
#include "us_defines.h"
+#include "iis2dh.h"
#define APP_BLE_CONN_CFG_TAG 1 /**< A tag identifying the SoftDevice BLE configuration. */
@@ -102,6 +103,11 @@ extern volatile bool ble_connected;
extern volatile bool msp_conf_received;
extern int BLE_packet_ready;
+extern volatile bool accel_stream_enabled;
+extern volatile bool accel_stream_requested;
+extern volatile bool accel_stream_update_pending;
+
+
void sleep_mode_enter(void);
BLE_NUS_DEF(m_nus, NRF_SDH_BLE_TOTAL_LINK_COUNT); /**< BLE NUS service instance. */
@@ -167,6 +173,8 @@ static void gap_params_init(void)
}
+
+
/**@brief Function for handling Queued Write Module errors.
*
* @details A pointer to this function will be passed to each service which may need to inform the
@@ -179,6 +187,18 @@ static void nrf_qwr_error_handler(uint32_t nrf_error)
APP_ERROR_HANDLER(nrf_error);
}
+
+// Helper to decode little-endian uint32_t
+static uint32_t read_u32_le(const uint8_t *p)
+{
+ return ((uint32_t)p[0]) |
+ ((uint32_t)p[1] << 8) |
+ ((uint32_t)p[2] << 16) |
+ ((uint32_t)p[3] << 24);
+}
+
+
+
/**@brief Function for handling the data from the Nordic UART Service.
*
* @details This function will process the data received from the Nordic UART BLE Service and send
@@ -189,11 +209,23 @@ static void nrf_qwr_error_handler(uint32_t nrf_error)
/**@snippet [Handling the data received over BLE] */
static void nus_data_handler(ble_nus_evt_t * p_evt)
{
-
if (p_evt->type == BLE_NUS_EVT_RX_DATA)
{
- // Copy received command from python to the SPI transmit buffer
- memcpy(m_tx_buf_1, p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);
+ const uint8_t *rx = p_evt->params.rx_data.p_data;
+ uint16_t len = p_evt->params.rx_data.length;
+
+ // Only decode config packets: start byte 0xFA, transFreq at bytes 5..8
+ if ((len >= 9) && (rx[0] == 0xFA))
+ {
+ uint32_t transFreq = read_u32_le(&rx[5]);
+
+ // If transFreq contains code 101, enable accelerometer
+ accel_stream_requested = (transFreq == 101u);
+ accel_stream_update_pending = true;
+ }
+
+ // Forward packet unchanged to MSP430
+ memcpy(m_tx_buf_1, rx, len);
msp_conf_received = true;
// Clear the BLE buffers to send US data with the received configuration
diff --git a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_spi.c b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_spi.c
index 876fe88..90b0272 100644
--- a/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_spi.c
+++ b/fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_spi.c
@@ -60,6 +60,8 @@ extern volatile bool ble_connected;
extern volatile bool msp_conf_received;
+volatile bool flag_add_IMU;
+
extern int buffer_content;
extern int buffer_counter;
@@ -173,11 +175,9 @@ void counter_cc0_event_handler(nrf_timer_event_t event_type, void* p_context)
//APP_ERROR_CHECK(1);
}
- buffer_counter++;
- if(buffer_counter == MAX_BUFFER_NUMBER_OF_US_FRAMES)
- buffer_counter = 0;
-
- BLE_packet_ready = 1;
+ // Buffer ready to add IMU data
+ flag_add_IMU = true;
+
//msp_conf_received = false;
// Relay the received SPI data to the BLE dongle