Comprender laestructura de la API¶

Antes de programar, exploremos los puntos finales clave que utilizaremos:

1. Obtener todoslos trabajos¶

GET /api/v6.8/jobs

Devuelve una lista de todos los trabajos en la máquina con información básica como la hora de inicio, la hora de finalización y el estado.

2. Obtengadetalles específicos del trabajo¶

GET /api/v6.8/jobs/{jobId}

Devuelve información detallada sobre un trabajo específico, incluyendo:

  • hora de inicio: Cuando comenzó el trabajo
  • tiempo finalizado: Cuando finalizó el trabajo (nulo si aún se está ejecutando)
  • resultado: Estado del trabajo (Pendiente, Exitoso, Cancelado por el usuario, Fallido)
  • duracionesDesglose del tiempo de exposición, tiempo de repintado, etc.
  • material, tarea, capaEnTareaActual: Parámetros de compilación

3. Obtener mensajes de usuario para untrabajo¶

GET /api/v6.8/software/mensajes_de_usuario

Devuelve detalles sobre los mensajes del usuario, incluyendo

Se devuelve una lista de mensajes o nada si no hay mensajes.

  • severidad: Gravedad del mensaje del usuario
  • id del trabajo: el identificador único del trabajo de compilación,
  • tiempoAumentado: la marca de tiempo en la que se crearon los mensajes.
  • mensaje: el mensaje en sí mismo,

La API utiliza la autenticación OAuth2 y devuelve los datos en formato JSON. Todas las marcas de tiempo están en UTC.

Para obtener una descripción completa de la interfaz de la API web, consulte el apéndice Documentación de la API.

Configuración delentorno¶

En primer lugar, instalemos los paquetes Python necesarios y configuremos nuestras importaciones.

En [ ]:
importar solicitudes
importar tiempo
importar json
desde datetime importar datetime, timezone
desde typing importar Dict, Optional, Tupla
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
desde dotenv import load_dotenv
import os
from datetime importar timedelta

# A efectos de demostración, desactivaremos las advertencias SSL
# En producción, ¡utilice certificados SSL adecuados!
import urllib3
urllib3.desactivar_advertencias(urllib3.exceptions.Advertencia de solicitud insegura)

imprimir(«✓ ¡Todos los paquetes se han cargado correctamente!»)

Configuración¶

Configuremos los parámetros de conexión. En un entorno de producción real, se utilizarían variables de entorno o un archivo de configuración seguro para las credenciales.

Para acceder a la interfaz de la máquina, necesitamos crear credenciales API. Esto se puede hacer cómodamente a través de la EOSCONNECT Core . Abra la página web de su impresora en un navegador; en nuestro caso, sería https://si16120019/.

imagen.png

En muchos casos, el navegador mostrará una advertencia indicando que el sitio no es seguro. No se preocupe, esta advertencia aparece porque las máquinas EOS vienen con certificados autofirmados de forma predeterminada (consulte también Comprensión de los certificados autofirmados en el apéndice).

imagen-2.png

En el menú Configuración de autorización, podemos generar un nuevo par de ID de cliente y clave secreta utilizando el botón (+).

imagen-3.png

Las credenciales API generadas recientemente se almacenan en el .env archivo.

¿Por qué utilizar un .env ¿Archivo?

Almacenar credenciales confidenciales, como claves API y contraseñas, directamente en tu código supone un riesgo para la seguridad, especialmente si compartes tus cuadernos o los envías a sistemas de control de versiones como Git. A .env El archivo proporciona una solución limpia:

  • Seguridad: las credenciales se mantienen separadas de su código.
  • Comodidad: fácil de actualizar las credenciales sin modificar el código.
  • Flexibilidad: Los diferentes entornos (desarrollo, producción) pueden utilizar diferentes .env archivos
  • Mejores prácticas: sigue la metodología «12-factor app» para la gestión de la configuración.

Configuración de lasvariables¶

A continuación, determinamos la versión de la API EOSCONNECT Core que queremos utilizar. EOSCONNECT Core compatibilidad con versiones anteriores para la API web, lo que significa que se pueden consultar máquinas con software actual y antiguo a través de la API web. Leemos la última versión directamente desde la máquina; en el momento de publicar este artículo, la última versión disponible era la versión v6.8

En [ ]:
# Load environment variables from .env file
load_dotenv()

# Load API credentials from environment variables
HOSTNAME = os.getenv("HOSTNAME")
API_VERSION_INFO_URL = f"https://{HOSTNAME}/api/supportedVersions"

# Fetch available API versions

# Check if API_VERSION is set in environment variables
API_VERSION = os.getenv("API_VERSION")

if API_VERSION:
    # Use the version from environment variable
    print(f"✓ Using API version from environment variable: {API_VERSION}")

else:
    # Fetch API versions from the endpoint
    print(f"\n🔍 Fetching available API versions from {API_VERSION_INFO_URL}...")
    response = requests.get(API_VERSION_INFO_URL, verify=False, timeout=10)
    versions_data = response.json()
    # Extract version strings from the response
    available_versions = [f"v{v['majorVersion']}.{v['minorVersion']}" for v in versions_data]

    if available_versions:
        # Sort versions to get the latest
        API_VERSION = sorted(available_versions, key=lambda v: [int(x) for x in v.lstrip('v').split('.')])[-1]
        print(f"✓ Available API versions: {', '.join(available_versions)}")
        print(f"✓ Using latest version: {API_VERSION}")
    else:
        # Fallback to default version
        API_VERSION = "v6.0"
        print(f"⚠️  No versions found in response, using default: {API_VERSION}")

Carga decredenciales API¶

Ahora leemos las credenciales de la API desde el .env archivo en la memoria

En [ ]:
from dotenv import load_dotenv
import os
from datetime import timedelta

# Load environment variables from .env file
load_dotenv()

API_BASE_URL = f"https://{HOSTNAME}/api/{API_VERSION}"
API_CLIENT_ID = os.getenv("API_CLIENT_ID")
API_CLIENT_SECRET = os.getenv("API_CLIENT_SECRET")

print("✓ Environment variables loaded")
print(f"  - Client ID: {API_CLIENT_ID}")
print(f"  - Client Secret: {'*' * len(API_CLIENT_SECRET) if API_CLIENT_SECRET else 'Not set'}")
print(f"  - Base URL: {API_BASE_URL}")

Recuperación deltoken de acceso¶

Para comunicarnos con la API web de EOSCONNECT, necesitamos un token de acceso. Este token sirve como una llave digital que:

  • Autenticación: confirma que estamos autorizados para acceder a la API.
  • Autorización: Define qué acciones estamos autorizados a realizar (en función de los permisos del cliente).
  • Seguridad: protege la máquina contra el acceso no autorizado.

El token se solicita a través del flujo de credenciales de cliente OAuth2, un procedimiento estandarizado para la comunicación entre máquinas. Las credenciales de la API (ID de cliente y secreto) que hemos creado previamente en la EOSCONNECT Core se intercambian por un token de acceso con límite de tiempo.

Importante: El token tiene un período de validez limitado (normalmente 1 hora). Para aplicaciones de mayor duración, el token debe renovarse antes de que caduque.

En [ ]:
def get_oauth_token(client_id: str, client_secret: str) -> Optional[Dict]:
    """
    Fetch OAuth2 token using client credentials flow.
    
    Args:
        client_id: OAuth2 client ID
        client_secret: OAuth2 client secret
        
    Returns:
        Dictionary containing token information or None if request fails
    """
    token_url = f"https://{HOSTNAME}/auth/connect/token"
    
    # Prepare the request data for client credentials grant
    data = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret
    }
    
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    
    try:
        response = requests.post(token_url, data=data, headers=headers, verify=False, timeout=10)
        response.raise_for_status()
        token_data = response.json()
        return token_data
    except requests.exceptions.RequestException as e:
        print(f"❌ Error fetching OAuth token: {e}")
        return None


def refresh_token() -> bool:
    """
    Refresh the OAuth2 token and update global HEADERS.
    
    Returns:
        True if token refresh was successful, False otherwise
    """
    global API_TOKEN, HEADERS
    
    token_data = get_oauth_token(API_CLIENT_ID, API_CLIENT_SECRET)
    
    if token_data:
        API_TOKEN = token_data.get("access_token", "")
        token_type = token_data.get("token_type", "Bearer")
        
        HEADERS = {
            "Authorization": f"{token_type} {API_TOKEN}",
            "Accept": "application/json"
        }

        return True
    else:
        print("❌ Failed to refresh token")
        return False

# Fetch the token
token_response = get_oauth_token(API_CLIENT_ID, API_CLIENT_SECRET)

if token_response:
    # Extract token details
    API_TOKEN = token_response.get("access_token", "")
    token_type = token_response.get("token_type", "Bearer")
    expires_in = token_response.get("expires_in", 0)
    
    # Update the headers with the new token
    HEADERS = {
        "Authorization": f"{token_type} {API_TOKEN}",
        "Accept": "application/json"
    }
    
    # Calculate expiry time
    current_time = datetime.now(timezone.utc)
    expiry_time = current_time + timedelta(seconds=expires_in)
    
    # Display token information
    print("📋 Token Details:")
    print(f"  Token Type: {token_type}")
    print(f"  Expires In: {expires_in} seconds ({expires_in / 3600:.2f} hours)")
    print(f"  Current Time (UTC): {current_time.strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"  Expiry Time (UTC): {expiry_time.strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"  Token (first 50 chars): {API_TOKEN[:50]}...")
    print(f"\n✓ Authorization headers updated")
else:
    print("⚠️  Failed to retrieve token. Please check your credentials.")

Paso 1: Conexión a laAPI¶

Creemos funciones para obtener detalles del trabajo y mensajes de usuario desde la API. ¡Esto demuestra lo sencillo que es interactuar con la interfaz EOSCONNECT!

En [ ]:
def get_last_job() -> Optional[Dict]:
    """
    Fetch the most recent job from the printer.
    
    Returns:
        Dictionary containing the last job's details or None if request fails
    """
    url = f"{API_BASE_URL}/jobs/last"
    try:
        response = requests.get(url, headers=HEADERS, verify=False, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"❌ Error fetching last job: {e}")
        return None
    
def get_user_message(jobId : str, limit: int = 20) -> Optional[Dict]:
    """
    Fetch the current user message displayed on the printer.
    
    Returns:
        Dictionary containing the user message or None if request fails
    """
    url = f"{API_BASE_URL}/software/usermessages"
    params = {"jobId": jobId, "limit":limit}
    try:
        response = requests.get(url, headers=HEADERS, params=params, verify=False, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"❌ Error fetching user message: {e}")
        return None

def get_all_jobs(limit: int = 10, from_date: Optional[str] = None, to_date: Optional[str] = None) -> Optional[list]:
    """
    Fetch a list of recent jobs.
    
    Args:
        limit: Maximum number of jobs to retrieve
        from_date: Filter jobs starting from this date (ISO 8601 format, e.g., '2024-01-01T00:00:00Z')
        to_date: Filter jobs up to this date (ISO 8601 format, e.g., '2024-12-31T23:59:59Z')
        
    Returns:
        List of job summaries or None if request fails
    """
    url = f"{API_BASE_URL}/jobs"
    params = {"take": limit}
    
    # Add optional date filters if provided
    if from_date:
        params["from"] = from_date
    if to_date:
        params["to"] = to_date
    
    try:
        response = requests.get(url, headers=HEADERS, params=params, verify=False, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"❌ Error fetching jobs list: {e}")
        return None


print("✓ API functions defined")

Paso 2: Comprobación de laconexión¶

Probemos nuestra conexión recuperando los trabajos recientes de la impresora. Esto nos ayudará a comprender la estructura de los datos.

El obtener_trabajos La función consulta a la máquina todos los trabajos que se iniciaron dentro del intervalo de tiempo comprendido entre desde_fecha y hasta la fecha. Utilizamos la función API definida anteriormente. obtener_todos_los_trabajos para este fin. El resultado de la función API se formatea y luego se muestra en una interfaz de usuario Gradio. El código para la representación gráfica no tiene nada que ver con EOSCONNECT Core se ha trasladado a la ui.py archivo en aras de la claridad.

Veamos los detalles del trabajo:

  • El id es un identificador único para el trabajo. Desafortunadamente, no proporciona información sobre nuestra tarea (receta), ya que solo representa una combinación del número de serie de la máquina y una marca de tiempo.
  • El resultado es una propiedad que nos interesa. Queremos que se nos notifique si un trabajo se ha completado y si ha tenido éxito o no.
En [ ]:
import pandas as pd
from datetime import datetime, timedelta
from ui import create_job_browser_ui

def fetch_jobs(from_date, to_date):
    """
    Fetch jobs within the specified date range and display them in a table.
    
    Args:
        from_date: Start date for filtering jobs
        to_date: End date for filtering jobs
        
    Returns:
        Pandas DataFrame with job information
    """
    # Convert dates to ISO 8601 format with time
    from_datetime = f"{from_date}T00:00:00.000Z" if from_date else None
    to_datetime = f"{to_date}T23:59:59.999Z" if to_date else None
    
    # Fetch jobs from API
    refresh_token()
    jobs = get_all_jobs(limit=500, from_date=from_datetime, to_date=to_datetime)
    
    if not jobs:
        return pd.DataFrame(columns=['id', 'result', 'timeStarted', 'timeEnded', 'task', 'material'])
    
    # Extract relevant fields
    job_data = []
    for job in jobs:
        job_data.append({
            'id': job.get('id', 'N/A'),
            'result': job.get('result', 'N/A'),
            'timeStarted': job.get('timeStarted', 'N/A'),
            'timeEnded': job.get('timeEnded', 'N/A'),
            'task': job.get('task', 'N/A'),
            'material': job.get('material', 'N/A')
        })
    
    # Create DataFrame
    df = pd.DataFrame(job_data)
    return df

# Launch the interface
demo = create_job_browser_ui(fetch_jobs)
demo.launch(share=False, inline=True)

Ejemplo: este es un ejemplo de resultado. imagen.png

Ahora vemos una descripción general de los trabajos creados más recientemente. Dependiendo del resultado del trabajo, podemos decidir si queremos obtener más detalles o no. En nuestro caso, solo nos interesan los detalles adicionales, concretamente los mensajes de usuario, si un trabajo se ha cancelado o finalizado debido a un error.

Paso 3:Lógica de supervisión de tareas¶

Ahora implementemos la lógica central de supervisión. Esta función comprueba si el trabajo creado más recientemente se ha completado correctamente o no. Si el trabajo se ha abortado o terminado debido a un error, recuperamos los mensajes del usuario para recopilar cualquier información relevante sobre la cancelación.

En [ ]:
def check_last_job_status(last_job_id: Optional[str] = None) -> Optional[Dict]:
    """
    Check the status of the last job.
    
    Args:
        last_job_id: Optional job ID to compare against. If provided and matches
                     the current last job, returns None (no change detected)
    
    Returns:
        Dictionary containing:
        - 'result': Job result status (Pending, Successful, Failed, UserCanceled)
        - 'jobId': The job ID
        - 'task': The task name
        - 'timeStarted': When the job started
        - 'timeEnded': When the job ended (None if still running)
        - 'userMessages': List of user messages (only for failed jobs)
        
        Returns None if last_job_id matches the current last job ID
    """
    refresh_token()
    # Fetch the last job
    last_job = get_last_job()
    
    if not last_job:
        return {
            'result': 'Error',
            'jobId': None,
            'task': None,
            'timeStarted': None,
            'timeEnded': None,
            'userMessages': None,
            'error': 'Failed to fetch last job'
        }
    
    job_id = last_job.get('id', 'Unknown')
    
    # If last_job_id is provided and matches current job, return None
    if last_job_id is not None and job_id == last_job_id:
        return None
    
    result = last_job.get('result', 'Unknown')
    task = last_job.get('task', 'N/A')
    time_started = last_job.get('timeStarted', 'N/A')
    time_ended = last_job.get('timeEnded', 'N/A')
    
    # Prepare base response
    response = {
        'result': result,
        'jobId': job_id,
        'task': task,
        'timeStarted': time_started,
        'timeEnded': time_ended,
        'userMessages': None
    }
    
    # If job failed or was canceled, fetch user messages
    if result in ['Failed', 'UserCanceled']:
        user_messages = get_user_message(job_id, limit=20)
        response['userMessages'] = user_messages if user_messages else []
    
    return response


print("✓ Last job status check function defined")
En [ ]:
import pandas as pd
from ui import create_job_status_ui

# Track the last job ID
last_job_id_tracker = None

def fetch_job_status():
    """
    Fetch the last job status and format it for display.
    Returns a tuple of (job_info_df, user_messages_df)
    """
    global last_job_id_tracker
    
    result = check_last_job_status(last_job_id_tracker)
    
    if result is None:
        # No new job detected
        return (
            pd.DataFrame([{"Info": f"No new job detected or job status unchanged. Tracked ID: {last_job_id_tracker}"}]),
            pd.DataFrame()
        )
    
    # Update tracker
    last_job_id_tracker = result.get('jobId')
    
    # Create job info DataFrame
    job_info = {
        'Field': ['Job ID', 'Task', 'Result', 'Time Started', 'Time Ended'],
        'Value': [
            result.get('jobId', 'N/A'),
            result.get('task', 'N/A'),
            result.get('result', 'N/A'),
            result.get('timeStarted', 'N/A'),
            result.get('timeEnded', 'N/A')
        ]
    }
    job_info_df = pd.DataFrame(job_info)
    
    # Create user messages DataFrame
    user_messages = result.get('userMessages')
    if user_messages:
        messages_data = []
        for msg in user_messages:
            messages_data.append({
                'Severity': msg.get('severity', 'N/A'),
                'timeRaised': msg.get('timeRaised', 'N/A'),
                'message': msg.get('message', 'N/A'),
                'additionalInfo': msg.get('additionalInfo', 'N/A')
            })
        user_messages_df = pd.DataFrame(messages_data)
    else:
        user_messages_df = pd.DataFrame([{"Info": "No user messages available"}])
    
    return job_info_df, user_messages_df

def clear_last_job_id_tracker():
    global last_job_id_tracker 
    last_job_id_tracker = None
    return [],[]
    

# Create Gradio interface for job status
job_status_demo = create_job_status_ui(fetch_job_status, clear_last_job_id_tracker)

# Launch the interface
job_status_demo.launch(share=False, inline=True)

Ejemplo: este es un ejemplo de resultado. imagen.png

En este ejemplo, podemos ver que el trabajo creado más recientemente no se completó correctamente. Al revisar los mensajes de usuario, se observa que el operador de la máquina canceló el trabajo.

Apéndice:¶

Visualización de la documentación de EOSCONNECT Core conel editor OpenAPI¶

Puedes utilizar el siguiente editor oficial Swagger: https://editor.swagger.io/ para renderizar el swagger.json archivo. Esto le ofrece una visión general de todas las llamadas API disponibles proporcionadas por EOSCONNECT Core. EOSCONNECT Core también ofrece directamente una interfaz similar EOSCONNECT Core se encuentra en la sección API web / Puntos de datos.

  1. Accede al editor oficial de Swagger: https://editor.swagger.io/
  2. Haga clic en Archivo → Importar URL
  3. Seleccione el swagger.json archivo

Comprensión de loscertificados autofirmados¶

Cuando te conectas a un sitio web, tu navegador comprueba si el certificado de seguridad del sitio está firmado por una autoridad de confianza (como una organización conocida).

Los certificados autofirmados son creados por el propio propietario del sitio web, no por una autoridad de confianza. Es como escribir tu propio documento de identidad en lugar de obtenerlo del gobierno.

Tu navegador muestra una advertencia porque no puede verificar si el certificado es fiable: podría ser legítimo (como en el caso de las impresoras EOS) o potencialmente peligroso. En el caso de las máquinas EOS, es seguro porque te estás conectando a tu propia impresora local, no a un sitio web aleatorio en Internet.

Piénsalo así: la impresora está diciendo «Confía en mí, soy quien digo ser» sin que haya un tercero que lo avale.