Soft pneumatic actuators using temporary hydrogel molds

Internship project: Elyas - Bio-Inspired Systems team, Institut des Sciences du Mouvement. Fabrication of soft pneumatic actuators inspired by plant motion using printed temporary hydrogel molds.

Soft pneumatic actuator fabricated with a temporary hydrogel mold.
Internship project: Elyas
Supervisor: Loïc TADRIST
Team: Bio-Inspired Systems (SBI)
Laboratory: Institut des Sciences du Mouvement (ISM)
Topics: soft pneumatic actuators, hydrogel, silicone, additive manufacturing and bio-inspired robotics

How can soft pneumatic actuators be fabricated using a temporary hydrogel mold?

The project focuses on the fabrication of silicone actuators with internal pneumatic cavities. The temporary mold is printed in hydrogel, embedded in silicone and then removed to reveal the internal network.

1. Scientific context

The project is part of the Bio-Inspired Systems team work on soft pneumatic actuators. The objective is to use temporary hydrogel molds to create internal cavities in silicone, inspired by plant motion such as Mimosa pudica.

2. Fabrication principle

The method uses a temporary hydrogel structure printed inside a mold. Silicone is poured around the hydrogel, cured, and then the hydrogel is removed to leave pneumatic channels and chambers inside the elastomer.

Silicone preparation

Silicone preparation before casting.

Preparation of the silicone components before mixing and casting around the temporary hydrogel mold.

Vacuum degassing

Vacuum degassing of silicone.

Degassing limits air bubbles in the silicone before or during casting.

3. First actuator prototypes

The first prototype used a 32-cell architecture. Several trials were required to improve the number of functional cells and the quality of the pneumatic network.

Molding box for the prototype

Molding box for the 32-cell hydrogel actuator.

Molding box used to structure the 32-cell prototype before casting silicone.

Internal network and cavities

Hydrogel network forming internal pneumatic cavities.

The printed hydrogel geometry is designed to create pneumatic chambers and channels inside the silicone part.

Silicone actuator after casting

Silicone actuator after casting.

Example of a silicone part obtained after molding around the hydrogel structure.

4. New images from the hydrogel tests

Additional media show the actuator during manual pneumatic testing, the printed cell grid and the molded part held in its frame.

Hydrogel actuator with syringe test

Hydrogel actuator connected to a syringe.

Manual syringe test used to inflate or check the soft actuator and its pneumatic connection.

Printed hydrogel cell grid

Printed hydrogel grid with repeated cells.

Hydrogel grid showing the cell pattern before or after integration in the silicone process.

Hydrogel mold in frame

Hydrogel mold or soft actuator held in a frame.

Prototype held in a dark frame, showing the repeated internal cell pattern.

Hydrogel test video 1

Video showing a hydrogel or soft-actuator test during manipulation.

Hydrogel test video 2

Second video showing the behavior of the hydrogel-based prototype during testing.

5. Improvement of the process

The molding box was redesigned to make fabrication more reliable. The work also included a fabrication notice and a lab notebook to make the process easier to reproduce.

Printing the temporary hydrogel mold

Printer fabricating the temporary hydrogel mold.

Hydrogel printing prepares the temporary geometry that will form the internal pneumatic cavities.

6. Nozzle-speed tests

Nozzle-travel speed tests were performed to reduce the printed filament diameter and improve the quality of the hydrogel geometry.

G-code trajectory structure

G-code trajectory diagram for the hydrogel structure.

Diagram associated with the G-code development, used to organize cells, channels and printing paths.

7. G-code generation

A Python notebook was used to generate the G-code trajectories. This makes it possible to control the number of cells, the geometry of the network and the printing sequence.

Python code: G-code generation

Current Python code extracted from the Jupyter notebook used to generate G-code trajectories for hydrogel printing.

creation_gcode_v7.py python
# %% Cellule 0import mathimport numpy as npimport os def generer_gcode_frc_cube(nom_fichier, a,b,coordonnees_start,extrusion_continu, epaisseur_fil, hauteur_couche,simulate=False):      """    Génère un G-code pour parcourir le volume d'un cube en spirale alternée avec extrusion constante.        :param nom_fichier: Nom du fichier de sortie (str).    :param a:  largeur (float) en mm.    :param b: hauteur (float) en mm.    :param coordonnees_start: Coordonées du point de départ du cube (list).    :param epaisseur_fil: Épaisseur du fil extrudé (float) en mm.    :param hauteur_couche: Hauteur de chaque couche (float) en mm.            """    #=== paramètre est-ce qu'on extrude le cube en premier ou si il vient après autre chose=====              # nom_fichier.write(f"G0 Z{coordonnees_start[2]-40:.2f} ;\n")            # nom_fichier.write(f"G0 X{coordonnees_start[0]:.2f} Y{coordonnees_start[1]:.2f} Z{coordonnees_start[2]:.2f} ;\n")       demi_taille = a / 2    nb_couches = round(b / hauteur_couche)    spirales_par_couche = round(demi_taille / epaisseur_fil)    extrusion = extrusion_continu # Position initiale de l'extrudeur         for couche in range(nb_couches):                z=coordonnees_start[2]-couche*hauteur_couche        if not simulate:            nom_fichier.write("\n")            nom_fichier.write(f"G0 Z{z:.2f} F800;\n")            # print(f"Z{z:.2f}")                # Déterminer si la spirale est concentrique ou excentrique        concentrique = (couche % 2 == 0)                for spirale in range(spirales_par_couche):            r1 = spirale * epaisseur_fil            r2 = (spirale + 1) * epaisseur_fil            if not concentrique:                r1, r2 = r2, r1  # Alterner concentrique/excentrique                        # Points de la spirale            points = [                (-r1, -r1),                (r2, -r1),                (r2, r2),                (-r1, r2),                (-r1, -r1)  # Retour au point de départ            ]                        # Générer les commandes G1 pour chaque segment            for i in range(len(points) - 1):                x_cube_st, y1 = points[i]                x2, y2 = points[i + 1]                                # Calcul de la distance et de la vitesse réelle de la buse                distance = math.sqrt((x2 - x_cube_st) ** 2 + (y2 - y1) ** 2)                #vitesse_buse = vitesse_extrusion / facteur_extrusion                #temps_deplacement = distance / vitesse_buse * 60  # Temps en secondes                #vitesse_deplacement = distance / temps_deplacement * 60  # Vitesse en mm/min                                extrusion += (distance*0.45**2)/(10**2)*1.75                if not simulate:                    nom_fichier.write(                        f"G1 X{x2 + demi_taille + coordonnees_start[0]:.2f} "                        f"Y{y2 + demi_taille + coordonnees_start[1]:.2f} "                        f"E{extrusion:.3f}\n"                    )                    # print(f" X{x2 + demi_taille + coordonnees_start[0]:.2f} "f"Y{y2 + demi_taille + coordonnees_start[1]:.2f} "f"E{extrusion:.3f}\n")                                         #print(f"G-code généré et sauvegardé dans {nom_fichier}")    # retourner la derniere position de la buse    return x2 + demi_taille + coordonnees_start[0],y2 + demi_taille + coordonnees_start[1],z - coordonnees_start[2], extrusion def maximize_min_distance(points, num_points):    def distance(p1, p2):        return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)     def can_place_points(min_distance):        count = 1        last_point = points[0]         for i in range(1, len(points)):            if distance(points[i], last_point) >= min_distance:                count += 1                last_point = points[i]                if count == num_points:                    return True        return False     # Trier les points par coordonnée x (ou y) pour faciliter le calcul    points.sort()     # Initialiser les bornes pour la recherche binaire    low, high = 0, distance(points[0], points[-1])    best_distance = 0    best_positions = []     while low <= high:        mid = (low + high) / 2        if can_place_points(mid):            best_distance = mid            low = mid + 1        else:            high = mid - 1     # Reconstruire la liste des positions choisies avec la meilleure distance    last_point = points[0]    best_positions.append(last_point)    count = 1     for i in range(1, len(points)):        if distance(points[i], last_point) >= best_distance:            best_positions.append(points[i])            last_point = points[i]            count += 1            if count == num_points:                break     return best_positionsdef calcul_extrud_by_points(point_dep,point_arriv):        x1, y1 = point_dep[0],point_dep[1]    x2, y2 = point_arriv[0],point_arriv[1]     # Calcul de la distance et de la vitesse réelle de la buse    distance = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)    extrusion = (distance*0.45**2)/(10**2)    return extrusiondef calcul_extrud_by_dist(dist):    extrusion = (dist*0.45**2)/(10**2)    return extrusiondef format_nombre(n):    if int(n) == n:        return str(int(n))  # pas de .0    else:        return ('%g' % n)  # notation courte def tracer_ligne(fichier, x1, y1, z, x2, y2, extrusion_continu):    dist = ((x2 - x1)**2 + (y2 - y1)**2)**0.5     # Nombre de segments de 30 mm + reste    nb_segments = int(dist // 10)    reste = dist % 10     # Vecteur direction normalisé    dx = (x2 - x1) / dist    dy = (y2 - y1) / dist     # Fonction interne pour tracer un segment    def tracer_segment(x_depart, y_depart, longueur, extrusion_continu):        extrusion_continu += calcul_extrud_by_dist(longueur) * 1.5        x_arrivee = x_depart + dx * longueur        y_arrivee = y_depart + dy * longueur        fichier.write(            f"G1 X{format_nombre(x_arrivee)} "            f"Y{format_nombre(y_arrivee)} "            f"Z{format_nombre(z)} "            f"E{extrusion_continu:.3f};\n"        )        return x_arrivee, y_arrivee, extrusion_continu     # Position courante    x_courant, y_courant = x1, y1     # Tracer les segments complets de 10 mm    for _ in range(nb_segments):        x_courant, y_courant, extrusion_continu = tracer_segment(x_courant, y_courant, 10, extrusion_continu)     # Tracer le segment final (reste)    if reste > 0:        x_courant, y_courant, extrusion_continu = tracer_segment(x_courant, y_courant, reste, extrusion_continu)     return extrusion_continu   import plotly.graph_objects as go def parse_gcode_line(line):    """Analyse une ligne G-code et retourne le type (G0/G1) et les coordonnées X, Y, Z."""    cmd = None    x = y = z = None    parts = line.strip().split()    for part in parts:        if part.startswith('G'):            cmd = part        elif part.startswith('X'):            x = float(part[1:])        elif part.startswith('Y'):            y = float(part[1:])        elif part.startswith('Z'):            z = float(part[1:])    return cmd, x, y, z def charger_trajectoires(fichier):    with open(fichier, 'r') as f:        lignes = f.readlines()     position_actuelle = [0.0, 0.0, 0.0]    lignes_G0 = []  # Vert    lignes_G1 = []  # Bleu     for ligne in lignes:        cmd, x, y, z = parse_gcode_line(ligne)        x = position_actuelle[0] if x is None else x        y = position_actuelle[1] if y is None else y        z = position_actuelle[2] if z is None else z         nouvelle_position = [x, y, z]         if cmd == 'G0':            lignes_G0.append((position_actuelle, nouvelle_position))        elif cmd == 'G1':            lignes_G1.append((position_actuelle, nouvelle_position))         position_actuelle = nouvelle_position     return lignes_G0, lignes_G1 def tracer_plotly_3d(fichier):    lignes_G0, lignes_G1 = charger_trajectoires(fichier)    traces = []     def add_traces(lignes, couleur, nom):        for start, end in lignes:            trace = go.Scatter3d(                x=[start[0], end[0]],                y=[start[1], end[1]],                z=[start[2], end[2]],                mode='lines',                line=dict(color=couleur, width=4),                name=nom,                showlegend=False            )            traces.append(trace)     add_traces(lignes_G0, 'green', 'G0')    add_traces(lignes_G1, 'blue', 'G1')     fig = go.Figure(data=traces)         # Point de départ (rouge)    first_point = lignes_G1[0][0] if lignes_G1 else lignes_G0[0][0]    fig.add_trace(go.Scatter3d(        x=[first_point[0]],        y=[first_point[1]],        z=[first_point[2]],        mode='markers',        marker=dict(size=6, color='red'),        name='Départ'    ))     # Point d’arrivée (noir)    last_point = lignes_G1[-1][1] if lignes_G1 else lignes_G0[-1][1]    fig.add_trace(go.Scatter3d(        x=[last_point[0]],        y=[last_point[1]],        z=[last_point[2]],        mode='markers',        marker=dict(size=6, color='black'),        name='Arrivée'    ))      fig.update_layout(        scene=dict(            xaxis_title='X',            yaxis_title='Y',            zaxis_title='Z',        ),        margin=dict(l=0, r=0, b=0, t=30)    )    # Déterminer les plages min/max pour chaque axe    all_x = [pt for ligne in lignes_G0 + lignes_G1 for pt in [ligne[0][0], ligne[1][0]]]    all_y = [pt for ligne in lignes_G0 + lignes_G1 for pt in [ligne[0][1], ligne[1][1]]]    all_z = [pt for ligne in lignes_G0 + lignes_G1 for pt in [ligne[0][2], ligne[1][2]]]     min_x, max_x = min(all_x), max(all_x)    min_y, max_y = min(all_y), max(all_y)    min_z, max_z = min(all_z), max(all_z)     # Calcul du centre et de la plage maximale    center_x = (min_x + max_x) / 2    center_y = (min_y + max_y) / 2    center_z = (min_z + max_z) / 2    max_range = max(max_x - min_x, max_y - min_y, max_z - min_z) / 2     fig.update_layout(        scene=dict(            xaxis=dict(nticks=10, range=[center_x - max_range, center_x + max_range]),            yaxis=dict(nticks=10, range=[center_y - max_range, center_y + max_range]),            zaxis=dict(nticks=10, range=[center_z + max_range,center_z - max_range]),            aspectmode='cube',            xaxis_title='X',            yaxis_title='Y',            zaxis_title='Z',        ),     )     fig.show() def y_by_indice(indice, t, a):    """    Y varie avec l indice au sein d une colonne.    Deux colonnes (8 indices par colonne)    """    colonne = 0 if indice <= 8 else 1    return t + ((indice - 1) % 8) * (t + a)  def x_by_indice(indice, t, a):    """    X change uniquement lorsqu on passe à la colonne suivante.    """    colonne = 0 if indice <= 8 else 1    return t + colonne * (t + a) # %% Cellule 1def generer_gcode_spirale_continue(nom_fichier, a, b, coordonnees_start, extrusion_continu, epaisseur_fil, hauteur_couche, simulate=False):    """    Génère un G-code pour un cube parcouru en spirale carrée continue (sans alternance).    """    demi_taille = a / 2    nb_couches = round(b / hauteur_couche)    spirales_par_couche = round(demi_taille / epaisseur_fil)    extrusion = extrusion_continu     for couche in range(nb_couches):        z = coordonnees_start[2] - couche * hauteur_couche        if not simulate:            nom_fichier.write("\n")            nom_fichier.write(f"G0 Z{z:.2f} F800;\n")         for spirale in range(spirales_par_couche):            r = (spirale + 1) * epaisseur_fil            points = [                (-r, -r),                (r, -r),                (r, r),                (-r, r),                (-r, -r)            ]             for i in range(len(points) - 1):                x1, y1 = points[i]                x2, y2 = points[i + 1]                distance = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)                extrusion += (distance * 0.45**2) / (10**2) * 1.75                 if not simulate:                    nom_fichier.write(                        f"G1 X{x2 + demi_taille + coordonnees_start[0]:.2f} "                        f"Y{y2 + demi_taille + coordonnees_start[1]:.2f} "                        f"E{extrusion:.3f}\n"                    )     return x2 + demi_taille + coordonnees_start[0], y2 + demi_taille + coordonnees_start[1], z - coordonnees_start[2], extrusion # %% Cellule 2def count_until_e(line: str) -> int:    """    Compte la longueur de la ligne jusqu'au premier 'E' inclus.    Si pas de 'E', retourne la longueur totale.    """    idx = line.find("E")    if idx != -1:        return idx + 1    return len(line)  def check_gcode_file(filepath: str, limit: int = 30):    """    Ouvre le fichier GCODE et affiche les lignes    qui dépassent la limite de caractères avant 'E'.    """    too_long_lines = []     with open(filepath, "r", encoding="utf-8") as f:        for lineno, line in enumerate(f, start=1):            line = line.rstrip("\n")  # enlever le retour chariot            length = count_until_e(line)            if length > limit:                too_long_lines.append((lineno, length, line))     if too_long_lines:        print("⚠️ Lignes trop longues trouvées :")        for lineno, length, content in too_long_lines:            print(f" - Ligne {lineno} ({length} caractères avant E) : {content}")    else:        print("✔️ Aucune ligne trop longue") # %% Cellule 3import math def calcul_tps_impression(fichier_gcode: str) -> dict:    """    Calcule la distance parcourue par la buse pour chaque étape,    séparée par les lignes contenant 'SECHAGE'.    """    position_actuelle = {"X": 0.0, "Y": 0.0, "Z": 0.0}    distances = {}    etape_index = 1    distances[f"etape_{etape_index}"] = 0.0     with open(fichier_gcode, "r", encoding="utf-8") as f:        for ligne in f:            ligne = ligne.strip()             # Ignore les commentaires classiques            if not ligne or ligne.startswith(";"):                continue             # Si on rencontre une nouvelle étape (mot clé SECHAGE)            if "SECHAGE" in ligne.upper():                etape_index += 1                distances[f"etape_{etape_index}"] = 0.0                continue             # Vérifie si c'est un G0 ou G1            if ligne.startswith(("G0", "G1")):                nouvelle_position = position_actuelle.copy()                 # Recherche coordonnées X, Y, Z                for axe in ["X", "Y", "Z"]:                    if axe in ligne:                        try:                            valeur = float(ligne.split(axe)[1].split()[0])                            nouvelle_position[axe] = valeur                        except ValueError:                            pass                 # Si c'est un G1 → calcul distance                if ligne.startswith("G1"):                    dx = nouvelle_position["X"] - position_actuelle["X"]                    dy = nouvelle_position["Y"] - position_actuelle["Y"]                    dz = nouvelle_position["Z"] - position_actuelle["Z"]                    distance = math.sqrt(dx*dx + dy*dy + dz*dz)                    distances[f"etape_{etape_index}"] += distance                 # Met à jour la position                position_actuelle = nouvelle_position     return distances # %% Cellule 4def definir_sequence_couche(couche):    """    Retourne la séquence d’impression (ordre et indices) pour chaque couche.    """    if couche == 1:        return [            {"nom": "fractions", "indices": [1,2,3,4,5,6,9,10,11,12,13,14], "type": "fraction"},            {"nom": "cases_pleines", "indices": [7,8,15,16], "type": "pleine"},#[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]            {"nom": "canaux", "indices": [7,8,15,16], "type": "canal"}#[1,2,3,9,10,11]        ]     elif couche == 2:        return [            {"nom": "fractions", "indices": [1,2,3,4,9,10,11,12], "type": "fraction"},            {"nom": "cases_pleines", "indices": [5,6,13,14], "type": "pleine"},            {"nom": "canaux", "indices": [5,6,13,14], "type": "canal"}        ]     elif couche == 3:        return [            {"nom": "fractions", "indices": [1,2,9,10], "type": "fraction"},            {"nom": "cases_pleines", "indices": [3,4,11,12], "type": "pleine"},            {"nom": "canaux", "indices": [3,4,11,12], "type": "canal"}        ]     elif couche == 4:        return [            {"nom": "cases_pleines", "indices": [1,2,9,10], "type": "pleine"},            {"nom": "canaux", "indices": [1,2,9,10], "type": "canal"}        ]     else:        return [] # %% Cellule 5def generer_canal(fichier, indice_case,x_origine,y_origine,z, position, direction, t, a, extrusion_continu):    """    position: 'premier', 'deuxieme', 'centre'    direction: 'droite' ou 'gauche'    """     zone_dechet=15    # --- Sélection de la position en X ---    if position == "premier":        x_canal =round(0.32*t,2)    elif position == "deuxieme":        x_canal = round(0.65*t,2)    elif position == "centre":        x_canal = a/2    else:        raise ValueError(f"Position canal inconnue : {position}")     # --- Longueur du canal selon la direction ---    if direction == "droite":        x_canal = x_canal    elif direction == "gauche":        if position == "premier":            x_canal = -round(0.65*t,2)   # valeur de 'deuxieme'        elif position == "deuxieme":            x_canal = -round(0.32*t,2)  # valeur de 'premier'    else:        raise ValueError(f"Direction canal inconnue : {direction}")     fichier.write(f"G0 Z{z-50}\n")     # --- Tracé du canal ---    if position == "centre":        fichier.write(f"G0 X{x_origine+x_by_indice(indice_case,t,a)+x_canal} Y{y_origine-zone_dechet} F1000;\n")        fichier.write(f"G0 Z{z} F1000;\n")         extrusion_continu+=0.2        fichier.write(f"E{extrusion_continu:.3f} F1;\n")        fichier.write("STOP 15000;\n")         extrusion_continu = tracer_ligne(            fichier,            x_origine+x_by_indice(indice_case,t,a)+x_canal,            y_origine-zone_dechet,            z,            x_origine+x_by_indice(indice_case,t,a)+x_canal,            y_origine+y_by_indice(indice_case,t,a),            extrusion_continu        )        else:        fichier.write(f"G0 X{x_origine+x_by_indice(indice_case,t,a)-t+x_canal} Y{y_origine-zone_dechet} F1000;\n")        fichier.write(f"G0 Z{z} F1000;\n")         extrusion_continu+=0.2        fichier.write(f"E{extrusion_continu:.3f} F1;\n")        fichier.write("STOP 15000;\n")         extrusion_continu = tracer_ligne(            fichier,            x_origine+x_by_indice(indice_case,t,a)-t+x_canal,            y_origine-zone_dechet,            z,            x_origine+x_by_indice(indice_case,t,a)-t+x_canal,            y_origine+y_by_indice(indice_case,t,a)+a/2,            extrusion_continu        )             extrusion_continu = tracer_ligne(            fichier,            x_origine+x_by_indice(indice_case,t,a)-x_canal,            y_origine+y_by_indice(indice_case,t,a)+a/2,            z,            x_origine+x_by_indice(indice_case,t,a)-x_canal+x_canal,            y_origine+y_by_indice(indice_case,t,a)+a/2,            extrusion_continu        )     return extrusion_continu # %% Cellule 6CONFIG_CANAUX = {    1: {"position": "centre",  "direction": "droite"},    2: {"position": "deuxieme",  "direction": "droite"},    3: {"position": "premier",  "direction": "droite"},    4: {"position": "deuxieme", "direction": "droite"},    5: {"position": "premier", "direction": "droite"},    6: {"position": "deuxieme",  "direction": "droite"},    7: {"position": "premier",  "direction": "droite"},    8: {"position": "premier",   "direction": "droite"},    9: {"position": "centre",  "direction": "droite"},    10: {"position": "deuxieme", "direction": "droite"},    11: {"position": "premier", "direction": "droite"},    12: {"position": "deuxieme","direction": "droite"},    13: {"position": "premier","direction": "droite"},    14: {"position": "deuxieme", "direction": "droite"},    15: {"position": "premier", "direction": "droite"},    16: {"position": "premier",  "direction": "droite"}}CONFIG_CANAUXV2 = {    1: {"position": "centre",  "direction": "droite"},    2: {"position": "premier",  "direction": "droite"},    3: {"position": "deuxieme",  "direction": "droite"},    4: {"position": "premier", "direction": "droite"},    5: {"position": "deuxieme", "direction": "droite"},    6: {"position": "premier",  "direction": "droite"},    7: {"position": "deuxieme",  "direction": "droite"},    8: {"position": "premier",   "direction": "droite"},    9: {"position": "centre",  "direction": "droite"},    10: {"position": "premier", "direction": "droite"},    11: {"position": "deuxieme", "direction": "droite"},    12: {"position": "premier","direction": "droite"},    13: {"position": "deuxieme","direction": "droite"},    14: {"position": "premier", "direction": "droite"},    15: {"position": "deuxieme", "direction": "droite"},    16: {"position": "premier",  "direction": "droite"}} # %% Cellule 7nom_fichier="8x2x2_trunkv5elyas2"extrusion_continu=0 x_origine=108-0.3y_origine=69z_origine=212 Nx=8Ny=2 a=15t=7.5surface_case=ahauteur_case=a x_premiercanal=round(0.32*t,2)x_deuxiemecanal=round(0.65*t,2) x_milieu_case=t+a/2 indice_case=0nombre_couche_tot=4 _,_,_,calcul_extrd=generer_gcode_spirale_continue(nom_fichier,surface_case,hauteur_case,[x_origine,y_origine,z_origine],extrusion_continu, 0.9, 1,True)recharge_serg=0                       for couche in range(1,nombre_couche_tot+1):    currentfichier = nom_fichier+f"_couche_{couche}.txt"    with open(currentfichier, "w") as fichier:         fichier.write(f"G0 Z{z_origine-100} F800;\n")        fichier.write(f"G0 X{x_origine+t+a/2} Y{y_origine-15} F500;\n")        fichier.write(f"G0 Z{z_origine-a*((couche-1)/nombre_couche_tot)} F800;\n")         extrusion_continu+=0.4        fichier.write(f"E{extrusion_continu:.3f} F1;\n")        fichier.write("STOP 30000;\n")          SEQUENCE_IMPRESSION = definir_sequence_couche(couche)        for etape in SEQUENCE_IMPRESSION:            for indice_case in etape["indices"]:                 if etape["type"] == "fraction":                    fichier.write(f"G0 Z{z_origine-50} F800;\n")                    fichier.write(f"G0 X{x_origine+x_by_indice(indice_case,t,a)+a/2} Y{y_origine+y_by_indice(indice_case,t,a)+a/2} F500;\n")                    x_end,y_end,z_end,extrusion_continu=generer_gcode_spirale_continue(fichier,surface_case,hauteur_case*1/nombre_couche_tot,[x_origine+x_by_indice(indice_case,t,a),y_origine+y_by_indice(indice_case,t,a),z_origine-a*((couche-1)/nombre_couche_tot)],extrusion_continu, 0.9, 1)                    # fichier.write(f"G0 X{x_end+2} Y{y_end+2} ;\n")                    fichier.write(f"G0 Z{z_origine-50} F800;\n")                    if (extrusion_continu>=75-calcul_extrd):                        fichier.write(f"G0 Z{50} F800\n")                        fichier.write(f"G0 X{15} Y{15} F500;\n")                        fichier.write("RECHARGE\n")                        recharge_serg+=1                        extrusion_continu=0                        fichier.write(f"G0 Z{z_origine-50} F800;\n")                        fichier.write(f"G0 X{x_origine+t+a/2} Y{y_origine-15} F500;\n")                        fichier.write(f"G0 Z{z_origine-a*((couche-1)/nombre_couche_tot)} F800;\n")                         extrusion_continu+=0.4                        fichier.write(f"E{extrusion_continu:.3f} F1;\n")                        fichier.write("STOP 30000;\n")                                elif etape["type"] == "pleine":                    fichier.write(f"G0 Z{z_origine-50} F800;\n")                    fichier.write(f"G0 X{x_origine+x_by_indice(indice_case,t,a)+a/2} Y{y_origine+y_by_indice(indice_case,t,a)+a/2} F500;\n")                    x_end,y_end,z_end,extrusion_continu=generer_gcode_spirale_continue(fichier,surface_case,hauteur_case*((5-couche)/nombre_couche_tot),[x_origine+x_by_indice(indice_case,t,a),y_origine+y_by_indice(indice_case,t,a),z_origine-a*((couche-1)/nombre_couche_tot)],extrusion_continu, 0.9, 1)                    fichier.write(f"G0 Z{z_origine-50} F800;\n")                    if (extrusion_continu>=75-calcul_extrd):                        fichier.write(f"G0 Z{50} F800\n")                        fichier.write(f"G0 X{15} Y{15} F500;\n")                        fichier.write("RECHARGE\n")                        recharge_serg+=1                        extrusion_continu=0                        fichier.write(f"G0 Z{z_origine-50} F800;\n")                        fichier.write(f"G0 X{x_origine+t+a/2} Y{y_origine-15} F500;\n")                        fichier.write(f"G0 Z{z_origine-a*((couche-1)/nombre_couche_tot)} F800;\n")                         extrusion_continu+=0.4                        fichier.write(f"E{extrusion_continu:.3f} F1;\n")                        fichier.write("STOP 30000;\n")                                elif etape["type"] == "canal":                    # Récupération du mode de canal pour cette case                    canal_cfg = CONFIG_CANAUXV2[indice_case]                    fichier.write(f"G0 Z{z_origine-50} F1000;\n")                    if (indice_case<=8):                        fichier.write(f"G0 X{x_origine-20} F1000;\n")                                         else :                        fichier.write(f"G0 X{x_origine + 2*a+3*t + 20} F1000;\n")                    fichier.write(f"G0 Y{y_origine} F1000;\n")                     z=z_origine-a*((couche-1)/nombre_couche_tot)                     extrusion_continu = generer_canal(                        fichier=fichier,                        indice_case=indice_case,                        x_origine=x_origine, y_origine=y_origine,z=z,                        position=canal_cfg["position"],                        direction=canal_cfg["direction"],                        t=t, a=a,                        extrusion_continu=extrusion_continu                    )                    extrusion_continu+=0.1                    fichier.write(f"E{extrusion_continu:.3f} F1;\n")                    fichier.write("STOP 3500;\n")                    fichier.write(f"G0 Z{z_origine-50} F800;\n")                 # déterminer coordonnées du canal associé                    # extrusion_continu = tracer_ligne(fichier, x_origine+x_by_indice(indice_case,t,a)-x_deuxiemecanal, y_origine, z_origine-a*((couche-1)/nombre_couche_tot), x_origine+x_by_indice(indice_case,t,a)-x_deuxiemecanal, y_origine+y_by_indice(indice_case,t,a)+a/2, extrusion_continu)                    # extrusion_continu = tracer_ligne(fichier, x_by_indice(indice_case,t,a)-x_premiercanal, y_origine+y_by_indice(indice_case,t,a), z_origine, x_by_indice(indice_case,t,a),  y_origine+y_by_indice(indice_case,t,a), extrusion_continu)                fichier.write(f"G0 Z{z_origine-50} F1000\n")        fichier.write(f"G0 X{15} Y{15} F1000;\n")        fichier.write("SECHAGE\n")     extrusion_continu=0  with open(nom_fichier + ".txt", "w") as outfile:    for couche in range(1, nombre_couche_tot + 1):        currentfichier = nom_fichier + f"_couche_{couche}.txt"        with open(currentfichier) as infile:            for line in infile:                outfile.write(line) chemin = nom_fichier + ".txt" check_gcode_file(chemin, limit=30)resultats = calcul_tps_impression(chemin)for etape, dist in resultats.items():    print(f"{etape} : {dist/110:.2f} min || {dist/110//60:.0f}h{round(dist/110%60):.0f}")print(f"{recharge_serg} recharge de seringue d_hydrogel seront necessaire pour effectuer le fichier {nom_fichier}") # %% Cellule 8import osfiles = os.listdir("/Users/elyashariiz/Desktop/programmation/stage2a/test3/niveau_2_v4")for f in files:    if "trunkv3" in f:        print(f) # %% Cellule 11a = 0.1 + 0.2  #certains nombres décimaux simples comme 0.1, 0.2, 0.3 ne peuvent pas être représentés exactement en binaire.#C’est une limite des nombres flottants (floating point, norme IEEE 754)a # %% Cellule 12import re def ajuster_valeurs_z(fichier_entree, fichier_sortie, decalage):    """    Lit un fichier texte contenant des lignes G-code et soustrait 'decalage' à chaque valeur de Z.    Le résultat est enregistré dans un nouveau fichier.    """     with open(fichier_entree, 'r') as f:        contenu = f.read()     # Expression régulière pour trouver les valeurs de Z (ex: Z248.15)    pattern = r'Z(\d+\.?\d*)'     def remplacer(match):        valeur = float(match.group(1))        nouvelle_valeur = valeur - decalage  # on soustrait le décalage        return f"Z{str(round(1e2*(nouvelle_valeur))/100)}"          # Remplacement dans tout le texte    contenu_modifie = re.sub(pattern, remplacer, contenu)     # Sauvegarde dans un nouveau fichier    with open(fichier_sortie, 'w') as f:        f.write(contenu_modifie)     print(f"✅ Fichier modifié enregistré sous : {fichier_sortie}") ajuster_valeurs_z("/Users/elyashariiz/Desktop/programmation/stage2a/test4/8x2x2_trunkv5elyas2_couche_4.txt", "/Users/elyashariiz/Desktop/programmation/stage2a/test4/8x2x2_trunkv5elyas2_couche_4.txt", -2.25) # %% Cellule 13import osfiles = os.listdir()print(files)

8. Prototype 49 cells

The work later moved toward a 49-cell prototype arranged in a 7 by 7 by 1 configuration with an interconnected pneumatic network and a single air inlet.

9. CAD archive and selected STL parts

The CAD archive contains the mold versions used for the hydrogel-actuator project. Selected STL files are displayed directly on the page, while the full archive remains available for the CATIA and technical files.

Conclusion

This project combines additive manufacturing, silicone molding, hydrogel printing, pneumatic actuation and Python programming to fabricate soft bio-inspired actuators.

Download this project page

Use this button to open the print dialog, then choose “Save as PDF”.