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://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:
- 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):
- 8 bit: Full humidity
- 8 bit: Decimal humidity (usually 0 on DHT11)
- 8 bit: Integer temperature
- 8 bit: Decimal temperature (usually 0 on DHT11)
- 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.
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.
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:
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
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:
- 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)
- 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”).
- Upload: Drag the downloaded .uf2 file into the RPI-RP2. The Pico will automatically restart with MicroPython.
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
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
- Coils: Boolean values (On/Off) for reading and writing (e.g. starting an engine).
- Discrete Inputs: Read-only Boolean values (e.g. the state of a limit switch).
- Input Registers: 16-bit read-only values (e.g. temperature read by a sensor).
- 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)
- 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.
For Modbus RTU, this polynomial is the standard x16+x15+x2+1.
- The sender divides the message by the polynomial and gets a remainder (the CRC).
- The sender “attaches” this remainder to the end of the original message.
- The receiver divides the entire packet (Message + CRC) by the same polynomial.
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)
The Master/Server software
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.
|
The app.py listing
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
def calculate_frequency(temperature):
def calculate_angular_value(humidity):
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
}
return render_template(‘index.html’, data=data, alarms=alarms, slave_status=slave_status)
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>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}>
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:
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
- 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!”
Development Tips: Inverter Simulation
The 28BYJ-48
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”.
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
The necessary prompt
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
The code will focus on the motor speed using step sequences.
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)
`import machine
import utime
from machine import Pin
import sys
import select
`import struct



























