Experimenting with a Capacitive Levelmeter

Place code snippets and demo code here
Post Reply
User avatar
Electroguard
Posts: 836
Joined: Mon Feb 08, 2021 6:22 pm
Has thanked: 268 times
Been thanked: 317 times

Experimenting with a Capacitive Levelmeter

Post by Electroguard »

Another nice example that is worth keeping...

(by mcguinn)
I was looking for a way to use Annex to measure waterlevels, and that with as little extra hardware as possible. I ended up using pwm and analogue voltage measurement. If a probe is used with enough capacitance swing, it is accurate enough for home-use

Details and schematic are in the code.

Constructing a capacitive probe doesn't have to be too complicated: I used 2 copperfoil strips in a laminating pouch.
When you have to monitor levels of more than the size of such a pouch, you will have to be creative with, for instance, metal strips and heat-shrinkable tube.

With the dimesioned 27k resistor the capacitance (when fully immersed) must be about 150 pf or over. Much bigger is much better.
Expect a jitter of about 4 % (less with higher capacitance). Twisted pair leads, not too long, can be used to connect the probe(s)

After starting the program begin with adding a new probe. Then open the setup for that probe and fill in the particulars.
If no valid pwm pinnumber is supplied (ie <0), then the program works in demo-mode.
When calibrating a probe (learning what is 100% and what is 0%) the hintbox shows the steps to take.

I think it can also be used to monitor the moisturelevel in a flowerpot, but I'm not sure about how water is distributed in the soil.
Or use it to detect waterspillage on the floor.

On the pictures:
One shows the laminated probes, the others show a rough test-setup.
The bottle was previously calibrated on 100% bottle is 100% probe. Then it is filled up at 25%, 50%, 75% and 100%.

Hope it's useful for someone out there.

Greetings
mcGuinn

setuppage.jpg
levelmeters.jpg
E_laminatedProbes.jpg
all4.jpg

Code: [Local Link Removed for Guests]

'capacitive-probe fluidlevel meters
'can show up to 5 levelmeters indicating the amount of fluid in a tank
'intended for watery fluids.

'Written by Mcguinn 07/2020 using Annex version 1.41.beta 6. Tested on Wemos D1 Mini esp8266
'Free to use. No warranties given. Only intended as an example.

'A probe here acts as a capacitor whose value changes with the amount of water covering the plates.
'A squarewave charges and discharges this capacitor through a resistor. A peakdetector feeds the resulting voltage to the adc.
'When calibrating a probe, immersed to the maximum level, the program looks for a frequency and dutycycle that result in a certain adc reading. 
'After that, with these parameters, the adc is read with the probe immersed to the minimum level.
'The values read during normal operation are converted to a percentage of minimum-maximum, and shown by an html meter-element.
'When reaching a certain percentage, actions can be executed. eg switching on a pump or sounding an alarm
'Please note that readings are not linear, but close enough for a fair indication.
'The probe circuit is VERY sensitive to jittery contacts. If you build it on a breadboard, expect jittery readings.

'schematic for probe(s):
'              _____            1n4148      A0  ______
'pwm(pin) o---[_27K_]------|-----[>|--x--|-o---|_220k_|---|--------o  adc
'                         _|_           _|_              ---
'                 probe   ---           --- 1uf          | | 100k
'                          |             |               | |
'                          |             |               ---
'                          |__________x__|________________|________o gnd 
'
'to the left of x, 1 probe. Other probes connected in parallel on the x points, Voltage devider (220k/100k) is incorporated in D1 mini
'about actions:
    'Actions are short pieces of annex code typed into a textfield; they can be saved.
    'Action fields may be empty or filled with commands that are executed using the COMMAND directive
    'for example to switch on a pump when reaching a high level, and switching the pump off when reaching a safe level
    'redaction  PUMP=13: PIN.MODE PUMP,OUTPUT:PIN(PUMP)=1
    'optimumaction PUMP=13:PIN.MODE PUMP,OUTPUT:PIN(PUMP)=0
    'for more complex actions: write it out in programlines as a subroutine under a label.
    'In the actionline put: GOSUB <label>
    '(because the action is executed by COMMAND, only simple commands can be used, no jumps,loops,functions, if then, pause and such
    'therefore if the action begins with GOSUB it is handled in a different way)
    'furthermore: if you want to use a literal string, enclose it in || instead of "" (otherwise websocket complains)
    'to show for example an alert message as an action, put in:     jscall |alert('your warningtext')|
   
    'Actions are executed once and reset (prepared for next execution) when reaching a certain level (even if that actionlevel has no action assigned to it)
    'Optimum resets orange and red actions, orange cq red reset optimum action
    'an empty actionfield means 'no action' and it's actionstatus will be 0, otherwise 1.
    'When an action is executed, the actionstatus is changed from 1 to -1, and on reset back to 1
    
    'About optimum, high, low:
    'The meter-element normally has a green bar indicating the value between min and max. here 0..100 (percentage)
    'It can also have a low, high and optimum value that are used to control the bar-colour from green to orange to red
    'The colorchange can be in the lower values region or in the higher values region.
    'When monitoring a falling level: value>high=green; value<=high=orange; value<low=red. requires optimum>high
    'When monitoring a rising level: value<low=green; value>=low=orange; value>high=red. requires optimum<low
    'when these 3 values are all 0 there will be no colorchange and no actions will be executed
    
maxprobes=5' if increased also create subroutines setupN, accordingly
usedprobes=0'number of defined probes

'for main webpage
dim filename$(maxprobes)'1 file per probe
dim reading$(maxprobes)'to indicate which probe is being processed
dim full$(maxprobes)' if probe reads maximum or above, has the word 'Full'
dim meter(maxprobes)'has the percent value for the level-meter
dim empty$(maxprobes)'if probe reads minimum or below, has the word 'Empty'
dim percentage(maxprobes)'the percentage for the probe. can show below 0 or over 100
dim litre(maxprobes)'if a coversion quantity is given, shows the measured volume of liquid in litres
dim actions$(maxprobes)'shows the actionstatus of the 3 actions (0, 1 or -1)
defbackcolour$="lightgreen"
defbordercolour$="green"
pollintervallist$="0,5,30,60,300,600"'time between the end of a series of measurements and the start of the next series. 0=off
pollinterval$=word$(pollintervallist$,1,",")
newtime$=""'when not connected to the internet, the systemtime can be input
systemtime$=""'shows systemtime

'various variables
dummy=0
dummy$=""
pollprobenr=0'probenr currently being processed
demoactive=0'if no defined probe has been assigned to a valid pwm pin. the program will run in demo mode.
timer0interval=1000
buttonsvisible=0
pollinterval=val(pollinterval$)
pollrunning=0
polldowncounter=0
probename$="/probes/probe"'basename for probefiles
pollintervalname$="/probes/pollinterval"'last set pollinterval is saved in this file
wwwconnected=ping ("8.8.8.8")'if not on the internet, systemtime can be input

'to keep track of actions to perform
'0= no action, 1=to perform when level reaches set point, -1=action has been performed
dim orangeaction(maxprobes)
dim redaction(maxprobes)
dim optimumaction(maxprobes)

'probefile data
caption$=""'informative text
backcolour$=""'for quick recognition
lowperc=0'level where meter changes colour
highperc=0'see above
optimumperc=0'pivot value that determines monitoring rising or falling level (high level orange/red or low level orange/red)
orangeaction$=""'action to perform when level reaches orange point
redaction$=""'action to perform when level reaches red point
optimumaction$=""'action to perform when reaching safe point
fulllitres=0'if <>0, coversion of percentage to litres
pwmgpionr=-1'pin to use for measuring. <0 is not valid/active
'following values filled by probe calibration
pwmfreq=0'the frequency of the pwm signal
maximumadc=0'adc value when probe immersed to maximum level
minimumadc=0'adc value when probe immersed to minimum level
pwmdutycycle=0'dutycycle of the pwm signal

'for setup webpage
setupprobenr=0
hint$=""' shows steps to take when calibrating
calibratestepnr=0'step to perform when calibrate is clicked
backcolourlist$="lightblue,mediumseagreen,mediumvioletred,lightslategray,lightgreen,orange,red,sandybrown,sienna,tan"'colors to choose
pausetime=500'timepause between consecutive measurements

'if a pollinterval was set earlier, use it
if file.exists(pollintervalname$)=1 then 
  pollinterval$=file.read$(pollintervalname$)
  changeinterval
end if

countprobes -1'count the number of probes defined and initialise actionstatus

if usedprobes=0 then 
  pollinterval$="0"
  changeinterval
end if

gosub fillmainpage

if pollinterval=0 then 
  show_setupbuttons
else
  hide_setupbuttons
end if

timer0 timer0interval,timer0routine

wait
'---------------------

dummy:
return

sub countprobes(initactions)
  usedprobes=0
  demoactive=1
  for t=1 to maxprobes
    f$=probename$+str$(t)
    if file.exists(f$)=1 then
      usedprobes=usedprobes+1
      filename$(usedprobes)=f$
      readprobefile usedprobes
      if pwmgpionr>=0 then demoactive=0'there is a defined probe, no demo
      if (initactions=-1) or (usedprobes=initactions) then' for all, or just this one probe, init actions
        redaction(usedprobes)=(redaction$<>"")
        orangeaction(usedprobes)=(orangeaction$<>"")
        optimumaction(usedprobes)=(optimumaction$<>"")
      end if
    end if
  next t
end sub

fillmainpage:
    cls
    autorefresh 1000
    p$="<div style='background-color: #20B2AA;'>"
    'meters and various data are put in a table with as many columns as there are probes
    p=100/maxprobes'width percentage per probe
    p$=p$+"<style>table, td {border: 2px solid "+defbordercolour$+"; border-collapse:collapse;text-align: center;} table {border-spacing: 5px;background-color:"+defbackcolour$+"}</style>"
    p$=p$+"<table id='leveltable' style='width:"+str$(p*usedprobes)+"%;'>"
    l$=""
    if demoactive then l$=" (Demo mode)"
    p$=p$+"<caption><h3>Fluid levels"+l$+"</h3></caption>"
    
    'row with filename(s)
    p$=p$+"<tr>"
    for t=1 to usedprobes
      p$=p$+"<th>"
      p$=p$+filename$(t)
      p$=p$+"</th>"
    next t
    p$=p$+"</tr>"

    'row with captions
    p$=p$+"<tr>"
    for t=1 to usedprobes
      readprobefile t
      p$=p$+"<th>"
      p$=p$+caption$
      p$=p$+"</th>"
    next t
    p$=p$+"</tr>"

    'row with 'reading' textbox
    p$=p$+"<tr>"
    addproperty "readonly size='6' style='color:red; border:0px'",textbox$(dummy$,"ro_textbox"),l$
    for t=1 to usedprobes
      v$="reading$("+str$(t)+")"'variablename underlying the textbox
      p$=p$+"<th>"
      p$=p$+replace$(l$,"dummy$",v$)
      p$=p$+"</th>"
    next t
    p$=p$+"</tr>"
    
    'row with 'full' textbox
    p$=p$+"<tr>"
    addproperty "readonly size='6' style='color:red; border:0px'",textbox$(dummy$,"ro_textbox"),l$
    for t=1 to usedprobes
      v$="full$("+str$(t)+")"'variablename underlying the textbox
      p$=p$+"<th>"
      p$=p$+replace$(l$,"dummy$",v$)
      p$=p$+"</th>"
    next t
    p$=p$+"</tr>"

    'row with meter(s)
    p$=p$+"<tr>"
    l$="<br><br><br><br><br><meter style='background:CCC; width: 200px; height: 40px;transform: rotate(270deg);' data-var='dummy' id='meters' value='0' min='0.00' max='100.00' low='LLL' high='HHH' optimum='PPP'></meter>"
    l$=l$+"<br><br><br><br><br><br>"
    for t=1 to usedprobes
      readprobefile t
      v$="meter("+str$(t)+")"'variablename underlying the meter
      p$=p$+"<td style='background:"+backcolour$+"'>"
      m$=replace$(l$,"dummy",v$)
      if lowperc+highperc+optimumperc=0 then
        m$=replace$(m$,"low='LLL' high='HHH' optimum='PPP'","")
      else
        m$=replace$(m$,"LLL",str$(lowperc))
        m$=replace$(m$,"HHH",str$(highperc))
        m$=replace$(m$,"PPP",str$(optimumperc))
      end if
      p$=p$+m$
      p$=p$+"</td>"
    next t
    p$=p$+"</tr>"
    
    'send a chunk to prevent out of memory
    html p$
    p$=""
    
    'row with 'empty' textbox
    p$=p$+"<tr>"
    addproperty "readonly size='6' style='color:red; border:0px'",textbox$(dummy$,"ro_textbox"),l$
    for t=1 to usedprobes
      v$="empty$("+str$(t)+")"'variablename underlying the textbox 
      p$=p$+"<td>"
      p$=p$+replace$(l$,"dummy$",v$)
      p$=p$+"</td>"
    next t
    p$=p$+"</tr>"
    
    'row with percentagebox
    p$=p$+"<tr>"
    addproperty "readonly size='1' style='color:black; border:0px; text-align:center;'",textbox$(dummy,"ro_textbox"),l$
    for t=1 to usedprobes
      v$="percentage("+str$(t)+")"'variablename
      p$=p$+"<td>"
      p$=p$+replace$(l$,"dummy",v$)+" %"
      p$=p$+"</td>"
    next t
    p$=p$+"</tr>"
    
    'row with litres
    p$=p$+"<tr>"
    addproperty "readonly size='1' style='color:black; border:0px; text-align:center;'",textbox$(dummy,"ro_textbox"),l$
    for t=1 to usedprobes
      readprobefile t
      if (demoactive=1) and (fulllitres=0) then fulllitres=20
      v$="litre("+str$(t)+")"'variablename
      p$=p$+"<td>"
      if fulllitres>0 then p$=p$+replace$(l$,"dummy",v$)+"ltr"
      p$=p$+"</td>"
    next t
    p$=p$+"</tr>"
    
    'row with setup buttons
    p$=p$+"<tr>"
    l$=button$("Setup",dummy,"setupbtn")
    for t=1 to usedprobes
      v$="setup"+str$(t)'construct labelname
      p$=p$+"<td>"
      p$=p$+replace$(l$,"dummy",v$)
      p$=p$+"</td>"
    next t
    p$=p$+"</tr>"
    
    'row with 'actions' textbox
    p$=p$+"<tr>"
    addproperty "readonly size='12' style='color:red; border:0px'",textbox$(dummy$,"ro_textbox"),l$
    for t=1 to usedprobes
      v$="actions$("+str$(t)+")"'variablename
      p$=p$+"<th>"
      p$=p$+replace$(l$,"dummy$",v$)
      p$=p$+"</th>"
    next t
    p$=p$+"</tr>"
  p$=p$+"</table><br>"
  
  'send a chunk to prevent out of memory
  html p$
  p$=""
    
  'table with pollinterval, new probe, systemtime and time setup
  p$=p$+"<table style='width: 40%;'>"
  p$=p$+"<caption><h3>For setup-functions polling must be off</h3></caption>"
  
  'row with headers
  p$=p$+"<tr>"
    p$=p$+"<th>Polling interval in seconds (o=Off)</th>"
    p$=p$+"<th>Add a new probe</th>"
    p$=p$+"<th>System time</th>"
    if wwwconnected=0 then p$=p$+"<th>Set time hh:mm</th>"
  p$=p$+"</tr>"
  
  'row with datafields and buttons
  p$=p$+"<tr>"
    p$=p$+"<td>"
      'listbox pollinterval
      l$=listbox$(pollinterval$,pollintervallist$,1,"polllistbox")
      'replace the standard 'Choose here' by a more informative prompt
      l$=replace$(l$,"Choose here","Poll every")
      p$=p$+l$
      addproperty "readonly size='4' style='color:black; border:0px; text-align:center;'",textbox$(polldowncounter,"ro_textbox"),l$
      p$=p$+l$
    p$=p$+"</td>"
      
    p$=p$+"<td>"
    'new probe button
    if usedprobes<maxprobes then p$=p$+button$("new probe",newprobe,"setupbtn")
    p$=p$+"</td>"
    
    p$=p$+"<td>"
      'systemtime
      addproperty "readonly size='4' style='color:black; border:0px; text-align:center;'",textbox$(systemtime$,"ro_textbox"),l$
      p$=p$+l$
    p$=p$+"</td>"
    
    if wwwconnected=0 then 
      p$=p$+"<td>"
        'setuptime button and input time
        p$=p$+button$("Set time",setuptime,"setupbtn")
        addproperty "size='1' placeholder='hh:mm'",textbox$(newtime$,"settimebox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    end if

  p$=p$+"</tr>"
  
  p$=p$+"</table><br><br><br><br></div>"
  
  p$=p$+cssid$("setupbtn", "text-align:center;background-color:"+defbackcolour$+";")
  p$=p$+cssid$("ro_textbox", "text-align:center;background-color:"+defbackcolour$+";")
  p$=p$+cssid$("polllistbox", "text-align:center;background-color:"+defbackcolour$+";")

  html p$
  p$=""
  
  ONHTMLRELOAD fillmainpage
  OnHtmlChange checkchange'for the pollinterval listbox
return

checkchange:
  if htmleventvar$="pollinterval$" then
    changeinterval
  end if
return

sub changeinterval
    pollinterval=val(pollinterval$)
    if pollinterval>0 then 
      hide_setupbuttons
      polldowncounter=1'start poll immediately then delay as set
    end if
    file.save pollintervalname$,pollinterval$
end sub

sub addproperty(p$,h$,r$)
    'add properties to an html element
    'assuming there is an 'id=' clause in the element
    r$=replace$(h$,"id=",p$+" id=")
end sub

setuptime:
  'not too much testing on correct input. enter rubbish, get rubbish
  y=val(word$(date$(2),1,"/"))
  m=val(word$(date$(2),2,"/"))
  d=val(word$(date$(2),3,"/"))
  onerror skip 3
  h=val(word$(newtime$,1,":"))
  mi=val(word$(newtime$,2,":"))
  settime h,m,d,h,mi
  showmessage "Time set to "+left$(time$,5)
return

sub hide_setupbuttons
  'hide setup buttons/time input textbox
  html cssid$("setupbtn","visibility: hidden;")
  html cssid$("settimebox","visibility: hidden;")
  buttonsvisible=0
end sub

sub show_setupbuttons
  'show setup buttons/time input textbox
  html cssid$("setupbtn","visibility: visible;")
  html cssid$("settimebox","visibility: visible;")
  buttonsvisible=1
end sub

timer0routine:
  systemtime$=left$(time$,5)'update that textbox
  if pollrunning then return
  if pollinterval=0 then
    if buttonsvisible=0 then show_setupbuttons
  else
    if buttonsvisible=1 then hide_setupbuttons
    polldowncounter=polldowncounter-1
    if polldowncounter<1 then
        pollrunning=1
        gosub processprobes
        polldowncounter=pollinterval
        pollrunning=0
    end if
  end if
return

processprobes:
  for pollprobenr=1 to usedprobes
      readprobefile pollprobenr
      'to determine the actions to take, need to know whether safe level is low or high
      probetype=(optimumperc>highperc)*1+(optimumperc<lowperc)*2
      if (pwmgpionr<0) and (demoactive=0) then
          reading$(pollprobenr)="Not active."
      else
          reading$(pollprobenr)="Reading..."
          if demoactive=1 then
            'fake values
            select case probetype
              case 0,2,3'rising or treat as rising
                mvalue=meter(pollprobenr)+rnd(20)
                if (mvalue>110) or (mvalue<0) then mvalue=0'<0 could be some old 'non demo' value
              case 1'falling
                mvalue=meter(pollprobenr)-rnd(20)
                if (mvalue>110) or (mvalue<0) then mvalue=100'>110 could be some old 'non demo' value
            end select
          else
           'read real values
            scalefactor=100
            onerror skip 1'to deal with minimumadc = maximumadc
            scalefactor=int(100000/(minimumadc-maximumadc))/1000'limit to 3 decimals. To convert adcvalue to a percentage
            adcval=0
            getaveragedadc pwmfreq,pwmgpionr,pwmdutycycle,adcval
            valuecorrection pollprobenr,maximumadc,minimumadc,adcval'now, effectively, an empty routine, but the shape of tank could call for correction of the adcval
            mvalue=int((minimumadc-adcval)*scalefactor+0.5)
          end if
         'display values and do actions
          percentage(pollprobenr)=mvalue
          meter(pollprobenr)=mvalue
          litre(pollprobenr)=fulllitres*mvalue/100
          if meter(pollprobenr)>=100 then 
            full$(pollprobenr)="FULL"
          else
            full$(pollprobenr)=""
          end if '
          if meter(pollprobenr)<=0 then 
            empty$(pollprobenr)="EMPTY"
          else
            empty$(pollprobenr)=""
          end if
          if (optimumperc+highperc+lowperc)>0 then 
            testforaction
          else
            actions$(pollprobenr)="no action"
          end if
          if pollinterval>0 then pause 1000'user not waiting to do setup
          reading$(pollprobenr)=left$(time$,5)'update last time processed
      end if
      if pollinterval=0 then exit for'user waiting to do setup
    next pollprobenr
return

sub testforaction
  select case probetype
    case 0,3'no orange, red, optimum defined in a sensible way, so no action
      actions$(pollprobenr)="no action"
    case 1'falling. value: >=optimumperc optimumaction <=highperc orangeaction <lowperc redaction
      if mvalue>=optimumperc then'optimum
        'reset red and orange action
        redaction(pollprobenr)=abs(redaction(pollprobenr))
        orangeaction(pollprobenr)=abs(orangeaction(pollprobenr))
        if optimumaction(pollprobenr)>0 then
           'optimumaction not yet performed
           optimumaction(pollprobenr)=-1
           action$=optimumaction$
           gosub doaction
        end if
      end if
      if mvalue<=highperc then'orange
         'reset optimum action
         optimumaction(pollprobenr)=abs(optimumaction(pollprobenr))
         if orangeaction(pollprobenr)>0 then
            orangeaction(pollprobenr)=-1
            action$=orangeaction$
            gosub doaction
         end if
      end if
      if mvalue<lowperc then'red
         optimumaction(pollprobenr)=abs(optimumaction(pollprobenr))
         if redaction(pollprobenr)>0 then
            redaction(pollprobenr)=-1
            action$=redaction$
            gosub doaction
         end if             
       end if
       updateactions pollprobenr
     case 2'rising. value: <=optimumperc optimumactions >=low orangeaction >high redaction
       if mvalue<=optimumperc then'optimum
          redaction(pollprobenr)=abs(redaction(pollprobenr))
          orangeaction(pollprobenr)=abs(orangeaction(pollprobenr))
          if optimumaction(pollprobenr)>0 then
             optimumaction(pollprobenr)=-1
             action$= optimumaction$
             gosub doaction
          end if
       end if
       if mvalue>=lowperc then'orange
          optimumaction(pollprobenr)=abs(optimumaction(pollprobenr))
          if orangeaction(pollprobenr)>0 then
             orangeaction(pollprobenr)=-1
             action$= orangeaction$
             gosub doaction
          end if
       end if
       if mvalue>highperc then'red
          optimumaction(pollprobenr)=abs(optimumaction(pollprobenr))
          if redaction(pollprobenr)>0 then
             redaction(pollprobenr)=-1
             action$= redaction$
             gosub doaction
          end if             
       end if
       updateactions pollprobenr
  end select
end sub

testorangeaction:
  demoactive=-demoactive'if demoactive=1 (>0) then actions are displayed not executed. for testing they must be executed
  action$= orangeaction$
  gosub doaction
  demoactive=abs(demoactive)
return

testredaction:
  demoactive=-demoactive
  action$=redaction$
  gosub doaction
  demoactive=abs(demoactive)
return

testoptimumaction:
  demoactive=-demoactive
  action$= optimumaction$
  gosub doaction
  demoactive=abs(demoactive)
return

doaction:
  if demoactive > 0 then
    actions$(pollprobenr)=action$'show action but do not execute
    refresh
    pause 1000
    return
  end if
  onerror clear
  if instr(ucase$(action$),"GOSUB")=1 then
    lab$=trim$(mid$(action$,6))
    onerror goto syntaxerror
    gosub lab$
    onerror goto off
    return
  end if
  onerror goto syntaxerror 
  command action$
  onerror goto off
return

syntaxerror:
  x$="error in: "+action$+":"+str$(bas.errnum)+" "+bas.errmsg$
  showmessage x$
return

sub updateactions(p)
  actions$(p)="red:"+str$(redaction(p))+" ora:"+str$(orangeaction(p))+" opt:"+str$(optimumaction(p))
end sub

sub valuecorrection (p,ma,mi,av)'probenr, maximumadc, minimumadc, adcval
  correctionfactor=1
  av=av*correctionfactor
  'if the tank tapers like a bottle or bucket, the higher/lower levelreadings could need a correction
  'especially true if a conversion to litres follows
  'besides that, the readings are not linear. If you need more linearity, 
  'there has to be a correction, and that may be different for each probe
end sub

'for each possible probe (maxprobes) there must be a setup-routine
setup1:
    setupprobenr=1
    setupprobe
return

setup2:
    setupprobenr= 2
    setupprobe
return

setup3:
    setupprobenr= 3
    setupprobe
return

setup4:
    setupprobenr= 4
    setupprobe
return

setup5:
    setupprobenr= 5
    setupprobe
return

sub setupprobe 
    ONHTMLRELOAD off
    hint$=""
    calibratestepnr=0
    readprobefile setupprobenr
    gosub fillsetuppage
end sub

fillsetuppage:
  cls
  autorefresh 1000
  p$="<div style='background-color: #20B2AA;'>"
  l$=""
  'probedata fields are put in a table
  p$=p$+"<style>table, td {border: 2px solid "+defbordercolour$+"; border-collapse:collapse;text-align: center;} table {border-spacing: 5px;background-color:"+defbackcolour$+"}</style>"
    
  p$=p$+"<table id='probetable' style='width:20%;'>"
  p$=p$+"<caption><h3>Setup probe #"+str$(setupprobenr)+"</h3></caption>"
  
  'row with filename
  p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"Filename:"
      p$=p$+"</th>"
      p$=p$+"<th>"
        p$=p$+filename$(setupprobenr)
      p$=p$+"</th>"
  p$=p$+"</tr>"

  'row with caption
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"Caption:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='20' style='color:black; border:0px; text-align:center;'",textbox$(caption$,"captionbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"

  'row with backcolour
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"Backcolour:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        l$=listbox$(backcolour$,backcolourlist$,1,"colourlistbox")
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"    
 
   'row with lowperc
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"Low %%:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='3' style='color:black; border:0px; text-align:center;'",textbox$(lowperc,"lowpercbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"   
    
    'row with highperc
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"High %%:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='3' style='color:black; border:0px; text-align:center;'",textbox$(highperc,"highpercbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"   
    
    'row with optimumperc
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"Optimum %%:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='3' style='color:black; border:0px; text-align:center;'",textbox$(optimumperc,"optimumpercbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"   
    
    'row with orange action
    p$=p$+"<tr>"
      p$=p$+"<th>"
        addproperty "title='Click to execute' style='width:120px;background-color:"+defbackcolour$+"';",button$("Orange action",testorangeaction,"testbutton"),l$
        p$=p$+l$
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='40' style='color:black; border:0px; text-align:center;'",textbox$(orangeaction$,"orangeactionbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"

    'row with red action
    p$=p$+"<tr>"
      p$=p$+"<th>"
        addproperty "title='Click to execute' style='width:120px;background-color:"+defbackcolour$+"';",button$("Red action",testredaction,"testbutton"),l$
        p$=p$+l$
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='40' style='color:black; border:0px; text-align:center;'",textbox$(redaction$,"redactionbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"
   
    'row with optimumaction
    p$=p$+"<tr>"
      p$=p$+"<th>"
        addproperty "title='Click to execute' style='width:120px;background-color:"+defbackcolour$+"';",button$("Optimum action",testoptimumaction,"testbutton"),l$
        p$=p$+l$
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='40' style='color:black; border:0px; text-align:center;'",textbox$(optimumaction$,"optimumactionbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"
    
    'row with fulllitres
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"Full litres:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='3' style='color:black; border:0px; text-align:center;'",textbox$(fulllitres,"fulllitresbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"

    'row with pwmgpio
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"PWM gpionr:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "size='3' style='color:black; border:0px; text-align:center;'",textbox$(pwmgpionr,"pwmgpionrbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"
    
    html p$
    p$=""
    
    'row with calibratebutton
    p$=p$+"<tr>"
      p$=p$+"<th>"
        addproperty "title='Click to calibrate probe' style='width:120px;background-color:"+defbackcolour$+"';",button$("Calibrate",calibrateprobe,"calibratebtn"),l$
        p$=p$+l$
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "readonly rows='4' cols='50' ",textarea$(hint$,"hintbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"
    
    'row with pwmfreq
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"PWM Freq.:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "readonly size='6' style='color:black;background-color:"+defbackcolour$+"; border:0px; text-align:center;'",textbox$(pwmfreq,"pwmfreqbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"                

    'row with maximumadc
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"maximum adc:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "readonly size='6' style='color:black; background-color:"+defbackcolour$+"; border:0px; text-align:center;'",textbox$(maximumadc,"maximumadcbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"          
    
     'row with minimumadc
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"minimum adc:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "readonly size='6' style='color:black; background-color:"+defbackcolour$+";border:0px; text-align:center;'",textbox$(minimumadc,"minimumadcbox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"      

    'row with pwmdutycycle
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"pwmdutycycle:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        addproperty "readonly size='6' style='color:black; background-color:"+defbackcolour$+";border:0px; text-align:center;'",textbox$(pwmdutycycle,"pwmdutycyclebox"),l$
        p$=p$+l$
      p$=p$+"</td>"
    p$=p$+"</tr>"     
         
    'row with save cancel delete buttons
    p$=p$+"<tr>"
      p$=p$+"<th>"
        p$=p$+"What to do:"
      p$=p$+"</th>"
      p$=p$+"<td>"
        p$=p$+button$("Save",saveprobe,"savebtn")+" "+button$("Cancel",cancelprobe,"cancelbtn")+" "+button$("Delete",deleteprobe,"deletebtn")
      p$=p$+"</td>"
    p$=p$+"</tr>"
                      
  p$=p$+"</table><br><br><br><br><br></div>"
  html p$
  p$=""
return

calibrateprobe:
  if pwmgpionr<0 then
    showmessage "First enter a valid pwmgpionr.(0 or higher)"
    return
  end if
  select case calibratestepnr
  case 0
    hint$="Immerse the probe to maximum level and then click on 'Calibrate'"
    calibratestepnr=1
  case 1
    o=0
    learnmaximum o'o for out of bounds 0=no 1=yes
    if o=0 then
      hint$="Immerse the probe to minimum level and then click on 'Calibrate'"
      calibratestepnr=2
    else
      'capacity too high or too low
      'text for the hint$ has already been set in the learnmaximum sub
      calibratestepnr=0
    end if
  case 2
    hint$= "Busy..."
    refresh
    v1=0
    v2=-1
    'need 2 (nearly) identical readings
    do
      getaveragedadc pwmfreq,pwmgpionr,pwmdutycycle,v1
      if abs(v1-v2)<=1 then exit do
      v2=v1
      pause pausetime
    loop while 1
    minimumadc=int((v1+v2)/2)
    hint$="Probe calibrated. Click on 'Save' to use these settings."
    calibratestepnr=0
  end select
return

sub showmessage (m$)
    m$=replace$(replace$(replace$(m$,"\","\\"),"/","\/"),"'","\'")'escape special characters
    jscall "alert('"+m$+"')"
end sub

sub learnmaximum(outofbounds)
    mindutycycle=150
    maxdutycycle=600
    testdutycycle=0
    goal=550'steer towards this adc value
    margin=2'goal plus or minus this value is ok
    maxtestfreq=15000'higher frequencies show too inconsistent readings
    mintestfreq=1000
    adcval=0
    amaxf=0
    aminf=0
    dutycyclestep=-50
    hint$="busy..."
    outofbounds=0
    'test for the lowest adcval that can be achieved with this probe
    'if the capacitance is too small, the adcval will still be higher than goal
    getaveragedadc maxtestfreq,pwmgpionr,mindutycycle,adcval
    if adcval>goal then 
      toosmall
    else
      'test for the highest adcval with this probe
      'if the capacitance is too large, the adcval will still be lower than goal
      getaveragedadc mintestfreq,pwmgpionr,maxdutycycle,adcval
      if adcval<goal then
        toolarge
      end if
    end if
    if outofbounds=0 then
      outofbounds=1
      for testdutycycle=maxdutycycle to mindutycycle step dutycyclestep
        'test if goal will be within range
        getaveragedadc maxtestfreq,pwmgpionr,testdutycycle,amaxf
        getaveragedadc mintestfreq,pwmgpionr,testdutycycle,aminf
        if (aminf>=goal) and (amaxf<=goal) then 
          'in range, but it can be just marginal
          outofbounds=0
          stepfrequency
          exit for
        end if
        hint$=hint$+"busy.."
      next testdutycycle
      if outofbounds=0 then
         'probe still immersed to maximum level, read the maximum value
        v1=0
        v2=-1
        'need 2 (nearly) identical readings
        do
          getaveragedadc pwmfreq,pwmgpionr,pwmdutycycle,v1
          if abs(v1-v2)<=1 then exit do
          v2=v1
          pause pausetime
        loop while 1
        maximumadc=int((v1+v2)/2)
      else
        hint$= "Could not find a fitting range. Capacitance most likely too small"
      end if
    end if
end sub   

sub toosmall
    outofbounds=1
    hint$="Capacitance too small (or disconnected probe?). Use a probe with larger plates."
end sub

sub toolarge
    outofbounds=1
    hint$="Capacitance too large (or shortcircuited?). Use a probe with smaller plates."
end sub
    
sub stepfrequency    
    freqstep=1000
    testfreq=maxtestfreq
    do while freqstep >0.1
      f=testfreq-freqstep
      getaveragedadc f,pwmgpionr,testdutycycle,adcval
      if abs(adcval-goal)<=margin then'found it
        testfreq=testfreq-freqstep
        exit do
      end if
      if adcval<goal then 
          testfreq=testfreq-freqstep' this step was ok, but need more
          if testfreq<mintestfreq then 
            outofbounds=1
            exit do
          end if
      else
           'there was an overshoot
          freqstep=freqstep/10'step was too big, take a smaller step
      end if 
    loop
    if outofbounds=0 then
        'found a fitting frequency and dutycycle
        pwmfreq=testfreq
        pwmdutycycle=testdutycycle
    end if
end sub

sub getaveragedadc(f,p,d,v)'frequency, pin, dutycycle, returnvalue
    'take the average of several measurements
    local t,a
    a=6'number of measurements
    option.pwmfreq f
    pwm(p)=d
    pause pausetime'needs enough time to settle
    v=0
    for t=1 to a
      v=v+adc
      pause pausetime/2'some waittime between measurements
    next t
    v=int(v/a)'average to a whole number
    pwm(p)=0'pin low behind the rectifier is of little influence to other probes
end sub

sub readprobefile(f)
    record$=""
    f$=filename$(f)
    record$=file.read$(f$)
    caption$=WORD.GETPARAM$(record$,"caption")
    backcolour$=WORD.GETPARAM$(record$,"backcolour")
    lowperc=val(WORD.GETPARAM$(record$,"lowperc"))
    highperc=val(WORD.GETPARAM$(record$,"highperc"))
    optimumperc=val(WORD.GETPARAM$(record$,"optimumperc"))
    orangeaction$=WORD.GETPARAM$(record$,"orangeaction")
    redaction$=WORD.GETPARAM$(record$,"redaction")
    optimumaction$=WORD.GETPARAM$(record$,"optimumaction")
    fulllitres=val(WORD.GETPARAM$(record$,"fulllitres"))
    pwmgpionr=val(WORD.GETPARAM$(record$,"pwmgpionr"))
    pwmfreq=val(WORD.GETPARAM$(record$,"pwmfreq"))
    maximumadc=val(WORD.GETPARAM$(record$,"maximumadc"))
    minimumadc=val(WORD.GETPARAM$(record$,"minimumadc"))
    pwmdutycycle=val(WORD.GETPARAM$(record$,"pwmdutycycle"))
end sub

saveprobe:
    'save settings for next run
    record$=""
    WORD.SETPARAM record$,"caption",caption$
    WORD.SETPARAM record$,"backcolour",backcolour$
    WORD.SETPARAM record$,"lowperc",str$(lowperc)
    WORD.SETPARAM record$,"highperc",str$(highperc)
    WORD.SETPARAM record$,"optimumperc",str$(optimumperc)
    WORD.SETPARAM record$,"orangeaction",orangeaction$
    WORD.SETPARAM record$,"redaction",redaction$
    WORD.SETPARAM record$,"optimumaction",optimumaction$
    WORD.SETPARAM record$,"fulllitres",str$(fulllitres)
    WORD.SETPARAM record$,"pwmgpionr",str$(pwmgpionr)
    WORD.SETPARAM record$,"pwmfreq",str$(pwmfreq)
    WORD.SETPARAM record$,"maximumadc",str$(maximumadc)
    WORD.SETPARAM record$,"minimumadc",str$(minimumadc)
    WORD.SETPARAM record$,"pwmdutycycle",str$(pwmdutycycle)
    file.save filename$(setupprobenr),record$
    countprobes setupprobenr
    dofinally
return

sub dofinally
    gosub fillmainpage
end sub

deleteprobe:
    x=file.delete (filename$(setupprobenr))
    if x=0 then showmessage "Probe was NOT deleted!!"
    countprobes -1
    dofinally
return

newprobe:
    onhtmlreload off
    for t=1 to maxprobes
      f$=probename$+str$(t)
      if file.exists(f$)=0 then
        file.save f$,"caption=new probe"+chr$(10)+"pwmgpionr=-1"
        exit for
      end if
    next t
    countprobes t
    dofinally
return

cancelprobe:
    dofinally
return
You do not have the required permissions to view the files attached to this post.
AndyGadget
Posts: 222
Joined: Mon Feb 15, 2021 1:44 pm
Has thanked: 120 times
Been thanked: 132 times

Re: Experimenting with a Capacitive Levelmeter

Post by AndyGadget »

As the capacitance between two plates is inversely proportional to the distance (i.e. smaller distance, larger capacitance) I wonder if McGuinn tried folding the laminating pouch so the plates are parallel, but spaced. This should give a higher empty capacitance which should increase by a factor of 80 or so (dielectric constant of water vs air) when fully immersed.
mcguinn
Posts: 2
Joined: Wed Mar 10, 2021 11:23 am

Re: Experimenting with a Capacitive Levelmeter

Post by mcguinn »

Hi AndyGadget,

I missed your post, sorry for that.
So here my answer, late as it is.
Since water (mostly) is electrically conductive it doesn't matter much if the plates are not directly opposite each other.
If the fluid medium is non-conductive, like oil or also pure distilled water, then the distance between the plates is of importance.

Regards
mcGuinn
Post Reply