Master & Slave


 Master & Slave

(Freeware for education, third part)


 

Life, a movie in my head
A page of lines we read

The words remain unsaid

Time, a race you never win
Look back at where we've been
And throw the towel in

 

These, if I’m not mistaken, were the opening words of the Kiss song, “Master & Slave,” in 1997! Many years have passed since then, but I’ve never stopped trying to learn how to learn!

To learn, I’ve realized that we must be humble and know how to ask for what we want to learn, from the right sources: the relationships between entities and events.

The true “Master” is the Universe, or the Multiverse, depending on how you interpret it. The communication interface is our body, at least as a first approximation, the tools through which we can amplify our senses. The protocol is mathematics, the language we adopt, because it’s coherent, to try to interpret the relationships between “things.” We are “Slaves,” we can do nothing but react, respond to stimuli, to messages, even if we do so in relation to subjective processes, as if we were responding in a personalized way to a message common to all.

Following on from the previous two articles, I again refer to temperature measurement and monitoring, this time for the habitat, along with relative humidity. 

Obviously, I always refer to the basics reported in the MMC text: Measurements – Monitoring – Control, on which I was able to implement the previous two practical articles that can be downloaded from these sites:

https://www.cyberservices.it/

https://romeoceccato.blogspot.com/

https://independent.academia.edu/RomeoCeccato

The reference book can be purchased on the Amazon platform or on Lulu, in different languages:


This book addresses topics such as plant and animal sensors as a starting point, then expands into measurement, monitoring, and control processes. It spans various fields, including computer science, physics, and medicine. It also covers cybernetics and artificial intelligence, with a particular focus on the fundamental mathematical concepts inherent in this context. The topics covered in previous articles focus on temperature, and therefore on monitoring this quantity and transmitting data from the measuring instrument, which acts as a “Client” to a “Server”, which will display the data on a web page and, at the same time, could act as a “Master”, providing the values ​​that could be referenced by a “Slave” assigned to the “Control” process: the slave, if all goes well, obeys the commands given by the master, implementing its operational “will.” In other words, the architecture is based on a master-slave computing model, where a “master” device controls one or more “slave” devices/processes. The master manages access to shared resources and sends commands (via a bus), while the slaves respond exclusively to the master’s requests, usually remaining passive… (and obedient).

Computer Architecture

In this educational tutorial, the architecture consists of:

A client that sends temperature and relative humidity data at regular intervals.


  • A server, (server/master) that exposes the client values ​​and other parameters calculated in relation to this data, through a web interface.
  • A master (master/server), which sends the parameters relating to an actuator to one (or more slave devices). One (or more) slave devices, which manage actuators relating to the type of control implemented by the master.

The proposed architecture integrates three main nodes with different physical interfaces and communication protocols, coordinated by a central unit that acts as Master and Server.

 

Description of Components and Type

The architecture is structured as a hybrid system in which the central Raspberry Pi acts as a bridge (gateway) between a wireless network and a local serial bus. The Client (Raspberry Pi Pico W): sends requests or telemetry data via the integrated CYW43439 module. It uses the TCP/IP stack to communicate via Wi-Fi. 

The Master/Server (Raspberry Pi or a PC): manages the control logic. It receives data from the client (acting as a Server, for example via Socket or MQTT) and queries the slave via Modbus RTU protocol. 

The Slave (Raspberry Pi Pico): connected via USB, it emulates a serial port (CDC - Communication Device Class). 

It implements the logic of a “Modbus slave”, responding to queries from the Master after verifying the integrity of the data.

 


Communication Protocol and Data Integrity

Communication between the Master and the Slave Pico occurs by simulating a Modbus-RTU system. In this context, each package (ADU - Application Data Unit) is composed of:

  • Slave address (1 byte)  
  • Function Code (1 byte)  
  • Data (n bytes) 
  • CRC16 (2 bytes)

The CRC16 (Cyclic Redundancy Check) is essential for detecting transmission errors on the USB/Serial cable. 

The standard polynomial used for Modbus is 0xA001 (reflected representation of 0x8005). The CRC calculation follows the pseudo-code for iterative algorithm:

                CRCinit = 0xFFFF 

for each Byte:CRC = CRC exor B

            for i = 0 up to i = 7

                    if the least significant bit is 1 then:

                        CRC=(CRC >> 1) exor 0xA001

                    otherwise:

                        CRC=CRC>>1


Data Flow

The interaction process follows a deterministic sequence:

Phase 1 (Wireless): The Pico W (Client) establishes a connection with the IP address of the Raspberry Pi on the dedicated port.

Phase 2 (Processing): The Raspberry Pi receives the packet, validates the request, processes the variables to send to the slave device and encapsulates the command in the Modbus RTU format.

Phase 3 (Serial): The command is sent via /dev/ttyACM0 (USB) to the Pico Slave.

Phase 4 (Validation): The Pico Slave receives the frame, calculates its CRC16 and compares it with the one received:

If CRCcalc = CRCrec, the Slave executes the command and responds.

If CRCcalc = CRCrec, the packet is discarded due to integrity error.


The hardware components

Everything you need to complete this project is essentially the following:

  • A Raspberry Pi 4b (recommended for good experimentation) 
  • A Raspberry Pi Pico-W (with DHT11 sensor and connection accessories) 
  • A Raspberry Pi Pico (with SG90 9G servo) 
  • A Raspberry Pi Pico (optional with 28BYJ-48 stepper motor and ULN2003A driver control board)


The Client

This is a device consisting of a Raspberry pi pico-W to which a DHT11 type sensor has been connected which is able to detect the temperature and relative humidity of the surrounding environment. 

The Raspberry pi pico-W

The Raspberry pi pico-w is a “Microcontroller” from the Raspberry family. It could be defined as a small computer, on a single chip that integrates a CPU, memory (RAM and ROM/Flash) and various input/output peripherals (I/O), also having a port for WIFI connection. 

Incidentally, it is fair to specify that the main difference between a microcontroller (MPU, Micro Processor Unit) and a microprocessor (CPU) consists in the fact that the processing unit “CPU” (the microprocessor or Central Processing Unit) requires external components (memory, I/O), while a microcontroller is a single chip that integrates CPU, memory (RAM and ROM) and input/output peripherals. 

This makes microcontrollers ideal for low-power, embedded applications such as household appliances, while microprocessors are used for general-purpose, high-performance tasks such as in computers.

This is its layout:



The components of the Raspberry pi pico – w can be identified in the following list: RP2040 microcontroller chip designed by Raspberry Pi in the United Kingdom.
  • Dual-core Arm Cortex M0+ processor, flexible clock running up to 133 MHz 
  • 264KB of SRAM, and 2MB of on-board flash memory • USB 1.1 with device and host support 
  • Low-power sleep and sleeping modes 
  • Drag-and-drop programming using mass storage over USB 
  • 26 × multi-function GPIO pins • 2 × SPI, 2 × I2C, 2 × UART, 3 × 12-bit ADC, 16 × controllable PWM channels • Accurate clock and timer on-chip 
  • Temperature sensor 
  • Accelerated floating-point libraries on-chip 
  • 8 × Programmable I/O (PIO) state machines for custom peripheral support 
  • Wireless (802.11n), single-band (2.4 GHz) 
  • WPA3 
  • Soft access point supporting up to four clients • Bluetooth 5.2


Raspberry Pi Pico W series devices support C/C++ or MicroPython programming languages. On the devices there is a BOOTSEL button which is used to put the equipment in bootloader mode, i.e. a mass storage mode, when connected to a computer via USB. In this mode, the Pico appears as a USB stick called “RPI-RP2”, allowing you to easily drag and drop firmware or “uf2” code to update or program it. Incidentally, it is specified that there is also a version of the Raspberry Pi Pico -W board, the Pico WH which comes with the male pin headers already soldered, both integrating the Infineon CYW43439 chip for Wi-Fi and Bluetooth connectivity.


The DHT11 temperature and humidity sensor

This is a sensor that outputs a digital signal proportional to the temperature and humidity measured by the sensor itself. The technology with which this sensor is made ensures high reliability and excellent long-term stability as well as very fast reaction times. Each DHT11 module is carefully calibrated in the laboratory. 


Its main features are:



The calibration coefficient is stored in an internal OTP memory and this value is used during the acquisition process. 

The single-wire serial interface makes integrating the sensor into digital systems quick and easy. This module can detect temperatures in a range from 0°C up to 50°C and allows you to build a temperature and humidity monitoring system at low costs. The sensor uses an exclusive digital technique which, combined with humidity detection technology, guarantees its reliability and stability. Its sensitive elements are connected to a single-chip 8-bit processor.


Connection diagram


  • Pins and power: The supply voltage must be between 3-5.5V DC. A 100nF capacitor can be inserted between VDD and GND for power supply filtering. 
  • Communication and signal: Single-bus data is used for communication between MCU and DHT11.



Data transmission:

The data communication of the DHT11 is based on a proprietary single-bus digital protocol that requires only one data line (in addition to VCC and GND) to send 40 bits of information (humidity, temperature and checksum) (Single-Wire Two-Way):

The Start Signal
The microcontroller (MCU) brings the data line to low level (GND) to at least wake up the sensor, then brings it high for 20-40 micro-seconds. 
Sensor Response
The DHT11 responds by bringing the line to a low level for 80 micro-seconds, followed by a high level for another 80 micro-seconds. 
 
Data transmission (40 bit). 
        The sensor sends 5 bytes (40 bit) serially:


  1. 8 bit: Full humidity
  2. 8 bit: Decimal humidity (usually 0 on DHT11)
  3. 8 bit: Integer temperature
  4. 8 bit: Decimal temperature (usually 0 on DHT11)
  5. 8 bit: Checksum (sum of the previous 4 bytes for verification).

Bit format:

  • Bit “0”: 50μs low level + 26-28μs high level.  
  • Bit “1”: 50μs low level + 70μs high level.

Once the 40 bits have been transmitted, the line goes back up and the sensor goes back to low-power mode. You must wait at least 1 second between two consecutive readings to ensure stability.

Type of data

Relative humidity (%RH) is the percentage measurement of water vapor present in the air compared to the maximum amount that the air could hold at the same temperature and pressure. 

In other words, it indicates how close the air is to saturation (100% %RH). 

Specifications:

  • Temperature dependence: Warm air can hold more water vapor than cold air. Consequently, for the same amount of water contained, if the temperature increases, the relative humidity decreases; if the temperature decreases, the relative humidity increases. 
  • Meaning of 100%: When the relative humidity reaches 100%, the air is saturated and fog, dew, or precipitation forms.
  • Measurement: It is calculated as the percentage ratio between the partial pressure of water vapor and the saturated vapor pressure.
  • Difference with absolute humidity: While relative humidity varies with temperature, absolute humidity indicates the actual amount of vapor in grams per cubic meter of air, regardless of temperature.

Practical example:

In winter, outdoor air at 0°C with fog (100% r.h.) contains less water than the same air heated to 22°C in the house, which will have low relative humidity (e.g. 23%) comfort parameters:

  • To ensure comfort and health indoors, relative humidity should usually be between 40% and 60%.
  • Too high (>80%): Increases the perception of heat or cold and promotes the formation of mold.
  • Too low (<30-40%): May cause dryness of the respiratory tract and skin.


Ideal thermal comfort in the home is generally achieved with a temperature between 19°C and 21°C in winter and around 26°C in summer, maintaining humidity between 40% and 60%. The perceived well-being, however, depends not only on the air temperature, but also on the radiation of the walls, the speed of the air and the thermal insulation. Here are the key details for temperature and comfort: Ideal temperatures per room:


  • Living room/Kitchen: 19-20°C, as they are active areas with heat production. 
  • Bedrooms: 16-18°C to promote quality sleep. • Bath: 20-22°C, ideal for comfort when undressed. 


What is meant by the difference between temperature and perceived comfort: the real temperature is that on the thermometer, while perceived comfort includes the effect of cold walls (radiation) or high humidity. If the walls are cold, it will feel cold even at 20°C.


Key factors for well-being:


  • Humidity: The ideal relative humidity is between 40% and 60%.
  • Air speed: Must be less than 2m/s.
  • Clothing: Heavier clothing can make you feel up to 4°C warmer.


Tips for saving energy: reducing the internal temperature (especially at night) and maintaining good insulation helps reduce the difference with the outside, reducing consumption by 6-7% for every degree less above 21°C. 

The Raspberry pi pico-W, in this case, is used for example to simulate the physical interface of a human (or pet), inserted into an environment.


The Comfort Graph



The ideal thermo-hygrometric comfort graph shows a wellness area with a temperature between 20 and 24°C in summer, while, in winter, between 20 and 22°C and relative humidity between 40% and 60%. Levels outside this range (too dry or humid) compromise health and comfort, requiring ventilation or dehumidification. Over the years, various bioclimatic indices have been developed (for humans and animals) to express the level of discomfort caused by unfavorable weather and climate conditions. The level of thermal stress of animals, for example, can be assessed by using the Temperature Humidity Index - THI which allows the perceived environmental temperature to be assessed in relation to the relative humidity values ​​of the air (NOAA, 1976). Stress depends both on the extent of exceeding the upper critical value of the THI (variable in relation to age, race, etc.) and on the temporal duration of this exceeding. Other critical elements are represented by the methods of transition from the thermo-neutrality condition to that of excessive heat and by the possibility of recovery that is offered to the animals by the THI values ​​recorded in the coolest hours of the day (night hours). Below is the most commonly used formula to calculate the THI (NOAA, 1976):


THI = [(1.8 x Ta) + 32] - (0.55 - 0.55 x Ur ) x [(1.8 x Ta) - 26] where:

Ta = air temperature (°C)

Ur = relative humidity (%)

It is obvious that what are the comfort conditions for human beings may not be ideal for other types of animals. (p.i. means animal = being endowed with a soul).


The Slave

This is a device consisting of a Raspberry pi pico to which a small SG90 9G type 180/360 degree digital servomotor has been connected.

The actuator: SG90 9G servomotor


A piece of equipment, which in this case is useful for simulating a flapper (or Hopper) type window opening and closing device, is characterized by very small dimensions, while maintaining excellent power performance, a characteristic that makes it the perfect actuator for small robots and dynamic models. 

Its characteristics can be summarized in these data:


  • Weight: 9 grams, 
  • power supply: from 3.3Vdc~ 6Vdc, 
  • torque at 4.8V: 1.2 kg x cm, 
  • torque at 6V: 1.5kg, • rotation: 160°, 
  • speed at 4.8V: 0.12 sec/60°, 
  • speed at 6V: 0.11 sec/60°, 
  • dimensions (mm): 22.5x12x29.


The position of 0 degrees is equivalent to that of 270 degrees, which is obtained based on the modulation of the “Duty Cycle”: Map 0 - 270 degrees at approximately 0.5 ms (500000 ns) - 2.5 ms (250000 ns)


The key components of the servo motor are: a DC motor, a gear system, a potentiometer (for position feedback) and an integrated control board. 

Position Feedback: The potentiometer connected to the shaft constantly sends the current position to the internal board, which adjusts the DC motor to reach and maintain the desired angle.


PWM type controls

(taken from the book MMC: Monitoring Control Measures) 

PWM type controls, i.e. controls based on pulse width modulation, are due to a digital control technique created to replace voltage and current regulation through the use of rheostats or potentiometers. Basically, PWM modulation is due to a signal capable of regulating the output voltage of a control device, starting from a direct current source, allowing the power dissipated by the electrical system to be limited.

The operation of a PWM circuit works as a train of high-speed square wave (ON/OFF) pulses, generating a value that depends on the duty cycle of the control signal, relative to the full capacity of the system.

PWM technology can be applied to the regulation of electrical systems with various types and functions, starting from LED driver power supplies up to motors, valves and hydraulic pumps.

A very frequent application is that used by switching power supplies: PWM modulation, in this case, is used to regulate power supplies without power losses, thus increasing the level of efficiency.

In switching power supplies, the PWM signal is regulated according to the output voltage, inducing a stabilizing feedback as the input current varies. In this case the small servomotor (servo control) simulates the degree of opening of the flapper system, in relation to the relative humidity, detected by the client and the data processing due to the master algorithm.

To allow a servo motor to maintain the position, an impulse relating to the desired position must be sent and the impulse must be repeated continuously. Normally it is recommended to repeat the command at a frequency between 50Hz and 100Hz, or with a period between 10ms and 20ms.

The positive pulse of the signal, in general, must have a width of the order of milliseconds, such as to allow positioning from an angular minimum of 0, to a maximum, usually of π radians.

If, for example, the position of π radians is obtained with 1ms to give the right torque to the motor and with 2ms, a stable position is obtained at 0 radians, then, the central position will be obtained with the width of 1.5ms The above is shown in the following graph: 


In the book MMC: Measurements – Monitoring – Control, a specific chapter is dedicated to this particular control technique:

 PWM type controls 415 

The generation of the Duty cycle 415

TTL control section 417 

U-turn section 418 

Mini shield PWM 420 

Control for servomotor 421


The Raspberry Pi Pico


The Raspberry Pi Pico is a compact and affordable microcontroller development board based on the RP2040 chip, designed by the Raspberry Pi Foundation. It features a dual-core ARM Cortex-M0+ processor up to 133 MHz, 264 KB of SRAM, 2 MB of Flash memory, 26 multi-function GPIO pins, and support for C/C++ and MicroPython.



  • Microcontroller: RP2040 dual-core ARM Cortex M0+ up to 133 MHz.
  • Memory: 264 KB SRAM and 2 MB integrated QSPI Flash memory.
  • GPIO: 26 multi-function pins, including 3 12-bit ADC analog inputs.
  • Interfaces: 2×UART, 2×I2C, 2×SPI, 16 PWM channels.
  • USB: Support USB 1.1 host and device.
  • Power supply: 5V via micro USB or 2-5V via VSYS pin.
  • Dimensions: 21 x 51 mm, with directly solderable pins (castellated)  


analog inputs

The Raspberry Pi Pico is the first Raspberry Pi that has analog inputs from the factory. It comes with a total of 5 ADC (analog to digital converter) inputs. Two of which are used by the Pico for its internal temperature sensor and voltage monitoring.   The Raspberry Pi Pico resolves 12-bit ADC signals. 

Other features include:

  • SAR ADC (successive approximation ADC) • 500 kS/s (with external clock at 48MHz) 
  • DMA interface on ADC inputs that can access memory without using the CPU. 


Programmable IO (PIO)

This is one of the coolest features of the Raspberry Pi Pico and is also what gives the Pico the dynamism that some other microcontrollers don’t have. PIO is a hardware interface that can be programmed independently of the main processors. It can therefore emulate many different interfaces: 


  • 8080 and 6800 parallel port
  • I2C
  • 3 I2S pins
  • SDIO (SD card interface)
  • SPI, DSPI, QSPI
  • UART
  • DPI or VGA (via resistor network / DAC) 


PIO State Machines are fully programmable and dedicated exclusively to I/O. Particular attention is given to precise timing.

For example, the PIO interface can be used to set up time-critical (time-sensitive) applications in a stable and reliable manner, such as the one required in this case, where this equipment must simulate a controller connected via Modbus protocol.


Connection diagram



The Pinout of the Raspberry Pico: 



The Master/Server

For this educational purpose, the device that could act as a master and also as a server could be the PC, or a Raspberry pi, for example a pi4 model B.





The Raspberry Pi 4 Model B is a powerful mini-computer equipped with a 64-bit 1.5GHz or 1.8GHz Broadcom BCM2711 (Cortex-A72) quad-core processor, 1GB, 2GB, 4GB or 8GB LPDDR4 RAM, Gigabit Ethernet, dual-band Wi-Fi, Bluetooth 5.0, two USB 3.0 ports, two micro-HDMI ports for support two 4K monitors and USB-C connector for power. Here are the main technical details:

  • Processor: Broadcom BCM2711, Quad-core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz (or 1.8GHz in newer revisions).
  • RAM memory: 1GB, 2GB, 4GB or 8GB LPDDR4-3200 SDRAM.
  • Connectivity:
  • Dual-band 802.11ac Wi-Fi (2.4GHz / 5.0GHz).
  • Bluetooth 5.0, BLE.
  • Gigabit Ethernet (wired).
  • I/O ports:
  • 2 USB 3.0 ports (super-speed).
  • 2 USB 2.0 ports.
  • 2 micro-HDMI ports (4Kp60 or 2x 4Kp30 support).
  • 1 x 40-pin GPIO connector (backwards compatible).
  • 1 MIPI CSI port for camera.
  • 1 MIPI DSI port for display.
  • 3.5mm audio/composite jack.
  • Video: VideoCore VI GPU (OpenGL ES 3.0, 3.1, Vulkan 1.0 support).
  • Storage: Micro-SD slot for operating system and data.
  • Power supply: 5V DC via USB-C (minimum 3A), 5V DC via GPIO header.
  • PoE Support: Available via separate PoE HAT.
  • Operating temperature: 0 – 50°C.


The Raspberry Pi 4 is significantly more powerful than previous versions, offering approximately three times the CPU speed of the 3B+ and advanced multimedia capabilities.

This device will be connected to the slave via USB port, while it will be connected to the client, via WIFI, therefore it will simultaneously perform both the server function towards the client device and the master function towards the slave device! The following image summarizes the case:



If a Raspberry is used as master/server, it will still be useful to have a PC to manage the network configuration via VNC. VNC (Virtual Network Computing) is an open-source desktop sharing system for remote control, based on the RFB protocol.
This technique allows you to view and interact with the graphic interface of a computer (Server) from another device (Viewer/Client) via network, ideal for technical assistance or remote work.


The software 

The basis of these three projects for educational purposes is the Python programming language, also in the Micropython variant, used for both the client and the slave created with the Raspberry pi pico-W and pico versions.


MicroPython


MicroPython is a streamlined and efficient implementation of the Python3 programming language, which includes a small subset of the Python standard library and is optimized for execution on microcontrollers and in constrained environments (embedded technology).MicroPython is packed with advanced features such as interactive prompting, arbitrary precision integers, closures, list comprehensions, generators, exception handling, and much more.However, it is compact enough to fit and run in just 256 KB of code space and 16 KB of RAM.



MicroPython is written in C99, and the entire MicroPython core is available for general use under the very liberal MIT license.

Most libraries and extension modules (some of them third-party) are also available under the MIT license or similar.

You can freely use and adapt MicroPython for personal, educational, and commercial products. MicroPython is developed in open source mode on GitHub and the source code is available to everyone.

MicroPython employs numerous advanced programming techniques and numerous tricks to maintain a compact size while offering a complete set of features. Some of the most important elements are:


  • Highly configurable thanks to numerous compile-time configuration options
  • Support for numerous architectures (x86, x86-64, ARM, ARM Thumb, Xtensa)
  • Large test suite with more than 590 tests and more than 18,500 individual test cases
  • 99.2% code coverage for the core and 98.5% for the core plus extended modules 
  • Fast boot time from boot to first script loading (150 microseconds to boot.py, on PYBv1.1 at 168 MHz)
  • A simple, fast, and robust mark-sweep garbage collector for heap memory
  • A MemoryError exception is thrown if the heap is exhausted
  • A RuntimeError exception is thrown if the stack limit is reached
  • Support for running Python code on a hard interrupt with minimal latency
  • Errors have a backtrace and report the line number of the source code
  • Wrapping up constants in the parser/compiler
  • Pointer Tagging to fit small integers, strings and objects into a machine word
  • Transparent transition from small integers to large integers
  • Support for the 64-bit NaN boxing object model
  • Support for 30-bit full floats, which do not require heap memory
  • A cross-compiler and frozen bytecode, to have precompiled scripts that do not take up RAM (except for the dynamic objects they create)
  • Multithreading via the “_thread” module, with an optional global-interpreter-lock (still in development, only available on selected ports)
  • A native emitter that points directly to the machine code rather than the bytecode virtual machine
  • Inline assembler (currently Thumb and Xtensa instruction sets only) 


Installing MicroPython:

How to install MicroPython on Raspberry Pi Pico and Pico-W:

  1. Download the firmware: Download the .uf2 file from the official MicroPython site or Raspberry Pi site. (Choose the correct version for your Raspberry Pi Pico or Pico-W type)
  2. Installation: Press and hold the BOOTSEL button on the Pico, connect it to your computer via USB and release the button. The card will appear as a memory drive (“RPI-RP2”).
  3. Upload: Drag the downloaded .uf2 file into the RPI-RP2. The Pico will automatically restart with MicroPython.

 

Note: MicroPython on Pico is ideal for those looking for simpler programming than C/C++ for electronics.


The Client software

The Client is created with the Pico-W version, which transmits environmental temperature and relative humidity data via WIFI, this is the listing:

 
import network
import urequests
import utime
import time
from machine import Pin
import dht
# --- CONFIGURATION ---
SSID = 'ssid'
PASSWORD = 'password'
SERVER_URL = 'http://10.100.100.100:5000/upload' # Replace with your RPi 4 IP address
DHT_PIN = 0 # GPIO pin connected to DHT11
# --- SETUP ---
wlan = network.WLAN(network.STA_IF)
sensor = dht.DHT11(Pin(DHT_PIN))
def blink(counter, on_off):
    led = Pin("LED", Pin.OUT)
    for _ in range(counter):
        led.value(1)
        utime.sleep_ms(on_off)
        led.value(0)
        utime.sleep_ms(on_off)
def connect_wifi():
    """Connect to the WiFi network."""
    wlan.active(True)
    wlan.connect(SSID, PASSWORD)
    print('Connecting to WiFi...')
    max_wait = 10
    while max_wait > 0:
        if wlan.status() < 0 or wlan.status() >= 3:
            break
        max_wait -= 1
        print('waiting for connection...')
        utime.sleep(1)
    if wlan.status() != 3:
        raise RunutimeError('Network connection failed')
    else:
        print('Connected')
        status = wlan.ifconfig()
        print('IP = ' + status[0])
def send_data(temp, hum):
    """Send temperature and humidity to the server."""
    data = {'temperature': temp, 'humidity': hum}
    try:
        response = urequests.post(SERVER_URL, json=data)
        print('Data sent:', response.text)
        response.close()
    except Exception as e:
        print('Failed to send data:', e

# --- MAIN LOOP ---
try:
    connect_wifi()
    while True:
        try:
            print('Reading sensor...')
            sensor.measure()
            temp = sensor.temperature()
            hum = sensor.humidity()          
            print(f'Temperature: {temp}°C, Humidity: {hum}%')
            blink(3,250)
            send_data(temp, hum)        
        except OSError as e:
            print('Failed to read sensor.')
        # Wait for 300 seconds (5 minutes)
        print('Waiting 300 seconds...')
        time.sleep(300)
except Exception as e:
    print('An error occurred:', e)


Note: The blink function is interesting because it signals correct operation, that is, data transmission when the pico-w is not connected to the port of the PC (or Raspberry) being tested. (The waiting time can be chosen according to needs). Further insights can be found in previous articles in this series.


The Slave software

The device used as a Slave is a Raspberry Pi pico, the one without a WIFI interface, which must therefore be connected to the PC or to a Raspberry Pi 4B, for example, via the USB serial port. The peculiarity of this connection is that it has been implemented to emulate a network device connected via Modbus protocol and that it has a cyclic redundancy check (CRC).
This algorithm generates a numerical value, based on the (binary) content of the data, which is added to the file that will be sent. Upon reception, the content composed of the data and the “CRC” element calculated before transmission is recalculated. If the calculation detects a discrepancy with the original, the data will be corrupted.


Modbus protocol

The Modbus protocol is one of the “grandfathers” of industrial automation, which may be why I like it, given that it was born way back in 1979, but I’m older… it was developed by Modicon, now Schneider Electric, but we can say that the old one is still the most widespread language for communicating embedded electronic devices. Its strength lies in its simplicity: it is open, free and relatively easy to implement. Originally, Modbus used a Master-Slave structure (wired network connections). In modern terminology, with the advent of the internet (especially for Modbus TCP), we talk about Client-Server:

  • Client (Master): the device requesting the information (e.g.a PLC or SCADA software).
  • Server (Slave): the device that provides data or executes commands (e.g. a temperature sensor, an inverter or an energy meter). The Client sends a request and the Server responds. In a standard network, Servers never talk to each other and never start a conversation without being interrogated. However, note that in this specific case the Client is not a Master and the Server is not a Slave. There are three main variants of the protocol:



Variant

Physical Support

Features

Modbus RTU

Serial (RS-485 or RS-232)

Compact binary transmission. It is the most common in the serial industrial sector.

Modbus ASCII

Serial

Use legible fonts. Slower, but useful if the connection is unstable.

Modbus TCP/IP

Ethernet

Encapsulates Modbus packets within a TCP network. Very fast and modern.

The Data Model

Modbus organizes information into four main tables, called “Registers”. Think of them as post office boxes numbered from 1 to 65,535:

  1. Coils: Boolean values (On/Off) for reading and writing (e.g. starting an engine).
  2. Discrete Inputs: Read-only Boolean values (e.g. the state of a limit switch).
  3. Input Registers: 16-bit read-only values (e.g. temperature read by a sensor).
  4. Holding Registers: 16-bit values for reading and writing (e.g. setting a setpoint).

A typical Modbus package contains four basic elements:
  • Device address: Who am I talking to? (IDs 1 to 247).
  • Function Code: What do I want to do? (e.g. 03 to read a register, 06 to write one). • Data: What records do I need? How many?
  • CRC (Error Check): A check code to ensure that the data has not been corrupted during travel.

 

Although more advanced protocols exist (such as Profinet or EtherCAT), Modbus remains the “universal” standard, the old man is a guarantee of communication compatibility between products, it is the “lowest common denominator” that allows machines from different manufacturers to understand each other without too many bureaucratic difficulties, as was done in the old days!



The CRC (cyclic redundancy check)

In practice it is like a “digital signature” applied to the end of a Modbus message to ensure that no bits have been altered by electrical interference or noise on the line. In Modbus RTU, CRC-16 is specifically used. Here’s a practical example of how it works in the “real world”. Let’s imagine that the Master wants to read a value from a sensor (Server ID: 1). 1. first phase: composition of the message (Without CRC) -> 01 03 00 00 00 01 ◦ 01: Slave address. ◦ 03: Function (Read Holding Register). ◦ 00 00: Address of the starting register. ◦ 00 01: How many registers to read (1). 2. Second phase: the calculation -> The Master takes these 6 bytes and passes them through a mathematical algorithm (a polynomial division based on the polynomial ). The result of this calculation is 84 0A. 3. Third phase: composition of the final message (the one sent on the cable) -> 01 03 00 00 00 01 84 0A Reception of the message by the Slave:
  • Scenario A (Success): The slave calculation results in 84 0A. Does this correspond to the CRC attached by the Master? Yes -> the Slave accepts the command and responds.
  • Scenario B (Error): An electrical interference transforms the message into 01 03 00 00 00 09 …. the Slave recalculates the CRC, but the result will no longer be 84 0A. The Slave does not accept and does not respond, because the data is corrupt.
At this point there may be other attempts to transmit the request which may possibly generate an error.
 
Why does it work? Because using a polynomial division is not an arbitrary choice: it is an extremely effective mathematical method for detecting the types of errors that typically affect industrial cables (interference, electrostatic discharge or noise “bursts”). 
Why does it work? Because using a polynomial division is not an arbitrary choice: it is an extremely effective mathematical method for detecting the types of errors that typically affect industrial cables (interference, electrostatic discharge or noise “bursts”). 
 
Here’s why it works so well: 
In mathematics, a sequence of bits (such as 1101) can be viewed as the coefficients of a polynomial. 
 
Message 1101 becomes:     1x3+1x2+0x1+1x0.
This turns a “data transmission” problem into an algebraic problem.
The CRC uses a fixed, known “divisor” (called the generating polynomial). 

For Modbus RTU, this polynomial is the standard    x16+x15+x2+1.

Polynomial division works like normal number division, but with one key difference: it uses modulus 2 arithmetic (i.e. the XOR logical operation).

 


The CRC is, technically, the remainder of this division.
 
  1. The sender divides the message by the polynomial and gets a remainder (the CRC).
  2. The sender “attaches” this remainder to the end of the original message.
  3. The receiver divides the entire packet (Message + CRC) by the same polynomial.
The mathematical magic: If the data has not been altered, the remainder of the division made by the recipient must be zero. If the remainder is not zero, it means that at least one bit has changed along the way, simple and effective I would say, but try doing it with binary numbers… this is the real lesson! Let’s see the listing used in this teaching session, in MicroPython:


import machine
import utime
from machine import Pin, PWM
import sys
import select
import struct
# Costanti Modbus
SLAVE_ID = 101
FC_READ_HOLDING = 3
FC_WRITE_SINGLE = 6
# Pin configurazione
servo_pin = 0
pin_auto_man = 1 # 1 = Manuale, 0 = Automatico
pin_alarm = 15   # 1 = Allarme
# Inizializza Pin
led = Pin("LED", Pin.OUT)
pwm = PWM(Pin(servo_pin))
pwm.freq(50)
p_man = Pin(pin_auto_man, Pin.IN, Pin.PULL_DOWN)
p_alm = Pin(pin_alarm, Pin.IN, Pin.PULL_DOWN)

# Variabili di stato
current_angle = 0

def blink(counter, on_off):
    led = Pin("LED", Pin.OUT)
    for _ in range(counter):
        led.value(1)
        utime.sleep_ms(on_off)
        led.value(0)
        utime.sleep_ms(on_off)

def set_angle(angle):
    global current_angle
    # Map 0 - 270 degrees to roughly 0.5ms (500000ns) - 2.5ms (250000ns)
    # Duty for 50Hz (20ms period). 0.5ms / 20ms = 2.5% = 1638, 2.5ms = 12.5% = 8192
    if angle < 0: angle = 0
    if angle > 270: angle = 270 
    # 0 degree = 500000 ns, 270 degree = 2500000 ns -> range 2000000 ns
    ns = 500000 + int((angle / 270.0) * 2000000)
    pwm.duty_ns(ns)
    current_angle = angle
def get_status():
    if p_alm.value() == 1:
        return 2 # Allarme
    elif p_man.value() == 1:
        return 1 # Manuale
    else:
        return 0 # Automatico
def crc16(data):
    crc = 0xFFFF
    for char in data:
        crc ^= char
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return struct.pack('<H', crc)
print("Starting Modbus Slave...")
set_angle(0)
poll = select.poll()
poll.register(sys.stdin, select.POLLIN)
import micropython
# MOLTO IMPORTANTE: Disabilita il Ctrl-C (KeyboardInterrupt) causato dal byte 0x03
# che viene inviato dal Master come Function Code (FC_READ_HOLDING = 3)
micropython.kbd_intr(-1)
rx_buffer = bytearray()

while True:
    events = poll.poll(10) # 10ms timeout
    if events:
        try:
 # Legge uno o più byte diponibili per evitare il timeout/EOF block su stdin
           # Utilizza poll(0) per verificare la disponibilità senza bloccare
            while poll.poll(0):
                chunk = sys.stdin.buffer.read(1)
                if not chunk:
                    break
                rx_buffer.extend(chunk)     
            # Quando il buffer accumula almeno un frame teorico di 8 bytes
            while len(rx_buffer) >= 8:
                buffer = rx_buffer[:8]    
                # Check ID
                if buffer[0] == SLAVE_ID:
                    fc = buffer[1]       
                    # Check CRC: estrae esattamente gli ultimi 2 bytes del pacchetto di 8 bytes
                    received_crc = buffer[-2:]
                    calc_crc = crc16(buffer[:-2])           
                    if received_crc == calc_crc:
                        # Rimuovi l'intero pacchetto da 8 bytes dal buffer
                        rx_buffer = rx_buffer[8:]  
                        led.value(1) # Blink LED indicator on success processing             
                        if fc == FC_WRITE_SINGLE:
                            # Write single register
                            reg_addr = struct.unpack('>H', buffer[2:4])[0]
                            value = struct.unpack('>H', buffer[4:6])[0]
                            if reg_addr == 1:
                                set_angle(value)               
                            # Response is echo of request
                            sys.stdout.buffer.write(buffer)        
                        elif fc == FC_READ_HOLDING:
                            # Read holding registers
                            reg_addr = struct.unpack('>H', buffer[2:4])[0]
                            num_regs = struct.unpack('>H', buffer[4:6])[0]  
                            if reg_addr == 2 and num_regs == 1:
                                status = get_status()
                                resp = bytearray([SLAVE_ID, FC_READ_HOLDING, 2]) # 2 bytes of data
                                resp.extend(struct.pack('>H', status))
                                resp.extend(crc16(resp))
                                sys.stdout.buffer.write(resp)
                        
                        utime.sleep_ms(10)
                        blink(3,250)                   
                        led.value(0)
                    else:
                        # Se il CRC non corrisponde, spostiamo di 1 byte e riproviamo a sincronizzare
                        rx_buffer = rx_buffer[1:]
                else:
                    # ID Sbagliato: spostiamo di 1 byte
                    rx_buffer = rx_buffer[1:]           
        except Exception as e:
            # In caso d'errore resetta il buffer ed esce (e.g., struct unpack errore generico)
            rx_buffer = bytearray()
            blink(5,250)

This is a software simulation of a device that communicates in Modbus, with CRC control support, written in MicroPython for a smaller Raspberry. Also in this case the Blink function is used to see if the main.py software cycles correctly. As you can see, in addition to receiving the value of a positioning angle of the servocontrol, it also transmits its working state: manual or automatic or an alarm state.


The Master/Server software

This is the key software of the system used for this teaching lesson, it can be installed on the PC or on a Raspberry Pi 4B, if you really want to build a small “Intranet”. Where in the term “Intranet” is a private network completely isolated from the external network (Internet) in terms of services offered (e.g. via LAN), therefore remaining for internal use only, but possibly capable of communicating with the external network and other networks through appropriate communication systems (TCP/IP protocol, also extending with WAN and VPN connections.
 

Flask



For this application, the Python language will obviously be used, and in particular, the Flask library.Flask is a lightweight framework for WSGI web applications. It is designed to make getting started quick and easy, with the ability to scale up to complex applications.
Flask is based on the WSGI Werkzeug toolkit, the Jinja templating engine, and the Click CLI toolkit. Thanks to Python’s excellent support for reflection, you can access the package to understand where templates and static files are stored. Flask uses the Werkzeug routing system, which is designed to automatically sort routes based on complexity. This means that you can declare the routes in an arbitrary order and they will still work as expected. This is a key requirement to successfully implement decorator-based routing, as decorators may run in an indefinite order when the application is split into multiple modules. Jinja has an extensive filter system, a specific way of handling template inheritance, supports reusable blocks (macros) that can be used both within templates and from Python code, supports iterative rendering of templates, offers configurable syntax, and more. Flask also supports “async” coroutines for view functions by running the coroutine on a separate thread rather than using an event loop on the main thread, in summary then, the idea behind Flask is to build a solid foundation for all applications, everything else depends on extensions. The directory structure might be something like this: 

The app.py listing

The “core” of the Flask application:


from flask import Flask, render_template, request, jsonify
from datetime import datetime
import json
import os
import serial
import struct
import threading
import time

slave_status = "In attesa..."
DATA_FILE = 'sensor_data.json'
DEFAULT_DATA = {
    'temperature': None,
    'humidity': None,
    'freq': None,
    'ang': None,
    'inverter_1': "spento",
    'servo_flap': "spento",
    'ang-input': None,
    'freq-input-1': None,
    'vent-1': "spento",
    'vent-2': "spento",
    'last_updated': None
}
app = Flask(__name__)


def crc16(data):
    crc = 0xFFFF
    for char in data:
        crc ^= char
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return struct.pack('<H', crc)

def load_data():
    if os.path.exists(DATA_FILE):
        print("read data file")
        try:
            with open(DATA_FILE, 'r') as f:
                return json.load(f)
        except Exception as e:
            print(f"Error loading data: {e}")
            # Fallback to default if load fails
            return DEFAULT_DATA.copy()
    else:
        # File doesn't exist, create it with default data
        try:
            with open(DATA_FILE, 'w') as f:
                json.dump(DEFAULT_DATA, f)
            print(f"Created new data file: {DATA_FILE}")
            return DEFAULT_DATA.copy()

        except Exception as e:
            print(f"Error creating data file: {e}")
            return DEFAULT_DATA.copy()

latest_data = load_data()

def modbus_loop():
    global slave_status
    print("start ciclo")
    while True:
        try:
            ang = latest_data.get('ang')
            if ang is not None:
                addr = 101
                func = 6
                reg = 1
                
                # Send Angle
                frame = bytearray([addr, func])
                frame.extend(struct.pack('>H', reg))
                # Usa il valore assoluto poiché l'angolo calcolato per l'interfaccia può            essere negativo,
                     # e il formato '>H' (unsigned short) richiede numeri positivi.
                frame.extend(struct.pack('>H', abs(int(ang))))
                frame.extend(crc16(frame))
                



                try:
                    with serial.Serial('/dev/ttyACM0', 9600, timeout=1) as ser:
                        ser.write(frame)
                        time.sleep(0.5)
                        
                        # Read status: FC 03 (Read Holding Registers), Reg 0x0002
                        func_read = 3
                        reg_read = 2
                        frame_read = bytearray([addr, func_read])
                        frame_read.extend(struct.pack('>H', reg_read))
                        frame_read.extend(struct.pack('>H', 1))
                        frame_read.extend(crc16(frame_read))
                        
                        ser.write(frame_read)
                        time.sleep(0.5)
                        if ser.in_waiting:
                            resp = ser.read_all()
                            if len(resp) >= 7 and resp[0] == addr and resp[1] == func_read:
                                val = struct.unpack('>H', resp[3:5])[0]
                                if val == 0:
                                    slave_status = "automatico"
                                elif val == 1:
                                    slave_status = "manuale"
                                elif val == 2:
                                    slave_status = "allarme"
                                else:
                                    slave_status = "sconosciuto"

                            else:
                                slave_status = "Errore Risposta"
                        else:
                            slave_status = "Timeout Slave"
                except Exception as e:
                    print(f"Serial Error: {e}")
                    slave_status = "Errore Seriale"
        except Exception as e:
            print(f"Modbus loop error: {e}")
            
        time.sleep(60)

threading.Thread(target=modbus_loop, daemon=True).start()

# Alarm thresholds
TEMP_MIN = 5.0
TEMP_MAX = 25.0
#HUM_MIN = 20.0
#HUM_MAX = 75.0

def calculate_frequency(temperature):
    """
    Calculate frequency based on temperature using linear interpolation.
    - <= TEMP_MIN: 20Hz
    - >= TEMP_MAX: 50Hz
    """
    if temperature is None:
        freq = None
        return freq
        
    if temperature <= TEMP_MIN:
        freq = 20
        return freq
    elif temperature >= TEMP_MAX:
        freq = 50 
        return freq
    else:
        # Linear interpolation: y = y1 + ((x - x1) * (y2 - y1) / (x2 - x1))
        # x = temperature, x1 = TEMP_MIN, x2 = TEMP_MAX
        # y1 = 20, y2 = 50
        print("temperatura =", temperature)
        freq = int(20.0 + ((temperature - TEMP_MIN) * (50.0 - 20.0) / (TEMP_MAX - TEMP_MIN)))
        return freq

def calculate_angular_value(humidity):
    """
    Calcola l'angolo del flap in base all'umidità percentuale.
    
    Args:
        hum (float): Valore di umidità percentuale (0-100)
    
    Returns:
        float: Angolo del flap in gradi
    """
    # Validazione dell'input
    if humidity < 0 or humidity > 100:
        raise ValueError("L'umidità deve essere compresa tra 0 e 100")
    
    # Caso umidità < 10%: flap orizzontale (0 gradi)
    if humidity < 10:
        return 0.0
    
    # Caso umidità > 90%: flap verticale (-90 gradi)
    elif humidity > 90:
        return -90.0
    
    # Caso intermedio (10% - 90%): interpolazione lineare
    else:
        # Mappatura lineare: 10% -> 0 gradi, 90% -> -90 gradi
        # Pendenza: (-90 - 0) / (90 - 10) = -90/80 = -1.125 gradi per punto percentuale
        ang = -1.125 * (humidity - 10)
        return ang



@app.route('/')
def index():
    """Render the dashboard with current data and alarm status."""
    global latest_data, slave_status
    data = latest_data.copy()
    alarms = {
        'temp_alarm': None
    }
    
    if data['temperature'] is not None:
        if data['temperature'] < TEMP_MIN:
            alarms['temp_alarm'] = 'min' # Low temperature alarm
        elif data['temperature'] > TEMP_MAX:
            alarms['temp_alarm'] = 'max' # High temperature alarm
            
    if data['humidity'] is not None:
        pass


    return render_template('index.html', data=data, alarms=alarms, slave_status=slave_status)
@app.route('/upload', methods=['POST'])
def upload_data():
    """Receive sensor data from the client."""
    try:
        content = request.json
        print(f"Received upload request: {content}") # Debug print
        data_updated = False
        
        # Update temperature and humidity if present
        temp = content.get('temperature')
        hum = content.get('humidity')
        
        if temp is not None:
            latest_data['temperature'] = float(temp)
            latest_data['freq'] = calculate_frequency(float(temp))
            data_updated = True
            
        if hum is not None:
            latest_data['humidity'] = float(hum)
            latest_data['ang'] = calculate_angular_value(float(hum))
            data_updated = True

        # Update specific fields if present in the request
        fields_to_update = [
            'inverter_1',
            'freq-input-1',
            'vent-1', 'vent-2',
        ]
        
        for field in fields_to_update:
            if field in content:
                latest_data[field] = content[field]
                data_updated = True

        if data_updated:
            latest_data['last_updated'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            
            # Save to JSON file
            try:
                with open(DATA_FILE, 'w') as f:
                    json.dump(latest_data, f)
            except Exception as e:
                print(f"Error saving data: {e}")
                return jsonify({"status": "error", "message": f"Error saving data: {str(e)}"}), 500
                
            print(f"Data updated and saved.")
            return jsonify({"status": "success", "message": "Data received and saved"}), 200
        else:
            return jsonify({"status": "ignored", "message": "No valid data fields found"}), 200
            
    except Exception as e:
        print(f"Error processing upload: {e}")
        return jsonify({"status": "error", "message": str(e)}), 500

#@app.route('/2026/server_master/client/server_cp', methods=['POST'])

if __name__ == '__main__':
    # Run server on all interfaces, port 5000
    app.run(host='0.0.0.0', port=5000, debug=True)
    app.run(debug=True, use_reloader=False) # Aggiungi use_reloader=False

 
This is the listing of the test version referring to the Flask library, written in Python of which I recommend an in-depth analysis, also referring to the documentation of the specific library. Note in particular the two functions:

def calculate_frequency(temperature):

def calculate_angular_value(humidity):

Which are fundamental for the generation of frequency controls, for example referring to an inverter connected in Modbus mode, or to a servomotor control, as described in the Slave software simulated by the Raspberry Pico. All the data referred to by this software are contained in a “Json” file, which would be generated if it did not exist:
 

DATA_FILE = 'sensor_data.json'
DEFAULT_DATA = {
'temperature': None,
'humidity': None,
'freq': None,
'ang': None,
'inverter_1': "spento",
'servo_flap': "spento",
'ang-input': None,
'freq-input-1': None,
'vent-1': "spento",
'vent-2': "spento",
'last_updated': None
}

JSON files (.json) are text file formats based on JavaScript Object Notation, used to structure and exchange data between servers and web applications. Lightweight and readable, they store information in key-value pairs, as reported above. The app.py exposes the values ​​via a reference web page with the statement:

return render_template(‘index.html’, data=data, alarms=alarms, slave_status=slave_status)

This reference page, generated by the test application, is currently incomplete, as it could also be implemented with the status of the inverter (which would be referenced via a second Slave device. Its listing, reported as an HTML file, is the following and is found in the “/templates” directory:

The index.html listing


<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IoT Dashboard - Raspberry Pi</title>
    <!-- Refresh page every 30 seconds to fetch new data -->
    <meta http-equiv="refresh" content="30">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <!-- Google Fonts for premium look -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        .gauge-container {
            width: 100%;
            max-width: 150px;
            margin: auto;
        }
        .needle {
            transform-origin: 50px 50px;
            transition: transform 1s ease-out;
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>Monitoraggio Ambientale</h1>
            <p>Ultimo aggiornamento: {{ data.last_updated if data.last_updated else 'In attesa di dati...' }}</p>
        </header>
        <main class="dashboard">
            <!-- Temperature Card -->
            <div class="card {% if alarms.temp_alarm %}alarm-{{ alarms.temp_alarm }}{% endif %}">
                <div class="icon">🌡️</div>
                <h2>Temperatura</h2>
                <div class="value">
                    {% if data.temperature is not none %}
                    {{ data.temperature }} °C
                    <br>
                    <span style="font-size: 0.5em;">Freq: {{ data.freq }} Hz</span>
                    {% else %}
                    --
                    {% endif %}
                </div>
                <div class="status">
                    {% if alarms.temp_alarm == 'min' %}
                    ⚠️ ALLARME: Temperatura Bassa (< 5°C) {% elif alarms.temp_alarm=='max' %} ⚠️ ALLARME: Temperatura
                        Alta (> 25°C)
                        {% else %}
                        Normale
                        {% endif %}
                </div>
            </div>

            <!-- Humidity Card -->
            <div class="card {% if alarms.hum_alarm %}alarm-{{ alarms.hum_alarm }}{% endif %}">
                <div class="icon">💧</div>
                <h2>Umidità</h2>
                <div class="value">
                    {% if data.humidity is not none %}
                    {{ data.humidity }} %
                    {% else %}
                    --
                    {% endif %}
                </div>
                <!-- flap -->
                <div class="gauge-container" style="margin-top: 15px;">
                    <svg viewBox="0 0 100 100" class="gauge">
                        <!-- Terzo quadrante: da Sinistra (10, 50) a Basso (50, 90) -->
                        <path d="M 10 50 A 40 40 0 0 0 50 90" fill="none" stroke="#ddd" stroke-width="5" />
                        <!-- Map ang_val (0 to -90) to rotation (-90 to -180 degrees) -->
                        {% set ang_val = data.ang | float if data.ang is not none else 0 %}
                        {% set gauge_angle = -90 + ang_val %}
                        <!-- CSS styling applied safely avoiding parsing errors -->
                        <line class="needle needle-dynamic" x1="50" y1="50" x2="50" y2="10" stroke="red"
                            stroke-width="2" data-rotation="{{ gauge_angle }}" />
                        <circle cx="50" cy="50" r="3" fill="#333" />
                        <script>
                            document.querySelector('.needle-dynamic').style.transform = `rotate(${document.querySelector('.needle-dynamic').dataset.rotation}deg)`;
                        </script>
                    </svg>
                    <div style="font-size: 0.8em; margin-top: 5px;">Angolo: {{ data.ang if data.ang is not none else
                        'N/A' }}°</div>
                </div>
                <div class="status"
                    style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);">
                    <strong>Stato Slave:</strong> {{ slave_status }}
                </div>
            </div>

            <!-- Inverter Section -->
            <section class="inverter-container">
                <h2>Inverter</h2>
                <div class="inverter-grid">
                    <!-- Inverter 1 -->
                    <div class="inverter" id="inv-1">
                        <div class="comandi-sinistra">
                            <button class="btn-manuale" onclick="setInverterMode(1, 'manuale')">Manuale</button>
                            <div class="freq-input-wrapper" id="freq-input-wrapper-1" style="display: none;">
                                <input type="number" class="freq-input" id="freq-input-1" placeholder="Hz"
                                    onchange="updateFreqDisplay(1)">
                            </div>
                            <button class="btn-automatico active"
                                onclick="setInverterMode(1, 'automatico')">Automatico</button>
                            <button class="btn-spento" onclick="setInverterMode(1, 'spento')">Spento</button>
                        </div>
                        <div class="icona-destra">
                            <i class="fas fa-microchip inverter-icon verde" id="inv-icon-1"></i>
                            <div class="freq-display" id="freq-display-1">0 Hz</div>
                        </div>
                    </div>
            </section>
            <!-- Ventilatori Section -->
            <section class="ventilatori-container">
                <h2>Ventilatori</h2>
                <div class="ventilatori-grid">
                    <!-- 8 Fans -->
                    <!-- Simulating different states as requested/implied -->
                    <i class="fas fa-fan ventilatore-icon spento" id="vent-1" onclick="toggleFan(1)"></i>
                    <i class="fas fa-fan ventilatore-icon spento" id="vent-2" onclick="toggleFan(2)"></i>
                </div>
            </section>

        </main>

        <script>
            // Store server-calculated frequency
            // Using 'safe' filter or default to 0 if None/null
            const serverFreq = Number("{{ data.freq if data.freq is not none else 0 }}");

            // State tracking (optional, but good for cleanliness)
            const inverterStates = {
                1: '{{ data.inverter_1 if data.inverter_1 else "spento" }}'
            };

            const freqInputs = {
                1: '{{ data["freq-input-1"] if data["freq-input-1"] is not none else "" }}'
            };

            function updateServer(dataParam) {
                fetch('/upload', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(dataParam)
                }).then(response => {
                    if (!response.ok) console.error("Update failed", response);
                }).catch(err => console.error("Update error", err));
            }

            function setInverterMode(id, mode, skipServerUpdate = false) {
                inverterStates[id] = mode;

                if (!skipServerUpdate) {
                    let reqData = {};
                    reqData['inverter_' + id] = mode;
                    updateServer(reqData);
                }

                // Reset buttons
                document.querySelector(`#inv-${id} .btn-manuale`).classList.remove('active');
                document.querySelector(`#inv-${id} .btn-automatico`).classList.remove('active');
                document.querySelector(`#inv-${id} .btn-spento`).classList.remove('active');

                // Set active button
                document.querySelector(`#inv-${id} .btn-${mode}`).classList.add('active');

                // Handle Icon Color, Input Visibility, and Frequency Display
                const icon = document.getElementById(`inv-icon-${id}`);
                const inputWrapper = document.getElementById(`freq-input-wrapper-${id}`);
                const freqDisplay = document.getElementById(`freq-display-${id}`);




                // Remove all color classes first
                icon.classList.remove('spento', 'verde', 'azzurro', 'rosso', 'nero');

                if (mode === 'manuale') {
                    icon.classList.add('azzurro');
                    inputWrapper.style.display = 'block';
                    // Show input value
                    const val = document.getElementById(`freq-input-${id}`).value;
                    freqDisplay.innerText = val + ' Hz';

                } else if (mode === 'automatico') {
                    icon.classList.add('verde');
                    inputWrapper.style.display = 'none';
                    // Show calculated frequency
                    freqDisplay.innerText = serverFreq + ' Hz';

                } else if (mode === 'spento') {
                    icon.classList.add('nero');
                    inputWrapper.style.display = 'none';
                    // Show 0 Hz
                    freqDisplay.innerText = '0 Hz';
                }
            }
            function updateFreqDisplay(id) {
                // Only update display if we are in Manual mode
                if (inverterStates[id] === 'manuale') {
                    const val = document.getElementById(`freq-input-${id}`).value;
                    document.getElementById(`freq-display-${id}`).innerText = val + ' Hz';
                    let reqData = {};
                    reqData['freq-input-' + id] = val;
                    updateServer(reqData);
                }
            }
            // Optional: Toggle fan states for demo
            function toggleFan(id) {
                const el = document.getElementById(`vent-${id}`);
                let state = '';
                if (el.classList.contains('attivo')) {
                    el.classList.remove('attivo');
                    el.classList.add('spento');
                    state = 'spento';
                } else if (el.classList.contains('spento')) {
                    el.classList.remove('spento');
                    el.classList.add('avaria');
                    state = 'avaria';
                } else {
                    el.classList.remove('avaria');
                    el.classList.add('attivo');
                    state = 'attivo';
                }

                let reqData = {};
                reqData['vent-' + id] = state;
                updateServer(reqData);
            }

            // Initialize displays on load based on correct HTML state sent from the server
            document.addEventListener('DOMContentLoaded', () => {
                // Initialize all inverters that are dynamically updated
                for (let id = 1; id <= 2; id++) {
                    const inputEl = document.getElementById(`freq-input-${id}`);
                    if (freqInputs[id] !== "") {
                        inputEl.value = freqInputs[id];
                    }
                    setInverterMode(id, inverterStates[id], true);
                }
                // Initialize fans based on server state
                const fanStates = {
                    1: '{{ data["vent-1"] if data["vent-1"] else "spento" }}',
                    2: '{{ data["vent-2"] if data["vent-2"] else "spento" }}'
                };
                for (let id = 1; id <= 2; id++) {
                    const fanEl = document.getElementById(`vent-${id}`);
                    fanEl.classList.remove('spento', 'attivo', 'avaria');
                    fanEl.classList.add(fanStates[id]);
                }
            });
        </script>

        </main>

        <footer>
            <p>Server: Raspberry Pi 4B | Client: Pico W</p>
        </footer>
    </div>
</body>

</html>

This listing includes Javascript code that is inserted into HTML files using tags <script> and </script>. This method allows you to include instructions directly on the page (inline) or link external .js files. The tag can be placed inside the <head> or at the end of the <body>, the latter practice being recommended to improve loading performance, as in this case. A self-respecting web page also has a style, which in this case refers to the following link: 

<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}>

The HTML style page uses Cascading Style Sheets (CSS) to define its appearance, layout, and formatting, separating content from presentation. CSS allows you to control colors, fonts, sizes and arrangements, making pages lighter and more responsible.

The listing of the ‘style.css’ file


:root {
--bg-color: #0f172a;
/* Dark blue/slate background */
--card-bg: #1e293b;
--text-primary: #e8bd6c;;
--text-secondary: #94a3b8;
--accent-color: #38bdf8;
/* Cyan */
--alarm-min: #0ea5e9;
/* Blue for cold/low */
--alarm-max: #ef4444;
/* Red for hot/high */
--alarm-bg-min: rgba(14, 165, 233, 0.2);
--alarm-bg-max: rgba(239, 68, 68, 0.2);
--success-color: #22c55e;
}

* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family: 'Outfit', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
width: 100%;
max-width: 800px;
padding: 2rem;
text-align: center;
}

header h1 {
font-weight: 600;
font-size: 2.5rem;
margin-bottom: 0.5rem;
background: linear-gradient(to right, var(--accent-color), #818cf8);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}

header p {
color: var(--text-secondary);
margin-bottom: 3rem;
font-size: 1.1rem;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
.card {
background-color: var(--card-bg);
border-radius: 1rem;
/* slightly smaller radius */
padding: 1rem;
/* Reduced from 2rem */
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
}
.icon {
font-size: 2rem;
/* Reduced from 3rem */
margin-bottom: 0.5rem;
/* Reduced margin */
}
.card h2 {
font-weight: 200;
color: var(--text-secondary);
font-size: 1rem;
/* Reduced from 1.2rem */
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 0.5rem;
}
.value {
font-size: 1.5rem;
/* Reduced from 4rem */
font-weight: 600;
margin-bottom: 0.5rem;
}


.status {
padding: 0.25rem 0.75rem;
/* Reduced padding */
border-radius: 9999px;
background-color: rgba(255, 255, 255, 0.05);
display: inline-block;
font-size: 0.8rem;
/* Reduced font size */
}

/* Alarm States */
.card.alarm-min {
border-color: var(--alarm-min);
background-color: var(--alarm-bg-min);
}
.card.alarm-min .value {
color: var(--alarm-min);
}
.card.alarm-max {
border-color: var(--alarm-max);
background-color: var(--alarm-bg-max);
}
.card.alarm-max .value {
color: var(--alarm-max);
}


footer {
margin-top: 4rem;
color: var(--text-secondary);
font-size: 0.8rem;
}
@media (max-width: 600px) {
header h1 {
font-size: 2rem;
}
.value {
font-size: 3rem;
}
}
section {
background: #000080;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
/* NEW: Force sections to be full width in the grid to stack vertically */
grid-column: 1 / -1;
width: 100%;
}
/* Stile per Inverter, Ventilatori, Finestre */
.inverter-grid,
.ventilatori-grid {
display: grid;
gap: 20px;
justify-content: center;
margin-top: 1rem;
}


/* Force 1 columns for inverters as requested "in una unica riga" */
.inverter-grid {
grid-template-columns: repeat(1, 1fr);
}

/* Force 2 columns for fans as requested "in una unica riga" */
.ventilatori-grid {
grid-template-columns: repeat(2, 1fr);
align-items: center;
justify-items: center;
}

.inverter {
background-color: #fff;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
/* Flex row to put buttons left, icon right */
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 10px;
}

/* Left controls (Buttons) */
.comandi-sinistra {
display: flex;
flex-direction: column;
gap: 8px;
width: 60%;
/* Occupy left side */
}
/* Buttons Styling */
.comandi-sinistra button {
padding: 8px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
background-color: #e2e8f0;
/* Default Gray (Inactive) */
color: #475569;
transition: all 0.2s;
}

/* Active Button States */
/* Spento active -> Black */
.comandi-sinistra button.btn-spento.active {
background-color: #000000;
color: #ffffff;
}
/* Automatico active -> Green */
.comandi-sinistra button.btn-automatico.active {
background-color: var(--success-color);
/* Green */
color: #ffffff;
}
/* Manuale active -> Cyan (Azzurro) */
.comandi-sinistra button.btn-manuale.active {
background-color: var(--accent-color);
/* Cyan */
color: #0f172a;
/* Dark text for contrast */
}
/* Input field wrapper */
.freq-input-wrapper {
margin-top: -4px;
/* Tighten up spacing */
margin-bottom: 4px;
}
.freq-input {
width: 100%;
padding: 6px;
border: 1px solid #cbd5e1;
border-radius: 6px;
text-align: center;
}
/* Right side (Icon + Display) */
.icona-destra {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 40%;
}
.inverter-icon {
font-size: 3.5rem;
/* Large icon */
margin-bottom: 8px;
transition: color 0.3s;
}

.freq-display {
font-weight: bold;
color: var(--text-secondary);
}

/* Icon Colors */
.inverter-icon.spento {
color: #000000;
}

.inverter-icon.verde {
color: var(--success-color);
}
/* Auto */
.inverter-icon.azzurro {
color: var(--accent-color);
}
/* Manual */
.inverter-icon.rosso {
color: var(--alarm-max);
}

/* Alarm */
.inverter-icon.nero {
color: #000000;
}

/* Fan Icons */
.ventilatore-icon {
font-size: 2.5rem;
cursor: pointer;
transition: color 0.3s;
}

.ventilatore-icon.spento {
color: #000000;
}

/* Black */
.ventilatore-icon.attivo {
color: #3b82f6;
}
/* Blue */
.ventilatore-icon.avaria {
color: var(--alarm-max);
}
/* Red */
.ventilatore-icon.blu {
color: #3b82f6;
}
/* Responsive adjustments */
@media (max-width: 900px) {
.inverter-grid {
grid-template-columns: repeat(2, 1fr);
/* 2x2 on tablets */
}
.ventilatori-grid {
grid-template-columns: repeat(4, 1fr);
/* 4x2 on tablets */
}
}
@media (max-width: 600px) {
.inverter-grid {
grid-template-columns: 1fr;
/* Stack on mobile */
}
.ventilatori-grid {
grid-template-columns: repeat(4, 1fr);
}
}

Finally, I report the graphic aspect of the web page:


In this image you can see the presence of a high temperature alarm, a fan in normal operation (blue colour) and a faulty fan (red colour). The inverter in automatic mode (at maximum frequency) and the servomotor in automatic mode (with calculated angle). The app.py file in the terminal, at the same time, returns the following

172.18.92.148 - - [15/Mar/2026 21:39:24] "POST /upload HTTP/1.0" 200 - 

127.0.0.1 - - [15/Mar/2026 21:39:24] "GET / HTTP/1.1" 200 -  

127.0.0.1 - - [15/Mar/2026 21:39:24] "GET /static/style.css HTTP/1.1" 304 - 

Received upload request: {'humidity': 45, 'temperature': 26} Data updated and saved.


Corollary

The project that is proposed again in this article, under construction, with the use of supports such as Visual Studio Code (abbreviated as VS Code) which is a free, lightweight and powerful source code editor developed by Microsoft for Windows, macOS and Linux, natively supports languages such as JavaScript, TypeScript and Node.js, and is highly extensible via plugins for programming in Python, C++, Java and more, but in managing communication with the Rasperry Pi Pico, connected to the port virtual serial port on USB, via the device file “/dev/ttyACM0”, could cause problems due to communication conflicts. In other ways, the file that can be installed on the Raspberry Pi Pico, which if named main.py, is automatically activated when the device is turned on, has a response peculiarity that makes it difficult, during the testing phase, to block it, to make any changes. So even if named differently, even if run from the PC’s local directory, once launched it cannot be stopped, you have to physically disconnect the device to make any changes to the software, at least this is what happened to me, with a PC with Ubuntu operating system.
For programming embedded devices, such as the Raspberry Pico, or, alternatively, Arduino Nano, or Adafruit Feather, Pimoroni Tiny 2040, all RP2040-based architectures, but also ESP32, or other development boards, the best IDEs for programming in MicroPython include Thonny (recommended for beginners, supports Raspberry Pi Pico), VS Code with extensions (powerful and flexible), and PyCharm (ideal for complex projects).
These platforms offer essential tools such as syntax highlighting, debugging, and file management on the microcontroller. My advice, both for beginners, but also for those who have been tinkering with it for some time, is to try using “Inferential Machines”, as I prefer to call them, that is, those platforms that allow the integration of A.I. agents. for code development. I won’t hide it, in order to make these contributions, in order to speed up the process, I relied on Google’s Anigravity which is a cutting-edge IDE (Integrated Development Environment) based on artificial intelligence, which uses autonomous agents to develop, test and correct software.
Based on VS Code, it allows you to manage multiple projects in parallel, monitor AI actions and generate code via prompts. The main features of Google Antigravity are:
  • Autonomous AI Agents: AI doesn’t just suggest code, it plans, writes, debugs, and interacts with the terminal and browser to create applications. • Vibe Coding and Development: Allows you to describe the desired outcome in natural language, letting AI handle the technical implementation.
  • Multi-Agent Management: Allows you to launch multiple agents in parallel for different projects, monitoring activities via a dedicated “inbox”.
  • Advanced Integration: Includes a three-section interface: files, AI chat and browser preview (Antigravity Browser Control) for real-time testing.
  • Security: Introduces “Safety Rails” to prevent irreversible actions without confirmation.
  • Compatibility: Supports advanced models including Gemini Pro, Claude and open source variants. What can I say? At this point, being the old cybernetic that I am, “I throw in the towel!”
The “Inference Machine” certainly read many more texts than me, reviewed much more code than I wrote and, I admit, it helped me a lot! It’s true that I had to work on the generated code to adapt it to my needs, but I must say that I was amazed by the potential that these machines have achieved in this last period. I am convinced that the support of these tools with operators in the IT sector does not diminish human work: if on the one hand there is a paradigm shift in creating software, on the other the skills remain essential and necessary for those who must or want to create certain logical structures. I do not feel absolutely diminished in using these tools, if anything I remain fascinated by their emerging potential, which, I must admit, could be such as to cause us to lose control of certain operations, if these operations were complex and completely delegated, in trust in the development and decision-making potential that algorithmic structures could adopt autonomously.


Development Tips: Inverter Simulation

Taking inspiration from this educational session, it could be interesting to implement another Slave device that could simulate an inverter for controlling the fans in frequency, also put into communication with the Modubs protocol, similar to the one already proposed, perhaps using a Raspberry Pi Pico, equipped with stepper motors.

The 28BYJ-48



This is a 5-wire unipolar stepper motor widely used for robotic applications. These motors provide good torque even when stopped as long as power is supplied to the motor. On the control board there is the ULN2003A integrated circuit which includes an array of 7 Darlington transistors, each capable of managing a current of 500mA with voltages in the order of 50V

Engine Specifications

  • Diameter: 28 mm / 1.1 inch.
  • Voltage: 5V
  • Length: 274 mm / 10.8 inches.
  • Step angle: 5.625 x 1/64
  • Reduction ratio: 1/64


Operating principle

A 5-wire unipolar stepper motor operates by sequentially powering four coils via a common pole, driven by the ULN2003A driver. The driver acts as a switch, activating Darlington transistors that ground each phase, attracting the magnetic rotor according to a sequence (e.g. “full-step” or “half-step”) to obtain precise rotations: The control excitation sequence occurs by activating one coil at a time (Wave Drive) or two at a time (Full Step), generating a magnetic field that attracts the rotor, producing the movement of a “step”.


The 28BYJ-48 stepper motor has a step angle of 5.625°/64 in half-step mode. This means that the motor has a step angle of 5.625°, so it needs 360°/5.625° = 64 steps in half step mode. In full step mode: 64/2 = 32 steps to complete one rotation. 
The output shaft is driven via a gear ratio of 64:1, this means that the shaft completes one rotation if the internal motor rotates 64 times. 
Therefore the motor will have to take 32 × 64 = 2048 steps for the shaft to complete one complete rotation, therefore an accuracy of 360°/2048 steps = 0.18°/step will be obtained.

This motor can reach a maximum output speed from the gearbox of approximately 15-20 revolutions per minute.

 

Connection diagram

An example of a wiring diagram is shown in the following image: 



Software generation

In this case I take this opportunity to describe the operations necessary to carry out the procedure I adopted to generate this software:

The necessary prompt

“generate a code in MicroPython for a second Slave device with SLAVE_ID = 102, naming it main_inverter.py using the Modbus protocol with CRC, as already done for the main.py program in the /slave directory, coordinated with the app.py application to simulate an inverter, using Raspberry pi pico which will have to control a 28BYJ-48 stepper motor with ULN2003A driver to modulate the maximum speed of 20 RPM, based on the 50Hz frequency value and the minimum speed 5 RPM for the 20Hz frequency, the frequency is calculated by the def calculate_frequency(temperature) function: 

If the inverter setting set on app.py is manual, the manually set frequency is forwarded. The calculation function for the speed command of the 28BYJ-48 is calculated by a function defined as”speed” in main_inverter.py If pin 0 of the Raspberry Pi is at 1, the main_inverter.py sends the “servo_flap” command: “failure”.

Antigravity’s answer

  • Analyzed main.py 
  • Analyzed app.py
Initiating Data Analysis I’m now diving deeper into the specifics, formulating my approach to extract the necessary information and ensure the next phase is set for success. The process of information analysis is underway.
Refining Execution Steps I’m now focusing on the script’s core functionalities, making sure that Modbus slave implementation and motor control are seamlessly integrated.
The next step is to ensure proper response to Modbus commands and the correct implementation of motor speed control based on the input data. I’m also ensuring the correct logic for handling manual frequency input.
Mapping Alarm States My focus is now mapping the alarm state logic, specifically how the “avaria” state is communicated via Modbus registers.
I’m examining the connection between the Raspberry Pi Pico’s pin 0 and the Modbus status register within app.py, ensuring the proper interpretation and response to this alarm condition, and I’m correlating this with the main.py implementation.
Defining Slave Communication I’m now determining how the inverter’s code will communicate with another Modbus slave, specifically addressing the creation and coordination of the MicroPython script and its interaction with the existing application.
I’m focusing on defining communication protocols and ensure proper integration of the components. I’m starting to generate the required MicroPython code, and will then update the application code to match the same.
Implementing Stepper Control I am now proceeding with implementing the stepper control logic, making sure it integrates smoothly within the existing system.
The core of this involves formulating and writing the code and making sure the motor control and Modbus communication are non-blocking.

The code will focus on the motor speed using step sequences.

Defining Motor Behavior I’m now determining how to define the motor’s operational logic, focusing on establishing the relation between frequency and motor speed, along with the calculation of appropriate step delays. I’m making certain the code addresses alarm states through the appropriate input pin to the Raspberry Pi Pico. Processing Modbus Frames
I’m now focusing on parsing incoming Modbus frames, checking the slave ID, function code, and CRC to ensure data integrity.
The logic is being written to handle valid frames, and any invalid data is being discarded.
I’m extracting data and determining the correct function code to ensure correct processing.
Responding to Read Requests I’m currently focused on responding correctly to read requests within the Modbus protocol, specifically when retrieving the device status.
I’m verifying the input data, constructing the response frame, and ensuring the data is correctly formatted, so it can be sent back through the communication channel.
Adjusting Motor Control I’m now refining the motor control logic within the program. I’m focusing on ensuring the step delay is properly managed, based on external frequency inputs.
The goal is to make sure the motor responds correctly to stop signals, and the system is properly handling different operational modes. Implementing Inverter Modbus Slave Create MicroPython Modbus Slave code for an inverter (main_inverter.py) controlling a stepper motor and coordinate it with app.py. Writing main_inverter.py and updating app.py
I created the main_inverter.py file inside the /slave directory (configured with SLAVE_ID = 102 and the control implementation for the 28BYJ-48 motor with the speed function).
Furthermore, I updated the /master/app.py file to coordinate communications in Modbus also with the second slave, sending the manual or calculated frequency depending on the mode and managing the read failure status.
You can verify the files and run them. If there are other changes or tests to do, let me know!
So, it generated this listing:

Listing of the inverter simulator


import machine
import utime
from machine import Pin
import sys
import select
import struct

# Costanti Modbus
SLAVE_ID = 102
FC_READ_HOLDING = 3
FC_WRITE_SINGLE = 6

# Pin configurazione
pin_alarm = 0 # Pin 0 per avaria
IN1 = Pin(28, Pin.OUT)
IN2 = Pin(27, Pin.OUT)
IN3 = Pin(26, Pin.OUT)
IN4 = Pin(22, Pin.OUT)

pins = [IN1, IN2, IN3, IN4]

# Sequenza a mezzo passo (half-step) per 28BYJ-48
step_sequence = [
    [1,0,0,0],
    [1,1,0,0],
    [0,1,0,0],
    [0,1,1,0],
    [0,0,1,0],
    [0,0,1,1],
    [0,0,0,1],
    [1,0,0,1]
]

p_alm = Pin(pin_alarm, Pin.IN, Pin.PULL_DOWN)
led = Pin("LED", Pin.OUT)

current_freq = 0
step_index = 0
last_step_time = utime.ticks_us()
step_delay_us = 0 

def speed(freq):
    """
    Calcola il delay in microsecondi per ogni passo del motore 28BYJ-48.
    Frequenza da 20Hz a 50Hz, Velocità da 5 RPM a 20 RPM.
    """
    if freq < 20:
        return 0 # Fermo
    if freq > 50:
        freq = 50
    # Equazione lineare: rpm = 5 + (freq - 20) * (15 / 30)
    rpm = 5.0 + (freq - 20.0) * 0.5
    # 4096 passi per rivoluzione completa (half-step)
    steps_per_sec = (rpm * 4096) / 60.0
    if steps_per_sec > 0:
        return int(1000000 / steps_per_sec)
    return 0

def get_status():
    if p_alm.value() == 1:
        return 2 # Allarme / Avaria
    else:
        return 0 # Normale

def crc16(data):
    crc = 0xFFFF
    for char in data:
        crc ^= char
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return struct.pack('<H', crc)

print("Starting Modbus Slave Inverter...")

poll = select.poll()
poll.register(sys.stdin, select.POLLIN)

import micropython
micropython.kbd_intr(-1)

rx_buffer = bytearray()

while True:
    events = poll.poll(0) # 0ms timeout, non bloccante
    if events:
        try:
            while poll.poll(0):
                chunk = sys.stdin.buffer.read(1)
                if not chunk:
                    break
                rx_buffer.extend(chunk)
            
            while len(rx_buffer) >= 8:
                buffer = rx_buffer[:8]
                if buffer[0] == SLAVE_ID:
                    fc = buffer[1]
                    received_crc = buffer[-2:]
                    calc_crc = crc16(buffer[:-2])
                    
                    if received_crc == calc_crc:
                        rx_buffer = rx_buffer[8:]
                        led.value(1)
                        

                        if fc == FC_WRITE_SINGLE:
                            reg_addr = struct.unpack('>H', buffer[2:4])[0]
                            value = struct.unpack('>H', buffer[4:6])[0]
                            if reg_addr == 1:
                                current_freq = value
                                step_delay_us = speed(current_freq)
                                
                            sys.stdout.buffer.write(buffer)
                            
                        elif fc == FC_READ_HOLDING:
                            reg_addr = struct.unpack('>H', buffer[2:4])[0]
                            num_regs = struct.unpack('>H', buffer[4:6])[0]
                            if reg_addr == 2 and num_regs == 1:
                                status = get_status()
                                resp = bytearray([SLAVE_ID, FC_READ_HOLDING, 2])
                                resp.extend(struct.pack('>H', status))
                                resp.extend(crc16(resp))
                                sys.stdout.buffer.write(resp)    
                        led.value(0)
                    else:
                        rx_buffer = rx_buffer[1:]
                else:
                    rx_buffer = rx_buffer[1:]
        except Exception as e:
            rx_buffer = bytearray()   
    # Motor control logic
    if step_delay_us > 0:
        now = utime.ticks_us()
        if utime.ticks_diff(now, last_step_time) >= step_delay_us:
            last_step_time = now
            for i in range(4):
                pins[i].value(step_sequence[step_index][i])
            step_index = (step_index + 1) % 8
    else:
        for i in range(4):
            pins[i].value(0)



My consideration, when I read the product code in relation to the prompt I proposed to the machine was: how long would it take me to “type this code”? How long would it take me to think about how to structure this logic? Antigravity took 46 seconds! Before even trying to install the software on the Raspberry Pico, to see “if it runs”,
I realized that, compared to the diagram I indicated, it had selected different pins for the stepper sequence, and I modified that part of the code, in order to adapt it to the diagram.
Then I started rereading the code, from the beginning, to try to understand which libraries he had used:

`import machine

import utime

from machine import Pin

import sys

import select

`import struct


and I started looking for further information: 
The “machine” library in MicroPython is the fundamental module for interacting with the microcontroller hardware. It allows you to manage GPIO pins (input/output), PWM signals, communication protocols (I2C, SPI, UART), ADC, timers and system reset. It is essential for the direct control of electronic peripherals… Below I could see that MicroPython uses “utime” (and other modules with the “u” prefix, such as ujson or uos) instead of the standard Python time module mainly for reasons of efficiency, size and compatibility with limited resources… Similar to Python, the “sys” library in MicroPython provides specific functions for interacting with the interpreter and hardware, such as sys.argv (arguments), sys.exit() (exit), sys.stdin/stdout/stderr (standard I/O), and sys.path (module search). It is critical to manage the execution flow and limited resources of the microcontroller.
The “select” library in MicroPython (often referenced as uselect) allows you to efficiently monitor multiple I/O streams (socket, file, UART) simultaneously, waiting for them to become ready for reading or writing. It is critical for asynchronous programming, avoiding main loop blocking. The “struct” library in MicroPython (often implemented as ustruct) converts between Python values and C structs represented as bytes objects. It is essential for managing compact binary data, crucial in embedded systems, offering pack() and unpack() functionality with format strings.
Below I moved on to the analysis of the sequence of functions:
def speed(freq):… def get_status():… def crc16(data):…
In short, if, on the one hand, I did not dedicate myself to writing code, on the other, I had the opportunity to better understand the ramifications of the software necessary to develop this small application, it is not “copy - paste”, working with “vibe coding” means developing software using generative AI (such as LLM) to write code through natural language descriptions, focusing on the logic and the result (“vibe”) rather than on manual syntax. 

It is a rapid and collaborative approach, ideal for prototypes and automations, where the human controls and the AI generates, the mind is not “lazy”, on the contrary! Those who have good will, imagination and are motivated discover that this approach stimulates them to expand and delve deeply into the work context. Speeding up the process does not mean “facilitating” the task, it means giving the opportunity to “improve performance”, it requires additional skills, it does not “remove responsibility”! “Using an accelerator instead of pedals not only means going faster, it requires quicker reflexes, greater attention to the (road) code, and… greater (mental) balance”.


Further developments

In relation to the above, I could also suggest developing the Master/Server part, for example starting from This:

The starting idea was precisely this, to create an application to simulate the management of environmental monitoring and the control of a series of four inverters that drive two fans each, of a control device for flapper windows, perhaps also associating the data of an external environmental control unit (not visible in the data reported on the example web page), climate management of an animal shelter environment, for example, where air exchange is also important. The figures in red are faulty equipment, those in black are stopped equipment, while in blue, they are functioning equipment (in green for automatic inverters). The web page will need to be further modified to describe the mode of operation and location of the flapper angular motion system, as in a test site for an inverter. Something that can be summarized very briefly by the following images:
 

Animal shelter

Climate electrical panel.


Thanks for having the patience to read. 
Romeo Ceccato


Master & Slave

 Master & Slave (Freeware for education, third part) “   Life, a movie in my head A page of lines we read The words remain u...