summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorErg <uinarf@autistici.org>2024-10-21 17:02:14 +0200
committerErg <uinarf@autistici.org>2024-10-21 17:02:14 +0200
commit1be9a52f11d7bb9cf519c6ee8027c9029824e29c (patch)
treea71b0edf5c4748000a8f5515dd215a9052ef927e
downloadPico-1be9a52f11d7bb9cf519c6ee8027c9029824e29c.tar.gz
Pico-1be9a52f11d7bb9cf519c6ee8027c9029824e29c.tar.bz2
Pico-1be9a52f11d7bb9cf519c6ee8027c9029824e29c.zip
Comment change
-rw-r--r--PID.py242
-rw-r--r--Readme.md47
-rw-r--r--class_diagram.pngbin0 -> 17179 bytes
-rw-r--r--class_diagram.txt33
-rwxr-xr-xclient_maker.sh150
-rw-r--r--config.py.example39
-rw-r--r--main.py388
-rw-r--r--umqttsimple.py214
8 files changed, 1113 insertions, 0 deletions
diff --git a/PID.py b/PID.py
new file mode 100644
index 0000000..d8957c5
--- /dev/null
+++ b/PID.py
@@ -0,0 +1,242 @@
+import time
+
+
+def _clamp(value, limits):
+ lower, upper = limits
+ if value is None:
+ return None
+ elif (upper is not None) and (value > upper):
+ return upper
+ elif (lower is not None) and (value < lower):
+ return lower
+ return value
+
+
+try:
+ # Get monotonic time to ensure that time deltas are always positive
+ _current_time = time.monotonic
+except AttributeError:
+ # time.monotonic() not available (using python < 3.3), fallback to time.time()
+ _current_time = time.time
+
+
+class PID(object):
+ """A simple PID controller."""
+
+ def __init__(
+ self,
+ Kp=1.0,
+ Ki=0.0,
+ Kd=0.0,
+ setpoint=0,
+ sample_time=0.01,
+ output_limits=(None, None),
+ auto_mode=True,
+ proportional_on_measurement=False,
+ error_map=None,
+ ):
+ """
+ Initialize a new PID controller.
+
+ :param Kp: The value for the proportional gain Kp
+ :param Ki: The value for the integral gain Ki
+ :param Kd: The value for the derivative gain Kd
+ :param setpoint: The initial setpoint that the PID will try to achieve
+ :param sample_time: The time in seconds which the controller should wait before generating
+ a new output value. The PID works best when it is constantly called (eg. during a
+ loop), but with a sample time set so that the time difference between each update is
+ (close to) constant. If set to None, the PID will compute a new output value every time
+ it is called.
+ :param output_limits: The initial output limits to use, given as an iterable with 2
+ elements, for example: (lower, upper). The output will never go below the lower limit
+ or above the upper limit. Either of the limits can also be set to None to have no limit
+ in that direction. Setting output limits also avoids integral windup, since the
+ integral term will never be allowed to grow outside of the limits.
+ :param auto_mode: Whether the controller should be enabled (auto mode) or not (manual mode)
+ :param proportional_on_measurement: Whether the proportional term should be calculated on
+ the input directly rather than on the error (which is the traditional way). Using
+ proportional-on-measurement avoids overshoot for some types of systems.
+ :param error_map: Function to transform the error value in another constrained value.
+ """
+ self.Kp, self.Ki, self.Kd = Kp, Ki, Kd
+ self.setpoint = setpoint
+ self.sample_time = sample_time
+
+ self._min_output, self._max_output = None, None
+ self._auto_mode = auto_mode
+ self.proportional_on_measurement = proportional_on_measurement
+ self.error_map = error_map
+
+ self._proportional = 0
+ self._integral = 0
+ self._derivative = 0
+
+ self._last_time = None
+ self._last_output = None
+ self._last_input = None
+
+ self.output_limits = output_limits
+ self.reset()
+
+ def __call__(self, input_, dt=None):
+ """
+ Update the PID controller.
+
+ Call the PID controller with *input_* and calculate and return a control output if
+ sample_time seconds has passed since the last update. If no new output is calculated,
+ return the previous output instead (or None if no value has been calculated yet).
+
+ :param dt: If set, uses this value for timestep instead of real time. This can be used in
+ simulations when simulation time is different from real time.
+ """
+ if not self.auto_mode:
+ return self._last_output
+
+ now = _current_time()
+ if dt is None:
+ dt = now - self._last_time if (now - self._last_time) else 1e-16
+ elif dt <= 0:
+ raise ValueError('dt has negative value {}, must be positive'.format(dt))
+
+ if self.sample_time is not None and dt < self.sample_time and self._last_output is not None:
+ # Only update every sample_time seconds
+ return self._last_output
+
+ # Compute error terms
+ error = self.setpoint - input_
+ d_input = input_ - (self._last_input if (self._last_input is not None) else input_)
+
+ # Check if must map the error
+ if self.error_map is not None:
+ error = self.error_map(error)
+
+ # Compute the proportional term
+ if not self.proportional_on_measurement:
+ # Regular proportional-on-error, simply set the proportional term
+ self._proportional = self.Kp * error
+ else:
+ # Add the proportional error on measurement to error_sum
+ self._proportional -= self.Kp * d_input
+
+ # Compute integral and derivative terms
+ self._integral += self.Ki * error * dt
+ self._integral = _clamp(self._integral, self.output_limits) # Avoid integral windup
+
+ self._derivative = -self.Kd * d_input / dt
+
+ # Compute final output
+ output = self._proportional + self._integral + self._derivative
+ output = _clamp(output, self.output_limits)
+
+ # Keep track of state
+ self._last_output = output
+ self._last_input = input_
+ self._last_time = now
+
+ return output
+
+ def __repr__(self):
+ return (
+ '{self.__class__.__name__}('
+ 'Kp={self.Kp!r}, Ki={self.Ki!r}, Kd={self.Kd!r}, '
+ 'setpoint={self.setpoint!r}, sample_time={self.sample_time!r}, '
+ 'output_limits={self.output_limits!r}, auto_mode={self.auto_mode!r}, '
+ 'proportional_on_measurement={self.proportional_on_measurement!r},'
+ 'error_map={self.error_map!r}'
+ ')'
+ ).format(self=self)
+
+ @property
+ def components(self):
+ """
+ The P-, I- and D-terms from the last computation as separate components as a tuple. Useful
+ for visualizing what the controller is doing or when tuning hard-to-tune systems.
+ """
+ return self._proportional, self._integral, self._derivative
+
+ @property
+ def tunings(self):
+ """The tunings used by the controller as a tuple: (Kp, Ki, Kd)."""
+ return self.Kp, self.Ki, self.Kd
+
+ @tunings.setter
+ def tunings(self, tunings):
+ """Set the PID tunings."""
+ self.Kp, self.Ki, self.Kd = tunings
+
+ @property
+ def auto_mode(self):
+ """Whether the controller is currently enabled (in auto mode) or not."""
+ return self._auto_mode
+
+ @auto_mode.setter
+ def auto_mode(self, enabled):
+ """Enable or disable the PID controller."""
+ self.set_auto_mode(enabled)
+
+ def set_auto_mode(self, enabled, last_output=None):
+ """
+ Enable or disable the PID controller, optionally setting the last output value.
+
+ This is useful if some system has been manually controlled and if the PID should take over.
+ In that case, disable the PID by setting auto mode to False and later when the PID should
+ be turned back on, pass the last output variable (the control variable) and it will be set
+ as the starting I-term when the PID is set to auto mode.
+
+ :param enabled: Whether auto mode should be enabled, True or False
+ :param last_output: The last output, or the control variable, that the PID should start
+ from when going from manual mode to auto mode. Has no effect if the PID is already in
+ auto mode.
+ """
+ if enabled and not self._auto_mode:
+ # Switching from manual mode to auto, reset
+ self.reset()
+
+ self._integral = last_output if (last_output is not None) else 0
+ self._integral = _clamp(self._integral, self.output_limits)
+
+ self._auto_mode = enabled
+
+ @property
+ def output_limits(self):
+ """
+ The current output limits as a 2-tuple: (lower, upper).
+
+ See also the *output_limits* parameter in :meth:`PID.__init__`.
+ """
+ return self._min_output, self._max_output
+
+ @output_limits.setter
+ def output_limits(self, limits):
+ """Set the output limits."""
+ if limits is None:
+ self._min_output, self._max_output = None, None
+ return
+
+ min_output, max_output = limits
+
+ if (None not in limits) and (max_output < min_output):
+ raise ValueError('lower limit must be less than upper limit')
+
+ self._min_output = min_output
+ self._max_output = max_output
+
+ self._integral = _clamp(self._integral, self.output_limits)
+ self._last_output = _clamp(self._last_output, self.output_limits)
+
+ def reset(self):
+ """
+ Reset the PID controller internals.
+
+ This sets each term to 0 as well as clearing the integral, the last output and the last
+ input (derivative calculation).
+ """
+ self._proportional = 0
+ self._integral = 0
+ self._derivative = 0
+
+ self._integral = _clamp(self._integral, self.output_limits)
+
+ self._last_time = _current_time()
+ self._last_output = None
+ self._last_input = None
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..5bd405f
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,47 @@
+Micropython project for Raspberry Pi Pico.
+Collects data from a onewire temperature sensor
+and feeds it to MQTT on a wireless connection.
+
+Before you can use it you need to:
+- connect onewire sensor to Pico,
+- set up MQTT broker on a server uneder your control,
+- optionally, but highly advisable, setup ntpd server in local network,
+- set up certificates,
+- provide settings in config.py. Skeleton in config.py.example
+
+Notes:
+
+In order to test Pico, best use Thonny.
+At the time of writing this (Jun 2024), you won't be able to see live
+interpreter in PyCharm
+Device path is /dev/ttyACM0
+To connect to it, add your user to dialout group.
+Alternatively change permissions on that device to something like:
+chmod o+rw /dev/ttyACM0
+Or change owner on that device to your user
+
+To address pin 25 with LED use "LED":
+from machine import Pin
+led = Pin("LED", Pin.OUT)
+
+Before using tls (mosquitto, requests) you'll need to set time, for instance:
+import ntptime
+ntptime.settime()
+Be aware that default timeout on a socket there is 1 second, which may not be enough.
+I'm using usocket directly so that I can set timeout to a larger value.
+The hard part is to deal with timeout from ntp server.
+I managed to cut down timeouts drastically by running my own ntp server on local network.
+
+Another hard part is setting up certificates so that umqttsimple can use them.
+TLDR, do it like that: https://github.com/JustinS-B/Mosquitto_CA_and_Certs/blob/main/client_maker
+
+Update:
+Script from that link, now dead, provided here as client_maker.sh
+
+For background information: https://github.com/orgs/micropython/discussions/10559
+
+Script for server running MQTT reading data from MQTT and updating rrd database
+in separate project, with Bash script generating rrd database and
+generating graph from this database.
+
+Controlling temperature with PID not tested.
diff --git a/class_diagram.png b/class_diagram.png
new file mode 100644
index 0000000..f8dbd1d
--- /dev/null
+++ b/class_diagram.png
Binary files differ
diff --git a/class_diagram.txt b/class_diagram.txt
new file mode 100644
index 0000000..84398ff
--- /dev/null
+++ b/class_diagram.txt
@@ -0,0 +1,33 @@
+@startuml
+class Wifi {
+ str essid
+ str pwd
+ str country
+ void wlan
+ wlan connection()
+}
+class TempSensor {
+ int read_retry
+ sensors sensors
+ void sensor_address
+ bool create_sensor()
+ int get_temperature()
+}
+class MqttClient {
+ str id
+ str server
+ int port
+ str user
+ str pwd
+ bool do_ssl
+ dict ssl_params
+ client mqtt_connect()
+ {static} void mqtt_reconnect()
+}
+class global {
+ .. global functions ..
+ ==
+ int mosfet_set(int)
+ int pid_value(float)
+}
+@enduml
diff --git a/client_maker.sh b/client_maker.sh
new file mode 100755
index 0000000..c5466b7
--- /dev/null
+++ b/client_maker.sh
@@ -0,0 +1,150 @@
+#!/bin/bash
+
+# you call this script using ./client_maker pem/der username <optional keytype RSA/EC/ED25519>
+# eg ./client_maker pem user1 OR ./client_maker der user2
+# The script WILL NOT DELETE ANYTHING - it just renames any certs dir that it finds
+
+# You will probably need to change this for your system
+mosquitto_dir="/etc/mosquitto"
+
+# Note: Pico W can use EC or RSA keys only, not EC25519, and Tasmota can only use 2048 bit RSA keys.
+
+# Default choice of Key Types: RSA, EC, or ED25519 (if it hasn't been given as Argument $3)
+key_type='EC'
+
+# Choice of NIST Curves for EC Keys: P-256, P-384 or P-521
+curve='P-256'
+
+# Choice of Bits for RSA Keys: 2048 or 4096
+rsa_bits='2048'
+
+# How many days is the Cert valid for?
+days='365'
+
+############################################################
+# End of user defined variables
+############################################################
+
+# Sanity check: have you called the script correctly?
+[ -z "$1" ] | [ -z "$2" ] && printf "\n Missing aurguments...\n\n Enter either DEM or PEM then the username \n eg: client_maker pem user1 \n or client_maker der user2\n\n You can also override the default Key Type by adding RSA, EC, or ED25519 as an optional third argument\n\n eg: client_maker pem user1 EC\n\n" && exit 1
+
+# If Argument $3 has been given, override the default key_type given above
+if [ -n "$3" ]
+then
+ key_type=$3
+fi
+
+
+# Which output Format Type do we need to use?
+if [ $1 = 'PEM' ] || [ $1 = 'pem' ]; then
+format_type="pem"
+elif [ $1 = 'DER' ] || [ $1 = 'der' ]; then
+format_type="der"
+fi
+
+# Set the algorithm
+algorithm="-algorithm ${key_type}"
+
+# Set the specific pkeyopt for the chosen algorithm (BLANK for ED25519)
+if [ "${key_type}" == "EC" ]; then
+ echo 'Create EC Key'
+ pkeyopt="-pkeyopt ec_paramgen_curve:${curve}"
+elif [ "${key_type}" == "RSA" ]; then
+ echo 'Create RSA Key'
+ pkeyopt="-pkeyopt rsa_keygen_bits:${rsa_bits}"
+elif [ "${key_type}" == "ED25519" ]; then
+ echo 'Create ED25519 Key'
+ pkeyopt=""
+else
+ echo 'Key Type not found'
+fi
+
+############################################################
+# Backup existing certs and create dir structure
+############################################################
+
+# if our user certs dir already exists, rename it so we don't overwrite anything important
+# but if it doesn't, then redirect the 'No such file or directory' error to null
+time_stamp=$(date +"%Y-%m-%d_%H-%M")
+mv $mosquitto_dir/certs/csr_files/$2_req.csr $mosquitto_dir/certs/clients/$2 2>/dev/null
+mv $mosquitto_dir/certs/clients/$2 $mosquitto_dir/certs/clients/$2-$time_stamp 2>/dev/null
+
+mkdir -p $mosquitto_dir/certs/clients/$2
+
+
+############################################################
+# Create the key in the requested format
+############################################################
+
+openssl genpkey \
+$algorithm $pkeyopt \
+-outform $format_type \
+-out $mosquitto_dir/certs/clients/$2/$2_key.$format_type
+
+
+############################################################
+# Create the cert signing request
+############################################################
+
+openssl req \
+-new \
+-nodes \
+-key $mosquitto_dir/certs/clients/$2/$2_key.$format_type \
+-subj "/CN=$2" \
+-out $mosquitto_dir/certs/clients/$2/$2_req.csr
+
+
+printf '\n\n'
+echo "#######################################################################"
+printf '\n\n'
+
+############################################################
+# Cert signing and creation
+############################################################
+
+openssl x509 \
+-req \
+-in $mosquitto_dir/certs/clients/$2/$2_req.csr \
+-CA $mosquitto_dir/certs/CA/ca_crt.pem \
+-CAkey $mosquitto_dir/certs/CA/ca_key.pem \
+-CAcreateserial \
+-out $mosquitto_dir/certs/clients/$2/$2_crt.$format_type -outform $format_type -days $days
+
+printf '\n\n'
+echo "#######################################################################"
+printf '\n\n'
+
+############################################################
+# Check the cert
+############################################################
+
+printf '\n'
+printf '# This is your new client certificate\n\n\n'
+
+openssl x509 -text -in $mosquitto_dir/certs/clients/$2/$2_crt.$format_type -noout
+
+printf '\n\n'
+echo "#######################################################################"
+printf '\n\n'
+
+############################################################
+# Housekeeping
+############################################################
+
+#Change the permissions on the key file to give read access so that whatever we need it for can read it
+chmod 644 $mosquitto_dir/certs/clients/$2/$2_key.$format_type
+
+#clean up after the client cert creation
+mv $mosquitto_dir/certs/clients/$2/$2_req.csr $mosquitto_dir/certs/csr_files
+
+# copy ca_crt.{der,pem} in the required format to the new client dir
+cp $mosquitto_dir/certs/clients/ca_crt.$format_type $mosquitto_dir/certs/clients/$2
+
+
+echo "# Here are the client files"
+
+ls -bl $mosquitto_dir/certs/clients/$2
+
+printf '\n\n'
+
+echo "#######################################################################"
diff --git a/config.py.example b/config.py.example
new file mode 100644
index 0000000..b6070ee
--- /dev/null
+++ b/config.py.example
@@ -0,0 +1,39 @@
+"""
+Settings for main.py
+"""
+
+# Wlan settings:
+WLAN_NAME =
+WLAN_PWD =
+COUNTRY =
+
+# PID settings
+KP = 1
+KI = 0
+KD = 0
+TARGET_TEMPERATURE = 24
+SAMPLE_TIME = 900
+
+# NTP server address:
+NTP_SERVER = 'pool.ntp.org'
+# Depends on your location:
+NTP_OFFSET =
+
+# MQTT settings:
+MQTT_SERVER_IP =
+MQTT_SERVER =
+MQTT_PORT = 8883
+MQTT_CLIENT_ID =
+MQTT_USER =
+MQTT_TOPIC = 'temperature'
+
+# TLS settings:
+SSL_CERT_FILE = "pico_crt.der"
+SSL_KEY_FILE = "pico_key.der"
+SSL_CADATA_FILE = "ca_crt.der"
+
+# Mosfet pin:
+MOSFET_PIN = 5
+
+# onewire pin:
+ONEWIRE_PIN = 17
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..3bc072d
--- /dev/null
+++ b/main.py
@@ -0,0 +1,388 @@
+"""
+Micropython module reading data from onewire sensor,
+connecting to wifi and sending temperature data to MQTT server.
+"""
+
+import ssl
+import time
+from math import isnan
+
+from ubinascii import hexlify
+from ds18x20 import DS18X20
+from onewire import OneWire
+import rp2
+from machine import Pin, PWM, reset, RTC
+from network import WLAN, STA_IF
+
+import utime
+import usocket
+import ustruct
+
+from PID import PID
+from umqttsimple import MQTTClient
+
+# Import settings from config.py:
+from config import *
+
+# That led is different for different MicroPython versions:
+led = Pin("LED", Pin.OUT)
+
+# Blinking communication:
+
+# 1 slow sequence of one: on initializatio
+# 2 fast sequences of two: no wifi or ntptime.settime() failed
+# 3 fast sequences of two: no mqtt
+# 2 fast sequences of three: no certificate files
+
+
+def blink(n=1, interval=0.5, repeat=0):
+ """
+ Blink to communicate failures.
+ :param n: number of flashes
+ :param interval: time interval between flashes
+ :param repeat: how many times to repeat sequence
+ :return: int
+ """
+ retval = 1
+ r = 0
+ try:
+ while repeat >= r:
+ for i in range(n):
+ led.on()
+ time.sleep(interval)
+ led.off()
+ time.sleep(interval)
+ r += 1
+ time.sleep(1)
+ retval = 0
+ except:
+ pass
+ return retval
+
+
+def create_wifi():
+ """
+ Create wireless connection. Takes SSID and password from global variables atm.
+ :return: wlan object, return value, connection state
+ """
+ wlan_retval = 1
+ wlan = WLAN(STA_IF)
+ wlan.active(True)
+ rp2.country(COUNTRY)
+ try: # FIXME: this often raises EPERM, find out if it's code or buggy wifi repeater
+ wlan.connect(WLAN_NAME, WLAN_PWD)
+ time.sleep(0.5)
+ wlan_retval = 0
+ except Exception as exc:
+ print("Failed to connect to wlan: ", exc)
+ return wlan, wlan_retval, wlan.isconnected()
+
+
+def _time():
+ """
+ Get time from NTP server.
+ :return: current time
+ """
+ cur_time, sock = None, None
+ counter = 0
+ # try 5 times to get time from NTP server:
+ while counter < 5:
+ try:
+ NTP_QUERY = bytearray(48)
+ NTP_QUERY[0] = 0x1B
+ addr = (NTP_SERVER, 123)
+ sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
+ sock.settimeout(10) # was 1 second
+ res = sock.sendto(NTP_QUERY, addr)
+ msg = sock.recv(48)
+ val = ustruct.unpack("!I", msg[40:44])[0]
+ cur_time = val - NTP_OFFSET
+ print("NTP time with offset is: ", cur_time)
+ break
+ except OSError as exc:
+ if exc.args[0] == 110: # ETIMEDOUT
+ print("Timed out getting NTP time.")
+ utime.sleep(2)
+ counter += 1
+ continue
+ print("Error from _time(): ", exc)
+ counter = 5
+ except Exception as exc:
+ print("Getting time failed: ", exc)
+ counter = 5
+ finally:
+ if sock:
+ sock.close()
+ if counter == 5:
+ print("Resetting board!!!")
+ reset()
+ return cur_time
+
+
+def set_time():
+ """
+ Set time.
+ :return: int
+ """
+ print("Calling ntptime.time() ...")
+ retval = 1
+ # t will never be less than zero on success:
+ t = -1
+ try:
+ t = _time()
+ except Exception as exc:
+ print("Exception calling ntptime.time: ", exc)
+ if t == -1:
+ # Getting time from NTP failed, return early:
+ print("Getting time failed, returning early from set_time()")
+ return retval
+ try:
+ tm = utime.gmtime(t)
+ except Exception as exc:
+ print("Failed to set tm = utime.gmtime(t)", exc)
+ try:
+ RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
+ retval = 0
+ except Exception as exc:
+ print("Failed to RTC().datetime ... : ", exc)
+ return retval
+
+
+def connect_wifi(wlan):
+ """
+ Connect to wifi.
+ :return: int
+ """
+ retcode = 1 # 1 for failure, 0 for success
+ r = 0
+ while not wlan.isconnected() and r < 3:
+ try:
+ wlan.connect()
+ time.sleep(1)
+ r += 1
+ except:
+ blink(3, 0.2)
+ r += 1
+ if wlan.isconnected():
+ retcode = 0
+ return retcode
+
+
+def load_certs():
+ """
+ Load certificates from files
+ :return: ssl parameters
+ """
+ try:
+ with open(SSL_CERT_FILE, 'rb') as _file:
+ SSL_CERT = _file.read()
+ except Exception as exc:
+ print(f"Reading {SSL_CERT_FILE} failed: {exc}")
+ led.off()
+ try:
+ with open(SSL_KEY_FILE, 'rb') as _file:
+ SSL_KEY = _file.read()
+ except Exception as exc:
+ print(f"Reading {SSL_KEY_FILE} failed: {exc}")
+ led.off()
+ try:
+ with open(SSL_CADATA_FILE, 'rb') as _file:
+ SSL_CADATA = _file.read()
+ except Exception as exc:
+ print(f"Reading {SSL_CADATA_FILE} failed: {exc}")
+ led.off()
+ ssl_params = {
+ "key": SSL_KEY,
+ "cert": SSL_CERT,
+ "cadata": SSL_CADATA,
+ "server_hostname": MQTT_SERVER,
+ "server_side": False,
+ "cert_reqs": ssl.CERT_REQUIRED,
+ "do_handshake": True,
+ }
+ return ssl_params
+
+
+def create_mqtt():
+ """
+ Instantiate MQTT client
+ :return: mqtt client
+ """
+ mqtt_client = None
+ try:
+ ssl_params = load_certs()
+ except Exception as exc:
+ print("failed to create mqtt client: ", exc)
+ blink(3, 0.3, 2)
+ ssl_params = None
+ if ssl_params:
+ try:
+ mqtt_client = MQTTClient(
+ client_id=MQTT_CLIENT_ID,
+ server=MQTT_SERVER_IP,
+ port=MQTT_PORT,
+ keepalive=60,
+ ssl=True,
+ ssl_params=ssl_params,
+ )
+ except:
+ blink()
+ return mqtt_client
+
+
+def connect_mqtt(client):
+ """
+ Connect MQTT client.
+ :return: int
+ """
+ retval = 1
+ try:
+ client.connect()
+ retval = 0
+ # reset to clear memory on OSError:
+ except OSError as exc:
+ print("OSError: ", exc)
+ if exc == 'Exception in thread rx':
+ pass
+ print("OSError repr: ", repr(exc))
+ print("OSError encountered, reseting ...")
+ reset()
+ except Exception as exc:
+ print("Failed to connect to mqtt: ", exc)
+ blink(2, 0.2, 3)
+ return retval
+
+
+def get_temperature():
+ """
+ Get temperatures and ids from one wire sensors.
+ :return: (id: str, temperature: float)
+ """
+ sensor = DS18X20(OneWire(Pin(ONEWIRE_PIN)))
+ sensor.convert_temp()
+ roms = sensor.scan()
+ _temperatures = None
+ time.sleep(1)
+ try:
+ _temperatures = [(hexlify(_id).decode(), str(round(float(sensor.read_temp(_id)), 1))) for _id in roms]
+ except Exception as exc:
+ print("Failed to get temperatures: ", exc)
+ return _temperatures
+
+
+class Mosfet:
+ """
+ Mosfet class.
+ """
+ def __init__(self):
+ self.mosfet_pin = Pin(MOSFET_PIN, Pin.OUT)
+ self.mosfet = PWM(self.mosfet_pin)
+
+ @staticmethod
+ def pid_value(
+ cur_temp
+ ):
+ """
+ Calculate PID value.
+ :param cur_temp: float
+ :return: float
+ """
+ if isnan(cur_temp):
+ retval = 0
+ else:
+ pid = PID(
+ KP,
+ KI,
+ KD,
+ setpoint=TARGET_TEMPERATURE,
+ output_limits=(0, 65535),
+ sample_time=SAMPLE_TIME,
+ )
+ retval = pid(cur_temp)
+ return retval
+
+ def set(
+ self,
+ value,
+ ):
+ """
+ Set mosfet value.
+ """
+ if value not in range(0, 65535):
+ print('Mosfet value not in range.')
+ try:
+ # Valid values in range 0-65535:
+ self.mosfet.duty_u16(value)
+ except Exception as exc:
+ led.off()
+ print(f"Setting mosfet value failed: {exc}")
+ raise
+
+
+WLAN_UNINITIALISED = CLIENT_UNINITIALISED = WLAN_RETVAL = TIME_RETVAL = MQTT_RETVAL = 1
+temperatures, client, wlan = None, None, None
+WLAN_CONNECTED = False
+while True:
+ # Create connection object only once:
+ if WLAN_UNINITIALISED:
+ print("Creating wlan... ")
+ blink(1, 1, 1)
+ wlan, WLAN_UNINITIALISED, WLAN_CONNECTED = create_wifi()
+ if WLAN_CONNECTED:
+ WLAN_RETVAL = 0
+ if wlan and not WLAN_UNINITIALISED and not WLAN_CONNECTED:
+ try:
+ print("Connecting to wifi ...")
+ WLAN_RETVAL = connect_wifi(wlan)
+ print("WLAN_RETVAL: ", WLAN_RETVAL)
+ print("Wlan connected: ", wlan.isconnected())
+ except Exception as exc:
+ print('Failed to connect to wifi: ', exc)
+ if TIME_RETVAL and wlan.isconnected():
+ print("Setting time: ...")
+ TIME_RETVAL = set_time()
+ print("TIME_RETVAL: ", TIME_RETVAL)
+ if CLIENT_UNINITIALISED and not WLAN_RETVAL and not TIME_RETVAL:
+ print("Creating client ...")
+ try:
+ client = create_mqtt()
+ CLIENT_UNINITIALISED = 0
+ except Exception as exc:
+ print("Won't be any client: ", exc)
+ if MQTT_RETVAL and not WLAN_RETVAL and not TIME_RETVAL and client:
+ try:
+ print("Connecting to mqtt broker ...")
+ MQTT_RETVAL = connect_mqtt(client)
+ print("MQTT_RETVAL: ", MQTT_RETVAL)
+ except Exception as exc:
+ print("Failed to connect to mqtt broker: ", exc)
+ if MQTT_RETVAL:
+ print("Failed to connect to mqtt broker: ", MQTT_RETVAL)
+ try:
+ temperatures = get_temperature()
+ print("Temperatures: ", temperatures)
+ except Exception as exc:
+ print("Failed to get temperature(s): ", exc)
+ if not WLAN_RETVAL and not MQTT_RETVAL and not TIME_RETVAL and temperatures:
+ try:
+ for _id, temp in temperatures:
+ print(f"Publishing temperature: sensor id: {_id} - {temp} ...")
+ client.publish(f'temperature/{_id}', temp)
+ client.check_msg()
+ except Exception as exc:
+ print("Exception publishing: ", exc)
+ blink(3, 0.2, 1)
+ else:
+ print(f"Failed to publish:\nWLAN_RETVAL: {WLAN_RETVAL}\nMQTT_RETVAL: {MQTT_RETVAL}\nTIME_RETVAL: {TIME_RETVAL}\n")
+ print("Going to sleep for 10")
+ time.sleep(10)
+# ntptime.settime() kills the board. Check out solutions at:
+# https://forum.micropython.org/viewtopic.php?f=20&t=10221&start=10
+# try:
+# pid_val = mft.pid_value(temp)
+# mft.set(pid_val)
+# except Exception as exc:
+# led.off()
+# print(f"Setting mosfet value failed: {exc}")
+# pass
+# time.sleep(10)
diff --git a/umqttsimple.py b/umqttsimple.py
new file mode 100644
index 0000000..44d1dde
--- /dev/null
+++ b/umqttsimple.py
@@ -0,0 +1,214 @@
+import usocket as socket
+import ustruct as struct
+
+
+class MQTTException(Exception):
+ pass
+
+
+class MQTTClient:
+ def __init__(
+ self,
+ client_id,
+ server,
+ port=0,
+ user=None,
+ password=None,
+ keepalive=0,
+ ssl=False,
+ ssl_params={},
+ ):
+ if port == 0:
+ port = 8883 if ssl else 1883
+ self.client_id = client_id
+ self.sock = None
+ self.server = server
+ self.port = port
+ self.ssl = ssl
+ self.ssl_params = ssl_params
+ self.pid = 0
+ self.cb = None
+ self.user = user
+ self.pswd = password
+ self.keepalive = keepalive
+ self.lw_topic = None
+ self.lw_msg = None
+ self.lw_qos = 0
+ self.lw_retain = False
+
+ def _send_str(self, s):
+ self.sock.write(struct.pack("!H", len(s)))
+ self.sock.write(s)
+
+ def _recv_len(self):
+ n = 0
+ sh = 0
+ while 1:
+ b = self.sock.read(1)[0]
+ n |= (b & 0x7F) << sh
+ if not b & 0x80:
+ return n
+ sh += 7
+
+ def set_callback(self, f):
+ self.cb = f
+
+ def set_last_will(self, topic, msg, retain=False, qos=0):
+ assert 0 <= qos <= 2
+ assert topic
+ self.lw_topic = topic
+ self.lw_msg = msg
+ self.lw_qos = qos
+ self.lw_retain = retain
+
+ def connect(self, clean_session=True):
+ self.sock = socket.socket()
+ addr = socket.getaddrinfo(self.server, self.port)[0][-1]
+ self.sock.connect(addr)
+ if self.ssl:
+ import ussl
+
+ self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
+ premsg = bytearray(b"\x10\0\0\0\0\0")
+ msg = bytearray(b"\x04MQTT\x04\x02\0\0")
+
+ sz = 10 + 2 + len(self.client_id)
+ msg[6] = clean_session << 1
+ if self.user is not None:
+ sz += 2 + len(self.user) + 2 + len(self.pswd)
+ msg[6] |= 0xC0
+ if self.keepalive:
+ assert self.keepalive < 65536
+ msg[7] |= self.keepalive >> 8
+ msg[8] |= self.keepalive & 0x00FF
+ if self.lw_topic:
+ sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
+ msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
+ msg[6] |= self.lw_retain << 5
+
+ i = 1
+ while sz > 0x7F:
+ premsg[i] = (sz & 0x7F) | 0x80
+ sz >>= 7
+ i += 1
+ premsg[i] = sz
+
+ self.sock.write(premsg, i + 2)
+ self.sock.write(msg)
+ # print(hex(len(msg)), hexlify(msg, ":"))
+ self._send_str(self.client_id)
+ if self.lw_topic:
+ self._send_str(self.lw_topic)
+ self._send_str(self.lw_msg)
+ if self.user is not None:
+ self._send_str(self.user)
+ self._send_str(self.pswd)
+ resp = self.sock.read(4)
+ assert resp[0] == 0x20 and resp[1] == 0x02
+ if resp[3] != 0:
+ raise MQTTException(resp[3])
+ return resp[2] & 1
+
+ def disconnect(self):
+ self.sock.write(b"\xe0\0")
+ self.sock.close()
+
+ def ping(self):
+ self.sock.write(b"\xc0\0")
+
+ def publish(self, topic, msg, retain=False, qos=0):
+ pkt = bytearray(b"\x30\0\0\0")
+ pkt[0] |= qos << 1 | retain
+ sz = 2 + len(topic) + len(msg)
+ if qos > 0:
+ sz += 2
+ assert sz < 2097152
+ i = 1
+ while sz > 0x7F:
+ pkt[i] = (sz & 0x7F) | 0x80
+ sz >>= 7
+ i += 1
+ pkt[i] = sz
+ # print(hex(len(pkt)), hexlify(pkt, ":"))
+ self.sock.write(pkt, i + 1)
+ self._send_str(topic)
+ if qos > 0:
+ self.pid += 1
+ pid = self.pid
+ struct.pack_into("!H", pkt, 0, pid)
+ self.sock.write(pkt, 2)
+ self.sock.write(msg)
+ if qos == 1:
+ while 1:
+ op = self.wait_msg()
+ if op == 0x40:
+ sz = self.sock.read(1)
+ assert sz == b"\x02"
+ rcv_pid = self.sock.read(2)
+ rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
+ if pid == rcv_pid:
+ return
+ elif qos == 2:
+ assert 0
+
+ def subscribe(self, topic, qos=0):
+ assert self.cb is not None, "Subscribe callback is not set"
+ pkt = bytearray(b"\x82\0\0\0")
+ self.pid += 1
+ struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
+ # print(hex(len(pkt)), hexlify(pkt, ":"))
+ self.sock.write(pkt)
+ self._send_str(topic)
+ self.sock.write(qos.to_bytes(1, "little"))
+ while 1:
+ op = self.wait_msg()
+ if op == 0x90:
+ resp = self.sock.read(4)
+ # print(resp)
+ assert resp[1] == pkt[2] and resp[2] == pkt[3]
+ if resp[3] == 0x80:
+ raise MQTTException(resp[3])
+ return
+
+ # Wait for a single incoming MQTT message and process it.
+ # Subscribed messages are delivered to a callback previously
+ # set by .set_callback() method. Other (internal) MQTT
+ # messages processed internally.
+ def wait_msg(self):
+ res = self.sock.read(1)
+ #self.sock.setblocking(True)
+ if res is None:
+ return None
+ if res == b"":
+ raise OSError(-1)
+ if res == b"\xd0": # PINGRESP
+ sz = self.sock.read(1)[0]
+ assert sz == 0
+ return None
+ op = res[0]
+ if op & 0xF0 != 0x30:
+ return op
+ sz = self._recv_len()
+ topic_len = self.sock.read(2)
+ topic_len = (topic_len[0] << 8) | topic_len[1]
+ topic = self.sock.read(topic_len)
+ sz -= topic_len + 2
+ if op & 6:
+ pid = self.sock.read(2)
+ pid = pid[0] << 8 | pid[1]
+ sz -= 2
+ msg = self.sock.read(sz)
+ self.cb(topic, msg)
+ if op & 6 == 2:
+ pkt = bytearray(b"\x40\x02\0\0")
+ struct.pack_into("!H", pkt, 2, pid)
+ self.sock.write(pkt)
+ elif op & 6 == 4:
+ assert 0
+
+ # Checks whether a pending message from server is available.
+ # If not, returns immediately with None. Otherwise, does
+ # the same processing as wait_msg.
+ def check_msg(self):
+ self.sock.setblocking(False)
+ return self.wait_msg() \ No newline at end of file