Skip to content

Logging environmental data using a I2C sensor on a SBC

Michiel TJampens edited this page Sep 22, 2021 · 2 revisions

Introduction

The end goal is to have the data logged on a database running on the Nano Pi and showing it all with Grafana.

NOTE: This is not compatible with the current version (0.11.0)

Dcafs features used

Used hardware

  • The BME280 measures barometric pressure, humidity and ambiant temperature.
  • This is connected to a Nano Pi Air running Armbian Buster but any other SBC will do.

The pinout of the Nano pi looks like this:

So the pins used are:

  • Pin 1: 3V3
  • Pin 3: I2C0_SDA
  • Pin 5: I2C0_SCL
  • Pin 6: GND

Used software

  • InfluxDB (I followed this guide but pick bmedata as database name)
  • Grafana (I followed this guide)
  • Putty (or ssh/telnet command in linux)
  • Winscp to copy from windows to SBC
  • AdoptOpenJDK11 and select java11 and appropriate arch, you must use JDK for some reason JRE doesn't work
  • dcafs 0.8.3 or newer

Note: On a 500MB ram sbc, InfluxDB will use about 21% ram while MariaDB and dcafs will each use 12% and grafana around 9%. So i memory is tight Mariadb might be a better option. Grafana doesn't support SQLiteDB so that's not an option.

From this point onwards i assume InfluxDB, Grafana, JDK11 are installed on the SBC and optionally putty and Winscp on a windows host machine.

Initial setup

  1. Start an SSH session to the SBC or connect to it via Winscp (in windows)
  2. In the home folder create a folder names 'scripts' and another on 'java'
  • scripts: hold the bash script that starts dcafs on boot and will be executed by crontab
  • java: contains the (various) project(s) using dcafs and sometimes the JDK/jre
  1. Copy the extracted dcafs release to the java folder and move into it on the shell
  2. Run the jar using 'sudo java -jar dcafs-0.*' a lot of text should start appearing (sudo is needed to host the telnet server) and finally DAS.main Barebone boot finished!
  3. Log into the telnet session using putty or telnet command (default port).

Setting up dcafs

Connecting to the BME280

  1. You can get an overview of all the i2c related commands with i2c:?

  2. First we need to be sure that the device is detected on the controller, in my case this is 0.
    i2c:detect,0
    0x76

  3. Given that we know nothing else is connected to this bus and the address matches the datasheets options, next up is registering it. The command for that is i2c:adddevice,id,bus,address,script in our case:

  • id = bme -> this will be used later to refer to it
  • bus = 0 -> because that's the bus it's on
  • address = 0x76 -> this is the number found with the previous command
  • script = bme280 -> this is the name for the script that will hold all the communication with the chip (and will be created in the scripts subfolder) i2c:adddevice,bme,0,0x76,bme280
    Device added, created blank script at devices/bme280.xml
    If there would be multiple BME280's connected, they all share the same script. To check if this worked i2c:list
bme -> @0:0x76  using script bme280 with label bme280

-Stored Scripts-
bme280
        cmdname -> what this does (8bits)

After this, your settings.xml should look like this:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<das>
  <settings>
    <mode>normal</mode>
    <!-- Settings related to the telnet server -->
    <telnet port="23" title="DAS">
      <ignore/>
    </telnet>
    <i2c>
      <bus controller="0">
        <device address="0x76" id="bme" script="bme280"/>
      </bus>
    </i2c>
  </settings>
  <streams>
    <!-- Defining the various streams that need to be read -->
  </streams>
</das>

A file called bme280.xml will be generated in a devices subfolder with the following content:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<commandset script="bme280">
  <!-- An empty command to start with -->
  <command id="cmdname" info="what this does"/>
</commandset>

Interacting with the BME280

  1. Normally this step involves reading the datasheet and figuring the communication out, but we'll skip it. The full bme280 script will be given at the end, but let's limit to temperature for now.

Alter the bme280.xml to look like this

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<commandset script="bme280">
<!-- Read the temperature data -->
	<command id="calc_temp" info="Read the temperature registers."  bits="16">
		<read reg="0xFA" bits="20" return="3"/>
		<read reg="0x88" msbfirst="false" return="2"/> <!-- T1: unsigned 16bit -->
		<read reg="0x8A" msbfirst="false" signed="true" return="4"/> <!-- T2 to T3: signed 16bit -->
	</command>
</commandset>

The above adds the following info.

  • command attribute bits="16" -> All read operations in this command will return 16bits numbers by default
  • read node -> execute a read operation on the device
    • reg="0xFA" -> the register to read is 0xFA
    • msbfirst="false" -> the first byte read is the lsb
    • bits="20" -> the amount of bits expected in the answer because it doesn't match the default
    • signed="true" -> whether or not the received data is signed
    • return "3" -> the amount of bytes that ought to be received

Note: Any attribute in the command node counts for all read nodes, unless overriden by a read node, so you could add signed="true" to the command node and remove all occurences of this in the read nodes. But this depends on personal preference.

  1. Reload the script to apply it i2c:reload All files (1) read ok.

  2. Before we can see the result of the command, we must request the data from the device to be shown in telnet i2c:forward,bme (-> Forward the data received from the i2c device with the id bme to whoever issued this command) Added forward

  3. Run the command
    i2c:bme,calc_temp (-> Check the script associated with the device with id bme for a command with id calc_temp and run it) Command added to the queue.
    And some time later...
    bme;calc_temp;518800;28350;26864;50 (-> default output of a read is deviceid;commandid;values read according to set bits/signed/return)

So now dcafs can interact with the sensor, but we'd want a continuous stream of data...

Polling values on an interval

  1. For this we'll create a blank taskmanager using
    tm:addblank,scheduler (-> Create a taskmanager with the id scheduler and a default xml script in scripts subfolder called scheduler.xml) Tasks script created, use tm:reload,scheduler to run it.

  2. This part doesn't have a telnet interface yet, so we'll have to edit the file directly. Look in the scripts subfolder for scheduler.xml and open it.

  3. You'll see that it contains a lot of info for a first time use and some examples, either read it or just delete all comments and the taskset node.

  4. Alter the task

<task output="system" trigger="delay:1s">taskset:example</task> 
<!-- output="system" means the value is a command -->
<!-- trigger="delay:1s" means one sec after this task is called (which is during startup of the taskmanager -->
<!-- taskset:example means it will run the taskset with the id example -->
# to
<!-- output stays the same because it should run a command -->
<!-- trigger should be an interval, normally this would be 5min of more, but for the guide we'll take 5s -->
<!-- i2c:bme,calc_temp is the same command used earlier to read the temp registers -->
<task output="system" trigger="interval:5s">i2c:bme,calc_temp</task>  
<!-- Note that interval can take two periods, an initial delay and an interval by default the initial delay is the interval is not specified -->
  1. Back in telnet, use the earlier suggested command
    tm:reload,scheduler
    Tasks:reloaded
  2. If nothing else happens, then the forward isn't active anymore. To reactive
    i2c:forward,bme

That's it, now every five seconds the chip is interrogated for the temperature info. Next step is converting this to actual temperature.

Converting to degrees celsius

  1. This is done using the MathForward feature
  2. In telnet, to create a blank math mf:addblank,id,source so...
    mf:addblank,bmet,i2c:forward,bme (-> Creates a mathforward with id bmet and i2c:forward,bme as source)
    Blank math with id bmet created.
  3. Some settings can be altered through commands:
    mf:bmet,alter,delim:; (-> Change the delimiter to ; from the default ,)
    mf:bmet,alter,label:generic:bmet (-> Set the label to use a generic with the id bmet, the label determines what is done with the data afterwards)
  4. You could add the operations with a command, but it's easier to just edit the settings.xml
<maths>
    <!-- the delimiter is used to convert the dataline into an array, so the first element is at index0 and is referred to with i0, then i1 and so on -->
    <math id="bmet" delimiter=";" label="generic:bmet" src="i2c:forward,bme">
      <operations> <!-- add this operations block -->
        <op cmd="maths:scratchpad,*,$" index="2">( (i2/16384-i3/1024)*i4 + ((i2/131072-i3/8192)*(i2/131072-i3/8192))*i5)</op>
        <!-- The cmd attribute allows a command to be run with the $ replaced with the result of the operation, in this case
             It passes the semi raw temperature to the other math instances (*=all other), this is available as o0 in those math -->
        <op index="2">i2/5120</op>
        <op index="2" type="scale">2</op> <!-- scale to only have two digits -->
      </operations>
    </math>
</maths>
  1. Reload the math mf:reload,bmet
    Math reloaded

  2. Now to see the result in telnet math:bmet
    Request for math:bmet ok.

  3. Now every 5 seconds the following will be shown, the first line is the raw and the second one has the result of the math bme;calc_temp;518416;28350;26864;50 bme;calc_temp;20.76;28350;26864;50

Storing the data in a database

  1. Create a connection to the earlier set up influx db
    dbm:addinflux,influx,bmedata,localhost,user:pass
  2. Earlier we mentioned the use of a generic, now it will be created. Given that the format of data is temperature at index 2.
    gens:addblank,bmet,ssr (-> Add a blank generic with the id bmet and we dont care about the first two array elements (s=skip) but third one is a real)

Look in the settings.xml for the generics node and alter accordingly

  <generics>
    <generic delimiter=";" id="bmet" influx="influx"> <!-- add influx="influx" attribute to link it to the database -->
      <real index="2">temperature</real> <!-- replace the . here with temperature -->
    </generic>
  </generics>
  1. Reload the generic with gens:reload

That's it. Now the database should slowly start receiving data.
This can be checked with st because this requests the current status of dcafs.

Full settings.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<das>
  <!-- In  here telnet, transserver, databases etc are defined -->
  <settings>
    <!-- Mode dcafsis running in, options are: normal,debug and log. -->
    <mode>normal</mode>
    <databases>
	  <server id="flux" type="influxdb">
                <db pass="pass" user="user">bmedata</db>
		<address>localhost</address>
	  </server>
    </databases>
    <!-- Databases can be defined here -->
    <!-- Settings related to the telnet server -->
    <telnet port="23" title="BMEreader">
      <ignore/>
    </telnet>
    <i2c>
      <bus controller="0">       
        <device address="0x76" id="bme" label="void" script="bme280"/>
      </bus>
    </i2c>
    <taskmanager id="scheduler">scripts/scheduler.xml</taskmanager>
  </settings>
  <filters>
    <filter id="bmet" src="i2c:forward,bme" type="contains">calc_temp</filter>
    <!-- get the data regarding temperature -->
    <filter id="bmeh" src="i2c:forward,bme" type="contains">calc_hum</filter>
    <!-- get the data regarding pressure    -->
    <filter id="bmep" src="i2c:forward,bme" type="contains">calc_pressure</filter>
    <!-- get the data regarding humidity    -->
  </filters>
  <maths>
    <!-- Some info on what the test math does -->
    <math id="bmet" label="generic:bmet" src="filter:bmet">
      <!-- works -->
      <operations>
        <op giveto="maths:scratchpad,*,$" index="2">( (i2/16384-i3/1024)*i4 + ((i2/131072-i3/8192)*(i2/131072-i3/8192))*i5)</op>
        <!-- i5 = max -> 6 -->
        <op index="2">i2/5120</op>
        <!-- i2 = max -> 3 -->
        <op index="2" type="scale">2</op>
      </operations>
    </math>
    <math id="bmeh" label="generic:bmeh" src="filter:bmeh">
      <operations>
        <op index="2">((i2 - (i6*64 + i7/16384*(o0 - 76800)))*(i4/65536*(1+i8/67108864*(o0 - 76800)* (1+i5/67108864*(o0 - 76800)))))</op>
        <op index="2">i2*(1-i3*i2/524288)</op>
        <op index="2" type="scale">2</op>
      </operations>
    </math>
    <math id="bmep" label="generic:bmep" src="filter:bmep">
      <!-- works -->
      <operations>
        <op index="6">((o0/2 -64000)^2*i8/32768 + (o0/2 -64000)*i7*2)/4 + (i6*65536.0)</op>
        <op index="3">(1 + ((i5*(o0/2 -64000)^2/524288 + i4*(o0/2 -64000))/524288)/32768.0)*i3</op>
        <op index="2">1048576 - i2</op>
        <op index="2">(i2-(i6/4096)) * 6250.0/i3</op>
        <op index="2">i2 + ( i11*i2*i2/2147483648.0 + i2*i10/32768 + i9)/16</op>
        <op index="2">i2/100</op>
        <!-- convert to hPa -->
        <op index="2" type="scale">2</op>
      </operations>
    </math>
  </maths>
  <generics>
    <!-- Where the generics are defined -->
    <generic delimiter=";" id="bmet" influx="flux:temps">
      <real index="2">temperature</real>
    </generic>
    <generic delimiter=";" id="bmeh" influx="flux:hums">
      <real index="2">humidity</real>
    </generic>
    <generic delimiter=";" id="bmep" influx="flux:press">
      <real index="2">pressure</real>
    </generic>
  </generics>
</das>

Full bme280.xml script

<commandset script="bme280">

	<!-- Read all the data/cal registers from the device -->
	<command id="read_hum" info="Read the humidity registers." bits="16">
		<read reg="0xFD" return="2"/>
	</command>
	<command id="read_temp" info="Read the temperature registers." bits="20">
		<read reg="0xFA" return="3"/>
		<!-- 0x7F 0xF5 0x00 so 0x00F57F -->
	</command>
	<command id="calc_temp" info="Read the temperature registers."  bits="16">
		<read reg="0xFA" bits="20" return="3"/>
		<read reg="0x88" msbfirst="false" return="2"/> <!-- T1: unsigned 16bit -->
		<read reg="0x8A" msbfirst="false" signed="true" return="4"/> <!-- T2 to T3: signed 16bit -->
	</command>
	<command id="calc_hum" info="Read the humidity registers."  msbfirst="false">
		<read reg="0xFD" msbfirst="true" bits="16" return="2"/>
		<read reg="0xA1" bits="8"  signed="false"  return="1"/> <!-- H1: unsigned 8bit -->
		<read reg="0xE1" bits="16" signed="true"   return="2"/> <!-- H2: signed 16bit  -->
		<read reg="0xE3" bits="8"  signed="false"  return="1"/> <!-- H3: unsigned 8bit -->
		<read reg="0xE4" msbfirst="true" bits="12" signed="true"  return="2"/> <!-- H4: signed 12bit  -->
		<read reg="0xE5" bits="12" signed="true"   return="2"/> <!-- H5: humidity signed short, WRONG SHIFT-->
		<read reg="0xE5" bits="8"  signed="true"   return="1"/>  <!-- H6: humidity signed char -->
	</command>
	<command id="calc_pressure" info="Read the pressure registers."  bits="16" msbfirst="false">
		<read reg="0xF7" msbfirst="true" bits="20" return="3"/>
		<read reg="0x8E" signed="false" return="2"/> <!-- P1: unsigned 16bit-->
		<read reg="0x90" signed="true" return="16"/> <!-- P2 to P9: signed 16bit -->
	</command>
	<!--
		double var1 = (adc/16384 - comp[0]/1024) * comp[1];
        double var2 = (adc/131072 - (comp[0]/8192)) * (adc/131072.0 - comp[0]/8192) * comp[2];
        t_fine = var1 + var2;
        
        double T = Tools.roundDouble((t_fine)/5120,2);
		
		x0 = i0/16384 
		x1 = i1/1024
		x2 = x0 - x1
		x3 = x2 * i2
		
		x4 = i0/131072 - i1/8192
		x4 *= x4
		x5 = x4 * i3
		x6 = x3 + x5
		x7 = x6/5120
	-->
				

	<command id="read_press" info="Read the pressure registers." bits="24">
		<read reg="0xF7" return="3"/>
	</command>
	<command id="read_status" info="Read the status registers." bits="8">
		<read reg="0xF3" return="1"/>
	</command>
	<command id="read_ctrl" info="Read the ctrl registers." bits="8">
		<read reg="0xF2" return="1"/>
		<read reg="0xF4" return="1"/>
	</command>	
	<command id="read_coeff" info="Read the calibration values from the ic" msbfirst="false" bits="16">
		<!-- Temperature -->
		<!-- 48750; -3992 ; 12800 -->
		<read reg="0x88"  return="2"/> <!-- T1: unsigned 16bit -->
		<read reg="0x8A"  signed="true" return="4"/> <!-- T2 to T3: signed 16bit -->
		
		<!-- Pressure -->
		<!-- 16785 ; 32982 ; 53259 ; 56609 ; 51455 ; 63999 ; 44070 ; 2776 ; 48400 -->
		<read reg="0x8E" signed="true" return="2"/> <!-- P1: signed 16bit-->
		<read reg="0x90" signed="false" return="16"/> <!-- P2 to P9: unsigned 16bit -->
		
		<!-- Humidity -->
		<!-- 75 ; -30463 ; 0 ; 242 ; 688 -->
		<read reg="0xA1" bits="8"  signed="false" return="1"/> <!-- H1: unsigned 8bit -->
		<read reg="0xE1" bits="16" signed="true"  return="2"/> <!-- H2: signed 16bit  -->
		<read reg="0xE3" bits="8"  signed="false" return="1"/> <!-- H3: unsigned 8bit -->
		<read reg="0xE4" bits="12" signed="true"  return="2"/> <!-- H4: signed 12bit  -->
		<read reg="0xE5" bits="12" signed="true"  return="2"/> <!-- H5: humidity signed short, WRONG SHIFT-->		
	</command>
	
	<!-- Mode changes -->
	<!-- Recommended settings for specific situations -->
	<command id="weather" info="Set weather recommends (1 sample/min, no filter, no oversampling)">
		<write reg="0xF5" >0x00</write> <!-- standby dont care (forced mode), filter off, no spi 3w -->
		<write reg="0xF2" >0x01</write> <!-- Humidity oversampling -->
		<write reg="0xF4" >0x26</write> <!-- 001 001 10 -->
	</command>
	<command id="humidity" info="Set humidity recommends (1 sample/sec, no filter, no oversampling, no pressure)">
		<write reg="0xF5" >0x00</write> <!-- standby dont care (forced mode), filter off, no spi 3w -->
		<write reg="0xF2" >0x01</write> <!-- Humidity oversampling x1 -->
		<write reg="0xF4" >0x22</write> <!-- temp x1, press x1, forced -->
	</command>
	<command id="indoornav" info="Set indoor nav recommends (normal mode, tstandby=0.5ms,pressure x16,temp x2, hum x1, filter 16)">
		<write reg="0xF5" >0x10</write> <!-- standby 0.5ms, filter 16, no spi 3w -->
		<write reg="0xF2" >0x01</write> <!-- Humidity oversampling x1 -->
		<write reg="0xF4" >0x57</write> <!-- temp x2=010, press x16=101, normal = 11 -->
	</command>
	
	<!-- General mode setting -->
	<command id="readmode" info="Read mode register">
		<read reg="0xF4" return="1"/>
	</command>	
	<command id="start" info="Start the sensors acc. at x1 oversampling" >
		<write reg="0xF2" >0x01</write> <!-- -->
		<write reg="0xF4" >0x27</write> <!-- oversampling x1 and active -> 0010 0111 -> 0x27 -->
	</command>	
	<command id="sleepmode" info="Go to sleep mode">
		<alter reg="0xF4" operand="and">0xFC</alter> <!-- Default:0x00 xxxx xxxx AND 0xFD = xxxx xx 00 -->
	</command>
	<command id="reset" info="Reset the ic" >
		<write reg="0xE0" >0xB6</write> <!-- -->		
	</command>	
</commandset>
<!--
0xF2 - ctrl hum
							humidity oversampling
		reserved[4:0]		osr_h[2:0]
		0000 0				000 = skipped			
							001 = oversampling x1
							010 = oversampling x2
							011 = oversampling x4
							100 = oversampling x8
							101 = oversampling x16

		
0xF4 - ctrl meas
		osrs_t[2:0]					osrs_p[2:0]					mode[1:0]
		temperaure oversampling		pressure oversampling		00 = sleep mode
		000 = skipped				000 = skipped				01 = forced mode
		001 = oversampling x1		001 = oversampling x1		10 = forced mode
		010 = oversampling x2		010 = oversampling x2		11 = normal mode
		011 = oversampling x4		011 = oversampling x4
		100 = oversampling x8		100 = oversampling x8
		101 = oversampling x16		101 = oversampling x16
0xF5 - config
	   t_sb[2:0] 			filter[2:0]				spi3w_en[0]
	-> 000 = 0,5ms			000 = filter off		0 = spi 3wire disabled
	-> 001 = 62.5ms			001 = 2					1 = spi 3wire enabled
	-> 010 = 125ms			010 = 4
	-> 011 = 250ms			011 = 8
	-> 100 = 500ms			1xx = 16
	-> 101 = 1000ms
	-> 110 = 10ms
	-> 111 = 20ms

Temperatue ADC data
0xFA = MSB
0xFB = LSB
0xFC = xLSB			
-->
Clone this wiki locally