Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: DC FAN RPM Input Sensor #302

Closed
pukkita opened this issue Oct 25, 2017 · 13 comments
Closed

Feature request: DC FAN RPM Input Sensor #302

pukkita opened this issue Oct 25, 2017 · 13 comments

Comments

@pukkita
Copy link

pukkita commented Oct 25, 2017

(related to #301)

I think IRF520 MOS Module 140C07 suits much better the purpose for fan control than HG7881/9110, it supports 3.3-5V for logic and up to 24V for power, up to 1A w/o heatsink, and up to 5A with a proper one. Uses a single GPIO.

MOS Module 140c07

Is just a typical mosfet circuit, but nowadays most often is cheaper to buy already assembled than sourcing components...

A nice addition would be integrating the reading of the fan hall sensor into Mycodo, this will mean a second GPIO would be used, but being able to read the RPM reported by the fan would open up possibilities...

AFAIK, according to this post

Everything I've seen suggests 3-wire PC fans (ground, power, tacho) are open-collector/open-drain tacho outputs. The tacho signal should not be pulled up by the fan itself. The specs say the tacho signal is pulled up to 12V by the motherboard.

I've just connected one to my Pi. Powering from 5V and ground (showing a bit of caution) with the tacho line direct to a gpio.

The gpio shows the expected pulse when internally pulled-up to 3V3 and no activity when internally pulled down to ground.

http://abyz.co.uk/videos/fan.webm

Could sensor wire be directly connected to RPi GPIO in this application (using the MOS module feeding 12V to the Fan from external PSU)? or should additional circuitry needed to hook up the fan sensor wire (yellow one) to RPi?

The way I envision this functionality: create a new input sensor: fan tachometer, that will take care of the specified GPIO pin setting (pullup etc), reading, etc. Any required parameters could be manually especified if needed (e.g. Fan DC voltage if required for the RPM calculations).

There are zillions of all sorts of DC/PC Fans with really respectable CFMs; for suitable applications they can be a cheaper, safer, and more precise option (control-wise) than using AC Fans I think.

Moreso, alarms or conditionals could be set in the event of the fan RPMs being out of expected ranges (failing or stuck fan, etc).

@kizniche
Copy link
Owner

kizniche commented Oct 26, 2017

Try out the following code to measure your fan signal. I would definitely use a voltage divider to get the signal voltage at or below 3.3 volts before testing.

Change PWM_GPIO to your GPIO (using BCM numbering), then run with:

~/Mycodo/env/bin/python ~/read_PWM.py

read_PWM.py:

# coding=utf-8

# read_PWM.py
# 2015-12-08
# Public Domain

import time
import pigpio # http://abyz.co.uk/rpi/pigpio/python.html

class reader:
   """
   A class to read PWM pulses and calculate their frequency
   and duty cycle.  The frequency is how often the pulse
   happens per second.  The duty cycle is the percentage of
   pulse high time per cycle.
   """
   def __init__(self, pi, gpio, weighting=0.0):
      """
      Instantiate with the Pi and gpio of the PWM signal
      to monitor.

      Optionally a weighting may be specified.  This is a number
      between 0 and 1 and indicates how much the old reading
      affects the new reading.  It defaults to 0 which means
      the old reading has no effect.  This may be used to
      smooth the data.
      """
      self.pi = pi
      self.gpio = gpio

      if weighting < 0.0:
         weighting = 0.0
      elif weighting > 0.99:
         weighting = 0.99

      self._new = 1.0 - weighting # Weighting for new reading.
      self._old = weighting       # Weighting for old reading.

      self._high_tick = None
      self._period = None
      self._high = None

      pi.set_mode(gpio, pigpio.INPUT)
      self._cb = pi.callback(gpio, pigpio.EITHER_EDGE, self._cbf)

   def _cbf(self, gpio, level, tick):
      if level == 1:
         if self._high_tick is not None:
            t = pigpio.tickDiff(self._high_tick, tick)
            if self._period is not None:
               self._period = (self._old * self._period) + (self._new * t)
            else:
               self._period = t
         self._high_tick = tick
      elif level == 0:
         if self._high_tick is not None:
            t = pigpio.tickDiff(self._high_tick, tick)
            if self._high is not None:
               self._high = (self._old * self._high) + (self._new * t)
            else:
               self._high = t

   def frequency(self):
      """
      Returns the PWM frequency.
      """
      if self._period is not None:
         return 1000000.0 / self._period
      else:
         return 0.0

   def pulse_width(self):
      """
      Returns the PWM pulse width in microseconds.
      """
      if self._high is not None:
         return self._high
      else:
         return 0.0

   def duty_cycle(self):
      """
      Returns the PWM duty cycle percentage.
      """
      if self._high is not None:
         return 100.0 * self._high / self._period
      else:
         return 0.0

   def cancel(self):
      """
      Cancels the reader and releases resources.
      """
      self._cb.cancel()

if __name__ == "__main__":
   import time
   import pigpio
   import read_PWM

   PWM_GPIO = 4
   RUN_TIME = 60.0
   SAMPLE_TIME = 2.0

   pi = pigpio.pi()
   p = read_PWM.reader(pi, PWM_GPIO)
   start = time.time()

   while (time.time() - start) < RUN_TIME:
      time.sleep(SAMPLE_TIME)

      f = p.frequency()
      pw = p.pulse_width()
      dc = p.duty_cycle()
     
      print("f={:.1f} pw={} dc={:.2f}".format(f, int(pw+0.5), dc))

   p.cancel()
   pi.stop()

@kizniche
Copy link
Owner

Actually, this is probably the script you should test.

Change RPM_GPIO to your GPIO (using BCM numbering), then run with:

~/Mycodo/env/bin/python ~/read_RPM.py

read_RPM.py:

# coding=utf-8

# read_RPM.py
# 2016-01-20
# Public Domain

import time
import pigpio # http://abyz.co.uk/rpi/pigpio/python.html

class reader:
   """
   A class to read speedometer pulses and calculate the RPM.
   """
   def __init__(self, pi, gpio, pulses_per_rev=1.0, weighting=0.0, min_RPM=5.0):
      """
      Instantiate with the Pi and gpio of the RPM signal
      to monitor.

      Optionally the number of pulses for a complete revolution
      may be specified.  It defaults to 1.

      Optionally a weighting may be specified.  This is a number
      between 0 and 1 and indicates how much the old reading
      affects the new reading.  It defaults to 0 which means
      the old reading has no effect.  This may be used to
      smooth the data.

      Optionally the minimum RPM may be specified.  This is a
      number between 1 and 1000.  It defaults to 5.  An RPM
      less than the minimum RPM returns 0.0.
      """
      self.pi = pi
      self.gpio = gpio
      self.pulses_per_rev = pulses_per_rev

      if min_RPM > 1000.0:
         min_RPM = 1000.0
      elif min_RPM < 1.0:
         min_RPM = 1.0

      self.min_RPM = min_RPM
      self._watchdog = 200 # Milliseconds.

      if weighting < 0.0:
         weighting = 0.0
      elif weighting > 0.99:
         weighting = 0.99

      self._new = 1.0 - weighting # Weighting for new reading.
      self._old = weighting       # Weighting for old reading.

      self._high_tick = None
      self._period = None

      pi.set_mode(gpio, pigpio.INPUT)

      self._cb = pi.callback(gpio, pigpio.RISING_EDGE, self._cbf)
      pi.set_watchdog(gpio, self._watchdog)

   def _cbf(self, gpio, level, tick):
      if level == 1: # Rising edge.
         if self._high_tick is not None:
            t = pigpio.tickDiff(self._high_tick, tick)
            if self._period is not None:
               self._period = (self._old * self._period) + (self._new * t)
            else:
               self._period = t
         self._high_tick = tick
      elif level == 2: # Watchdog timeout.
         if self._period is not None:
            if self._period < 2000000000:
               self._period += (self._watchdog * 1000)

   def RPM(self):
      """
      Returns the RPM.
      """
      RPM = 0.0
      if self._period is not None:
         RPM = 60000000.0 / (self._period * self.pulses_per_rev)
         if RPM < self.min_RPM:
            RPM = 0.0
      return RPM

   def cancel(self):
      """
      Cancels the reader and releases resources.
      """
      self.pi.set_watchdog(self.gpio, 0) # cancel watchdog
      self._cb.cancel()

if __name__ == "__main__":
   import time
   import pigpio
   import read_RPM

   RPM_GPIO = 4
   RUN_TIME = 60.0
   SAMPLE_TIME = 2.0

   pi = pigpio.pi()
   p = read_RPM.reader(pi, RPM_GPIO)
   start = time.time()

   while (time.time() - start) < RUN_TIME:
      time.sleep(SAMPLE_TIME)
      RPM = p.RPM()
      print("RPM={}".format(int(RPM+0.5)))

   p.cancel()
   pi.stop()

@kizniche
Copy link
Owner

I have nearly everything working with two new inputs, rpm and pwm sensing. I need sleep, so I'll finish it tomorrow and test it with the scope. I'll push where I'm at tonight.

@pukkita
Copy link
Author

pukkita commented Oct 26, 2017

You left me speechless when saw you already had it this morning!

Should receive the MOS modules tomorrow, will test and report.

I assume you pushed to 3.6, should I pull that branch?

@kizniche
Copy link
Owner

kizniche commented Oct 26, 2017

Hah! I got in a good coding mood and thought those were some really nice inputs that I haven't thought of before.

Just use the scripts I commented with above (it's the same code in Mycodo). Currently the code has been committed but I haven't made a release that users can upgrade to. In order to get the new code that hasn't been released you would have to clone the repository with git and copy over your database file. I'll release the next version soon. I don't feel like going into all the issues that can arise from cloning the repo if you're not familiar with it, so I'll just say test with the scripts until the release. I'm going to work on the PID using different outputs feature tonight and also get more RPM and PWM Input options added. If I get all that done tonight I'll make a release. Otherwise, expect one sometime this weekend.

@kizniche
Copy link
Owner

Also, I'd like to have someone test the code before the release, so it's better you work with the bare scripts so it's easy to modify and retest.

@kizniche
Copy link
Owner

I assume you pushed to 3.6, should I pull that branch?

Rereading your comment you appear to be familiar with git. So, if you want to test the code that's been committed, go ahead and see what happens. I pushed to master.

@pukkita
Copy link
Author

pukkita commented Oct 27, 2017

I setup the PWM output driving a LED until I receive the MOS modules.

captura de pantalla 2017-10-27 a la s 08 20 05

I put these two scripts on /usr/local/bin, used a input of type Linux command, and the command line

/usr/bin/python /usr/local/bin/read_PWM.py

captura de pantalla 2017-10-27 a la s 08 18 17

running Command Line parameter verbatim on CLI returns:

f=20000.0 pw=27 dc=54.00
f=20000.0 pw=27 dc=54.00
f=20000.0 pw=27 dc=54.00

So it is working from the CLI.

However mycodo seems to struggle digesting the script output (no values showing on Data > Live Measurements).

captura de pantalla 2017-10-27 a la s 08 17 17

Relevant snippet of /var/log/mycodo/mycodo.log:

2017-10-27 08:12:13,483 - mycodo.sensor_2 - ERROR - Error while attempting to read sensor: Unknown format code 'f' for object of type 'str'
Traceback (most recent call last):
  File "/var/www/mycodo/mycodo/controller_sensor.py", line 774, in update_measure
    measurements = self.measure_sensor.next()
  File "/var/www/mycodo/mycodo/sensors/linux_command.py", line 39, in next
    return {self.condition: float('{0:.2f}'.format(self._measurement))}
ValueError: Unknown format code 'f' for object of type 'str'

System:

Stock mycodo 5.3.0 over a freshly installed NOOBs 2.4.4, both installed yesterday from scratch.

@kizniche
Copy link
Owner

kizniche commented Oct 27, 2017

The Linux Command Input requires the script to return a single value representing a number (integer or float). This value should be the value of the Measurement and Unit you set in the Input options.

@kizniche
Copy link
Owner

I'll add a more verbose error that indicates to the user that the return value of the script is not a single numerical value.

kizniche added a commit that referenced this issue Oct 27, 2017
…ipt not included, system on master will be broken with this commit until added) (#302)
@kizniche
Copy link
Owner

kizniche commented Oct 28, 2017

I just released v5.3.1

Let me know if everything works for you after you've upgraded. Thanks.

@pukkita
Copy link
Author

pukkita commented Oct 28, 2017

Wow! :) thanks!

kizniche added a commit that referenced this issue Oct 28, 2017
@kizniche
Copy link
Owner

I made another release with fixes for various PWM and RPM issues. You'll need to delete your current PWM and RPM Inputs and add them again for everything to work properly.

kizniche added a commit that referenced this issue Oct 29, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants