Files
scanengine-3/docs/hardware/laser_control_implementation_guide.md
T
2026-07-28 09:20:58 -05:00

15 KiB
Executable File

Genesis SLM MX 532 Laser Control - PyQt6 Implementation Guide

Protocol Overview

The laser uses NXP I2C-over-serial protocol on /dev/ttyUSB0 at 9600 8N1.

Packet Format

Write:

[0x53] [I2C_ADDR_WRITE] [LENGTH] [COMMAND_BYTES] [DATA_BYTES] [0x50]

Read:

[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]

I2C Device Map

Device Address Write Read Purpose
X9119 (current) 0x152 0x52 0x53 Laser current control
X9119 (photo) 0x152 0x52 0x53 Power feedback control
PCA9555 (PS DIO) 0x140 0x40 0x41 Power supply digital I/O
PCA9555 (Head DIO) 0x144 0x44 0x45 Head digital I/O
PCA9555 (PS Glue In) 0x148 0x48 0x49 Power supply glue logic input
PCA9555 (PS Glue Out) 0x14a 0x4a 0x4b Power supply glue logic output
AD5254 (current limit) 0x158 0x58 0x59 Current limit potentiometer
AD5254 (photo limit) 0x158 0x58 0x59 Photo limit potentiometer
ADS7828 (ADC) 0x190 0x90 0x91 Analog sensor readings
M24C64 (EEPROM) 0x2a4 0xa4 0xa5 Configuration storage
STM32 (MCU) 0x2ae 0xae 0xaf Microcontroller
DS1682 (hour meter) 0x1d6 0xd6 0xd7 Hour meter

Control Commands (from SendI2cCommand)

SET Commands with Vtable Offsets

Command Offset Device Details
PCMD= 0xf8 X9119 @ 0x152 Power command, cmd 0xa0, 0-1023
PMEM= 0x100 X9119 @ 0x152 Power memory (writes to NV)
CCMD= 0xe8 X9119 @ 0x152 Current command, cmd 0xa0, 0-1023
SHCMD= 0x54 PCA9555 @ 0x14a Shutter, port 0, bit 0x01
KSWCMD= 0x60 PCA9555 @ 0x14a Keyswitch, port 0, bit 0x20
CMODECMD= 0x84 PCA9555 @ 0x14a Current mode, port 0, bit 0x04
ANACMD= 0x70 PCA9555 @ 0x14a Analog enable, port 0, bit 0x10
REM= 0x7c PCA9555 @ 0x14a Remote enable, port 0, bit 0x08

QUERY Commands with Vtable Offsets

Command Offset Returns
?TMAINCMD 0xdc Main temperature command
?MAIND 0xe0 Main diode current
?PSGLUEIN 0x44 PS glue input (PCA9555 @ 0x148)
?PSGLUEOUT 0x48 PS glue output (PCA9555 @ 0x14a)
?HEADDIO 0x4c Head DIO (PCA9555 @ 0x144)
?WAVELENGTH 0x14 Wavelength reading
?LASERMODEL 0x10 Model string
?POWERUNITS 0x110 Power units string

Device-Specific Commands

X9119 Digital Potentiometer

  • Write wiper: Command 0xa0, 2 bytes (10-bit value 0-1023)
  • Valid range: 0-1023
  • Error message: "Valid code range is 0-1023"

PCA9555 I/O Expander Registers

Register Address Purpose
Input Port 0 0x00 Read input state
Input Port 1 0x01 Read input state
Output Port 0 0x02 Write output state
Output Port 1 0x03 Write output state
Polarity Inv 0 0x04 Invert polarity
Polarity Inv 1 0x05 Invert polarity
Config Port 0 0x06 Direction (0=out, 1=in)
Config Port 1 0x07 Direction (0=out, 1=in)

PCA9555 @ 0x14a (Power Supply Glue Out) Bit Assignments

Bit Mask Function
0 0x01 Shutter command
2 0x04 Current mode command
3 0x08 Remote enable command
4 0x10 Analog input enable
5 0x20 Keyswitch command

ADS7828 ADC Commands

  • Current actual: Channel 0x84
  • Photo actual (alt): Channel 0xe4
  • Main temp actual: Channel 0x94

Read Strategies (Critical!)

The DLL uses three different read strategies:

1. ReadOne - Single Read

def i2c_read_one(addr, cmd, data_len):
    """Single read, no filtering. Use for digital I/O."""
    return nxp_read(addr, cmd, 1 if cmd <= 0xFF else 2, data_len)

Use for: Digital I/O states (PCA9555 reads)

2. ReadMatchTwo - Match Until Consistent

def i2c_read_match_two(addr, cmd, data_len, max_attempts=5):
    """Read until 2 values match (up to 5 attempts). Most reliable."""
    readings = []
    for attempt in range(max_attempts):
        value = nxp_read(addr, cmd, 1 if cmd <= 0xFF else 2, data_len)
        readings.append(value)
        if readings.count(value) >= 2:
            return value
        time.sleep(0.01)
    return readings[0]

Use for: EEPROM reads, configuration values

3. ReadDiscardHighLow - Median Filter

def i2c_read_discard_high_low(addr, cmd, data_len):
    """Read 3 times, return median. Filters noise."""
    readings = []
    for _ in range(3):
        value = nxp_read(addr, cmd, 1 if cmd <= 0xFF else 2, data_len)
        readings.append(value)
        time.sleep(0.01)
    return sorted(readings)[1]  # Median

Use for: ADC readings (current, temperature, voltage)

Implementation Requirements

Core Protocol Functions

def nxp_write(i2c_addr_write, cmd, data, data_len):
    """
    Build NXP I2C write packet.
    
    Args:
        i2c_addr_write: I2C device address (write bit cleared)
        cmd: Command byte(s) - int for 1 byte, int for 2 bytes
        data: Data value to write
        data_len: 1, 2, or 4 bytes
    
    Packet: [0x53][addr][len][cmd...][data...][0x50]
    """
    pass

def nxp_read(i2c_addr_write, cmd, cmd_len, data_len):
    """
    Build NXP I2C read packet.
    
    Args:
        i2c_addr_write: I2C device address (write bit cleared)
        cmd: Command byte(s)
        cmd_len: 1 or 2 bytes
        data_len: Number of bytes to read
    
    Packet: [0x53][addr][cmd_len][cmd...][0x53][addr|0x01][data_len][0x50]
    Returns: Bytes read from device
    """
    pass

def pca9555_read_port(addr, port):
    """Read PCA9555 port register (0x00-0x07)."""
    return i2c_read_one(addr, port, 1)

def pca9555_write_port(addr, port, value):
    """Write PCA9555 port register."""
    nxp_write(addr, port, value, 1)

def pca9555_set_bit(addr, port, bitmask, state):
    """
    Set or clear specific bit on PCA9555.
    Reads current value, modifies bit, writes back.
    """
    current = pca9555_read_port(addr, 0x02 + port)  # Output port
    if state:
        new_value = current | bitmask
    else:
        new_value = current & ~bitmask
    pca9555_write_port(addr, 0x02 + port, new_value)

def x9119_write_wiper(addr, value):
    """Set X9119 wiper position (0-1023)."""
    if value > 1023:
        raise ValueError("X9119 value must be 0-1023")
    nxp_write(addr, 0xa0, value, 2)

def ads7828_read(addr, channel):
    """Read ADS7828 ADC channel."""
    return i2c_read_discard_high_low(addr, channel, 2)

High-Level Control Functions

def set_current(value):
    """CCMD= - Set laser current (0-1023)."""
    x9119_write_wiper(0x52, value)

def set_power_cmd(value):
    """PCMD= - Set power command (0-1023)."""
    x9119_write_wiper(0x52, value)  # Same device, different context

def set_shutter(state):
    """SHCMD= - Control shutter (True=open, False=closed)."""
    pca9555_set_bit(0x4a, 0, 0x01, state)

def set_keyswitch(state):
    """KSWCMD= - Control keyswitch (True=on, False=off)."""
    pca9555_set_bit(0x4a, 0, 0x20, state)

def set_remote_enable(state):
    """REM= - Enable remote control (True=enabled)."""
    pca9555_set_bit(0x4a, 0, 0x08, state)

def set_analog_enable(state):
    """ANACMD= - Enable analog input (True=enabled)."""
    pca9555_set_bit(0x4a, 0, 0x10, state)

def set_current_mode(state):
    """CMODECMD= - Set current mode (True=enabled)."""
    pca9555_set_bit(0x4a, 0, 0x04, state)

def get_current_actual():
    """Read actual current from ADC."""
    raw = ads7828_read(0x90, 0x84)
    # TODO: Scale based on AmpsFullscale
    return raw * 0.000244140625  # Placeholder scaling

def get_main_temp():
    """Read main crystal temperature."""
    return ads7828_read(0x90, 0x94)

def get_interlock_status():
    """Check interlock status."""
    value = pca9555_read_port(0x48, 0x00)  # Input port 0
    return bool(value & 0x01)

def get_ldd_enable():
    """Check if laser diode driver is enabled."""
    value = pca9555_read_port(0x40, 0x00)
    return not bool(value & 0x01)  # Inverted logic

GUI Structure

Tab 1: Basic Controls

  • Current Control

    • Slider: 0-1023
    • Spinbox: Numeric input
    • Label: Percentage (0-100%)
    • Button: Set to 0 (quick disable)
  • Power Command

    • Slider: 0-1023
    • Spinbox: Numeric input
  • Digital Controls (Toggle Buttons)

    • Shutter (Open/Closed) - Red when open
    • Keyswitch (On/Off)
    • Remote Enable (On/Off)
    • Analog Input (On/Off)
    • Current Mode (On/Off)
  • Emergency Stop

    • Large red button
    • Always enabled (even when disconnected)
    • Actions: Close shutter, current=0, keyswitch=off
  • Status Display

    • Actual current reading (live)
    • Connection status indicator

Tab 2: Monitoring

  • Real-Time Readings (auto-refresh 500ms)

    • Current Actual: [value] A
    • Main Temperature: [value] °C
    • SHG Temperature: [value] °C
    • Interlock Status: [OK/FAULT] (colored indicator)
    • LDD Enable: [ON/OFF]
  • System Info (read once on connect)

    • Laser Model: [from EEPROM]
    • Wavelength: [value] nm
    • Serial Number: [from EEPROM]
  • Controls

    • Refresh Rate slider (100ms - 2000ms)
    • Enable/Disable auto-refresh checkbox
    • Manual refresh button

Tab 3: Advanced

  • Raw I2C Interface

    • Device Address (hex): [input]
    • Command (hex): [input]
    • Data (hex): [input]
    • Data Length: [dropdown: 1/2/4 bytes]
    • Buttons: [Write] [Read]
    • Response display (hex)
  • Packet Monitor

    • Scrolling log of all TX/RX
    • Format: [HH:MM:SS.mmm] TX: 53 52 02 a0 01 00 50
    • Clear button
    • Export to file button
  • EEPROM Tools

    • Warning label (red): "Caution: Can damage laser!"
    • Read Location (hex): [input]
    • Read Length: [input]
    • [Read] button
    • Data display
    • Write protection checkbox

Tab 4: Configuration

  • Serial Port

    • Dropdown: Auto-detect available ports
    • Baud Rate: [dropdown: 9600 default]
    • Data Bits: 8 (fixed)
    • Parity: None (fixed)
    • Stop Bits: 1 (fixed)
  • Connection

    • [Connect]/[Disconnect] button (toggle)
    • DTR checkbox
    • RTS checkbox
    • Status: [Connected/Disconnected]
  • Debug Options

    • Show raw sensor values (before filtering) checkbox
    • Packet timeout (ms): [input]
    • Retry count: [input]

Safety Features

Pre-Flight Checks

Before opening shutter, verify:

  1. Remote enable is ON
  2. Keyswitch is ON
  3. Interlock is OK
  4. Current is set to safe value
  5. Show confirmation dialog

Automatic Safety

  • All controls disabled until serial connected
  • Emergency stop always enabled
  • Auto-disable on serial errors
  • Auto-close shutter on disconnect
  • Set current to 0 on disconnect
  • Watchdog timer - if no commands for 5 seconds, safe state

Error Handling

  • Catch all serial exceptions
  • Display errors in status bar
  • Log errors with timestamps
  • Retry logic for critical operations
  • Timeout handling (default 1 second)

Code Organization

Class Structure

LaserControlApp (QMainWindow)
├── SerialComm (QObject)
│   ├── Methods: connect(), disconnect(), write_packet(), read_packet()
│   └── Signals: connected, disconnected, error, data_received
│
├── I2CProtocol (QObject)
│   ├── Low-level: nxp_write(), nxp_read()
│   ├── Device-specific: pca9555_*, x9119_*, ads7828_*
│   └── Read strategies: read_one(), read_match_two(), read_discard_high_low()
│
├── LaserControl (QObject)
│   ├── High-level: set_current(), set_shutter(), etc.
│   ├── Monitoring: get_current_actual(), get_temps(), etc.
│   └── Safety: emergency_stop(), safe_state(), pre_flight_check()
│
└── UI Tabs
    ├── BasicControlTab (QWidget)
    ├── MonitoringTab (QWidget)
    ├── AdvancedTab (QWidget)
    └── ConfigTab (QWidget)

Constants (at top of file)

# I2C Addresses (7-bit, shifted)
ADDR_X9119_CURRENT = 0x52
ADDR_PCA9555_PS_DIO = 0x40
ADDR_PCA9555_HEAD_DIO = 0x44
ADDR_PCA9555_PS_GLUE_IN = 0x48
ADDR_PCA9555_PS_GLUE_OUT = 0x4a
ADDR_AD5254 = 0x58
ADDR_ADS7828 = 0x90
ADDR_EEPROM = 0xa4
ADDR_STM32 = 0xae

# PCA9555 Bit Masks (0x14a)
BIT_SHUTTER = 0x01
BIT_CURRENT_MODE = 0x04
BIT_REMOTE_ENABLE = 0x08
BIT_ANALOG_ENABLE = 0x10
BIT_KEYSWITCH = 0x20

# X9119 Commands
CMD_X9119_WRITE_WIPER = 0xa0

# ADS7828 Channels
CHAN_CURRENT_ACTUAL = 0x84
CHAN_PHOTO_ACTUAL = 0xe4
CHAN_MAIN_TEMP = 0x94

# Serial Protocol
NXP_START_BYTE = 0x53
NXP_STOP_BYTE = 0x50

Styling

Use QSS for professional appearance:

STYLESHEET = """
QMainWindow {
    background-color: #2b2b2b;
}

QPushButton {
    background-color: #3c3c3c;
    color: white;
    border: 1px solid #555;
    padding: 5px;
    border-radius: 3px;
}

QPushButton:hover {
    background-color: #4c4c4c;
}

QPushButton:pressed {
    background-color: #2c2c2c;
}

QPushButton#emergency {
    background-color: #cc0000;
    font-weight: bold;
    font-size: 14pt;
}

QPushButton#emergency:hover {
    background-color: #ff0000;
}

QLabel#status_ok {
    color: #00ff00;
    font-weight: bold;
}

QLabel#status_warning {
    color: #ffaa00;
    font-weight: bold;
}

QLabel#status_error {
    color: #ff0000;
    font-weight: bold;
}

QSlider::groove:horizontal {
    background: #3c3c3c;
    height: 8px;
}

QSlider::handle:horizontal {
    background: #5c5c5c;
    width: 16px;
    margin: -4px 0;
    border-radius: 8px;
}
"""

Testing Checklist

Before Monday demo:

  • Serial connection/disconnection works
  • Current slider sets X9119 correctly
  • Shutter opens/closes (verify with read-back)
  • Emergency stop works from any state
  • Sensor readings update (current, temp)
  • Error handling doesn't crash app
  • All safety interlocks functional
  • Confirm dialog before shutter open
  • Auto-disable on disconnect
  • Packet monitor shows correct data

README Section for Top of File

"""
Genesis SLM MX 532 Laser Control Application
=============================================

This application controls a Genesis SLM MX 532 laser via I2C-over-serial protocol.

PROTOCOL OVERVIEW:
- Serial: /dev/ttyUSB0 @ 9600 8N1
- Protocol: NXP I2C tunneled over serial
- Packet format: [0x53][addr][len][cmd][data][0x50]

SAFETY WARNINGS:
- Always close shutter before adjusting current
- Verify interlock status before opening shutter
- Use emergency stop if anything looks wrong
- Never bypass safety interlocks

USAGE:
1. Connect serial port in Configuration tab
2. Enable Remote and Keyswitch
3. Set current to desired level
4. Open shutter (with confirmation)
5. Monitor temperature and current
6. Close shutter when done

For more information, see protocol documentation.

Author: [Your Name]
Date: [Date]
License: [License]
"""

Notes for Implementation

  • All I2C addresses are 7-bit and need write bit cleared (& 0xFE) for writes
  • Read addresses are write address | 0x01
  • Multi-byte data is big-endian
  • Always validate inputs before sending to hardware
  • Use type hints throughout
  • Add docstrings to all functions
  • Log all I2C transactions for debugging
  • Include unit tests for packet construction
  • Make sure emergency stop can interrupt any operation

CRITICAL: Test thoroughly before Monday! The laser is expensive - safety first!