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.
~ この記事の内容 / Contents ~
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.
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).
- 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.
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.
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.
#
# 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です。