WindShape

Modules

Python interface to Get informations from WindShaper 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.

Note the type hint indicating that the callback function is passed a dictionary mapping module indices (row, column) to their information. This lets you iterate over all modules and access their data in a structured way.
dict[tuple[int, int], ModuleInfo]
output
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
read_tracking.py
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 module indicies (row, column) are 1-indexed and correspond to the physical position of the module in the WindShaper when looking from DownStream (eyes facing the wind).Top-Left module is (1, 1)

The ModuleInfo object passed to the callback contains the following information about each module:

FieldTypeDescription
rowintRow position of the module in the WindShaper grid
colintColumn position of the module in the WindShaper grid
macstrMAC address of the module
ipstrIP address of the module
typestrModule type identifier (e.g., "0816", "0812", "2420")
lifepointsintHealth indicator of the module
target_pwmlist[list[float]]Target PWM values per layer and fan
current_pwmlist[list[float]]Current PWM values per layer and fan
current_rpmlist[list[float]]Current RPM values per layer and fan
target_psu_stateboolTarget power supply unit state
current_psu_stateboolCurrent power supply unit state
is_connectedboolWhether 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:

FieldTypeDescription
esc_rpmint | NoneESC-reported RPM value
applied_pwm_usint | NoneApplied PWM value in microseconds
esc_voltage_mvint | NoneESC voltage in millivolts
esc_bus_current_maint | NoneESC bus current in milliamperes
esc_motor_current_maint | NoneESC motor current in milliamperes
esc_indexint | NoneESC index identifier
esc_temp_mos_celciusint | NoneESC MOSFET temperature in Celsius
esc_temp_capacitor_celciusint | NoneESC capacitor temperature in Celsius
esc_temp_mcu_celciusint | NoneESC microcontroller temperature in Celsius
esc_temp_motor_celciusint | NoneESC motor temperature in Celsius
esc_running_errorint | NoneESC running error code
esc_uptimeint | NoneESC uptime in seconds
applied_psuint | NoneApplied power supply unit identifier
psu_v_infloat | NonePSU input voltage in volts
psu_i_infloat | NonePSU input current in amperes
psu_p_infloat | NonePSU input power in watts
psu_v_outfloat | NonePSU output voltage in volts
psu_i_outfloat | NonePSU output current in amperes
psu_p_outfloat | NonePSU output power in watts
psu_temp1float | NonePSU temperature sensor 1 in Celsius
psu_temp2float | NonePSU temperature sensor 2 in Celsius
psu_temp3float | NonePSU temperature sensor 3 in Celsius
These additional fields are optional and may not be available for all module types. When data is not available, the fields will contain None.
OUTPUT
--------------------
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

read_all_module_info.py
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()