diff options
author | Erg <uinarf@autistici.org> | 2024-10-21 17:02:14 +0200 |
---|---|---|
committer | Erg <uinarf@autistici.org> | 2024-10-21 17:02:14 +0200 |
commit | 1be9a52f11d7bb9cf519c6ee8027c9029824e29c (patch) | |
tree | a71b0edf5c4748000a8f5515dd215a9052ef927e | |
download | Pico-1be9a52f11d7bb9cf519c6ee8027c9029824e29c.tar.gz Pico-1be9a52f11d7bb9cf519c6ee8027c9029824e29c.tar.bz2 Pico-1be9a52f11d7bb9cf519c6ee8027c9029824e29c.zip |
Comment change
-rw-r--r-- | PID.py | 242 | ||||
-rw-r--r-- | Readme.md | 47 | ||||
-rw-r--r-- | class_diagram.png | bin | 0 -> 17179 bytes | |||
-rw-r--r-- | class_diagram.txt | 33 | ||||
-rwxr-xr-x | client_maker.sh | 150 | ||||
-rw-r--r-- | config.py.example | 39 | ||||
-rw-r--r-- | main.py | 388 | ||||
-rw-r--r-- | umqttsimple.py | 214 |
8 files changed, 1113 insertions, 0 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..f8dbd1d --- /dev/null +++ b/class_diagram.png 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 @@ -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 |