feat: MageDok 7in display setup for Jetson Orin (Issue #369)

Add complete display integration for MageDok 7" IPS touchscreen:

Configuration Files:
- X11 display config (xorg-magedok.conf) — 1024×600 @ 60Hz
- PulseAudio routing (pulseaudio-magedok.conf) — HDMI audio to speakers
- Udev rules (90-magedok-touch.rules) — USB touch device permissions
- Systemd service (magedok-display.service) — auto-start on boot

ROS2 Launch:
- magedok_display.launch.py — coordinate display/touch/audio setup

Helper Scripts:
- verify_display.py — validate 1024×600 resolution via xrandr
- touch_monitor.py — detect MageDok USB touch, publish status
- audio_router.py — configure PulseAudio HDMI sink routing

Documentation:
- MAGEDOK_DISPLAY_SETUP.md — complete installation and troubleshooting guide

Features:
✓ DisplayPort → HDMI video from Orin DP connector
✓ USB touch input as HID device (driver-free)
✓ HDMI audio routing to built-in speakers
✓ 1024×600 native resolution verification
✓ Systemd auto-launch on boot (no login prompt)
✓ Headless fallback when display disconnected
✓ ROS2 status monitoring (touch/audio/resolution)

Supports Salty Face UI (Issue #370) and accessibility features (Issue #371)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
sl-webui 2026-03-03 15:44:00 -05:00
parent 631282b95f
commit 45d456049a
9 changed files with 658 additions and 0 deletions

View File

@ -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

View 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

View File

@ -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

View File

@ -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',
),
])

View 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()

View 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()

View File

@ -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()

View File

@ -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

View File

@ -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"