Spaces:
Sleeping
Sleeping
| import json | |
| import queue | |
| import time | |
| import paho.mqtt.client as mqtt | |
| import streamlit as st | |
| import matplotlib.pyplot as plt | |
| from datetime import datetime, timezone | |
| # Initialize Streamlit app | |
| st.title("Fan Control Panel") | |
| # Description and context | |
| st.markdown( | |
| """ | |
| This application accesses a public test demo of a small computer fan (Canakit | |
| RPi 5 fan) controlled by a Pico W microcontroller via an EMC2101 fan controller. | |
| Send speed commands to the motor and visualize the RPM data. | |
| For context, see the [fan control | |
| code](https://github.com/AccelerationConsortium/ac-training-lab/tree/main/src/ac_training_lab/picow/fan-control) | |
| [[permalink](https://github.com/AccelerationConsortium/ac-training-lab/tree/a8b9cad0dc8c162bdff20c0d0fdff2f45c5f0012/src/ac_training_lab/picow/fan-control)] | |
| . You may also be interested in the Acceleration Consortium's ["Hello World" | |
| microcourse](https://ac-microcourses.readthedocs.io/en/latest/courses/hello-world/index.html) | |
| for self-driving labs. | |
| """ | |
| ) | |
| # MQTT Configuration | |
| with st.form("mqtt_form"): | |
| HIVEMQ_HOST = st.text_input( | |
| "Enter your HiveMQ host:", | |
| "248cc294c37642359297f75b7b023374.s2.eu.hivemq.cloud", | |
| type="password", | |
| ) | |
| HIVEMQ_USERNAME = st.text_input("Enter your HiveMQ username:", "sgbaird") | |
| HIVEMQ_PASSWORD = st.text_input( | |
| "Enter your HiveMQ password:", "D.Pq5gYtejYbU#L", type="password" | |
| ) | |
| PORT = st.number_input( | |
| "Enter the port number:", min_value=1, max_value=65535, value=8883 | |
| ) | |
| PICO_ID = st.text_input("Enter your Pico ID:", "test-fan", type="password") | |
| # Slider for fan speed | |
| SPEED = st.slider( | |
| "Select the Fan Speed (0-100%):", min_value=0, max_value=100, value=50 | |
| ) | |
| submit_button = st.form_submit_button(label="Send Speed Command") | |
| # Topics | |
| COMMAND_TOPIC = f"fan-control/picow/{PICO_ID}/speed" | |
| SENSOR_DATA_TOPIC = f"fan-control/picow/{PICO_ID}/rpm" | |
| # Queue for incoming sensor data | |
| rpm_data_queue = queue.Queue() | |
| # Initialize or load session state variables | |
| if "rpm_data" not in st.session_state: | |
| st.session_state.rpm_data = [] | |
| if "elapsed_times" not in st.session_state: | |
| st.session_state.elapsed_times = [] | |
| if "initial_timestamp" not in st.session_state: | |
| st.session_state.initial_timestamp = None | |
| # Singleton: https://docs.streamlit.io/develop/api-reference/caching-and-state/st.cache_resource | |
| def create_paho_client(tls=True): | |
| client = mqtt.Client(protocol=mqtt.MQTTv5) | |
| if tls: | |
| client.tls_set(tls_version=mqtt.ssl.PROTOCOL_TLS_CLIENT) | |
| return client | |
| # Define setup separately since sensor_data is dynamic | |
| def setup_paho_client( | |
| client, sensor_data_topic, hostname, username, password=None, port=8883 | |
| ): | |
| def on_message(client, userdata, msg): | |
| rpm_data_queue.put(json.loads(msg.payload)) | |
| def on_connect(client, userdata, flags, rc, properties=None): | |
| if rc == 0: | |
| print("Connected successfully") | |
| client.subscribe(sensor_data_topic, qos=1) | |
| else: | |
| print(f"Connection failed with code {rc}") | |
| client.on_connect = on_connect | |
| client.on_message = on_message | |
| client.username_pw_set(username, password) | |
| client.connect(hostname, port) | |
| client.loop_start() # Start non-blocking loop | |
| return client | |
| def send_command(client, command_topic, msg): | |
| print("Sending command...") | |
| result = client.publish(command_topic, json.dumps(msg), qos=2) | |
| result.wait_for_publish() # Ensure the message is sent | |
| if result.rc == mqtt.MQTT_ERR_SUCCESS: | |
| print(f"Command sent: {msg} to topic {command_topic}") | |
| else: | |
| print(f"Failed to send command: {result.rc}") | |
| # Function to plot RPM data | |
| fig, ax = plt.subplots() | |
| def plot_rpm_data(elapsed_times, rpms, placeholder): | |
| ax.clear() # Clear the previous plot | |
| # Filter data to only include the last 90 seconds | |
| current_time = elapsed_times[-1] | |
| filtered_times = [time for time in elapsed_times if current_time - time <= 90] | |
| filtered_rpms = rpms[-len(filtered_times) :] | |
| ax.plot(filtered_times, filtered_rpms, marker="o") | |
| ax.set_xlabel("Time (s)") | |
| ax.set_ylabel("RPM") | |
| ax.set_title("Fan RPM Over Time") | |
| ax.xaxis.set_major_locator(plt.MaxNLocator(10)) # Show max 10 ticks on x-axis | |
| plt.xticks(rotation=45) # Rotate x-axis labels for better readability | |
| placeholder.pyplot(fig) | |
| # Publish button | |
| if submit_button: | |
| if not PICO_ID or not HIVEMQ_HOST or not HIVEMQ_USERNAME or not HIVEMQ_PASSWORD: | |
| st.error("Please enter all required fields.") | |
| else: | |
| command_msg = {"speed": SPEED} | |
| st.info(f"Sending speed command {command_msg} to Pico ID {PICO_ID}...") | |
| client = create_paho_client(tls=True) | |
| client = setup_paho_client( | |
| client, | |
| SENSOR_DATA_TOPIC, | |
| HIVEMQ_HOST, | |
| HIVEMQ_USERNAME, | |
| password=HIVEMQ_PASSWORD, | |
| port=int(PORT), | |
| ) | |
| send_command(client, COMMAND_TOPIC, command_msg) | |
| # Create a placeholder for the plot | |
| plot_placeholder = st.empty() | |
| # Continuously fetch RPM data and update the plot | |
| while True: | |
| try: | |
| data = rpm_data_queue.get(timeout=30) | |
| rpm = data["rpm"] | |
| current_timestamp = datetime.fromtimestamp( | |
| data["utc_timestamp"], tz=timezone.utc | |
| ) | |
| if st.session_state.initial_timestamp is None: | |
| st.session_state.initial_timestamp = current_timestamp | |
| elapsed_time = ( | |
| current_timestamp - st.session_state.initial_timestamp | |
| ).total_seconds() | |
| st.session_state.rpm_data.append(rpm) | |
| st.session_state.elapsed_times.append(elapsed_time) | |
| plot_rpm_data( | |
| st.session_state.elapsed_times, | |
| st.session_state.rpm_data, | |
| plot_placeholder, | |
| ) | |
| except queue.Empty: | |
| st.error("No sensor data received within the timeout period.") | |
| break | |
| # Stop the MQTT loop | |
| client.loop_stop() | |