diff --git a/.gitignore b/.gitignore index 91bcee6..1a41a56 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ .DStore/ thumbs.db #storage/profiles -#config.py +config.py .idea/* state.json venv/* diff --git a/README.md b/README.md index 66d21d9..470c168 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Download [Raspberry PI OS](https://www.raspberrypi.org/software/). Use Rasberry $ sudo apt-get update $ sudo apt-get dist-upgrade + $ sudo apt-get install python3-dev $ git clone https://github.com/jbruce12000/kiln-controller $ cd kiln-controller $ python3 -m venv venv @@ -86,6 +87,10 @@ If you're done playing around with simulations and want to deploy the code on a ## Configuration +Before you begin, make sure to copy the default settings: + + $cp config-default.py config.py + All parameters are defined in config.py. You need to read through config.py carefully to understand each setting. Here are some of the most important settings: | Variable | Default | Description | diff --git a/config.py b/config-default.py old mode 100644 new mode 100755 similarity index 92% rename from config.py rename to config-default.py index c133e9c..9d4380a --- a/config.py +++ b/config-default.py @@ -20,8 +20,8 @@ listening_port = 8081 # This is used to calculate a cost estimate before a run. It's also used # to produce the actual cost during a run. My kiln has three # elements that when my switches are set to high, consume 9460 watts. -kwh_rate = 0.1319 # cost per kilowatt hour per currency_type to calculate cost to run job -kw_elements = 9.460 # if the kiln elements are on, the wattage in kilowatts +kwh_rate = 1 # cost per kilowatt hour per currency_type to calculate cost to run job +kw_elements = 6.9 # if the kiln elements are on, the wattage in kilowatts currency_type = "$" # Currency Symbol to show when calculating cost to run job ######################################################################## @@ -84,11 +84,11 @@ currency_type = "$" # Currency Symbol to show when calculating cost to run j try: import board - spi_sclk = board.D17 #spi clock - spi_miso = board.D27 #spi Microcomputer In Serial Out - spi_cs = board.D22 #spi Chip Select + spi_sclk = board.D11 #spi clock + spi_miso = board.D9 #spi Microcomputer In Serial Out + spi_cs = board.D8 #spi Chip Select spi_mosi = board.D10 #spi Microcomputer Out Serial In (not connected) - gpio_heat = board.D23 #output that controls relay + gpio_heat = board.D25 #output that controls relay gpio_heat_invert = False #invert the output state except (NotImplementedError,AttributeError): print("not running on blinka recognized board, probably a simulation") @@ -102,8 +102,8 @@ except (NotImplementedError,AttributeError): max31855 = 1 max31856 = 0 # uncomment these two lines if using MAX-31856 -import adafruit_max31856 -thermocouple_type = adafruit_max31856.ThermocoupleType.K +# import adafruit_max31856 +# thermocouple_type = adafruit_max31856.ThermocoupleType.S # here are the possible max-31856 thermocouple types # ThermocoupleType.B @@ -140,9 +140,9 @@ sensor_time_wait = 2 # 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 = 10 # Proportional 25,200,200 -pid_ki = 80 # Integral -pid_kd = 220.83497910261562 # Derivative +pid_kp = 25 # Proportional 25,200,200 +pid_ki = 200 # Integral +pid_kd = 200 # Derivative ######################################################################## # @@ -176,7 +176,7 @@ sim_speedup_factor = 1 # # 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 = "c" # 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 @@ -214,7 +214,7 @@ thermocouple_offset=0 temperature_average_samples = 10 # Thermocouple AC frequency filtering - set to True if in a 50Hz locale, else leave at False for 60Hz locale -ac_freq_50hz = False +ac_freq_50hz = True ######################################################################## # Emergencies - or maybe not @@ -238,12 +238,12 @@ ignore_tc_cold_junction_temp_low = False ignore_tc_temp_high = False ignore_tc_temp_low = False ignore_tc_voltage_error = False -ignore_tc_short_errors = False +ignore_tc_short_errors = True ignore_tc_unknown_error = False # This overrides all possible thermocouple errors and prevents the # process from exiting. -ignore_tc_too_many_errors = False +ignore_tc_too_many_errors = True ######################################################################## # automatic restarts - if you have a power brown-out and the raspberry pi @@ -277,3 +277,13 @@ kiln_profiles_directory = os.path.abspath(os.path.join(os.path.dirname( __file__ # To prevent throttling, set throttle_percent to 100. throttle_below_temp = 300 throttle_percent = 20 + + +#mqtt config +mqtt_enable = True +mqtt_host = 'example.com' +mqtt_port = 1883 +mqtt_user = 'my_user' +mqtt_pass = 'my_passwd' +mqtt_topic = 'kiln/sensor' +mqtt_kiln_name = 'My Kiln' diff --git a/lib/ovenWatcher.py b/lib/ovenWatcher.py index 681fa54..66ed3b1 100644 --- a/lib/ovenWatcher.py +++ b/lib/ovenWatcher.py @@ -1,7 +1,28 @@ import threading,logging,json,time,datetime +import config +import paho.mqtt.client as mqtt +import json + from oven import Oven log = logging.getLogger(__name__) +# Callback when the client successfully connects to the broker +def on_connect(client, userdata, flags, rc): + if rc == 0: + log.info("Connected to MQTT broker") + else: + log.warning(f"MQTT connection failed, return code: {rc}") + +# Callback when the client disconnects from the broker +def on_disconnect(client, userdata, rc): + log.warning(f"Disconnected from MQTT broker (code: {rc}), retrying in 5s...") + time.sleep(5) + try: + client.reconnect() + except Exception as e: + log.error(f"Reconnection attempt failed: {e}") + + class OvenWatcher(threading.Thread): def __init__(self,oven): self.last_profile = None @@ -12,6 +33,8 @@ class OvenWatcher(threading.Thread): threading.Thread.__init__(self) self.daemon = True self.oven = oven + if config.mqtt_enabled: + self._setup_mqtt() self.start() # FIXME - need to save runs of schedules in near-real-time @@ -22,18 +45,46 @@ class OvenWatcher(threading.Thread): # out more than N minutes, don't restart # FIXME - this should not be done in the Watcher, but in the Oven class + + def _setup_mqtt(self): + self.client = mqtt.Client() + self.client.username_pw_set(config.mqtt_user, config.mqtt_pass) + #self.client.on_connect = on_connect + #self.client.on_disconnect = on_disconnect + self.client.connect(config.mqtt_host, config.mqtt_port) + self.client.loop_start() + def run(self): + # Main loop: each iteration handles exceptions internally to stay alive while True: - oven_state = self.oven.get_state() - - # record state for any new clients that join - if oven_state.get("state") == "RUNNING": - self.last_log.append(oven_state) - else: - self.recording = False - self.notify_all(oven_state) + try: + oven_state = self.oven.get_state() + + # Save state for new observers if the oven is running + if oven_state.get("state") == "RUNNING": + self.last_log.append(oven_state) + else: + self.recording = False + + # Notify all connected observers via WebSocket + self.notify_all(oven_state) + + # Publish temperature to MQTT topic + if config.mqtt_enabled: + oven_state["name"] = config.mqtt_kiln_name + payload = json.dumps(oven_state) + result = self.client.publish(config.mqtt_topic, payload) + if result.rc != mqtt.MQTT_ERR_SUCCESS: + log.error(f"Publish failed, code: {result.rc}") + + except Exception as exc: + log.exception(f"Exception in OvenWatcher iteration: {exc}") + + # Sleep for configured time_step, even after exception time.sleep(self.oven.time_step) + + def lastlog_subset(self,maxpts=50): '''send about maxpts from lastlog by skipping unwanted data''' totalpts = len(self.last_log) diff --git a/requirements.txt b/requirements.txt index ac8aed5..fbc8ec6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,10 @@ -setuptools -greenlet -bottle -gevent -gevent-websocket -websocket-client -requests - -# for folks running raspberry pis -# we have no proof of anyone using another board yet, but when that -# happens, you might want to comment this out. -RPi.GPIO - -# List of all supported adafruit modules for thermocouples -adafruit-circuitpython-max31855 -adafruit-circuitpython-max31856 - -# for folks using sw spi (bit banging) -adafruit-circuitpython-bitbangio - -# untested - for PT100 platinum thermocouples -#adafruit-circuitpython-max31865 - -# untested - for mcp9600 and mcp9601 -#adafruit-circuitpython-mcp9600 +adafruit_blinka==8.58.1 +adafruit_circuitpython_bitbangio==1.3.18 +adafruit_circuitpython_max31855==3.2.24 +adafruit_circuitpython_max31856==0.12.3 +bottle==0.13.3 +gevent==25.5.1 +gevent_websocket==0.10.1 +paho_mqtt==2.1.0 +Requests==2.32.4 +websocket_client==1.8.0 diff --git a/start-on-boot b/start-on-boot index 5f02dae..dd253ce 100755 --- a/start-on-boot +++ b/start-on-boot @@ -1,3 +1,5 @@ #!/bin/bash -sudo cp /home/pi/kiln-controller/lib/init/kiln-controller.service /etc/systemd/system/ +path=$PWD +sed -i "/ExecStart=/c\ExecStart\=$path/venv/bin/python $path/kiln-controller.py" lib/init/kiln-controller.service +sudo cp $path/lib/init/kiln-controller.service /etc/systemd/system/ sudo systemctl enable kiln-controller