Skip to content

Commit

Permalink
Merge pull request #85 from bstbud/fea_bsec2_with_webupdate
Browse files Browse the repository at this point in the history
add support for BSEC2 library features
  • Loading branch information
sebromero authored Oct 20, 2023
2 parents 18c4299 + 8c6d6a6 commit c6f2b0c
Show file tree
Hide file tree
Showing 16 changed files with 695 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* This sketch shows how the Nicla board could be used to scan / classify certian gases of interest
* using the on-board BME688 4-in-1 evnironmental sensor cluster
*
*/

#include "Arduino.h"
#include "Arduino_BHY2.h"

SensorBSEC2 bsec2(SENSOR_ID_BSEC2);

const uint8_t BSEC2CONFIG[] =
//With this example configuration, the BSEC2 library is able to classify 2 types of gases:
//gas 0: ambient regular air in a room
//gas 1: alcohol in a container
//note that the data collected for training the classifying algorithm was rather limited,
//thus the example config string for classifying might not work for your particular settings,
//for optimal results, please collect the data in your settings of interest and generate the config string using the AI studio accordingly
//generally speaking, more data under different scenarios yields better performance
{0,0,2,2,189,1,0,0,0,0,0,0,213,8,0,0,52,0,1,0,0,168,19,73,64,49,119,76,0,192,40,72,0,192,40,72,137,65,0,191,205,204,204,190,0,0,64,191,225,122,148,190,10,0,3,0,216,85,0,100,0,0,96,64,23,183,209,56,28,0,2,0,0,244,1,150,0,50,0,0,128,64,0,0,32,65,144,1,0,0,112,65,0,0,0,63,16,0,3,0,10,215,163,60,10,215,35,59,10,215,35,59,13,0,5,0,0,0,0,0,100,254,131,137,87,88,0,9,0,7,240,150,61,0,0,0,0,0,0,0,0,28,124,225,61,52,128,215,63,0,0,160,64,0,0,0,0,0,0,0,0,205,204,12,62,103,213,39,62,230,63,76,192,0,0,0,0,0,0,0,0,145,237,60,191,251,58,64,63,177,80,131,64,0,0,0,0,0,0,0,0,93,254,227,62,54,60,133,191,0,0,64,64,12,0,10,0,0,0,0,0,0,0,0,0,173,6,11,0,0,0,2,231,201,67,189,125,37,201,61,179,41,106,189,97,167,196,61,84,172,113,62,155,213,214,61,133,10,114,61,62,67,214,61,56,97,57,62,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,215,83,190,42,215,83,62,0,0,0,0,0,0,0,0,97,101,165,61,88,151,51,190,184,89,165,62,240,207,20,191,47,208,53,63,177,43,63,190,176,56,145,189,228,194,10,191,173,194,44,191,0,0,0,0,146,253,150,61,217,5,157,59,36,134,171,190,159,38,128,59,58,78,29,189,204,88,63,191,210,42,125,190,59,171,228,190,78,165,243,190,0,0,0,0,171,98,187,188,83,234,57,191,66,87,75,62,209,91,130,62,133,244,221,61,242,192,118,190,13,13,52,62,235,86,146,62,147,48,2,191,0,0,0,0,80,192,203,190,252,170,134,189,5,138,208,62,255,220,147,62,184,119,166,62,192,231,125,189,181,36,79,190,124,71,210,62,55,239,13,191,0,0,0,0,226,139,200,189,182,220,91,190,113,205,238,189,235,255,228,190,201,16,66,63,123,50,149,61,80,26,112,62,66,108,128,62,233,205,253,190,0,0,0,0,223,117,24,189,133,115,60,62,197,48,0,189,60,64,194,61,189,86,246,61,185,197,54,189,133,63,90,190,239,233,46,190,14,247,19,191,0,0,0,0,193,26,240,62,151,185,23,190,33,105,234,190,5,24,166,190,197,45,23,63,196,211,145,190,178,103,164,190,125,36,6,191,234,28,114,190,0,0,0,0,136,73,125,62,234,189,27,62,200,69,225,189,15,56,142,190,188,47,134,190,174,248,193,190,221,81,161,190,152,89,51,189,86,157,105,61,0,0,0,0,116,72,209,190,237,104,63,189,60,50,39,189,40,194,15,191,232,34,133,62,163,192,193,61,38,90,147,189,198,159,7,191,240,239,146,61,0,0,0,0,93,146,86,61,185,23,6,62,12,52,10,62,9,82,26,191,186,80,1,63,130,184,195,190,43,204,83,62,73,27,220,189,254,195,200,189,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,63,9,53,212,189,25,224,132,190,0,0,0,0,0,0,0,0,133,45,39,63,65,50,45,191,0,0,0,0,0,0,0,0,76,73,7,62,150,167,209,189,0,0,0,0,0,0,0,0,242,163,107,63,193,223,173,62,0,0,0,0,0,0,0,0,192,205,68,190,213,103,28,63,0,0,0,0,0,0,0,0,60,148,171,62,151,246,154,189,0,0,0,0,0,0,0,0,162,104,218,62,88,44,237,190,0,0,0,0,0,0,0,0,253,226,216,62,249,223,161,189,0,0,0,0,0,0,0,0,168,65,13,190,119,123,179,190,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,160,184,18,72,125,52,223,75,204,85,211,75,119,27,192,75,83,228,150,73,122,154,142,73,133,214,135,73,145,149,237,71,56,196,2,72,221,197,11,72,0,0,0,0,0,0,0,0,0,0,0,0,158,236,10,72,35,30,221,75,206,136,209,75,14,146,190,75,218,105,148,73,65,65,140,73,150,149,133,73,206,248,222,71,219,117,246,71,96,19,4,72,0,0,128,63,0,0,128,63,0,0,128,63,0,0,0,87,1,254,0,2,1,5,48,117,100,0,44,1,112,23,151,7,132,3,197,0,92,4,144,1,64,1,64,1,144,1,48,117,48,117,48,117,48,117,100,0,100,0,100,0,48,117,48,117,48,117,100,0,100,0,48,117,48,117,8,7,8,7,8,7,8,7,8,7,100,0,100,0,100,0,100,0,48,117,48,117,48,117,100,0,100,0,100,0,48,117,48,117,100,0,100,0,255,255,255,255,255,255,255,255,255,255,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,255,255,255,255,255,255,255,255,255,255,8,7,8,7,8,7,8,7,8,7,8,7,8,7,8,7,8,7,8,7,8,7,8,7,8,7,8,7,255,255,255,255,255,255,255,255,255,255,112,23,112,23,112,23,112,23,112,23,112,23,112,23,112,23,112,23,112,23,112,23,112,23,112,23,112,23,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,220,5,220,5,220,5,255,255,255,255,255,255,220,5,220,5,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,48,117,0,1,0,5,0,2,0,10,0,30,0,5,0,5,0,5,0,5,0,5,0,5,0,64,1,100,0,100,0,100,0,200,0,200,0,200,0,64,1,64,1,64,1,10,0,0,0,0,95,8,0,0
};

void setup()
{
Serial.begin(115200);
while(!Serial);

BHY2.begin();
sensortec.bhy2_bsec2_setConfigString(BSEC2CONFIG, sizeof(BSEC2CONFIG)/sizeof(BSEC2CONFIG[0]));
bsec2.begin();
}

void loop()
{
// Update function should be continuously polled
BHY2.update(100);

if (bsec2.getNewDataFlag()) {
bsec2.setNewDataFlag(false);

Serial.println(bsec2.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* This sketch is used for collecting raw data of BME688,
and the data log after conversion with the helper tools can be used in Bosch Sensortec's AI Studio to train an algorithm
and generate the corresponding config string for the BSEC2 library which can be later used for gas type classification/scanning
*/

#include "Arduino.h"
#include "Arduino_BHY2.h"

SensorBSEC2Collector bsec2Collector(SENSOR_ID_BSEC2_COLLECTOR);

#define CONFIG_BSEC2_USE_DEAULT_HP 1

#if CONFIG_BSEC2_USE_DEAULT_HP
// Default Heater temperature and time base(Recommendation)
const uint16_t BSEC2HP_TEMP[] = {320, 100, 100, 100, 200, 200, 200, 320, 320, 320}; // HP-354 /
const uint16_t BSEC2HP_DUR[] = {5, 2, 10, 30, 5, 5, 5, 5, 5, 5}; // the duration in steps of 140ms, 5 means 700ms, 2 means 280ms
#else
// customized Heater temperature and time base
const uint16_t BSEC2HP_TEMP[] = {100, 320, 320, 200, 200, 200, 320, 320, 320, 320}; // HP-321 /
const uint16_t BSEC2HP_DUR[] = {43, 2, 2, 2, 21, 21, 2, 14, 14, 14}; // the duration in steps of 140ms, 5 means 700ms, 2 means 280ms
#endif

void setup()
{
Serial.begin(115200);
while(!Serial);

BHY2.begin();
sensortec.bhy2_bsec2_setHP((uint8_t*)BSEC2HP_TEMP, sizeof(BSEC2HP_TEMP), (uint8_t*)BSEC2HP_DUR, sizeof(BSEC2HP_DUR));

bsec2Collector.begin();
}

void loop()
{
static auto last_index = 0;

// Update function should be continuously polled
BHY2.update();

if (last_index != bsec2Collector.gas_index()) {
last_index = bsec2Collector.gas_index();
Serial.println(String((uint32_t)bsec2Collector.timestamp()) + " "
+ String(bsec2Collector.temperature()) + " "
+ String(bsec2Collector.pressure()) + " "
+ String(bsec2Collector.humidity()) + " "
+ String(bsec2Collector.gas()) + " "
+ String(bsec2Collector.gas_index())
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
from datetime import timezone
import datetime
import sys
import os
import json

# =~=~=~=~=~=~=~=~=~=~=~= PuTTY log 2022.10.21 08:32:17 =~=~=~=~=~=~=~=~=~=~=~=
# 245166 31.27 102027.92 35.10 2049537.13 5

# this script converts the data log generated by the Arduino_BHY2/BSEC2GasScanningCollectData.ino example sketch
# to data format acceptable by the BME AI Studio for the training of AI models

class BSEC2DataLogConverter():
# sometimes you may want to add an offset to the converted data in situations such as reset of the board, this way
# the converted data contains timestamp that will succeed eariler log files,
# otherwise, there will be overlap of timestamp ranges among the converted data which might confuse the AI studio software
def __init__(self, log_file_name, gas_label, bmeconfig_file, timestamp_offset_ms = 0, dbg=False):
self.log_file_name = log_file_name
self.log_file = open(self.log_file_name, 'r')
self.lines = self.log_file.readlines()
self.log_time_start = None
self.log_timestamp_start = 0
self.rawdata = []
self.gas_label = int(gas_label)
self.bmeconfig_file = bmeconfig_file
self.timestamp_offset_ms = timestamp_offset_ms
self.dbg = dbg
if len(self.lines) < 3:
sys.exit("Err: There's too little data in log")
self._clean_data()
self.check_gas_index_missing()

def _clean_data(self):
# Putty's default log has header which contains the time when the log was created
if "PuTTY" in self.lines[0]:
# 2022.10.21 07:31:17
self.log_time_start = self.lines[0].split(" ")[3] + " " + self.lines[0].split(" ")[4]

lines_clean = []
for line in self.lines[1:]:
if len(line.split(' ')) == 6:
lines_clean.append(line)
#print('line:', '"', line, '"', sep='')
self.lines = lines_clean

def toString(self):
print(self.lines)

def check_gas_index_missing(self):
last_index = int(self.lines[0].split(' ')[-1])
#for i, line in enumerate(self.lines[1:]):
for i, line in enumerate(self.lines[1:-1]):
if self.dbg:
print('dbg:', i, ':"', line, '"', sep='')
cur_index = int(line[-2:-1])
# print(cur_index)
if (cur_index - last_index != 1) and (cur_index - last_index != -9):
print("Missing gas index at line {}, {} {}".format(i+1, cur_index, last_index))
last_index = cur_index

def _get_timestamp_start(self, line):
infos = line.split(' ')
timestamp_ms_start = int(infos[0])
return timestamp_ms_start

def unpack_data(self, line):
infos = line.split(' ')
timestamp_ms = int(infos[0]) + self.timestamp_offset_ms
temp = float(infos[1])
pres = float(infos[2])
hum = float(infos[3])
gas = float(infos[4])
gas_index = int(infos[5])
return (timestamp_ms, temp, pres, hum, gas, gas_index)

def format_data(self, Ttphg, time_start_s, label, sensor_index=0, sensor_id=1730555495, scanning_mode_ena=1, error_code=0):
'''
Ttphg: timestamp_ms, temp, pres, hum, gas, gas_index
'''
return (sensor_index, sensor_id, Ttphg[0], time_start_s + Ttphg[0]//1000 - self.log_timestamp_start // 1000,
Ttphg[1], Ttphg[2], Ttphg[3], Ttphg[4], Ttphg[5], scanning_mode_ena, label, error_code)

def parse(self):
if self.log_time_start:
dt = datetime.datetime.strptime(self.log_time_start[2:], "%y.%m.%d %H:%M:%S")
utc_timestamp = int(dt.timestamp())
else:
dt = datetime.datetime.now(timezone.utc)
utc_time = dt.replace(tzinfo=timezone.utc)
utc_timestamp = int(utc_time.timestamp())

self.log_timestamp_start = self._get_timestamp_start(self.lines[0])
for line in self.lines:
# print(line)
data_unformated = self.unpack_data(line)
data_formated = self.format_data(data_unformated, utc_timestamp, self.gas_label)
# print(data_formated)
self.rawdata.append(data_formated)


def _modify_incompatible_keys(self, cfg):
cfg["configHeader"]["dateCreated"] = ""
return cfg

def _add_raw_data_header(self, cfg):
rawDataHeader = {
"counterPowerOnOff": 1,
"seedPowerOnOff": "",
"counterFileLimit": 1,
"dateCreated": "",
"dateCreated_ISO": "",
"firmwareVersion": "0",
"boardId": "1730555495"
}
cfg["rawDataHeader"] = rawDataHeader
return cfg

def _add_raw_data_body(self, cfg, data_list):
dataColumns = [
{
"name": "Sensor Index",
"unit": "",
"format": "integer",
"key": "sensor_index"
},
{
"name": "Sensor ID",
"unit": "",
"format": "integer",
"key": "sensor_id"
},
{
"name": "Time Since PowerOn",
"unit": "Milliseconds",
"format": "integer",
"key": "timestamp_since_poweron"
},
{
"name": "Real time clock",
"unit": "Unix Timestamp: seconds since Jan 01 1970. (UTC); 0 = missing",
"format": "integer",
"key": "real_time_clock"
},
{
"name": "Temperature",
"unit": "DegreesCelcius",
"format": "float",
"key": "temperature"
},
{
"name": "Pressure",
"unit": "Hectopascals",
"format": "float",
"key": "pressure"
},
{
"name": "Relative Humidity",
"unit": "Percent",
"format": "float",
"key": "relative_humidity"
},
{
"name": "Resistance Gassensor",
"unit": "Ohms",
"format": "float",
"key": "resistance_gassensor"
},
{
"name": "Heater Profile Step Index",
"unit": "",
"format": "integer",
"key": "heater_profile_step_index"
},
{
"name": "Scanning Mode Enabled",
"unit": "",
"format": "integer",
"key": "scanning_mode_enabled"
},
{
"name": "Label Tag",
"unit": "",
"format": "integer",
"key": "label_tag"
},
{
"name": "Error Code",
"unit": "",
"format": "integer",
"key": "error_code"
}
]
rawDataBody = {
"dataColumns": dataColumns,
"dataBlock": data_list
}

cfg["rawDataBody"]= rawDataBody

return cfg

def gen_ai_studio_training_data(self):
bme_cfg = json.loads(open(self.bmeconfig_file, "r").read())
bme_cfg = self._modify_incompatible_keys(bme_cfg)
bme_cfg = self._add_raw_data_header(bme_cfg)
bme_cfg = self._add_raw_data_body(bme_cfg, self.rawdata)
cfg_str = json.dumps(bme_cfg, indent=4)
#filename = self.bmeconfig_file.split(".")[0] + "_gas_" + str(self.gas_label) + ".bmerawdata"
file_name_base = os.path.basename(self.log_file_name)
path_folder = os.path.dirname(self.log_file_name)
filename_wo_ext = os.path.splitext(file_name_base)[0]

filename = path_folder + '/' + "_gas_" + str(self.gas_label) + '_' + filename_wo_ext + ".bmerawdata"
if self.dbg:
print("dbg: path_folder:", path_folder)
print("dbg: filename_wo_ext:", filename_wo_ext)
print("dbg: filename:", filename)

bmerawdata_file = open(filename, "w")
bmerawdata_file.write(cfg_str)
print('bmedata: ', '"'+filename+'"', 'generated')


if __name__ == '__main__':
"""
Make sure to change gas_label if you collect multiple gas, choose from 0,1,2,3.
"""
if (len(sys.argv) < 4):
print("usage:")
print("\tpython", sys.argv[0], "LOG_FILENAME GAS_LABEL_NUMBER BME_CONFIG_FILE")
print("\t\t GAS_CLASS_NUMBER is a number between 0-3 which is mapped to class A-D in the algorithm settings of BME AI Studio")
print("\t\t BME_CONFIG_FILE is a board config file generated by BME AI Studio Software")
print("example:")
print("\tpython", sys.argv[0], "./datalog/session3-room2/ambient-air.log", "0", "./NiclaSenseME_BoardConfiguration.bmeconfig")
raise BaseException("missing argument")
else:
parser = BSEC2DataLogConverter(log_file_name = sys.argv[1], gas_label = sys.argv[2], bmeconfig_file = sys.argv[3])

parser.parse()
"""
Pass the path of the bmeconfig file generated by the BME AI Studio,
and this script will generate 2022_10_21_15_13_BoardConfiguration{gas_label}.bmerawdata for you
"""
parser.gen_ai_studio_training_data()

1 change: 1 addition & 0 deletions Arduino_BHY2/src/Arduino_BHY2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ bool Arduino_BHY2::begin(NiclaConfig config, NiclaWiring niclaConnection)
//in this case, we want to start BLEHandler and DFUManager
//so they could come to the rescue the failed firmware for BHI260AP

sensortec.bsecSetBoardTempOffset(0.5f);//assuming the device is powered by USB, if on battery only, use a negative value such as -3.0

if (!(_niclaConfig & NICLA_STANDALONE)) {
if (_niclaConfig & NICLA_BLE) {
Expand Down
2 changes: 2 additions & 0 deletions Arduino_BHY2/src/Arduino_BHY2.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include "sensors/SensorXYZ.h"
#include "sensors/SensorQuaternion.h"
#include "sensors/SensorBSEC.h"
#include "sensors/SensorBSEC2.h"
#include "sensors/SensorBSEC2Collector.h"
#include "sensors/SensorActivity.h"
#include "sensors/Sensor.h"

Expand Down
Loading

0 comments on commit c6f2b0c

Please sign in to comment.