A pure-Python library for programmatically building .rti project files for RTI home automation remotes and processors.
No Integration Designer required at build time — this library encodes the full binary format from scratch, including image compression, TLV records, and OLE2 container structure.
Confirmed working: Generated files open and display correctly in RTI Integration Designer, including full-colour background images on the T2i touchscreen.
RTI .rti project files are OLE2 Compound File Binary containers (the same low-level format as old .doc files). Each device — processor and remotes — stores its configuration as a stream of custom TLV (Tag-Length-Value) binary records inside named streams in that container.
This library:
- Encodes the complete TLV wire format from scratch, including all 350 header fields shared by every RTI remote type
- Builds OLE2/CFB containers without any external dependencies (no
olefile, nocompoundfiles) - Encodes T2i background images: raw RGB → bottom-up BGR → zlib compress → BITMAPINFOHEADER → nested TLV CONT
- Provides a graphic design layer for building Oasis-style button layouts with icon loading, gradient backgrounds, and rounded-rect chips
- Ships a reverse-engineering toolkit for diffing and decoding existing
.rtifiles field-by-field
| Device | Type byte | Display | Notes |
|---|---|---|---|
| T2i | 0x4B |
240×320 24-bit colour touchscreen | Fully supported, with image encoding |
| U2 | 0x1D |
64×128 2-bit mono | Supported |
| U1 | 0x11 |
None (button-only) | Supported |
| XP-3/6/8 | 0x31 |
— (processor) | Macro/command encoding |
pip install Pillow # required for image encoding and ButtonDesignerPython 3.10+ required.
from rti_lib import RTIProject, T2iRemote
from rti_lib.devices.xp import XPProcessor
xp = XPProcessor('XP-6', display_name='Living Room AV')
m1 = xp.add_macro('Watch TV', serial=b'src hdmi1\r', baud=9600, port=0,
device_name='AV Receiver', manufacturer='Generic', model_str='AVR')
t2i = T2iRemote(display_name='Living Room')
t2i.add_source_button('Watch TV', macro=m1, x=0, y=80, w=120, h=80)
proj = RTIProject()
proj.add_device(xp)
proj.add_device(t2i)
proj.save('my_project.rti')from rti_lib.assets.icon_library import IconLibrary
from rti_lib.assets.button_designer import ButtonDesigner, Style
TEMPLATE_DIR = r'C:\Program Files (x86)\RTI\Integration Designer\Templates'
# Load icons from the bundled RTI template library
lib = IconLibrary.load(TEMPLATE_DIR + r'\Source and Zone Icons.rtitemplate')
icon_png = lib.get_png('Icon (112x112) - Cable')
# Generate Oasis-style gradient background (240×320 raw RGB bytes)
bg_rgb = ButtonDesigner.background(Style.OASIS)
# Generate a rounded-rect button chip with an icon
chip_png = ButtonDesigner.button_chip(
width=115, height=78, label='Cable TV',
style=Style.OASIS, icon_png=icon_png,
)
# Composite the chip onto the background and assign it to the T2i
bg_rgb = ButtonDesigner.composite(bg_rgb, chip_png, x=3, y=38)
t2i.set_background(bg_rgb)# Simple T2i project with gradient background + all hardware buttons
python examples/basic_t2i_project.py
# Full Oasis home screen with library icons, chips, and all hardware buttons
python examples/oasis_home_screen.pyBoth write a .rti file to examples/ that opens directly in RTI Integration Designer.
rti_lib/
├── __init__.py Public surface: RTIProject, T2iRemote, XPProcessor, …
│
├── core/
│ ├── tlv.py TLV encode / decode (the RTI binary wire format)
│ ├── cfb.py OLE2/CFB container reader
│ ├── cfb_writer.py OLE2/CFB container writer
│ ├── fields.py Named field constants + 350-field stream registry
│ └── models.py Device-type byte constants
│
├── devices/
│ ├── common.py Shared TLV builders (button CONT, group headers)
│ ├── xp/ XP processor — XPProcessor, Macro
│ ├── u1/ U1 button-only handheld — U1Remote
│ ├── u2/ U2 mono touchscreen — U2Remote, BMLFile
│ └── t2i/
│ ├── remote.py T2iRemote — high-level API
│ ├── stream_profile.py T2i base stream builder + image encoder
│ ├── encoders.py Low-level button CONT encoder
│ └── image.py load_image_rgb() convenience wrapper
│
├── assets/
│ ├── icon_library.py IconLibrary — reads icons from .rtitemplate files
│ ├── image_loader.py ImageLoader — any-source → raw RGB bytes
│ └── button_designer.py ButtonDesigner — gradient backgrounds, button chips
│
├── project/
│ ├── project.py RTIProject — assembles devices, writes .rti
│ └── metadata.py Job Info, VariableIDs, RTI Data Directory V3
│
└── tools/
├── stream_diff.py Stream diff / annotated decode tool
└── browse_template.py Icon-library browser (list, grid, HTML sheet, export)
proj = RTIProject()
proj.add_device(xp) # → Device Data Stream 0000
proj.add_device(t2i) # → Device Data Stream 0001
size = proj.save('my_project.rti') # returns bytes writtenxp = XPProcessor('XP-6', display_name='Living Room AV')
macro = xp.add_macro(
name='Watch TV',
serial=b'src hdmi1\r',
baud=9600,
port=0, # RS-232 port index
device_name='Receiver',
manufacturer='Denon',
model_str='AVR-X3700H',
)t2i = T2iRemote(display_name='Living Room')
# Background image (240×320 raw RGB bytes — use ImageLoader or ButtonDesigner)
t2i.set_background(rgb_bytes)
# Home page: touchscreen buttons (register touch region + macro)
t2i.add_source_button('Watch TV', macro=m, x=0, y=80, w=120, h=80)
t2i.assign_button(index=128, label='Watch TV', macro=m, x=0, y=80, w=120, h=80)
# Secondary page (swipe or nav button to reach)
t2i.add_secondary_button('Play', macro=m_play, x=0, y=60, w=120, h=60)
# Physical hardware buttons (apply to every page regardless of touch layout)
t2i.assign_hw_button_macro(index=138, macro=m_volup) # Vol+ physical button
stream = t2i.build_stream() # → raw TLV bytesfrom rti_lib.assets.icon_library import IconLibrary
TEMPLATE_DIR = r'C:\Program Files (x86)\RTI\Integration Designer\Templates'
lib = IconLibrary.load(TEMPLATE_DIR + r'\Source and Zone Icons.rtitemplate')
print(lib.summary()) # 'IconLibrary("Source and Zone Icons", 1230 images, 112x112px)'
png = lib.get_png('Icon (112x112) - Cable') # normal / up state
png = lib.get_png('Icon (112x112) - Cable', 'down') # pressed / down state
up, down = lib.get_all_png('Icon (112x112) - Cable')
for entry in lib: # iterate ImageEntry dataclass objects
print(entry.name, entry.width, entry.height)from rti_lib.assets.image_loader import ImageLoader
rgb = ImageLoader.from_file('photo.jpg') # any Pillow-readable file
rgb = ImageLoader.from_bytes(png_data) # from memory
rgb = ImageLoader.from_library_entry(lib, 'Icon (112x112) - Cable')
rgb = ImageLoader.solid(r=20, g=30, b=80)
rgb = ImageLoader.gradient_v(top=(10, 20, 60), bottom=(100, 200, 160))
rgb = ImageLoader.gradient_h(left=(10, 20, 60), right=(40, 100, 120))
rgb = ImageLoader.gradient_radial(center=(60, 180, 200), edge=(5, 10, 30))
rgb = ImageLoader.overlay_png(bg_rgb, chip_png, x=3, y=38)All methods return width × height × 3 raw RGB bytes (top-to-bottom), ready for t2i.set_background().
from rti_lib.assets.button_designer import ButtonDesigner, Style
# Available styles: Style.OASIS, Style.DARK_STEEL, Style.NIGHT_BLUE, Style.MINIMAL
# Full-screen background → raw RGB bytes
bg_rgb = ButtonDesigner.background(Style.OASIS)
bg_rgb = ButtonDesigner.background(Style.OASIS, width=240, height=320,
icon_png=png_bytes, icon_x=89, icon_y=137)
# Rounded-rect button chip → PNG bytes with RGBA transparency
chip_png = ButtonDesigner.button_chip(
width=115, height=78, label='Cable TV',
style=Style.OASIS, font_size=11, icon_png=icon_png,
)
# Paste a chip (alpha-composite) onto a background
rgb = ButtonDesigner.composite(bg_rgb, chip_png, x=3, y=38)
# Save or retrieve as PNG
ButtonDesigner.save_preview(rgb, 'preview.png')
png_bytes = ButtonDesigner.preview_png(rgb)Compare two .rti files field-by-field — invaluable for reverse-engineering unknown fields:
# Show only changed fields across all device streams
python -m rti_lib.tools.stream_diff before.rti after.rti
# Diff a specific device slot (0-based)
python -m rti_lib.tools.stream_diff before.rti after.rti --slot 0
# Show all fields including identical ones
python -m rti_lib.tools.stream_diff before.rti after.rti --all
# Annotated decode of a single stream
python -m rti_lib.tools.stream_diff --print project.rti --slot 0Example diff output:
RTI Stream Diff
A: before.rti
B: after.rti
====================================================================================================
Slot 0001 A=U2 (0x1D) B=T2i (0x4B)
pos tag type field name A value -> B value
----------------------------------------------------------------------------------------------------
0 0x01 BYTE device_type 29 (U2)
-> 75 (T2i)
11 0x2D BYTE ui_version 1 (mono B&W (U1/U2))
-> 2 (colour (T2i))
Summary: 7 difference(s), 343 identical record(s)
Explore and export icons from RTI's bundled .rtitemplate icon libraries:
# List all images in a library
python -m rti_lib.tools.browse_template list "Source and Zone Icons.rtitemplate"
# Save a single icon as PNG
python -m rti_lib.tools.browse_template get "Source and Zone Icons.rtitemplate" "Icon (112x112) - Cable" cable.png
# Export all icons to a folder
python -m rti_lib.tools.browse_template export "Source and Zone Icons.rtitemplate" ./icons/
# Generate a PNG contact sheet
python -m rti_lib.tools.browse_template grid "Source and Zone Icons.rtitemplate" grid.png
# Generate an HTML contact sheet (hover shows pressed state, filterable)
python -m rti_lib.tools.browse_template sheet "Source and Zone Icons.rtitemplate" sheet.htmlBare filenames are automatically resolved against:
C:\Program Files (x86)\RTI\Integration Designer\Templates\
python extract_project.py Test2.rti
python extract_project.py Test2.rti --tlv # full TLV decode
python extract_project.py Test2.rti --json # JSON output
python extract_project.py Test2.rti --extract-streams ./streams/All 350 common header positions are named in rti_lib/core/fields.py. Import them in device-stream code to avoid raw magic numbers:
from rti_lib.core.fields import CFG_HAS_TOUCHSCREEN, NET_HAS_COLOR, STREAM_FIELDS
# Look up a field definition by stream position
fdef = STREAM_FIELDS[28] # pos 28 = has_touchscreen
print(fdef.name) # 'has_touchscreen'
print(fdef.description) # 'Has touchscreen display'
print(fdef.known_values) # {0: 'no', 1: 'yes'}The diff tool uses these names as column headers so the output is immediately readable without cross-referencing source code.
- USB upload not implemented. Generating
.rtifiles works fully; pushing them directly to a device over USB requires the USB bulk-pipe handshake, which has not yet been reverse-engineered. Use Integration Designer's built-in sync for now. - Read-back is partial.
cfb.pyandtlv.pycan decode any.rtifile for inspection; modifying and re-saving an existing project (round-trip) is not yet supported. - T4/T4+ not yet supported. The T4 uses a different page-layout container structure.
See PROTOCOL.md for the complete binary format reference, including:
- OLE2/CFB stream layout
- TLV type codes and encoding rules
- The 350-field common device stream header
- T2i image CONTAINER encoding (zlib + BITMAPINFOHEADER + GUID sentinel)
- Button CONT structure and hardware slot map