ADR: SOL Terminal Emulation & OEM Capabilities Encoding Support - device-management-toolkit/deployment GitHub Wiki

ADR: SOL Terminal Emulation & OEM Capabilities Encoding Support

Status

Proposed

Context

Serial-over-LAN (SOL) redirection on Intel vPro and ISM platforms transmits console text and terminal control sequences between the remote target's firmware/operating system and the management console. Currently, ui-toolkit acts as the intermediary processor between the redirection WebSocket stream and the terminal display component (xterm.js), which is consumed by frontend component libraries (ui-toolkit-angular, ui-toolkit-react) and applications (sample-web-ui, console).

As tracked in ui-toolkit Issue #1855 and ui-toolkit Issue #1768, character corruption occurs under two primary operational scenarios:

  1. Chunk-Boundary Multi-Byte Fragmentation: Network packets and WebSocket frames fragment multi-byte UTF-8 sequences (2 to 4 bytes). If an incoming frame cuts off mid-character, standard stateless decoders treat trailing bytes as illegal sequences, emitting \uFFFD replacement symbols.
  2. Terminal Emulation and Character Set Mismatches: Legacy BIOS POST and configuration setup utilities output single-byte extended ASCII (Code Page 437) to render menus, dialog boundaries, and drop shadows (e.g., 0xDA, 0xC4, 0xBF). When parsed as UTF-8, these single bytes violate UTF-8 leading-byte rules, corrupting screen layout (Console Issue #530).

The Intel CSME/AMT Redirection specification establishes that the host communicates its expected terminal format through the 2nd Byte of OEM Capabilities (Terminal Emulation):

  • Bit 0:3 – Emulation Profile:
    • 1 – VT52 (Standard monochrome, 2-byte escape grammar)
    • 2 – VT100+ (ANSI/VT100 baseline with function keys F5–F14; Note: Deprecated and unsupported in Intel CSME 14.0 and later)
    • 3 – VT-UTF8 (Standard streaming UTF-8 Unicode stream with OEM function key F8 mapping)
    • 4 – PC-ANSI (IBM PC-compatible ANSI sequence set with Code Page 437 box-drawing glyphs)
    • 5–15 – Reserved for future architecture definitions

Console operators must have access to accurate default rendering negotiated from hardware capability bits, paired with runtime UI controls to override the terminal mode when legacy firmware communicates invalid or incomplete capability descriptors.


Decision Drivers

  • Spike Objectives (Issue #1855): Define an end-to-end decoding and sequence translation flow for Intel vPro and ISM devices that fixes multi-byte chunk corruption while preserving legacy BIOS display rendering.
  • Preserve Component Boundaries: Keep decoding logic inside TerminalDataProcessor in ui-toolkit so downstream consumers (ui-toolkit-angular, ui-toolkit-react, sample-web-ui) remain clean wrappers.
  • Avoid Screen Distortion in Legacy BIOS: Ensure DOS-style box-drawing characters in BIOS setup menus render correctly as crisp Unicode lines instead of broken question marks or replacement symbols.
  • Handle Network Chunks Safely: Ensure trailing bytes cut across network packets do not crash the decoder or leak memory.
  • Operator Flexibility: Allow users to manually change the emulation mode in the UI if the device reports incorrect capabilities.

Decisions

D1 β€” Decouple Character Decoding from Terminal Sequence Emulation

TerminalDataProcessor in ui-toolkit will manage both a stateful byte decoder and an emulation profile based on the active terminal mode:

  1. VT-UTF8 Mode (OEM Bit 0:3 = 3):

    • Why it is needed: UTF-8 characters can take between 1 and 4 bytes. In network streaming, a packet or WebSocket chunk can end right in the middle of a multi-byte character (for example, receiving byte 1 of a 2-byte character in Chunk $A$, and byte 2 in Chunk $B$). A standard decoder like Buffer.toString('utf-8') fails on Chunk $A$ because the trailing byte looks like an invalid character, producing replacement symbols like (\uFFFD).
    • How it works: Employs a stateful stream decoder using native Web APIs:
      this.decoder = new TextDecoder('utf-8', { fatal: false });
      const text = this.decoder.decode(incomingChunk, { stream: true });
      Setting { stream: true } instructs TextDecoder to hold any incomplete 1- to 3-byte sequence in memory at the end of the chunk instead of erroring out. When the subsequent chunk arrives, TextDecoder automatically prepends those held bytes to complete the character cleanly.
    • Sequence behavior: Standard ANSI/ECMA-48 escape codes pass directly to xterm.js. For keyboard input, function key F8 is mapped to the OEM sequence expected by pre-boot menus (such as <ESC>Oq).
  2. PC-ANSI / CP437 Mode (OEM Bit 0:3 = 4):

    • Why it is needed: Legacy BIOS configuration utilities (such as Aptio, Phoenix, or Insyde BIOS setup screens) do not use multi-byte UTF-8. Instead, they use IBM Code Page 437 (DOS character set), where single bytes between 0x80 and 0xFF represent box borders, lines, corners, and blocks. If the decoder treats these bytes as UTF-8, they violate UTF-8 encoding rules and fail to display, collapsing menus and dialog borders (Console Issue #530).
    • How it works: Instead of UTF-8 decoding, incoming bytes are processed byte-by-byte:
      • Standard ASCII bytes (0x00–0x7F): Converted to standard characters.
      • High-ASCII bytes (0x80–0xFF): Mapped directly to their equivalent Unicode box-drawing characters using a static lookup table before sending the string to xterm.js:
        • 0xDA $\rightarrow$ β”Œ (Top-left corner, \u250C)
        • 0xBF $\rightarrow$ ┐ (Top-right corner, \u2510)
        • 0xC0 $\rightarrow$ β”” (Bottom-left corner, \u2514)
        • 0xD9 $\rightarrow$ β”˜ (Bottom-right corner, \u2518)
        • 0xC4 $\rightarrow$ ─ (Horizontal border, \u2500)
        • 0xB3 $\rightarrow$ β”‚ (Vertical border, \u2502)
        • 0xC5 $\rightarrow$ β”Ό (Cross intersection, \u253C)
        • 0xCD $\rightarrow$ ═ (Double horizontal border, \u2550)
        • 0xBA $\rightarrow$ β•‘ (Double vertical border, \u2551)
        • 0xDB $\rightarrow$ β–ˆ (Full solid block, \u2588)
    • Sequence behavior: PC-ANSI color attributes (<ESC>[3Xm, <ESC>[4Xm) and standard cursor movements are already recognized by xterm.js and pass through without modification.
  3. VT52 Mode (OEM Bit 0:3 = 1):

    • Why it is needed: VT52 is a vintage terminal standard that predates modern ANSI (xterm.js). It uses compact 2-byte escape codes (like <ESC>A instead of ANSI <ESC>[A) and direct character-offset cursor coordinates (<ESC>Y<row><col>). Because xterm.js only understands ANSI/ECMA-48 escape sequences, raw VT52 streams result in unhandled text and failed cursor jumps.
    • How it works: Bytes are decoded as standard 7-bit ASCII (byte & 0x7F). TerminalDataProcessor runs a lightweight translator that intercepts VT52 commands and converts them into the ANSI equivalents that xterm.js expects:
      • <ESC>A (Cursor Up) $\rightarrow$ <ESC>[A
      • <ESC>B (Cursor Down) $\rightarrow$ <ESC>[B
      • <ESC>C (Cursor Right) $\rightarrow$ <ESC>[C
      • <ESC>D (Cursor Left) $\rightarrow$ <ESC>[D
      • <ESC>H (Cursor Home) $\rightarrow$ <ESC>[H
      • <ESC>I (Reverse Line Feed) $\rightarrow$ <ESC>M
      • <ESC>J (Clear to End of Screen) $\rightarrow$ <ESC>[J
      • <ESC>K (Clear to End of Line) $\rightarrow$ <ESC>[K
      • <ESC>Y<row><col> (Direct Cursor Jump) $\rightarrow$ <ESC>[{row - 31};{col - 31}H
    • Keyboard input: Translates local arrow keys back into VT52 sequences (<ESC>A–<ESC>D) when sent upstream to the host.
  4. VT100+ Mode (OEM Bit 0:3 = 2):

    • Why it is needed: Provided for backward compatibility on older firmware (Intel CSME < 14.0). It behaves identically to standard VT100/ANSI with extended function keys F5–F14.
    • Behavior: On modern platforms (Intel CSME $\ge 14.0$), this mode is deprecated by firmware. If encountered, TerminalDataProcessor logs a deprecation warning and operates with standard ANSI handling.

D2 β€” Dynamic UI Configuration and Selection

The UI architecture provides automatic capability negotiation alongside real-time manual operator controls across consumer applications (sample-web-ui, console):

  1. Capability Handshake & Auto-Negotiation:

    • Backend Transparency (MPS / Console): MPS operates as a transparent TCP/WebSocket proxy and does not inspect redirection payload bytes. The OEM Capabilities structure is already transmitted over the wire by the AMT/CSME firmware today; no backend additions or API modifications are required on MPS.
    • Client Extraction in ui-toolkit: During the Intel AMT Redirection handshake (receiving Start Channel Reply 0x21 for SOL), AMTRedirector inspects the 2nd Byte of OEM Capabilities.
    • Bitmask Parsing: The lower 4 bits are isolated via bitmasking:
      const oemTerminalCode = oemCapabilitiesByte & 0x0F;
    • Initial Mapping:
      • 0x01 $\rightarrow$ Initializes SolTerminalType.VT52
      • 0x02 $\rightarrow$ Initializes SolTerminalType.VT100Plus (if firmware CSME < 14.0)
      • 0x03 $\rightarrow$ Initializes SolTerminalType.VTUTF8 (Modern default)
      • 0x04 $\rightarrow$ Initializes SolTerminalType.PCANSI (BIOS default)
    • Fallback Heuristic: If Bit 0:3 is 0 (unspecified) or $\ge 5$ (reserved for future definitions), the client defaults safely to SolTerminalType.VTUTF8 and logs a debug notice.
  2. UI Selector & Toolbar Presentation:

    • Placement: Exposed as a compact <select> or dropdown component in the SOL header/action bar directly adjacent to terminal power controls and connection status indicators.
    • Display Items & Hints:
      • VT-UTF8 β€” "Modern Linux/Windows, UEFI Shell (Default)"
      • PC-ANSI β€” "Legacy BIOS Setup Menus (CP437 Box-Drawing)"
      • VT52 β€” "Basic Monochrome Serial"
      • VT100+ β€” "Legacy only (< CSME 14)" (Visually flagged or disabled when connected to CSME $\ge 14.0$)
    • Negotiation Indicator: Displays an "Auto" badge when the active selection matches the hardware-negotiated OEM capability, switching to "Manual Override" if an operator changes the dropdown.
  3. Runtime Non-Destructive Switching:

    • Component Flow: Dropdown change event $\rightarrow$ UI wrapper binding ((terminalTypeChange) in Angular / onTerminalTypeChange in React) $\rightarrow$ calls terminalDataProcessor.setTerminalType(selectedType).
    • Zero Socket Disruption: The underlying WebSocket and TCP redirection session are not disconnected, restarted, or interrupted.
    • State Buffer Flush: Calling setTerminalType() immediately triggers internal cleanup (this.reset()):
      • Empties any pending 1- to 3-byte partial UTF-8 cache to prevent stale byte contamination.
      • Re-binds the active parser without wiping the rendered screen buffer in xterm.js.
    • Upstream Keyboard Switching: The keyboard input handler instantaneously updates its keymap (e.g., swapping VT52 2-byte arrow keys or VT-UTF8 OEM function key mappings) for all subsequent keystrokes sent to the remote host.

Implementation & Technical Specifications

Strongly Typed Terminal Enumeration (ui-toolkit)

To establish a clear contract across the toolkit and consumer layers, terminal modes are declared via an explicit enum matching the OEM capability values:

export enum SolTerminalType {
  VT52 = 1,
  VT100Plus = 2, // Deprecated in Intel CSME >= 14.0
  VTUTF8 = 3,
  PCANSI = 4
}

Buffer Lifecycle Controls & Memory Safety

To prevent stale byte bleed, memory leaks, or corrupted decoding states across stream lifecycles:

  • Hard 3-Byte Buffer Ceiling: Because valid UTF-8 sequences are at most 4 bytes long, incomplete fragments stored across chunks can never exceed 3 bytes. If a buffer reaches 4 bytes without resolving a valid sequence, the processor flushes \uFFFD, discards the invalid sequence, and resets the cache.
  • Deterministic Session Reset (reset()): TerminalDataProcessor exposes an explicit .reset() method invoked whenever the SOL WebSocket disconnects, reconnects, or when the operator switches the terminal emulation mode in the UI.

Architecture Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               Consumer UI / Sample Web UI              β”‚
β”‚     [ Emulation Selector: VT-UTF8 | PC-ANSI | VT52 ]   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚ (Input / User Selection)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚       ui-toolkit-angular / ui-toolkit-react Wrappers   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚ setTerminalType(type)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 ui-toolkit Core                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚             TerminalDataProcessor                β”‚  β”‚
β”‚  β”‚                                                  β”‚  β”‚
β”‚  β”‚   Incoming Chunks ──► [ Stateful Stream Buffer ] β”‚  β”‚
β”‚  β”‚                                β”‚                 β”‚  β”‚
β”‚  β”‚                     Active Emulation Mode        β”‚  β”‚
β”‚  β”‚            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚  β”‚
β”‚  β”‚            β–Ό                   β–Ό               β–Ό β”‚  β”‚
β”‚  β”‚       [ VT-UTF8 ]        [ PC-ANSI ]       [ VT52 ]β”‚β”‚
β”‚  β”‚     TextDecoder Stream  CP437 Table Map   ANSI Transβ”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”˜
                β–Ό                   β–Ό               β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚              xterm.js Terminal Display            β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Consequences

Positive

  • Resolves Issue #1855: Stateful streaming prevents multi-byte character corruption across split network packets.
  • Restores BIOS Setup Screen Fidelity: Direct CP437 mapping ensures legacy setup menus, borders, and dialogs render as clean box lines instead of corrupt symbols (Console Issue #530).
  • Standard Compliant: Adheres directly to Intel AMT/CSME OEM Capabilities specifications.
  • Operator Flexibility: If firmware negotiates an incorrect terminal mode, the operator can switch modes instantly via the UI dropdown.

Negative / Trade-offs

  • CP437 Lookup Table: Requires embedding a static 128-entry Unicode character lookup array in ui-toolkit.
  • Multi-Package Release Dependency: Changes must be released in order: ui-toolkit $\rightarrow$ ui-toolkit-angular / ui-toolkit-react $\rightarrow$ sample-web-ui.

Implementation & Release Plan

Step Repository Scope of Work
Step 1 ui-toolkit Add SolTerminalType enum, implement TerminalDataProcessor modes (VT-UTF8 streaming, CP437 lookup map, VT52 sequence translation), add buffer guardrails and .reset().
Step 2 ui-toolkit-angular Update ui-toolkit dependency; expose @Input() terminalType and change emitter on the SOL component.
Step 3 ui-toolkit-react Update ui-toolkit dependency; expose terminalType prop and change handlers.
Step 4 sample-web-ui & console Update toolkit dependencies; add terminal selection dropdown in the SOL header/toolbar; bind selection to the terminal session.

References

⚠️ **GitHub.com Fallback** ⚠️