/* Metriful_sensor.cpp This file defines functions which are used in the code examples. Copyright 2020 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ #include "Metriful_sensor.h" #include "host_pin_definitions.h" // The Arduino Wire library has a limited internal buffer size: #define ARDUINO_WIRE_BUFFER_LIMIT_BYTES 32 void SensorHardwareSetup(uint8_t i2c_7bit_address) { pinMode(LED_BUILTIN, OUTPUT); #ifdef ESP8266 // Must specify the I2C pins Wire.begin(SDA_PIN, SCL_PIN); digitalWrite(LED_BUILTIN, HIGH); #else // Default I2C pins are used Wire.begin(); digitalWrite(LED_BUILTIN, LOW); #endif Wire.setClock(I2C_CLK_FREQ_HZ); // READY, light interrupt and sound interrupt lines are digital inputs. pinMode(READY_PIN, INPUT); pinMode(L_INT_PIN, INPUT); pinMode(S_INT_PIN, INPUT); // Set up interrupt monitoring of the READY signal, triggering on a falling edge // event (high-to-low voltage change) indicating READY assertion. The // function ready_ISR() will be called when this happens. attachInterrupt(digitalPinToInterrupt(READY_PIN), ready_ISR, FALLING); // Start the serial port. // Full settings are: 8 data bits, no parity, one stop bit Serial.begin(SERIAL_BAUD_RATE); // Wait for the MS430 to finish power-on initialization: while (digitalRead(READY_PIN) == HIGH) { yield(); } // Reset to clear any previous state: TransmitI2C(i2c_7bit_address, RESET_CMD, 0, 0); delay(5); // Wait for reset completion and entry to standby mode while (digitalRead(READY_PIN) == HIGH) { yield(); } } volatile bool ready_assertion_event = false; // This function is automatically called after a falling edge (assertion) of READY. // The flag variable is set true - it must be set false again in the main program. void ISR_ATTRIBUTE ready_ISR(void) { ready_assertion_event = true; } //////////////////////////////////////////////////////////////////////// // Functions to convert data from integer representation to floating-point representation. // Floats are easy to use for writing programs but require greater memory and processing // power resources, so may not always be appropriate. void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out) { // Decode the signed value for T (in Celsius) airDataF_out->T_C = convertEncodedTemperatureToFloat(airData_in->T_C_int_with_sign, airData_in->T_C_fr_1dp); airDataF_out->P_Pa = airData_in->P_Pa; airDataF_out->H_pc = ((float) airData_in->H_pc_int) + (((float) airData_in->H_pc_fr_1dp)/10.0); airDataF_out->G_Ohm = airData_in->G_ohm; } void convertAirQualityDataF(const AirQualityData_t * airQualityData_in, AirQualityData_F_t * airQualityDataF_out) { airQualityDataF_out->AQI = ((float) airQualityData_in->AQI_int) + (((float) airQualityData_in->AQI_fr_1dp)/10.0); airQualityDataF_out->CO2e = ((float) airQualityData_in->CO2e_int) + (((float) airQualityData_in->CO2e_fr_1dp)/10.0); airQualityDataF_out->bVOC = ((float) airQualityData_in->bVOC_int) + (((float) airQualityData_in->bVOC_fr_2dp)/100.0); airQualityDataF_out->AQI_accuracy = airQualityData_in->AQI_accuracy; } void convertLightDataF(const LightData_t * lightData_in, LightData_F_t * lightDataF_out) { lightDataF_out->illum_lux = ((float) lightData_in->illum_lux_int) + (((float) lightData_in->illum_lux_fr_2dp)/100.0); lightDataF_out->white = lightData_in->white; } void convertSoundDataF(const SoundData_t * soundData_in, SoundData_F_t * soundDataF_out) { soundDataF_out->SPL_dBA = ((float) soundData_in->SPL_dBA_int) + (((float) soundData_in->SPL_dBA_fr_1dp)/10.0); for (uint16_t i=0; iSPL_bands_dB[i] = ((float) soundData_in->SPL_bands_dB_int[i]) + (((float) soundData_in->SPL_bands_dB_fr_1dp[i])/10.0); } soundDataF_out->peakAmp_mPa = ((float) soundData_in->peak_amp_mPa_int) + (((float) soundData_in->peak_amp_mPa_fr_2dp)/100.0); soundDataF_out->stable = (soundData_in->stable == 1); } void convertParticleDataF(const ParticleData_t * particleData_in, ParticleData_F_t * particleDataF_out) { particleDataF_out->duty_cycle_pc = ((float) particleData_in->duty_cycle_pc_int) + (((float) particleData_in->duty_cycle_pc_fr_2dp)/100.0); particleDataF_out->concentration = ((float) particleData_in->concentration_int) + (((float) particleData_in->concentration_fr_2dp)/100.0); particleDataF_out->valid = (particleData_in->valid == 1); } //////////////////////////////////////////////////////////////////////// // The following five functions print data (in floating-point // representation) over the serial port as text void printAirDataF(const AirData_F_t * airDataF) { Serial.print("Temperature = "); #ifdef USE_FAHRENHEIT float temperature_F = convertCtoF(airDataF->T_C); Serial.print(temperature_F,1);Serial.println(" " FAHRENHEIT_SYMBOL); #else Serial.print(airDataF->T_C,1);Serial.println(" " CELSIUS_SYMBOL); #endif Serial.print("Pressure = ");Serial.print(airDataF->P_Pa);Serial.println(" Pa"); Serial.print("Humidity = ");Serial.print(airDataF->H_pc,1);Serial.println(" %"); Serial.print("Gas Sensor Resistance = ");Serial.print(airDataF->G_Ohm);Serial.println(" " OHM_SYMBOL); } void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF) { if (airQualityDataF->AQI_accuracy > 0) { Serial.print("Air Quality Index = ");Serial.print(airQualityDataF->AQI,1); Serial.print(" ("); Serial.print(interpret_AQI_value((uint16_t) airQualityDataF->AQI)); Serial.println(")"); Serial.print("Estimated CO" SUBSCRIPT_2 " = ");Serial.print(airQualityDataF->CO2e,1); Serial.println(" ppm"); Serial.print("Equivalent Breath VOC = ");Serial.print(airQualityDataF->bVOC,2); Serial.println(" ppm"); } Serial.print("Air Quality Accuracy: "); Serial.println(interpret_AQI_accuracy(airQualityDataF->AQI_accuracy)); } void printLightDataF(const LightData_F_t * lightDataF) { Serial.print("Illuminance = ");Serial.print(lightDataF->illum_lux,2);Serial.println(" lux"); Serial.print("White Light Level = ");Serial.print(lightDataF->white);Serial.println(); } void printSoundDataF(const SoundData_F_t * soundDataF) { char strbuf[50] = {0}; Serial.print("A-weighted Sound Pressure Level = "); Serial.print(soundDataF->SPL_dBA,1);Serial.println(" dBA"); for (uint16_t i=0; ivalid) { Serial.println("Yes"); } else { Serial.println("No (Initializing)"); } } //////////////////////////////////////////////////////////////////////// // The following five functions print data (in integer representation) over the serial port as text. // printColumns determines the print format: // choosing printColumns = false gives labeled values with measurement units // choosing printColumns = true gives columns of numbers (convenient for spreadsheets). void printAirData(const AirData_t * airData, bool printColumns) { char strbuf[50] = {0}; uint8_t T_intPart = 0; uint8_t T_fractionalPart = 0; bool isPositive = true; const char * T_unit = getTemperature(airData, &T_intPart, &T_fractionalPart, &isPositive); if (printColumns) { // Print: temperature, pressure/Pa, humidity/%, gas sensor resistance/ohm sprintf(strbuf,"%s%u.%u %" PRIu32 " %u.%u %" PRIu32 " ",isPositive?"":"-", T_intPart, T_fractionalPart, airData->P_Pa, airData->H_pc_int, airData->H_pc_fr_1dp, airData->G_ohm); Serial.print(strbuf); } else { sprintf(strbuf,"Temperature = %s%u.%u %s", isPositive?"":"-", T_intPart, T_fractionalPart, T_unit); Serial.println(strbuf); Serial.print("Pressure = ");Serial.print(airData->P_Pa);Serial.println(" Pa"); sprintf(strbuf,"Humidity = %u.%u %%",airData->H_pc_int,airData->H_pc_fr_1dp); Serial.println(strbuf); Serial.print("Gas Sensor Resistance = ");Serial.print(airData->G_ohm);Serial.println(" " OHM_SYMBOL); } } void printAirQualityData(const AirQualityData_t * airQualityData, bool printColumns) { char strbuf[50] = {0}; if (printColumns) { // Print: Air Quality Index, Estimated CO2/ppm, Equivalent breath VOC/ppm, Accuracy sprintf(strbuf,"%u.%u %u.%u %u.%02u %u ",airQualityData->AQI_int, airQualityData->AQI_fr_1dp, airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp, airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp, airQualityData->AQI_accuracy); Serial.print(strbuf); } else { if (airQualityData->AQI_accuracy > 0) { sprintf(strbuf,"Air Quality Index = %u.%u (%s)", airQualityData->AQI_int, airQualityData->AQI_fr_1dp, interpret_AQI_value(airQualityData->AQI_int)); Serial.println(strbuf); sprintf(strbuf,"Estimated CO" SUBSCRIPT_2 " = %u.%u ppm", airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp); Serial.println(strbuf); sprintf(strbuf,"Equivalent Breath VOC = %u.%02u ppm", airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp); Serial.println(strbuf); } Serial.print("Air Quality Accuracy: "); Serial.println(interpret_AQI_accuracy(airQualityData->AQI_accuracy)); } } void printSoundData(const SoundData_t * soundData, bool printColumns) { char strbuf[50] = {0}; if (printColumns) { // Print: Sound pressure level/dBA, Sound pressure level for frequency bands 1 to 6 (six columns), // Peak sound amplitude/mPa, stability sprintf(strbuf,"%u.%u ", soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); Serial.print(strbuf); for (uint16_t i=0; iSPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]); Serial.print(strbuf); } sprintf(strbuf,"%u.%02u %u ", soundData->peak_amp_mPa_int, soundData->peak_amp_mPa_fr_2dp, soundData->stable); Serial.print(strbuf); } else { sprintf(strbuf,"A-weighted Sound Pressure Level = %u.%u dBA", soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); Serial.println(strbuf); for (uint8_t i=0; iSPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]); Serial.println(strbuf); } sprintf(strbuf,"Peak Sound Amplitude = %u.%02u mPa", soundData->peak_amp_mPa_int, soundData->peak_amp_mPa_fr_2dp); Serial.println(strbuf); } } void printLightData(const LightData_t * lightData, bool printColumns) { char strbuf[50] = {0}; if (printColumns) { // Print: illuminance/lux, white level sprintf(strbuf,"%u.%02u %u ", lightData->illum_lux_int, lightData->illum_lux_fr_2dp, lightData->white); Serial.print(strbuf); } else { sprintf(strbuf,"Illuminance = %u.%02u lux", lightData->illum_lux_int, lightData->illum_lux_fr_2dp); Serial.println(strbuf); Serial.print("White Light Level = ");Serial.print(lightData->white);Serial.println(); } } void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor) { char strbuf[50] = {0}; if (printColumns) { // Print: duty cycle/%, concentration sprintf(strbuf,"%u.%02u %u.%02u %u ", particleData->duty_cycle_pc_int, particleData->duty_cycle_pc_fr_2dp, particleData->concentration_int, particleData->concentration_fr_2dp, particleData->valid); Serial.print(strbuf); } else { sprintf(strbuf,"Particle Duty Cycle = %u.%02u %%", particleData->duty_cycle_pc_int, particleData->duty_cycle_pc_fr_2dp); Serial.println(strbuf); sprintf(strbuf,"Particle Concentration = %u.%02u ", particleData->concentration_int, particleData->concentration_fr_2dp); Serial.print(strbuf); if (particleSensor == PARTICLE_SENSOR_PPD42) { Serial.println("ppL"); } else if (particleSensor == PARTICLE_SENSOR_SDS011) { Serial.println(SDS011_UNIT_SYMBOL); } else { Serial.println("(?)"); } Serial.print("Particle data valid: "); if (particleData->valid == 0) { Serial.println("No (Initializing)"); } else { Serial.println("Yes"); } } } //////////////////////////////////////////////////////////////////////// // Send data to the Metriful MS430 using the I2C-compatible two wire interface. // // Returns true on success, false on failure. // // dev_addr_7bit = the 7-bit I2C address of the MS430 board. // commandRegister = the settings register code or command code to be used. // data = array containing the data to be sent; its length must be at least "data_length" bytes. // data_length = the number of bytes from the "data" array to be sent. // bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length) { if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) { // The Arduino Wire library has a limited internal buffer size return false; } Wire.beginTransmission(dev_addr_7bit); uint8_t bytesWritten = Wire.write(commandRegister); if (data_length > 0) { bytesWritten += Wire.write(data, data_length); } if (bytesWritten != (data_length+1)) { return false; } return (Wire.endTransmission(true) == 0); } // Read data from the Metriful MS430 using the I2C-compatible two wire interface. // // Returns true on success, false on failure. // // dev_addr_7bit = the 7-bit I2C address of the MS430 board. // commandRegister = the settings register code or data location code to be used. // data = array to store the received data; its length must be at least "data_length" bytes. // data_length = the number of bytes to read. // bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length) { if (data_length == 0) { // Cannot do a zero byte read return false; } if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) { // The Arduino Wire library has a limited internal buffer size return false; } Wire.beginTransmission(dev_addr_7bit); Wire.write(commandRegister); if (Wire.endTransmission(false) != 0) { return false; } if (Wire.requestFrom(dev_addr_7bit, data_length, (uint8_t) 1) != data_length) { // Did not receive the expected number of bytes return false; } for (uint8_t i=0; i 0) { data[i] = Wire.read(); } } return true; } //////////////////////////////////////////////////////////////////////// // Provide a readable interpretation of the accuracy code for // the air quality measurements (applies to all air quality data) const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code) { switch (AQI_accuracy_code) { default: case 0: return "Not yet valid, self-calibration incomplete"; case 1: return "Low accuracy, self-calibration ongoing"; case 2: return "Medium accuracy, self-calibration ongoing"; case 3: return "High accuracy"; } } // Provide a readable interpretation of the AQI (air quality index) const char * interpret_AQI_value(uint16_t AQI) { if (AQI < 50) { return "Good"; } else if (AQI < 100) { return "Acceptable"; } else if (AQI < 150) { return "Substandard"; } else if (AQI < 200) { return "Poor"; } else if (AQI < 300) { return "Bad"; } else { return "Very bad"; } } // Set the threshold for triggering a sound interrupt. // // Returns true on success, false on failure. // // threshold_mPa = peak sound amplitude threshold in milliPascals, any 16-bit integer is allowed. bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa) { uint8_t TXdata[SOUND_INTERRUPT_THRESHOLD_BYTES] = {0}; TXdata[0] = (uint8_t) (threshold_mPa & 0x00FF); TXdata[1] = (uint8_t) (threshold_mPa >> 8); return TransmitI2C(dev_addr_7bit, SOUND_INTERRUPT_THRESHOLD_REG, TXdata, SOUND_INTERRUPT_THRESHOLD_BYTES); } // Set the threshold for triggering a light interrupt. // // Returns true on success, false on failure. // // The threshold value in lux units can be fractional and is formed as: // threshold = thres_lux_int + (thres_lux_fr_2dp/100) // // Threshold values exceeding MAX_LUX_VALUE will be limited to MAX_LUX_VALUE. bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, uint8_t thres_lux_fr_2dp) { uint8_t TXdata[LIGHT_INTERRUPT_THRESHOLD_BYTES] = {0}; TXdata[0] = (uint8_t) (thres_lux_int & 0x00FF); TXdata[1] = (uint8_t) (thres_lux_int >> 8); TXdata[2] = thres_lux_fr_2dp; return TransmitI2C(dev_addr_7bit, LIGHT_INTERRUPT_THRESHOLD_REG, TXdata, LIGHT_INTERRUPT_THRESHOLD_BYTES); } //////////////////////////////////////////////////////////////////////// // Convenience functions for reading data (integer representation) // // For each category of data (air, sound, etc.) a pointer to the data // struct is passed to the ReceiveI2C() function. The received byte // sequence fills the data struct in the correct order so that each // field within the struct receives the value of an environmental data // quantity (temperature, sound level, etc.) SoundData_t getSoundData(uint8_t i2c_7bit_address) { SoundData_t soundData = {0}; ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); return soundData; } AirData_t getAirData(uint8_t i2c_7bit_address) { AirData_t airData = {0}; ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); return airData; } LightData_t getLightData(uint8_t i2c_7bit_address) { LightData_t lightData = {0}; ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); return lightData; } AirQualityData_t getAirQualityData(uint8_t i2c_7bit_address) { AirQualityData_t airQualityData = {0}; ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); return airQualityData; } ParticleData_t getParticleData(uint8_t i2c_7bit_address) { ParticleData_t particleData = {0}; ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); return particleData; } // Convenience functions for reading data (float representation) SoundData_F_t getSoundDataF(uint8_t i2c_7bit_address) { SoundData_F_t soundDataF = {0}; SoundData_t soundData = getSoundData(i2c_7bit_address); convertSoundDataF(&soundData, &soundDataF); return soundDataF; } AirData_F_t getAirDataF(uint8_t i2c_7bit_address) { AirData_F_t airDataF = {0}; AirData_t airData = getAirData(i2c_7bit_address); convertAirDataF(&airData, &airDataF); return airDataF; } LightData_F_t getLightDataF(uint8_t i2c_7bit_address) { LightData_F_t lightDataF = {0}; LightData_t lightData = getLightData(i2c_7bit_address); convertLightDataF(&lightData, &lightDataF); return lightDataF; } AirQualityData_F_t getAirQualityDataF(uint8_t i2c_7bit_address) { AirQualityData_F_t airQualityDataF = {0}; AirQualityData_t airQualityData = getAirQualityData(i2c_7bit_address); convertAirQualityDataF(&airQualityData, &airQualityDataF); return airQualityDataF; } ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address) { ParticleData_F_t particleDataF = {0}; ParticleData_t particleData = getParticleData(i2c_7bit_address); convertParticleDataF(&particleData, &particleDataF); return particleDataF; } //////////////////////////////////////////////////////////////////////// // Functions to convert Celsius temperature to Fahrenheit, in float // and integer formats float convertCtoF(float C) { return ((C*1.8) + 32.0); } // Convert Celsius to Fahrenheit in sign, integer and fractional parts void convertCtoF_int(float C, uint8_t * F_int, uint8_t * F_fr_1dp, bool * isPositive) { float F = convertCtoF(C); bool isNegative = (F < 0.0); if (isNegative) { F = -F; } F += 0.05; F_int[0] = (uint8_t) F; F -= (float) F_int[0]; F_fr_1dp[0] = (uint8_t) (F*10.0); isPositive[0] = (!isNegative); } // Decode and convert the temperature as read from the MS430 (integer // representation) into a float value float convertEncodedTemperatureToFloat(uint8_t T_C_int_with_sign, uint8_t T_C_fr_1dp) { float temperature_C = ((float) (T_C_int_with_sign & TEMPERATURE_VALUE_MASK)) + (((float) T_C_fr_1dp)/10.0); if ((T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { // the most-significant bit is set, indicating that the temperature is negative temperature_C = -temperature_C; } return temperature_C; } // Obtain temperature, in chosen units (C or F), as sign, integer and fractional parts const char * getTemperature(const AirData_t * pAirData, uint8_t * T_intPart, uint8_t * T_fractionalPart, bool * isPositive) { #ifdef USE_FAHRENHEIT float temperature_C = convertEncodedTemperatureToFloat(pAirData->T_C_int_with_sign, pAirData->T_C_fr_1dp); convertCtoF_int(temperature_C, T_intPart, T_fractionalPart, isPositive); return FAHRENHEIT_SYMBOL; #else isPositive[0] = ((pAirData->T_C_int_with_sign & TEMPERATURE_SIGN_MASK) == 0); T_intPart[0] = pAirData->T_C_int_with_sign & TEMPERATURE_VALUE_MASK; T_fractionalPart[0] = pAirData->T_C_fr_1dp; return CELSIUS_SYMBOL; #endif }