Skip to content

Firmware Embedded Java Application Layer

Kristiyan edited this page Oct 3, 2026 · 22 revisions

The embedded Java application layer — where Bluetooth, climate, and the real UI actually live

Part of Firmware Reversing. Resolves a question this wiki had open for a while: why do well-documented real RNS-510 features (Bluetooth hands-free, steering-wheel/MFSW integration, climate-control display, parking sensors) never appear anywhere in the native PowerPC C++ symbol/EH-descriptor table, no matter how it's searched?

The answer: they're not native code at all — they're Java

A direct raw-byte string search across the complete 85.6MB FHDD6.FLI (not just the 24MB native-code region this wiki otherwise focuses on) for Bluetooth/climate/parking-sensor keywords immediately surfaced literal Java class-path strings — class$vdo$most$dev$app$phon..., Lvdo/dvc/pdc/impl/parrot/CT... — not native symbols. Pulling every Lvdo/.../Lwdg/...-style Java class signature out of the whole file found 2,806 distinct class paths, archived in full at references/java_classpaths_2806.txt. This is a real, substantial, previously entirely unexplored embedded Java application layer — not a handful of stray classes.

Confirms the specific features directly:

  • Bluetooth: SC_bluetooth_audio_EUR, BluetoothCon, BapPhoneListener (ties to the already-documented real BAP MOST-bus protocol — see HOST BSP and Vehicle Coding), HFP_ON/HFP_OFF (real Bluetooth Hands-Free Profile states), handsfreeOnOff, setRingerVolume, m_pairingTimer/m_pairingState. Lvdo/dvc/pdc/impl/parrot/CT... names Parrot — the real French automotive Bluetooth hands-free module supplier — as the actual vendor behind this subsystem.
  • Climate control: wdg.climate.ClimateZone, climateECUServiceableError/Status, notifyClimateECUServiceable, getClimateECUServiceable, solarRoofConfig/solarRoofError.
  • Parking sensors: OPSControlTracker (OPS = Optical Parking System), m_pdcDeffectMode (PDC = Park Distance Control, the real automotive term), matching the vdo/dvc/pdc Java package.
  • A separate, lower-level SPP (Serial Port Profile) init log ("SPP init: no reactor assigned", "SSP: Startup V%2.2f") — C-style error-message phrasing, plausibly a native shim underneath the Java Bluetooth stack rather than part of it; not resolved further.

This is a real, substantial Java application platform, not a thin shim

Clustering all 2,806 class paths by their 3rd-level package reveals a complete architecture — this is the real, user-facing application:

Package Classes Role
vdo/rns/app 370 The main RNS application itself
vdo/nav/api 362 Navigation API
vdo/rns/uii 160 RNS UI implementation
vdo/uis/wdg 105 UI widgets
vdo/most/dev 94 MOST-bus device layer
vdo/jam/dev 93 "JAM" device layer (plausibly Java Application Manager)
vdo/nav/cfc 86 Navigation "Cfc" classes — see below
vdo/app/mm, vdo/svc/mm 82, 75 Multimedia
vdo/nav/ctrl 64 Navigation control
vdo/bsl/util 62 "BSL" utility layer
vdo/mm/cp2, vdo/mex/mm 58, 50 Media/codec
vdo/nav/tri 53 Plausibly trip computer
vdo/app/mediaplayer 34 Media player
vdo/s2i/pim 29 PIM (phone contacts/calendar) — "s2i" plausibly smartphone-to-infotainment sync
vdo/mm/tv 28 TV tuner
vdo/rns/speech, vdo/speech/asr 19, 12 Speech/voice, incl. real ASR (Automatic Speech Recognition)
vdo/dvc/pdc 15 Park Distance Control
vdo/mm/dab 13 DAB radio
vdo/svc/fm 18 FM radio
vdo/sf/diag 19 Diagnostics

Also present: a real standard Java class library subset — java.awt (a full GUI toolkit: Container, GridLayout, event listeners), javax.speech (Central, EngineCentral, recognition.Recognizer, synthesis — a real, standard Java Speech API implementation), javax.media (Codec, Renderer, PlugIn, DataSource), javax.microedition.pim/io (J2ME-style mobile APIs). This strongly suggests a CDC/J2ME-class embedded JVM with a fairly complete AWT + Speech + Media API surface, not a minimal/stripped runtime.

The app framework has a real session/factory architecture: vdo/app/fwk includes AppFactoryAbstract, ApplicationInterface, LaunchableApplication, AppContext, UserContext, and a session factory with a genuine internal codename, SalsaSessionFactory — worth remembering as a project/platform codename if it recurs elsewhere.

A naming-convention link to the native C++ layer: vdo/nav/cfc (86 Java classes) uses the exact same Cfc prefix this whole wiki's native C++ work has run into constantly (CfcTypeCoordinate, CfcMapViewProxy, CfcTypeRoadClassStatistic, dozens more). This is very likely not a coincidence — plausibly the Java layer has classes that mirror, wrap, or call into (via JNI or a similar bridge) the native Cfc* C++ classes this project has been cataloguing all session, consistent with "Cfc" being a cross-language internal framework convention (plausibly "Common Framework Classes" or similar), not something scoped to one language layer. Not independently confirmed — a concrete lead for a future session.

Real decompiled source code — this works, and it's immediately readable

Downloaded a real JDK (via the install-jdk pip package, which fetches an Eclipse Temurin build) and the open-source CFR decompiler (github.com/leibnitz27/cfr), then searched the whole 85.6MB file for the real Java class-file magic number, CAFEBABE. 351 raw matches, but most are coincidental 4-byte collisions, not real classes (a from-scratch class-file parser — walking the constant pool, fields, methods, and attributes to find each class's true end, since class files don't declare their own total length — confirms only 17 are genuine, well-formed class files; the rest fail with nonsense constant- pool tags that don't exist in the real JVM spec). All 17 are real Java 1.0/1.2-era class files (major version 45/46) and all 17 decompile cleanly to correct, readable Java source — CFR needed no special handling. Real confirmed classes include:

// vdo.most.comm.FBlockObserver — the real MOST-bus FBlock interface
public interface FBlockObserver {
    public static final short FBLOCK_REGISTERED = 257;
    public static final short FBLOCK_UNREGISTERED = 258;
    public static final short OVER_TEMPERATURE = 1;
    public static final short UNDER_TEMPERATURE = 2;
    public static final short HIGH_VOLTAGE = 3;
    public static final short LOW_VOLTAGE = 4;
    public static final short MECH_CHANGER_ERROR = 16;
    public static final short MECH_LOADING_ERROR = 17;
    public void fblockAvailabilityChanged(short var1, short var2, short var3);
}

// vdo.nav.api.map.types.map.MapPerspective — real 2D/2.5D/3D map modes
public class MapPerspective extends EnumDescription {
    public static final MapPerspective MAP_PERSPECTIVE_2D = new MapPerspective(0, "MAP_PERSPECTIVE_2D");
    public static final MapPerspective MAP_PERSPECTIVE_2_5D = new MapPerspective(1, "MAP_PERSPECTIVE_2_5D");
    public static final MapPerspective MAP_PERSPECTIVE_3D = new MapPerspective(2, "MAP_PERSPECTIVE_3D");
}

Also recovered: BTAudioPlayer/DefaultBTAudioItem/BTPremiumItem (real Bluetooth audio classes, with BTPremiumItem extending MdiItem — confirming Bluetooth audio is integrated as an MDI device type, matching the unit's official "MDI interface (usb, iPod™)" feature description exactly), TIItem (Traffic Information radio item with real station-preset constants 1620/1629), MediaPlayer.State (STOPPED/PLAYING/PAUSED/ERROR/NO_MEDIA), ExitStreetNameResolver/StreetNameResolver (voice-guidance street naming), AnnouncementPlayer, ICDNavigationVisibilityListener.

Why only 17 when 2,806 class names exist: these 17 are all small (118-809 bytes) interfaces/enums/stub classes — far too few and too small to be the real application. The overwhelming majority of the real 2,806-class application is almost certainly stored in a different, pre-linked/"romized" binary format rather than individual standard class files — a common space-saving technique for old embedded J2ME/CDC-class JVMs (e.g. Sun's own JavaCodeCompact romizer tooling used exactly this approach). These 17 survived as plain, standalone class files most likely because they're loaded dynamically/reflectively at runtime (interfaces and small implementation stubs are classic candidates for that), while the bulk of the application was pre-processed into the ROM image and no longer exists as individual CAFEBABE-prefixed files. Recovering the rest is a separate, larger task: finding and reverse-engineering whatever custom romized-class container format this JVM uses — not yet attempted.

Full CFR output for all 17: archived at references/java_decompiled_17classes.txt.

The real JVM is identified: Wind River's Jeode EVM (licensed from Insignia Solutions)

Direct, unambiguous string evidence: "(c) Copyright Insignia Solutions plc, 1998 - 2002", jeode.boot.library.path, EVM_HOME, com.jeode.evm.version, java.vendor → "Wind Ri[ver]". Independently confirmed via public sources (Insignia licensed Jeode's Embedded Virtual Machine to Wind River for VxWorks/PowerPC; Jeode implements Sun's PersonalJava 1.1.1 and EmbeddedJava 1.0.3 specifications — real, standard, now-obsolete Java profiles from the late 1990s/early 2000s embedded-Java era).

A real preload/std/ClassPreloaderImpl class is referenced directly (getConfigurationURL method name visible) — this is almost certainly the actual romizer/class-preloading mechanism responsible for why only 17 of 2,806+ classes survive as standalone, standard CAFEBABE class files. Checked whether ClassPreloaderImpl itself could be recovered the same way — it can't: zero CAFEBABE markers exist anywhere within 200KB of its own string reference, confirming its own class body is embedded in the pre-linked/romized bulk, not a standalone survivor. Fully cracking the romized container format is a separate, substantial task — no technical specification is publicly available for it (only Insignia/Wind River marketing material), unlike the compiler-manual-driven win this project had earlier for .frame_info. A concrete starting point for a future session: the real on-disk/in-ROM container likely has its own recognizable header structure (distinct from standard .class files) immediately preceding the dense run of Lvdo/.../Lwdg/... name strings — not yet investigated byte-by-byte.

Sources: EE Times — Insignia's Jeode Available for Wind River's VxWorks on PowerPC, Wind River press release.

What this means for the rest of this project

The native PowerPC C++ code this wiki has spent this whole project reverse-engineering (dbal, navicore, TMC, weather, SDARS persistence, the EH/exception-handling mechanism, etc.) is real and substantial, but it is a lower-level data/service layer, not the user-facing application itself — the actual UI, navigation display logic, media playback, Bluetooth/phone handling, voice/speech, and climate/parking-sensor integration all live in this separate, much larger (2,806+ classes) embedded Java layer this project has never examined. This also retroactively explains the "Java-class content" region found during the 9MB+ wall investigation (see Firmware Reversing) — that was only one narrow ~4.24MB slice of a much bigger, pervasive Java presence spanning tens of megabytes across the whole 85.6MB file (class-path string hits were found from file offset ~1MB out to ~82MB in this session's search).

UPDATE, done the same session — real class/image extraction now works, and has its own reusable tooling in the project's main repo (not this wiki): research/firmware_java_reader.py scans any firmware file live for real Java class-path strings, real standalone .class files, and real embedded screen images; research/ firmware_java_decompile.py decompiles recovered classes to actual Java source via the open-source CFR decompiler, auto-installing a portable JDK + CFR on first use (nothing binary committed to the repo); rns510_firmware_viewer.py is a new tkinter GUI exposing all of this (package tree, decompiled source, real screens) — a sibling to rns510_map_viewer.py, not a change to it, since that tool renders the map disc, not the firmware.

Results: of 351 raw CAFEBABE occurrences, only 17 are real, well-formed class files (the rest are coincidental 4-byte collisions — confirmed by writing a full class-file structure parser, since class files don't declare their own length, and most "hits" fail on invalid constant-pool tags that don't exist in the real JVM spec). All 17 decompile cleanly — see the earlier section above for real examples (FBlockObserver, BTAudioPlayer, MapPerspective, etc.). Of image signatures tried, GIF (6-byte magic, no real false-positive risk) found 11 real 800×480/100×50 images, of which 10 now decode (8 fully, 2 partially) — real per-brand "Software update" splash screens (SEAT, Škoda, VW, and others) confirming the exact real screen resolution visually, not just from widget-bounds strings, plus real UI widgets like an actual "CANCEL" button icon:

Recovered CANCEL button icon Recovered splash screen

Getting from 4 to 10 took 3 real, separate fixes, not just a bigger window — each targeting a different manifestation of the same underlying phenomenon this whole page keeps running into: small, localized, reproducible corruption scattered through this firmware dump, independent of file format.

  1. A wrong color-table-size bit (most of the full-screen splash images): several GIFs declare a wrong global-color-table-size field in their Logical Screen Descriptor header — the declared size claims more color-table bytes than are actually present, so the real image descriptor sits earlier than the header says to look (e.g. 0xf7 where 0xf5 would give the correct, much smaller table — a single flipped bit). Fix: locate the real image descriptor by content instead of trusting the declared size — every real example here is a single full-screen frame starting left=0,top=0,w=800,h=480 — then rebuild a corrected header with the right color-table length. (_recover_gif_with_corrected_gct) This alone took recovery from 4 to 8 of 11.
  2. A falsely-claimed color table on small icons: a real 100×50 UI button icon's image descriptor claims a local color table that doesn't actually exist. Fix: rebuild assuming no color table at all (Pillow then falls back to a default grayscale palette — real pixel shape is recovered even though the true button color isn't) and brute-force the real LZW-data start position, validating each candidate with an actual decode rather than guesswork. (_recover_gif_icon_without_table) Recovered 2 real button icons this way — the first non-splash-screen, actual UI widget assets found in this firmware.
  3. Genuine corruption inside the compressed LZW codes themselves (not just the header): for a few images, even a correctly-located image descriptor and color table still won't decode — the damage is inside the compressed data, the GIF counterpart to the streamed Java classes' DEFLATE corruption. Standard LZW can't resynchronize after one bad code (the code table is stateful), so there's no clean "skip and continue." Built a from-scratch instrumented LZW decoder that decodes as many genuinely-correct leading rows as possible and stops at the first real corruption, returning only that real partial image (ImageInfo.partial=True) rather than nothing. Recovered 2 more this way — 59-60% of the image height decoded cleanly in both cases, enough to clearly identify a previously-unseen Škoda (red bar) and Volkswagen (green bar) variant screen, evidently a brand × status matrix of update screens:

Partial Škoda screen Partial VW screen

All 3 fixes are wired into find_embedded_images() as generic, content-validated fallbacks (_recover_gif_with_corrected_gct → _recover_gif_icon_without_table → _recover_gif_partial_rows), not one-off patches for specific offsets, so they apply to any firmware file. The 1 remaining failure corrupts almost immediately (under 1% of rows recoverable) and isn't worth returning.

Confirmed and ruled out: the file's already-known 24-85.6MB "Java/ VxWorks-kernel" region does contain real class-path strings (spanning roughly 1MB-82MB, not confined to one narrow band), but the bulk of the ~2,800-class application is NOT recoverable as standalone class files — it's pre-linked/"romized" into Jeode's own undocumented bundle format. That remains the real, still-open task (see the Jeode section below).

A 2nd, richer recovery path: real classes inside streamed ZIP/JAR entries

Separately from the standalone CAFEBABE class files above, this firmware's real OSGi bundle packaging mechanism (vdo/rio/impl/fwk/ BundleManifest/Archive, referencing a real META-INF/MANIFEST.MF and the standard OSGi manifest header vocabulary — Bundle-Name, Export-Package, Bundle-ClassPath, Bundle-Activator, etc. — all found as literal string constants inside that framework's own code) stores some class entries as streamed ZIP local file header + DEFLATE data pairs — general-purpose bit flag 3 set, meaning sizes/CRC32 are deferred to a trailing data descriptor rather than declared up front.

Same false-positive lesson as every other magic-number search this project has done: of 123 raw PK\x03\x04 hits, only a minority validate against a strict filter (plausible filename bytes + successful immediate DEFLATE decompression) — loosening the filter (longer names, larger extra fields, bigger decompression window) didn't find more exact matches (still 7), but checking the NAMES of the 30 candidates that fail to decompress at all found they are ALSO unmistakably real class names (ClimateControlAdapter.class, DvdPlayer.class, JunctionType.class, even a brand-specific ICDDisplayDeviceStateMachineMonochrom.class for Audi) — meaning at least 37 real classes sit in this one narrow region alone, not just 7, all blocked by the same underlying decompression problem. These are a much richer catch than the 17 standalone classes: real implementation classes, not just tiny interfaces/enums — vdo/rns/app/icdent/impl/toolkit/DefaultBTAudioPlayer.class (890 bytes), vdo/rns/app/icdent/impl/std/ DefaultEntertainmentServiceBuilder$4.class (7,098 bytes — the largest real class body recovered from this firmware so far).

Only 1 of 7 "exact match" entries currently decompiles cleanly (DiagnosysService$1, a trivial empty anonymous class). The other 6 all reach a structurally valid DEFLATE end-of-stream (eof=True, ruling out simple truncation) but produce a class file CFR can't parse (corrupted-looking constant pool). Rigorously narrowed down, ruling out 4 hypotheses in turn (the 4th — a premature BFINAL bit — was this project's own earlier working theory, written up in a previous session; a from-scratch instrumented decoder built to go fix it instead disproved it):

  1. Multi-frame concatenation — ruled out: the leftover bytes after the first frame don't decompress as a 2nd standalone DEFLATE stream.
  2. Shared preset DEFLATE dictionary — ruled out directly: tried decompressing with several real candidate dictionaries (the one working class, a standalone class, all 17 concatenated, even reversed) via zlib.decompressobj(-15, zdict=...) — the output is byte-identical regardless of dictionary, including with none at all, meaning the stream never actually references one.
  3. Wrong format/decoder bug — ruled out: tried standard zlib-wrapped mode (wbits=15/31/47, all fail outright — only raw deflate at the exact computed offset works at all) and cross-checked Python's zlib against Java's own java.util.zip.Inflater (the real, original JVM family this firmware's JVM descends from) on the exact same compressed bytes — byte-for-byte identical 890-byte output, ruling out a decoder implementation quirk.
  4. A premature BFINAL bit (the leading theory from the previous session) — also ruled out, by building a from-scratch, instrumented, bit-level raw-DEFLATE decoder (independent of zlib/Inflater) that logs every block header and every decoded LZ77 symbol. Run with BFINAL deliberately ignored (so it would keep decoding past it looking for a hidden 2nd block), every one of these 7 entries turns out to be exactly one single DEFLATE block, with BFINAL=1 set correctly and legitimately in the very first 3 bits of the stream — there is no 2nd block hiding afterward, and nothing about the block boundary is premature. (Each class really was compressed on its own, as a single short, complete, correctly-framed DEFLATE stream — unsurprising in hindsight.)

What's actually happening, now precisely isolated: the DEFLATE decoding itself is 100% clean, for every byte, in every case — every Huffman code and every LZ77 length/distance back-reference decodes validly and in-bounds, all the way to a legitimate end-of-block symbol. The corruption is already present inside the correctly-decompressed bytes themselves — e.g. in DefaultBTAudioPlayer.class, a 50-byte UTF8 constant-pool entry starts out perfectly (vdo/rns/app/icdent/, reusing two genuine earlier back-references to identical text 43 bytes back — completely normal LZ77 behavior) and then, mid-string, decodes two literal bytes 'e', 0x00 where real path text should be — bytes that a correctly-functioning compressor could only have emitted if they were genuinely present in its input, since the Huffman/LZ77 decode around them is provably lossless. This rules out a decompression bug entirely: the corruption predates compression.

Two further findings pin this down hard:

  • It's reproducible byte-for-byte across unrelated firmware builds. The exact same DefaultBTAudioPlayer.class entry, pulled from two completely different release trains (5238_MOD_C3_C4 / 5238_update_original on one hand, and the much older 3890 build on the other), decodes to the exact same corrupted byte sequence (...icdent/e\x00e\x00...) at the exact same offset, before the two diverge further on (consistent with the rest of each build's constant pool differing). Storage bit-rot or a one-off extraction glitch would never reproduce identically across independently-built firmware images — this is a deterministic artifact of the original build toolchain.

  • The damage is small and localized, not systemic — but its position varies per class, which rules out a simpler theory than it first looked like. A full structural walk of all 6 failing entries (not just reading CFR's one reported error) gives a clean 3-way split:

    class constant pool class header (access_flags/this_class/super_class/interfaces_count) right after it where it actually breaks
    TIPlayer$ActivationMode clean (22 entries) clean (0x0021, this_class=2, super_class=3, 0 interfaces — all valid) after 2 clean fields (real ConstantValue attributes) and one entire clean method (isValid, real descriptor (I)Z, a real, plausible Code attribute body: max_stack=2 max_locals=1 code_length=16) — breaks at the 2nd method's own access_flags/name/descriptor
    BTPremiumItem clean (7 entries) garbage (this_class=0, super_class=1799, interfaces_count=114) immediately at the cp/header boundary
    Mp3ActionEvent clean (30 entries — independently re-confirmed by parsing the pool open-ended, ignoring the declared count entirely; it naturally stops at the exact same byte, ruling out a corrupted cp_count as the cause) garbage (access_flags=0, this_class=25461, super_class=29703, interfaces_count=97) immediately at the cp/header boundary (not inside method bytecode as an earlier pass on this page guessed from CFR's error text alone — the real break is right here)
    DefaultBTAudioPlayer breaks mid-pool (the garbled UTF8 entry documented above) — inside the constant pool itself
    BTPremiumPlayerBuilder$3, DefaultEntertainmentServiceBuilder$4 also break mid-pool — inside the constant pool itself

    So corruption isn't anchored to any single structural landmark (not always "right after the pool", not always "deep in bytecode") — it shows up at a different relative position in each class. Combined with the earlier finding that it's byte-for-byte reproducible across unrelated firmware builds, this is most consistent with a reproducible bug in whatever original tool carved each class's bytes out of Jeode's one big romized blob — e.g. miscomputing which bytes to copy for a given class (not a random storage fault, which wouldn't reproduce identically across builds, and not a clean alternate format, since the garbage header values don't follow any consistent new convention either). The large index-like numbers seen in these header fields and in CFR's errors (#15539, #25964, this_class=25461, super_class=29703) remain suggestively similar in magnitude to each other, keeping the "leftover reference into Jeode's larger shared romizer table" idea alive as a plausible reason the extraction tool reads the wrong bytes, but no consistent transformation between these numbers has been found yet, and it's no longer the leading open question — where the tool goes wrong per class is.

Follow-up this session: checked exactly that. Across all 6 failing classes, the real/good content always runs out somewhere in the first ~100–500 decompressed bytes — regardless of the class's total size (a 7,098-byte class and a 3,015-byte class both break well under 500 bytes in; a 160-byte class, the only one with nothing beyond that range, decodes 100% clean). That alone rules out anything tied to total class size. Checked firmware offset (idx, data_start) modulo every plausible block size from 128 bytes to 64KB (flash sector/erase -block granularities) against each class's break offset — no correlation at any of them, so it isn't simple physical-address alignment either.

What does show a pattern: the garbage past the break point isn't random noise. In Mp3ActionEvent.class, a 13-byte fragment of the class's own real text (toolkit/Enter, part of the real class-path string vdo/rns/app/icdent/impl/toolkit/EntertainmentEvent, itself decoded cleanly earlier in the very same file) reappears in the "corrupted" region 127 bytes later, amid runs of 0x00 padding and other scrambled single-byte fragments; DefaultEntertainmentServiceBuilder$4 shows the same shape of damage — real substrings (ChangeSlotStrategy) dissolving into short scrambled echoes and heavy zero-padding. This is the same repeating-fragment signature noted in earlier passes on this page — but this session independently proved, three different ways, that our own ZIP/DEFLATE decoding is provably lossless and complete end-to-end (the from-scratch bit-level decoder, and Python zlib vs. Java's own Inflater producing byte-identical output). A real LZ77 -style decoding artifact appearing in bytes our own decoder reproduces faithfully means that artifact must already have existed in the plaintext fed into our compressor — i.e. it's inherited from an earlier, upstream reconstruction step, not introduced by the ZIP packaging this page has been analyzing.

Best-supported conclusion for now: the real fault lies further upstream than anything this page's tooling can see — most plausibly in Jeode's own ROM-compaction/compression scheme (itself apparently some kind of LZ-style dictionary format, given the repeating-fragment artifacts) failing or being imperfectly reconstructed by whatever tool pulled individual class bodies back out of the romized image, reliably recovering only each class's first ~100–500 bytes before the reconstruction goes wrong. This is close to the practical limit of what's recoverable without actual documentation or source for Jeode's romizer internals.

A real, patented answer for why: Insignia Solutions' "global constant pool"

Chasing this further in a later pass turned up something concrete, not just speculation: Insignia Solutions itself holds a real, issued patent for exactly the kind of mechanism this page has been circumstantially inferring — US7240341B2, "Global constant pool to allow deletion of constant pool entries" (inventors Wayne Plummer and Jerry Kramskoy; see also its application publication US20030172194A1). It describes exactly this: a shared, global constant pool across the whole application, with per-class constants that are determined to be common/invariant moved out of each class's own ("load-unit specific") constant pool, after which the original entry is deleted. References to a moved constant are rewritten using a different, new opcode to distinguish a global lookup from a local one — the patent's own example:

rewriting fragment LDC 14 (meaning load the constant value at entry 17 in the load-unit specific constant pool) to LDCG 523 (meaning load the constant value at entry 523 in the global constant pool)

This reframes this entire section: the "corruption" this page has been chasing is very plausibly not corruption at all in the usual sense, but this page's own tooling (and CFR) correctly reading genuinely-Jeode-specific bytecode and structural fields using standard-JVM-spec assumptions — e.g. CFR's "attempted to access entry #15539" in a 29-entry pool, or this page's own this_class=25461 found above, are exactly the shape you'd expect from a real reference into a large global pool shared across the whole ~2,800-class application, landing in a field our tooling still interprets as "must be a small local index."

The patent is architectural, not an implementation spec — it doesn't give the actual LDCG opcode byte value, its operand width/format, or confirm whether the same global-pool trick extends beyond simple constant loading to structural references like this_class/ super_class/field and method refs (its own text scopes it to "constants" specifically). So this doesn't hand over a working decoder, but it does turn "maybe a shared table, reason unknown" into a specific, named, real, and citable mechanism — a much stronger place to resume from than before.

The decisive test: comparing the SAME class across 20+ independent firmware builds

Trying to act on the concrete next step above (hunting for a global-pool opcode in TIPlayer$ActivationMode's bytecode) led somewhere more directly useful first: this repo has FHDD6.FLI from 21 different firmware releases, spanning years of revisions (3810 through 6276_original_update). Since streamed-class recovery works on any firmware file, the obvious thing to try was comparing the exact same class across all of them.

The result settles the question with real statistical weight, not speculation:

  • BTPremiumItem.class is byte-for-byte identical across all 21 builds — zero differences, anywhere, in any version, ever built for this product line. Its "garbage"-looking header (this_class=0, super_class=1799, interfaces_count=114, documented earlier on this page) isn't corruption at all by any reasonable definition of the word — it is the exact same, perfectly reproducible 113 bytes every single time. Whatever produces it does so completely deterministically.
  • Mp3ActionEvent.class diverges from the reference build at almost exactly the same byte (432, with a handful of builds off by 3-5 bytes due to small genuine content differences before that point) in every one of 20 builds — even though each build's total decompressed size varies wildly (1,608 to 3,015 bytes), meaning each build's real underlying method actually differs in length (the implementation evidently grew/changed over the product's lifetime), yet every single one independently hits trouble at the same spot.
  • TIPlayer$ActivationMode.class shows the same thing: total sizes range from 403 to 5,512 bytes across builds, but every one matches the reference byte-for-byte for the first ~326-336 bytes before diverging.

This is far stronger evidence than anything else gathered this session (which relied on 2 builds at most). With 20+ independent data points, "random storage corruption" or "build-specific damage" are no longer remotely plausible — this is a fixed, per-class, build-independent cutoff, reproduced perfectly by whatever process has generated every single release of this firmware for over a decade. Different classes get different cutoffs (BTPremiumItem effectively "never", TIPlayer around 330, Mp3ActionEvent around 432) — but each class's own cutoff never moves, no matter how much its real implementation changes underneath it release to release.

That reframes the right way to think about this data entirely: this isn't "a complete class that got corrupted" — it's much more likely a fixed-size, per-class allocation (probably computed once, early, and simply never revisited even as the class grew) that was only ever designed to capture a bounded prefix, with whatever follows being incidental leftover content (not meaningfully "wrong", just not intended to be read past that point). A related but distinct public mechanism exists in this same technology family — US7096466, "Loading attribute for partial loading of class files into virtual machines" — describing deferred/lazy loading of whole methods or attributes marked by a dedicated attribute; it doesn't match what's observed here precisely (our cutoffs land mid-structure, not on clean method/ attribute boundaries), but confirms partial/bounded class loading was a known, deliberately-designed feature area for VMs in this lineage, not just an accident.

Finding the formula: the cutoff scales with each class's own constant pool

Acting on that next step directly: this repo holds FHDD6.FLI from 21 different firmware builds, which turned out to cover 156 distinct real streamed-class names in total (not just the original 7 found on one reference build) — 30 of them with 3+ independent build samples, enough to measure a reliable cutoff for each. Correlating each class's cutoff against several structural properties measurable from its own (always-intact) constant pool:

feature correlation with cutoff
constant-pool byte length (cp_end) r = 0.985
total UTF8 bytes in the pool r = 0.972
constant-pool entry count r = 0.899
UTF8 entry count r = 0.916

cp_end wins clearly. A linear fit across 16 classes gives cutoff ≈ cp_end + 44 (slope 0.998, i.e. almost exactly 1-to-1), with real classes scattered roughly ±25-50 bytes around that line — genuinely strong (r² ≈ 0.97) but not perfect: Mp3ActionEvent, cross -checked over all 20 of its builds specifically, has a constant cp_end of 430 in every single one, yet its own cutoff sits right around 432 — far closer to cp_end + 0 than cp_end + 44 for this one class. So the right way to read this: the dominant factor by far is each class's own constant-pool length, not some unrelated fixed buffer size — exactly what you'd expect if whatever process generates these entries correctly walks and copies the real constant pool in full (whatever its length), then reliably captures only a small, roughly-fixed amount of whatever comes after it.

Reusable tooling: research/firmware_java_reader.py's find_streamed_zip_classes() implements the validated extraction (real filename + decompressed bytes + whether it's fully_valid); wired into rns510_firmware_viewer.py's class list alongside the standalone classes (partial/corrupted ones are clearly flagged, not hidden). Partial classes now also carry a trustworthy_prefix_estimate (via _estimate_trustworthy_prefix()), an honestly-caveated application of the formula above — e.g. it correctly says BTPremiumItem.class's full 113 bytes are trustworthy (matching its 21-build-identical result), while knowingly overestimating for outliers like Mp3ActionEvent.class. Shown in the viewer's class list as "~N bytes trustworthy" for any streamed class that isn't fully valid.

Scaling recovery up: real signatures for classes that still can't fully decompile

Acting on the formula directly: 156 distinct real streamed-class names are findable across this repo's 21 firmware builds (not just the original 7 on one build), and an obvious next step is to try patching each one into something CFR can read at all, using only its real, intact constant pool.

A first attempt — truncate to the pool, then declare 0 fields/0 methods — proved pointless: CFR happily decompiled the result, but with nothing in it (public abstract class X extends X {}), since there was nothing left to print.

The useful version mines the pool itself for real content first: Fieldref/Methodref constant-pool entries whose "class" portion references the class itself are an extremely common, naturally -occurring pattern (a class reading its own field, or calling its own method), and their NameAndType entries hand over the real field/ method name and real type signature — straight from data that's already independently confirmed intact. Declaring the mined fields as plain public fields and the methods as public abstract (so no real Code attribute/bytecode is needed at all) is enough for CFR to print them.

Across all 156 names, this recovers real signatures — not just an empty shell — for 29 classes, including genuinely interesting real application data:

// real ignition/power states (VW "clamp 15/30/50" terminology)
public abstract class VehicleClampStatusEvent {
    public short m_clampS;
    public short m_clamp15;
    public short m_clamp50;
}

// real RDS (radio data system) fields
public abstract class FMListEntryInfo {
    public int pICode;
    public int freq;
    public String psName;
    public short pTYCode;
    public short trafficProgramInfo;
    public short globalQual;
}

// real touchscreen calibration data
public abstract class CalibrationPoint {
    public int m_expectedX;
    public int m_expectedY;
    public int m_actualX;
    public int m_actualY;
}

This is real signatures, not real behavior — method bodies are always fabricated stand-ins, never the true implementation, and this is clearly labeled wherever it's shown (the method/field shape is real and useful architectural information; what the code actually does inside is not recoverable this way). research/ firmware_java_reader.py's reconstruct_class_signatures() implements this, and rns510_firmware_viewer.py falls back to it automatically whenever a normal decompile of a partial/corrupted streamed class fails outright, with a clear on-screen note that what follows is signatures-only.

Two further scaling attempts, tried and ruled out: (1) the other firmware component images in this repo (RADIO.FLI, DAB.FLI, GATEWAY.FLI, SWL.FLI, MPEGAPPS.FLI, etc.) were checked for any embedded Java at all — zero class-path strings, zero CAFEBABE hits, zero streamed-ZIP headers in any of them; the Jeode layer lives exclusively in FHDD6.FLI (unsurprising — those other images are almost certainly native firmware for other ECUs in the car, e.g. the radio tuner and CAN gateway, not the Java-based nav/infotainment head unit). (2) Tolerating a constant pool that breaks partway through (salvaging whatever entries parsed validly before the break, instead of requiring the whole pool to parse cleanly) looked promising at first — it can still return plausible-looking synthesized bytes — but real decompile attempts show why reconstruct_class_signatures() deliberately requires a fully clean pool: once the pool's internal consistency breaks at one point, index values that still parse structurally can resolve to the wrong entry type elsewhere (CFR: ClassCastException: ConstantPoolEntryUTF8 cannot be cast to ConstantPoolEntryClass) — confirmed to add zero net-new real recoveries across all 156 class names when actually tested through CFR, not just checked for "did bytes come out."

First visual prototype: a boot → menu → select mockup

With the Java-recovery avenue thoroughly mined for now (and the last two scaling attempts above both coming back negative), this session pivoted to the thing all this recovery work was ultimately in service of: rns510_firmware_viewer.py now has a Simulator tab that assembles what's actually been recovered into a real, clickable mockup rather than leaving it as separate inspector tabs.

Simulator: boot screen Simulator: menu grid

Everything shown is real and computed live from whatever firmware file is opened — nothing is hardcoded:

  • Boot screen: cycles through every real, fully-decoded 800×480 splash image actually recovered from the opened firmware (typically 8-10 — see the GIF-recovery sections above), with Prev/Next controls and a caption showing each one's real offset and dimensions, rather than only ever showing the same one:

Simulator: cycling through real recovered screens

  • Menu grid: the real vdo/rns/app/* package tree — starting from the top-level names (nav, icdmgr, icdent, mda, rdo, commgr, ...), sized by how many real classes were actually found under each. Drillable: each button descends one level further into the real package structure (with a breadcrumb + Back button), bottoming out at the real leaf class names once there's no more real sub-package structure to descend into — real structural information the whole way down, not invented labels at any level.
  • CANCEL button: the real recovered 100×50 button-icon asset, used as an actual clickable widget.

Simulator: drilled into vdo/rns/app/nav

What this is NOT: real, verified menu logic. Drilling down just walks the real package tree — there's no recovered navigation/dispatch bytecode behind it, since the bulk of the real application is still pre-linked into Jeode's romized format this page documents working on above. The tab says so directly on-screen, not just here. This is a first-pass visual skeleton — honest about what's real (the assets and the package tree) versus placeholder (the navigation logic) — not a finished simulator.

Connected to real decompiled source: ClassFileInfo (standalone classes) now carries the class's own real name too, resolved from this_class during the same constant-pool walk find_class_files already does — matching what StreamedClassInfo already exposed. When the Simulator's drill-down reaches real leaf class names, any that also happen to be one of this firmware's real decompilable classes (standalone or streamed) render as a clickable green button instead of a plain label; clicking jumps straight to the Decompiled Classes tab and shows the real source. This is genuinely narrow in practice — most of a package's real classes aren't independently recoverable as full class files, so most leaf names stay plain labels — but on the reference firmware it does work, e.g. drilling into vdo/rns/app/icdmgr/std surfaces ICDNavigationVisibilityListener as a real, clickable, fully-decompiled interface:

Simulator: a real leaf class linked to its real decompiled source

This also caught and fixed a real gap in the drill-down itself: a package can legitimately have both real sub-packages and real classes declared directly at the same level (as icdmgr/std does above — 2 sub-packages, 16 direct classes) — the first version of the drill-down only ever showed one or the other.

Opens directly from a firmware ISO, no extraction: File > Open Firmware now accepts a firmware ISO directly (alongside a raw .FLI file) — the main apps image (FHDD*.FLI) is located and read straight out of the ISO's own byte range via rns510_iso.py's existing find_file()/open_file_ref(), the same no-extraction, direct-from -ISO pattern already used elsewhere in this project (research/ swl_5238_reader.py's find_screen_resolution()). Nothing is ever written to a separate extracted file on disk. Verified byte-for-byte (sha256 match, in ~0.3s) against a separately-extracted copy, and the full viewer — status bar, class/image counts, Simulator — works correctly end to end when opened this way:

Simulator populated directly from a firmware ISO, no extraction step

Swept across every firmware ISO in this repo (27 distinct real discs, spanning this project's full range of eras/variants) as a follow-up check: every one resolved correctly, in 0.06-0.35s each, zero exceptions. Notably this included real cases find_file()'s own docstring specifically calls out but hadn't actually been exercised end-to-end before: North-America discs (RNS510_4366.ISO, US_RNS -510_SW1140.iso) correctly resolving the per-variant NARBY/PROG/ path rather than the standard RNSMIDEC/PROG/ one, and the older FHDD.FLI (no "6") naming on SWL_CD_1020_1022.iso and firmware_900.iso. The 2 non-firmware discs in the archive (CD_8555 .ISO, the map disc; VIM_Berto89.iso) correctly reported "not found" rather than crashing.

Reproducing this work

import re

with open("FHDD6.FLI", "rb") as f:
    data = f.read()

# Real Java class-path signatures (JVM constant-pool UTF8 entries referencing
# class names) live as plain ASCII throughout the whole 85.6MB file, not just
# the 24MB native-code region.
pat = re.compile(rb"L(vdo|wdg)/[A-Za-z0-9_/]{3,80}")
classpaths = {m.group(0) for m in pat.finditer(data)}
# 2,806 distinct paths found this session; see references/java_classpaths_2806.txt

For real class-file/image extraction and decompilation, use the project's own research/firmware_java_reader.py and research/ firmware_java_decompile.py (not duplicated here — see those modules' own docstrings for the full API), or just run the new rns510_firmware_viewer.py GUI directly.

Clone this wiki locally