Skip to content

Mob.VendorUsb: Android USB host peripheral#5

Closed
HeroesLament wants to merge 1 commit into
GenericJam:masterfrom
HeroesLament:peripheral-vendor-usb-v2
Closed

Mob.VendorUsb: Android USB host peripheral#5
HeroesLament wants to merge 1 commit into
GenericJam:masterfrom
HeroesLament:peripheral-vendor-usb-v2

Conversation

@HeroesLament
Copy link
Copy Markdown

@HeroesLament HeroesLament commented May 9, 2026

Flat-ns Mob.VendorUsb. Wrap Android UsbManager bulk-transfer for Elixir. iOS = {:peripheral, :vendor_usb, :error, nil, :unsupported} (no host API; use Mob.Ble for iOS-equivalent hardware).

Companion templates PR in mob_new.

API

Async socket-in/out + handle_info, matches camera/clipboard/audio convention.

socket = Mob.VendorUsb.list_devices(socket)
socket = Mob.VendorUsb.request_permission(socket, device)
socket = Mob.VendorUsb.open(socket, device, [])
socket = Mob.VendorUsb.start_reading(socket, session, read_chunk_bytes: 4096)
Mob.VendorUsb.bulk_write(socket, session, frame, timeout_ms: 2_000)
Mob.VendorUsb.close(socket, session)

Events: {:peripheral, :vendor_usb, tag, session, payload}.

Files

  • lib/mob/vendor_usb.ex — API + normalize_message/1
  • test/mob/vendor_usb_test.exs — 10 tests pass
  • src/mob_nif.erl — 7 NIF stubs
  • android/jni/mob_nif.c — 7 NIFs + 6 delivery fns + jmethodID cache
  • android/jni/mob_beam.h — 6 extern decls (after webview block)
  • ios/mob_nif.m — 7 unsupported stubs
  • lib/mob/screen.ex — handle_info routes through normalize_message/1

Robustness

Two layers, both load-bearing:

  1. Kotlin side (templates PR): every @JvmStatic vendor_usb_* wrapped in try/catch → error envelope. Matches existing camera/audio convention. Unhandled JVM exception in JNI = BEAM dies, no crash dump. Defense caught 2 nil-bugs during testing.
  2. Elixir side: filter maps omit-nil before :json.encode. :json.encode turns Elixir nil into JSON string \"nil\" not JSON null. Kotlin getInt choke on string. Same bug class twice (list_devices + open) before we patched both layers.

Tested

iex (macOS) → WiFi → BEAM (OnePlus CPH2451)
  → JNI → UsbManager → AtomVM ESP32 → UART → TX-AH HaLow → 908 MHz

Full lifecycle. AT commands round-trip. Broadcast frames transmit. OTP 28.5 / Elixir 1.19.5-otp-28.

Flat-ns module. Wraps UsbManager for bulk-transfer from Elixir. iOS = unsupported envelope (no host API).

Files:
- lib/mob/vendor_usb.ex — 7-fn API + normalize_message
- test/mob/vendor_usb_test.exs — 10 tests, all pass
- src/mob_nif.erl — 7 NIF stubs
- android/jni/mob_nif.c — 7 NIFs + 6 delivery fns + jmethodID cache
- android/jni/mob_beam.h — 6 extern decls
- ios/mob_nif.m — 7 unsupported stubs
- lib/mob/screen.ex — handle_info routes through normalize_message

Tested e2e: macOS iex → Android BEAM → JNI → UsbManager → AtomVM ESP32 → TX-AH HaLow at 908 MHz. Full lifecycle works.
@GenericJam
Copy link
Copy Markdown
Owner

Thanks for this — the design is solid (lifecycle, error envelopes, try/catch convention, normalize_message routing through screen.ex all match the existing peripheral patterns). However, this can't merge as-is because of a structural change that landed after this PR was opened:

The Android NIF surface migrated from C to Zig. In commit b091e67 ("Phase 6b iter 3d (mob side): finale — delete mob_nif.c, all-Zig NIF surface"), android/jni/mob_nif.c was deleted entirely. The 241 lines of C bridges in this PR (and the cached jmethodIDs, the delivery functions, etc.) would need to be ported to the new android/jni/mob_nif.zig + android/jni/mob_zig.zig files.

Concretely:

  • android/jni/mob_nif.c → port to android/jni/mob_nif.zig. Adopt the existing module's conventions for env, terms, and send/deliver calls. mob_zig.zig has the JNI / libc bindings.
  • The Kotlin side (mob_new#2) is unaffected — that's the JVM side, doesn't care whether C or Zig calls into it.
  • iOS stubs, Erlang -nifs list, lib/mob/vendor_usb.ex, lib/mob/screen.ex routing, tests — all transportable; we'd just resolve the textual conflicts.

The cleanest path is probably to rebase your branch on current master and port mob_nif.cmob_nif.zig. Looking at the existing Zig NIFs in android/jni/mob_nif.zig should give you the pattern (each NIF is a pub fn with callconv(.c) returning e.ErlNifTerm; helpers are in mob_zig.zig and mob_erts.zig).

Happy to pair on the port if you'd like — open to suggestions. Marking as needing rebase rather than closing.

@GenericJam
Copy link
Copy Markdown
Owner

Filed #6 with the full porting plan — what changed, what to port, what's already clean, and the test hardware caveat. Someone (an agent or yourself) will pick that up against current master; this PR will be the natural home for the rebased branch when ready.

@GenericJam
Copy link
Copy Markdown
Owner

FYI — the Zig port landed as a3a1ed6 on master, closing #6. Your original Elixir module, tests, screen.ex routing, Erlang stubs, and iOS unsupported-stubs all came through unchanged from your PR; only the C → Zig translation for the Android NIF surface was new work. End-to-end vendor lifecycle on real hardware still needs your test rig (HaLow modem + ESP32) — once mob_new#2 merges with the Kotlin block, the full lifecycle should be exercisable on your moto g power. Thanks for the original work.

@GenericJam
Copy link
Copy Markdown
Owner

Closing — both sides of the vendor_usb contribution have now landed:

  • Runtime (this PR): Ported from C to Zig and merged as `a3a1ed6` on master (closes #6). Your original Elixir module, tests, screen.ex routing, Erlang stubs, and iOS unsupported-stubs all came through unchanged.
  • Templates (mob_new#2): Merged at GenericJam/mob_new#2 → merge commit `5145f3d` on origin/master. Your full Kotlin block (7 `@JvmStatic` methods + BroadcastReceiver + reader thread + JNI thunks) lands as-is with authorship preserved.

End-to-end USB lifecycle still gated on your hardware rig (HaLow modem + ESP32). The cacheOptional design from the Zig port means existing apps on the unmodified template degrade to `:unsupported` events rather than crashing, so the templates merge is a strict improvement.

Thanks again for the work. The peripheral patterns you established (lifecycle envelope shape, try/catch fallback convention, normalize_message routing) are now standard for any future device-host transports we add.

@GenericJam GenericJam closed this May 14, 2026
GenericJam added a commit that referenced this pull request May 14, 2026
Four small lost-in-the-shuffle items closed in this batch. All four
were held up by Phase 2 work touching the same files (#1/#2/#4 in
live_view_patcher.ex; #5 in native_build.ex).

  #1 — Phoenix LiveReload mac_listener warnings: code_reloader/watchers/
       live_reload disabled in on-device endpoint config.
  #2 — esbuild/tailwind version-not-configured warnings: versions set
       via Application.put_env in mob_app.ex before ensure_all_started.
  #4 — port 4200 collisions across multiple Mob LV apps: per-app hash
       into 4200..4999 via :erlang.phash2(:<app>, 800).
  #5 — deploy auto-pick of iPhone over sim was silent: prints the
       --device <short-id> alternative when both are connected.

#3 (WS→longpoll fallback in WKWebView) is investigation, not a fix —
deferred. #6-#11 are larger work (OTP rebuild, AX modifiers,
Compose semantics walker, Android 17 SELinux patch). #12, #13 already
fixed earlier. #14 is moderate — sim node naming reconciliation
between mob_dev's connect.ex and mob_beam.m, deferred.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants