Evaporator Regulator

This isn’t the most involved project but I might as well document it. I’ve been trying to automate some of the more boring tasks of running the evaporator, I’ve got some nice stainless steel float valves to regulate the sap going in now for example. One of the things that kept requiring constant attention is the air intake to adjust the strength of the fire. I’d have to sit with a foot on it to be able to regulate it almost constantly, to make sure the fire wasn’t burning too hard or too weak. And so naturally I thought I could do something with a Pi.

This proved quite successful even with very loose wiring and fastening just to see how it would work.

All of a sudden I barely need to pay attention to the fire’s strength, with a few refinements I won’t have to at all.

The circuit is quite simple:

Wifi barely reaches the sugarhouse so I made sure this could run independent of connectivity. Which involves coding threads on a Pi Pico, which is supported but not as one would expect.

main.py
import machine import time import network import socket from max6675 import MAX6675 import _thread html = """{\"evaporator_temperature\":<TEMPERATURE>}""" # LED led = machine.Pin( "LED", machine.Pin.OUT ) # temperature sck = machine.Pin( 2, machine.Pin.OUT ) cs = machine.Pin( 3, machine.Pin.OUT ) so = machine.Pin( 4, machine.Pin.IN ) sensor = MAX6675( sck, cs , so ) temperature_min = 25 temperature_max = 30 temperature = -1337.0 # servo servo = machine.PWM( machine.Pin(0) ) servo.freq( 50 ) servo_min = 1000 servo_max = 8000 servo_at = 0 def temp_to_servo( temp ): if (temperature_max - temperature_min)==0: # right in the middle return int( (servo_max-servo_min)/2 ) result = (temp - temperature_min) * (servo_max - servo_min) / (temperature_max - temperature_min) + servo_min if result>servo_max: result = servo_max if result<servo_min: result = servo_min return int( result ) def blink_number( number ): number = str( int(number) ) for char in number: for i in range(int(char)): led.value( 1 ) time.sleep( 0.2 ) led.value( 0 ) time.sleep( 0.2 ) time.sleep( 0.3 ) led.value( 1 ) time.sleep( 0.1 ) led.value( 0 ) time.sleep( 0.3 ) keep_going = False def servo_thread(): global temperature, servo_at, servo while keep_going: time.sleep( 5 ) print( "# measuring average temperature over 1 seconds..." ) temperature_total = 0.0 for i in range(10): temperature_total += sensor.read() time.sleep( 0.1 ) temperature = temperature_total / 10 blink_number( temperature ) print( "# " + str(temperature) ) new_servo_position = temp_to_servo( temperature ) print( "# new_servo_position: " + str(new_servo_position) ) step = 1 if new_servo_position<servo_at: step = -1 for i in range(servo_at, new_servo_position, step): time.sleep( 0.001 ) servo_at = i servo.duty_u16( i ) print( "# servo tread finishing" ) # main thread servo.duty_u16( servo_min ) for i in range(servo_min, servo_max): time.sleep( 0.001 ) servo_at = i servo.duty_u16( i ) try: ssid = "<wifi_ssid>" password = "<wifi_password>" wlan = network.WLAN( network.STA_IF ) wlan.active( True ) wlan.connect( ssid, password ) # wait for connect or fail max_wait = 20 while max_wait>0: if wlan.status() < 0 or wlan.status() >= 3: break max_wait -= 1 print( "> waiting for connection..." ) time.sleep( 1 ) # handle connection error if wlan.status()!=3: print( "> network connection failed, will launch servo thread in 1 minute" ) time.sleep( 60 ) print( "> launching" ) keep_going = True _thread.start_new_thread(servo_thread, ()) while True: time.sleep( 1 ) else: print( "> connected" ) status = wlan.ifconfig() print( "ip = " + status[0] ) # open socket addr = socket.getaddrinfo( "0.0.0.0", 80)[0][-1] s = socket.socket() s.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) #s.settimeout(1) s.bind( addr ) s.listen( 1 ) print( "> web server listening on", addr ) # listen for connections while True: print( ">" ) try: cl, addr = s.accept() print( "client connected from", addr) request = cl.recv( 1024 ) request = request.decode( "utf-8" ).strip() print( request ) if request.startswith( "GET / " ): print( "get data" ) response = html.replace( "<TEMPERATURE>", str(temperature) ) cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" ) cl.send( response ) elif request.startswith( "GET /min_temperature " ): print( "get min_temperature" ) cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" ) cl.send( str(temperature_min) ) elif request.startswith( "GET /max_temperature " ): print( "get max_temperature" ) cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" ) cl.send( str(temperature_max) ) elif request.startswith( "PUT /min_temperature " ): print( "put min_temperature" ) new_min_temperature = int(request.split( "\r\n\r\n" )[1]) print( new_min_temperature ) temperature_min = new_min_temperature cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" ) cl.send( "\"ok\"" ) elif request.startswith( "PUT /max_temperature " ): print( "put max_temperature" ) new_max_temperature = int(request.split( "\r\n\r\n" )[1]) print( new_max_temperature ) temperature_max = new_max_temperature cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" ) cl.send( "\"ok\"" ) elif request.startswith( "PUT /start " ): print( "put start" ) cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" ) if keep_going: print( " already started" ) cl.send( "\"already started\"" ) else: keep_going = True _thread.start_new_thread(servo_thread, ()) cl.send( "\"started\"" ) for i in range(5000, 6000): time.sleep( 0.001 ) servo_at = i servo.duty_u16( i ) elif request.startswith( "PUT /stop " ): print( "put stop" ) #cl.send( "HTTP/1.0 501 OK\r\nContent-type: application/json\r\n\r\n" ) #cl.send( "\"not implemented\"" ) # crash on stop, thread support is that bad cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" ) if keep_going: keep_going = False cl.send( "\"stopped\"" ) else: print( " already stopped" ) cl.send( "\"already stopped\"" ) cl.close() except OSError as e: cl.close() print( "> connection closed" ) keep_going = False time.sleep( 2 ) except KeyboardInterrupt: print( "> ctrl+c, wrapping up..." ) keep_going = False time.sleep( 10 ) except Exception as e: print( e ) print( "> unexpected exception, wrapping up..." ) keep_going = False time.sleep( 10 )
max6675.py
import time class MAX6675: MEASUREMENT_PERIOD_MS = 220 def __init__(self, sck, cs, so): """ Creates new object for controlling MAX6675 :param sck: SCK (clock) pin, must be configured as Pin.OUT :param cs: CS (select) pin, must be configured as Pin.OUT :param so: SO (data) pin, must be configured as Pin.IN """ # Thermocouple self._sck = sck self._sck.low() self._cs = cs self._cs.high() self._so = so self._so.low() self._last_measurement_start = 0 self._last_read_temp = 0 self._error = 0 def _cycle_sck(self): self._sck.high() time.sleep_us(1) self._sck.low() time.sleep_us(1) def refresh(self): """ Start a new measurement. """ self._cs.low() time.sleep_us(10) self._cs.high() self._last_measurement_start = time.ticks_ms() def ready(self): """ Signals if measurement is finished. :return: True if measurement is ready for reading. """ return time.ticks_ms() - self._last_measurement_start > MAX6675.MEASUREMENT_PERIOD_MS def error(self): """ Returns error bit of last reading. If this bit is set (=1), there's problem with the thermocouple - it can be damaged or loosely connected :return: Error bit value """ return self._error def read(self): """ Reads last measurement and starts a new one. If new measurement is not ready yet, returns last value. Note: The last measurement can be quite old (e.g. since last call to `read`). To refresh measurement, call `refresh` and wait for `ready` to become True before reading. :return: Measured temperature """ # Check if new reading is available if self.ready(): # Bring CS pin low to start protocol for reading result of # the conversion process. Forcing the pin down outputs # first (dummy) sign bit 15. self._cs.low() time.sleep_us(10) # Read temperature bits 14-3 from MAX6675. value = 0 for i in range(12): # SCK should resemble clock signal and new SO value # is presented at falling edge self._cycle_sck() value += self._so.value() << (11 - i) # Read the TC Input pin to check if the input is open self._cycle_sck() self._error = self._so.value() # Read the last two bits to complete protocol for i in range(2): self._cycle_sck() # Finish protocol and start new measurement self._cs.high() self._last_measurement_start = time.ticks_ms() self._last_read_temp = value * 0.25 return self._last_read_temp

Web requests collection.

Beefed up Sensoring

A friend bought a couple of Sensirion SEN54s and I helped him get one working, and ended up buying the extra from him. It had been a few years since I researched what sensors were out there that worked well with Raspberry Pis, and were more on the industrial side than the hobbyist side. I was immediately enthused by Sensirion’s documentation, and their sensor looked top notch. My friend did all the homework on reading specs and comparing with others, it was really a no brainer.

With this SEN54 we’ve gained:

  • accurate humidity (the previous sensor was worthless)
  • VOC
  • PM1.0
  • PM2.5
  • PM4.0
  • PM10.0

It’ll be interesting to see the patterns. I’m honestly a little worried about what the particles will reveal seeing as we’re running 2 wood stoves in the house for half of the year. Reassuringly, the first few readings show we’re in the green, but then Nicole opened the stove to let out a bunch of grilled cheese sandwiches and the readings skyrocketed well above WHO guidelines for particles.

But those are only delicious cheese particles finding their way into your nostrils, surely that can’t have and adverse health effect. Jokes aside it’s interesting to see how much of a tail this benign event has. I’ll be really curious to discover more, I really have no idea what I’m looking at yet.

I am very glad to see the Pis become established as industry capable devices. It’s honestly remarkable what I’ve thrown at them over the years while they kept serving their purpose.

GPIO 2 Inverter

I figured out a way to turn our inverter on and off with a Pi so I can leave it off when Winter forces us to be frugal. It takes a little more than an amp hour just sitting there doing nothing, so I do like to turn it off. This capability will serve to automate the fridge in the future, before I do that though, I need to figure out a way to bring cold air from the outside into it. The idea is to have a logic which looks at outside temperature, solar status, and fridge temperature to decide if we just turn it on or if we can simply (and for less electricity) fan in cold air from the outside.

Running a fridge on the coldest months, when solar power is scarce, is doubly absurd.