<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrey Sazonov</title>
    <description>The latest articles on DEV Community by Andrey Sazonov (@firefighter19).</description>
    <link>https://dev.to/firefighter19</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F700832%2F384978b4-22de-4136-b437-b77a0dd749e4.jpeg</url>
      <title>DEV Community: Andrey Sazonov</title>
      <link>https://dev.to/firefighter19</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/firefighter19"/>
    <language>en</language>
    <item>
      <title>Building a Mac-native Subaru ECU tuning tool in Rust</title>
      <dc:creator>Andrey Sazonov</dc:creator>
      <pubDate>Tue, 26 May 2026 11:01:18 +0000</pubDate>
      <link>https://dev.to/firefighter19/building-a-mac-native-subaru-ecu-tuning-tool-in-rust-5bg5</link>
      <guid>https://dev.to/firefighter19/building-a-mac-native-subaru-ecu-tuning-tool-in-rust-5bg5</guid>
      <description>&lt;p&gt;If you want to tune a Subaru ECU on a Mac in 2026, you have three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a Windows VM with RomRaider (works, but it's 2026 and I'm not running
Parallels for a hobby project).&lt;/li&gt;
&lt;li&gt;Hope someone's Wine + J2534-DLL stack still compiles (it doesn't, on
Apple Silicon).&lt;/li&gt;
&lt;li&gt;Buy a separate Windows laptop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I picked option four — write a Rust toolkit that talks to the ECU directly&lt;br&gt;
through the Tactrix Openport 2.0 cable, no J2534 DLL anywhere in sight. Three&lt;br&gt;
months later it dumps the full 1 MiB firmware over CAN in 44 seconds and the&lt;br&gt;
binary is byte-for-byte identical to what EcuFlash produces on Windows. The&lt;br&gt;
code lives at &lt;a href="https://github.com/firefighter-19/tuneforge" rel="noopener noreferrer"&gt;&lt;code&gt;firefighter-19/tuneforge&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is about the three most interesting bits of the journey: reverse-&lt;br&gt;
engineering Subaru's SecurityAccess seed/key, talking to the Tactrix cable&lt;br&gt;
without J2534, and gluing it all into a one-binary dump pipeline.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem the hard way: Subaru's SecurityAccess
&lt;/h2&gt;

&lt;p&gt;Modern Subaru ECUs (2007+) reject most reads in the default UDS session.&lt;br&gt;
They want you to climb a small ladder first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;StartDiagnosticSession 0x10 0x03    →  ExtendedDiagnosticSession
SecurityAccess 0x27 0x01            →  ECU returns a 4-byte "seed"
SecurityAccess 0x27 0x02 &amp;lt;key&amp;gt;      →  ECU validates, returns "67 02" (granted)
StartDiagnosticSession 0x10 0x02    →  ProgrammingSession (now we can RAM-write)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;key&lt;/code&gt; is a function of the seed. Subaru's nicest property: that function&lt;br&gt;
is &lt;strong&gt;a Feistel cipher with 16 32-bit round-keys&lt;/strong&gt;, and the round-keys live&lt;br&gt;
inside the ROM itself at offset &lt;code&gt;0x05972C&lt;/code&gt;. Different firmware images → &lt;br&gt;
different round-keys → different &lt;code&gt;f(seed)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This was the part I expected to take an evening and ended up taking a week.&lt;br&gt;
The good news: I didn't have to break the cipher. I extracted the round-key&lt;br&gt;
table from a known-good ROM dump (got the dump from EcuFlash on a borrowed&lt;br&gt;
Windows laptop — one-time bootstrap) and the implementation reduces to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;subaru_genkey_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;round_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_be_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.try_into&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// initial complement is part of the Subaru variant&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rk&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;round_keys&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;feistel_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Feistel round itself is a couple of rotates and XORs — nothing&lt;br&gt;
cryptographically scary. What makes it Subaru's is which round-keys you use,&lt;br&gt;
and those are firmware-specific. The CAN variant and the K-Line variant use&lt;br&gt;
&lt;strong&gt;different&lt;/strong&gt; Feistel constants too, which cost me a couple of days because I&lt;br&gt;
naively assumed they'd share the algorithm.&lt;/p&gt;

&lt;p&gt;Validation was straightforward: capture a successful EcuFlash session in&lt;br&gt;
Wireshark, find the &lt;code&gt;27 01 &amp;lt;seed&amp;gt;&lt;/code&gt; request and &lt;code&gt;27 02 &amp;lt;key&amp;gt;&lt;/code&gt; response, run&lt;br&gt;
my code on the seed, compare. When they matched on the first try I assumed&lt;br&gt;
I had a bug and re-verified three times.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Aside on legality:&lt;/strong&gt; this is read-only stuff. Reading your own ECU's&lt;br&gt;
calibration via a documented diagnostic service isn't bypassing anything;&lt;br&gt;
it's the same data RomRaider has been reading for 20 years. I have not&lt;br&gt;
implemented flash-write and don't plan to.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Tactrix without J2534
&lt;/h2&gt;

&lt;p&gt;The Tactrix Openport 2.0 ships with a J2534 DLL on Windows that gives you a&lt;br&gt;
nice high-level API: &lt;code&gt;PassThruConnect&lt;/code&gt;, &lt;code&gt;PassThruReadMsgs&lt;/code&gt;, etc. On macOS&lt;br&gt;
this DLL doesn't exist, doesn't have an equivalent, and emulating it through&lt;br&gt;
Wine is — to use a technical term — terrible.&lt;/p&gt;

&lt;p&gt;What the DLL hides is a very normal USB device with two bulk endpoints. The&lt;br&gt;
real wire protocol is AT-commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ati                          # init
ata                          # query device
ato0033 0x1                  # open protocol 0x33 = ISO9141 with flag 0x1
att0033 0x4 …                # set tx timing
atf0033 …                    # configure receive filters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and you push frames into the OUT endpoint, read replies from IN. That's&lt;br&gt;
the whole API. It's documented in the Tactrix firmware source which they&lt;br&gt;
released on GitHub years ago and most people never noticed.&lt;/p&gt;

&lt;p&gt;In Rust this is &lt;code&gt;rusb&lt;/code&gt;. Tactrix-specific glue lives in &lt;code&gt;tuneforge-io&lt;/code&gt; and&lt;br&gt;
exposes the same &lt;code&gt;Transport&lt;/code&gt; trait the rest of the codebase uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;Transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;write_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;IoResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;IoResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;purge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;IoResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;set_baud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;IoResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* default: unsupported */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the same trait abstracting away whether the bytes are flowing over&lt;br&gt;
serial K-Line at 4800 baud, Tactrix-ISO9141 K-Line, or Tactrix-ISO15765 CAN&lt;br&gt;
at 500 kbps — and a &lt;code&gt;MockTransport&lt;/code&gt; for tests — the protocol code never has&lt;br&gt;
to care.&lt;/p&gt;

&lt;p&gt;The one rough edge: macOS won't let libusb claim an unclaimed USB device&lt;br&gt;
without root. So &lt;code&gt;tuneforge ssm-init --tactrix&lt;/code&gt; needs &lt;code&gt;sudo&lt;/code&gt;. There's no&lt;br&gt;
nice fix without shipping a system extension, which I'm not going to do for&lt;br&gt;
a hobby tool. The GUI shows a clear "rerun with sudo" message in the error&lt;br&gt;
state of the ECU-tools modal and that's about as good as it gets on modern&lt;br&gt;
macOS.&lt;/p&gt;
&lt;h2&gt;
  
  
  The dump in 44 seconds
&lt;/h2&gt;

&lt;p&gt;With seed/key working and Tactrix bytes flowing, the actual ROM dump is a&lt;br&gt;
sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OBD-II identify&lt;/strong&gt; (Mode 09 PID 02 + 06): grab the VIN and the
Calibration Verification Number. Doubles as a "did we plug in the right
car?" check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ExtendedDiagnosticSession&lt;/strong&gt; (&lt;code&gt;10 03&lt;/code&gt;) + &lt;strong&gt;SecurityAccess&lt;/strong&gt; (&lt;code&gt;27 01/02&lt;/code&gt;)
with our generated key. This unlocks the ProgrammingSession.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProgrammingSession&lt;/strong&gt; (&lt;code&gt;10 02&lt;/code&gt;) + &lt;strong&gt;RequestDownload&lt;/strong&gt; (&lt;code&gt;34 04 33&lt;/code&gt;),
followed by &lt;strong&gt;64× TransferData&lt;/strong&gt; (&lt;code&gt;36&lt;/code&gt;) frames carrying the encrypted
&lt;code&gt;OpenECU Subaru SH7058 OCP CAN Kernel V1.07&lt;/code&gt;. The encryption between
&lt;code&gt;36&lt;/code&gt; payloads and what actually ends up in RAM is the ECU bootloader's
business — we just stream the bytes EcuFlash streams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StartRoutine&lt;/strong&gt; (&lt;code&gt;31 01 02 02 02&lt;/code&gt;) — the kernel takes over the ECU.&lt;/li&gt;
&lt;li&gt;The kernel speaks a tiny 1-byte protocol over CAN (&lt;code&gt;01&lt;/code&gt; = read at
address, &lt;code&gt;03&lt;/code&gt; = jump). Loop reads in 2 KB chunks at 500 kbps. 1 MiB
takes the wire ~42 seconds + ~2 seconds for the rest.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most of the credit for understanding the kernel protocol belongs to&lt;br&gt;
&lt;a href="https://github.com/fenugrec/npkern" rel="noopener noreferrer"&gt;&lt;code&gt;fenugrec/npkern&lt;/code&gt;&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://github.com/james-portman/subaru-ecu-flashing" rel="noopener noreferrer"&gt;&lt;code&gt;james-portman/subaru-ecu-flashing&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
The dump bytes match the EcuFlash output exactly (same SHA-256), which&lt;br&gt;
gives a strong "we're doing this right" signal.&lt;/p&gt;

&lt;p&gt;The kernel-upload code lives in a separate crate (&lt;code&gt;tuneforge-kernel&lt;/code&gt;)&lt;br&gt;
licensed GPL-3.0+, because it's derived from GPL-3 upstream. The rest of&lt;br&gt;
the workspace stays under GPL-2.0+ and only pulls the kernel crate in when&lt;br&gt;
you opt in via &lt;code&gt;--features kernel-upload&lt;/code&gt;. This is a tidy way to keep&lt;br&gt;
license boundaries explicit at the Cargo level rather than in a header&lt;br&gt;
comment everyone ignores.&lt;/p&gt;
&lt;h2&gt;
  
  
  The boring stuff that mattered
&lt;/h2&gt;

&lt;p&gt;A few things that aren't glamorous but made the project actually finish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A real fixture.&lt;/strong&gt; I committed a known-good ROM dump
(&lt;code&gt;fixtures/forester-xt-2007-4E42504007.bin&lt;/code&gt;) and the integration tests
load it. Half the protocol bugs I had would have stayed hidden without a
byte-for-byte target to diff against. The dump command does the same diff
at the end of each run and complains loudly if anything drifted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MockTransport&lt;/code&gt; for tests.&lt;/strong&gt; A FIFO of "byte sequences the device will
return" is enough to unit-test the entire SSM/OBD-II/UDS stack without
the car. 220+ tests run in under a second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;egui for the GUI.&lt;/strong&gt; Immediate-mode means the editor's "live diff
against baseline" and the logger's XY-plot are like 30 lines of code
each. No state machines, no observers, no "models". Resize the window,
scroll, drag a cell — it just redraws.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;clippy &lt;code&gt;-D warnings&lt;/code&gt; as a CI gate.&lt;/strong&gt; I started with the pedantic group
and ~300 warnings, spent an afternoon classifying them into "real
problems" (~10) and "doesn't apply to our embedded byte-crunching code"
(~290), put the latter in a workspace allow-list with one-line
justifications, and now anything red is actually red.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Where this is going
&lt;/h2&gt;

&lt;p&gt;The editor + logger + diagnostics + read-rom pipeline is feature-complete&lt;br&gt;
for my own car (a 2007 USDM Forester XT). I'm not implementing flash-write&lt;br&gt;
without a donor ECU, which means tuneforge isn't a full ECU-tuning loop —&lt;br&gt;
yet. What it is, today, is the parts of that loop that don't require risking&lt;br&gt;
a brick. For me that covers ~95% of what I actually do with the car.&lt;/p&gt;

&lt;p&gt;The fun bits if you're poking around the repo: the seed/key code is in&lt;br&gt;
&lt;code&gt;crates/tuneforge-kernel/src/seed_key.rs&lt;/code&gt;, the Tactrix transport is in&lt;br&gt;
&lt;code&gt;crates/tuneforge-io/src/tactrix/&lt;/code&gt;, the full dump-rom orchestrator (with&lt;br&gt;
Wireshark-comment-grade documentation of each phase) is in&lt;br&gt;
&lt;code&gt;crates/tuneforge-kernel/src/orchestrator.rs&lt;/code&gt;, and the GUI panels are in &lt;code&gt;crates/tuneforge-gui/src/panels/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you read this and now want to dump your own Subaru on a Mac:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;libusb
curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-LsSf&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    https://github.com/firefighter-19/tuneforge/releases/latest/download/tuneforge-cli-installer.sh | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;tuneforge dump-rom-can &lt;span class="nt"&gt;--output&lt;/span&gt; ./my-rom.bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not done. A lot of RomRaider's depth is still un-ported (advanced editor features, more vendor protocols, flash-write is an explicit non-goal for now — no donor ECU). &lt;/p&gt;

&lt;p&gt;Issues, ideas, and PRs are genuinely welcome.&lt;/p&gt;

&lt;p&gt;Source, license info, and the full slice-by-slice progress doc are at&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/firefighter-19/tuneforge" rel="noopener noreferrer"&gt;https://github.com/firefighter-19/tuneforge&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built on a 2007 USDM Subaru Forester XT, ROM &lt;code&gt;4E42504007&lt;/code&gt;, Tactrix Openport&lt;br&gt;
2.0, Apple Silicon Mac. Powered by &lt;a href="https://github.com/RomRaider/RomRaider" rel="noopener noreferrer"&gt;RomRaider&lt;/a&gt;&lt;br&gt;
XML definitions, &lt;a href="https://github.com/fenugrec/npkern" rel="noopener noreferrer"&gt;&lt;code&gt;fenugrec/npkern&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
kernel work, and a lot of Wireshark.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
