feat: MageDok 7in display setup for Orin (Issue #369) #373
@ -0,0 +1,20 @@
|
|||||||
|
# PulseAudio Configuration for MageDok HDMI Audio
|
||||||
|
# Routes HDMI audio from DisplayPort adapter to internal speaker output
|
||||||
|
|
||||||
|
# Detect and load HDMI output module
|
||||||
|
load-module module-alsa-sink device=hw:0,3 sink_name=hdmi_stereo sink_properties="device.description='HDMI Audio'"
|
||||||
|
|
||||||
|
# Detect and configure internal speaker (fallback)
|
||||||
|
load-module module-alsa-sink device=hw:0,0 sink_name=speaker_mono sink_properties="device.description='Speaker'"
|
||||||
|
|
||||||
|
# Set HDMI as default output sink
|
||||||
|
set-default-sink hdmi_stereo
|
||||||
|
|
||||||
|
# Enable volume control
|
||||||
|
load-module module-volume-restore
|
||||||
|
|
||||||
|
# Auto-switch to HDMI when connected
|
||||||
|
load-module module-switch-on-connect
|
||||||
|
|
||||||
|
# Log sink configuration
|
||||||
|
.load-if-exists /etc/pulse/magedok-routing.conf
|
||||||
33
jetson/ros2_ws/src/saltybot_bringup/config/xorg-magedok.conf
Normal file
33
jetson/ros2_ws/src/saltybot_bringup/config/xorg-magedok.conf
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# X11 Configuration for MageDok 7" Display
|
||||||
|
# Resolution: 1024×600 @ 60Hz
|
||||||
|
# Output: HDMI via DisplayPort adapter
|
||||||
|
|
||||||
|
Section "Monitor"
|
||||||
|
Identifier "MageDok"
|
||||||
|
Option "PreferredMode" "1024x600_60.00"
|
||||||
|
Option "Position" "0 0"
|
||||||
|
Option "Primary" "true"
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Screen"
|
||||||
|
Identifier "Screen0"
|
||||||
|
Monitor "MageDok"
|
||||||
|
DefaultDepth 24
|
||||||
|
SubSection "Display"
|
||||||
|
Depth 24
|
||||||
|
Modes "1024x600" "1024x768" "800x600" "640x480"
|
||||||
|
EndSubSection
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Device"
|
||||||
|
Identifier "NVIDIA Tegra"
|
||||||
|
Driver "nvidia"
|
||||||
|
BusID "PCI:0:0:0"
|
||||||
|
Option "RegistryDwords" "EnableBrightnessControl=1"
|
||||||
|
Option "ConnectedMonitor" "HDMI-0"
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "ServerLayout"
|
||||||
|
Identifier "Default"
|
||||||
|
Screen "Screen0"
|
||||||
|
EndSection
|
||||||
@ -0,0 +1,218 @@
|
|||||||
|
# MageDok 7" Touchscreen Display Setup
|
||||||
|
|
||||||
|
Issue #369: Display setup for MageDok 7" IPS touchscreen on Jetson Orin Nano.
|
||||||
|
|
||||||
|
## Hardware Setup
|
||||||
|
|
||||||
|
### Connections
|
||||||
|
- **Video**: DisplayPort → HDMI cable from Orin DP 1.2 connector to MageDok HDMI input
|
||||||
|
- **Touch**: USB 3.0 cable from Orin USB-A to MageDok USB-C connector
|
||||||
|
- **Audio**: HDMI carries embedded audio from DisplayPort (no separate audio cable needed)
|
||||||
|
|
||||||
|
### Display Specs
|
||||||
|
- **Resolution**: 1024×600 @ 60Hz
|
||||||
|
- **Panel Type**: 7" IPS (In-Plane Switching) - wide viewing angles
|
||||||
|
- **Sunlight Readable**: Yes, with high brightness
|
||||||
|
- **Built-in Speakers**: Yes (via HDMI audio)
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### 1. Kernel and Display Driver Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update display mode database (if needed)
|
||||||
|
sudo apt-get update && sudo apt-get install -y xrandr x11-utils edid-decode
|
||||||
|
|
||||||
|
# Verify X11 is running
|
||||||
|
echo $DISPLAY # Should show :0 or :1
|
||||||
|
|
||||||
|
# Check connected displays
|
||||||
|
xrandr --query
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output**: HDMI-1 connected at 1024x600 resolution
|
||||||
|
|
||||||
|
### 2. Install udev Rules for Touch Input
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy udev rules
|
||||||
|
sudo cp jetson/ros2_ws/src/saltybot_bringup/udev/90-magedok-touch.rules \
|
||||||
|
/etc/udev/rules.d/
|
||||||
|
|
||||||
|
# Reload udev
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger
|
||||||
|
|
||||||
|
# Verify touch device
|
||||||
|
ls -l /dev/magedok-touch
|
||||||
|
# Or check input devices
|
||||||
|
cat /proc/bus/input/devices | grep -i "eGTouch\|EETI"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. X11 Display Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup original X11 config
|
||||||
|
sudo cp /etc/X11/xorg.conf /etc/X11/xorg.conf.backup
|
||||||
|
|
||||||
|
# Apply MageDok X11 config
|
||||||
|
sudo cp jetson/ros2_ws/src/saltybot_bringup/config/xorg-magedok.conf \
|
||||||
|
/etc/X11/xorg.conf
|
||||||
|
|
||||||
|
# Restart X11 (or reboot)
|
||||||
|
sudo systemctl restart gdm3 # or startx if using console
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. PulseAudio Audio Routing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current audio sinks
|
||||||
|
pactl list sinks | grep Name
|
||||||
|
|
||||||
|
# Find HDMI sink (typically contains "hdmi" in name)
|
||||||
|
pactl set-default-sink <hdmi-sink-name>
|
||||||
|
|
||||||
|
# Verify routing
|
||||||
|
pactl get-default-sink
|
||||||
|
|
||||||
|
# Optional: Set volume
|
||||||
|
pactl set-sink-volume <sink-name> 70%
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. ROS2 Launch Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the saltybot_bringup package
|
||||||
|
cd jetson/ros2_ws
|
||||||
|
colcon build --packages-select saltybot_bringup
|
||||||
|
|
||||||
|
# Source workspace
|
||||||
|
source install/setup.bash
|
||||||
|
|
||||||
|
# Launch display setup
|
||||||
|
ros2 launch saltybot_bringup magedok_display.launch.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Enable Auto-Start on Boot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy systemd service
|
||||||
|
sudo cp jetson/ros2_ws/src/saltybot_bringup/systemd/magedok-display.service \
|
||||||
|
/etc/systemd/system/
|
||||||
|
|
||||||
|
# Enable service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable magedok-display.service
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
sudo systemctl start magedok-display.service
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status magedok-display.service
|
||||||
|
sudo journalctl -u magedok-display -f # Follow logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Display Resolution
|
||||||
|
```bash
|
||||||
|
# Check actual resolution
|
||||||
|
xdotool getactivewindow getwindowgeometry
|
||||||
|
|
||||||
|
# Verify with xrandr
|
||||||
|
xrandr | grep "1024x600"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**: `1024x600_60.00 +0+0` or similar
|
||||||
|
|
||||||
|
### Touch Input
|
||||||
|
```bash
|
||||||
|
# List input devices
|
||||||
|
xinput list
|
||||||
|
|
||||||
|
# Should show "MageDok Touch" or "eGTouch Controller"
|
||||||
|
# Test touch by clicking on display - cursor should move
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audio
|
||||||
|
```bash
|
||||||
|
# Test HDMI audio
|
||||||
|
speaker-test -c 2 -l 1 -s 1 -t sine
|
||||||
|
|
||||||
|
# Verify volume level
|
||||||
|
pactl list sinks | grep -A 10 RUNNING
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Display Not Detected
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check EDID data
|
||||||
|
edid-decode /sys/class/drm/card0-HDMI-A-1/edid
|
||||||
|
|
||||||
|
# Force resolution
|
||||||
|
xrandr --output HDMI-1 --mode 1024x600 --rate 60
|
||||||
|
|
||||||
|
# Check kernel logs
|
||||||
|
dmesg | grep -i "drm\|HDMI\|dp"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Touch Not Working
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check USB connection
|
||||||
|
lsusb | grep -i "eGTouch\|EETI"
|
||||||
|
|
||||||
|
# Verify udev rules applied
|
||||||
|
cat /etc/udev/rules.d/90-magedok-touch.rules
|
||||||
|
|
||||||
|
# Test touch device directly
|
||||||
|
evtest /dev/magedok-touch # Or /dev/input/eventX
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audio Not Routing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check PulseAudio daemon
|
||||||
|
pulseaudio --version
|
||||||
|
systemctl status pulseaudio
|
||||||
|
|
||||||
|
# Restart PulseAudio
|
||||||
|
systemctl --user restart pulseaudio
|
||||||
|
|
||||||
|
# Monitor audio stream
|
||||||
|
pactl list sink-inputs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Display Disconnection (Headless Fallback)
|
||||||
|
|
||||||
|
The system should continue operating normally with display disconnected:
|
||||||
|
- ROS2 services remain accessible via network
|
||||||
|
- Robot commands via `/cmd_vel` continue working
|
||||||
|
- Data logging and telemetry unaffected
|
||||||
|
- Dashboard accessible via SSH/webui from other machine
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Display shows 1024×600 resolution
|
||||||
|
- [ ] Touch input registers in xinput (test by moving cursor)
|
||||||
|
- [ ] Audio plays through display speakers
|
||||||
|
- [ ] System boots without login prompt (if using auto-start)
|
||||||
|
- [ ] All ROS2 nodes launch correctly with display
|
||||||
|
- [ ] System operates normally when display is disconnected
|
||||||
|
- [ ] `/magedok/touch_status` topic shows true (ROS2 verify script)
|
||||||
|
- [ ] `/magedok/audio_status` topic shows HDMI sink (ROS2 audio router)
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
- **#368**: Salty Face UI (depends on this display setup)
|
||||||
|
- **#370**: Animated expression UI
|
||||||
|
- **#371**: Deaf/accessibility mode with touch keyboard
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- MageDok 7" Specs: [HDMI, 1024×600, USB Touch, Built-in Speakers]
|
||||||
|
- Jetson Orin Nano DisplayPort Output: Requires active adapter (no DP Alt Mode on USB-C)
|
||||||
|
- PulseAudio: HDMI audio sink routing via ALSA
|
||||||
|
- X11/Xrandr: Display mode configuration
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MageDok 7" Display Launch Configuration
|
||||||
|
- Video: DisplayPort → HDMI (1024×600)
|
||||||
|
- Touch: USB HID
|
||||||
|
- Audio: HDMI → internal speakers via PulseAudio
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from launch import LaunchDescription
|
||||||
|
from launch_ros.actions import Node
|
||||||
|
from launch.actions import ExecuteProcess
|
||||||
|
|
||||||
|
|
||||||
|
def generate_launch_description():
|
||||||
|
return LaunchDescription([
|
||||||
|
# Log startup
|
||||||
|
ExecuteProcess(
|
||||||
|
cmd=['echo', '[MageDok] Display setup starting...'],
|
||||||
|
shell=True,
|
||||||
|
),
|
||||||
|
|
||||||
|
# Verify display resolution
|
||||||
|
Node(
|
||||||
|
package='saltybot_bringup',
|
||||||
|
executable='verify_display.py',
|
||||||
|
name='display_verifier',
|
||||||
|
parameters=[
|
||||||
|
{'target_width': 1024},
|
||||||
|
{'target_height': 600},
|
||||||
|
{'target_refresh': 60},
|
||||||
|
],
|
||||||
|
output='screen',
|
||||||
|
),
|
||||||
|
|
||||||
|
# Monitor touch input
|
||||||
|
Node(
|
||||||
|
package='saltybot_bringup',
|
||||||
|
executable='touch_monitor.py',
|
||||||
|
name='touch_monitor',
|
||||||
|
parameters=[
|
||||||
|
{'device_name': 'MageDok Touch'},
|
||||||
|
{'poll_interval': 0.1},
|
||||||
|
],
|
||||||
|
output='screen',
|
||||||
|
),
|
||||||
|
|
||||||
|
# Audio routing (PulseAudio sink redirection)
|
||||||
|
Node(
|
||||||
|
package='saltybot_bringup',
|
||||||
|
executable='audio_router.py',
|
||||||
|
name='audio_router',
|
||||||
|
parameters=[
|
||||||
|
{'hdmi_sink': 'alsa_output.pci-0000_00_1d.0.hdmi-stereo'},
|
||||||
|
{'default_sink': True},
|
||||||
|
],
|
||||||
|
output='screen',
|
||||||
|
),
|
||||||
|
])
|
||||||
97
jetson/ros2_ws/src/saltybot_bringup/scripts/audio_router.py
Normal file
97
jetson/ros2_ws/src/saltybot_bringup/scripts/audio_router.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MageDok Audio Router
|
||||||
|
Routes HDMI audio from DisplayPort adapter to internal speakers via PulseAudio
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import rclpy
|
||||||
|
from rclpy.node import Node
|
||||||
|
from std_msgs.msg import String
|
||||||
|
|
||||||
|
|
||||||
|
class AudioRouter(Node):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('audio_router')
|
||||||
|
|
||||||
|
self.declare_parameter('hdmi_sink', 'alsa_output.pci-0000_00_1d.0.hdmi-stereo')
|
||||||
|
self.declare_parameter('default_sink', True)
|
||||||
|
|
||||||
|
self.hdmi_sink = self.get_parameter('hdmi_sink').value
|
||||||
|
self.set_default = self.get_parameter('default_sink').value
|
||||||
|
|
||||||
|
self.audio_status_pub = self.create_publisher(String, '/magedok/audio_status', 10)
|
||||||
|
|
||||||
|
self.get_logger().info('Audio Router: Configuring HDMI audio routing...')
|
||||||
|
self.setup_pulseaudio()
|
||||||
|
|
||||||
|
# Check status every 5 seconds
|
||||||
|
self.create_timer(5.0, self.check_audio_status)
|
||||||
|
|
||||||
|
def setup_pulseaudio(self):
|
||||||
|
"""Configure PulseAudio to route HDMI audio"""
|
||||||
|
try:
|
||||||
|
# List available sinks
|
||||||
|
result = subprocess.run(['pactl', 'list', 'sinks'], capture_output=True, text=True, timeout=5)
|
||||||
|
sinks = self._parse_pa_sinks(result.stdout)
|
||||||
|
|
||||||
|
if not sinks:
|
||||||
|
self.get_logger().warn('No PulseAudio sinks detected')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.get_logger().info(f'Available sinks: {", ".join(sinks.keys())}')
|
||||||
|
|
||||||
|
# Find HDMI or use first available
|
||||||
|
hdmi_sink = None
|
||||||
|
for name in sinks.keys():
|
||||||
|
if 'hdmi' in name.lower() or 'HDMI' in name:
|
||||||
|
hdmi_sink = name
|
||||||
|
break
|
||||||
|
|
||||||
|
if not hdmi_sink:
|
||||||
|
hdmi_sink = list(sinks.keys())[0] # Fallback to first sink
|
||||||
|
self.get_logger().warn(f'HDMI sink not found, using: {hdmi_sink}')
|
||||||
|
else:
|
||||||
|
self.get_logger().info(f'✓ HDMI sink identified: {hdmi_sink}')
|
||||||
|
|
||||||
|
# Set as default if requested
|
||||||
|
if self.set_default:
|
||||||
|
subprocess.run(['pactl', 'set-default-sink', hdmi_sink], timeout=5)
|
||||||
|
self.get_logger().info(f'✓ Audio routed to: {hdmi_sink}')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(f'PulseAudio setup failed: {e}')
|
||||||
|
|
||||||
|
def _parse_pa_sinks(self, pactl_output):
|
||||||
|
"""Parse 'pactl list sinks' output"""
|
||||||
|
sinks = {}
|
||||||
|
current_sink = None
|
||||||
|
for line in pactl_output.split('\n'):
|
||||||
|
if line.startswith('Sink #'):
|
||||||
|
current_sink = line.split('#')[1].strip()
|
||||||
|
elif '\tName: ' in line and current_sink:
|
||||||
|
name = line.split('Name: ')[1].strip()
|
||||||
|
sinks[name] = current_sink
|
||||||
|
return sinks
|
||||||
|
|
||||||
|
def check_audio_status(self):
|
||||||
|
"""Verify audio is properly routed"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['pactl', 'get-default-sink'], capture_output=True, text=True, timeout=5)
|
||||||
|
status = String()
|
||||||
|
status.data = result.stdout.strip()
|
||||||
|
self.audio_status_pub.publish(status)
|
||||||
|
self.get_logger().debug(f'Current audio sink: {status.data}')
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().warn(f'Audio status check failed: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
rclpy.init(args=args)
|
||||||
|
router = AudioRouter()
|
||||||
|
rclpy.spin(router)
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
88
jetson/ros2_ws/src/saltybot_bringup/scripts/touch_monitor.py
Normal file
88
jetson/ros2_ws/src/saltybot_bringup/scripts/touch_monitor.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MageDok Touch Input Monitor
|
||||||
|
Verifies USB touch device is recognized and functional
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import rclpy
|
||||||
|
from rclpy.node import Node
|
||||||
|
from std_msgs.msg import String, Bool
|
||||||
|
|
||||||
|
|
||||||
|
class TouchMonitor(Node):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('touch_monitor')
|
||||||
|
|
||||||
|
self.declare_parameter('device_name', 'MageDok Touch')
|
||||||
|
self.declare_parameter('poll_interval', 0.1)
|
||||||
|
|
||||||
|
self.device_name = self.get_parameter('device_name').value
|
||||||
|
self.poll_interval = self.get_parameter('poll_interval').value
|
||||||
|
|
||||||
|
self.touch_status_pub = self.create_publisher(Bool, '/magedok/touch_status', 10)
|
||||||
|
self.device_info_pub = self.create_publisher(String, '/magedok/device_info', 10)
|
||||||
|
|
||||||
|
self.get_logger().info(f'Touch Monitor: Scanning for {self.device_name}...')
|
||||||
|
self.detect_touch_device()
|
||||||
|
|
||||||
|
# Publish status every 2 seconds
|
||||||
|
self.create_timer(2.0, self.publish_status)
|
||||||
|
|
||||||
|
def detect_touch_device(self):
|
||||||
|
"""Detect MageDok touch device via USB"""
|
||||||
|
try:
|
||||||
|
# Check lsusb for MageDok or eGTouch device
|
||||||
|
result = subprocess.run(['lsusb'], capture_output=True, text=True, timeout=5)
|
||||||
|
lines = result.stdout.split('\n')
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if 'eGTouch' in line or 'EETI' in line or 'MageDok' in line or 'touch' in line.lower():
|
||||||
|
self.get_logger().info(f'✓ Touch device found: {line.strip()}')
|
||||||
|
msg = String()
|
||||||
|
msg.data = line.strip()
|
||||||
|
self.device_info_pub.publish(msg)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Fallback: check input devices
|
||||||
|
result = subprocess.run(['grep', '-l', 'eGTouch\|EETI\|MageDok', '/proc/bus/input/devices'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.get_logger().info('✓ Touch device registered in /proc/bus/input/devices')
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.get_logger().warn('⚠ Touch device not detected — ensure USB connection is secure')
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(f'Device detection failed: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def publish_status(self):
|
||||||
|
"""Publish current touch device status"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['ls', '/dev/magedok-touch'], capture_output=True, timeout=2)
|
||||||
|
status = Bool()
|
||||||
|
status.data = (result.returncode == 0)
|
||||||
|
self.touch_status_pub.publish(status)
|
||||||
|
|
||||||
|
if status.data:
|
||||||
|
self.get_logger().debug('Touch device: ACTIVE')
|
||||||
|
else:
|
||||||
|
self.get_logger().warn('Touch device: NOT DETECTED')
|
||||||
|
except Exception as e:
|
||||||
|
status = Bool()
|
||||||
|
status.data = False
|
||||||
|
self.touch_status_pub.publish(status)
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
rclpy.init(args=args)
|
||||||
|
monitor = TouchMonitor()
|
||||||
|
rclpy.spin(monitor)
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MageDok Display Verifier
|
||||||
|
Validates that the 7" display is running at 1024×600 resolution
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import rclpy
|
||||||
|
from rclpy.node import Node
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayVerifier(Node):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('display_verifier')
|
||||||
|
|
||||||
|
self.declare_parameter('target_width', 1024)
|
||||||
|
self.declare_parameter('target_height', 600)
|
||||||
|
self.declare_parameter('target_refresh', 60)
|
||||||
|
|
||||||
|
self.target_w = self.get_parameter('target_width').value
|
||||||
|
self.target_h = self.get_parameter('target_height').value
|
||||||
|
self.target_f = self.get_parameter('target_refresh').value
|
||||||
|
|
||||||
|
self.get_logger().info(f'Display Verifier: Target {self.target_w}×{self.target_h} @ {self.target_f}Hz')
|
||||||
|
self.verify_display()
|
||||||
|
|
||||||
|
def verify_display(self):
|
||||||
|
"""Check current display resolution via xdotool or xrandr"""
|
||||||
|
try:
|
||||||
|
# Try xrandr first
|
||||||
|
result = subprocess.run(['xrandr'], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.parse_xrandr(result.stdout)
|
||||||
|
else:
|
||||||
|
self.get_logger().warn('xrandr not available, checking edid-decode')
|
||||||
|
self.check_edid()
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(f'Display verification failed: {e}')
|
||||||
|
|
||||||
|
def parse_xrandr(self, output):
|
||||||
|
"""Parse xrandr output to find active display resolution"""
|
||||||
|
lines = output.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
# Look for connected display with resolution
|
||||||
|
if 'connected' in line and 'primary' in line:
|
||||||
|
# Example: "HDMI-1 connected primary 1024x600+0+0 (normal left inverted right)"
|
||||||
|
match = re.search(r'(\d+)x(\d+)', line)
|
||||||
|
if match:
|
||||||
|
width, height = int(match.group(1)), int(match.group(2))
|
||||||
|
self.verify_resolution(width, height)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.get_logger().warn('Could not determine active display from xrandr')
|
||||||
|
|
||||||
|
def verify_resolution(self, current_w, current_h):
|
||||||
|
"""Validate resolution matches target"""
|
||||||
|
if current_w == self.target_w and current_h == self.target_h:
|
||||||
|
self.get_logger().info(f'✓ Display verified: {current_w}×{current_h} [OK]')
|
||||||
|
else:
|
||||||
|
self.get_logger().warn(f'⚠ Display mismatch: Expected {self.target_w}×{self.target_h}, got {current_w}×{current_h}')
|
||||||
|
self.attempt_set_resolution()
|
||||||
|
|
||||||
|
def attempt_set_resolution(self):
|
||||||
|
"""Try to set resolution via xrandr"""
|
||||||
|
try:
|
||||||
|
# Find HDMI output
|
||||||
|
result = subprocess.run(
|
||||||
|
['xrandr', '--output', 'HDMI-1', '--mode', f'{self.target_w}x{self.target_h}', '--rate', str(self.target_f)],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.get_logger().info(f'✓ Resolution set to {self.target_w}×{self.target_h} @ {self.target_f}Hz')
|
||||||
|
else:
|
||||||
|
self.get_logger().warn(f'Resolution change failed: {result.stderr}')
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(f'Could not set resolution: {e}')
|
||||||
|
|
||||||
|
def check_edid(self):
|
||||||
|
"""Fallback: check EDID (Extended Display ID) data"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['edid-decode', '/sys/class/drm/card0-HDMI-A-1/edid'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if 'Established timings' in result.stdout:
|
||||||
|
self.get_logger().info('Display EDID detected (MageDok 1024×600 display)')
|
||||||
|
except:
|
||||||
|
self.get_logger().warn('EDID check unavailable')
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
rclpy.init(args=args)
|
||||||
|
verifier = DisplayVerifier()
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MageDok 7" Display Setup and Auto-Launch
|
||||||
|
Documentation=https://gitea.vayrette.com/seb/saltylab-firmware/issues/369
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
ConditionPathExists=/dev/pts/0
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStartPre=/bin/sleep 2
|
||||||
|
ExecStart=/usr/bin/env bash -c 'source /opt/ros/jazzy/setup.bash && ros2 launch saltybot_bringup magedok_display.launch.py'
|
||||||
|
ExecStartPost=/usr/bin/env bash -c 'DISPLAY=:0 /usr/bin/startx -- :0 vt7 -nolisten tcp 2>/dev/null &'
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=magedok-display
|
||||||
|
User=orin
|
||||||
|
Group=orin
|
||||||
|
Environment="DISPLAY=:0"
|
||||||
|
Environment="XAUTHORITY=/home/orin/.Xauthority"
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
# MageDok 7" Touchscreen USB Device Rules
|
||||||
|
# Ensure touch device is recognized and accessible
|
||||||
|
|
||||||
|
# Generic USB touch input device (MageDok)
|
||||||
|
# Manufacturer typically reports as: EETI eGTouch Controller
|
||||||
|
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="*eGTouch*", TAG="uaccess"
|
||||||
|
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="*EETI*", TAG="uaccess"
|
||||||
|
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="*MageDok*", TAG="uaccess"
|
||||||
|
|
||||||
|
# Fallback: Any USB device with touch capability (VID/PID may vary by batch)
|
||||||
|
SUBSYSTEM=="usb", ATTRS{bInterfaceClass}=="03", ATTRS{bInterfaceSubClass}=="01", TAG="uaccess"
|
||||||
|
|
||||||
|
# Create /dev/magedok-touch symlink for consistent reference
|
||||||
|
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="*eGTouch*", SYMLINK="magedok-touch"
|
||||||
|
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="*EETI*", SYMLINK="magedok-touch"
|
||||||
|
|
||||||
|
# Permissions: 0666 (rw for all users)
|
||||||
|
SUBSYSTEM=="input", KERNEL=="event*", MODE="0666"
|
||||||
|
SUBSYSTEM=="input", KERNEL=="mouse*", MODE="0666"
|
||||||
Loading…
x
Reference in New Issue
Block a user