67aabde4b6
- uc480_camera: drop never-called _capture_paused/get_framerate (the hardware question _capture_paused encoded is now in KNOWN_ISSUES.md) - t3r_protocol: drop read_reg/write_reg/decode_reg/Reg (commands never wired into the driver) - bbd20x: drop _update0x0212 (never dispatched) and 8 of 9 unused trigger convenience wrappers; apt_constants: drop TriggerBitsStepper (servo-only rig) - ruff --fix: 35 unused imports across all apps; drop unused T3R_BAUD - genesis_core.py: quarantine warning header; docs/genesis_verification.md bench checklist for the 7 divergences vs tools/genesis_laser_gui.py Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
679 lines
20 KiB
Python
Executable File
679 lines
20 KiB
Python
Executable File
"""
|
|
Genesis SLM MX 532 Laser Core Hardware Control Module
|
|
======================================================
|
|
|
|
.. warning::
|
|
QUARANTINED — do not modify semantics or dedupe against
|
|
``tools/genesis_laser_gui.py`` until the bench checklist in
|
|
``docs/genesis_verification.md`` has been run. This module was
|
|
extracted from that GUI but diverges from it in ways only hardware can
|
|
adjudicate: ADS7828 command byte (0x84 vs 0x8C), LDD enable polarity
|
|
(inverted vs not), shutter semantics (manual vs bit-controlled),
|
|
dropped median-of-3 ADC filtering, dropped Amps/Watts scaling, dropped
|
|
temperature reads and pre-flight check.
|
|
|
|
This module provides low-level hardware control for the Genesis SLM MX 532 laser
|
|
using NXP I2C-over-serial protocol. It contains reusable classes for serial
|
|
communication, I2C protocol handling, device control, and laser operations.
|
|
|
|
This module is GUI-independent and can be used by both command-line and GUI applications.
|
|
|
|
Protocol Overview:
|
|
-----------------
|
|
The laser uses NXP I2C-over-serial protocol with packet format:
|
|
[0x53] [I2C_ADDR] [LENGTH] [COMMAND_BYTES] [DATA_BYTES] [0x50]
|
|
|
|
For reads:
|
|
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
|
|
|
I2C Devices:
|
|
-----------
|
|
- X9119 digital potentiometer at 0x52 - laser current control
|
|
- PCA9555 I/O expander at 0x4a - digital I/O
|
|
- AD5254 digital potentiometer at 0x58 - limits control
|
|
- ADS7828 ADC at 0x90 - sensor readings
|
|
- M24C64 EEPROM at 0xa4 - configuration storage
|
|
|
|
Author: Claude
|
|
Date: 2026-01-24
|
|
"""
|
|
|
|
import time
|
|
from datetime import datetime
|
|
from typing import Optional, List
|
|
from enum import IntEnum
|
|
|
|
import serial
|
|
|
|
|
|
# ============================================================================
|
|
# Constants - I2C Addresses
|
|
# ============================================================================
|
|
|
|
class I2CAddress(IntEnum):
|
|
"""I2C device addresses (7-bit addresses shifted left by 1)"""
|
|
# X9119 Digital Potentiometer (current control)
|
|
X9119_CURRENT_WRITE = 0x52
|
|
X9119_CURRENT_READ = 0x53
|
|
|
|
# PCA9555 I/O Expanders
|
|
PCA9555_DIO_WRITE = 0x4a # Main control I/O at 0x14a
|
|
PCA9555_DIO_READ = 0x4b
|
|
PCA9555_PSGLUE_WRITE = 0x48 # Power supply glue at 0x148
|
|
PCA9555_PSGLUE_READ = 0x49
|
|
PCA9555_HEAD_WRITE = 0x44 # Head DIO at 0x144
|
|
PCA9555_HEAD_READ = 0x45
|
|
PCA9555_LDD_WRITE = 0x40 # LDD control at 0x140
|
|
PCA9555_LDD_READ = 0x41
|
|
|
|
# AD5254 Digital Potentiometer (limits)
|
|
AD5254_WRITE = 0x58
|
|
|
|
# ADS7828 ADC (sensor readings)
|
|
ADS7828_WRITE = 0x90
|
|
ADS7828_READ = 0x91
|
|
|
|
# M24C64 EEPROM (configuration)
|
|
EEPROM_WRITE = 0xa4
|
|
EEPROM_READ = 0xa5
|
|
|
|
|
|
# ============================================================================
|
|
# Constants - PCA9555 Register Addresses
|
|
# ============================================================================
|
|
|
|
class PCA9555Register(IntEnum):
|
|
"""PCA9555 I/O expander register addresses"""
|
|
INPUT_PORT_0 = 0x00
|
|
INPUT_PORT_1 = 0x01
|
|
OUTPUT_PORT_0 = 0x02
|
|
OUTPUT_PORT_1 = 0x03
|
|
POLARITY_INV_0 = 0x04
|
|
POLARITY_INV_1 = 0x05
|
|
CONFIG_PORT_0 = 0x06
|
|
CONFIG_PORT_1 = 0x07
|
|
|
|
|
|
# ============================================================================
|
|
# Constants - Control Bitmasks
|
|
# ============================================================================
|
|
|
|
class ControlBitmask(IntEnum):
|
|
"""Bitmasks for PCA9555 port 0 control bits"""
|
|
SHUTTER = 0x01 # Bit 0
|
|
CURRENT_MODE = 0x04 # Bit 2
|
|
REMOTE_ENABLE = 0x08 # Bit 3
|
|
ANALOG_ENABLE = 0x10 # Bit 4
|
|
KEYSWITCH = 0x20 # Bit 5
|
|
INTERLOCK = 0x01 # Bit 0 (on different PCA9555)
|
|
LDD_ENABLE = 0x01 # Bit 0 (on LDD PCA9555)
|
|
|
|
|
|
# ============================================================================
|
|
# Serial Communication Layer
|
|
# ============================================================================
|
|
|
|
class SerialComm:
|
|
"""Handles low-level serial port communication"""
|
|
|
|
def __init__(self):
|
|
self.port: Optional[serial.Serial] = None
|
|
self.port_name: str = "/dev/ttyUSB0"
|
|
self.baudrate: int = 9600
|
|
self.timeout: float = 1.0
|
|
self.packet_log: List[str] = []
|
|
|
|
def connect(self, port_name: str, baudrate: int = 9600) -> bool:
|
|
"""
|
|
Connect to serial port
|
|
|
|
Args:
|
|
port_name: Serial port device name
|
|
baudrate: Baud rate (default 9600)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
try:
|
|
self.port_name = port_name
|
|
self.baudrate = baudrate
|
|
self.port = serial.Serial(
|
|
port=port_name,
|
|
baudrate=baudrate,
|
|
bytesize=serial.EIGHTBITS,
|
|
parity=serial.PARITY_NONE,
|
|
stopbits=serial.STOPBITS_ONE,
|
|
timeout=self.timeout
|
|
)
|
|
time.sleep(0.1) # Allow port to stabilize
|
|
return True
|
|
except Exception as e:
|
|
print(f"Serial connection error: {e}")
|
|
return False
|
|
|
|
def disconnect(self):
|
|
"""Disconnect from serial port"""
|
|
if self.port and self.port.is_open:
|
|
self.port.close()
|
|
self.port = None
|
|
|
|
def is_connected(self) -> bool:
|
|
"""Check if serial port is connected"""
|
|
return self.port is not None and self.port.is_open
|
|
|
|
def write(self, data: bytes) -> bool:
|
|
"""
|
|
Write data to serial port
|
|
|
|
Args:
|
|
data: Bytes to write
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
if not self.is_connected():
|
|
return False
|
|
|
|
try:
|
|
self.port.write(data)
|
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
hex_str = " ".join(f"{b:02x}" for b in data)
|
|
self.packet_log.append(f"[{timestamp}] TX: {hex_str}")
|
|
return True
|
|
except Exception as e:
|
|
print(f"Serial write error: {e}")
|
|
return False
|
|
|
|
def read(self, size: int) -> Optional[bytes]:
|
|
"""
|
|
Read data from serial port
|
|
|
|
Args:
|
|
size: Number of bytes to read
|
|
|
|
Returns:
|
|
Bytes read or None on error
|
|
"""
|
|
if not self.is_connected():
|
|
return None
|
|
|
|
try:
|
|
data = self.port.read(size)
|
|
if data:
|
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
hex_str = " ".join(f"{b:02x}" for b in data)
|
|
self.packet_log.append(f"[{timestamp}] RX: {hex_str}")
|
|
return data
|
|
except Exception as e:
|
|
print(f"Serial read error: {e}")
|
|
return None
|
|
|
|
def flush(self):
|
|
"""Flush serial port buffers"""
|
|
if self.is_connected():
|
|
self.port.reset_input_buffer()
|
|
self.port.reset_output_buffer()
|
|
|
|
def get_packet_log(self, last_n: int = 100) -> List[str]:
|
|
"""Get last N entries from packet log"""
|
|
return self.packet_log[-last_n:]
|
|
|
|
def clear_packet_log(self):
|
|
"""Clear packet log"""
|
|
self.packet_log.clear()
|
|
|
|
|
|
# ============================================================================
|
|
# I2C Protocol Layer
|
|
# ============================================================================
|
|
|
|
class I2CProtocol:
|
|
"""Handles NXP I2C-over-serial protocol packet construction"""
|
|
|
|
NXP_START = 0x53
|
|
NXP_STOP = 0x50
|
|
|
|
def __init__(self, serial_comm: SerialComm):
|
|
self.serial = serial_comm
|
|
|
|
def write(self, i2c_addr_write: int, cmd: bytes, data: bytes = b'') -> bool:
|
|
"""
|
|
Build and send NXP I2C write packet
|
|
|
|
Packet format: [0x53] [I2C_ADDR] [LENGTH] [COMMAND] [DATA] [0x50]
|
|
|
|
Args:
|
|
i2c_addr_write: I2C write address
|
|
cmd: Command bytes
|
|
data: Data bytes (optional)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
if not isinstance(cmd, bytes):
|
|
cmd = bytes([cmd])
|
|
if not isinstance(data, bytes):
|
|
data = bytes(data)
|
|
|
|
length = len(cmd) + len(data)
|
|
packet = bytes([self.NXP_START, i2c_addr_write, length]) + cmd + data + bytes([self.NXP_STOP])
|
|
|
|
return self.serial.write(packet)
|
|
|
|
def read(self, i2c_addr_write: int, cmd: bytes, cmd_len: int, data_len: int) -> Optional[bytes]:
|
|
"""
|
|
Build and send NXP I2C read packet, return data
|
|
|
|
Packet format for read:
|
|
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
|
|
|
Args:
|
|
i2c_addr_write: I2C write address
|
|
cmd: Command bytes
|
|
cmd_len: Length of command
|
|
data_len: Expected data length to read
|
|
|
|
Returns:
|
|
Data bytes read or None on error
|
|
"""
|
|
if not isinstance(cmd, bytes):
|
|
cmd = bytes([cmd])
|
|
|
|
i2c_addr_read = i2c_addr_write | 0x01 # Set read bit
|
|
|
|
# First part: write command
|
|
packet_write = bytes([self.NXP_START, i2c_addr_write, cmd_len]) + cmd
|
|
|
|
# Second part: read data
|
|
packet_read = bytes([self.NXP_START, i2c_addr_read, data_len, self.NXP_STOP])
|
|
|
|
packet = packet_write + packet_read
|
|
|
|
if not self.serial.write(packet):
|
|
return None
|
|
|
|
# Read response
|
|
# Expected response: [data_bytes]
|
|
time.sleep(0.05) # Give device time to respond
|
|
response = self.serial.read(data_len)
|
|
|
|
return response if response and len(response) == data_len else None
|
|
|
|
|
|
# ============================================================================
|
|
# I2C Device Functions
|
|
# ============================================================================
|
|
|
|
class I2CDevices:
|
|
"""High-level I2C device control functions"""
|
|
|
|
def __init__(self, protocol: I2CProtocol):
|
|
self.protocol = protocol
|
|
|
|
# ------------------------------------------------------------------------
|
|
# PCA9555 I/O Expander Functions
|
|
# ------------------------------------------------------------------------
|
|
|
|
def pca9555_read_port(self, addr_write: int, port: int) -> Optional[int]:
|
|
"""
|
|
Read PCA9555 port register
|
|
|
|
Args:
|
|
addr_write: I2C write address
|
|
port: Register address (0-7)
|
|
|
|
Returns:
|
|
Port value (0-255) or None on error
|
|
"""
|
|
data = self.protocol.read(addr_write, bytes([port]), 1, 1)
|
|
return data[0] if data else None
|
|
|
|
def pca9555_write_port(self, addr_write: int, port: int, value: int) -> bool:
|
|
"""
|
|
Write PCA9555 port register
|
|
|
|
Args:
|
|
addr_write: I2C write address
|
|
port: Register address (0-7)
|
|
value: Value to write (0-255)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
return self.protocol.write(addr_write, bytes([port]), bytes([value]))
|
|
|
|
def pca9555_set_bit(self, addr_write: int, port: int, bitmask: int, state: bool) -> bool:
|
|
"""
|
|
Set or clear specific bit on PCA9555
|
|
|
|
Args:
|
|
addr_write: I2C write address
|
|
port: Register address
|
|
bitmask: Bit mask (e.g., 0x01 for bit 0)
|
|
state: True to set bit, False to clear
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
# Read current value
|
|
current = self.pca9555_read_port(addr_write, port)
|
|
if current is None:
|
|
return False
|
|
|
|
# Modify bit
|
|
if state:
|
|
new_value = current | bitmask
|
|
else:
|
|
new_value = current & ~bitmask
|
|
|
|
# Write back
|
|
return self.pca9555_write_port(addr_write, port, new_value)
|
|
|
|
# ------------------------------------------------------------------------
|
|
# X9119 Digital Potentiometer Functions
|
|
# ------------------------------------------------------------------------
|
|
|
|
def x9119_write_wiper(self, addr_write: int, value: int) -> bool:
|
|
"""
|
|
Set X9119 wiper position
|
|
|
|
Args:
|
|
addr_write: I2C write address
|
|
value: Wiper position (0-1023, 10-bit)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
# X9119 write wiper command: 0xa0 followed by 2 bytes (10-bit value)
|
|
# Value is split into two bytes: MSB contains upper 2 bits, LSB contains lower 8 bits
|
|
value = max(0, min(1023, value)) # Clamp to valid range
|
|
|
|
msb = (value >> 8) & 0x03 # Upper 2 bits
|
|
lsb = value & 0xFF # Lower 8 bits
|
|
|
|
cmd = bytes([0xa0])
|
|
data = bytes([msb, lsb])
|
|
|
|
return self.protocol.write(addr_write, cmd, data)
|
|
|
|
# ------------------------------------------------------------------------
|
|
# ADS7828 ADC Functions
|
|
# ------------------------------------------------------------------------
|
|
|
|
def ads7828_read(self, addr_write: int, channel: int) -> Optional[int]:
|
|
"""
|
|
Read ADS7828 ADC channel
|
|
|
|
Args:
|
|
addr_write: I2C write address
|
|
channel: Channel to read (0-7)
|
|
|
|
Returns:
|
|
12-bit ADC value (0-4095) or None on error
|
|
"""
|
|
# ADS7828 command byte format:
|
|
# Bit 7: SD (single-ended=1)
|
|
# Bits 6-4: Channel select
|
|
# Bit 3: PD1 (power-down mode)
|
|
# Bit 2: PD0 (power-down mode)
|
|
# Bits 1-0: Don't care
|
|
|
|
# For single-ended read with internal reference on
|
|
cmd_byte = 0x80 | ((channel & 0x07) << 4) | 0x0c
|
|
|
|
data = self.protocol.read(addr_write, bytes([cmd_byte]), 1, 2)
|
|
if not data or len(data) != 2:
|
|
return None
|
|
|
|
# Combine two bytes into 12-bit value
|
|
value = (data[0] << 8) | data[1]
|
|
value = (value >> 4) & 0x0FFF # Extract 12-bit value
|
|
|
|
return value
|
|
|
|
|
|
# ============================================================================
|
|
# Laser Control Layer
|
|
# ============================================================================
|
|
|
|
class LaserControl:
|
|
"""High-level laser control functions"""
|
|
|
|
def __init__(self, devices: I2CDevices):
|
|
self.devices = devices
|
|
self._safe_state_active = False
|
|
|
|
# ------------------------------------------------------------------------
|
|
# SET Commands
|
|
# ------------------------------------------------------------------------
|
|
|
|
def set_current(self, value: int) -> bool:
|
|
"""
|
|
Set laser current command (CCMD=)
|
|
|
|
Args:
|
|
value: Current value (0-1023)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
return self.devices.x9119_write_wiper(I2CAddress.X9119_CURRENT_WRITE, value)
|
|
|
|
def set_power_cmd(self, value: int) -> bool:
|
|
"""
|
|
Set power command (PCMD=)
|
|
|
|
Note: Uses same device as current control, different channel
|
|
|
|
Args:
|
|
value: Power value (0-1023)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
return self.devices.x9119_write_wiper(I2CAddress.X9119_CURRENT_WRITE, value)
|
|
|
|
def set_shutter(self, state: bool) -> bool:
|
|
"""
|
|
Set shutter state (SHCMD=)
|
|
|
|
Args:
|
|
state: True for open, False for closed
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
return self.devices.pca9555_set_bit(
|
|
I2CAddress.PCA9555_DIO_WRITE,
|
|
PCA9555Register.OUTPUT_PORT_0,
|
|
ControlBitmask.SHUTTER,
|
|
state
|
|
)
|
|
|
|
def set_keyswitch(self, state: bool) -> bool:
|
|
"""
|
|
Set keyswitch state (KSWCMD=)
|
|
|
|
Args:
|
|
state: True for on, False for off
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
return self.devices.pca9555_set_bit(
|
|
I2CAddress.PCA9555_DIO_WRITE,
|
|
PCA9555Register.OUTPUT_PORT_0,
|
|
ControlBitmask.KEYSWITCH,
|
|
state
|
|
)
|
|
|
|
def set_current_mode(self, state: bool) -> bool:
|
|
"""
|
|
Set current mode (CMODECMD=)
|
|
|
|
Args:
|
|
state: True for on, False for off
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
return self.devices.pca9555_set_bit(
|
|
I2CAddress.PCA9555_DIO_WRITE,
|
|
PCA9555Register.OUTPUT_PORT_0,
|
|
ControlBitmask.CURRENT_MODE,
|
|
state
|
|
)
|
|
|
|
def set_analog_enable(self, state: bool) -> bool:
|
|
"""
|
|
Set analog input enable (ANACMD=)
|
|
|
|
Args:
|
|
state: True for enabled, False for disabled
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
return self.devices.pca9555_set_bit(
|
|
I2CAddress.PCA9555_DIO_WRITE,
|
|
PCA9555Register.OUTPUT_PORT_0,
|
|
ControlBitmask.ANALOG_ENABLE,
|
|
state
|
|
)
|
|
|
|
def set_remote_enable(self, state: bool) -> bool:
|
|
"""
|
|
Set remote enable (REM=)
|
|
|
|
Args:
|
|
state: True for enabled, False for disabled
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
return self.devices.pca9555_set_bit(
|
|
I2CAddress.PCA9555_DIO_WRITE,
|
|
PCA9555Register.OUTPUT_PORT_0,
|
|
ControlBitmask.REMOTE_ENABLE,
|
|
state
|
|
)
|
|
|
|
# ------------------------------------------------------------------------
|
|
# GET/Query Commands
|
|
# ------------------------------------------------------------------------
|
|
|
|
def get_current_actual(self) -> Optional[float]:
|
|
"""
|
|
Get actual current reading
|
|
|
|
Returns:
|
|
Current in arbitrary units (0-4095) or None on error
|
|
"""
|
|
# Read from ADS7828, channel based on command 0x84
|
|
# Command 0x84 suggests channel 0
|
|
value = self.devices.ads7828_read(I2CAddress.ADS7828_WRITE, 0)
|
|
return value if value is not None else None
|
|
|
|
def get_interlock_status(self) -> Optional[bool]:
|
|
"""
|
|
Get interlock status
|
|
|
|
Returns:
|
|
True if interlock OK, False if fault, None on error
|
|
"""
|
|
value = self.devices.pca9555_read_port(
|
|
I2CAddress.PCA9555_PSGLUE_WRITE,
|
|
PCA9555Register.INPUT_PORT_0
|
|
)
|
|
if value is None:
|
|
return None
|
|
|
|
# Bit 0 indicates interlock status
|
|
return bool(value & ControlBitmask.INTERLOCK)
|
|
|
|
def get_ldd_enable_status(self) -> Optional[bool]:
|
|
"""
|
|
Get laser diode driver enable status
|
|
|
|
Returns:
|
|
True if enabled, False if disabled, None on error
|
|
"""
|
|
value = self.devices.pca9555_read_port(
|
|
I2CAddress.PCA9555_LDD_WRITE,
|
|
PCA9555Register.INPUT_PORT_0
|
|
)
|
|
if value is None:
|
|
return None
|
|
|
|
return bool(value & ControlBitmask.LDD_ENABLE)
|
|
|
|
def get_psglue_in_status(self) -> Optional[int]:
|
|
"""
|
|
Get power supply glue input status
|
|
|
|
Returns:
|
|
Port value or None on error
|
|
"""
|
|
return self.devices.pca9555_read_port(
|
|
I2CAddress.PCA9555_PSGLUE_WRITE,
|
|
PCA9555Register.INPUT_PORT_0
|
|
)
|
|
|
|
def get_psglue_out_status(self) -> Optional[int]:
|
|
"""
|
|
Get power supply glue output status
|
|
|
|
Returns:
|
|
Port value or None on error
|
|
"""
|
|
return self.devices.pca9555_read_port(
|
|
I2CAddress.PCA9555_DIO_WRITE,
|
|
PCA9555Register.OUTPUT_PORT_0
|
|
)
|
|
|
|
def get_head_dio_status(self) -> Optional[int]:
|
|
"""
|
|
Get head DIO status
|
|
|
|
Returns:
|
|
Port value or None on error
|
|
"""
|
|
return self.devices.pca9555_read_port(
|
|
I2CAddress.PCA9555_HEAD_WRITE,
|
|
PCA9555Register.INPUT_PORT_0
|
|
)
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Safety Functions
|
|
# ------------------------------------------------------------------------
|
|
|
|
def emergency_stop(self) -> bool:
|
|
"""
|
|
Emergency stop: close shutter, set current to 0, disable keyswitch
|
|
|
|
Returns:
|
|
True if all operations successful, False otherwise
|
|
"""
|
|
success = True
|
|
success &= self.set_shutter(False)
|
|
success &= self.set_current(0)
|
|
success &= self.set_keyswitch(False)
|
|
self._safe_state_active = True
|
|
return success
|
|
|
|
def enter_safe_state(self) -> bool:
|
|
"""
|
|
Enter safe state (similar to emergency stop but also disables remote)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
success = True
|
|
success &= self.set_shutter(False)
|
|
success &= self.set_current(0)
|
|
success &= self.set_power_cmd(0)
|
|
success &= self.set_keyswitch(False)
|
|
success &= self.set_remote_enable(False)
|
|
self._safe_state_active = True
|
|
return success
|