kopia lustrzana https://github.com/jbruce12000/kiln-controller
				
				
				
			Merge branch 'master' into ziegler
						commit
						0770fb5ac2
					
				
							
								
								
									
										17
									
								
								README.md
								
								
								
								
							
							
						
						
									
										17
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -13,9 +13,12 @@ Turns a Raspberry Pi into an inexpensive, web-enabled kiln controller.
 | 
			
		|||
  * supports PID parameters you tune to your kiln
 | 
			
		||||
  * monitors temperature in kiln after schedule has ended
 | 
			
		||||
  * api for starting and stopping at any point in a schedule
 | 
			
		||||
  * support of MAX31856
 | 
			
		||||
  * supports MAX31856 and MAX31855 thermocouple boards
 | 
			
		||||
  * support for K, J, N, R, S, T, E, or B type thermocouples
 | 
			
		||||
  * accurate simulation
 | 
			
		||||
  * support for shifting schedule when kiln cannot heat quickly enough  
 | 
			
		||||
  * support for shifting schedule when kiln cannot heat quickly enough
 | 
			
		||||
  * support for preventing initial integral wind-up
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
**Run Kiln Schedule**
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -52,12 +55,14 @@ My controller plugs into the wall, and the kiln plugs into the controller.
 | 
			
		|||
 | 
			
		||||
## Software 
 | 
			
		||||
 | 
			
		||||
### Raspbian
 | 
			
		||||
### Raspberry PI OS
 | 
			
		||||
 | 
			
		||||
Download [NOOBs](https://www.raspberrypi.org/downloads/noobs/). Copy files to an SD card. Install raspbian on RPi using NOOBs.
 | 
			
		||||
Download [Raspberry PI OS](https://www.raspberrypi.org/software/). Use Rasberry PI Imaging tool to install the OS on an SD card. Boot the OS, open a terminal and...
 | 
			
		||||
 | 
			
		||||
    $ sudo apt-get install python3-pip python3-virtualenv libevent-dev git virtualenv
 | 
			
		||||
    $ git clone https://github.com/jbruce12000/kiln-controller.git
 | 
			
		||||
    $ sudo apt-get update
 | 
			
		||||
    $ sudo apt-get dist-upgrade
 | 
			
		||||
    $ sudo apt-get install python3-virtualenv libevent-dev virtualenv
 | 
			
		||||
    $ git clone https://github.com/jbruce12000/kiln-controller
 | 
			
		||||
    $ cd kiln-controller
 | 
			
		||||
    $ virtualenv -p python3 venv
 | 
			
		||||
    $ source venv/bin/activate
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										31
									
								
								config.py
								
								
								
								
							
							
						
						
									
										31
									
								
								config.py
								
								
								
								
							| 
						 | 
				
			
			@ -57,7 +57,7 @@ sensor_time_wait = 2
 | 
			
		|||
# These parameters work well with the simulated oven. You must tune them
 | 
			
		||||
# to work well with your specific kiln. Note that the integral pid_ki is
 | 
			
		||||
# inverted so that a smaller number means more integral action.
 | 
			
		||||
pid_kp = 25   # Proportional 
 | 
			
		||||
pid_kp = 25   # Proportional
 | 
			
		||||
pid_ki = 200  # Integral
 | 
			
		||||
pid_kd = 200  # Derivative
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -66,14 +66,12 @@ pid_kd = 200  # Derivative
 | 
			
		|||
#
 | 
			
		||||
# Initial heating and Integral Windup
 | 
			
		||||
#
 | 
			
		||||
# During initial heating, if the temperature is constantly under the 
 | 
			
		||||
# During initial heating, if the temperature is constantly under the
 | 
			
		||||
# setpoint,large amounts of Integral can accumulate. This accumulation
 | 
			
		||||
# causes the kiln to run above the setpoint for potentially a long
 | 
			
		||||
# period of time. These settings allow integral accumulation only when
 | 
			
		||||
# the temperature is within stop_integral_windup_margin percent below 
 | 
			
		||||
# or above the setpoint. This applies only to the integral.
 | 
			
		||||
# the temperature is close to the setpoint. This applies only to the integral.
 | 
			
		||||
stop_integral_windup = True
 | 
			
		||||
stop_integral_windup_margin = 10
 | 
			
		||||
 | 
			
		||||
########################################################################
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -96,20 +94,20 @@ sim_R_ho_air   = 0.05   # K/W  " with internal air circulation
 | 
			
		|||
# If you change the temp_scale, all settings in this file are assumed to
 | 
			
		||||
# be in that scale.
 | 
			
		||||
 | 
			
		||||
temp_scale          = "f" # c = Celsius | f = Fahrenheit - Unit to display 
 | 
			
		||||
temp_scale          = "f" # c = Celsius | f = Fahrenheit - Unit to display
 | 
			
		||||
time_scale_slope    = "h" # s = Seconds | m = Minutes | h = Hours - Slope displayed in temp_scale per time_scale_slope
 | 
			
		||||
time_scale_profile  = "m" # s = Seconds | m = Minutes | h = Hours - Enter and view target time in time_scale_profile
 | 
			
		||||
 | 
			
		||||
# emergency shutoff the profile if this temp is reached or exceeded.
 | 
			
		||||
# This just shuts off the profile. If your SSR is working, your kiln will
 | 
			
		||||
# naturally cool off. If your SSR has failed/shorted/closed circuit, this 
 | 
			
		||||
# naturally cool off. If your SSR has failed/shorted/closed circuit, this
 | 
			
		||||
# means your kiln receives full power until your house burns down.
 | 
			
		||||
# this should not replace you watching your kiln or use of a kiln-sitter
 | 
			
		||||
emergency_shutoff_temp = 2264 #cone 7 
 | 
			
		||||
emergency_shutoff_temp = 2264 #cone 7
 | 
			
		||||
 | 
			
		||||
# If the kiln cannot heat or cool fast enough and is off by more than 
 | 
			
		||||
# If the kiln cannot heat or cool fast enough and is off by more than
 | 
			
		||||
# kiln_must_catch_up_max_error  the entire schedule is shifted until
 | 
			
		||||
# the desired temperature is reached. If your kiln cannot attain the 
 | 
			
		||||
# the desired temperature is reached. If your kiln cannot attain the
 | 
			
		||||
# wanted temperature, the schedule will run forever.
 | 
			
		||||
kiln_must_catch_up = True
 | 
			
		||||
kiln_must_catch_up_max_error = 10 #degrees
 | 
			
		||||
| 
						 | 
				
			
			@ -119,3 +117,16 @@ kiln_must_catch_up_max_error = 10 #degrees
 | 
			
		|||
# set set this offset to -4 to compensate.  This probably means you have a
 | 
			
		||||
# cheap thermocouple.  Invest in a better thermocouple.
 | 
			
		||||
thermocouple_offset=0
 | 
			
		||||
 | 
			
		||||
# some kilns/thermocouples start erroneously reporting "short" errors at higher temperatures
 | 
			
		||||
# due to plasma forming in the kiln.
 | 
			
		||||
# Set this to False to ignore these errors and assume the temperature reading was correct anyway
 | 
			
		||||
honour_theromocouple_short_errors = False
 | 
			
		||||
 | 
			
		||||
# number of samples of temperature to average.
 | 
			
		||||
# If you suffer from the high temperature kiln issue and have set honour_theromocouple_short_errors to False,
 | 
			
		||||
# you will likely need to increase this (eg I use 40)
 | 
			
		||||
temperature_average_samples = 40 
 | 
			
		||||
 | 
			
		||||
# Thermocouple AC frequency filtering - set to True if in a 50Hz locale, else leave at False for 60Hz locale
 | 
			
		||||
ac_freq_50hz = False
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,45 +10,41 @@ A controller with properly tuned PID values reacts quickly to changes in the set
 | 
			
		|||
 | 
			
		||||
## Try the Existing Values
 | 
			
		||||
 | 
			
		||||
My kiln is Skutt KS-1018 with a kiln vent.  Try the current settings for pid_kp, pid_ki, and pid_kd and if they work for you, you're done.  Otherwise, you have some experimentation ahead of you.  The following exercise took me about 2 hours of testing. 
 | 
			
		||||
My kiln is Skutt KS-1018 with a kiln vent.  Try the current settings for pid_kp, pid_ki, and pid_kd and if they work for you, you're done.  Otherwise, you have some experimentation ahead of you.  The following exercise took me about an hour of testing. 
 | 
			
		||||
 | 
			
		||||
## The Tuning Process
 | 
			
		||||
 | 
			
		||||
in config.py set the PID values like so...
 | 
			
		||||
I'm a big fan of manual tuning. Let's start with some reasonable values for PID settings in config.py...
 | 
			
		||||
 | 
			
		||||
    pid_kp = 1.0
 | 
			
		||||
    pid_ki = 0.0
 | 
			
		||||
    pid_kd = 0.0
 | 
			
		||||
    pid_kp = 20
 | 
			
		||||
    pid_ki = 50
 | 
			
		||||
    pid_kd = 100
 | 
			
		||||
 | 
			
		||||
run a test schedule. I used a schedule that switches between 200 and 250 F every 30 minutes.
 | 
			
		||||
When you change values, change only one at a time and watch the impact. Change values by either doubling or halving.
 | 
			
		||||
 | 
			
		||||
What you are looking for is overshoot (in my case 25F) past 200F to 225F. The next thing the controller should do is undershoot by just a little below the set point of 200F. If these two things happen, great.  If not, you will need to change pid_kp to a higher or lower value.
 | 
			
		||||
Run a test schedule. I used a schedule that switches between 200 and 250 F every 30 minutes. The kiln will likely shoot past 200. This is normal. We'll eventually get rid of most of the overshoot, but probably not all.
 | 
			
		||||
 | 
			
		||||
Once you get the overshoot and minimal undershoot, you need to record some values.  First grab the overshoot... in my case 25F.
 | 
			
		||||
Let's balance pid_ki first (the integral). The lower the pid_ki, the greater the impact it will have on the system. If a system is consistently low or high, the integral is used to help bring the system closer to the set point. The integral accumulates over time and has [potentially] a bigger and bigger impact.
 | 
			
		||||
 | 
			
		||||
    pid_kp = 25
 | 
			
		||||
* If you have a steady state (no oscillations), but the temperature is always above the set point, increase pid_ki.
 | 
			
		||||
* If you have a steady state (no oscillations), but the temperature is always below the set point, decrease pid_ki.
 | 
			
		||||
* If you have an oscillation but the temperature is mostly above the setpoint, increase pid_ki.
 | 
			
		||||
* If you have an oscillation but the temperature is mostly below the setpoint, decrease pid_ki.
 | 
			
		||||
 | 
			
		||||
Measure the time in seconds from high peak to low peak. In my case this was 725 seconds.  Multiply that number by 1.5 to get the Integral. So 725 * 1.5 = 1088.
 | 
			
		||||
Let's set pid_kp next (proportional). Think of pid_kp as a dimmable light switch that turns on the heat when below the set point and turns it off when above. The brightness of the dimmable light is defined by pid_kp. Be careful reducing pid_kp too much. It can result in strange behavior.
 | 
			
		||||
 | 
			
		||||
    pid_ki = 1088
 | 
			
		||||
* If you have oscillations that don't stop or increase in size, reduce pid_kp
 | 
			
		||||
* If you have too much overshoot (after adjusting pid_kd), reduce pid_kp
 | 
			
		||||
* If you approach the set point wayyy tooo sloooowly, increase pid_kp
 | 
			
		||||
 
 | 
			
		||||
Now set pid_kd (derivative). pid_kd makes an impact when there is a change in temperature. It's used to reduce oscillations.
 | 
			
		||||
 | 
			
		||||
Now set the derivative at 1/5 of the Integral. So 1088/5 = 217
 | 
			
		||||
* If you have oscillations that take too long to settle, increase pid_kp
 | 
			
		||||
* If you have crazy, unpredictable behavior from the controller, reduce pid_kp
 | 
			
		||||
 | 
			
		||||
    pid_kd = 217
 | 
			
		||||
 | 
			
		||||
in essence these values mean...
 | 
			
		||||
 | 
			
		||||
| setting | Value | Action |
 | 
			
		||||
| ------- | ----- | ------ |
 | 
			
		||||
| pid_kp | 25 | react pretty slowly |
 | 
			
		||||
| pid_ki | 1088 | predict really far forward in time and make changes early |
 | 
			
		||||
| pid_kd | 217 | heavily dampen oscillations |
 | 
			
		||||
 | 
			
		||||
Now, run the test schedule again and see how well it works.  Expect some overshoot as the kiln reaches the set temperature the first time, but no oscillation.  Any holds or ramps after that should have a smooth transition and should remain really close to the set point [1 or 2 degrees F].
 | 
			
		||||
Expect some overshoot as the kiln reaches the set temperature the first time, but no oscillation.  Any holds or ramps after that should have a smooth transition and should remain really close to the set point [1 or 2 degrees F].
 | 
			
		||||
 | 
			
		||||
## Troubleshooting
 | 
			
		||||
 | 
			
		||||
* only change one value at a time, then test it.
 | 
			
		||||
* If there is too much overshoot, decrease pid_kp.
 | 
			
		||||
* If the temp is always below the set point, decrease pid_ki (which increases the integral action).
 | 
			
		||||
* if the above does not work, try the Ziegler / Nichols method https://blog.opticontrols.com/archives/477
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ class MAX31855(object):
 | 
			
		|||
        '''Initialize Soft (Bitbang) SPI bus
 | 
			
		||||
 | 
			
		||||
        Parameters:
 | 
			
		||||
        - cs_pin:    Chip Select (CS) / Slave Select (SS) pin (Any GPIO)  
 | 
			
		||||
        - cs_pin:    Chip Select (CS) / Slave Select (SS) pin (Any GPIO)
 | 
			
		||||
        - clock_pin: Clock (SCLK / SCK) pin (Any GPIO)
 | 
			
		||||
        - data_pin:  Data input (SO / MOSI) pin (Any GPIO)
 | 
			
		||||
        - units:     (optional) unit of measurement to return. ("c" (default) | "k" | "f")
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ class MAX31855(object):
 | 
			
		|||
        self.units = units
 | 
			
		||||
        self.data = None
 | 
			
		||||
        self.board = board
 | 
			
		||||
        self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
 | 
			
		||||
 | 
			
		||||
        # Initialize needed GPIO
 | 
			
		||||
        GPIO.setmode(self.board)
 | 
			
		||||
| 
						 | 
				
			
			@ -70,20 +71,13 @@ class MAX31855(object):
 | 
			
		|||
        if data_32 is None:
 | 
			
		||||
            data_32 = self.data
 | 
			
		||||
        anyErrors = (data_32 & 0x10000) != 0    # Fault bit, D16
 | 
			
		||||
        noConnection = (data_32 & 0x00000001) != 0       # OC bit, D0
 | 
			
		||||
        shortToGround = (data_32 & 0x00000002) != 0      # SCG bit, D1
 | 
			
		||||
        shortToVCC = (data_32 & 0x00000004) != 0         # SCV bit, D2
 | 
			
		||||
        if anyErrors:
 | 
			
		||||
            if noConnection:
 | 
			
		||||
                raise MAX31855Error("No Connection")
 | 
			
		||||
            elif shortToGround:
 | 
			
		||||
                raise MAX31855Error("Thermocouple short to ground")
 | 
			
		||||
            elif shortToVCC:
 | 
			
		||||
                raise MAX31855Error("Thermocouple short to VCC")
 | 
			
		||||
            else:
 | 
			
		||||
                # Perhaps another SPI device is trying to send data?
 | 
			
		||||
                # Did you remember to initialize all other SPI devices?
 | 
			
		||||
                raise MAX31855Error("Unknown Error")
 | 
			
		||||
            self.noConnection = (data_32 & 0x00000001) != 0       # OC bit, D0
 | 
			
		||||
            self.shortToGround = (data_32 & 0x00000002) != 0      # SCG bit, D1
 | 
			
		||||
            self.shortToVCC = (data_32 & 0x00000004) != 0         # SCV bit, D2
 | 
			
		||||
            self.unknownError = not (self.noConnection | self.shortToGround | self.shortToVCC)    # Errk!
 | 
			
		||||
        else:
 | 
			
		||||
            self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
 | 
			
		||||
 | 
			
		||||
    def data_to_tc_temperature(self, data_32 = None):
 | 
			
		||||
        '''Takes an integer and returns a thermocouple temperature in celsius.'''
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +132,7 @@ class MAX31855(object):
 | 
			
		|||
        GPIO.setup(self.clock_pin, GPIO.IN)
 | 
			
		||||
 | 
			
		||||
    def data_to_LinearizedTempC(self, data_32 = None):
 | 
			
		||||
        '''Return the NIST-linearized thermocouple temperature value in degrees 
 | 
			
		||||
        '''Return the NIST-linearized thermocouple temperature value in degrees
 | 
			
		||||
        celsius. See https://learn.adafruit.com/calibrating-sensors/maxim-31855-linearization for more infoo.
 | 
			
		||||
        This code came from https://github.com/nightmechanic/FuzzypicoReflow/blob/master/lib/max31855.py
 | 
			
		||||
'''
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -89,7 +89,7 @@ class MAX31856(object):
 | 
			
		|||
    MAX31856_S_TYPE = 0x6 # Read S Type Thermocouple
 | 
			
		||||
    MAX31856_T_TYPE = 0x7 # Read T Type Thermocouple
 | 
			
		||||
 | 
			
		||||
    def __init__(self, tc_type=MAX31856_S_TYPE, units="c", avgsel=0x0, software_spi=None, hardware_spi=None, gpio=None):
 | 
			
		||||
    def __init__(self, tc_type=MAX31856_S_TYPE, units="c", avgsel=0x0, ac_freq_50hz=False, ocdetect=0x1, software_spi=None, hardware_spi=None, gpio=None):
 | 
			
		||||
        """
 | 
			
		||||
        Initialize MAX31856 device with software SPI on the specified CLK,
 | 
			
		||||
        CS, and DO pins.  Alternatively can specify hardware SPI by sending an
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +100,8 @@ class MAX31856(object):
 | 
			
		|||
                MAX31856.MAX31856_X_TYPE.
 | 
			
		||||
            avgsel (1-byte Hex): Type of Averaging.  Choose from values in CR0 table of datasheet.
 | 
			
		||||
                Default is single sample.
 | 
			
		||||
            ac_freq_50hz: Set to True if your AC frequency is 50Hz, Set to False for 60Hz,
 | 
			
		||||
            ocdetect: Detect open circuit errors (ie broken thermocouple). Choose from values in CR1 table of datasheet
 | 
			
		||||
            software_spi (dict): Contains the pin assignments for software SPI, as defined below:
 | 
			
		||||
                clk (integer): Pin number for software SPI clk
 | 
			
		||||
                cs (integer): Pin number for software SPI cs
 | 
			
		||||
| 
						 | 
				
			
			@ -112,6 +114,8 @@ class MAX31856(object):
 | 
			
		|||
        self.tc_type = tc_type
 | 
			
		||||
        self.avgsel = avgsel
 | 
			
		||||
        self.units = units
 | 
			
		||||
        self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
 | 
			
		||||
 | 
			
		||||
        # Handle hardware SPI
 | 
			
		||||
        if hardware_spi is not None:
 | 
			
		||||
            self._logger.debug('Using hardware SPI')
 | 
			
		||||
| 
						 | 
				
			
			@ -132,11 +136,13 @@ class MAX31856(object):
 | 
			
		|||
        self._spi.set_mode(1)
 | 
			
		||||
        self._spi.set_bit_order(SPI.MSBFIRST)
 | 
			
		||||
 | 
			
		||||
        self.cr1 = ((self.avgsel << 4) + self.tc_type)
 | 
			
		||||
        self.cr0 = self.MAX31856_CR0_READ_CONT | ((ocdetect & 3) << 4) | (1 if ac_freq_50hz else 0)
 | 
			
		||||
        self.cr1 = (((self.avgsel & 7) << 4) + (self.tc_type & 0x0f))
 | 
			
		||||
 | 
			
		||||
        # Setup for reading continuously with T-Type thermocouple
 | 
			
		||||
        self._write_register(self.MAX31856_REG_WRITE_CR0, self.MAX31856_CR0_READ_CONT)
 | 
			
		||||
        self._write_register(self.MAX31856_REG_WRITE_CR0, 0)
 | 
			
		||||
        self._write_register(self.MAX31856_REG_WRITE_CR1, self.cr1)
 | 
			
		||||
        self._write_register(self.MAX31856_REG_WRITE_CR0, self.cr0)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _cj_temp_from_bytes(msb, lsb):
 | 
			
		||||
| 
						 | 
				
			
			@ -297,8 +303,39 @@ class MAX31856(object):
 | 
			
		|||
        '''Convert celsius to fahrenheit.'''
 | 
			
		||||
        return celsius * 9.0/5.0 + 32
 | 
			
		||||
 | 
			
		||||
    def checkErrors(self):
 | 
			
		||||
        data = self.read_fault_register()
 | 
			
		||||
        self.noConnection = (data & 0x00000001) != 0
 | 
			
		||||
        self.unknownError = (data & 0xfe) != 0
 | 
			
		||||
 | 
			
		||||
    def get(self):
 | 
			
		||||
        self.checkErrors()
 | 
			
		||||
        celcius = self.read_temp_c()
 | 
			
		||||
        return getattr(self, "to_" + self.units)(celcius)
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
 | 
			
		||||
    # Multi-chip example
 | 
			
		||||
    import time
 | 
			
		||||
    cs_pins = [6]
 | 
			
		||||
    clock_pin = 13
 | 
			
		||||
    data_pin = 5
 | 
			
		||||
    di_pin = 26
 | 
			
		||||
    units = "c"
 | 
			
		||||
    thermocouples = []
 | 
			
		||||
    for cs_pin in cs_pins:
 | 
			
		||||
        thermocouples.append(MAX31856(avgsel=0, ac_freq_50hz=True, tc_type=MAX31856.MAX31856_K_TYPE, software_spi={'clk': clock_pin, 'cs': cs_pin, 'do': data_pin, 'di': di_pin}, units=units))
 | 
			
		||||
 | 
			
		||||
    running = True
 | 
			
		||||
    while(running):
 | 
			
		||||
        try:
 | 
			
		||||
            for thermocouple in thermocouples:
 | 
			
		||||
                rj = thermocouple.read_internal_temp_c()
 | 
			
		||||
                tc = thermocouple.get()
 | 
			
		||||
                print("tc: {} and rj: {}, NC:{} ??:{}".format(tc, rj, thermocouple.noConnection, thermocouple.unknownError))
 | 
			
		||||
            time.sleep(1)
 | 
			
		||||
        except KeyboardInterrupt:
 | 
			
		||||
            running = False
 | 
			
		||||
    for thermocouple in thermocouples:
 | 
			
		||||
        thermocouple.cleanup()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										110
									
								
								lib/oven.py
								
								
								
								
							
							
						
						
									
										110
									
								
								lib/oven.py
								
								
								
								
							| 
						 | 
				
			
			@ -8,6 +8,7 @@ import config
 | 
			
		|||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Output(object):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.active = False
 | 
			
		||||
| 
						 | 
				
			
			@ -31,10 +32,10 @@ class Output(object):
 | 
			
		|||
        if tuning:
 | 
			
		||||
            return
 | 
			
		||||
        time.sleep(sleepfor)
 | 
			
		||||
        self.GPIO.output(config.gpio_heat, self.GPIO.LOW)
 | 
			
		||||
 | 
			
		||||
    def cool(self,sleepfor):
 | 
			
		||||
        '''no active cooling, so sleep'''
 | 
			
		||||
        self.GPIO.output(config.gpio_heat, self.GPIO.LOW)
 | 
			
		||||
        time.sleep(sleepfor)
 | 
			
		||||
 | 
			
		||||
# FIX - Board class needs to be completely removed
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +85,9 @@ class TempSensor(threading.Thread):
 | 
			
		|||
        threading.Thread.__init__(self)
 | 
			
		||||
        self.daemon = True
 | 
			
		||||
        self.temperature = 0
 | 
			
		||||
        self.bad_percent = 0
 | 
			
		||||
        self.time_step = config.sensor_time_wait
 | 
			
		||||
        self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
 | 
			
		||||
 | 
			
		||||
class TempSensorSimulated(TempSensor):
 | 
			
		||||
    '''not much here, just need to be able to set the temperature'''
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +99,11 @@ class TempSensorReal(TempSensor):
 | 
			
		|||
       during the time_step'''
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        TempSensor.__init__(self)
 | 
			
		||||
        self.sleeptime = self.time_step / float(config.temperature_average_samples)
 | 
			
		||||
        self.bad_count = 0
 | 
			
		||||
        self.ok_count = 0
 | 
			
		||||
        self.bad_stamp = 0
 | 
			
		||||
 | 
			
		||||
        if config.max31855:
 | 
			
		||||
            log.info("init MAX31855")
 | 
			
		||||
            from max31855 import MAX31855, MAX31855Error
 | 
			
		||||
| 
						 | 
				
			
			@ -112,26 +120,48 @@ class TempSensorReal(TempSensor):
 | 
			
		|||
                             'do': config.gpio_sensor_data,
 | 
			
		||||
                             'di': config.gpio_sensor_di }
 | 
			
		||||
            self.thermocouple = MAX31856(tc_type=config.thermocouple_type,
 | 
			
		||||
                                         software_spi = sofware_spi,
 | 
			
		||||
                                         units = config.temp_scale
 | 
			
		||||
                                         software_spi = software_spi,
 | 
			
		||||
                                         units = config.temp_scale,
 | 
			
		||||
                                         ac_freq_50hz = config.ac_freq_50hz,
 | 
			
		||||
                                         )
 | 
			
		||||
 | 
			
		||||
    def run(self):
 | 
			
		||||
        '''take 5 measurements over each time period and return the
 | 
			
		||||
        average'''
 | 
			
		||||
        '''use a moving average of config.temperature_average_samples across the time_step'''
 | 
			
		||||
        temps = []
 | 
			
		||||
        while True:
 | 
			
		||||
            maxtries = 5
 | 
			
		||||
            sleeptime = self.time_step / float(maxtries)
 | 
			
		||||
            temps = []
 | 
			
		||||
            for x in range(0,maxtries):
 | 
			
		||||
                try:
 | 
			
		||||
                    temp = self.thermocouple.get()
 | 
			
		||||
                    temps.append(temp)
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    log.exception("problem reading temp")
 | 
			
		||||
                time.sleep(sleeptime)
 | 
			
		||||
            if len(temps) > 0:
 | 
			
		||||
                self.temperature = sum(temps)/len(temps)
 | 
			
		||||
            # reset error counter if time is up
 | 
			
		||||
            if (time.time() - self.bad_stamp) > (self.time_step * 2):
 | 
			
		||||
                if self.bad_count + self.ok_count:
 | 
			
		||||
                    self.bad_percent = (self.bad_count / (self.bad_count + self.ok_count)) * 100
 | 
			
		||||
                else:
 | 
			
		||||
                    self.bad_percent = 0
 | 
			
		||||
                self.bad_count = 0
 | 
			
		||||
                self.ok_count = 0
 | 
			
		||||
                self.bad_stamp = time.time()
 | 
			
		||||
 | 
			
		||||
            temp = self.thermocouple.get()
 | 
			
		||||
            self.noConnection = self.thermocouple.noConnection
 | 
			
		||||
            self.shortToGround = self.thermocouple.shortToGround
 | 
			
		||||
            self.shortToVCC = self.thermocouple.shortToVCC
 | 
			
		||||
            self.unknownError = self.thermocouple.unknownError
 | 
			
		||||
 | 
			
		||||
            is_bad_value = self.noConnection | self.unknownError
 | 
			
		||||
            if config.honour_theromocouple_short_errors:
 | 
			
		||||
                is_bad_value |= self.shortToGround | self.shortToVCC
 | 
			
		||||
 | 
			
		||||
            if not is_bad_value:
 | 
			
		||||
                temps.append(temp)
 | 
			
		||||
                if len(temps) > config.temperature_average_samples:
 | 
			
		||||
                    del temps[0]
 | 
			
		||||
                self.ok_count += 1
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                log.error("Problem reading temp N/C:%s GND:%s VCC:%s ???:%s" % (self.noConnection,self.shortToGround,self.shortToVCC,self.unknownError))
 | 
			
		||||
                self.bad_count += 1
 | 
			
		||||
 | 
			
		||||
            if len(temps):
 | 
			
		||||
                self.temperature = sum(temps) / len(temps)
 | 
			
		||||
            time.sleep(self.sleeptime)
 | 
			
		||||
 | 
			
		||||
class Oven(threading.Thread):
 | 
			
		||||
    '''parent oven class. this has all the common code
 | 
			
		||||
| 
						 | 
				
			
			@ -154,8 +184,22 @@ class Oven(threading.Thread):
 | 
			
		|||
        self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
 | 
			
		||||
 | 
			
		||||
    def run_profile(self, profile, startat=0):
 | 
			
		||||
        log.info("Running schedule %s" % profile.name)
 | 
			
		||||
        self.reset()
 | 
			
		||||
 | 
			
		||||
        if self.board.temp_sensor.noConnection:
 | 
			
		||||
            log.info("Refusing to start profile - thermocouple not connected")
 | 
			
		||||
            return
 | 
			
		||||
        if self.board.temp_sensor.shortToGround:
 | 
			
		||||
            log.info("Refusing to start profile - thermocouple short to ground")
 | 
			
		||||
            return
 | 
			
		||||
        if self.board.temp_sensor.shortToVCC:
 | 
			
		||||
            log.info("Refusing to start profile - thermocouple short to VCC")
 | 
			
		||||
            return
 | 
			
		||||
        if self.board.temp_sensor.unknownError:
 | 
			
		||||
            log.info("Refusing to start profile - thermocouple unknown error")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        log.info("Running schedule %s" % profile.name)
 | 
			
		||||
        self.profile = profile
 | 
			
		||||
        self.totaltime = profile.get_duration()
 | 
			
		||||
        self.state = "RUNNING"
 | 
			
		||||
| 
						 | 
				
			
			@ -185,6 +229,9 @@ class Oven(threading.Thread):
 | 
			
		|||
 | 
			
		||||
    def update_runtime(self):
 | 
			
		||||
        runtime_delta = datetime.datetime.now() - self.start_time
 | 
			
		||||
        if runtime_delta.total_seconds() < 0:
 | 
			
		||||
            runtime_delta = datetime.timedelta(0)
 | 
			
		||||
 | 
			
		||||
        if self.startat > 0:
 | 
			
		||||
            self.runtime = self.startat + runtime_delta.total_seconds()
 | 
			
		||||
        else:
 | 
			
		||||
| 
						 | 
				
			
			@ -194,12 +241,24 @@ class Oven(threading.Thread):
 | 
			
		|||
        self.target = self.profile.get_target_temperature(self.runtime)
 | 
			
		||||
 | 
			
		||||
    def reset_if_emergency(self):
 | 
			
		||||
        '''reset if the temperature is way TOO HOT'''
 | 
			
		||||
        '''reset if the temperature is way TOO HOT, or other critical errors detected'''
 | 
			
		||||
        if (self.board.temp_sensor.temperature + config.thermocouple_offset >=
 | 
			
		||||
            config.emergency_shutoff_temp):
 | 
			
		||||
            log.info("emergency!!! temperature too high, shutting down")
 | 
			
		||||
            self.reset()
 | 
			
		||||
 | 
			
		||||
        if self.board.temp_sensor.noConnection:
 | 
			
		||||
            log.info("emergency!!! lost connection to thermocouple, shutting down")
 | 
			
		||||
            self.reset()
 | 
			
		||||
 | 
			
		||||
        if self.board.temp_sensor.unknownError:
 | 
			
		||||
            log.info("emergency!!! unknown thermocouple error, shutting down")
 | 
			
		||||
            self.reset()
 | 
			
		||||
 | 
			
		||||
        if self.board.temp_sensor.bad_percent > 30:
 | 
			
		||||
            log.info("emergency!!! too many errors in a short period, shutting down")
 | 
			
		||||
            self.reset()
 | 
			
		||||
 | 
			
		||||
    def reset_if_schedule_ended(self):
 | 
			
		||||
        if self.runtime > self.totaltime:
 | 
			
		||||
            log.info("schedule ended, shutting down")
 | 
			
		||||
| 
						 | 
				
			
			@ -328,6 +387,10 @@ class RealOven(Oven):
 | 
			
		|||
        # start thread
 | 
			
		||||
        self.start()
 | 
			
		||||
 | 
			
		||||
    def reset(self):
 | 
			
		||||
        super().reset()
 | 
			
		||||
        self.output.cool(0)
 | 
			
		||||
 | 
			
		||||
    def heat_then_cool(self):
 | 
			
		||||
        pid = self.pid.compute(self.target,
 | 
			
		||||
                               self.board.temp_sensor.temperature +
 | 
			
		||||
| 
						 | 
				
			
			@ -340,8 +403,10 @@ class RealOven(Oven):
 | 
			
		|||
        if heat_on > 0:
 | 
			
		||||
            self.heat = 1.0
 | 
			
		||||
 | 
			
		||||
        self.output.heat(heat_on)
 | 
			
		||||
        self.output.cool(heat_off)
 | 
			
		||||
        if heat_on:
 | 
			
		||||
            self.output.heat(heat_on)
 | 
			
		||||
        if heat_off:
 | 
			
		||||
            self.output.cool(heat_off)
 | 
			
		||||
        time_left = self.totaltime - self.runtime
 | 
			
		||||
        log.info("temp=%.2f, target=%.2f, pid=%.3f, heat_on=%.2f, heat_off=%.2f, run_time=%d, total_time=%d, time_left=%d" %
 | 
			
		||||
            (self.board.temp_sensor.temperature + config.thermocouple_offset,
 | 
			
		||||
| 
						 | 
				
			
			@ -413,8 +478,7 @@ class PID():
 | 
			
		|||
 | 
			
		||||
        if self.ki > 0:
 | 
			
		||||
            if config.stop_integral_windup == True:
 | 
			
		||||
                margin = setpoint * config.stop_integral_windup_margin/100
 | 
			
		||||
                if (abs(error) <= abs(margin)):
 | 
			
		||||
                if abs(self.kp * error) < window_size:
 | 
			
		||||
                    self.iterm += (error * timeDelta * (1/self.ki))
 | 
			
		||||
            else:
 | 
			
		||||
                self.iterm += (error * timeDelta * (1/self.ki))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue