Resume-Website/blog/SteamController/index3.html

621 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Article - Tech Blog</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--deep-navy: #0A1128;
--rich-gold: #D4AF37;
--warm-gold: #F4E5C2;
--cream: #FFF8E7;
--charcoal: #2C2C2C;
--silver: #C0C0C0;
--burgundy: #7C2D37;
}
body {
font-family: 'Didot', 'Bodoni MT', 'Playfair Display', Georgia, serif;
line-height: 1.8;
color: var(--charcoal);
background: linear-gradient(180deg, var(--deep-navy) 0%, #1a2847 100%);
position: relative;
overflow-x: hidden;
}
/* Art Deco geometric background pattern */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
repeating-linear-gradient(45deg, transparent, transparent 35px, rgba(212, 175, 55, 0.03) 35px, rgba(212, 175, 55, 0.03) 70px),
repeating-linear-gradient(-45deg, transparent, transparent 35px, rgba(212, 175, 55, 0.03) 35px, rgba(212, 175, 55, 0.03) 70px);
z-index: -1;
}
/* Navigation */
nav {
background: rgba(10, 17, 40, 0.95);
border-bottom: 2px solid var(--rich-gold);
padding: 1.5rem 0;
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(10px);
}
.nav-content {
max-width: 1000px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-logo {
font-size: 1.5rem;
font-weight: 300;
color: var(--warm-gold);
text-decoration: none;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.nav-back {
color: var(--silver);
text-decoration: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 0.9rem;
letter-spacing: 0.1em;
text-transform: uppercase;
transition: color 0.3s;
}
.nav-back:hover {
color: var(--rich-gold);
}
.nav-back::before {
content: '←';
margin-right: 0.5rem;
}
/* Article Container */
.article-container {
max-width: 1000px;
margin: 4rem auto;
padding: 0 2rem;
}
/* Article Header */
.article-header {
background: var(--cream);
padding: 4rem 4rem 3rem;
border: 2px solid var(--rich-gold);
position: relative;
margin-bottom: 3rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}
/* Corner decorations */
.article-header::before,
.article-header::after {
content: '';
position: absolute;
width: 60px;
height: 60px;
border: 2px solid var(--rich-gold);
}
.article-header::before {
top: 0;
left: 0;
border-right: none;
border-bottom: none;
}
.article-header::after {
bottom: 0;
right: 0;
border-left: none;
border-top: none;
}
.article-number {
font-size: 4rem;
font-weight: 300;
color: var(--rich-gold);
line-height: 1;
margin-bottom: 1rem;
}
.article-meta {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.15em;
}
.article-category {
color: var(--burgundy);
font-weight: 700;
position: relative;
padding-left: 1rem;
}
.article-category::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%) rotate(45deg);
width: 6px;
height: 6px;
background: var(--rich-gold);
}
.article-title {
font-size: 3.5rem;
font-weight: 400;
color: var(--deep-navy);
line-height: 1.2;
letter-spacing: 0.02em;
margin-bottom: 1.5rem;
}
.article-subtitle {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 1.25rem;
line-height: 1.6;
color: var(--charcoal);
font-weight: 400;
}
/* Tech Stack */
.tech-stack {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 2rem;
padding-top: 2rem;
border-top: 2px solid var(--rich-gold);
}
.tech-item {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 0.9rem;
font-weight: 600;
color: var(--deep-navy);
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, rgba(212, 175, 55, 0.15), rgba(212, 175, 55, 0.05));
border-left: 3px solid var(--rich-gold);
letter-spacing: 0.05em;
}
/* Featured Image */
.featured-image {
width: 100%;
height: 500px;
background: linear-gradient(135deg, var(--deep-navy) 0%, var(--burgundy) 100%);
border: 2px solid var(--rich-gold);
margin-bottom: 3rem;
position: relative;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}
/* Sunburst pattern on image */
.featured-image::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 200%;
height: 200%;
background: repeating-conic-gradient(
from 0deg,
rgba(212, 175, 55, 0.1) 0deg 10deg,
transparent 10deg 20deg
);
transform: translate(-50%, -50%);
}
.image-placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 6rem;
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.5));
}
/* Article Content */
.article-content {
background: var(--cream);
padding: 4rem;
border: 2px solid var(--rich-gold);
position: relative;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}
/* Corner decorations for content */
.article-content::before,
.article-content::after {
content: '';
position: absolute;
width: 40px;
height: 40px;
border: 2px solid var(--rich-gold);
}
.article-content::before {
top: 0;
right: 0;
border-left: none;
border-bottom: none;
}
.article-content::after {
bottom: 0;
left: 0;
border-right: none;
border-top: none;
}
.article-content h2 {
font-size: 2rem;
font-weight: 400;
color: var(--deep-navy);
margin: 3rem 0 1.5rem 0;
letter-spacing: 0.02em;
position: relative;
padding-bottom: 1rem;
}
.article-content h2::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 80px;
height: 2px;
background: var(--rich-gold);
}
.article-content h2:first-child {
margin-top: 0;
}
.article-content h3 {
font-size: 1.5rem;
font-weight: 400;
color: var(--burgundy);
margin: 2.5rem 0 1rem 0;
letter-spacing: 0.02em;
}
.article-content p {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 1.1rem;
line-height: 1.9;
margin-bottom: 1.5rem;
color: var(--charcoal);
}
.article-content ul,
.article-content ol {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 1.1rem;
line-height: 1.9;
margin: 1.5rem 0 1.5rem 2rem;
color: var(--charcoal);
}
.article-content li {
margin-bottom: 0.75rem;
padding-left: 0.5rem;
}
.article-content ul li::marker {
color: var(--rich-gold);
}
.article-content code {
font-family: 'Courier New', monospace;
background: rgba(212, 175, 55, 0.1);
padding: 0.2rem 0.5rem;
border-radius: 3px;
font-size: 0.95rem;
color: var(--burgundy);
}
.article-content pre {
background: var(--charcoal);
color: var(--warm-gold);
padding: 1.5rem;
border-radius: 4px;
overflow-x: auto;
margin: 2rem 0;
border-left: 4px solid var(--rich-gold);
}
.article-content pre code {
background: none;
color: var(--warm-gold);
padding: 0;
}
.article-content blockquote {
border-left: 4px solid var(--rich-gold);
padding-left: 2rem;
margin: 2rem 0;
font-style: italic;
color: var(--burgundy);
background: linear-gradient(90deg, rgba(212, 175, 55, 0.05), transparent);
padding: 1.5rem 2rem;
}
/* Inline images */
.article-content img {
max-width: 100%;
height: auto;
margin: 2rem 0;
border: 2px solid var(--rich-gold);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
/* Decorative divider */
.deco-divider {
display: flex;
align-items: center;
justify-content: center;
margin: 3rem 0;
gap: 1rem;
}
.deco-divider::before,
.deco-divider::after {
content: '';
width: 100px;
height: 2px;
background: linear-gradient(90deg, transparent, var(--rich-gold), transparent);
}
.deco-divider .diamond {
width: 12px;
height: 12px;
background: var(--rich-gold);
transform: rotate(45deg);
}
/* Footer */
footer {
text-align: center;
padding: 4rem 0;
color: var(--silver);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 0.9rem;
letter-spacing: 0.15em;
text-transform: uppercase;
margin-top: 4rem;
}
.footer-ornament {
width: 200px;
height: 2px;
background: linear-gradient(90deg, transparent, var(--rich-gold), transparent);
margin: 0 auto 1.5rem;
position: relative;
}
.footer-ornament::before {
content: '';
position: absolute;
width: 10px;
height: 10px;
background: var(--rich-gold);
transform: rotate(45deg);
top: -4px;
left: 50%;
margin-left: -5px;
}
/* Responsive */
@media (max-width: 768px) {
.article-header,
.article-content {
padding: 2rem;
}
.article-title {
font-size: 2.5rem;
}
.article-number {
font-size: 3rem;
}
.featured-image {
height: 300px;
}
.image-placeholder {
font-size: 4rem;
}
.article-container {
padding: 0 1rem;
margin: 2rem auto;
}
.nav-content {
padding: 0 1rem;
}
.nav-logo {
font-size: 1.2rem;
}
}
</style>
</head>
<body>
<!-- Navigation -->
<nav>
<div class="nav-content">
<a href="#" class="nav-logo">Steam Controller Driver</a>
<a href="https://steen.run/blog" class="nav-back">Back to Projects</a>
</div>
</nav>
<!-- Article Container -->
<div class="article-container">
<!-- Article Header -->
<header class="article-header">
<div class="article-number">04</div>
<div class="article-meta">
<span class="article-category">Hardware</span>
<span>November 2025</span>
</div>
<h1 class="article-title">Steam Controller Driver for Actual Motors</h1>
<p class="article-subtitle">
lorem ipsum
</p>
<div class="tech-stack">
<div class="tech-item">React</div>
<div class="tech-item">Node.js</div>
<div class="tech-item">WebSocket</div>
<div class="tech-item">MongoDB</div>
<div class="tech-item">Redis</div>
</div>
</header>
<!-- Featured Image -->
<div class="featured-image">
<div class="image-placeholder"></div>
</div>
<!-- Article Content -->
<article class="article-content">
<h2>Backstory</h2>
<p>
I was cleaning out a closet and I discovered an old Steam Controller I bought about a decade ago. To my surprise, it still turned on. Way to go, Duracell. I remembered it being a pretty niche thing that had very little adoption and was quickly deprecated by Valve. Boy was I wrong. I turned it on and immediately remembered how amazing the haptic feedback was on the touchpads. How could I forget the grip inputs? Even a gyroscope! The design and hardware were top notch, but adoption was the problem.
</p>
<p>
The haptic feedback touchpads made Games like Civ and XCOM playable on the couch. But I had my eyes on something much bigger target that could use a controller with fourteen buttons, six directional axes, two analog triggers, and a gryoscope: Flight Simulator. With traditional controllers, you had to have a plethora of different button configurations to make it work. Joysticks make it hard to sense and control your aircraft. Touchpads allow for refined movement with feedback to help you ramp up and down with ease.
</p>
<div class="deco-divider">
<div class="diamond"></div>
</div>
<h2>Flight Simulator</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eget tortor massa. Sed sed nulla vitae enim accumsan pulvinar fermentum nec nunc. Phasellus scelerisque sem vel massa tempus, id lobortis arcu mattis. Donec sed lectus cursus libero auctor facilisis. Donec mattis luctus congue. Maecenas et nunc odio. Sed eu diam lectus. Phasellus sit amet mi vitae lacus rhoncus elementum. Nullam dictum justo sed dignissim placerat. Pellentesque porta velit vel luctus efficitur.
</p>
<h3>WebSocket Server</h3>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eget tortor massa. Sed sed nulla vitae enim accumsan pulvinar fermentum nec nunc. Phasellus scelerisque sem vel massa tempus, id lobortis arcu mattis. Donec sed lectus cursus libero auctor facilisis. Donec mattis luctus congue. Maecenas et nunc odio. Sed eu diam lectus. Phasellus sit amet mi vitae lacus rhoncus elementum. Nullam dictum justo sed dignissim placerat. Pellentesque porta velit vel luctus efficitur.
</p>
<ul>
<li>Handles client connections and disconnections gracefully</li>
<li>Broadcasts operations to all connected clients in a room</li>
<li>Implements heartbeat mechanisms to detect stale connections</li>
<li>Scales horizontally using Redis pub/sub for multi-server deployments</li>
</ul>
<div class="deco-divider">
<div class="diamond"></div>
</div>
<h2>Physical Applications</h2>
<h3>Parsing the Byte Stream</h3>
<p>
If I want to make a driver for this controller, I need to see what the output is. Controllers are just like a keyboard: character devices. When you type an "a" character into your keyboard, the keyboard sends data for an "a" key to the computer. When you press an "a" button on your controller, it does just about the same thing. It sends data, which in this case is 41 in hex or 01000010 in binary, to the computer and it is handled by the OS, which monitors for any special inputs, before sending it to the application you're using. This happens so quickly, we don't even think about it. It's a little more complicated than that, but I'll explain later. What I need to do is reverse engineer what data the inputs on my controller output and write a driver to turn those inputs into actions.
</p>
<p>
Knowing my controller is a character device, I can "read" it just like a file, but I have to jump through a couple hoops first. I don't know what that actual file is yet. I'm going to have to check around. When you plug a device into a computer running an OS, it has to decide what to do with it. Devices that already have their firmware configured will tell the OS what to do with it, usually through the Human Interface Device (HID) standard. So I need to ask the OS where it put the file.
</p>
<pre><code>$ lsusb
Bus 003 Device 022: ID 28de:1102 Valve Software Wired Controller</code></pre>
<p>
This gives us the Bus number, Device number, and both the Device ID and Vendor ID. The bus and device number are abstractions for userspace created by udev. In this instance they seem very redundant and abstract away from what I'm trying to accomplish. What I am trying to do is take the raw input from the device at a kernel level, so I need to ask udev where it's getting this device from.
<pre><code>$ udevadm info -q path -n /dev/bus/usb/003/022
/sys/devices/pci0000:00/0000:00:14.0/usb3/3-7</pre></code>
This shows us where the kernel has put the device. We can go into this series of directories and find it. Problem solved! Almost.
</p>
<p>
In this case, with the steam controller, it actually has 3 subdevices, each with their own hidraw file. As it turns out, the controller can also be used in place of a mouse and keyboard. One could poke around the files some more to differentiate them, but I found a better solution.
<pre><code>sudo usbhid-dump -m 28de:1102 -ed > descriptor.txt
grep -A 10 "DESCRIPTOR" descriptor.txt | grep -v "DESCRIPTOR" | grep -v "STREAM" | tr -d ' \n' > descriptor_hex.txt
rexx rd.rex -d --hex "$(cat descriptor_hex.txt)"</pre></code>
<pre><code>=== /dev/hidraw6 ===
Name: Valve Software Wired Controller
Vendor:Product: 0x28de:0x1102
Bus type: 3
Physical: usb-0000:00:14.0-7/input0
Descriptor size: 65 bytes
Descriptor (first 32 bytes): 05 01 09 06 95 01 A1 01 05 07 19 E0 29 E7 15 00 25 01 75 01 95 08 81 02 95 01 75 08 81 01 95 05
Type hints: [KEYBOARD]
=== /dev/hidraw7 ===
Name: Valve Software Wired Controller
Vendor:Product: 0x28de:0x1102
Bus type: 3
Physical: usb-0000:00:14.0-7/input1
Descriptor size: 52 bytes
Descriptor (first 32 bytes): 05 01 09 02 A1 01 09 01 A1 00 05 09 19 01 29 05 15 00 25 01 95 05 75 01 81 02 95 01 75 03 81 01
Type hints: [MOUSE]
=== /dev/hidraw8 ===
Name: Valve Software Wired Controller
Vendor:Product: 0x28de:0x1102
Bus type: 3
Physical: usb-0000:00:14.0-7/input2
Descriptor size: 33 bytes
Descriptor (first 32 bytes): 06 00 FF 09 01 A1 01 15 00 26 FF 00 75 08 95 40 09 01 81 02 95 40 09 01 91 02 95 40 09 01 B1 02 </pre></code>
</p>
<p>
I found a tool called <i>RDD! HID Report Descriptor Decoder</i> on github by <a href="https://github.com/abend0c1/hidrdd">abend0c1</a>. It takes the data from usbhid-dump and performs lookups on all the devices given. It's a very comprehensive tool that gave much more output than I listed.
</p>
<p>
</p>
<p>
</p>
</article>
</div>
<!-- Footer -->
<footer>
<div class="footer-ornament"></div>
<p>Crafted with Precision & Elegance © 2025</p>
</footer>
</body>
</html>