Skip to content

Latest commit

 

History

2,407 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

iRaceDeck

iRaceDeck is an Elgato Stream Deck, Mirabox, and Ulanzi Deck plugin for iRacing. Turn your Stream Deck, Mirabox, or Ulanzi Deck into a fully-featured button box with live telemetry, pit controls, camera management, and more.

CodeRabbit Pull Request Reviews Discord

Table of contents

Features

33 actions with 265+ modes across 9 categories:

Category Actions Modes Examples
Display & Session 2 10 Incidents, laps, position, iRating, gaps ahead/behind, fuel, flags
Driving Controls 6 32 AI spotter, audio (incl. Race Engineer & Radar volume), black box cycling, look direction, car control, pit crew
Cockpit & Interface 5 35 Wipers, FFB, splits & reference, telemetry, UI toggles
View & Camera 6 95 FOV, replay, replay markers, camera controls, broadcast tools
Media 1 7 Video recording, screenshots
Pit Service 3 15 Fuel (button and dial), tires, compounds, tearoff, fast repair
Car Setup 7 44 Brakes (button and dial), chassis, aero, engine, fuel mix, hybrid/ERS, traction control
Communication 2 34 Chat, macros, whisper, reply, race admin commands
Stream Deck 1 1 Switch to bundled iRaceDeck profiles (Elgato only)

Key highlights:

  • Live telemetry at 4 Hz with automatic iRacing connection/reconnection
  • Pit Crew action with Radar directional proximity ticks and a spoken Race Engineer, both through a multi-channel audio mixer (miniaudio); no voice ships inside the plugin — every one is a downloadable pack, and the plugin installs and updates its own at launch
  • All keyboard shortcuts are user-configurable via the Property Inspector
  • SDK-first design: uses iRacing broadcast commands where possible, keyboard simulation only as fallback
  • Native C++ addon for low-latency Win32 API access (iRacing SDK, keyboard input, audio engine)

Installation

Users

  1. Download the latest .streamDeckPlugin release
  2. Double-click to install
  3. Find iRaceDeck in the Stream Deck action list

Developers

Prerequisites:

git clone https://github.com/niklam/iracedeck.git
cd iracedeck
pnpm install
pnpm build

Development workflow

# Build only the Stream Deck plugin packages
pnpm build:stream-deck

# Watch mode with hot-reload (restarts Stream Deck automatically)
pnpm watch:stream-deck

# Run tests: all of them, a path filter, or one package
pnpm test
pnpm test packages/iracing-sdk/
pnpm --filter @iracedeck/deck-core test

# Lint and format
pnpm lint:fix
pnpm format:fix

# Hear a Race Engineer voice edit in the sim: opt this machine in once
# (then open a new terminal), and every `pnpm build` stages the voice packs
# and points the plugins at them. Build with the deck host stopped; while it
# runs, restage after a voice edit and press Rescan voices (no restart)
setx IRACEDECK_DEV_VOICES 1
pnpm stage:voices
# Override it for one worktree: `off` plays the real download, `auto` follows
# the variable again, `on` turns it on without the variable
pnpm dev:voices off

Project Structure

A pnpm monorepo built with Turborepo:

packages/
  iracing-actions/         Platform-agnostic iRacing action implementations
  deck-adapter-elgato/     Elgato Stream Deck adapter (bridges Elgato SDK to deck-core)
  deck-adapter-mirabox/    Mirabox VSD Craft adapter (WebSocket protocol to deck-core)
  deck-adapter-ulanzi/     Ulanzi Deck adapter (UlanziStudio WebSocket protocol to deck-core)
  deck-core/               Platform-agnostic base classes, types, and shared utilities
  icon-composer/           Standalone SVG icon assembly (zero dependencies)
  icons/                   SVG icon templates (Mustache)
  iracing-native/          C++ N-API addon (shared memory, window messaging, scan codes)
  iracing-sdk/             TypeScript SDK (telemetry, broadcast commands, session parsing)
  logger/                  Shared logger interface
  iracing-plugin-mirabox/          Mirabox device plugin
  iracing-plugin-ulanzi/           Ulanzi Deck device plugin
  pi-components/           Shared Property Inspector assets (web components, EJS templates, partials, data)
  iracing-plugin-stream-deck/      Elgato Stream Deck plugin
  website/                 Documentation website (iracedeck.com)
Package Role
@iracedeck/iracing-actions All 32 action implementations, platform-agnostic
@iracedeck/deck-core Base classes, types, keyboard service, icon templates, global settings, settings window
@iracedeck/deck-adapter-elgato Bridges the Elgato SDK to deck-core's IDeckPlatformAdapter interface
@iracedeck/deck-adapter-mirabox Bridges the Mirabox VSD Craft WebSocket protocol to deck-core
@iracedeck/deck-adapter-ulanzi Bridges the UlanziStudio WebSocket protocol to deck-core
@iracedeck/icon-composer Standalone SVG icon assembly (pure functions, zero dependencies)
@iracedeck/icons SVG icon Mustache templates with colorization support
@iracedeck/iracing-native C++ Node.js addon for Win32 APIs (memory-mapped files, window messaging, scan-code input)
@iracedeck/iracing-sdk TypeScript SDK for reading telemetry and sending iRacing broadcast commands
@iracedeck/logger Shared logging interface with scoped loggers
@iracedeck/pi-components Shared PI web components, EJS partials, Rollup EJS plugin, and the Ulanzi + settings-window bridges
@iracedeck/iracing-plugin-stream-deck Elgato Stream Deck plugin — registers actions, PI templates, manifest
@iracedeck/iracing-plugin-mirabox Mirabox plugin — registers the same actions for Mirabox devices
@iracedeck/iracing-plugin-ulanzi Ulanzi Deck plugin — registers the same actions for Ulanzi devices
@iracedeck/website Documentation website at iracedeck.com

How it fits together

Button press (Stream Deck, Mirabox, or Ulanzi Deck)
  -> adapter (deck-adapter-elgato / deck-adapter-mirabox / deck-adapter-ulanzi)
    -> actions (platform-agnostic action handler)
      -> deck-core (keyboard service / SDK commands)
        -> iracing-sdk (broadcast command) or iracing-native (scan-code keystroke)
          -> iRacing

iRacing telemetry (shared memory)
  -> iracing-native (reads memory-mapped file)
    -> iracing-sdk (parses telemetry buffer, 4 Hz update loop)
      -> deck-core (notifies subscribers)
        -> actions (updates button display via adapter)

Releasing

All packages share a single version number, driven by release-it plus one custom hook. Nothing is inferred from commit messages — you choose the version, and release-it asks before it commits, tags, pushes, or creates a GitHub Release.

How to release

pnpm release           # prompts for the version
pnpm release -- 3.2.0  # release a specific version
pnpm release:dry       # safe preview — writes nothing

pnpm release wraps release-it (scripts/release.mjs): it strips the -- separator pnpm inserts, exports GITHUB_TOKEN from gh auth token when that variable is not already set, and signals a dry run to the hook below.

A release then runs in this order:

  1. The before:bump hook (scripts/release-hooks.mjs) bumps every packages/*/package.json. The root package.json is bumped separately, by release-it's own npm plugin.
  2. The same hook bumps all three plugin manifest.json files (Stream Deck, Mirabox, Ulanzi). It discovers them rather than using a hardcoded list, and writes a 4-part major.minor.patch.build version — Elgato's manifest schema rejects semver suffixes — with the build slot taken from git rev-list --count HEAD.
  3. On a stable version only, the hook stamps the _Unreleased_ line of the matching ## <version> section in packages/website/src/content/docs/changelog.mdx with today's date, and regenerates packages/iracing-actions/src/actions/data/changelog.json from the stamped text. Any version containing a dash (3.2.0-dev.0, 3.2.0-rc.1) skips both, as does a section that is missing or already dated.
  4. release-it commits the result as chore(release): vX.Y.Z.
  5. It creates the annotated tag vX.Y.Z.
  6. It pushes the branch and the tag to origin.
  7. It creates a GitHub Release named vX.Y.Z.

Steps 4–7 are each prompted outside CI, and declining one skips it. Development bumps are normally committed without a tag or a GitHub Release, which is why most -dev.0 commits in the history are untagged. Pushing a v* tag is what triggers the packaging workflow below, so a bump you chose not to tag publishes nothing.

The changelog is not generated. It is written by hand in changelog.mdx as part of each feature's own PR (see .claude/rules/changelog.md), and the release only stamps its date. GitHub Release notes are generated by GitHub from merged PR labels (github.autoGenerate, categorised by .github/release.yml), not from the changelog.

Preview with pnpm release:dry, never npx release-it --dry-run. release-it runs before:bump hooks even during a dry run, and only the pnpm wrapper sets the environment flag the hook checks for. Invoking release-it directly with --dry-run really does rewrite and stage every package and manifest file, plus the changelog whenever step 3's stamp applies.

Plugin packaging (CI)

.github/workflows/release-pack.yml is a reusable workflow invoked by release.yml (which runs automatically when a version tag is pushed). It:

  1. Builds the full monorepo once on Windows
  2. Packs every published Race Engineer voice pack and checks it against its committed catalog entry (see below)
  3. Packs all three plugins (Stream Deck, Mirabox, Ulanzi) from that single build using streamdeck pack
  4. Attaches each packed plugin to the GitHub Release

Voice packs (CI)

Race Engineer voices are distributed as downloadable packs (packages/audio-assets/src/build/voice-packs.mjs is the list). Their archives are never uploaded by hand: scripts/publish-voice-packs.mjs runs inside the tag workflow above, packs every published voice through the same pipeline the plugin build uses, verifies the archive's sha-256 and size against the committed packages/audio-assets/catalog/<id>.json, and attaches it to the voices-<id>-<version> GitHub release, created with --latest=false so it never takes the "latest" slot the plugin download links resolve through. A pack whose version has not changed is found already published and skipped; an archive the runner cannot reproduce, or a pack whose bytes changed without a version bump, fails the job before anything is published. Every website deploy — the release path and any manual one — first asks each catalog entry's URL and refuses to build if an archive answers anything but 200, so voice-catalog.json can never go live naming an archive that does not exist.

A pack carries more than clips: each voice's callout script (voice/<id>/callouts.json, extracted from configs/<id>.voice.json by pnpm generate:callout-scripts) travels in the archive beside them. When a pack's clips or script change: bump its version, run pnpm --filter @iracedeck/audio-assets pack:voice, and commit the regenerated catalog entry. The next release publishes it.

Writing a pack — ours or anyone's — is documented on the site under Voice Packs: the concept, a first-pack tutorial, the callouts.json format, and a reference of every callout, every name a script may use and the full recording script, generated from the catalog by pnpm generate:pack-reference; from a clone of the repo, pnpm lint:pack <packDir> (after pnpm build) reports everything in a pack folder that the plugin would otherwise skip quietly.

.github/workflows/publish-voice-packs.yml is the manual path, dispatched from the Actions tab. With publish off (the default) it is a dry run — pack and verify, upload nothing, attach the archives as a voice-packs artifact — worth running once before tagging a release that changes a pack. With publish on it publishes the archives and then deploys the website so the catalog offers the new version, for a pack that has to ship between plugin releases. That deploy builds the site from the latest stable plugin tag with only packages/audio-assets/catalog/ taken from the dispatched branch, so documentation for unreleased work on master never reaches the site early.

Requirements

  • A clean working tree — release-it refuses to start otherwise (requireCleanWorkingDir)
  • GitHub CLI (gh) authenticated — pnpm release reads a token from gh auth token unless GITHUB_TOKEN is already set. Only the GitHub Release step needs it, so declining that prompt works without either

Contributing

Contributions are welcome! Here's how to get started:

  1. Fork the repo and create a branch (feature/123-your-feature)
  2. Follow conventional commits with package scope (e.g. feat(iracing-plugin-stream-deck): add new action)
  3. Add tests for new code (Vitest)
  4. Make sure pnpm build and pnpm test pass
  5. Open a pull request

Adding a new action

Actions live in packages/iracing-actions/src/actions/<action-name>/, one folder per action. Each action needs:

  1. <action-name>.ts — action class extending ConnectionStateAwareAction from @iracedeck/deck-core
  2. <action-name>.test.ts — unit tests
  3. <action-name>.ejs — Property Inspector template (compiled to ui/<action-name>.html)
  4. icon.svg + key.svg — static category and key icons (copied into each plugin's imgs/actions/<name>/ at build time)
  5. Mustache SVGs in packages/icons/<action-name>/ for any dynamic variants
  6. Registration in both packages/iracing-plugin-stream-deck/src/plugin.ts and packages/iracing-plugin-mirabox/src/plugin.ts
  7. Manifest entry in each plugin's manifest.json
  8. Entries in packages/iracing-actions/src/actions/data/{key-bindings,docs-urls,icon-defaults}.json where applicable

See the existing actions for reference, or check packages/iracing-plugin-stream-deck/CLAUDE.md for step-by-step instructions.

Troubleshooting

Problem Solution
Double-clicking .streamDeckPlugin doesn't install Rename the file to add .zip at the end, extract the contents to %APPDATA%\Elgato\StreamDeck\Plugins, and restart Stream Deck
Plugin doesn't connect Make sure iRacing is running and you're in a session (on track)
Buttons show nothing iRacing telemetry is only available while driving; the plugin reconnects automatically
Native addon build fails Install Python 3.x and VS Build Tools with C++ workload. Try npm config set msvs_version 2022
Key presses don't work Check your key bindings in the Property Inspector match your iRacing configuration

Usage and license

iRaceDeck is source-available and licensed under the iRaceDeck Non-Commercial License v1.1.

Free for personal and non-commercial use, including use in sim racing events and competitions.

Plugin downloads ship the license alongside THIRD-PARTY-LICENSES.md, which aggregates the license notices of every bundled third-party component.

For full details, see USAGE.md.

The name “iRaceDeck” and associated branding are not included in the license and may not be used for derived versions without permission.

If you're unsure whether your use case is allowed, feel free to reach out.

Why this license?

iRaceDeck is source-available rather than open source.

The goal is simple:

  • Keep iRaceDeck free for sim racers, hobbyists, and the community
  • Allow people to build on top of it (profiles, icons, tools, integrations)
  • Prevent others from selling or commercializing the plugin itself without permission
  • Require that anyone who distributes a modified version of iRaceDeck makes their modifications publicly available, so the community keeps benefiting from improvements

If you're just using, modifying, or contributing to iRaceDeck, nothing changes for you.

If you're building something commercial around iRaceDeck, that's usually fine — just don't redistribute or sell the plugin itself. If you're unsure, feel free to reach out.

For companies

If you're working on something commercial based on iRaceDeck, feel free to reach out.

I'm open to licensing, collaboration, or helping you build on top of it.

Contact: niklas@iracedeck.com

Inspiration

This project was inspired by iRaceIT, a Stream Deck plugin for iRacing.

Acknowledgements

About

iRacing plugin for Elgato Stream Deck, Mirabox, and UIanzi ecosystems.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

41 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages