Pico W PR

How to use Omron’s environmental sensor “2JCIE-BU” with RaspberryPi Pico W

記事内に商品プロモーションを含む場合があります

Introduction

This is an explanation of how to use OMRON’s USB type “2JCIE-BU” environmental sensor with RaspberryPi Pico W via Bluetooth. The language is MicroPython.

2JCIE-BU” is a USB type environmental sensor made by OMRON, and it is an amazing sensor that can collect various environmental information such as temperature, humidity, CO2 concentration, and so on.

This time, we received a question from Pico W about how to use “2JCIE-BU”, and we would like to explain it with an example of how to obtain temperature information.

  • This article has been machine translated.
  • Use all content of this article at your own risk.

日本語版は以下から切り替えできます。

Click below for the Japanese version.

Development Environments

Environments Version, etc Remarks
OS for development PC Windows 11 Can be used with Windows 10
Language MicroPython
Thonny 4.0.2
Board RaspberryPi Pico W

USB type environmental sensor “2JCIE-BU”

This USB type environmental sensor from OMRON can be used from PCs, smartphones, board computers, etc. via USB or Bluetooth connection.

Although it looks like a small USB memory, it can collect up to 10 types of data as described below and store up to 60,000 data points.

Environmental data obtainable by “2JCIE-BU”

The data that can be captured by “2JCIE-BU” is as follows. For more detailed specifications, please refer to Omron’s official website.

  • Temperature
  • Humidity
  • Illuminance
  • Barometric Pressure
  • Noise
  • 3-axis acceleration
  • eTVOC
  • Discomfort index
  • Heat stroke severity
  • Vibration information (earthquake frequency, vibration frequency, etc.)

Connecting Pico W and “2JCIE-BU”

Connect using Bluetooh (BLE). No wiring or circuitry is required.

Parts to be used

The components used in this article are as follows.

RaspberryPi Pico W

Raspberry Pi Pico W main unit.

¥1,581 (2024/06/24 11:50時点 | Amazon調べ)

Environmental Sensor 2JCIE-BU

The USB type environmental sensor mentioned above is made by OMRON. The price is a little high, but the overall cost is definitely lower than other sensors that require preparation and mounting of multiple sensors, as well as space for placement.

Program Overview

The outline of this programme is as follows.

Assuming that Pico W is the “Central” of the BLE and 2JCIE-BU is the “Peripheral”, the temperature information is obtained from the feature “0x5012” of the service “0x5010”, which can obtain the latest value (Latest Data).

System configuration diagram
  • Activate Pico W as a BLE central.
  • Scan 2JCIE as BLE peripheral
  • Get temperature information from Latest Data service (0x5010).
  • Display the temperature information on Pico W.

Execution Result

When the programme described below is run, the temperature information of 2JCIE is displayed on the Thonny shell as shown below.

exec result

The code that is based on

This code is based on the “BLE Central” code (picow_ble_temp_reader.py) in the official PicoW tutorial. Most parts of the code can be used as is in 2JCIE-BU, so you only need to modify (alter) a few parts such as UUIDs.

Note that the utility code “ble_advertising.py” used for the original is also required after the modification, so please obtain it from the following link and save it in Pico W.

PicoW Tutorial (PDF)

https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf

※ MicroPython Bluetooth is described in Chapter 6

All code

The overall code is as follows. The details are explained in the “Key Points of the Code” section below.

import bluetooth
import random
import struct
import time
import micropython
from ble_advertising import decode_services, decode_name
from micropython import const
from machine import Pin

import ubinascii

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_IRQ_GATTS_READ_REQUEST = const(4)
_IRQ_SCAN_RESULT = const(5)
_IRQ_SCAN_DONE = const(6)
_IRQ_PERIPHERAL_CONNECT = const(7)
_IRQ_PERIPHERAL_DISCONNECT = const(8)
_IRQ_GATTC_SERVICE_RESULT = const(9)
_IRQ_GATTC_SERVICE_DONE = const(10)
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
_IRQ_GATTC_READ_RESULT = const(15)
_IRQ_GATTC_READ_DONE = const(16)
_IRQ_GATTC_WRITE_DONE = const(17)
_IRQ_GATTC_NOTIFY = const(18)
_IRQ_GATTC_INDICATE = const(19)

_ADV_IND = const(0x00)
_ADV_DIRECT_IND = const(0x01)
_ADV_SCAN_IND = const(0x02)
_ADV_NONCONN_IND = const(0x03)

# Define UUID for "Latest Data Service (0x5010)" 
_ENV_SENSE_UUID = bluetooth.UUID('AB705010-0A3A-11E8-BA89-0ED5F89F718B')

# Defines the UUID of the "Latest sensing data (0x5012)" characteristic
_TEMP_UUID = bluetooth.UUID('AB705012-0A3A-11E8-BA89-0ED5F89F718B')
_TEMP_CHAR = (
    _TEMP_UUID,
    bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,
)
_ENV_SENSE_SERVICE = (
    _ENV_SENSE_UUID,
    (_TEMP_CHAR,),
)


class BLETemperatureCentral:

    # Initializes a member and activates an object in ble.
    def __init__(self, ble):
        self._ble = ble
        self._ble.active(True)
        self._ble.irq(self._irq)
        self._reset()
        self._led = Pin('LED', Pin.OUT)

    # Empty the member
    def _reset(self):
        
        self._name = None
        self._addr_type = None
        self._addr = None
        self._value = None
        
        self._scan_callback = None
        self._conn_callback = None
        self._read_callback = None
        self._notify_callback = None

        self._conn_handle = None
        self._start_handle = None
        self._end_handle = None
        self._value_handle = None

    #
    # Function called by the library when an event occurs
    #
    def _irq(self, event, data):
        
        #
        # Scan Results
        #
        if event == _IRQ_SCAN_RESULT:
            
            # Obtains various data from advertisements
            addr_type, addr, adv_type, rssi, adv_data = data
            
            if adv_type in (_ADV_IND, _ADV_DIRECT_IND):
                
                # Decodes the data content of advertisements
                type_list = decode_services(adv_data)
                               
                # Output data (for debugging)
                #print('type:{} addr:{} rssi:{} data:{}'.format(addr_type, ubinascii.hexlify(addr,':'), rssi, ubinascii.hexlify(adv_data)))
                
                # 
                # Use the MAC address to determine if the scan partner is 2JCIE-BU.
                # Address should be rewritten to match your device
                # 
                if ubinascii.hexlify(addr,':') == b"d8:39:2d:24:7e:80":
                
                # Checks if the scanned peripheral is the UUID of Pico W
                # if _ENV_SENSE_UUID in type_list:

                    # Get address type
                    self._addr_type = addr_type

                    #
                    # Get the Bluetooth address. Since data can change,
                    # Copy the entity (deep copy) is required.
                    #
                    self._addr = bytes(addr)
                    
                    # Get service name.
                    self._name = decode_name(adv_data) or "?"

                    # Stops scanning.
                    self._ble.gap_scan(None)

        #
        # Scan complete
        #
        elif event == _IRQ_SCAN_DONE:
            
            if self._scan_callback:
                
                # Bluetooth address is taken (not timed out)
                if self._addr:
                    self._scan_callback(self._addr_type, self._addr, self._name)
                    self._scan_callback = None
                
                # Timed out
                else:
                    self._scan_callback(None, None, None)

        #
        # Successful connection to peripheral
        #
        elif event == _IRQ_PERIPHERAL_CONNECT:
                       
            # get data
            conn_handle, addr_type, addr = data
            
            # Address type and MAC address are the same as 2JCIE-BU
            if addr_type == self._addr_type and addr == self._addr:

                # Get a connection handle and find the peripheral's service
                self._conn_handle = conn_handle
                self._ble.gattc_discover_services(self._conn_handle)

        #
        # When disconnected from a peripheral
        #
        elif event == _IRQ_PERIPHERAL_DISCONNECT:

            # If the disconnection was a connected periferal
            conn_handle, _, _ = data
            if conn_handle == self._conn_handle:
                # Reset the destination information held in member variables.
                self._reset()
                
        #
        # Acquire Periferal's services
        #
        elif event == _IRQ_GATTC_SERVICE_RESULT:
            
            # get data
            conn_handle, start_handle, end_handle, uuid = data
            
            # "2JCIE-DU"のサービスのUUIDと同じ場合
            if conn_handle == self._conn_handle and uuid == _ENV_SENSE_UUID:
                
                # Obtain start and end handles
                self._start_handle, self._end_handle = start_handle, end_handle

        #
        # Peripheral service acquisition completed.
        #
        elif event == _IRQ_GATTC_SERVICE_DONE:
            
            if self._start_handle and self._end_handle:
                
                # Obtain service characteristics
                self._ble.gattc_discover_characteristics(
                    self._conn_handle, self._start_handle, self._end_handle
                )
            else:
                print("Failed to find environmental sensing service.")

        #
        # Get the Characteristic of a peripheral
        #
        elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
            # Get data          
            conn_handle, def_handle, value_handle, properties, uuid = data
            
            # If the UUID of Characteristics is the same as the UUID of 2JCIE-DU.
            if conn_handle == self._conn_handle and uuid == _TEMP_UUID:
                
                # Get a handle to the ATT for the Value of the characteristic.
                self._value_handle = value_handle

        #
        # Completed getting the peripheral's Characteristic
        #
        elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
            
            # If I could get information on the characteristics of the peripherals,
            if self._value_handle:
                if self._conn_callback:
                    # Call a callback function.
                    self._conn_callback()
            else:
                print("Failed to find temperature characteristic.")

        #
        # Get Read to Characteristic
        # Event occurs after Read is executed from demo function.
        #
        elif event == _IRQ_GATTC_READ_RESULT:
            
            # Get Data
            conn_handle, value_handle, char_data = data

            # If the read source is 2JCIE-DU.            
            if conn_handle == self._conn_handle and value_handle == self._value_handle:
                
                # Convert, display, and set temperature information with acquired data.        
                #self._update_value(char_data)
                self._update_omron_temp_value(char_data)
                
                # Callback function is called if set.
                if self._read_callback:
                    self._read_callback(self._value)
                    self._read_callback = None

        # When READ is completed.
        elif event == _IRQ_GATTC_READ_DONE:
            
            # Get data
            conn_handle, value_handle, status = data

        # If Notify is sent from a peripheral.
        elif event == _IRQ_GATTC_NOTIFY:
                        
            # Get data
            conn_handle, value_handle, notify_data = data
            
            # As with Read, if it is from a connected peripheral,
            if conn_handle == self._conn_handle and value_handle == self._value_handle:
                
                # Convert and display values.
                self._update_value(notify_data)
                if self._notify_callback:
                    self._notify_callback(self._value)


    def is_connected(self):
        # If the connection handle to the peripheral and the handle to the ATT of the Value of the feature have already been obtained.
        # Determine that the connection to the peripheral has been made.
        return self._conn_handle is not None and self._value_handle is not None

    # Scanning for advertised peripherals
    def scan(self, callback=None):
        self._addr_type = None
        self._addr = None
        self._scan_callback = callback
        self._ble.gap_scan(2000, 30000, 30000)

    # Connect to the peripheral with the information obtained from the scan.
    def connect(self, addr_type=None, addr=None, callback=None):
        self._addr_type = addr_type or self._addr_type
        self._addr = addr or self._addr
        self._conn_callback = callback
        if self._addr_type is None or self._addr is None:
            return False
        self._ble.gap_connect(self._addr_type, self._addr)
        return True

    # Peripherals and disconnects.
    def disconnect(self):
        if not self._conn_handle:
            return
        self._ble.gap_disconnect(self._conn_handle)
        self._reset()

    # Do Read.
    def read(self, callback):
        if not self.is_connected():
            return
        self._read_callback = callback
        try:
            self._ble.gattc_read(self._conn_handle, self._value_handle)
        except OSError as error:
            print(error)

    # Set callback when notify is received (not used).
    def on_notify(self, callback):
        self._notify_callback = callback

    # Convert temperature information.
    def _update_value(self, data):
        try:
            self._value = struct.unpack("<h", data)[0] / 100
        except OSError as error:
            print(error)
            
            
    # Convert the temperature information of the "2JCIE-BU".
    def _update_omron_temp_value(self, data):
        
        # Output all data of the characteristic.
        # print( 'data:{}'.format(ubinascii.hexlify(data)) )
        
        # Convert data into an array of byte types.
        raw_data = bytes( data )
                     
        # Little-endian, so join in the order 2->1
        upper = int(data << 8 )
        lower = int(data )
        
        temp_raw = -100
        temp_raw = upper + lower
        temp     = temp_raw * 0.01
                
        self._value = temp
        
    # Function to get the member variable Value.
    def value(self):
        return self._value

# Function to flash the LEDs on the Pico body.
def sleep_ms_flash_led(self, flash_count, delay_ms):
    self._led.off()
    while(delay_ms > 0):
        for i in range(flash_count):            
            self._led.on()
            time.sleep_ms(100)
            self._led.off()
            time.sleep_ms(100)
            delay_ms -= 200
        time.sleep_ms(1000)
        delay_ms -= 1000

def print_temp(result):
    print("read temp: %.2f degc" % result)

# Functions that perform the main processing of the program.
def demo(ble, central):
    not_found = False

    #Function to be called on completion of scan.
    #Displays the name of the peripheral and makes the connection.
    def on_scan(addr_type, addr, name):
        if addr_type is not None:
            print("Found sensor: %s" % name)
            central.connect()
        else:
            nonlocal not_found
            not_found = True
            print("No sensor found.")
    
    # Peripheral scanning.
    central.scan(callback=on_scan)

    # Wait for the library to process the connection.
    while not central.is_connected():
        time.sleep_ms(100)
        if not_found:
            return

    print("Connected")
    time.sleep_ms(1000)

    # Read the value of the temperature of the peripheral for the duration of the connection.
    while central.is_connected():
        central.read(callback=print_temp)
        sleep_ms_flash_led(central, 2, 2000)

    print("Disconnected")

# Main Loop
if __name__ == "__main__":
    ble = bluetooth.BLE()
    central = BLETemperatureCentral(ble)
    while(True):
        demo(ble, central)
        sleep_ms_flash_led(central, 1, 10000)

Key Points of the Code

Definition of UUID

Defines the UUID (0x5010) of the service from which the latest values can be retrieved and the UUID (0x5012) characteristic that is included in that service and handles sensing data.

2JCIE-BU’s UUID is the same for all parts except XXXX, no matter which service or characteristic is used.

Therefore, please define the XXXX part below by applying a number such as “0x5010” described in the manual.

UUID 「AB70XXXX-0A3A-11E8-BA89-0ED5F89F718B」

# Define UUID for "Latest Data Service (0x5010)" 
_ENV_SENSE_UUID = bluetooth.UUID('AB705010-0A3A-11E8-BA89-0ED5F89F718B')

# Defines the UUID of the "Latest sensing data (0x5012)" characteristic
_TEMP_UUID = bluetooth.UUID('AB705012-0A3A-11E8-BA89-0ED5F89F718B')

Check by MAC address, not UUID

The code in the tutorial before modification used the UUID in the advertised data to confirm the scanned peer, but 2JCIE-BU does not include UUID in the advertised data. Therefore, in this program, the MAC address of 2JCIE-BU is used to confirm the other party.

Since the MAC address is different for each sensor unit, please check the MAC address of your 2JCIE-BU using an app for Bluetooth such as LightBlue, and rewrite the address in the code.

LightBlueで2JCIE-BUのMACアドレスを表示させた際の画像
Displaying MAC Addresses in LightBlue
        #
        # Scan Results
        #
        if event == _IRQ_SCAN_RESULT:
            
            # Obtains various data from advertisements
            addr_type, addr, adv_type, rssi, adv_data = data
            
            if adv_type in (_ADV_IND, _ADV_DIRECT_IND):
                
                # Decodes the data content of advertisements
                type_list = decode_services(adv_data)
                               
                # Output data (for debugging)
                #print('type:{} addr:{} rssi:{} data:{}'.format(addr_type, ubinascii.hexlify(addr,':'), rssi, ubinascii.hexlify(adv_data)))
                
                # 
                # Use the MAC address to determine if the scan partner is 2JCIE-BU.
                # Address should be rewritten to match your device
                # 
                if ubinascii.hexlify(addr,':') == b"d8:39:2d:24:7e:80":
                
                # Checks if the scanned peripheral is the UUID of Pico W
                # if _ENV_SENSE_UUID in type_list:

                    # Get address type
                    self._addr_type = addr_type

                    #
                    # Get the Bluetooth address. Since data can change,
                    # Copy the entity (deep copy) is required.
                    #
                    self._addr = bytes(addr)
                    
                    # Get service name.
                    self._name = decode_name(adv_data) or "?"

                    # Stops scanning.
                    self._ble.gap_scan(None)

Calculate temperature

The temperature calculation part is very different from the tutorial code, so it is created as a new function.

The measurement data sent by the characteristic (0x5012) takes 17 bytes, with the temperature information (starting from 0) being the 1st and -2nd bytes, 16-bit data with a sign.

Data composition of sensing data
byte 内容 フォーマット
0 Sequence number UInt8
1-2 Temperature Uint16
3-4 Relative humidity Uint16
5-6 Ambient light Uint16
7-10 Barometric pressure Uint32
11-12 Sound noise Uint16
13-14 eTVOC Uint16
15-16 eCO2 Uint16

※ Excerpt from the manual “2JCIE-BU”

So the “first byte” and the “second byte” are taken from the array and concatenated. As the data sequence is little-endian, the second byte is treated as the high bit in the concatenation.

After concatenation, multiply by [0.01 degrees], the unit of the data, to complete the temperature information.

 # Convert the temperature information of the "2JCIE-BU".
    def _update_omron_temp_value(self, data):
        
        # Output all data of the characteristic.
        # print( 'data:{}'.format(ubinascii.hexlify(data)) )
        
        # Convert data into an array of byte types.
        raw_data = bytes( data )
                     
        # Little-endian, so join in the order 2->1
        upper = int(data << 8 )
        lower = int(data )
        
        temp_raw = -100
        temp_raw = upper + lower
        temp     = temp_raw * 0.01
                
        self._value = temp
        

Adjust the reading interval to avoid errors.

It is not in the original code, but if used as is, an “[Errno 114] EALREADY” exception is thrown on the first read, so a sleep of 1 second is added.

# Wait for the library to process the connection.
    while not central.is_connected():
        time.sleep_ms(100)
        if not_found:
            return

    print("Connected")
    time.sleep_ms(1000)

    # Read the value of the temperature of the peripheral for the duration of the connection.
    while central.is_connected():
        central.read(callback=print_temp)
        sleep_ms_flash_led(central, 2, 2000)

Conclusion

This article explains how to use Omron’s USB-based environmental sensor “2JCIE-BU” with the RaspberryPi Pico W via Bluetooth.

The 2JCIE-BU is a very easy to use sensor as you can use the code from the Pico W tutorial and the information you need is well documented in the manual.

The price is a bit high, but if you want to get 10 different environmental readings with other sensors, you have to check the manuals for several sensors, wire them up, program them, etc. Considering the amount of work involved and the size of the circuit, I think it is a very good performing sensor.

If you want to get various environmental data quickly, please consider this sensor.

I would be happy to help. Thank you.

質問・要望 大歓迎です

「こんな解説記事作って」「こんなことがしたいけど、〇〇で困ってる」など、コメント欄で教えてください。 質問・要望に、中の人ができる限り対応します。

使えたよ・設定できたよの一言コメントも大歓迎。気軽に足跡を残してみてください。記事を紹介したい方はブログ、SNSにバシバシ貼ってもらってOKです。

ABOUT ME
えす
現役のソフトウェアエンジニアです。 C++ C# Python を使ってます。10年ちょい設計/開発部門にいましたが、今はQAエンジニアっぽいことをしています。

COMMENT

Your email address will not be published. Required fields are marked *

Index