Skip to content

nicesprinkler API Documentation

controller

Created on 13.08.2024

@author: wf

model

Created on 13.08.2024

@author: wf

sprinkler_cmd

Created on 13.08.2024

@author: wf

NiceSprinklerCmd

Bases: WebserverCmd

command line handling for nicesprinkler

Source code in sprinkler/sprinkler_cmd.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class NiceSprinklerCmd(WebserverCmd):
    """
    command line handling for nicesprinkler
    """

    def __init__(self):
        """
        constructor
        """
        config = NiceSprinklerWebServer.get_config()
        WebserverCmd.__init__(self, config, NiceSprinklerWebServer, DEBUG)

    def getArgParser(self, description: str, version_msg) -> ArgumentParser:
        """
        override the default argparser call
        """
        parser = super().getArgParser(description, version_msg)
        parser.add_argument(
            "-v",
            "--verbose",
            action="store_true",
            help="show verbose output [default: %(default)s]",
        )
        parser.add_argument(
            "--config",
            default="example_config.yaml",
            help="path to sprinkler configuration file [default: %(default)s]",
        )
        parser.add_argument(
            "--stl",
            default="example_garden.stl",
            help="path to sprinkler configuration file [default: %(default)s]",
        )
        return parser

__init__()

constructor

Source code in sprinkler/sprinkler_cmd.py
25
26
27
28
29
30
def __init__(self):
    """
    constructor
    """
    config = NiceSprinklerWebServer.get_config()
    WebserverCmd.__init__(self, config, NiceSprinklerWebServer, DEBUG)

getArgParser(description, version_msg)

override the default argparser call

Source code in sprinkler/sprinkler_cmd.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def getArgParser(self, description: str, version_msg) -> ArgumentParser:
    """
    override the default argparser call
    """
    parser = super().getArgParser(description, version_msg)
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="show verbose output [default: %(default)s]",
    )
    parser.add_argument(
        "--config",
        default="example_config.yaml",
        help="path to sprinkler configuration file [default: %(default)s]",
    )
    parser.add_argument(
        "--stl",
        default="example_garden.stl",
        help="path to sprinkler configuration file [default: %(default)s]",
    )
    return parser

main(argv=None)

main call

Source code in sprinkler/sprinkler_cmd.py
56
57
58
59
60
61
62
def main(argv: list = None):
    """
    main call
    """
    cmd = NiceSprinklerCmd()
    exit_code = cmd.cmd_main(argv)
    return exit_code

sprinkler_core

Created on 2024-08-13

@author: wf

Angles

Initial angles for the sprinkler

Source code in sprinkler/sprinkler_core.py
30
31
32
33
34
35
36
@lod_storable
class Angles:
    """
    Initial angles for the sprinkler
    """
    horizontal: float
    vertical: float

HosePerformance

Hose performance specifications

Source code in sprinkler/sprinkler_core.py
38
39
40
41
42
43
44
@lod_storable
class HosePerformance:
    """
    Hose performance specifications
    """
    max_distance: float
    optimal_angle: float

Lawn

Lawn dimensions in meters

Source code in sprinkler/sprinkler_core.py
13
14
15
16
17
18
19
@lod_storable
class Lawn:
    """
    Lawn dimensions in meters
    """
    width: float
    length: float

MotorConfig

Configuration for a single motor

Source code in sprinkler/sprinkler_core.py
46
47
48
49
50
51
52
53
54
55
56
@lod_storable
class MotorConfig:
    """
    Configuration for a single motor
    """
    ena_pin: int
    dir_pin: int
    pul_pin: int
    steps_per_revolution: int
    min_angle: int
    max_angle: int

SprinklerConfig

Complete configuration for the sprinkler system

Source code in sprinkler/sprinkler_core.py
58
59
60
61
62
63
64
65
66
67
@lod_storable
class SprinklerConfig:
    """
    Complete configuration for the sprinkler system
    """
    lawn: Lawn
    sprinkler_position: SprinklerPosition
    initial_angles: Angles
    hose_performance: HosePerformance
    motors: Dict[str, MotorConfig] = field(default_factory=dict)

SprinklerPosition

Sprinkler position in the lawn

Source code in sprinkler/sprinkler_core.py
21
22
23
24
25
26
27
28
@lod_storable
class SprinklerPosition:
    """
    Sprinkler position in the lawn
    """
    x: float
    y: float
    z: float

SprinklerSystem

Main sprinkler system class

Source code in sprinkler/sprinkler_core.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class SprinklerSystem:
    """
    Main sprinkler system class
    """

    def __init__(self, config_path: str, stl_file_path: str):
        self.config = SprinklerConfig.load_from_yaml_file(config_path)
        self.stl_mesh = mesh.Mesh.from_file(stl_file_path)
        self.stl_analysis = self.analyze_stl()


    def analyze_stl(self):
        """Analyze the STL file to determine key points for spray calculation"""
        stl_min = np.min(self.stl_mesh.vectors, axis=(0, 1))
        stl_max = np.max(self.stl_mesh.vectors, axis=(0, 1))
        stl_dimensions = stl_max - stl_min
        spray_origin = self.get_spray_origin()
        return {
            'min': stl_min,
            'max': stl_max,
            'dimensions': stl_dimensions,
            'spray_origin': spray_origin
        }

    def get_spray_origin(self):
        """Get the spray origin from the config"""
        return np.array([
            self.config.sprinkler_position.x,
            self.config.sprinkler_position.y,
            self.config.sprinkler_position.z
        ])

    def calculate_spray_points(self):
        spray_points = []
        for h_angle in range(-90, 91, 5):  # horizontal angles from -90 to 90 in 5-degree steps
            for v_angle in range(10, 61, 5):  # vertical angles from 10 to 60 in 5-degree steps
                distance = self.calculate_spray_distance(v_angle)
                x = self.stl_analysis['spray_origin'][0] + distance * np.cos(np.radians(h_angle))
                y = self.stl_analysis['spray_origin'][1] + distance * np.sin(np.radians(h_angle))
                spray_height = self.stl_analysis['spray_origin'][2] + distance * np.tan(np.radians(v_angle))

                if self.is_point_within_boundaries(x, y, spray_height):
                    spray_points.append((h_angle, v_angle, distance))

        return spray_points

    def is_point_within_boundaries(self, x: float, y: float, spray_height: float) -> bool:
        return (0 <= x <= self.config.lawn.width and
                0 <= y <= self.config.lawn.length)

    def calculate_spray_distance(self, angle: float) -> float:
        v0 = np.sqrt(self.config.hose_performance.max_distance * 9.8 /
                     np.sin(2 * np.radians(self.config.hose_performance.optimal_angle)))
        t = 2 * v0 * np.sin(np.radians(angle)) / 9.8
        return v0 * np.cos(np.radians(angle)) * t

analyze_stl()

Analyze the STL file to determine key points for spray calculation

Source code in sprinkler/sprinkler_core.py
81
82
83
84
85
86
87
88
89
90
91
92
def analyze_stl(self):
    """Analyze the STL file to determine key points for spray calculation"""
    stl_min = np.min(self.stl_mesh.vectors, axis=(0, 1))
    stl_max = np.max(self.stl_mesh.vectors, axis=(0, 1))
    stl_dimensions = stl_max - stl_min
    spray_origin = self.get_spray_origin()
    return {
        'min': stl_min,
        'max': stl_max,
        'dimensions': stl_dimensions,
        'spray_origin': spray_origin
    }

get_spray_origin()

Get the spray origin from the config

Source code in sprinkler/sprinkler_core.py
 94
 95
 96
 97
 98
 99
100
def get_spray_origin(self):
    """Get the spray origin from the config"""
    return np.array([
        self.config.sprinkler_position.x,
        self.config.sprinkler_position.y,
        self.config.sprinkler_position.z
    ])

sprinkler_sim

Created on 2024-08-13

@author: wf

SprinklerSimulation

A sprinkler simulation

Source code in sprinkler/sprinkler_sim.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class SprinklerSimulation:
    """
    A sprinkler simulation
    """
    def __init__(self, sprinkler_system: SprinklerSystem):
        self.sprinkler_system = sprinkler_system
        self.setup_scene()

    def setup_scene(self):
        self.scene = ui.scene(
            width=800,
            height=600,
            grid=True,
            background_color='#87CEEB'  # Sky blue
        ).classes('w-full h-64')

        # Add lawn
        lawn_width = self.sprinkler_system.config.lawn.width
        lawn_length = self.sprinkler_system.config.lawn.length
        with self.scene.group().move(y=-0.01):  # Slightly below zero to avoid z-fighting with the grid
            self.scene.box(lawn_width, 0.02, lawn_length).material('#7CFC00')  # Lawn green


        # Add sprinkler
        sprinkler_pos = self.sprinkler_system.config.sprinkler_position
        with self.scene.group().move(x=sprinkler_pos.x, y=sprinkler_pos.y, z=sprinkler_pos.z):
            self.scene.cylinder(0.1, 0.1, 0.5).material('#808080')  # Grey cylinder for sprinkler base
            self.sprinkler_head = self.scene.sphere(0.05).material('#4682B4').move(z=0.5)  # Steel blue sphere for sprinkler head

        self.scene.move_camera(x=lawn_width/2, y=-lawn_length/2, z=lawn_length/2, look_at_x=lawn_width/2, look_at_y=lawn_length/2, look_at_z=0)

    def simulate_sprinkler(self):
        spray_points = self.sprinkler_system.calculate_spray_points()

        for h_angle, v_angle, distance in spray_points:
            # Move sprinkler head
            self.sprinkler_head.move(z=0.5)  # Reset to original position
            self.sprinkler_head.rotate(h_angle, 0, v_angle)

            # Show water spray
            sprinkler_pos = self.sprinkler_system.config.sprinkler_position
            end_x = sprinkler_pos.x + distance * math.cos(math.radians(h_angle))
            end_y = sprinkler_pos.y + distance * math.sin(math.radians(h_angle))
            end_z = sprinkler_pos.z + distance * math.tan(math.radians(v_angle))

            self.scene.line([sprinkler_pos.x, sprinkler_pos.y, sprinkler_pos.z + 0.5],
                            [end_x, end_y, end_z]).material('#1E90FF', opacity=0.5)  # Light blue, semi-transparent

            ui.timer(0.1, lambda: None)  # Small delay to visualize movement

stepper

Created on 13.08.2024

@author: wf

version

Created on 2024-08-13

@author: wf

Version dataclass

Bases: object

Version handling for nicesprinkler

Source code in sprinkler/version.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@dataclass
class Version(object):
    """
    Version handling for nicesprinkler
    """

    name = "nicesprinkler"
    version = sprinkler.__version__
    date = "2024-08-13"
    updated = "2024-08-10"
    description = "Computer Controlled 2 Stepper motor 3D lawn sprinkler system"

    authors = "Wolfgang Fahl"

    doc_url = "https://wiki.bitplan.com/index.php/nicesprinkler"
    chat_url = "https://github.com/WolfgangFahl/nicesprinkler/discussions"
    cm_url = "https://github.com/WolfgangFahl/nicesprinkler"

    license = f"""Copyright 2024 contributors. All rights reserved.

  Licensed under the Apache License 2.0
  http://www.apache.org/licenses/LICENSE-2.0

  Distributed on an "AS IS" basis without warranties
  or conditions of any kind, either express or implied."""
    longDescription = f"""{name} version {version}
{description}

  Created by {authors} on {date} last updated {updated}"""

webserver

Created on 13.08.2024

@author: wf

NiceSprinklerSolution

Bases: InputWebSolution

the NiceSprinkler solution

Source code in sprinkler/webserver.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class NiceSprinklerSolution(InputWebSolution):
    """
    the NiceSprinkler solution
    """

    def __init__(self, webserver: NiceSprinklerWebServer, client: Client):
        """
        Initialize the solution

        Args:
            webserver (NiceSprinklerWebServer): The webserver instance associated with this context.
            client (Client): The client instance this context is associated with.
        """
        super().__init__(webserver, client)
        self.simulation = None

    async def home(self):
        """Generates the home page with a 3D viewer and controls for the sprinkler."""
        self.setup_menu()
        with ui.column():
            self.simulation = SprinklerSimulation(self.webserver.sprinkler_system)
            self.simulation.setup_scene()
            with ui.row():
                ui.button('Start Simulation', on_click=self.simulation.simulate_sprinkler)
                ui.button('Reset', on_click=self.reset_simulation)

        await self.setup_footer()

    async def reset_simulation(self):
        """Resets the simulation to its initial state."""
        self.simulation.scene.clear()
        self.simulation.setup_scene()
        ui.notify('Simulation reset')

    def configure_settings(self):
        """Generates the settings page with options to modify sprinkler configuration."""
        config_str = self.webserver.sprinkler_system.config.to_yaml()
        ui.textarea("Configuration", value=config_str).classes("w-full").on('change', self.update_config)

    def update_config(self, e):
        """Updates the simulation configuration based on user input."""
        try:
            new_config = SprinklerConfig.from_yaml(e.value)
            self.webserver.sprinkler_system.config = new_config
            self.simulation.sprinkler_system = self.webserver.sprinkler_system
            self.reset_simulation()
            ui.notify('Configuration updated successfully')
        except Exception as ex:
            ui.notify(f'Error updating configuration: {str(ex)}', color='red')

__init__(webserver, client)

Initialize the solution

Parameters:

Name Type Description Default
webserver NiceSprinklerWebServer

The webserver instance associated with this context.

required
client Client

The client instance this context is associated with.

required
Source code in sprinkler/webserver.py
71
72
73
74
75
76
77
78
79
80
def __init__(self, webserver: NiceSprinklerWebServer, client: Client):
    """
    Initialize the solution

    Args:
        webserver (NiceSprinklerWebServer): The webserver instance associated with this context.
        client (Client): The client instance this context is associated with.
    """
    super().__init__(webserver, client)
    self.simulation = None

configure_settings()

Generates the settings page with options to modify sprinkler configuration.

Source code in sprinkler/webserver.py
100
101
102
103
def configure_settings(self):
    """Generates the settings page with options to modify sprinkler configuration."""
    config_str = self.webserver.sprinkler_system.config.to_yaml()
    ui.textarea("Configuration", value=config_str).classes("w-full").on('change', self.update_config)

home() async

Generates the home page with a 3D viewer and controls for the sprinkler.

Source code in sprinkler/webserver.py
82
83
84
85
86
87
88
89
90
91
92
async def home(self):
    """Generates the home page with a 3D viewer and controls for the sprinkler."""
    self.setup_menu()
    with ui.column():
        self.simulation = SprinklerSimulation(self.webserver.sprinkler_system)
        self.simulation.setup_scene()
        with ui.row():
            ui.button('Start Simulation', on_click=self.simulation.simulate_sprinkler)
            ui.button('Reset', on_click=self.reset_simulation)

    await self.setup_footer()

reset_simulation() async

Resets the simulation to its initial state.

Source code in sprinkler/webserver.py
94
95
96
97
98
async def reset_simulation(self):
    """Resets the simulation to its initial state."""
    self.simulation.scene.clear()
    self.simulation.setup_scene()
    ui.notify('Simulation reset')

update_config(e)

Updates the simulation configuration based on user input.

Source code in sprinkler/webserver.py
105
106
107
108
109
110
111
112
113
114
def update_config(self, e):
    """Updates the simulation configuration based on user input."""
    try:
        new_config = SprinklerConfig.from_yaml(e.value)
        self.webserver.sprinkler_system.config = new_config
        self.simulation.sprinkler_system = self.webserver.sprinkler_system
        self.reset_simulation()
        ui.notify('Configuration updated successfully')
    except Exception as ex:
        ui.notify(f'Error updating configuration: {str(ex)}', color='red')

NiceSprinklerWebServer

Bases: InputWebserver

WebServer class that manages the server and handles Sprinkler operations.

Source code in sprinkler/webserver.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class NiceSprinklerWebServer(InputWebserver):
    """WebServer class that manages the server and handles Sprinkler operations."""

    @classmethod
    def get_config(cls) -> WebserverConfig:
        copy_right = "(c)2024 Wolfgang Fahl"
        config = WebserverConfig(
            copy_right=copy_right,
            version=Version(),
            default_port=9859,
            short_name="nicesprinkler",
        )
        server_config = WebserverConfig.get(config)
        server_config.solution_class = NiceSprinklerSolution
        return server_config

    def __init__(self):
        """Constructs all the necessary attributes for the WebServer object."""
        InputWebserver.__init__(self, config=NiceSprinklerWebServer.get_config())
        self.sprinkler_system = None

    def configure_run(self):
        """
        Configure the run based on command line arguments
        """
        examples_path = self.examples_path()
        if hasattr(self.args, "root_path"):
            self.root_path = self.args.root_path
        else:
            self.root_path = examples_path
        self.config_path = (
            self.args.config
            if os.path.isabs(self.args.config)
            else os.path.join(self.root_path, self.args.config)
        )
        self.stl_path = (
            self.args.stl
            if os.path.isabs(self.args.stl)
            else os.path.join(self.root_path, self.args.stl)
        )

        # Create SprinklerSystem
        self.sprinkler_system = SprinklerSystem(self.config_path, self.stl_path)

    @classmethod
    def examples_path(cls) -> str:
        path = os.path.join(os.path.dirname(__file__), "../nicesprinkler_examples")
        path = os.path.abspath(path)
        return path

__init__()

Constructs all the necessary attributes for the WebServer object.

Source code in sprinkler/webserver.py
32
33
34
35
def __init__(self):
    """Constructs all the necessary attributes for the WebServer object."""
    InputWebserver.__init__(self, config=NiceSprinklerWebServer.get_config())
    self.sprinkler_system = None

configure_run()

Configure the run based on command line arguments

Source code in sprinkler/webserver.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def configure_run(self):
    """
    Configure the run based on command line arguments
    """
    examples_path = self.examples_path()
    if hasattr(self.args, "root_path"):
        self.root_path = self.args.root_path
    else:
        self.root_path = examples_path
    self.config_path = (
        self.args.config
        if os.path.isabs(self.args.config)
        else os.path.join(self.root_path, self.args.config)
    )
    self.stl_path = (
        self.args.stl
        if os.path.isabs(self.args.stl)
        else os.path.join(self.root_path, self.args.stl)
    )

    # Create SprinklerSystem
    self.sprinkler_system = SprinklerSystem(self.config_path, self.stl_path)