Dimiter Kendri
Published © GPL3+

Lake turbidity and environmental monitoring with BLE

Monitor lake turbidity as well as temperature and humidity with your smartphone via BLE

AdvancedFull instructions provided20 hours4,299

Things used in this project

Story

Read more

Schematics

I2C digital Sensors and microphone connections

Arduino BLE sensor data

Code

CurieLakeMonitor

C/C++
Curie lake monitoring app.
/* Arduino 101 CurieLakeMonitor for lake turbidity monitoring
 * Copyright (C) 2017 by Dimiter Kendri
 *
 *
 * This Library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This Library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the Arduino 101 Curie Lake monitor.  If not, see
 * <http://www.gnu.org/licenses/>.
 */


#include <CurieBLE.h>

#include <TFT.h>  // Arduino LCD library
#include <SPI.h>

#include "Si7020.h"
#include "SI1133.h"

// pin definition for the Uno
#define cs   10
#define dc   9
#define rst  8

// create an instance of the library
TFT TFTscreen = TFT(cs, dc, rst);
SI1133 uvir = SI1133();
Si7020 temphum;

/*********************************************************************************/
// char array to print to the screen
char SPLPrintout[6];
char humidityPrintout[6];
char tempPrintout[6];
char UVPrintout[6];

float temperature = 0;
float humidity =    0;
float uv =    0;
float ir =    0;

int16_t oldTemp =   0;
uint16_t oldHum =   0;
/*********************************************************************************/

const char *moduleName = "ArduinoLakeMonitor01";

BLEPeripheral blePeripheral;

// Environment sensing service UUID
// https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.environmental_sensing.xml
#define SERVICE_UUID_ESS "181A"           
#define CHAR_UUID_TEMPERATURE "2A6E"        // Temperature characteristic UUID
#define CHAR_UUID_HUMIDITY "2A6F"           // Humidity characteristic UUID

BLEService esSvc(SERVICE_UUID_ESS);

// We want notifications to be supported
BLEShortCharacteristic tempChar(CHAR_UUID_TEMPERATURE,  BLERead | BLENotify);

BLEUnsignedShortCharacteristic humChar(CHAR_UUID_HUMIDITY,   BLERead | BLENotify);
/*********************************************************************************/

void setupTemperatureHumidityIRSensors(void);
void updateSensorData(void);

void setup() {
  Serial.begin(9600);
  while (!Serial);    // wait for the serial port to open

  setupTemperatureHumidityIRSensors();
  
  // Put this line at the beginning of every sketch that uses the GLCD:
  TFTscreen.begin();
  TFTscreen.background(0, 0, 0);        // clear the screen with a black background

  // write the static text to the screen  set the font color to white
  TFTscreen.stroke(255, 255, 40);              // set the font size
  TFTscreen.setTextSize(2);                      // write the text to the top left corner of the screen
  TFTscreen.text("SPL Value :\n ", 0, 0);       // ste the font size very large for the loop
  TFTscreen.text("Hum: ", 0, 40);       // ste the font size very large for the loop
  TFTscreen.text("Temp: ", 0, 60);       // ste the font size very large for the loop
  TFTscreen.text("UV: ", 0, 80);       // ste the font size very large for the loop
  TFTscreen.setTextSize(2);


    // Set BLE name for the module
  blePeripheral.setLocalName(moduleName);
  // Add the service UUID
  blePeripheral.setAdvertisedServiceUuid(esSvc.uuid());
  // Add the BLE service
  blePeripheral.addAttribute(esSvc);
  // Add characteristics
  blePeripheral.addAttribute(tempChar);
  blePeripheral.addAttribute(humChar);

  // Set initial values
  updateSensorData();

  /* Activate the BLE device.  It will start continuously transmitting BLE
     advertising packets and will be visible to remote BLE central devices
     until it receives a new connection */
  blePeripheral.begin();
  
}

void loop() {

  // Read the value of the sensor on A0
  String sensorVal        =  String(analogRead(A0));
  String humidityVal      =  String(humidity);
  String temperatureVal   =  String(temperature);
  String uvVal            =  String(uv);
  
  // convert the reading to a char array
  sensorVal.toCharArray(SPLPrintout, 6);
  temperatureVal.toCharArray(tempPrintout, 6);
  humidityVal.toCharArray(humidityPrintout, 6);       // convert the reading to a char array
  uvVal.toCharArray(UVPrintout, 6);

  // set the font color
  TFTscreen.stroke(2, 215, 255);   // print the sensor values in yellow
  
  TFTscreen.text(SPLPrintout, 0, 20);     
  TFTscreen.text(humidityPrintout, 60, 40);    
  TFTscreen.text(tempPrintout, 60, 60);    
  TFTscreen.text(UVPrintout, 60, 80);    
  
  TFTscreen.text("Arduino 101", 10, 110);     // wait for a moment
  
  updateSensorData();

  TFTscreen.stroke(0, 0, 0);
  TFTscreen.text(SPLPrintout, 0, 20);
  TFTscreen.text(humidityPrintout, 60, 40);
  TFTscreen.text(tempPrintout, 60, 60);
  TFTscreen.text(UVPrintout, 60, 80);

  BLECentral central = blePeripheral.central();

  // If central has connected to us
  if (central) {

    // Update sensor data as long as central is connected
    while (central.connected()) {
      updateSensorData();
    }
  }
}


void setupTemperatureHumidityIRSensors(void)
{
  Serial.println("Initializing Si1133 device...");
  
  if (! uvir.begin()) {
    Serial.println("Can't find Si1133");
    while (1);
  }
  Serial.println("Initializing Si7020 device...\r\n");
  
  temphum.begin();
  temphum.resetSettings();
  // To change resolution of the sensor use sensor.changeResolution(int i) where i=[0-3], 
  temphum.changeResolution(3);
}

  
// Get data from sensors, process and push to BLE characteristics
void updateSensorData(void)
{
  // We don't really need it to be very frequent, let's say every 10 seconds.
  // TODO: this must be exposed through BLE and configurable by user
  delay(10000);

  uv = uvir.readUV();
  ir = uvir.readIR();
  temperature = temphum.getTemp();
  humidity = temphum.getRH();

  
  // BLE ESS has float data with 0.01 precision stored as ints
  int16_t essTemp = temperature*100;
  uint16_t essHum = round(humidity)*100;

  // Update BLE characteristics if values changed
  if (essTemp != oldTemp) {
    tempChar.setValue(essTemp);
    oldTemp = essTemp;
  }
  if (essHum != oldHum) {
    humChar.setValue(essHum);
    oldHum = essHum;
  }

  
  Serial.print("FULL-IR: ");    Serial.println(ir,BIN);
  Serial.print("UV: ");     Serial.println(uv,BIN);
  Serial.print("UV: ");     Serial.println(uv);
  if(uv>545){
    Serial.println("UVI: 14");
  }
  else
  {
    Serial.print("UVI: ");  
  Serial.println(0.0082*(0.00391*uv*uv+uv));
  }
  
  
  Serial.println("Status\tHumidity (%)\tTemperature (C)");
  Serial.print("\t");
  Serial.print(humidity);
  Serial.print("\t\t");
  Serial.print(temperature,2);
  
}

SI1133.cpp

C/C++
IR/UV and ambient light sensor
#include "SI1133.h"

SI1133::SI1133() {
  _addr = SI1133_ADDR;
}

boolean SI1133::begin(void) {
Wire.begin();
 
  uint8_t id = read8(SI1133_REG_PARTID);
  if (id != 0x33) return false;
  
  reset();


  writeParam(SI1133_PARAM_CHLIST,0X01);
  //=======================================================

  writeParam(SI1133_PARAM_ADCCONFIG0,0x78 );

  writeParam(SI1133_PARAM_ADCSENS0,0x09);

  writeParam(SI1133_PARAM_ADCPSOT0,0x00);
  writeParam(SI1133_PARAM_MEASCONFIG0,COUNT0);


  write8(SI1133_REG_COMMAND, SI1133_START);

   Serial.println(Wire.read());
   return true;
}

void SI1133::reset() {
  write8(SI1133_REG_COMMAND, SI1133_RESET_SW);
  delay(10);  
}

uint8_t SI1133::read8(uint8_t reg) {
    Wire.beginTransmission(_addr);
    Wire.write((uint8_t)reg);
    Wire.endTransmission();
    Wire.requestFrom((uint8_t)_addr, (uint8_t)1);  
    return Wire.read();
}

uint16_t SI1133::read16(uint8_t a) {
  uint16_t ret;
  Wire.beginTransmission(_addr); 
  Wire.write(a); 
  Wire.endTransmission(); // complete transmission
  Wire.requestFrom(_addr, (uint8_t)2);// 
  ret = Wire.read(); // 
  ret |= (uint16_t)Wire.read() << 8; //
  return ret;
}
void SI1133::write8(uint8_t reg, uint8_t val) {

  Wire.beginTransmission(_addr); //
  Wire.write(reg); //
  Wire.write(val); // 
  Wire.endTransmission(); // fin
}

/*********************************************************************/

uint8_t SI1133::writeParam(uint8_t p, uint8_t v) {
  //Serial.print("Param 0x"); Serial.print(p, HEX);
  //Serial.print(" = 0x"); Serial.println(v, HEX);
  
  write8(SI1133_REG_HOSTIN0, v);
  write8(SI1133_REG_COMMAND, p | SI1133_PARAM_SET);
  return read8(SI1133_REG_RESPONSE1);
}

uint8_t SI1133::readParam(uint8_t p) {
  write8(SI1133_REG_COMMAND, p | SI1133_PARAM_QUERY);
  return read8(SI1133_REG_RESPONSE1);
}

/*********************************************************************/

uint32_t SI1133::readUV(void) {
	uint32_t temp;
	temp=read8(SI1133_REG_HOSTOUT0);
	temp<<=8;
	temp|=read8(SI1133_REG_HOSTOUT1);
 	return temp; 
}
uint32_t SI1133::readIR(void) {
	uint32_t temp;
	read8(SI1133_REG_HOSTOUT2);
	temp<<=8;
	temp|=read8(SI1133_REG_HOSTOUT3);
	temp<<=8;
	temp|=read8(SI1133_REG_HOSTOUT4);
 	return temp; 
}

uint32_t SI1133::printOut() {
	uint32_t temp = 0;
	temp = read8(SI1133_REG_HOSTOUT0);
  Serial.print("HOSTOUT0 : ");  Serial.println(temp);
	temp = read8(SI1133_REG_HOSTOUT1);
  Serial.print("HOSTOUT1 : ");  Serial.println(temp);
  temp = read8(SI1133_REG_HOSTOUT2);
  Serial.print("HOSTOUT2 : ");  Serial.println(temp);
  temp = read8(SI1133_REG_HOSTOUT3);
  Serial.print("HOSTOUT3 : ");  Serial.println(temp);
  temp = read8(SI1133_REG_HOSTOUT4);
  Serial.print("HOSTOUT4 : ");  Serial.println(temp);
  temp = read8(SI1133_REG_HOSTOUT5);
  Serial.print("HOSTOUT5 : ");  Serial.println(temp);
  temp = read8(SI1133_REG_HOSTOUT6);
  Serial.print("HOSTOUT6 : ");  Serial.println(temp);
  temp = read8(SI1133_REG_HOSTOUT7);
  Serial.print("HOSTOUT7 : ");  Serial.println(temp);
  temp = read8(SI1133_REG_HOSTOUT8);
  Serial.print("HOSTOUT8 : ");  Serial.println(temp);
  temp = read8(SI1133_REG_HOSTOUT9);
  Serial.print("HOSTOUT9 : ");  Serial.println(temp);
 	return temp; 
}

SI1133.h

C/C++
#if (ARDUINO >= 100)
 #include "Arduino.h"
#else
 #include "WProgram.h"
#endif
#include <Wire.h>

//VALORES
//============================
#define RATE_SHORT  0X60 //24.4us
#define RATE_NORMAL 0X00 //48.8us
#define RATE_LONG   0X20 //97.6us
#define RATE_VLONG  0X40  //195us
//============================
#define BITS_24 0X00
#define BITS_16 0X40
//============================
#define COUNT0 0X40
#define COUNT1 0X80
//===========================




//PHOTODIODOS
//===========================
#define F_SMALL_IR  0X00 
#define F_MEDIUM_IR 0X01
#define F_LARGE_IR  0X02
#define F_WHITE     0X0B
#define F_LARGE_WHITE   0X0C
#define F_UV    0X18
#define F_UV_DEEP   0X19
//=============================

/* COMMANDS */

#define SI1133_RESET_CMD_CTR  0X00  
#define SI1133_RESET_SW 0X01
#define SI1133_FORCE  0X11
#define SI1133_PAUSE  0X12
#define SI1133_START  0X13
#define SI1133_PARAM_QUERY  0X40
#define SI1133_PARAM_SET  0X80

/* REGISTERS */

#define SI1133_REG_PARTID 0X00  //IN
#define SI1133_REG_REVID  0X01  //IN 
#define SI1133_REG_MFRID  0X02  //IN
#define SI1133_REG_INFO0  0X03  //IN
#define SI1133_REG_INFO1  0X04  //IN
#define SI1133_REG_HOSTIN3  0X07
#define SI1133_REG_HOSTIN2  0X08
#define SI1133_REG_HOSTIN1  0X09
#define SI1133_REG_HOSTIN0  0X0A
#define SI1133_REG_COMMAND  0X0B
#define SI1133_REG_IRQ_ENABLE 0X0F
#define SI1133_REG_RESPONSE1  0X10
#define SI1133_REG_RESPONSE0  0X11
#define SI1133_REG_IRQ_STATUS 0X12
#define SI1133_REG_HOSTOUT0   0X13
#define SI1133_REG_HOSTOUT1   0X14
#define SI1133_REG_HOSTOUT2   0X15
#define SI1133_REG_HOSTOUT3   0X16
#define SI1133_REG_HOSTOUT4   0X17
#define SI1133_REG_HOSTOUT5   0X18
#define SI1133_REG_HOSTOUT6   0X19
#define SI1133_REG_HOSTOUT7   0X1A
#define SI1133_REG_HOSTOUT8   0X1B
#define SI1133_REG_HOSTOUT9   0X1C
#define SI1133_REG_HOSTOUT10  0X1D
#define SI1133_REG_HOSTOUT11  0X1E
#define SI1133_REG_HOSTOUT12  0X1F
#define SI1133_REG_HOSTOUT13  0X20
#define SI1133_REG_HOSTOUT14  0X21
#define SI1133_REG_HOSTOUT15  0X22
#define SI1133_REG_HOSTOUT16  0X23
#define SI1133_REG_HOSTOUT17  0X24
#define SI1133_REG_HOSTOUT18  0X25
#define SI1133_REG_HOSTOUT19  0X26
#define SI1133_REG_HOSTOUT20  0X27
#define SI1133_REG_HOSTOUT21  0X28
#define SI1133_REG_HOSTOUT22  0X29
#define SI1133_REG_HOSTOUT23  0X2A
#define SI1133_REG_HOSTOUT24  0X2B
#define SI1133_REG_HOSTOUT25  0X2C

/* Parameters */

#define SI1133_PARAM_I2CADDR   0X00
#define SI1133_PARAM_CHLIST   0X01
#define SI1133_PARAM_ADCCONFIG0 0X02
#define SI1133_PARAM_ADCSENS0   0X03
#define SI1133_PARAM_ADCPSOT0   0X04
#define SI1133_PARAM_MEASCONFIG0  0X05
#define SI1133_PARAM_ADCCONFIG1   0X06
#define SI1133_PARAM_ADCSENS1   0X07
#define SI1133_PARAM_ADCPSOT1   0X08
#define SI1133_PARAM_MEASCONFIG1  0X09
#define SI1133_PARAM_ADCCONFIG20  0X0A
#define SI1133_PARAM_ADCSENS2   0X0B
#define SI1133_PARAM_ADCPSOT2   0X0C
#define SI1133_PARAM_MEASCONFIG2  0X0D
#define SI1133_PARAM_ADCCONFIG3 0X0E
#define SI1133_PARAM_ADCSENS3   0X0F
#define SI1133_PARAM_ADCPSOT3   0X10
#define SI1133_PARAM_MEASCONFIG3  0X11
#define SI1133_PARAM_ADCCONFIG4   0X12
#define SI1133_PARAM_ADCSENS4   0X13
#define SI1133_PARAM_ADCPSOT4   0X14
#define SI1133_PARAM_MEASCONFIG4  0X15
#define SI1133_PARAM_ADCCONFIG5   0X16
#define SI1133_PARAM_ADCSENS5   0X17
#define SI1133_PARAM_ADCPSOT5   0X18
#define SI1133_PARAM_MEASCONFIG5  0X19
#define SI1133_PARAM_MEASRATEH  0X1A
#define SI1133_PARAM_MEASRATEL  0X1B
#define SI1133_PARAM_MEASCOUNT0   0X1C
#define SI1133_PARAM_MEASCOUNT1   0X1D
#define SI1133_PARAM_MEASCOUNT2   0X1E
#define SI1133_PARAM_THRESHOLD0_H   0X25
#define SI1133_PARAM_THRESHOLD0_L   0X26  
#define SI1133_PARAM_THRESHOLD1_H   0X27
#define SI1133_PARAM_THRESHOLD1_L   0X28
#define SI1133_PARAM_THRESHOLD2_H   0X29
#define SI1133_PARAM_THRESHOLD2_L   0X2A
#define SI1133_PARAM_BURST   0X2B

#define SI1133_ADDR 0x55

class SI1133  {
 public:
  SI1133(void);
  boolean begin();
  void reset();
  uint32_t readUV();
  uint32_t readIR();
  uint32_t printOut();


 private:
  uint16_t read16(uint8_t addr);
  uint8_t read8(uint8_t addr);
  void write8(uint8_t reg, uint8_t val);
  uint8_t readParam(uint8_t p);
  uint8_t writeParam(uint8_t p, uint8_t v);
  
  uint8_t _addr;
};

Si7020.cpp

C/C++
Temperature , humidity sensor
/* Arduino Si7010 relative humidity + temperature sensor
 * Copyright (C) 2014 by Jakub Kaminski
 *
 * This file is part of the Arduino Si7020 Library
 *
 * This Library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This Library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the Arduino Si7020 Library.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 * TODO: implement checksum checking
 */

#include "Si7020.h"

Si7020::Si7020(){}

void Si7020::begin()
{
	Wire.begin();
}

float Si7020::getRH()
{
	// Measure the relative humidity 
	uint16_t RH_Code = makeMeasurment(RH_NOHOLD);
	float result = (125.0*RH_Code/65536)-6;
	return result;
}

float Si7020::readTemp()
{
	// Read temperature from previous RH measurement.
	uint16_t temp_Code = makeMeasurment(TEMP_PREV);
	float result = (175.25*temp_Code/65536)-46.85;
	return result;
}

float Si7020::getTemp()
{
	// Measure temperature 
	uint16_t temp_Code = makeMeasurment(TEMP_NOHOLD);
	float result = (175.25*temp_Code/65536)-46.85;
	return result;
}

void Si7020::heaterOn()
{
	// Turns on the Si7020 heater
	uint8_t regVal = readReg();
	regVal |= _BV(HTRE);
	//turn on the heater
	writeReg(regVal);
}

void Si7020::heaterOff()
{
	// Turns off the Si7020 heater
	uint8_t regVal = readReg();
	regVal &= ~_BV(HTRE);
	writeReg(regVal);
}

void Si7020::changeResolution(uint8_t i)
{
	// Changes to resolution of Si7020 measurements.
	// Set i to:
	//      RH         Temp
	// 0: 12 bit       14 bit (default)
	// 1:  8 bit       12 bit
	// 2: 10 bit       13 bit
	// 3: 11 bit       11 bit

	uint8_t regVal = readReg();
	// zero resolution bits
	regVal &= 0b011111110;
	switch (i) {
	  case 1:
	    regVal |= 0b00000001;
	    break;
	  case 2:
	    regVal |= 0b10000000;
	    break;
	  case 3:
	    regVal |= 0b10000001;
	  default:
	    regVal |= 0b00000000;
	    break;
	}
	// write new resolution settings to the register
	writeReg(regVal);
}

void Si7020::resetSettings()
{
	//Reset user resister
	writeReg(RESET_SI);
}

uint16_t Si7020::makeMeasurment(uint8_t command)
{
	// Take one Si7020 measurement given by command.
	// It can be either temperature or relative humidity
	// TODO: implement checksum checking
	
	uint16_t nBytes = 3;
	// if we are only reading old temperature, read olny msb and lsb
	if (command == 0xE0) nBytes = 2;

	Wire.beginTransmission(SI7020);
	Wire.write(command);
	Wire.endTransmission();
	// When not using clock stretching (*_NOHOLD commands) delay here 
	// is needed to wait for the measurement.
	// According to datasheet the max. conversion time is ~22ms
	 delay(100);
		
	Wire.requestFrom(SI7020,nBytes);
	//Wait for data
	int counter = 0;
	while (Wire.available() < nBytes){
	  delay(1);
	  counter ++;
	  if (counter >100){
	    // Timeout: Sensor did not return any data
	    return 100;
	  }
	}

	unsigned int msb = Wire.read();
	unsigned int lsb = Wire.read();
	// Clear the last to bits of LSB to 00. 
	// According to datasheet LSB of RH is always xxxxxx10
	lsb &= 0xFC;
	unsigned int mesurment = msb << 8 | lsb;

	return mesurment;
}

void Si7020::writeReg(uint8_t value)
{  
	// Write to user register on Si7020
	Wire.beginTransmission(SI7020);
	Wire.write(WREG);
	Wire.write(value);
	Wire.endTransmission();
}

uint8_t Si7020::readReg()
{
	// Read from user register on Si7020
	Wire.beginTransmission(SI7020);
	Wire.write(RREG);
	Wire.endTransmission();
	Wire.requestFrom(SI7020,1);
	uint8_t regVal = Wire.read();
	return regVal;
}

Si7020.h

C/C++
/* Arduino Si7010 relative humidity + temperature sensor
 * Copyright (C) 2014 by Jakub Kaminski
 *
 * This file is part of the Arduino Si7020 Library
 *
 * This Library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This Library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the Arduino Si7020 Library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

// There are some differences in libraries between Arduino and Spark
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#include "Wire.h"
#elif defined(SPARK)
#include "application.h"
#define _BV(bit) (1 << (bit))
#endif

#ifndef Si7020_h
#define Si7020_h

#define SI7020      0x40
#define RH_HOLD     0xE5
#define RH_NOHOLD   0xF5
#define TEMP_HOLD   0xE3
#define TEMP_NOHOLD 0xF3
#define TEMP_PREV   0xE0
#define RESET_SI    0xFE
#define WREG        0xE6
#define RREG        0xE7
#define HTRE        0x02 

class Si7020
{
	public:
		Si7020();
		void  begin();
		float getRH();
		float readTemp();
		float getTemp();
		void  heaterOn();
		void  heaterOff();
		void  changeResolution(uint8_t i);
		void  resetSettings();
	private:
		uint16_t makeMeasurment(uint8_t command);
		void     writeReg(uint8_t value);
		uint8_t  readReg();
};

#endif

Credits

Dimiter Kendri

Dimiter Kendri

23 projects • 151 followers
Robotics and AI
Thanks to Jakub Kaminski.

Comments