15 KiB
15 KiB
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:
- Remote enable is ON
- Keyswitch is ON
- Interlock is OK
- Current is set to safe value
- 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!