555 lines
15 KiB
Markdown
Executable File
555 lines
15 KiB
Markdown
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
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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)
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
"""
|
|
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!
|