Commit Graph

30 Commits

Author SHA1 Message Date
75a51def79 fix: correct keyboard scancode mapping from KeyTranslator.java
All checks were successful
CI / fmt (push) Successful in 39s
Publish / frontend (push) Successful in 45s
CI / check (push) Successful in 1m22s
CI / clippy (push) Successful in 1m38s
Publish / backend (push) Successful in 2m48s
The mapping was built assuming keynr followed physical keyboard order
with Escape=0. In reality, KeyTranslator.java maps Java VK_* codes to
keynr values with a different layout:

- keynr 0 = Backquote (not Escape)
- keynr 59 = Escape
- keynr 27 = Enter (not 40)
- keynr 40 = Backslash (not 42)

This caused the number row and QWERTY row to be off by 1 (Escape
was inserted at position 0, pushing everything). The home row
(CapsLock=28, A=29...L=37) happened to align by coincidence, which
is why 'g'(33) worked but 'r'(should be 18, was 19=T) didn't.

Also fixes: F1-F12 off by 1, PrintScreen/ScrollLock/Pause off by 1,
numpad operator keys swapped with numpad 7/8/9.

Corrected both Rust (input.rs) and TypeScript (input.ts) mappings
to match the authoritative KeyTranslator.java VK→keynr table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 10:42:02 +03:00
4f7d69c75a fix: Tight palette selector 0 reads two separate color bytes
All checks were successful
CI / fmt (push) Successful in 46s
Publish / frontend (push) Successful in 51s
CI / check (push) Successful in 1m28s
CI / clippy (push) Successful in 1m35s
Publish / backend (push) Successful in 2m45s
When pal_selector=0 (no predefined palette), the Java code reads two
separate full bytes for the 2-color palette (line 421-422 of
ByteColorRFBRenderer.java). We were reading one packed byte and
splitting into nibbles, consuming 1 byte too few and misaligning all
subsequent reads including the zlib compressed data.

This caused "deflate decompression error" on stream 1 because the
zlib header (78 da) was offset by 1 byte.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 09:55:21 +03:00
acf99f849b feat: phase 9 — encoding 9 (IIP) with tile-versioned delta cache
All checks were successful
CI / check (push) Successful in 1m22s
Publish / frontend (push) Successful in 43s
CI / fmt (push) Successful in 54s
CI / clippy (push) Successful in 1m40s
Publish / backend (push) Successful in 2m27s
codec/iip.rs:
- TileCache: (fb_width/16 × fb_height/16) tiles, 8 versions × 256 bytes
  each, matching t.java's (8, 16*16) allocation
- TileEntry: versioned read/write at byte offsets within tile data
- decode_iip() handles all 4 modes from the control byte:
  - Mode 0/12 (cache-read): tile control bytes select which cached
    version of each 16x16 tile to display, no new pixel data on wire
  - Mode 4 (write-only): Tight-decoded pixel data written to cache
  - Mode 8 (update+read): conditionally writes new data to cache
    (bit 7 of control byte = 0 means update), then reads from cache
- Tile control bytes compressed via zlib (varint length) when >= 12
- Sub-types 1-4/8 map to bit-depths 1/2/4/4/8 bpp
- Cache resized on framebuffer resize (ModeChange msg 128)

Wired into session dispatch for encoding 9. Not advertised in default
encoding list — only active if explicitly requested.

codec/tight.rs: made get_or_init() pub for IIP's zlib access.

39 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 09:39:49 +03:00
e39555196d chore: deployment todos 2026-05-07 09:23:39 +03:00
c31508f138 feat: phase 10 — reconnection, encoding 10, disconnect button
All checks were successful
Publish / frontend (push) Successful in 42s
CI / fmt (push) Successful in 44s
CI / check (push) Successful in 1m43s
CI / clippy (push) Successful in 1m43s
Publish / backend (push) Successful in 2m47s
Frontend reconnection:
- WebSocket auto-reconnects with exponential backoff (1s → 30s)
- Re-authenticates with OmniView to get fresh APPLET_ID on reconnect
- Credentials stored in sessionStorage for automatic re-login
- Status bar shows connection state and reconnect countdown
- Disconnect button returns to login screen

Encoding 10 (Raw with tile interleave):
- codec/raw_tile.rs: decodes encoding 10 per ByteColorRFBRenderer.for()
- Flag byte bit 0 selects plain raw vs 16x16 tile-interleaved data
- Deinterleave handles edge tiles smaller than 16x16
- Wired into session dispatch
- 2 unit tests

39 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 09:21:21 +03:00
865a08da17 chore: setup backend environment 2026-05-07 09:08:28 +03:00
3ba05bcb05 chore: setup hosting environment 2026-05-07 08:41:16 +03:00
2539a1fd06 ci: ssh resolved
All checks were successful
CI / fmt (push) Successful in 43s
Publish / frontend (push) Successful in 58s
CI / clippy (push) Successful in 1m31s
CI / check (push) Successful in 1m34s
Publish / backend (push) Successful in 2m36s
2026-05-06 18:50:33 +03:00
ee4b0a2124 ci: debug ssh
All checks were successful
CI / fmt (push) Successful in 38s
CI / check (push) Successful in 1m52s
CI / clippy (push) Successful in 1m51s
Publish / frontend (push) Successful in 37s
Publish / backend (push) Successful in 2m27s
2026-05-06 18:38:50 +03:00
8440d653b3 ci: debug ssh
Some checks are pending
Publish / backend (push) Waiting to run
CI / fmt (push) Successful in 37s
Publish / frontend (push) Successful in 46s
CI / check (push) Successful in 1m46s
CI / clippy (push) Successful in 1m51s
2026-05-06 18:31:01 +03:00
022c38bdc2 fix(ci): frontend SSH init should test UI_HOST not WS_HOST
Some checks failed
CI / fmt (push) Successful in 44s
CI / clippy (push) Successful in 1m19s
CI / check (push) Successful in 1m34s
Publish / backend (push) Waiting to run
Publish / frontend (push) Failing after 51s
Also bump ConnectTimeout to 5s for the frontend SSH init.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 18:08:36 +03:00
3b9ef6407c ci: debug ssh
Some checks are pending
CI / fmt (push) Waiting to run
Publish / frontend (push) Waiting to run
Publish / backend (push) Waiting to run
CI / check (push) Successful in 1m34s
CI / clippy (push) Successful in 1m26s
2026-05-06 18:01:34 +03:00
99e337d387 ci: debug ssh
Some checks failed
Publish / backend (push) Waiting to run
CI / fmt (push) Successful in 36s
Publish / frontend (push) Failing after 39s
CI / check (push) Successful in 1m31s
CI / clippy (push) Successful in 1m36s
2026-05-06 17:35:30 +03:00
5e5908804a fix(ci): use --rsync-path 'sudo rsync' for privileged deploys
Some checks failed
CI / fmt (push) Successful in 43s
Publish / frontend (push) Failing after 44s
CI / clippy (push) Successful in 1m20s
CI / check (push) Successful in 1m31s
Publish / backend (push) Failing after 22m42s
Simplified deploy steps — rsync directly to final paths using
sudo rsync on the remote instead of temp-file staging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 16:27:49 +03:00
075fef0ea9 fix(ci): drop Node.js install step — already on runner
Some checks failed
CI / check (push) Waiting to run
CI / fmt (push) Successful in 49s
CI / clippy (push) Successful in 1m37s
Publish / backend (push) Waiting to run
Publish / frontend (push) Failing after 28s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 16:16:07 +03:00
2700821559 fix(ci): use Node.js tarball instead of fnm (runner lacks unzip)
Some checks failed
CI / check (push) Waiting to run
CI / fmt (push) Waiting to run
CI / clippy (push) Waiting to run
Publish / frontend (push) Waiting to run
Publish / backend (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 16:15:43 +03:00
2627bab72a ci: add publish workflow with frontend and backend deploy
Some checks failed
Publish / backend (push) Waiting to run
Publish / frontend (push) Failing after 36s
CI / fmt (push) Successful in 53s
CI / clippy (push) Successful in 1m20s
CI / check (push) Successful in 1m28s
publish.yml — triggered on push to main, two parallel jobs:

frontend:
- Builds Vite frontend (fnm + npm ci + npm run build)
- Rsyncs dist/ to gitea_ci@UI_HOST:UI_PATH/
- Rsyncs nginx config to UI_HOST, creates sites-enabled symlink,
  runs nginx -t && systemctl reload nginx

backend:
- Builds release binary (cargo build --release -p ericrfb-proxy)
- Stops blekin.service on WS_HOST
- Rsyncs binary to WS_HOST:/usr/local/bin/ericrfb-proxy via sudo rsync
- Rsyncs systemd unit to WS_HOST:/etc/systemd/system/blekin.service
- Enables and starts the service

asset/nginx/blekin.kosherinata.internal.conf:
- Serves static frontend from UI_PATH
- Reverse proxies /api/ to frootmig:3000 with WebSocket upgrade
- 24h read/send timeouts for long-lived KVM sessions

asset/systemd/blekin.service:
- Runs ericrfb-proxy with BLEKIN_HOST=10.3.0.130
- Restart on failure with 5s backoff

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 16:12:46 +03:00
8692c0e46a feat: phase 8 — Vite/TS canvas-based KVM console frontend
All checks were successful
CI / fmt (push) Successful in 28s
CI / check (push) Successful in 1m31s
CI / clippy (push) Successful in 1m29s
crates/ericrfb-frontend — vanilla TypeScript + Vite:

login.ts:
- Login form POSTs to /api/login, receives applet_id
- Error display, auto-transitions to console view on success

console.ts:
- Canvas-based renderer sized to framebuffer dimensions
- WebSocket binary protocol decoder: TAG_BLIT → putImageData,
  TAG_RESIZE → canvas resize
- Keyboard capture: keydown/keyup → JS code → e-RIC scancode → WS
- Mouse capture: move/click/wheel → scaled coords + button mask → WS
- Right-click and context menu suppressed for pass-through

input.ts:
- Full 104-key JS KeyboardEvent.code → scancode mapping table

protocol.ts:
- Binary message builders matching proxy WS protocol tags

Toolbar: Ctrl+Alt+Del button, Fullscreen toggle.
Dark theme, pixelated canvas rendering, cursor hidden over console.

Vite config proxies /api to localhost:3000 for dev mode.
Build outputs to ../../dist for proxy static serving.

Builds to 5.8KB JS + 1.4KB CSS gzipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 15:19:53 +03:00
3bd7ee8eac feat: phase 7 — proxy daemon with HTTP login and WebSocket bridge
All checks were successful
CI / fmt (push) Successful in 38s
CI / check (push) Successful in 1m20s
CI / clippy (push) Successful in 1m19s
ericrfb-proxy axum server with three endpoints:

POST /api/login:
- Proxies credentials to OmniView auth.asp
- Extracts session cookie, fetches title_app.asp
- Returns JSON with applet_id, port, protocol_version, board_name

GET /api/ws?applet_id=...&port=...:
- WebSocket upgrade, connects to OmniView via e-RIC RFB
- Bidirectional pump: OmniView frames → RGBA blits over WS,
  browser input events → key/mouse/hotkey to OmniView
- Binary protocol: TAG_BLIT(0x01), TAG_RESIZE(0x03) server→client;
  TAG_KEY_PRESS(0x10), TAG_KEY_RELEASE(0x11), TAG_POINTER(0x12),
  TAG_CTRL_ALT_DEL(0x13) client→server

Static file fallback via tower-http ServeDir.

Config via config.toml or BLEKIN_HOST env var.

Tested against real OmniView:
- Login endpoint returns valid APPLET_ID
- WebSocket upgrade succeeds (HTTP 101)
- Session connects and pumps frames

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 15:11:50 +03:00
ab74f607e8 feat: phase 6 — keyboard/mouse input + fix Tight zlib init
All checks were successful
CI / fmt (push) Successful in 30s
CI / check (push) Successful in 1m9s
CI / clippy (push) Successful in 1m9s
input.rs:
- write_key_press/release/tap: scancode | 0x80 = press, bare = release
- JavaScript KeyboardEvent.code → e-RIC scancode mapping (104pc layout)
- Hotkey sequence sender (raw hex byte strings from applet params)
- write_ctrl_alt_del() using HOTKEYCODE_0 "36 f0 37 f0 4e"
- PointerEvent writer already in msg.rs (8 bytes, absolute mode)
- 5 unit tests

Tight zlib fix:
- Changed Decompress::new(true) for zlib-wrapped format (matching
  Java's Inflater() which expects zlib header 78 9C)
- Added output length verification after decompression
- Tested: Tight-encoded frames now decode correctly from real device

37 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 14:51:50 +03:00
c8f981f045 feat: phase 5 — Tight decoder with zlib streams and sub-palettes
All checks were successful
CI / fmt (push) Successful in 36s
CI / check (push) Successful in 1m1s
CI / clippy (push) Successful in 1m4s
codec/tight.rs:
- Full Tight (encoding 7) decoder per ByteColorRFBRenderer.a() line 324
- Control byte: bottom 4 bits = zlib stream reset flags,
  top 4 bits = subencoding (0-15)
- Subencoding 8: solid fill (1-byte color)
- Subencoding 15: palette-indexed fill (selector + index)
- Subencodings 4-7: filtered data with optional 2-color palette
- Subencodings 10-13: reduced bit-depth packed (1/2/4 bpp)
- Subencodings 0-3: raw 8bpp data
- Data >= 12 bytes uses zlib compression with varint length
- 4 persistent zlib streams with reset-on-flag logic
- All 4 hardcoded sub-palettes ported as RGB332 indices:
  PALETTE_BW (2), PALETTE_GRAY4 (4), PALETTE_GRAY16 (16),
  PALETTE_COLOR16 (16 EGA-like colors)
- Bit-depth unpackers: 1bpp, 2bpp, 4bpp (MSB-first)
- 5 unit tests

Updated examples to request [7, 5, 1, 0, -250] (Tight preferred).
Tested against real OmniView: correct rendering with Tight encoding.
32 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 14:44:06 +03:00
21ed797302 feat: phase 4 — Hextile decoder and recording example
All checks were successful
CI / fmt (push) Successful in 28s
CI / check (push) Successful in 1m12s
CI / clippy (push) Successful in 1m12s
codec/hextile.rs:
- Full Hextile (encoding 5) decoder per ByteColorRFBRenderer.int()
- Handles: Raw tiles, BackgroundSpecified, ForegroundSpecified,
  AnySubrects, SubrectsColoured flags
- Background/foreground colors persist across tiles
- 4 unit tests covering all subencoding paths

framebuffer.rs:
- Added fill_rect() for Hextile background/subrect fills

session.rs:
- Wired Hextile encoding 5 into the rect dispatch

examples/record.rs:
- 30-second (configurable) recording session
- Saves 1 PNG per second to out/ directory
- Requests encodings [5, 1, 0] (Hextile, CopyRect, Raw)
- Tested against real OmniView: 10 frames in 10s, no errors

27 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 14:35:20 +03:00
1164ffdd98 fix: send SetPixelFormat to request 8bpp RGB332
All checks were successful
CI / fmt (push) Successful in 35s
CI / clippy (push) Successful in 57s
CI / check (push) Successful in 1m0s
The OmniView defaults to 16bpp when no SetPixelFormat is sent.
The Java applet sends SetPixelFormat (msg type 0) at
ByteColorRFBRenderer.new() line 76 to request 8bpp RGB332
(bpp=8, depth=8, true_color, red_max=7, green_max=7, blue_max=3,
shifts 0/3/6). Without this, pixel data arrives as 16bpp pairs
that produce green stripe artifacts when interpreted as 8bpp.

Verified: snapshot now matches the reference screenshot from
http://10.3.0.130/screenshot.jpg (dark screen with "Free" label).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 14:31:37 +03:00
e9823aff03 feat: phase 3 — framebuffer, raw decoder, session pump, snapshot
All checks were successful
CI / fmt (push) Successful in 37s
CI / check (push) Successful in 1m0s
CI / clippy (push) Successful in 1m4s
framebuffer.rs:
- Framebuffer struct (8bpp RGB332, row-major)
- apply_raw() blit, copy_rect() with overlap-safe logic
- to_rgba() via compile-time RGB332 LUT

session.rs:
- ActiveSession: connect + SetEncodings + initial FBUpdateRequest
- Full server message dispatch loop (all 15 message types)
- Raw (encoding 0) and CopyRect (encoding 1) decoders
- Ping response, bandwidth probe bookends, mode change resize

examples/snapshot.rs:
- Connects, waits for first FramebufferUpdate, saves PNG
- Tested against real OmniView at 10.3.0.130:443
- Successfully captured 640x480 frame

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 14:23:43 +03:00
1bd43fc1f9 feat: phase 2 — handshake, message writers, and server message dispatch
All checks were successful
CI / fmt (push) Successful in 30s
CI / check (push) Successful in 1m1s
CI / clippy (push) Successful in 1m4s
handshake.rs:
- Config, PixelFormat, ServerInit, Session types
- connect() walks all 11 handshake steps per aw.g() line 226
- Auth error mapping from aw.a(int) line 350 (7 error codes)

msg.rs — client-to-server writers:
- SetEncodings (type 2), FramebufferUpdateRequest (type 3)
- KeyEvent (type 4), PointerEvent (type 5, 8 bytes)
- PingResponse (type 149), BandwidthMarker (type 151)

msg.rs — server message dispatch:
- ServerMsg enum covering all 15 message types
- Readers for ping, bandwidth probe, ack, debug string,
  RFB command, server cut text, server name update,
  layout/locale, RDP event, FB update header

examples/handshake.rs: connects and prints session info.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 14:11:31 +03:00
07db90094d style: apply rustfmt to proto.rs
All checks were successful
CI / fmt (push) Successful in 32s
CI / check (push) Successful in 55s
CI / clippy (push) Successful in 59s
Fixes CI fmt check failure — rustfmt wants multi-line assert_eq! for
long struct literals and the varint roundtrip assertion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 13:59:54 +03:00
c4e3df5a44 ci: add Gitea Actions workflow for check, fmt, clippy
Some checks failed
CI / fmt (push) Failing after 40s
CI / check (push) Successful in 1m2s
CI / clippy (push) Successful in 1m10s
Three parallel jobs on the rust runner:
- cargo check --workspace
- rustfmt --check on all .rs files
- cargo clippy --workspace -- -D warnings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 13:47:58 +03:00
3db2927add feat: phase 0+1 — workspace scaffold and protocol primitives
Phase 0: Cargo workspace with ericrfb (lib) and ericrfb-proxy (bin)
crates, .envrc for RUST_LOG, workspace dependency pins.

Phase 1: ericrfb/src/proto.rs implements all wire primitives:
- read/write helpers matching h.java (u8, i8, u16-BE, i16-BE, i32-BE)
- varint reader/writer matching aw.int() (1-3 bytes, Tight lengths)
- modified-UTF-8 string reader matching h.byte()
- RectHeader { x: u16, y: u16, w: u16, h: u16, encoding: i32 }
- RGB332 → RGBA compile-time LUT (256 entries)

20 tests including proptest varint roundtrip over [0, 2^22).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 13:44:24 +03:00
a60cee3f23 fix: reconcile implementation plan with decompiled Java source
Cross-referenced every wire-format claim against rc-src and corrected
several errors that would have caused stream desync during implementation:

- rect header is 4×u16 + i32 (12 bytes), not varints + u8
- ping payload is i32 (4 bytes), not 1 byte
- PointerEvent is 8 bytes (includes trailing extra_u16)
- ServerInit is 19 bytes, not 16
- pixel-format struct is variable-length, not fixed 25 bytes
- string primitive uses modified-UTF-8, not ISO-8859-1
- bandwidth probe is read-only (no echo response)
- Phase 1 uses sync Read traits (async deferred to Phase 2)
- corrected all stale line-number references in ac.java/aw.java

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 13:40:40 +03:00
6e1a6fc29d doc: implementation plan 2026-05-06 13:36:07 +03:00