JacChr
Created March 20, 2017

BLE enabled gas leakage sensor

Simple but functional intelligent(i hope ;-) and BLE connected sensor which controls the state of a gas boiler.

Work in progress276
BLE enabled gas leakage sensor

Things used in this project

Hardware components

Arduino 101
Arduino 101
×1
Seeed Studio Grove LCD RGB Backlight
×1
Seeed Studio Grove Led socket kit + blue Led
×1
Seeed Studio Grove buzzer
×1
Seeed Studio Grove touch sensor
×1
Seeed Studio Grove temperature sensor v1.2
×1
Seeed Studio Grove sound sensor
×1
Seeed Studio Grove base shield v2
×1
MQ4 gas sensor
×1
General Purpose Transistor NPN
General Purpose Transistor NPN
×2
BC327 general purpose PNP transistor
×1
V23042 - bistable relay - 3v
×1
DS18B20 Programmable Resolution 1-Wire Digital Thermometer
Maxim Integrated DS18B20 Programmable Resolution 1-Wire Digital Thermometer
×1
Resistor 27k
×1
Resistor 2k2
×1
Resistor 4k7
×2
Resistor 510
×1
Resistor 10k ohm
Resistor 10k ohm
×1

Software apps and online services

nRF Connect SDK
Nordic Semiconductor nRF Connect SDK
Used to test BLE communication.

Story

Read more

Schematics

Schematics of the device

I've made it using MS Paint. There are two reasons explaining this choice - first - fritizing works very slowly on my computer, second - it doesn't have library with all Grove components - and because of that the MS Paint version seemed more readable to me.

Code

Firmware of an intelligent gas sensor

Arduino
Source code of firmware of my project
#include <TaskScheduler.h>
#include <Wire.h>
#include "rgb_lcd.h"
#include <OneWire.h>
#include <CurieBLE.h>

//port where the ds18b20 temperature sensor is attached
#define ONE_WIRE 0

//all time-related constants are expressed in miliseconds
//periods used by the MQ4 sensor
// - period of heating
#define HEATING_TIME 60000
// - how often gas concentration is measured
#define MEASUREMENT_INTERVAL 180000
// - how long it takes to initialize MQ4 sensor
#define INITIALIZATION_TIME 180000

//periods of different actions
// - how long LCD is on after any event
#define ON_TIME 60000
// - LCD's frequency of blinking
#define LCD_BLINK_TIME 1000
// - how often content of LCD is refreshed
#define REFRESH_TIME 1000
// - status led's frequency of blinking
#define LED_BLINK_TIME 500
// - delay between initialization of temperature measurement (ds18b20) and reading of its result
// without this delay sensor can give us unreliable results
#define READ_TIME 1000

// periods of sound alarm - initially - one 200 ms beep every 400 ms, then one 200 ms beep every 5 s
#define BUZZER_INITIAL_TIME 200
#define BUZZER_ON_TIME 200
#define BUZZER_OFF_TIME 5000
//after what time the alarm should be less noisy - ~1 minute
#define INTENS_ALARM_TIME 60000

//definition of ports 
//digital IO ports
// proximity sensor
#define TOUCH_BUTTON 4
//buzzer
#define BUZZER 3
//status led
#define STATUS_LED 2
//heater control
//pins controlling h-bridge - X_ON - turns on relay, X_OFF - turns it off
//LOW - connected to npn transistor, HIGH - to pnp
#define H_BRIDGE_LOW_ON 9
#define H_BRIDGE_HIGH_ON 8
#define H_BRIDGE_LOW_OFF 6
#define H_BRIDGE_HIGH_OFF 7

//analog inputs
// - gas sensor
#define GAS_SENSOR A1
// - analog temperature sensor
#define TEMP_SENSOR A3
// - microphone
#define SOUND_SENSOR A2

//constants used by the analog temperature sensor
#define B 4275
#define R0 100000

//states of the device
#define INITIALIZATION 0
#define WORK_LCD_ON 1
#define WORK_LCD_OFF 2
#define DAMAGE 3
#define ALARM 4

//conditions which have to be met to raise alarm
// - level of sound - expressed in values returned by ADC
#define SOUND_LIMIT 1000
// - gas limit expressed in ppm (parts per million)
#define GAS_LIMIT 2000
//acceptable heater temperature 
#define HEATER_TEMP_VALUE 35 //use something close to external temperature if temp. sensor doesn't touch MQ4 sensor

// object used to access devices connected to OneWire bus - ds18b20 in our case
OneWire dsi(ONE_WIRE);

//BLE section
BLEPeripheral blePeripheral; // creates peripheral instance
BLEService sensorService("19B10010-E8F2-537E-4F6C-D104768A1214"); // create service, number taken from BLE example - but it can be changed
//characteristics with gas concentration and external temperature - read and notify
BLEFloatCharacteristic gasConcentrationCharacteristic("19B10011-E8F2-537E-4F6C-D104768A1214", BLERead| BLENotify);
BLEFloatCharacteristic extTemperatureCharacteristic("19B10011-E8F2-537E-4F6C-D104768A1214", BLERead| BLENotify);
//create button characteristic and allow remote device to change the state
BLECharCharacteristic buttonCharacteristic("19B10012-E8F2-537E-4F6C-D104768A1214", BLEWrite); 

//class used to pass the device's state between components
class DeviceState
    {
      //state of a device - may contain the following constans
      int deviceState;     
	    //content of LCD display - these variables are used to pass this information from different external sensors to LCD
      String firstLine;
      String secondLine;
	    //value of external temperature - purpose of it is the same like two variables above
      int externalTemeprature;
	    //constructor + getters + setters
      public:
      DeviceState(int state)
      {
        deviceState = state;
      }
    
      void changeState(int state){
        deviceState = state;
      }

      int getState(){
        return deviceState;
      }

      void setFirstLine(String line){
        firstLine = line;
      }

      void setSecondLine(String line){
        secondLine = line;
      }

      String getFirstLine(){
        return firstLine;
      }

      String getSecondLine(){
        return secondLine;
      }
      void setExternalTemperature(int temp){
        externalTemeprature = temp;
      }

      int getExternalTemperature(){
        return externalTemeprature;
      }

    };

//class responsible for handling DS18B20 device
class DsTask
    {
      unsigned long previousMillis;

      OneWire *ds;

      //DS18B20 address
      byte addr[8];

      boolean readInitialized;
      
      public:
      DsTask(OneWire *owds)
      {
        ds = owds;
        previousMillis = 0;
      }
	    //initialization of sensor - code taken from example from the internet
	    boolean Init(){
		    ds->reset_search();
    
		    if ( !ds->search(addr)) {
			    //no more addresses
			    ds->reset_search();
			    return false;
		    }

		    if ( OneWire::crc8( addr, 7) != addr[7]) {
			    return false;
		    }
		    if ( addr[0] != 0x10 && addr[0] != 0x28) {
			    //device not recognized
			    return false;
		    }
		    return true;
	    }

	    //start of measurement
	    void initRead(){
    		ds->reset();
    		ds->select(addr);
    		ds->write(0x44,1);
	    }
  
  	  //reads temperature from DS18B20
	    int readDSData(){
    		byte i;
    		byte present = 0;
    		byte data[12];
    		int HighByte, LowByte, TReading, SignBit, Tc_100;
    		
    		present = ds->reset();
    		ds->select(addr);    
    		ds->write(0xBE);
    
    		for ( i = 0; i < 9; i++) {
    		  data[i] = ds->read();
    		}
    
    		LowByte = data[0];
    		HighByte = data[1];
    		TReading = (HighByte << 8) + LowByte;
    		SignBit = TReading & 0x8000;
    		if (SignBit)
    		{
    		  TReading = (TReading ^ 0xffff) + 1;
    		}
    		Tc_100 = (6 * TReading) + TReading / 4;
    
    		if(SignBit)
    		  Tc_100 = (-1)*Tc_100;
    
    		return Tc_100;
    	  }	
    	  //temperature conversion
    	  String convertTemperature(int temp){
    		String output = "";
    		boolean negative = false;
    		int Whole, Fract;
        
    		if(temp<0){
    			temp = temp*(-1);
    			negative = true;  
    			output = "-";
    		}
        
    		Whole = temp / 100;
    		Fract = temp % 100;
    
    		
    		output = output+String(Whole)+".";
    		
    		if (Fract < 10)
    		{
    		  output = output + "0";
    		}
    		output = output + Fract;
    
    		return output;
    	}
	    // method responsible for handling the device
	    void Update(DeviceState *state)
	    {
        unsigned long currentMillis = millis();
  
        int devState = state->getState();
  	    //if time from the previous measurement is longer than defined - start conversion
        if(!readInitialized && currentMillis - previousMillis > MEASUREMENT_INTERVAL){
          initRead();
          readInitialized = true;
          previousMillis = currentMillis;
        }
  	    //1 second after start of conversion - read its result
        if(readInitialized && currentMillis - previousMillis > READ_TIME){
          int temperature = readDSData();
          
          state->setExternalTemperature(temperature);
          String tempString = "Temp:";
          String convertedTemperature = convertTemperature(temperature);
          tempString = tempString+convertedTemperature+" C";
  		    //if device is in the DAMAGE state - don't remove the reason of a damage - it is always displayed in second line of LCD
          if(devState!=DAMAGE)
            state->setSecondLine(tempString);
          previousMillis = currentMillis;
          extTemperatureCharacteristic.setValue(convertedTemperature.toFloat());
          readInitialized = false;
        }
      
	  }
};

//class responsible for handling LCD
class LcdTask
    {
      rgb_lcd lcd;

      String firstLine;
      String secondLine;

      int prevState;
      unsigned long previousMillis;
      unsigned long prevRefresh;     
      unsigned long onTime;

      boolean blinkState;
      boolean isOn;
      
      public:
      LcdTask()
      {
        firstLine = "";
        secondLine = "";
        
       
        previousMillis = 0;
        prevRefresh = 0;
        onTime = 0;
        isOn = true;
        prevState = INITIALIZATION;
        blinkState = false;
      }
      
       //Initializes lcd display
      void Init(){
        lcd.begin(16, 2);
        lcd.setRGB(0,255,0);
        lcd.setCursor(0,0);
      }

      void setFirstLine(String line){
        firstLine = line;
      }

      void setSecondLine(String line){
        secondLine = line;
      }
      
      void Update(DeviceState *state)
      {
        
        int devState = state->getState();

        unsigned long currentMillis = millis();

		    //first "if" handles turning off LCD after 1 minute without important events
        if(devState == WORK_LCD_ON)
        {
          
          if(prevState!=WORK_LCD_ON){
            onTime = currentMillis;
            lcd.setRGB(0,255,0);
            lcd.display();
            prevRefresh = 0;
            isOn = true;
          }

          if(currentMillis - onTime > ON_TIME){
            lcd.setRGB(0,0,0);
            lcd.noDisplay();
            isOn = false;
            state->changeState(WORK_LCD_OFF);
          }
        
        }else if(devState == ALARM){
		      //blinking in case of alarm is handled here
          if(!isOn){
            isOn = true;
            lcd.display();
          }
          if(currentMillis - previousMillis > LCD_BLINK_TIME){
            if(blinkState){
              lcd.setRGB(255,0,0);
              blinkState = false;
            }else{
              lcd.setRGB(0,0,0);
              blinkState = true;
            }
            previousMillis = currentMillis;
          }
        }else if(devState == DAMAGE){
          if(!isOn){
            isOn = true;
            lcd.display();            
            lcd.setRGB(0,0,255);
          }
        }
		    //content of display is refreshed every REFRESH_TIME miliseconds
        if(isOn && (currentMillis - prevRefresh >=REFRESH_TIME)){
          
          firstLine = state->getFirstLine();
          secondLine = state->getSecondLine();
          lcd.clear();
          lcd.setCursor(0,0);
          lcd.print(firstLine);
          lcd.setCursor(0,1);
          lcd.print(secondLine);
          prevRefresh = currentMillis;
        }

        prevState = devState;
      }
};

//class responsible for handling status led
class StatusLed
    {
      unsigned long previousMillis;
      boolean isOn;
      
      public:
      StatusLed()
      {
        previousMillis = 0;
        isOn = false;
      }
      
      void Update(DeviceState *state)
      {
        
        int devState = state->getState();

        unsigned long currentMillis = millis();

        
        if(devState == INITIALIZATION || devState == DAMAGE)
        {
          if(currentMillis - previousMillis > LED_BLINK_TIME){
            
            if(isOn){
            
              digitalWrite(STATUS_LED, LOW);
              isOn = false;
            }else{
            
              digitalWrite(STATUS_LED, HIGH);
              isOn = true;
            }
            previousMillis = currentMillis;
          }
        }else if(devState == WORK_LCD_ON || devState == WORK_LCD_OFF){
          
          digitalWrite(STATUS_LED, HIGH);
        }
      
      }
};


// Class responsible for handling proximity sensor
class Button
    {
      
      public:
      Button()
      {
      }
      
      void Update(DeviceState *state)
      {
        
        int devState = state->getState();
        //handle BLE button
        if(devState == ALARM && buttonCharacteristic.written() && buttonCharacteristic.value()){
          state->changeState(WORK_LCD_ON);
        }
        if(digitalRead(TOUCH_BUTTON) == HIGH){
          if(devState == ALARM)
          {
            state->changeState(WORK_LCD_ON);
          }else if(devState == WORK_LCD_OFF){
            state->changeState(WORK_LCD_ON);
          }
        }
      }
};


// Class responsible for handling sound sensor
class SoundSensor
    {
      
      int previousMillis;

      int readings[5];
      int arrayPointer;
      
      public:
      SoundSensor()
      {
        previousMillis = 0;
        for(int i=0;i<5;i++)
          readings[i] = 0;
        arrayPointer = 0;
      }
      //checks alarm condition - if 3 of 5 consecutive samples are greater than limit -> raises alarm
      boolean checkAlarmCondition(){
        int i;
        int numberOfExceedings = 0;
        for(i=0;i<5;i++){
          if(readings[i]>SOUND_LIMIT)
            numberOfExceedings++;
        }

        if(numberOfExceedings>3)
          return true;

        return false;
      }
      
      void Update(DeviceState *state)
      {
        
        int devState = state->getState();

        unsigned long currentMillis = millis();

        if(devState == WORK_LCD_ON or devState == WORK_LCD_OFF){
          int value = analogRead(SOUND_SENSOR);

          readings[arrayPointer] = value;
          arrayPointer++;
          if(arrayPointer>4)
            arrayPointer = 0;

          if(checkAlarmCondition()){
            state->changeState(ALARM);
            for(int i=0;i<5;i++)
              //it has to be erased - otherwise alarm might not be turned off after first use of a button - especially BLE button
              readings[i] = 0;
          }
          previousMillis = currentMillis;
        }
        
      }
    };


// Class responsible for handling the buzzer
class Buzzer
    {
      boolean isOn;
      unsigned long previousMillis;
      unsigned long intensAlarmTime;
      
      public:
      Buzzer()
      {
        previousMillis = 0;
        intensAlarmTime = 0;
        isOn = false;
      }


      
      void Update(DeviceState *state)
      {
        
        int devState = state->getState();

        unsigned long currentMillis = millis();

        if(devState == ALARM){
          if(intensAlarmTime == 0)
            intensAlarmTime = currentMillis;
          if((currentMillis - intensAlarmTime)> INTENS_ALARM_TIME){
            if(isOn and (currentMillis - previousMillis)>BUZZER_ON_TIME){
              isOn = false;
              digitalWrite(BUZZER, LOW);
              previousMillis = currentMillis;
            }else if(!isOn and (currentMillis - previousMillis)>BUZZER_OFF_TIME){
              isOn = true;
              digitalWrite(BUZZER, HIGH);
              previousMillis = currentMillis;
            }
          }else{
            if((currentMillis - previousMillis)>BUZZER_INITIAL_TIME){
              previousMillis = currentMillis;
              if(isOn){
                digitalWrite(BUZZER, LOW);      
                isOn = false;
              }else{
                digitalWrite(BUZZER, HIGH);      
                isOn = true;
              }
            }
          }
        }else{
          digitalWrite(BUZZER, LOW);
          if(intensAlarmTime!=0)
            intensAlarmTime = 0;
        }
        
        
      }
};

//class responsible for handling gas sensor
class GasSensor
    {
      unsigned long previousMillis;

      boolean heaterOn;
      
      public:
      GasSensor()
      {
        previousMillis = 0;
        heaterOn = false;
      }

      //Turns the heater off.
	    void turnHeaterOff(){
    		digitalWrite(H_BRIDGE_LOW_OFF, LOW);
    		digitalWrite(H_BRIDGE_HIGH_OFF, HIGH); 
    		delay(100);
    		digitalWrite(H_BRIDGE_LOW_ON, HIGH);
    		digitalWrite(H_BRIDGE_HIGH_ON, LOW);
    		delay(10);
    		turnOffBridge();  
    		heaterOn = false;
      }

      //Turns the heater on
      void turnHeaterOn(){
    		digitalWrite(H_BRIDGE_LOW_ON, LOW);
   	    digitalWrite(H_BRIDGE_HIGH_ON, HIGH); 
    		delay(100);
    		digitalWrite(H_BRIDGE_LOW_OFF, HIGH);
    		digitalWrite(H_BRIDGE_HIGH_OFF, LOW);
    		delay(10);
    		turnOffBridge(); 
    		heaterOn = true;
      }


	  // Sets the h-bridge into neutral position
      void turnOffBridge(){
    		digitalWrite(H_BRIDGE_LOW_ON, LOW);
    		digitalWrite(H_BRIDGE_HIGH_ON, HIGH);
    		digitalWrite(H_BRIDGE_LOW_OFF, LOW);
    		digitalWrite(H_BRIDGE_HIGH_OFF, HIGH);
  	  }
    
	
      // Calculates temperature of tha analog temp sensor
      float calculateTemperature(int value){
    		//due to a difference between max voltage of ADC (3.3v) and supply voltage of a sensor (5v)
    		//value must be properly conditioned
    		float R = 1023.0/(((float)value)*3.3/5)-1.0;
    		R = 100000.0*R;
          
    		return (1.0/(log(R/100000.0)/B+1/298.15)-273.15);
    	}
    
	    //calculates temperature coefficient of MQ4 sensor
	    double calculateTempCoefficient(int externalTemperature){
        int degreesTemp = externalTemperature/100;

        float m, b;
		    //temperature characteristic is approximated by set of straight lines
        if(degreesTemp <=5){
          m = - 0.013333;
          b = 1.166667; 
        }else if (degreesTemp<20){
          m = - 0.006666;
          b = 1.133333;
        }else{
          m = -0.003333;
          b = 1.066666;
        }

        float coefficient = m*((float)degreesTemp) + b;
        return coefficient;
	    }
    
      //algorithm taken from http://www.jayconsystems.com/tutorials/gas-sensor-tutorial/
	    //compensation of temperature has been added
      double calculateGasConcentration(int sensorValue, int externalTemperature){
    		float m = -0.318; //Slope 
    		float b = 1.133; //Y-Intercept 
    		float RZ = 11.820; //Sensor Resistance in fresh air - it should be measured in the way given on the page mentioned above
    		float sensor_volt; //Define variable for sensor voltage 
    		float RS_gas; //Define variable for sensor resistance  
    		float ratio; //Define variable for ratio
    		float externalTempCoeff = calculateTempCoefficient(externalTemperature);
    		
    		//thanks the network of resistors we can treat the MQ4 sensor as if it is powered by 5v
    		sensor_volt = ((float)sensorValue)*(5/1023.0); //Convert analog values to voltage, 
            RS_gas = ((5*10.0)/sensor_volt)-10.0; //Get value of RS in a gas
            //include external temperatue
            RS_gas = RS_gas/externalTempCoeff;
            ratio = RS_gas/RZ;  // Get ratio RS_gas/RS_air
     
            double ppm_log = (log10(ratio)-b)/m; //Get ppm value in linear scale according to the the ratio value  
            double ppm = pow(10, ppm_log); //Convert ppm value to log scale 
    
            return ppm;
      }

	  //at the end of a heating cycle, heater should be rather hot - so we can use this fact to check condition of a sensor
      boolean isHeaterOK(){
    		float heaterTemperature = calculateTemperature(analogRead(TEMP_SENSOR));
    		return heaterTemperature>HEATER_TEMP_VALUE;
	    }
    
	    void Update(DeviceState *state)
      {
        int devState = state->getState();

        int externalTemperature = state->getExternalTemperature();        
        
        unsigned long currentMillis = millis();

        //thanks this we can properly detect moment of initialization
		    if(previousMillis == 0)
          previousMillis = currentMillis;
        //initialization - heater on for 3 minutes
        if(devState == INITIALIZATION)
        {
          if(!heaterOn){
            turnHeaterOn();
          }
          if(currentMillis - previousMillis > INITIALIZATION_TIME){
            //check, whether the sensor is hot enough - after initialization
            if(!isHeaterOK()){
              state->changeState(DAMAGE);
              //if not - switch to DAMAGE state
            }else{
              state->changeState(WORK_LCD_ON);
            }
            previousMillis = currentMillis; 
            
            turnHeaterOff();   
          }
        
        }else if(devState!=DAMAGE){
            if(heaterOn){
              if(currentMillis - previousMillis > HEATING_TIME){
                //check, whether sensor is hot enough
                if(!isHeaterOK()){
                  turnHeaterOff();
                  state->changeState(DAMAGE);
                  state->setFirstLine("Sensor's heater");
                  state->setSecondLine("Failure");
                  
                  return;
                }
                //read gas concentration value
                int sensorValue = 0;
                //take 10 consecutive samples and calculate their average
				        for(int i=0;i<10;i++)
                  sensorValue = sensorValue + analogRead(GAS_SENSOR);
                sensorValue = sensorValue/10;

                double tempGasConcentration = calculateGasConcentration(sensorValue, externalTemperature);
                
                gasConcentrationCharacteristic.setValue(tempGasConcentration);

                String tempString = "Gas:";
                
                tempString = tempString+String(tempGasConcentration)+" ppm";
                state->setFirstLine(tempString);
                
                if(tempGasConcentration>GAS_LIMIT){
                  state->changeState(ALARM);                
                }else{
                  if(devState == ALARM){
                    state->changeState(WORK_LCD_ON);
                  }
                }
                turnHeaterOff();
                previousMillis = currentMillis;
              }
            }else{
              if(currentMillis - previousMillis > MEASUREMENT_INTERVAL){
                //if it is time to start next measurement - turn the heater on
				        turnHeaterOn();
                previousMillis = currentMillis;
              }
            }
        }
      
      }
};

//creation of objects used to control external components
LcdTask lcdisplay;
DsTask ds(&dsi);
StatusLed sl;
GasSensor gs;
Button proximity;
SoundSensor sound;
Buzzer buzzer;
//and object with state
DeviceState devState(INITIALIZATION);

//method responsible for initialization of BLE - taken from BLE example
void bleInitialization(){
  blePeripheral.setLocalName("GasSensor");
  blePeripheral.setAdvertisedServiceUuid(sensorService.uuid());

  blePeripheral.addAttribute(sensorService);
  blePeripheral.addAttribute(gasConcentrationCharacteristic);
  blePeripheral.addAttribute(extTemperatureCharacteristic);
  blePeripheral.addAttribute(buttonCharacteristic);
  // set initial values of characteristics
  gasConcentrationCharacteristic.setValue(0);
  extTemperatureCharacteristic.setValue(0);
  buttonCharacteristic.setValue(0);

  // advertise the service
  blePeripheral.begin();
}


void setup() {
  configurePins();
  gs.turnOffBridge();
  lcdisplay.Init();
  if(!ds.Init()){
    //if DS18B20 sensor doesn't work - switch to DAMAGE state
	devState.changeState(DAMAGE);
    devState.setFirstLine("Ext. temp. sensor");
    devState.setSecondLine("Failure");
  }
  bleInitialization();
}

//configure pins
void configurePins(){
  //setting up digital pins
  pinMode(TOUCH_BUTTON, INPUT);
  pinMode(BUZZER, OUTPUT);
  pinMode(STATUS_LED, OUTPUT);
  pinMode(H_BRIDGE_LOW_ON, OUTPUT);
  pinMode(H_BRIDGE_HIGH_ON, OUTPUT);
  pinMode(H_BRIDGE_LOW_OFF, OUTPUT);
  pinMode(H_BRIDGE_HIGH_OFF, OUTPUT);
  //setting up analog pins
  pinMode(GAS_SENSOR, INPUT);
  pinMode(TEMP_SENSOR, INPUT);
}

//main loop - very easy as you can see
void loop() {
  blePeripheral.poll();
  
  ds.Update(&devState);
  lcdisplay.Update(&devState);
  sl.Update(&devState);
  gs.Update(&devState);
  sound.Update(&devState);
  buzzer.Update(&devState);
  proximity.Update(&devState);
}

Credits

JacChr

JacChr

3 projects • 1 follower
I'm an enthusiast of smart homes with emphasis on security systems and with strong sentiment to hardware solutions.

Comments