Hello Guest it is February 06, 2026, 08:02:48 PM

Author Topic: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales  (Read 375 times)

0 Members and 1 Guest are viewing this topic.

MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« on: February 03, 2026, 10:38:42 AM »
Hi,

I have a pokeys57cnc and mach4 and would like to control a laser with PWM to engrave images.

A few years earlier, I used Auggie. It was fine but not very user-friendly.
I saw a bit of developments coming from pokeys's developers and I thought they might have implemented new ways to control a pwm and bypass S commands breaking constant velocity.

I saw somewhere a way to use an arduino nano to take step/dir signals from a motor and convert it as a pwm signal for my diode. But with a 150€ cards, isn't that possible ? I tried M62/M67 with no sucess.
I would like to avoid the nano solution or change my card and therefore settings.

Thanks,
Alexandre
Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #1 on: February 03, 2026, 04:32:06 PM »
In the changelog of the pokey plugin for mach4, it says :
9.06.0.5325 (28.3.2024)
- Support for M67 (analog motion-sync commands - e.g. laser PWM control) - M67 E0 Q100 (only Analog output 0 is supported)

Does anybody know how to make it work ?
Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #2 on: February 04, 2026, 10:46:26 AM »
Ok, I will answer to myself in order to maybe help others on the subject.

After a few echange with poscope's support, I installed last plugin (9.17.0.5596) and it appeared that my mach4 version was too old (5036). It was the version available on their website. But there are new versions available on https://www.machsupport.com/ftp/Mach4/DevlopmentVersions/
I installed the last build (6693) and finally could assign the desired pwm pin on the analog output 0 in order to use M67 E0 QXX to control my laser with synced movements.

I'll now need to change my scripts since the new version seems to have broke those and make some tunings with my laser settings.

Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #3 on: February 05, 2026, 01:47:25 AM »
I first tried to use LaserGRBL or lightburn but those two doesn't suit my CNC. It is a queenbee screw driven. Therefore I need spaces to be able to reach the maximum velocity. Auggie did that and I used a python script to generate a greyscale matrix with variable feedrates and power. Since the raster is bidirectionnal, I saw some blur caused by the small delay of M67 commands. With a bit of tweaking, I managed to wipe them. If someone is interested, here's the script :
Code: [Select]
# =========================================
# Laser Calibration - Feedrates
# =========================================

def linspace(start, stop, n):
    if n <= 1: return [start]
    return [start + i * (stop - start) / (n - 1) for i in range(n)]

# --- PARAMETERS ---
min_power = 8
max_power = 35
num_steps = 10
power_steps = linspace(min_power, max_power, num_steps)

feedrates = [1400, 1600, 2000, 2400, 2600, 2800, 3000]

# Base 16ms (Parfaite à F1500)
laser_latency_ms = 16.0

# blur tweak
boost_at_3000 = -0.25
reference_feed = 1500.0

# runway premove
pre_move = 15.0 

band_width = 2.0
gap_width = 1.0
line_step = 0.14

output_file = "laser_calib_negative_boost.nc"

# --- CALCULATIONS ---
lines_per_band = int(band_width / line_step)
total_length = (num_steps * 2.0) + ((num_steps - 1) * 0.5)

gcode = []
gcode.append("G21 G90 G17 G94")
gcode.append("M3 M67 E0 Q0")

current_y = 0.0

for feed in feedrates:
    # 1. Base 16ms
    base_offset = (feed * laser_latency_ms) / 60000
   
    # 2. Boost négatif progressif
    extra_boost = 0.0
    if feed > reference_feed:
        # La valeur sera négative ici
        extra_boost = boost_at_3000 * ((feed - reference_feed) / (3000.0 - reference_feed))
   
    current_offset = base_offset + extra_boost
   
    gcode.append(f"\n( BAND F{feed} | Offset: {current_offset:.3f}mm )")
   
    for line in range(lines_per_band):
        if line % 2 == 0:
            x_dir, x_start = 1, 0.0
            corr = current_offset
        else:
            x_dir, x_start = -1, total_length
            corr = -current_offset

        start_runway = x_start - (pre_move * x_dir)
        gcode.append(f"G0 X{start_runway:.3f} Y{current_y:.3f}")

        x_cursor = x_start
        gcode.append(f"G1 X{x_cursor + corr:.3f} F{feed}")

        p_seq = list(enumerate(power_steps)) if x_dir == 1 else list(enumerate(power_steps))[::-1]
       
        for i, p in p_seq:
            gcode.append(f"M67 E0 Q{p:.2f}")
            x_cursor += x_dir * 2.0
            gcode.append(f"G1 X{x_cursor + corr:.3f}")

            if (x_dir == 1 and i < num_steps-1) or (x_dir == -1 and i > 0):
                gcode.append("M67 E0 Q0")
                x_cursor += x_dir * 0.5
                gcode.append(f"G1 X{x_cursor + corr:.3f}")

        gcode.append("M67 E0 Q0")
        end_runway = x_cursor + (pre_move * x_dir)
        gcode.append(f"G1 X{end_runway:.3f}")

        current_y += line_step
    current_y += gap_width

gcode.append("\nM5\nM30")

with open(output_file, "w") as f:
    f.write("\n".join(gcode))
   
print(f"G-code généré : {output_file}")
print(f"F1500 : Offset = {(1500*laser_latency_ms)/60000:.3f} mm (Boost: 0.000)")
print(f"F3000 : Offset = {((3000*laser_latency_ms)/60000 + boost_at_3000):.3f} mm (Boost: {boost_at_3000})")

Next step : gamma settings and a converter Image ->Gcode.
« Last Edit: February 05, 2026, 01:49:14 AM by momofr83 »

Offline Tweakie.CNC

*
  • *
  •  9,335 9,335
  • Super Kitty
Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #4 on: February 05, 2026, 02:22:08 AM »
Following your progress with interest.

Tweakie.
PEACE
Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #5 on: February 05, 2026, 03:09:13 AM »
Hi Tweakie,
It's a honor to have you since I red your laser project topic several years ago.

I made a python script to convert an image in Gcode, not that bad but more tuning is needed to get the fullscale gray level potential of my laser. Here's the code :
Code: [Select]
from PIL import Image, ImageOps

# --- PARAMÈTRES UTILISATEUR ---
input_image_path = "IMG_3901.jpg"
output_file = "IMG_3901.nc"

target_width_mm = 50.0   
feedrate = 3000         
line_step = 0.14         

min_power = 10            # Blanc / Gris très clair
max_power = 25           # Noir profond (Saturation à partir de 20)

# --- REGLAGE DU CONTRASTE ---
# gamma > 1.0 : éclaircit les gris moyens (donne plus de nuances dans les zones sombres)
gamma_correction = 1.6 

# --- CALIBRATION DYNAMIQUE ---
laser_latency_ms = 16.0
boost_at_3000 = -0.25
reference_feed = 1500.0
pre_move = 15.0         

# --- CALCUL DE L'OFFSET ---
base_offset = (feedrate * laser_latency_ms) / 60000
extra_boost = 0.0
if feedrate > reference_feed:
    extra_boost = boost_at_3000 * ((feedrate - reference_feed) / (3000.0 - reference_feed))
current_offset = base_offset + extra_boost

# --- TRAITEMENT IMAGE ---
img = Image.open(input_image_path).convert('L')

# 1. Égalisation et Contraste pour utiliser toute l'échelle de puissance
img = ImageOps.equalize(img)
img = ImageOps.autocontrast(img, cutoff=1)

# 2. Redimensionnement proportionnel
original_w, original_h = img.size
width_pixels = int(target_width_mm / line_step)
height_pixels = int(width_pixels * (original_h / original_w))
img = img.resize((width_pixels, height_pixels), Image.Resampling.LANCZOS)

real_width_mm = width_pixels * line_step
real_height_mm = height_pixels * line_step

# --- GÉNÉRATION G-CODE ---
gcode = []
gcode.append(f"( Image: {input_image_path} | Gamma: {gamma_correction} )")
gcode.append("G21 G90 G17 G94")
gcode.append("M3 M67 E0 Q0")

# Boucle de gravure (de bas en haut pour origine en bas à gauche)
for py in range(height_pixels - 1, -1, -1):
    y_pos = (height_pixels - 1 - py) * line_step
    is_forward = (py % 2 == 0)
   
    if is_forward:
        x_start, x_end, x_dir = 0.0, (width_pixels - 1) * line_step, 1
        x_range = range(width_pixels)
        corr = current_offset
    else:
        x_start, x_end, x_dir = (width_pixels - 1) * line_step, 0.0, -1
        x_range = range(width_pixels - 1, -1, -1)
        corr = -current_offset

    # Approche (Runway)
    gcode.append(f"G0 X{x_start - (pre_move * x_dir):.3f} Y{y_pos:.3f}")
    gcode.append(f"G1 X{x_start + corr:.3f} F{feedrate}")

    # Gravure de la ligne pixel par pixel
    for px in x_range:
        pixel_val = img.getpixel((px, py)) # 0 à 255
       
        # Inversion et Normalisation (0.0 à 1.0)
        norm_val = 1.0 - (pixel_val / 255.0)
       
        # Courbe Gamma
        corrected_val = norm_val ** gamma_correction
       
        # Conversion en puissance réelle
        power = min_power + (corrected_val * (max_power - min_power))
       
        x_pixel = px * line_step
        gcode.append(f"M67 E0 Q{power:.2f}")
        gcode.append(f"G1 X{x_pixel + corr:.3f}")

    # Fin de ligne et Overscan
    gcode.append("M67 E0 Q0")
    gcode.append(f"G1 X{x_end + (pre_move * x_dir):.3f}")

gcode.append("M5\nM30")

# Écriture du fichier
with open(output_file, "w") as f:
    f.write("\n".join(gcode))

# Affichage des informations
print(f"Fichier généré avec correction gamma {gamma_correction}")
print(f"Nombre de pixels : {width_pixels}x{height_pixels}")
print(f"Taille de sortie : {real_width_mm:.2f}x{real_height_mm:.2f} mm")
I tried to upload an image but an error occured, I uploaded it : https://i.postimg.cc/Zq9d0N0h/IMG-4230c.jpg

Ps : Don't mind the spoiler board, the real one is underneath !
Ps2 : the verification method is broken, forced to listen to it ? (The letters you typed don't match the letters that were shown in the picture)
« Last Edit: February 05, 2026, 03:13:23 AM by momofr83 »
Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #6 on: February 06, 2026, 03:37:23 AM »
I had some troubles with my macros not working anymore. I don't know why but .mcc wouldn't create. It was admin restrictions, just had to allow total control on the folder.
I took the occasion to test a notification system when the job is done on my smartphone, with the application "ntfy". I just need to add M334 at the end of every Gcode.
Code: [Select]
function m334()
    local inst = mc.mcGetInstance()
   
    local topic = "mach4_momo"
    local message = "Job's done ! Wake up !"
   
    -- curl command
    local cmd = string.format('curl -d "%s" http://ntfy.sh/%s', message, topic)
   
    mc.mcCntlSetLastError(inst, "Execution : " .. cmd)
   
    local success, reason, code = os.execute(cmd)
   
    if success then
        mc.mcCntlSetLastError(inst, "NTFY : Success (Code " .. tostring(code) .. ")")
    else
        mc.mcCntlSetLastError(inst, "NTFY : Error (" .. tostring(reason) .. ")")
    end
end

if (mc.mcInEditor() == 1) then
    m334()
end
« Last Edit: February 06, 2026, 03:39:05 AM by momofr83 »
Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #7 on: February 06, 2026, 04:22:58 AM »
Note : In a Gcode, to ensure the successfull sending of the notification :
M5    -- Stop the laser in this case
M334 -- our macro to send notification
G4 P2 -- Pause 2s 
M30 -- Program end
Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #8 on: February 06, 2026, 04:39:13 AM »
Back to my Gcode converter, configuring the ideal settings is tricky. It seems that my Laser is very sensible. It's a laser tree 80W.
With a range limit of 9.5 to 20%, I got the nuances I want but the burning wood is not a linear process. So I'm trying to change the power map and what's best than a histogram (in attachment) to see my image translation in power ?
Re: MACH4\pokeys57 : Controlling PWM to laser engrave in greyscales
« Reply #9 on: February 06, 2026, 09:18:57 AM »
I spent some time figuring why there was no difference in small power percentage, the PWM was too high (20000Hz).
With 5000Hz, gray are less quantized. I'm not sure whose the most guilty pokey's card or the laser. (I'm assuming the latter)

I'll pursue my tests and post a picture when i'm satisfied.