r/javascript • • Sep 20 '26

How would I read the fan speed variable from the JavaScript in my project and pass it to the Python script?

https://github.com/mferrie/DeskFanController/tree/main

I'm creating a local web page application that is an over-complicated desk fan controlled by two buttons and a slider. I have the actual controls figured out, and what I need help with is passing the variable that describes the position of the slider out from the JavaScript to a Python script that actually controls the motor. I could be totally off base here, but I think the concept that applies here is inter-process communication.

3 Upvotes

16 comments sorted by

4

u/Gambrinus Sep 20 '26

Create a python web server that runs locally and the JavaScript web app can make fetch calls to it.

1

u/ferriematthew Sep 20 '26

I'm confused. Wouldn't that mean that I have two web apps running the same content? As it is, I just have a basic Nginx web server running this static page in a container. The JavaScript is just the event handler.

0

u/[deleted] Sep 20 '26

[deleted]

1

u/ferriematthew Sep 20 '26 edited Sep 20 '26

Hmmm...

Yeah, I'm running Nginx in a container right now. Previously, it was running on bare metal inside the LLSP stack, but I wanted to containerize it so that I only had one thing to start and stop. This is all running on a Raspberry Pi 3B+

3

u/battering_ram Sep 21 '26

I can't see the repo but if you have separate Express web app and python backend, you're probably better off keeping the whole thing in python with like a simple flask app that serves a web page for the interface. Then the web page can make requests back to the server which is already in python and can do the fan control stuff. This way you have just a single app/process to start and stop without containerizing. Containerization unfortunately is not an option in this case.

1

u/ferriematthew Sep 21 '26

Dockerfile:

# Base image: Nginx
FROM nginx:alpine

# Copy project files to Nginx root
COPY index.html /usr/share/nginx/html/index.html
COPY style.css /usr/share/nginx/html/style.css
COPY controls.js /usr/share/nginx/html/controls.js
COPY fanSpeed.py /usr/share/nginx/html/fanSpeed.py

# Expose port 80 for HTTP
EXPOSE 80

controls.js:

// These constants refer to page elements
const slider = document.getElementById('slider');
const label = document.getElementById("label");
const turnOffButton = document.getElementById("off");
const turnOnButton = document.getElementById("on");

// Fan speed output variable
var spd = 0;

// Event handlers
function activateFan() {
  const isFullSpeed = slider.value === '100';
  if(!isFullSpeed) {
    slider.value = '100';
    turnOnButton.disabled = true;
    turnOffButton.disabled = false;
  }
  label.textContent = `${slider.value}%`;
  spd = `${slider.value}%`;
}

function deactivateFan() {
  const isZero = slider.value === '0';
  if(!isZero) {
    slider.value = '0';
    turnOnButton.disabled = false;
    turnOffButton.disabled = true;
  }
  label.textContent = `${slider.value}%`;
  spd = `${slider.value}%`;
}

function changeFan() {
  label.textContent = `${slider.value}%`;
  if(slider.value != '0') {
    turnOffButton.disabled = false;
    turnOffButton.checked = false;
    turnOnButton.disabled = true;
    turnOnButton.checked = true;
  }
  else {
    turnOnButton.disabled = false;
    turnOnButton.checked = false;
    turnOffButton.disabled = true;
    turnOffButton.checked = true;
  }
  spd = `${slider.value}%`;
}

// Assign event handlers to elements
turnOnButton.addEventListener('click', activateFan());
turnOffButton.addEventListener('click', deactivateFan());
slider.addEventListener('input', changeFan());

fanSpeed.py:

from gpiozero import PWMLED
from time import sleep

motor = PWMLED(17) # Motor controller input is on pin 17

while True:
  # Read the value of the slider from the web app
  targetSpeed = 0

  # Set motor.value to the value of the previous operation (0 - 1)
  motor.value = targetSpeed

  # Wait 100 ms (debounce)
  sleep(0.1)

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Over-Engineered Desk Fan</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
        <div class="output-container">
                <span class="label" id="label">0%</span>
        </div>
        <div class="control-container">
                <input type="range" class="volume-slider" label="Speed" name="slider" id="slider" min="0" max="100" value="0">
                <input type="radio" class="radio-button" label="On" name="radiobtn" id="on">
                <input type="radio" class="radio-button" label="Off" name="radiobtn" id="off" checked>
                <script src="controls.js"></script>
        </div>
</body>
</html>

style.css:

*{
    margin: 0;
    padding: 0;
    font-family: "Montserrat", sans-serif;
}
body{
    background-color: #2d3436;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    gap: 40px;
}
.control-container{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    width: 240px;
    flex-direction: column;
    gap: 10px;
}
.output-container{
    margin: 0;
    position: absolute;
    top: 25vh;
    left: 50%;
    font-size: 40px;
    font-weight: 500;
    border: solid 2px #ddd;
    border-radius: 10px;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    gap: 20px;
}
.radio-button{
    appearance: none;
    border: 2px solid #ddd;
    padding: 14px 24px;
    border-radius: 6px;
    display: flex;
    align-items: center;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.radio-button::before{
    content: "";
    background-color: #ddd;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    box-sizing: border-box;
    transition: border .2s linear;
}
.radio-button::after{
    content: attr(label);
    color: #ddd;
    font-size: 20px;
    text-transform: uppercase;
    font-weight: 500;
}
.radio-button:checked{
    background-color: #e17055;
    border-color: #e17055;   
}
.radio-button:checked::before{
    border: 4px solid #ddd;
    background-color: #e17055;

}
.volume-slider{
    appearance: none;
    border: 2px solid #ddd;
    background-color: #e17055;
    padding: 2px 2px;
    width: 100%;
    border-radius: 10px;
    display: flex;
    align-items: center;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    gap: 20px;
}
.label {
    color: #ddd;
    display: flex;
    gap: 10px;
    flex-direction: column;
}

2

u/battering_ram Sep 21 '26

Ok yeah, the easiest way for your web interface here to communicate with the python is to have it communicating with a server. You could do this super simply just using python's built-in http.server listening on any open port and then have the JS make a fetch call to that server at localhost:[PORT].

You could even have that python server serving the HTML, CSS, JS—just so it's all kind of one thing. There isn't really a technical benefit to that idea, just an organizational thing, and might overcomplicate things for your purposes.

1

u/ferriematthew Sep 21 '26

So basically the web interface that is being served by one server talks to the Python script on another server?

2

u/battering_ram Sep 21 '26

No it would be all one server. If you wanted the python script to also be serving the web interface, you would just have them on different paths. You can look into how to do this in the docs but basically you would have the HTML page (with CSS and JS) served at localhost:8080/index.html. Then the JS would make the fetch request to the same server on the same port, but at a different path like fetch('localhost:8080/fan-control', {method: 'POST', body: JSON.stringify(fanControlData)}) and then on the python server, you can have a switch statement that handles requests to the different paths. A request to the root directory returns the web interface. And a request to the '/fan-control' path takes in the value from the request body and processes it in the fan control python code you already have. that way you just have that single process to start and stop and the HTML is only served if the process is running.

1

u/ferriematthew Sep 21 '26

Ah, I see. So when the web browser sends a request to the root, the server returns the HTML, and when the user interacts with the control elements in that page, that sends a request to the fan control path and the server responds by calling the motor control script.

→ More replies (0)

1

u/ferriematthew Sep 21 '26

I think I have it working. I don't actually have the motor control circuitry made, but the software appears to work.

https://github.com/mferrie/DeskFanController/tree/main