Comprendere lastruttura dell'API¶

Prima di scrivere il codice, esaminiamo i principali endpoint che utilizzeremo:

1. Ottieni tuttii lavori¶

GET /api/v6.8/lavori

Restituisce un elenco di tutti i lavori in esecuzione sul computer con informazioni di base quali ora di inizio, ora di fine e stato.

2. Ottienidettagli specifici sul lavoro¶

GET /api/v6.8/lavori/{idlavoro}

Restituisce informazioni dettagliate su un lavoro specifico, tra cui:

  • ora di inizio: Quando il lavoro è iniziato
  • tempo terminato: Quando il processo è terminato (null se ancora in esecuzione)
  • risultato: Stato del lavoro (In attesa, Di successo, Annullato dall'utente, Fallito)
  • durate: Ripartizione del tempo di esposizione, tempo di ricopertura, ecc.
  • materiale, compito, livelloInAttivitàCorrente: Parametri di costruzione

3. Ottenere i messaggi utente per unlavoro¶

GET /api/v6.8/software/messaggiutente

Restituisce dettagli sui messaggi dell'utente, inclusi

Viene restituito un elenco di messaggi o nulla se non esistono messaggi.

  • gravità: Gravità del messaggio utente
  • ID lavoro: l'identificatore univoco del processo di compilazione,
  • tempoAumentato: il timestamp in cui sono stati creati i messaggi,
  • messaggio: il messaggio stesso,

L'API utilizza l'autenticazione OAuth2 e restituisce i dati in formato JSON. Tutti i timestamp sono in UTC.

Per una panoramica completa dell'interfaccia API Web, consultare l'appendice Documentazione API.

Configurazione dell'ambiente¶

Per prima cosa, installiamo i pacchetti Python necessari e configuriamo le nostre importazioni.

In [ ]:
importare richieste
importare tempo
importazione json
da datetime import datetime, timezone
da digitazione import Dict, Optional, Tuple
import smtplib
da email.mime.text import MIMEText
da email.mime.multipart import MIMEMultipart
da dotenv import load_dotenv
import os
da datetime import timedelta

# A scopo dimostrativo, disabiliteremo gli avvisi SSL
# In produzione, utilizzare certificati SSL adeguati!
import urllib3
urllib3.disable_warnings(urllib3.eccezioni.Avviso di richiesta non sicura)

stampa("✓ Tutti i pacchetti sono stati caricati correttamente!")

Configurazione¶

Configuriamo i parametri di connessione. In un ambiente di produzione reale, per le credenziali si utilizzerebbero variabili di ambiente o un file di configurazione sicuro.

Per accedere all'interfaccia della macchina, è necessario creare delle credenziali API. Ciò può essere fatto comodamente tramite EOSCONNECT Core . Apri la pagina web della tua stampante in un browser: nel nostro caso, sarebbe https://si16120019/.

immagine.png

In molti casi, il browser visualizzerà un avviso che indica che il sito non è sicuro: non preoccuparti, questo avviso appare perché le macchine EOS sono dotate di certificati autofirmati per impostazione predefinita (vedi anche Comprensione dei certificati autofirmati nell'appendice).

immagine-2.png

Nel menu Impostazioni autorizzazione, è possibile generare una nuova coppia ID cliente e segreto utilizzando il pulsante (+).

immagine-3.png

Le credenziali API generate di recente sono memorizzate nel .env file.

Perché utilizzare un .env file?

Memorizzare credenziali sensibili come chiavi API e password direttamente nel codice rappresenta un rischio per la sicurezza, specialmente se si condividono i propri notebook o li si inserisce in sistemi di controllo delle versioni come Git. A .env Il file fornisce una soluzione pulita:

  • Sicurezza: le credenziali sono conservate separatamente dal codice
  • Praticità: facile aggiornare le credenziali senza modificare il codice
  • Flessibilità: Ambienti diversi (sviluppo, produzione) possono utilizzare diversi .env file
  • Best practice: segue la metodologia "12-factor app" per la gestione della configurazione

Impostazione dellevariabili¶

Di seguito, determiniamo la versione dell'API EOSCONNECT Core che desideriamo utilizzare. EOSCONNECT Core retrocompatibilità per l'API Web, il che significa che è possibile interrogare tramite l'API Web sia le macchine con software attuale che quelle con software precedente. Leggiamo la versione più recente direttamente dalla macchina: al momento della pubblicazione di questo post, l'ultima versione disponibile era la versione v6.8

In [ ]:
# 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}")

Caricamentodelle credenziali API¶

Ora leggiamo le credenziali API dal file .env memorizzare in memoria

In [ ]:
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}")

Recupero deltoken di accesso¶

Per comunicare con l'API Web EOSCONNECT, abbiamo bisogno di un token di accesso. Questo token funge da chiave digitale che:

  • Autenticazione: conferma che siamo autorizzati ad accedere all'API
  • Autorizzazione: definisce quali azioni siamo autorizzati a eseguire (in base alle autorizzazioni del cliente)
  • Sicurezza: protegge la macchina da accessi non autorizzati

Il token viene richiesto tramite OAuth2 Client Credentials Flow, una procedura standardizzata per la comunicazione tra macchine. Le credenziali API (ID cliente e segreto) che abbiamo creato in precedenza EOSCONNECT Core vengono scambiate con un token di accesso a tempo limitato.

Importante: il token ha un periodo di validità limitato (in genere 1 ora). Per le applicazioni con durata maggiore, il token deve essere rinnovato prima della scadenza.

In [ ]:
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.")

Passaggio 1: connessioneall'API¶

Creiamo delle funzioni per recuperare i dettagli dei lavori e i messaggi degli utenti dall'API. Questo dimostra quanto sia semplice interagire con l'interfaccia EOSCONNECT!

In [ ]:
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")

Passaggio 2: Verifica dellaconnessione¶

Verifichiamo la nostra connessione recuperando i lavori recenti dalla stampante. Questo ci aiuterà a comprendere la struttura dei dati.

Il fetch_jobs La funzione interroga la macchina per tutti i lavori avviati nell'intervallo di tempo compreso tra da_data e ad oggi. Utilizziamo la funzione API definita in precedenza ottieni_tutti_i_lavori a questo scopo. Il risultato della funzione API viene formattato e quindi visualizzato in un'interfaccia utente Gradio. Il codice per la rappresentazione grafica non ha nulla a che vedere con EOSCONNECT Core è stato spostato nel ui.py file per chiarezza.

Diamo un'occhiata ai dettagli del lavoro:

  • Il id è un identificatore univoco per il lavoro. Purtroppo, non fornisce alcuna informazione sul nostro compito (ricetta), poiché rappresenta semplicemente una combinazione del numero di serie della macchina e di un timestamp.
  • Il risultato è una proprietà che ci interessa. Vogliamo essere avvisati quando un lavoro è stato completato e se ha avuto esito positivo o meno.
In [ ]:
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)

Esempio: questo è un esempio di output immagine.png

Ora vediamo una panoramica dei lavori creati più di recente. A questo punto, a seconda del risultato del lavoro, possiamo decidere se recuperare ulteriori dettagli o meno. Nel nostro caso, siamo interessati solo a dettagli aggiuntivi, in particolare ai messaggi degli utenti, se un lavoro è stato annullato o terminato a causa di un errore.

Passaggio 3:Logica di monitoraggio dei lavori¶

Ora implementiamo la logica di monitoraggio principale. Questa funzione verifica se l'ultimo lavoro eseguito è stato completato con successo o meno. Se il lavoro è stato interrotto o terminato a causa di un errore, recuperiamo i messaggi dell'utente per raccogliere tutte le informazioni rilevanti relative alla cancellazione.

In [ ]:
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")
In [ ]:
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)

Esempio: questo è un esempio di output immagine.png

In questo esempio, possiamo vedere che l'ultimo lavoro creato non è stato completato con successo. Dai messaggi dell'utente risulta che l'operatore della macchina ha interrotto il lavoro.

Appendice:¶

Visualizzazione della documentazione EOSCONNECT Core con OpenAPIEditor¶

È possibile utilizzare il seguente editor Swagger ufficiale: https://editor.swagger.io/ per rendere il swagger.json file. Questo ti offre una panoramica di tutte le chiamate API disponibili fornite da EOSCONNECT Core. Un'interfaccia simile è offerta anche direttamente da EOSCONNECT Core e si trova nella sezione Web API / Data Points.

  1. Accedi all'editor ufficiale Swagger: https://editor.swagger.io/
  2. Clicca su File → Importa URL
  3. Seleziona il swagger.json file

Comprenderei certificati autofirmati¶

Quando ti connetti a un sito web, il tuo browser verifica se il certificato di sicurezza del sito è firmato da un'autorità affidabile (come un'organizzazione ben nota).

I certificati autofirmati sono creati dal proprietario del sito web stesso, non da un'autorità affidabile. È come scrivere la propria carta d'identità invece di ottenerne una dal governo.

Il browser visualizza un avviso perché non è in grado di verificare l'affidabilità del certificato, che potrebbe essere legittimo (come nel caso delle stampanti EOS) o potenzialmente pericoloso. Nel caso delle macchine EOS, la connessione è sicura perché si collega alla propria stampante locale e non a un sito web casuale su Internet.

Pensateci in questi termini: la stampante sta dicendo "Fidati di me, sono chi dico di essere" senza che una terza parte ne garantisca l'autenticità.