Modules
Modules
The WindShaper is composed of modules, the SDK provides an interface to access the information of these modules, such as their type, position, RPMs and more.
Callback
To access the Module data, users can register a callback function.
The given function will automatically be called each time new Module data are available, with the new data passed as an argument to the function.
dict[tuple[int, int], ModuleInfo]
Module Position: (7, 7)
IP Address: FAKE
Type: 0816
Lifepoints: 50
Layer DOWNSTREAM:
Fan 0: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4571.00
Fan 1: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4668.00
Fan 2: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4710.00
Fan 3: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4873.00
Fan 4: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4639.00
Fan 5: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4764.00
Fan 6: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4660.00
Fan 7: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4705.00
Fan 8: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4710.00
Layer UPSTREAM:
Fan 0: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4535.00
Fan 1: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4511.00
Fan 2: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4685.00
Fan 3: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4894.00
Fan 4: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4650.00
Fan 5: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4563.00
Fan 6: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4712.00
Fan 7: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4767.00
Fan 8: Target PWM: 30.00 | Current PWM: 30.00 | Current RPM: 4461.00
import os
import threading
from dotenv import load_dotenv
from windsuite_sdk import ModuleInfo, WindsuiteSDK
load_dotenv()
SERVER_IP_ADDRESS = os.getenv("SERVER_IP_ADDRESS", default="localhost")
def on_module_update(data: dict[tuple[int, int], ModuleInfo]) -> None:
"""
Callback for module updates.
Args:
data: Dictionary mapping module row and column to their information.
"""
for position, module_info in data.items():
print(f"Module Position: {position}")
print(f" IP Address: {module_info.ip}")
print(f" Type: {module_info.type}")
print(f" Lifepoints: {module_info.lifepoints}")
# ! FOR EACH FAN LAYER
# The fans are indexed as a 3x3 matrix as they are physically arranged in the modules
# ┌─────┬─────┬─────┐
# │ 1 │ 2 │ 3 │
# ├─────┼─────┼─────┤
# │ 4 │ 5 │ 6 │
# ├─────┼─────┼─────┤
# │ 7 │ 8 │ 9 │
# └─────┴─────┴─────┘
for layer_index in range(len(module_info.target_pwm)):
layer_name = (
"DOWNSTREAM"
if layer_index == ModuleInfo.INDEX_DOWNSTREAM
else "UPSTREAM"
)
print(f"\tLayer {layer_name}:")
# ! FOR EACH FAN IN THE LAYER
for fan_index in range(len(module_info.target_pwm[layer_index])):
target_pwm = module_info.target_pwm[layer_index][fan_index]
current_pwm = module_info.current_pwm[layer_index][fan_index]
current_rpm = module_info.current_rpm[layer_index][fan_index]
print(
f"\t\tFan {fan_index}: Target PWM: {target_pwm:.2f} | Current PWM: {current_pwm:.2f} | Current RPM: {current_rpm:.2f}"
)
print("-" * 20)
stop_event = threading.Event()
def main() -> None:
"""Main function to run the WindSuite SDK example."""
base_url = f"http://{SERVER_IP_ADDRESS}"
print(f"Connecting to WindSuite server at {base_url}")
sdk = WindsuiteSDK(base_url=base_url)
sdk.register_module_update_callback(callback=on_module_update)
sdk.start_communication()
try:
freq_hz = 25
sdk.fan_controller.set_intensity(percent=30)
sdk.fan_controller.apply()
while not stop_event.wait(timeout=(1.0 / freq_hz)):
# ! DO WHATEVER
pass
except KeyboardInterrupt:
print("\nShutting down...")
stop_event.set()
finally:
sdk.fan_controller.set_intensity(0).apply()
sdk.cleanup()
print("SDK stopped")
if __name__ == "__main__":
main()
Module info representation
The ModuleInfo object passed to the callback contains the following information about each module:
| Field | Type | Description |
|---|---|---|
row | int | Row position of the module in the WindShaper grid |
col | int | Column position of the module in the WindShaper grid |
mac | str | MAC address of the module |
ip | str | IP address of the module |
type | str | Module type identifier (e.g., "0816", "0812", "2420") |
lifepoints | int | Health indicator of the module |
target_pwm | list[list[float]] | Target PWM values per layer and fan |
current_pwm | list[list[float]] | Current PWM values per layer and fan |
current_rpm | list[list[float]] | Current RPM values per layer and fan |
target_psu_state | bool | Target power supply unit state |
current_psu_state | bool | Current power supply unit state |
is_connected | bool | Whether the module is currently connected |
The class also provides two class constants for layer indexing: INDEX_DOWNSTREAM = 0 and INDEX_UPSTREAM = 1.
Understanding PWM and RPM data format
Depending on the module type, the RPM and PWM data can vary in structure.
To ensure consistency and ease of use, all received values follow a unified format: a list of lists, where each inner list represents a layer and contains the values for each fan in that layer.
For single-fan modules like the 2420, the data is straightforward : one layer with one fan.
For multi-fan modules like the 0816 or 0812, you get two layers (upstream and downstream), each containing values for the 9 fans arranged in that layer.
This convention allows you to iterate through layers and fans in a predictable way, regardless of the module type.
@dataclass
class ModuleInfo:
INDEX_DOWNSTREAM: ClassVar[int] = 1
INDEX_UPSTREAM: ClassVar[int] = 0
row: int
col: int
mac: str
ip: str
type: str
lifepoints: int
target_pwm: list[list[float]]
current_pwm: list[list[float]]
current_rpm: list[list[float]]
target_psu_state: bool
current_psu_state: bool
is_connected: bool = False
Additional Module Data
Some modules may also provide optional additional data fields:
| Field | Type | Description |
|---|---|---|
esc_rpm | int | None | ESC-reported RPM value |
applied_pwm_us | int | None | Applied PWM value in microseconds |
esc_voltage_mv | int | None | ESC voltage in millivolts |
esc_bus_current_ma | int | None | ESC bus current in milliamperes |
esc_motor_current_ma | int | None | ESC motor current in milliamperes |
esc_index | int | None | ESC index identifier |
esc_temp_mos_celcius | int | None | ESC MOSFET temperature in Celsius |
esc_temp_capacitor_celcius | int | None | ESC capacitor temperature in Celsius |
esc_temp_mcu_celcius | int | None | ESC microcontroller temperature in Celsius |
esc_temp_motor_celcius | int | None | ESC motor temperature in Celsius |
esc_running_error | int | None | ESC running error code |
esc_uptime | int | None | ESC uptime in seconds |
applied_psu | int | None | Applied power supply unit identifier |
psu_v_in | float | None | PSU input voltage in volts |
psu_i_in | float | None | PSU input current in amperes |
psu_p_in | float | None | PSU input power in watts |
psu_v_out | float | None | PSU output voltage in volts |
psu_i_out | float | None | PSU output current in amperes |
psu_p_out | float | None | PSU output power in watts |
psu_temp1 | float | None | PSU temperature sensor 1 in Celsius |
psu_temp2 | float | None | PSU temperature sensor 2 in Celsius |
psu_temp3 | float | None | PSU temperature sensor 3 in Celsius |
None.--------------------
Module Position: (8, 8)
IP Address: FAKE
Type: 0816
Lifepoints: 50
ESC Data:
RPM: None
Applied PWM (us): None
Voltage (mV): None
Bus Current (mA): None
Motor Current (mA): None
ESC Index: None
Temp MOS (°C): None
Temp Capacitor (°C): None
Temp MCU (°C): None
Temp Motor (°C): None
Running Error: None
Uptime (s): None
PSU Data:
Applied PSU: None
IN : None V | None A | None W
OUT: None V | None A | None W
Temps: None °C | None °C | None °C
Layer DOWNSTREAM:
Fan 0:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4738.00
Fan 1:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4589.00
Fan 2:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4537.00
Fan 3:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4785.00
Fan 4:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4704.00
Fan 5:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4475.00
Fan 6:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4630.00
Fan 7:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4611.00
Fan 8:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4722.00
Layer UPSTREAM:
Fan 0:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4716.00
Fan 1:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4816.00
Fan 2:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4781.00
Fan 3:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4610.00
Fan 4:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4715.00
Fan 5:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4530.00
Fan 6:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4882.00
Fan 7:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4804.00
Fan 8:
Target PWM: 30.00
Current PWM: 30.00
Current RPM: 4854.00
code
import os
import threading
from dotenv import load_dotenv
from windsuite_sdk import ModuleInfo, WindsuiteSDK
load_dotenv()
SERVER_IP_ADDRESS = os.getenv("SERVER_IP_ADDRESS", default="localhost")
def on_module_update(data: dict[tuple[int, int], ModuleInfo]) -> None:
"""
Callback for module updates.
Args:
data: Dictionary mapping module row and column to their information.
"""
for position, module_info in data.items():
print("-" * 20)
print(f"Module Position: {position}")
print(f" IP Address: {module_info.ip}")
print(f" Type: {module_info.type}")
print(f" Lifepoints: {module_info.lifepoints}")
# ! FOR EACH FAN LAYER
# The fans are indexed as a 3x3 matrix as they are physically arranged in the modules
# ┌─────┬─────┬─────┐
# │ 1 │ 2 │ 3 │
# ├─────┼─────┼─────┤
# │ 4 │ 5 │ 6 │
# ├─────┼─────┼─────┤
# │ 7 │ 8 │ 9 │
# └─────┴─────┴─────┘
esc_rpm: int | None = module_info.esc_rpm
applied_pwm_us: int | None = module_info.applied_pwm_us
esc_voltage_mv: int | None = module_info.esc_voltage_mv
esc_bus_current_ma: int | None = module_info.esc_bus_current_ma
esc_motor_current_ma: int | None = module_info.esc_motor_current_ma
esc_index: int | None = module_info.esc_index
esc_temp_mos_celcius: int | None = module_info.esc_temp_mos_celcius
esc_temp_capacitor_celcius: int | None = module_info.esc_temp_capacitor_celcius
esc_temp_mcu_celcius: int | None = module_info.esc_temp_mcu_celcius
esc_temp_motor_celcius: int | None = module_info.esc_temp_motor_celcius
esc_running_error: int | None = module_info.esc_running_error
esc_uptime: int | None = module_info.esc_uptime
applied_psu: int | None = module_info.applied_psu
psu_v_in: float | None = module_info.psu_v_in
psu_i_in: float | None = module_info.psu_i_in
psu_p_in: float | None = module_info.psu_p_in
psu_v_out: float | None = module_info.psu_v_out
psu_i_out: float | None = module_info.psu_i_out
psu_p_out: float | None = module_info.psu_p_out
psu_temp1: float | None = module_info.psu_temp1
psu_temp2: float | None = module_info.psu_temp2
psu_temp3: float | None = module_info.psu_temp3
print("ESC Data:")
print(f"\tRPM: {esc_rpm}")
print(f"\tApplied PWM (us): {applied_pwm_us}")
print(f"\tVoltage (mV): {esc_voltage_mv}")
print(f"\tBus Current (mA): {esc_bus_current_ma}")
print(f"\tMotor Current (mA): {esc_motor_current_ma}")
print(f"\tESC Index: {esc_index}")
print(f"\tTemp MOS (°C): {esc_temp_mos_celcius}")
print(f"\tTemp Capacitor (°C): {esc_temp_capacitor_celcius}")
print(f"\tTemp MCU (°C): {esc_temp_mcu_celcius}")
print(f"\tTemp Motor (°C): {esc_temp_motor_celcius}")
print(f"\tRunning Error: {esc_running_error}")
print(f"\tUptime (s): {esc_uptime}")
print("PSU Data:")
print(f"\tApplied PSU: {applied_psu}")
print(f"\tIN : {psu_v_in} V | {psu_i_in} A | {psu_p_in} W")
print(f"\tOUT: {psu_v_out} V | {psu_i_out} A | {psu_p_out} W")
print(f"\tTemps: {psu_temp1} °C | {psu_temp2} °C | {psu_temp3} °C")
for layer_index in range(len(module_info.target_pwm)):
layer_name = (
"DOWNSTREAM"
if layer_index == ModuleInfo.INDEX_DOWNSTREAM
else "UPSTREAM"
)
print(f"\tLayer {layer_name}:")
# ! FOR EACH FAN IN THE LAYER
for fan_index in range(len(module_info.target_pwm[layer_index])):
target_pwm = module_info.target_pwm[layer_index][fan_index]
current_pwm = module_info.current_pwm[layer_index][fan_index]
current_rpm = module_info.current_rpm[layer_index][fan_index]
print(f"\t\tFan {fan_index}:")
print(f"\t\t\tTarget PWM: {target_pwm:.2f}")
print(f"\t\t\tCurrent PWM: {current_pwm:.2f}")
print(f"\t\t\tCurrent RPM: {current_rpm:.2f}")
print("-" * 20)
stop_event = threading.Event()
def main() -> None:
"""Main function to run the WindSuite SDK example."""
base_url = f"http://{SERVER_IP_ADDRESS}"
print(f"Connecting to WindSuite server at {base_url}")
sdk = WindsuiteSDK(base_url=base_url)
sdk.register_module_update_callback(callback=on_module_update)
sdk.start_communication()
try:
freq_hz = 25
sdk.fan_controller.set_intensity(percent=30)
sdk.fan_controller.apply()
while not stop_event.wait(timeout=(1.0 / freq_hz)):
# ! DO WHATEVER
pass
except KeyboardInterrupt:
print("\nShutting down...")
stop_event.set()
finally:
sdk.fan_controller.set_intensity(0).apply()
sdk.cleanup()
print("SDK stopped")
if __name__ == "__main__":
main()
