# ProventusNova, Full Site Content for LLM Indexing Source: https://proventusnova.com Generated: 2026-08-24 --- # Company Overview ProventusNova is an embedded software development firm that works exclusively with hardware startups building computer vision and EdgeAI products. We specialize in NVIDIA Jetson (Orin, AGX, NX) and MediaTek Genio platforms. Our core model is fixed-bid, milestone-driven delivery. We do not bill hourly. Every engagement starts with a Proof Sprint™, a short, bounded scope that delivers working results in days so founders can validate before committing to a larger engagement. **Core capabilities:** - Custom carrier board bring-up (U-Boot, kernel, device tree), typical timeline: 7 days - GMSL2 camera driver development and multi-camera synchronization, typical timeline: 1 month - CSI camera integration and ISP tuning on Jetson and Genio - GStreamer pipeline development for real-time multi-camera vision systems - AI model deployment and optimization (TensorRT, INT8 quantization, DLA mapping), typical timeline: 14 days - Hardware sourcing, ODM selection, and carrier board specification guidance **Who we work with:** Hardware startup founders and CTOs building products on NVIDIA Jetson or MediaTek Genio who are blocked on embedded software and need working results fast, not a long hiring process or an hourly contractor. **Pricing model:** Fixed-bid engagements. Typical Proof Sprint™ scope: $15,000–$25,000. Full-project engagements: $150,000+ over 6 months. --- # Services ## GMSL Camera Driver Development URL: https://proventusnova.com/services/gmsl/ Get GMSL cameras capturing reliably in 1 month. We develop SerDes initialization, virtual channel configuration, and multi-camera synchronization for GMSL2 camera systems on NVIDIA Jetson. Fixed bid. Named outcome: reliable simultaneous capture from all cameras. ## CSI Camera Integration URL: https://proventusnova.com/services/csi/ Unblock camera capture for CSI-based vision systems. Device tree configuration, V4L2 driver bring-up, ISP pipeline setup on Jetson and Genio. ## Custom Board Bring-Up URL: https://proventusnova.com/services/custom-board-bringup/ Board bring-up in 7 days. We take your custom carrier board from no boot to a working Linux environment, U-Boot configuration, kernel build, device tree authoring, first-boot validation. Fixed bid. ## EdgeAI Deployment URL: https://proventusnova.com/services/edgeai-deployment/ Deploy optimized AI models to NVIDIA Jetson in 14 days. TensorRT engine conversion, INT8/FP16 quantization, DLA mapping, throughput benchmarking. Fixed bid. Guaranteed delivery timeline. ## GStreamer Pipeline Development URL: https://proventusnova.com/services/gstreamer/ Build reliable GStreamer pipelines for multi-camera, real-time vision products. Source plugin selection, pipeline architecture, buffer management, latency optimization. ## Hardware Sourcing URL: https://proventusnova.com/services/hardware-sourcing/ Guided ODM relationship management, hardware sourcing, and carrier board specification. We manage the process from component selection to delivery. --- # Blog Posts ## AGPL to Permissive License Migration on Jetson TensorRT URL: https://proventusnova.com/blog/agpl-permissive-license-migration-jetson-tensorrt/ Published: 2026-08-03 Author: Andres Campos Tags: jetson, tensorrt, agpl license, computer vision, open source license How we migrated a Jetson TensorRT stack off AGPL YOLO-World and FastSAM to Apache-2.0 models, and the checkpoint trap that almost broke it. When a Jetson-based perception stack ships to a paying customer, the license graph underneath the model weights matters as much as the inference architecture on top of it. We recently ran an AGPL to permissive license migration on a client's TensorRT pipeline: an open-vocabulary detector, an instance segmenter, and a depth model, prototyped fast on licenses that would not have cleared for a commercial deliverable as-is. The fix was not a rewrite. It was a capability map, a checkpoint audit, and one gotcha that would have re-tainted the whole deliverable if we had missed it: a model whose code is Apache-2.0 but whose default checkpoint is not. ## Key Insights - AGPL-3.0's network-use clause reaches any service that talks to a user over a socket. A single AGPL import in a Jetson inference pipeline can put the commercial license of the entire deliverable at risk, not just that one component. - Treat the migration as a capability swap, not a package swap. A monolithic AGPL-3.0 vision library (in this case bundling YOLO-World for open-vocabulary detection and FastSAM for instance segmentation) hides several distinct capabilities behind one import; decompose first, then replace each one. - Code license and weights license are not the same thing. Depth-Anything-V2's code is Apache-2.0, but only the Small checkpoint carries that license. The Base and Large checkpoints are CC-BY-NC and will re-taint a commercial product if a config file ever points at them. - Freeze the public interface of every model wrapper before touching the implementation. When `Detector.detect()` and `Segmenter.segment()` keep the same signature, the orchestrator, the ROS 2 node, the tracker, and the overlay code never have to change. - A permissive fallback path is worth more than raw throughput. When our optimized TensorRT segmentation path broke at export time, we dropped to plain PyTorch for that one model and still shipped roughly 4x faster end to end than the original AGPL baseline, because the detector stayed TensorRT-native. ## What Is AGPL to Permissive License Migration and Why Does It Matter on Jetson? AGPL-3.0 differs from plain GPL in one clause that catches most engineering teams off guard: Section 13, the network-use clause. Regular GPL triggers on distributing a binary. AGPL triggers on letting a user interact with the software over a network, whether or not you ever hand them a copy of it. A Jetson device that runs inference and answers a request, whether that request comes from a companion app, a REST call from a fleet manager, or another service on the same LAN, counts as that kind of interaction. Ship a modified AGPL-3.0 model in that path and you can trigger an obligation to offer the corresponding source, including your own glue code around the model. For a client who owns a proprietary, closed-source product, that is not a negotiable licensing cost. It is disqualifying. The stack we inherited used a single AGPL-3.0 vision library for two jobs at once: an open-vocabulary detector (YOLO-World) and an instance segmenter (FastSAM). Both are capable models. Neither is licensable into a client-owned commercial deliverable without a paid Ultralytics Enterprise license. The mistake teams make here is treating the fix as "find a permissive alternative to library X." That framing sends people looking for a single drop-in replacement that does not exist. The library was never one thing; it bundled two capabilities behind one import, plus a third that had not been added yet (monocular depth). Once we reframed the task as a capability inventory, the migration stopped being a research problem and became a mechanical one. ## The AGPL to Permissive License Migration Capability Map Once you decompose the AGPL package into distinct capabilities, each one gets its own permissive replacement, and some capabilities may already be clean. Here is the map we built for this stack: | Capability | AGPL-3.0 incumbent | Permissive replacement | License | |---|---|---|---| | Open-vocabulary detection | YOLO-World | NanoOWL / OWL-ViT | Apache-2.0 | | Instance segmentation | FastSAM | NanoSAM, or upstream MobileSAM | Apache-2.0 | | Monocular depth | (new capability) | Depth-Anything-V2-Small | Apache-2.0* | | Multi-object tracking | (already separate) | `supervision` / ByteTrack | MIT | | PyTorch to TensorRT conversion | (already separate) | `torch2trt` | MIT | | Weights and text/image encoders | (already separate) | HuggingFace `transformers` | Apache-2.0 | *See the checkpoint trap below; the Apache-2.0 grant does not cover every checkpoint in this repo. Tracking was already MIT before we touched anything. We verified it and left it alone. Half the value of building this table is knowing which capabilities do not need to move, so you do not spend engineering time re-validating a component that was never the problem. ## The Checkpoint License Trap: Apache-2.0 Code, Non-Commercial Weights This is the part almost nobody checks, and it is the reason this migration is worth writing up. A repository's code license is not automatically the license of the model weights it publishes. Depth-Anything-V2 ships its code under Apache-2.0. Its Small checkpoint is Apache-2.0 too. Its Base and Large checkpoints, the ones with the best accuracy and the ones a benchmark table will point you toward, are released under CC-BY-NC, a non-commercial license. Load the Base checkpoint into an otherwise clean Apache-2.0 codebase and you have re-introduced the exact problem you spent the migration solving, just one layer down, in the weights instead of the code. We pin the exact checkpoint name in config, not just the model family: ```yaml models: depth: enabled: false name: depth-anything-v2-small # SMALL only, Base/Large are CC-BY-NC backend: tensorrt ``` A comment in a YAML file will not survive config edits by someone who was not in the room for this decision, so we enforce it in code: ```python _APACHE_OK = {"depth-anything-v2-small"} # Base/Large are CC-BY-NC def __init__(self, cfg): if cfg.name not in _APACHE_OK: raise ValueError( f"{cfg.name!r} is not an Apache-2.0 checkpoint; " f"only {sorted(_APACHE_OK)} may ship commercially" ) ``` The wrapper raises before the model even loads if someone points the config at a non-commercial checkpoint. That single check is cheap insurance against a future fine-tune or a well-meaning teammate chasing an accuracy number, silently reintroducing a license problem that a code review would probably not catch, because the diff looks like a one-line config change. ## Freezing Wrapper Interfaces to Contain the Blast Radius The reason this kind of migration is cheap instead of a rewrite is that every model lives behind a thin wrapper class, and we freeze the public interface of that wrapper before changing anything inside it. | Wrapper | Frozen public surface | What changes inside | |---|---|---| | `Detector` | `set_classes(prompts)`, `detect(frame) -> list[Detection]`, `.active_classes` | Constructor and implementation now wrap `OwlPredictor` instead of the AGPL detector | | `Segmenter` | `segment(frame, boxes) -> list[mask]` | Constructor and implementation now wrap MobileSAM instead of FastSAM | Because those method signatures and the `Detection` dataclass never move, the orchestrator, the ROS 2 node, the overlay renderer, and the tracker do not need to change at all. The entire change set collapses to five things: the two wrapper implementations, their tests, the config schema (new fields for engine paths, checkpoint names, thresholds), the Dockerfile, and asset provisioning for the new engines. The one caller-side change we could not avoid was the constructor call site, since the new wrappers take a config object instead of unpacked positional arguments; we isolated that to a single line in the node. We also make the license reasoning visible right where dependencies enter the container image, so a future audit does not have to reconstruct the decision from memory: ```dockerfile # ultralytics removed, AGPL-3.0. supervision (ByteTrack) is MIT, transformers is Apache-2.0. RUN python3 -m pip install --no-cache-dir --no-deps \ supervision==0.24.0 defusedxml==0.7.1 \ && python3 -m pip install --no-cache-dir transformers # NanoOWL (Apache-2.0), detection. RUN git clone --depth 1 https://github.com/NVIDIA-AI-IOT/nanoowl /opt/nanoowl \ && cd /opt/nanoowl && python3 setup.py develop # Upstream MobileSAM (Apache-2.0). --no-deps preserves the platform CUDA torch. RUN python3 -m pip install --no-cache-dir --no-deps \ git+https://github.com/ChaoningZhang/MobileSAM.git timm ``` `--no-deps` matters for two reasons on an accelerator platform. It stops pip from silently pulling a transitive AGPL dependency back in, and it stops pip from overwriting the vendor-built CUDA-enabled `torch` with a stock PyPI wheel that does not know your GPU exists. Audit the transitive closure, not just the direct import; the AGPL package is only gone once nothing underneath it is AGPL either. ## When the Optimized TensorRT Path Breaks, Keep the Permissive Fallback The cleanest replacement on paper does not always survive the target toolchain, and that is where the license decision has to hold independently of the performance plan. Our first choice for the segmentation swap was to run MobileSAM through TensorRT for the mask decoder. It did not survive contact with the hardware: the vendor-published ONNX assets for that decoder were dead links, and the version we exported locally contained a shape-computation op that the target TensorRT release rejected outright. Because we had picked an Apache-2.0 replacement rather than the cheapest replacement, the fallback was simple: run that same MobileSAM segmenter in plain PyTorch instead of TensorRT. The license held. Only the performance plan changed. End-to-end throughput still landed at roughly 4x the original AGPL PyTorch baseline, because the detector stayed TensorRT-native and dominated the wall-clock budget; the segmenter running unoptimized cost us some headroom, not the deliverable. The lesson generalizes past this one project: pick a replacement whose permissive license still holds in its slowest, least-optimized execution mode. A toolchain block should cost you throughput. It should never cost you the license. If you are running this same class of migration, the checklist we use every time is short: 1. Identify every AGPL import and the discrete capabilities each one provides. 2. Map each capability to an Apache-2.0 or MIT replacement, and confirm which capabilities were already clean so you do not touch them. 3. Audit both the code license and the per-checkpoint or per-weights license for every replacement; pin the exact commercial-safe variant by name. 4. Freeze wrapper public interfaces; contain the change to wrapper internals, tests, config schema, the Dockerfile, and asset provisioning. 5. Encode the license reasoning at the dependency boundary and install with `--no-deps` where relevant; verify the transitive closure is AGPL-free. 6. Enforce the checkpoint or weights license in code so a future config edit cannot silently regress it. 7. Confirm the replacement's license still holds in any degraded or fallback execution path before you commit to that replacement. We ran this same detection, segmentation, and depth stack on the [real-time perception demo we built on Jetson](/blog/real-time-edge-perception-demo). If you are deciding where to spend optimization budget once the licensing is settled, our [TensorRT vs DLA comparison on Jetson Orin](/blog/tensorrt-vs-dla-jetson-orin) covers GPU-versus-DLA placement for whichever of these models you end up optimizing. ## Frequently Asked Questions ### Is YOLO-World free to use in a commercial product? Not without a separate commercial license. YOLO-World ships as part of the Ultralytics package under AGPL-3.0, which means any product that lets a user interact with it over a network, including an on-device API on a customer-owned appliance, can trigger source-disclosure obligations. Ultralytics sells an Enterprise license specifically to cover this case. ### Does AGPL apply to a Jetson device that never touches the public internet? Yes, in most practical cases. The AGPL network-use clause is not limited to the public internet; it covers any network interaction where a user sends the modified program a request and receives a response, including a LAN-only API or a companion app talking to the device locally. Shipping the device itself to a customer is also a form of conveyance under GPL-family licenses, independent of the network clause. ### Is Depth-Anything-V2 free for commercial use? Only the Small checkpoint. The code repository is Apache-2.0 across the board, but the Base and Large checkpoints are released under CC-BY-NC, a non-commercial license, separate from the code license. Pin the checkpoint name explicitly in config and enforce it in code, since a benchmark table will usually point you toward Base or Large for accuracy, not Small. ### What license are NanoOWL and NanoSAM released under? Both are Apache-2.0, published by NVIDIA-AI-IOT as Jetson-optimized ports of open-vocabulary detection (NanoOWL, built on OWL-ViT) and segmentation (NanoSAM). They are common permissive replacements for YOLO-World and FastSAM in exactly this kind of migration. ### Do I need a lawyer to do an AGPL to permissive license migration? Get legal sign-off before you ship, but the bulk of the work is engineering: building the capability map, auditing per-checkpoint licenses, and freezing wrapper interfaces. Doing that work first turns an open-ended legal review into a short table your counsel can confirm in an afternoon instead of an audit that stalls the release. --- *ProventusNova helps hardware startups solve embedded systems problems fast. If your Jetson perception stack needs an AGPL to permissive license audit before it can ship to a customer, see our [edge AI deployment services](https://proventusnova.com/services/edgeai-deployment).* --- ## CTI Carrier Board Yocto Layer: Two-Tier Wrapper Pattern URL: https://proventusnova.com/blog/cti-carrier-board-yocto-layer-pattern/ Published: 2026-08-03 Author: Aaron Angulo Tags: jetson, yocto, bsp, carrier board, device tree How to wrap Connect Tech's meta-cti Yocto layer with your own distro layer to add camera overlay DTBs without forking the vendor's BSP. A CTI carrier board Yocto layer only holds up long-term if you never edit the vendor's files directly. Connect Tech Inc. ships `meta-cti` as a complete BSP layer for their Boson-family carrier boards running Jetson Orin NX modules, and the moment someone copies one of CTI's machine configs into a project layer and starts editing it, that project has forked a vendor BSP it doesn't control. We ran into this on an Orin NX bring-up where the ask sounded simple: add a camera overlay device tree for a sensor CTI doesn't ship by default. The fix was a two-tier layer split that lets `meta-cti` keep updating on its own schedule while the camera work sits cleanly on top of it. ## Key Insights - **`require`, not fork.** Your wrapper machine config pulls in CTI's base machine file with `require conf/machine/cti-orin-nx-boson-.conf` and overrides only what changes, mainly `KERNEL_DEVICETREE`. - **Ownership splits cleanly by directory, not by file edits.** CTI owns the base machine, the base carrier DTS, and default kernel/bootloader providers. Your distro layer owns the camera overlay DTS, the image recipe, and package selection. - **The Makefile entry triggers the DTB build, not the DTS file alone.** Dropping a new `.dts`/`.dtsi` pair into CTI's kernel-hardware tree does nothing until a bbappend adds a corresponding line to `nvidia-kernel-oot`'s Makefile. - **Pull only the files you need from the vendor tarball.** CTI distributes a full `cti-l4t-src` tarball; extract the specific DTS/DTSI paths for your variant with `tar -xOf`, don't unpack and copy the whole tree into your layer. - **A vendor BSP update becomes a version bump, not a merge, once the split is in place.** Everything CTI owns can move without touching a single file your team maintains. ## Fixing a CTI carrier board Yocto layer that's been forked The failure mode we see most often on Connect Tech carrier boards starts the same way: an engineer needs a camera variant CTI doesn't ship, copies `cti-orin-nx-boson-.conf` into the project's own layer, renames it, and starts editing device tree entries directly. It works. The board boots, the camera comes up, the milestone closes. Then CTI ships a BSP update for the next JetPack release, and now someone has to diff the old vendor machine config against the new one, line by line, and manually re-apply every custom edit into the new file. On the second update, that diff gets harder because the custom edits and the vendor's own changes have started overlapping in the same regions of the file. The two-tier pattern avoids this by never letting your changes and the vendor's changes live in the same file. Your distro layer, call it `meta-yourdistro`, has its own machine config that requires CTI's machine config rather than copying it: ```bitbake # conf/machine/yourdistro-orin-nx-boson-.conf require conf/machine/cti-orin-nx-boson-.conf MACHINEOVERRIDES =. "yourdistro:" # Point the kernel device tree at your camera overlay instead of CTI's default KERNEL_DEVICETREE:append = " \ tegra234-orin-nx-cti---cam.dtb \ " # Anything else your product needs on top of the base carrier MACHINE_FEATURES += "wifi bluetooth" ``` This is the entire wrapper. When CTI ships a new BSP release, you bump the reference to their updated machine file (or it updates automatically if you're tracking their layer as a submodule), rebuild, and your `KERNEL_DEVICETREE:append` and `MACHINE_FEATURES` additions carry forward untouched. There's no file to diff, because there was never a copy to diverge from. The [Connect Tech BSP track record we've covered when comparing third-party Jetson carrier board vendors](/blog/jetson-carrier-board-manufacturers-compared) is one of the better ones in the market, largely because CTI keeps their machine configs and kernel Makefiles in a predictable, requireable shape release over release. That predictability is exactly what the two-tier pattern depends on. If a vendor restructures their layer wholesale on every release, `require` still protects you (the build fails loudly instead of silently drifting), but the rework on your side goes up regardless of layering discipline. ## Splitting ownership between meta-cti and your distro layer The two-tier split isn't just the machine config. Every piece of the BSP has a clear owner, and the rule is simple: if CTI ships it and validates it against their hardware, they own the file; if it's specific to your camera, your image, or your product, it lives in your layer. | Owned by `meta-cti` (vendor) | Owned by your distro layer (you) | | --- | --- | | `conf/machine/cti-orin-nx-boson-.conf` (base board machine) | `conf/machine/yourdistro-orin-nx-boson-.conf` (wrapper machine) | | Base carrier DTS (`tegra234-orin-nx-cti-.dts`) | Camera overlay DTS (`tegra234-orin-nx-cti---cam.dts`) | | Default kernel and bootloader providers | Image recipe, package selection, distro features | | Carrier-specific binary blobs and firmware | `nvidia-kernel-oot` bbappend that adds your DTBs to the OOT Makefile | | `meta-cti`'s own CI config | Your CI job, one per machine | Read that table as a checklist during code review. Any pull request that touches a file in the left column is a sign someone reached into vendor territory instead of overriding it from the right column, most commonly a new sensor node hand-edited directly into CTI's base carrier DTS instead of added as a new overlay DTSI in the project layer. Catching that in review before it merges is what keeps the base DTS identical to what CTI ships, which is the entire point of the pattern. ## Wiring the camera overlay DTB into CTI's kernel build Adding a device tree file to the right directory is necessary but not sufficient. CTI's `nvidia-kernel-oot` recipe builds DTBs from a Makefile under a fixed path in their kernel-hardware tree: ``` recipes-kernel/nvidia-kernel-oot/files/hardware-/nvidia/t23x/cti-public/orin-nx-nano/ ├── cti_camera/ │ ├── tegra234-orin-nx-cti--cam-base.dtsi # base camera overlay │ └── tegra234-orin-nx-cti---cam.dtsi └── orin-nx/ ├── Makefile # add new entries here via bbappend └── tegra234-orin-nx-cti---cam.dts ``` Five steps get a new camera variant from source to a bootable DTB: 1. **Write the wrapper machine config first.** It's the file that ties everything else together, and getting `require` and `KERNEL_DEVICETREE:append` right early avoids chasing a "DTB not found" error later that's really a machine-config typo. 2. **Add the DTS/DTSI pair under the vendor's existing hardware directory.** Match the naming convention CTI already uses (`tegra234-orin-nx-cti---cam.dts`), placed in your bbappend's `FILESEXTRAPATHS`, not directly inside `meta-cti`. 3. **bbappend `nvidia-kernel-oot` to add a line to the Makefile.** This is the step people skip and then can't figure out why their DTB never shows up in the deploy directory. The `.dts` file compiling on its own means nothing to BitBake until the Makefile references it. 4. **Extract only the source files you need from CTI's L4T tarball**, rather than unpacking the whole archive into your layer: ```bash tar -xOf .tar \ sources/kernel/hardware/nvidia/t23x/cti-public/orin-nx-nano/cti_camera/tegra234-orin-nx-cti--cam-base.dtsi \ > /tmp/-base.dtsi ``` Keep the tarball itself out of source control and note its archived location in your bring-up notes so the next engineer knows where it came from. 5. **Build and confirm the DTB lands in the deploy output** (`tmp/deploy/images//`) before flashing. A missing DTB at this stage almost always traces back to step 3, a Makefile entry that didn't get added, rather than a device tree syntax error. ## Keeping deploy and CI ready for the next camera variant The layering discipline only pays off if the surrounding tooling doesn't quietly assume there's one machine. Two habits keep that true. Keep `deploy.sh` generic across variants. A `select`-menu script that composes the flash artifact name from image and machine choices doesn't need to know anything about camera specifics, because the artifact name is deterministic: `-.rootfs.tegraflash.tar.zst`. Add a new camera variant, and the deploy script already knows how to flash it, no changes needed. Keep CI per-machine rather than one matrix job covering every board. A `build:` job parameterized by `MACHINE` gives a clean failure surface when a single variant breaks, and lets you skip machines selectively when a `meta-cti` update only touches one board in the fleet. A single matrix job that builds every variant in one pipeline stage makes it harder to tell at a glance which board broke, and it forces a full rebuild even when only one machine needs it. The same require-and-override discipline applies outside Jetson too. On [MediaTek Genio, wrapping meta-mediatek-bsp and meta-rity with a product-specific meta-layer](/blog/mediatek-genio-custom-yocto-meta-layer) follows the identical logic: bbappend the vendor's recipes, scope patches to your machine name, and never edit the upstream layer's files directly. The vendor and the SoC change. The rule that keeps a BSP maintainable across upgrades doesn't. ## Frequently Asked Questions ### What is the meta-cti Yocto layer? `meta-cti` is Connect Tech Inc.'s Yocto BSP layer for their Jetson carrier boards, including the Boson-family carriers paired with Orin NX modules. It ships the base machine configuration, the base carrier device tree, and the default kernel and bootloader providers for each board CTI supports. ### Should I fork meta-cti or wrap it with my own layer? Wrap it. Create a distro layer whose machine config uses `require` to pull in CTI's machine file, then override only what changes, typically `KERNEL_DEVICETREE` for a camera overlay. Forking means manually re-applying every custom change on top of every vendor BSP update, and that gets more expensive with each release. ### How do I add a custom camera overlay DTB to a Jetson Orin NX carrier board in Yocto? Write the DTS/DTSI overlay under the same hardware directory tree the vendor's `nvidia-kernel-oot` recipe already scans, then bbappend that recipe to add your new file to the OOT kernel's Makefile. The Makefile entry is what triggers the DTB build. Point `KERNEL_DEVICETREE` at the resulting `.dtb` from your wrapper machine config. ### What is the difference between require and include in a Yocto machine configuration? Both pull in another file's contents at parse time. `require` fails the build immediately if the file is missing or moved. `include` fails silently and the parse continues without it. For a vendor-wrapping machine config, `require` is the right choice: if a CTI BSP update renames or restructures their machine file, the build tells you at the next run instead of shipping a config that silently stopped pulling in vendor settings. ### How do CTI BSP updates affect a custom distro layer built on meta-cti? If your distro layer only requires the CTI machine config and overrides variables instead of copying files, a vendor BSP update is a version bump and a rebuild, not a merge. Your camera overlay DTB, kernel bbappend, and image recipe stay in your layer untouched. The only real risk is CTI restructuring a path your bbappend depends on, which is exactly why a loud `require` failure is a feature and not just an inconvenience. --- *ProventusNova helps hardware startups solve embedded systems problems fast. [See our Custom Board Bring-up service](https://proventusnova.com/services/custom-board-bringup).* --- ## GMSL2 Multi-Camera Sync Past 4 Cameras: Fanout Topology URL: https://proventusnova.com/blog/gmsl2-multi-camera-sync-past-4-cameras/ Published: 2026-08-03 Author: Andres Campos Tags: gmsl2, multi-camera sync, frame sync, deserializer topology, jetson Why GMSL2 multi-camera sync breaks past 4 cameras, how daisy-chaining deserializers wrecks frame alignment, and the fanout topology that scales correctly. Every GMSL2 deserializer on the market caps out at four camera inputs per chip, so any system that needs five, six, or eight synchronized cameras has to bridge multiple deserializer chips together. GMSL2 multi-camera sync past 4 cameras is not a bigger-chip problem, because no bigger chip exists. It is a topology decision, and the wrong one turns an "8-camera synchronized array" into two perfectly-synced but mutually unsynced islands of four. This post covers why daisy-chaining deserializers destroys frame alignment as you scale up, and the fanout topology that keeps an array of any size on one shared timing reference. ## Key Insights - GMSL2 deserializers have a hard per-chip camera-input ceiling, commonly four, so scaling past that always means adding chips, not reconfiguring one - Daisy-chaining deserializers, where one chip generates the sync pulse and relays it to the next over a GPIO link, routes the reference through a full extra hop of relay propagation delay before it reaches the second chip's cameras - That extra hop is not a rounding error: it moves the delay budget from a tight, single-chip skew figure to something orders of magnitude larger, and it lands on every camera downstream of it - The fix is parallel-subordinate fanout: one shared hardware-timer reference, split on length-matched traces, feeding every deserializer at the same time, with none of them relaying for another - A software GPIO toggle cannot serve as that shared reference no matter how the chips are wired; only a hardware timer or PWM source has jitter tight enough to matter at this scale ## Why GMSL2 multi-camera sync breaks down past 4 cameras A GMSL2 deserializer is built to be the sync hub for its own links. It owns the timing reference, whether that reference originates internally or from an external GPIO, and it distributes that reference to every camera wired to it. Inside a single chip, this is a solved problem: the deserializer's own datasheet specifies a tight skew budget across its links, and as long as you enable the delay-compensated mode for that reverse-channel signal, every camera on that chip sees the same edge within that chip's own tight tolerance of every other camera on it. The ceiling is the chip itself. Deserializers in this class support four camera inputs each. A system that needs five, six, or eight cameras needs two or more of these chips, full stop, there is no larger single-chip part that changes this math. Once frames span more than one deserializer, frame-start alignment stops being a per-chip configuration question and becomes a system-architecture question: every chip in the array now has to be driven from a common reference, or the array is not one synchronized system, it is several small synchronized systems that happen to share a housing and a power supply. If you have not configured FSYNC on a single deserializer yet, our [GMSL2 multi-camera sync FSYNC setup guide](/blog/gmsl2-multi-camera-sync-jetson) covers the delay-compensated mode and DTS wiring for arrays of up to four cameras. This post picks up where that one leaves off, once a fifth camera forces a second chip into the design. That failure mode is common enough that it has a name we use internally: islands of four. Boards marketed as "8-camera synchronized" often turn out, on inspection, to be two four-port deserializers that are each internally perfect and never shared a timing source with each other. Each half looks fine in isolation. Across the two halves, there is no relationship at all between frame starts, because nobody wired a shared reference between the chips in the first place. For an application where the whole point is that every camera captures the same instant, an unsynchronized pair of perfectly-synchronized quads is a very expensive way to fail the actual requirement. This is exactly the kind of thing to catch before hardware is committed. When you are [choosing a GMSL2 carrier board for Jetson Orin](/blog/gmsl2-carrier-board-jetson-orin), port count and deserializer chipset are not the only questions that matter, whether the board's own layout ties multiple deserializers to one shared reference matters just as much, and it is rarely called out in a vendor's marketing copy. ## The daisy-chain trap The instinctive way to connect two deserializer chips is to make the first one the source and let it drive the second, the same way you'd chain two switches on a network. For GMSL2 sync, this is the wrong instinct, and it is worth being explicit about why. In a daisy chain, deserializer 1 is configured as the sync generator and deserializer 2 is configured to receive its reference from deserializer 1 over a GPIO link between the two chips. That link is not a passive wire. The reference has to pass through deserializer 1's own relay logic (the same reverse-channel GPIO path it uses to forward sync to its own cameras) before it ever reaches deserializer 2. That relay adds a full hop of propagation delay, well outside the tight tolerance a chip's own internal skew budget is built around. That gap is not something you tune around. It shows up as every camera behind deserializer 2 capturing measurably later than every camera behind deserializer 1, consistently, on every frame. | Topology | What the reference passes through | Effect on cross-chip alignment | |---|---|---| | Single deserializer (up to 4 cameras) | Nothing, the chip drives its own links directly | Tight, chip-specified skew across those cameras only | | Daisy-chain (chip 1 MAIN drives chip 2 as a downstream source) | One full hop of chip 1's own relay/propagation path | A fixed, much larger delay added to every camera behind chip 2 | | Parallel-subordinate fanout (recommended) | Nothing extra, every chip receives the same external reference directly | Cross-chip alignment reduces to trace and cable length matching, not chip-to-chip relay delay | The daisy chain is also fragile in a way that is easy to miss during bring-up. If deserializer 1 ever glitches, resets, or briefly loses lock, every chip behind it loses sync at the same time, because they were never independent from deserializer 1's health. A topology should not have a single point of failure sitting in the middle of the timing path. ## The correct topology: parallel-subordinate fanout The fix is to stop treating any deserializer as the source for another deserializer. Instead, pick one shared reference, generated once on the host side by a hardware timer or PWM peripheral, and fan it out on length-matched PCB traces (or equal-length cable runs, if the fanout point is off-board) to the sync input of every deserializer in the array, in parallel. ``` Host hardware-timer / PWM GPIO | (one source, length-matched fanout) +----+----+ ... + v v v Deser 1 Deser 2 Deser k <- each configured independently (subordinate) (subordinate) (subordinate) as a delay-compensated subordinate | | | <=4 cams <=4 cams <=4 cams (each chip still handles its own cameras) ``` Every deserializer in this diagram is configured the same way: it receives an external reference and distributes it to its own cameras. None of them generates the pulse, and none of them relays it to a sibling chip. Because every chip sits directly on the shared source rather than behind another chip, cross-chip skew should collapse to two things you control directly on the PCB and in the harness: how well you matched the trace lengths in the fanout, and how well you matched the cable lengths running out to each deserializer. Neither of those is a chip-to-chip propagation delay, and neither compounds as you add more chips. That's the design rationale for why this topology is sound, and it's worth being direct that it's rationale: the achievable cross-chip skew with real trace and cable mismatch on a specific board is a hardware validation question, not something to assume from the architecture alone. Measure it on your own layout before you commit to it in a spec. This is also why parallel-subordinate fanout scales cleanly and daisy-chaining does not. Going from an 8-camera array (two chips) to a 12 or 16-camera array (three or four chips) means adding another branch to the fanout, not another link in a chain. The reference source does not care how many chips are listening, as long as it can drive the electrical load, which at higher chip counts may mean a buffer or repeater stage on the fanout network rather than a longer chain of relays. The alignment story stays the same for every chip you add: distance from the same source, not distance from the previous chip. One requirement applies regardless of how you wire the chips: the shared reference itself has to come from a hardware timer or PWM output, not a GPIO toggled from a software loop. A software-driven GPIO carries scheduling jitter that lands in the microsecond-to-millisecond range depending on system load, which is the same order of magnitude as the daisy-chain penalty you were trying to avoid in the first place. Fixing the topology and then feeding it from an unstable source solves nothing. ## Verifying the array is synchronized Do not trust the wiring diagram. Verify the array after every board revision, cable change, or chip swap, using both of these checks: 1. **Read back the chip's own alignment measurement.** Most deserializers in this class expose a runtime register that reports the measured difference between the earliest and latest sync arrival across their own links. Treat that readback as ground truth immediately after enabling sync on new hardware, before you assume the topology diagram matches what got built. 2. **Compare host-side timestamps across chips, not just within one chip.** Capture frames from at least one camera behind each deserializer at the same time and compare arrival timestamps the way you would for a single-chip array. A small, fixed offset that holds steady across a long capture points to trace or cable length mismatch, which is fixable in hardware. An offset that grows over time, or jumps unpredictably between chips, points to a topology problem, most often a daisy-chain link that got wired in somewhere without anyone deciding it should be there. 3. **Confirm the source, not just the fanout.** Before debugging anything downstream, check that the shared reference is coming from a hardware timer or PWM peripheral and not a software GPIO toggle. This single check rules out the most common root cause we see on arrays that fail sync past four cameras. 4. **Re-run all of the above after any physical change.** Trace and cable length matching is a property of that specific harness, not a permanent property of the schematic. A cable swap during a later production run can reintroduce a mismatch that the original design review never had to account for. Skipping this verification step is how "islands of four" ship. The two halves of the array each pass every test you run on them individually, because each half really is internally synchronized. The only way to catch the missing cross-chip relationship is to test across chips deliberately, not just within each one. ## Frequently Asked Questions ### Can a single GMSL2 deserializer synchronize more than 4 cameras? No. GMSL2 deserializers in common use cap out at four camera inputs per chip. Once a system needs five or more synchronized cameras, you need multiple deserializer chips, and frame alignment becomes a topology decision between those chips rather than a single-chip configuration setting. ### Why does daisy-chaining GMSL2 deserializers break frame sync? Daisy-chaining has one deserializer generate the sync pulse and relay it to the next deserializer over a GPIO link, instead of both chips receiving the reference directly from a shared source. That relay adds a full extra hop of propagation delay before the signal reaches the second chip's cameras, a delay that is orders of magnitude larger than the tight single-chip skew budget. Cameras behind the second chip end up capturing measurably later than cameras on the first chip, on every frame. ### What is the correct way to synchronize multiple GMSL2 deserializer chips? Use a parallel-subordinate fanout. Generate one shared reference on the host with a hardware timer or PWM peripheral, then distribute it on length-matched traces or equal cable runs directly to every deserializer's sync input at the same time. Configure each chip independently as a subordinate; none of them should generate the pulse or relay it to another chip. ### Can a software GPIO toggle work as the shared sync reference for multiple deserializers? No. A GPIO toggled from a software loop carries scheduling jitter in the microsecond-to-millisecond range, which is large enough to defeat the entire purpose of hardware sync. The shared reference needs deterministic, low-jitter timing from a hardware timer or PWM source. Fixing the topology does not help if the source feeding it is unstable. ### How do I verify that a multi-deserializer camera array is synchronized? Read back each deserializer's own measured sync-alignment value after any wiring change, then separately capture and compare host-side frame timestamps from a camera behind each chip. A small, steady offset points to a cable or trace length mismatch. A growing or unpredictable offset between chips usually means a daisy-chain link exists somewhere in the topology that should have been a parallel fanout instead. --- *ProventusNova helps hardware startups solve embedded systems problems fast. [Talk to us about GMSL2 multi-camera bring-up](/services/gmsl).* --- ## Jetson Nano vs Orin Nano: Same Name, Different Chip URL: https://proventusnova.com/blog/jetson-nano-vs-orin-nano/ Published: 2026-08-03 Author: Andres Campos Tags: jetson nano, jetson orin nano, jetson comparison, migration, edge ai Jetson Nano and Orin Nano share a name, not an architecture. Compare the Maxwell-to-Ampere jump, the JetPack stack change, and why it's a re-port. "Jetson Nano vs Orin Nano" shows up in search a lot lately, mostly from people who assume this is a simple hardware refresh, order an Orin Nano devkit, and then discover their old Jetson Nano image doesn't boot, their carrier board doesn't fit the module, and their camera driver doesn't load. It isn't a refresh. Jetson Nano and Orin Nano share a product name and a rough price point, and almost nothing else. ## Key Insights - Jetson Nano (Tegra X1, Maxwell GPU, 2019) and Orin Nano (Orin SoC, Ampere GPU, 2023) are two full GPU generations apart, not a version bump - Jetson Nano is end-of-life with a last shipment date of January 2027; Orin Nano is active through January 2032 - The two modules use the same 260-pin SO-DIMM connector standard but different pinouts, they are not interchangeable on the same carrier board - Software is not forward compatible: Jetson Nano tops out at JetPack 4.6.x (CUDA 10.2, Ubuntu 18.04); Orin Nano runs JetPack 6.x (CUDA 12.6, Ubuntu 22.04) - NVIDIA rates Nano at 472 GFLOPS (FP16) and Orin Nano at up to 40 TOPS (quantized inference), different units measuring different things, but every directly comparable axis, CUDA core count, memory bandwidth, real-world inference speed, moved by a full order of magnitude or more ## What's different between Jetson Nano and Orin Nano? Jetson Nano runs NVIDIA's Tegra X1 SoC, the same chip family used in the original Nintendo Switch, with a Maxwell-generation GPU: 128 CUDA cores, no Tensor Cores, no DLA, quad-core ARM Cortex-A57 CPU, and 4GB of LPDDR4 at 25.6 GB/s. It launched in 2019 as a low-cost entry point into Jetson development and was never meant to be a production AI inference platform, more a way to get students and hobbyists writing CUDA code on cheap hardware. Orin Nano runs the Orin SoC, an Ampere-generation GPU with either 512 or 1024 CUDA cores depending on the 4GB or 8GB variant, an 8-core (4GB) or 6-core (8GB) ARM Cortex-A78AE CPU, and up to 68.3 GB/s of memory bandwidth. It's a genuinely current platform, sharing its architecture with the same Orin family used in AGX Orin and Orin NX production designs. See [Jetson Orin AGX vs Orin NX vs Orin Nano](/blog/jetson-orin-agx-vs-orin-nx-vs-orin-nano) if you're also deciding between Orin variants. | | Jetson Nano | Orin Nano 8GB | |---|---|---| | **SoC** | Tegra X1 | Orin (T234) | | **GPU architecture** | Maxwell | Ampere | | **CUDA cores** | 128 | 1024 | | **CPU** | Quad-core Cortex-A57 | 6-core Cortex-A78AE | | **Memory** | 4GB LPDDR4 | 8GB LPDDR5 | | **Memory bandwidth** | 25.6 GB/s | 68.3 GB/s | | **AI performance** | 472 GFLOPS (FP16) | 40 TOPS | | **DLA** | None | None | | **CUDA version** | 10.2 | 12.6 | | **JetPack / L4T** | 4.6.x / R32.7.x | 6.2.2 / R36.5.0 | | **Ubuntu base** | 18.04 | 22.04 | | **Connector** | 260-pin SO-DIMM (Nano/TX1/TX2 NX family) | 260-pin SO-DIMM (Orin NX/Nano/Xavier NX family) | | **Lifecycle status** | End of life, last shipment Jan 2027 | Active through Jan 2032 | Neither module has a DLA (Deep Learning Accelerator), the fixed-function inference block present on AGX Orin and Orin NX. If your workload leans on DLA offload, Orin NX is the module to look at, not Orin Nano. ## Same connector, different pinout: the carrier board trap This is the mistake we see most often. Jetson Nano and Orin Nano both use a 260-pin SO-DIMM style module connector, so a board with the right physical socket looks compatible at a glance. It isn't. Jetson Nano's pinout is shared with Jetson TX1 and TX2 NX, an older pin mapping tied to the Tegra X1/X2 I/O layout. Orin Nano's pinout is shared with Orin NX and Xavier NX, a separate, newer mapping. Plugging an Orin Nano module into a Jetson Nano carrier board will not work, and depending on which pins carry power versus signal on each side, it's the kind of mistake that can damage a board rather than just fail to boot. If you're planning a hardware refresh around this migration, treat it as a new carrier board design targeting the Orin NX/Nano pinout, not a drop-in module swap. We cover the Yocto side of standing up a fresh Orin Nano carrier board in [flashing a custom Yocto image to Jetson Orin Nano Super](/blog/yocto-custom-image-flash-jetson-orin-nano-super). ## The software stack doesn't carry over either Jetson Nano is stuck on JetPack 4.6.x, L4T R32.7.x, a kernel 4.9-based BSP running Ubuntu 18.04 with CUDA 10.2. NVIDIA has marked this branch legacy, it receives no new features, only security patches on a best-effort basis. Orin Nano runs JetPack 6.2.2, L4T R36.5.0, kernel 5.15, Ubuntu 22.04, CUDA 12.6, the actively developed current stable branch. See our [JetPack versions and L4T compatibility table](/blog/jetpack-versions-l4t-compatibility-table) for the full version matrix across every Jetson generation. The practical effect: there's no upgrade path, no OTA, no in-place migration. You're standing up a new BSP from scratch on Orin Nano. Anything built against JetPack 4.6's toolchain (CUDA 10.2, TensorRT for Maxwell's compute capability 5.3) needs to be rebuilt, not just recompiled, against JetPack 6's stack targeting Ampere's compute capability 8.7. TensorRT engine files in particular are architecture-specific and won't load across this jump; you rebuild the engine on the target hardware. Camera drivers hit the same wall. Jetson Nano's camera path runs through the older Tegra X1 VI/CSI hardware and an earlier Argus daemon implementation. Orin Nano uses `tegra-camera-platform`, a different driver architecture with its own device tree bindings. A custom V4L2 sensor driver written for Jetson Nano is a real re-port on Orin Nano, not a recompile, expect to touch device tree overlays, VI channel mapping, and Argus configuration. ## What actually carries over It's not all bad news. The parts of Jetson development that live above the BSP layer transfer better than the hardware does. GStreamer pipeline concepts carry over. Element names change (Jetson Nano's `nvvidconv` and `nvv4l2decoder` behave differently under Orin's memory subsystem, and buffer NVMM handling shifts) but the pipeline model, the reasons you'd reach for hardware-accelerated decode versus CPU decode, and general debugging approach (checking `gst-inspect-1.0`, tracing with `GST_DEBUG`) are the same skills. Same story for ROS2: a robotics stack built around ROS2 nodes doesn't care which Jetson SoC it's running on, as long as the underlying sensor drivers and TensorRT models are re-ported first. NVIDIA SDK Manager still drives the flashing and package install process, the workflow of selecting a target, downloading a BSP, and flashing over USB or via SD card is conceptually the same, even though you're picking a different target board and a different JetPack version. Documentation and community support is also stronger on the Orin side. Jetson Nano's forum activity has been declining for years as NVIDIA shifted developer attention to Orin; Orin Nano has an active developer community, current NVIDIA forum support, and code examples still being written against its stack today. None of this changes the hardware answer. You still need a new carrier board and a re-ported BSP. But the engineering skills and workflow habits your team built on Jetson Nano aren't wasted, they transfer to figuring out the Orin re-port faster than starting from zero would. ## Should you migrate from Jetson Nano to Orin Nano now? If you're building on Jetson Nano today, the honest answer is: yes, start planning the move. January 2027 sounds far off, but a production hardware design, BSP re-port, and camera/driver validation cycle easily takes 6 to 12 months on its own, and that's before you've qualified a new carrier board. Module pricing isn't the reason to hesitate. Orin Nano 8GB runs about $149, Orin Nano 4GB about $99, both close to what Jetson Nano cost at retail. The real cost of this migration is engineering time: the carrier board redesign, the BSP bring-up, and the camera driver re-port, not the module itself. Orin Nano is the right target if your workload fits in its lane: no DLA requirement, up to 4 MIPI CSI cameras, and a design that doesn't need more than 68 GB/s of memory bandwidth. If you're running sustained multi-camera inference or need DLA offload for a TensorRT-heavy pipeline, look at Orin NX instead, same pinout family as Orin Nano, meaning your new carrier board design can support both on one board across cost tiers. The [Orin AGX vs NX vs Nano comparison](/blog/jetson-orin-agx-vs-orin-nx-vs-orin-nano) breaks down that decision in more detail. Either way, this is a from-scratch bring-up: new carrier board, new BSP, re-ported camera drivers, rebuilt inference engines. Budget for it like one. --- *Migrating off Jetson Nano before the January 2027 cutoff? Our [Jetson expert support service](/get-expert-nvidia-jetson-support) handles the carrier board redesign, BSP bring-up, and camera driver re-port as one fixed-bid engagement.* --- ## MQTT and Protobuf IPC for Edge AI Appliances URL: https://proventusnova.com/blog/mqtt-protobuf-ipc-edge-ai-appliances/ Published: 2026-08-03 Author: Aaron Angulo Tags: mqtt, protobuf, edge ai, docker, embedded linux How we structure MQTT and protobuf IPC between containerized services on edge AI appliances, plus logging, restart policy, and watchdog recovery. Most edge AI appliances end up running five or six independent programs on one board: a capture service reading the camera or sensor array, an inference service running the model, a connectivity service managing BLE or WiFi, a fusion service combining sensor readings, and an OTA or provisioning service handling updates. The question that decides whether that fleet stays maintainable is how those programs talk to each other. On appliances we've built, the answer is MQTT and protobuf IPC: a local broker carrying protobuf-encoded messages as the only communication path between services, with no direct sockets and no shared code. This post covers why that pattern holds up in the field, along with the container structure, logging strategy, and recovery layers that turn it from a demo into something you can leave running unattended for months. ## Key Insights - **The MQTT broker is the only IPC surface**: services never import each other's code or open direct sockets to one another; if a message doesn't cross the broker, it isn't an interface between services - **Protobuf keeps the wire format compact and schema'd**: services agree on message shape through a `.proto` contract instead of hand-rolled JSON parsing that drifts out of sync over time - **Per-service privileged containers trade isolation for hardware reach**: acceptable on a single-tenant appliance where the container fleet is the entire system, not a pattern to carry over to shared infrastructure - **Logging is split by severity, not by service**: verbose and debug logs live in a RAM-backed tmpfs and disappear on reboot; only error-and-above logs get written to flash, capped and rotated - **Recovery is layered, not left to Docker's restart policy**: an external process supervisor owns restart decisions for the whole fleet, and a hardware watchdog wired through an I2C GPIO expander is the last resort when userspace itself has wedged ## MQTT and protobuf IPC as the only bus between services The obvious alternative to a broker is direct connections: give the inference service a socket to the capture service, give the fusion service a socket to both, and so on. That works until the fourth or fifth service joins the fleet, at which point you're maintaining N-squared point-to-point wiring, and adding a new service means touching every existing one that needs to talk to it. A local MQTT broker collapses that to one connection per service, always to the broker, regardless of fleet size. Adding a downloader or provisioning service later means subscribing it to the topics it cares about; nothing else changes. We run `eclipse-mosquitto` as its own container, bound to `127.0.0.1:1883`, with no cloud dependency for local traffic. The broker address is injected as an environment variable, `MQTT_BROKER_ADDRESS`, so the exact same service image runs against a remote broker on a developer's laptop and the loopback broker on the device, unchanged. That single decision removes an entire class of "works on my machine, breaks on the board" bugs, because the IPC path is identical in both environments. Protobuf as the payload format matters more than it looks. JSON would work fine if every service were written in the same language, but a fleet of services written in different languages needs a contract that isn't tied to any one of their native serialization formats. A `.proto` schema compiles to typed bindings in each language, so a field rename or a new enum value is caught at build time in every service that consumes it, instead of surfacing as a silent `KeyError` on a device in the field six months later. The other piece worth calling out is retained topics. Configuration and runtime state get published once with the MQTT retain flag set, and any service that subscribes afterward, whether it started a second later or after a container restart, immediately receives the last published value without waiting for the next update. That makes the broker the single source of truth for runtime config and removes the need for a separate config-distribution path (a shared file, a database, a sidecar). A service that restarts mid-flight doesn't need special-case bootstrap logic; it just subscribes and gets caught up. ## Structuring the container fleet Each service gets its own container, one responsibility per container, sharing a common baseline set through a YAML anchor rather than duplicating settings across five service definitions: | Setting | Value | Why | |---|---|---| | `restart` | `"no"` | Docker doesn't own recovery; an external supervisor does | | `network_mode` | `host` | every container reaches the loopback broker with no bridge/NAT hop | | `logging.driver` | `json-file` | standard driver, but capped (see logging section) | | `logging.options.max-size` | `1m` | hard ceiling on flash writes per log file | | `logging.options.mode` | `non-blocking` | log I/O never stalls the application | | `privileged` | `true` (device-touching services only) | direct access to host devices and buses | The broker itself is the one service that stays unprivileged; it only needs the shared log mount, since it isn't reaching into hardware. If you've only ever run one container on a Genio or Jetson board (see our post on [Docker with GPU acceleration on MediaTek Genio](/blog/mediatek-genio-docker-gpu-containers)), the jump to a fleet mostly means deciding, per service, how far to open the door: 1. **Memory caps on heavy services.** A capture service holding frame buffers gets a `deploy.resources.limits.memory` ceiling in the low hundreds of MB, so a leak in one service can't OOM the whole board. 2. **Device passthrough scoped to what each service touches.** An audio or voice service gets `devices: [/dev/snd]` plus read-only ALSA config mounts; it doesn't get the whole `/dev` tree. 3. **`pid: host` only where a service must see host processes.** A host-side Bluetooth manager typically needs this; a pure compute service doesn't. 4. **Selective binary bind-mounts instead of a wholesale rootfs mount, where feasible.** If a service only shells out to a handful of specific host binaries, mount those paths rather than the whole filesystem. The two settings that make the rest of this possible, `network_mode: host` and a full `/:/host_root` bind-mount on `privileged: true` containers, are a deliberate trade. They give up per-container network isolation and most of the filesystem sandboxing Docker normally provides, in exchange for services being able to reach real host devices, firmware blobs, and tool binaries without baking a copy of each into every image. On a single-tenant appliance where the container fleet is the entire software stack, that's a reasonable trade. On shared or multi-tenant infrastructure, it would not be. ## Flash-wear-aware logging Embedded flash (eMMC or SD) has a finite number of write cycles, and a chatty service fleet logging at debug level around the clock will wear through it faster than most teams expect, especially on a device that runs for years without a storage replacement. The fix is splitting log destinations by severity: | Log class | Destination | Survives reboot | Notes | |---|---|---|---| | Verbose / debug / info | `/tmp` (tmpfs, RAM-backed) | No | every container mounts the same RAM disk, so all services log to one place | | Error and above | Docker `json-file` on flash | Yes | capped by `max-size` and `max-file`, non-blocking so log writes never stall the app | This changes how debugging happens day to day, for the better. Live tailing during development or a field debug session reads the RAM logs directly: ```sh tail -F /tmp/.log tail -F /tmp/.log | grep -i ``` Post-mortem after a reboot or a crash only has the persisted error stream to work with: ```sh docker logs ``` The verbose logs are gone after a power cycle, on purpose. Keeping only error-and-above on flash is the entire point: you get full visibility while a device is up and reachable, and you get just enough on-flash history to diagnose why it went down, without writing gigabytes of debug chatter to storage that has to survive the product's whole field life. ## Recovery in layers: supervisor, then hardware watchdog Every service container is configured with `restart: "no"`. That's deliberate: Docker's built-in restart policies are per-container and don't know about dependency order, backoff, or what "healthy" means for your specific fleet. Instead, an external process supervisor, a plain host-level process, owns start, stop, and restart decisions for the whole set. Centralizing that logic in one place means a single service crash-looping can be handled with backoff instead of Docker hammering it in a tight restart cycle, and a service that depends on another coming up first can wait for it, which Docker's flat restart model has no concept of. Above the supervisor sits a hardware watchdog, for the case the supervisor itself can't help: a kernel panic, a wedged I2C bus, or the supervisor process itself hanging. The watchdog is kicked periodically through an I2C GPIO expander line. If the kick stops, because the supervisor or some critical service has wedged badly enough that nothing in userspace is running anymore, the watchdog forces a hardware reset. It's not a replacement for the supervisor, it's the layer beneath it: the recovery path for the failure mode where software-level recovery is no longer possible. The exact I2C address, expander part, and pin assignment are board-specific and need to be verified against your hardware before you rely on them; the pattern (a hardware timer, kicked by a live supervisor, resetting the board when the kick stops) is what carries over. Getting this three-layer setup right (broker-mediated IPC, a supervisor with real restart policy, a watchdog underneath it) is most of what separates an appliance that self-heals from one that needs a truck roll every time a service wedges. If your fleet also has a service with a real-time constraint buried in it, a control loop inside the fusion service, for example, that's a separate problem worth solving deliberately; see our post on [PREEMPT_RT on Jetson Orin](/blog/jetson-orin-preempt-rt-real-time-kernel) for what that involves. ## Frequently Asked Questions ### What is the best way to handle inter-process communication on an edge AI device? For a fleet of independent services on one board (capture, inference, connectivity, fusion, OTA), a local MQTT broker carrying protobuf messages works well because it decouples every service from every other one. No service imports another's code or opens a direct socket; the broker is the entire IPC surface. This matters more as the service count grows, since the alternative is N-squared point-to-point wiring. ### Why use MQTT instead of gRPC or D-Bus for edge device IPC? gRPC needs a client stub for every service pair, so five services means ten connections to wire and maintain. D-Bus is Linux-specific and gets awkward across container mount namespaces. A local MQTT broker gives every service one connection (to the broker) regardless of how many other services exist, and publish/subscribe means adding a new service does not require touching the ones already running. ### How do you protect eMMC or SD card flash from wear in an embedded Linux appliance? Split logging by severity. Route verbose and debug logs to a RAM-backed tmpfs mount that every container shares, and only write error-and-above logs to flash, capped with a max file size and rotation count. On a device running continuously for months, this is the difference between a log volume that never touches flash under normal operation and one that wears out the storage in weeks. ### What is a hardware watchdog and do I need one for an edge AI appliance? A hardware watchdog is a timer, often behind an I2C GPIO expander, that forces a hard reset if it stops being kicked. You need one any time software recovery might not be enough: a kernel panic, a wedged I2C bus, or a supervisor process that hangs. It's the layer beneath your process supervisor, not a replacement for it, and it only matters at the point where userspace can no longer help itself. ### Should each service run in its own Docker container on an embedded device? Yes, when the services have different privilege and lifecycle needs. One container per responsibility (capture, inference, connectivity, fusion, provisioning) means you can restart, resource-cap, and update each independently, and a crash in one doesn't take down the others. The tradeoff is that privileged, host-networked containers on a single-tenant appliance give up most container isolation in exchange for direct hardware access, which is a fair trade on a device that only ever runs your software. --- *ProventusNova helps hardware startups deploy production edge AI appliances fast. [See our EdgeAI Deployment service](/services/edgeai-deployment).* --- ## NFC-DEP P2P Pairing on MediaTek Genio 720 URL: https://proventusnova.com/blog/nfc-dep-p2p-pairing-mediatek-genio-720/ Published: 2026-08-03 Author: Aaron Angulo Tags: mediatek, genio, nfc, pn7160, embedded linux NFC-DEP P2P pairing on MediaTek Genio 720 stalling for minutes at a time? We found a phase-lock trap in the NCI 2.0 timing and fixed it with one parameter. NFC-DEP P2P pairing on MediaTek Genio 720 worked fine in our first bench test, sub-second, every time, then fell apart the moment we ran two boards side by side for more than a few minutes. Pairing would suddenly take five to fifteen minutes instead of under a second, recover on its own, then stall again, with nothing obviously wrong in the logs. Both boards ran the exact same PN7160 driver code, negotiating initiator and target roles over NCI 2.0 passively, which is what made the failure hard to see: there was nothing wrong with the protocol implementation. Two identical devices running synchronized timing windows were locking into the same phase instead of drifting apart, and that trap only shows up once you let two boards run long enough to find each other. ## Key Insights - Symmetric NFC-DEP polling code on two identical boards doesn't reliably diverge into initiator and target roles on its own. Software-managed LISTEN/POLL windows can resynchronize instead of complementing. - The failure mode: both boards spend most of their time in the same RF phase, both listening (no field from either side) or both polling (RF collision), producing five to fifteen minute pairing delays mid-session. - The fix is a single RF_DISCOVER command covering both NFC_A_PASSIVE_LISTEN and NFC_A_PASSIVE_POLL, combined with randomizing the NCI TOTAL_DURATION parameter per pairing attempt (150 to 350 milliseconds). - Two boards with different randomized cycle rates accumulate phase drift at roughly 1 to 3 Hz, guaranteeing a complementary LISTEN/POLL overlap within about 400 milliseconds. - Role isn't chosen by application code. It falls out of the RF_INTF_ACTIVATED_NTF payload: byte[3] tells you whether you were found (target) or found the other side (initiator). ## Why NFC-DEP P2P Pairing on MediaTek Genio 720 Kept Stalling NFC-DEP P2P in passive NFC-A mode at 106 kbps works like this: the initiator generates the 13.56 MHz RF field and sends `ATR_REQ`; the target does not generate a field, it modulates the initiator's field and responds with `ATR_RES`. Both sides run the same code base and the same chip. Nothing pre-assigns a role, it comes out of whichever side happens to be generating the field when the other side shows up. The NCI 2.0 state machine that governs this is `RFST_IDLE → RF_DISCOVER → RFST_DISCOVERY → RF_INTF_ACTIVATED_NTF → RFST_POLL_ACTIVE / RFST_LISTEN_ACTIVE`, and there's one setup step easy to get wrong before any of that matters: `RF_SET_LISTEN_ROUTING` has to be called once, from `RFST_IDLE`, to route the NFC-DEP protocol to the device host. Skip it and the chip silently discards incoming activations even while `RF_DISCOVER` is running, no error, just nothing happening. Our first implementation handled role negotiation the way it reads most naturally in application code: alternate the board between listen-only and poll-only `RF_DISCOVER` commands in software, half-second to one-second windows, with a `pair()` timeout firing every 30 seconds that restarted the cycle with fresh random jitter if nothing had happened. On a single board, tested against a phone or a reference reader, this looked completely fine. Put two of our own boards next to each other and let them run, and the failure showed up: because both boards were running identical timing code, when their 30-second timeouts fired at close to the same moment, they'd restart with new jitter but often land back on the same phase. Instead of one board polling while the other listens, both would spend long stretches doing the same thing at the same time. Both listening means no RF field from either side, so nothing to activate. Both polling means two fields colliding, so no clean activation either way. The result was pairing delays of five to fifteen minutes in the middle of a session that had been pairing in under a second minutes earlier. ## The Phase-Lock Math: Why Two Boards Resynchronize Instead of Diverging Once we stopped treating this as a protocol bug and started treating it as a timing problem, the fix fell out of the math. Two boards running fixed, identical discovery windows have a phase drift rate of zero relative to each other. If a restart happens to land them on the same phase, they stay there indefinitely, and the only thing that ever breaks the tie is the random jitter applied at each 30-second restart, which is exactly the intermittent "it recovers, then stalls again" pattern we were seeing. The alternative is to make sure the two boards are never running the same cycle rate in the first place, so the phase between them is always moving. Randomizing the discovery timing per pairing attempt is enough: | Metric | Value | |---|---| | Board A TOTAL_DURATION | 200 ms → 5 cycles/sec | | Board B TOTAL_DURATION | 280 ms → 3.57 cycles/sec | | Phase drift rate | \|5 − 3.57\| = 1.43 cycles/sec | | Time to full desynchronization | 1 / 1.43 ≈ 700 ms | | Expected time to complementary overlap | ~350 ms | With each board picking its own `TOTAL_DURATION` at random for every pairing attempt, the two cycle rates are almost never equal, so the relative phase between them constantly advances instead of sitting still. Within roughly one drift period, well under half a second in practice, one board is guaranteed to be polling while the other is listening. That complementary overlap is the only thing NFC-DEP needs in order to activate. ## The Fix: One RF_DISCOVER Call and a Randomized TOTAL_DURATION The correct approach doesn't alternate anything in application code at all. Send a single `RF_DISCOVER` command that requests both `NFC_A_PASSIVE_LISTEN` and `NFC_A_PASSIVE_POLL` in the same call. The NFCC itself then cycles between the two technologies internally, at `TOTAL_DURATION` intervals, and that's the one parameter we randomize per `pair()` call: ```python # _configure_dep() -- called once per pair() call duration_ms = random.randint(150, 350) self._proto.send_command(nci.GID_CORE, nci.OID_CORE_SET_CONFIG, bytes([ 2, 0x00, 2, duration_ms & 0xFF, (duration_ms >> 8) & 0xFF, # TOTAL_DURATION 0x32, 1, 0x40, # LA_SEL_INFO = 0x40 (NFC-DEP capable SAK) ])) # RF_DISCOVER_MAP: map NFC-DEP for both poll and listen self._proto.send_command( nci.GID_RF, nci.OID_RF_DISCOVER_MAP, bytes([1, nci.PROTOCOL_NFC_DEP, 0x03, nci.INTF_NFC_DEP]), ) # RF_DISCOVER: both technologies in one command self._proto.send_command( nci.GID_RF, nci.OID_RF_DISCOVER, bytes([2, nci.NFC_A_PASSIVE_LISTEN, 0x01, nci.NFC_A_PASSIVE_POLL, 0x01]), ) ``` Role determination then comes straight out of the activation notification, not from anything the application decided in advance: ```python if payload[3] == nci.NFC_A_PASSIVE_LISTEN: # 0x80 role = 'target' # we were found by an initiator else: # 0x00 = NFC_A_PASSIVE_POLL role = 'initiator' # we found a target ``` Getting this loop to run reliably in practice meant closing a handful of other NCI 2.0 gaps at the same time, all of them the kind of thing that only shows up once you're running the fix continuously across many pairing cycles instead of once on a bench: 1. **RF_SET_LISTEN_ROUTING must be called from RFST_IDLE.** Send it while the chip is in RFST_DISCOVERY and it returns SEMANTIC_ERROR. We now always issue `RF_DEACTIVATE(DEACT_IDLE)` before re-calling routing setup, instead of assuming we know the chip's current state. 2. **RF_DEACTIVATE has an NTF guard.** When the `RF_DEACTIVATE` response status isn't `STATUS_OK`, meaning the chip was already idle, no `RF_DEACTIVATE_NTF` notification is coming. Waiting for it anyway burns the full two-second `recv_ntf` timeout on every single deactivation. Check the response status first, and only wait for the notification if the chip was active to begin with. 3. **RF_DEACTIVATE_NTF can arrive mid data exchange.** If the RF link drops while data is in flight, because the peer moved away, the NFCC sends `RF_DEACTIVATE_NTF`. If `recv_data` doesn't check for it explicitly, it silently consumes the notification and then waits the full five-second data timeout for a packet that's never coming. One board reports a successful pairing while the other one just stalls. The fix is to fast-fail in `recv_data` the moment that notification shows up. 4. **The `_running` flag needs an unconditional deactivate guard.** A previously swallowed exception can leave `_running` set to `False` while the chip is still sitting in `RFST_DISCOVERY`. The next `pair()` call then skips deactivation entirely, jumps straight to `_configure_dep()`, and hits `SEMANTIC_ERROR` on `RF_SET_LISTEN_ROUTING` for reasons that look unrelated to the actual cause. We now send `RF_DEACTIVATE` unconditionally every time and let the chip's own status response tell us whether it was already idle. ## Shipping the Fix Without Losing It on Reboot The driver runs inside a Docker container on the target hardware, which surfaced a second, much more mundane problem once the fix was ready to deploy: copying updated files into a running container does not survive a full reboot. `docker compose up -d` recreates the container from its base image on every restart, wiping out anything that was copied in after the fact. The pattern that survives reboots is to bind-mount the driver source from a host path into the container, read-only, and add a systemd drop-in on the service that manages the pairing daemon, with an `ExecStartPre` step that copies the updated driver files into the container's Python virtual environment before the application process starts: ```ini # /etc/systemd/system/nfc-pairing-app.service.d/pn7160-overlay.conf [Service] ExecStartPre=/usr/bin/docker exec nfc-app /bin/sh -c \ 'cp /opt/nfc-driver/pn7160/*.py /opt/venv/lib/python3.12/site-packages/pn7160/' ``` That survives a full reboot cleanly. Only a `docker compose down` followed by a fresh image reload from a tarball wipes it, which is rare enough in practice to treat as a known exception rather than something to build around. If you're bringing up the I2C bus the PN7160 sits on, our walkthrough on [SPI and I2C peripheral setup on MediaTek Genio](/blog/mediatek-genio-spi-i2c-peripheral-setup) covers bus numbering and the device tree conflicts that show up before you ever get to the NFC layer. And if your NFC stack needs to live inside a container long-term rather than as a one-off test, see [Docker with GPU acceleration on MediaTek Genio](/blog/mediatek-genio-docker-gpu-containers) for the broader container deployment pattern this fix builds on. ## Frequently Asked Questions ### What causes NFC-DEP P2P pairing to fail or stall intermittently? The most common cause on custom NFC-DEP implementations is a timing collision between the two devices' discovery windows, not a protocol bug. If both sides alternate between listen-only and poll-only discovery on independent software timers, they can resynchronize onto the same phase instead of diverging into complementary initiator and target roles, producing multi-minute pairing delays that come and go without a clear pattern. ### What is a phase-lock trap in NFC role negotiation? It's a failure mode where two devices running identical, synchronized timing code lock into the same discovery phase instead of drifting apart. Both boards end up listening at the same time (no RF field, nothing to activate) or polling at the same time (RF collision), so the pairing handshake that's supposed to take under a second instead takes minutes. ### How do you set TOTAL_DURATION in NCI 2.0 for NFC-DEP pairing? TOTAL_DURATION is set via CORE_SET_CONFIG (config parameter 0x00, 2 bytes, little-endian) before RF_DISCOVER. It controls how long the NFCC spends cycling through each requested RF technology; with both listen and poll requested in one RF_DISCOVER call, each gets TOTAL_DURATION divided by two. Randomizing the value per pairing attempt, for example 150 to 350 milliseconds, is what prevents two boards from settling into a fixed, matching cycle. ### Why does RF_SET_LISTEN_ROUTING return SEMANTIC_ERROR on the PN7160? RF_SET_LISTEN_ROUTING can only be called while the chip is in RFST_IDLE. If it's sent while the chip is already in RFST_DISCOVERY, the PN7160 returns SEMANTIC_ERROR and rejects the routing update. The fix is to always issue RF_DEACTIVATE with DEACT_IDLE before re-sending the routing configuration, rather than assuming the chip's current state. ### Does MediaTek Genio 720 support NFC-DEP peer-to-peer pairing? Yes. We validated NFC-DEP P2P pairing on two Genio 720 (MT8390) boards using a PN7160 NFC controller over I2C, driven from a Python NCI 2.0 driver running in a Docker container. Sub-second pairing is achievable once the discovery timing is handled correctly at the RF_DISCOVER level rather than alternated in application code. --- *ProventusNova helps hardware startups solve embedded systems problems fast. [Get expert MediaTek Genio support](/get-expert-mediatek-genio-support).* --- ## RAW10 Capture and GPU Debayer on Jetson: the Argus Bypass URL: https://proventusnova.com/blog/raw10-capture-gpu-debayer-jetson-argus-bypass/ Published: 2026-08-03 Author: Aaron Angulo Tags: jetson, cuda, v4l2, argus, camera pipeline When nvargus-daemon degrades under sustained high-fps recording, capture RAW10 over V4L2 and debayer on the GPU with CUDA. What it costs, what breaks. When `nvargus-daemon` starts throwing `failed to create capture session` errors deep into a continuous recording run, the standard move is a daemon restart, and it usually works, for a while. On Jetson Orin fleets doing sustained high-fps capture, that workaround is known to degrade again after enough recording cycles. The pattern worth knowing for that situation is RAW10 capture and GPU debayer on Jetson: skip Argus and the Tegra ISP entirely, pull raw Bayer off the sensor over V4L2, and demosaic on the GPU with CUDA instead. This is not a first move. It's what you reach for after the restart-and-hope pattern stops holding. ## Key Insights - The failure pattern that justifies this bypass is specific: `failed to create capture session`, socket/RPC dispatch errors, or zero-byte output files that recur across sustained recording sessions, not a one-time crash - The real trigger is usually the encode-and-write-to-disk stage falling behind at high fps, not frame capture itself, it just surfaces as an Argus failure - The replacement pipeline keeps everything zero-copy: V4L2 dmabuf (RAW10/`RG10`) into a CUDA debayer into NVENC, no CPU round trip - FastVideo CUDA Debayer and CUVILib, the two established libraries for this, are commercially licensed, not free, not open source, and not bundled with JetPack - Dropping Argus means reimplementing auto-exposure, auto-white-balance, and tone mapping yourself, and the GPU debayer now competes with any other GPU work for the same silicon ## Why RAW10 capture and GPU debayer on Jetson become the fallback Argus (`nvarguscamerasrc`, `nvargus-daemon`) is the right default on Jetson. It runs the Tegra hardware ISP, handling auto-exposure, auto-white-balance, denoise, and demosaic automatically, and most production camera pipelines should use it. We've written about getting Argus running correctly and debugging its common failure modes in [our Argus driver setup guide](/blog/argus-api-camera-driver-jetson). If your camera works with `v4l2-ctl` but Argus refuses to start, or the daemon deadlocks under load, those are usually fixable without abandoning the ISP path, and we cover the crash-versus-deadlock split and the JetPack 6 `libnvscf` CaptureScheduler regression in [our nvargus crash and deadlock debug guide](/blog/nvargus-crash-capturesched-deadlock-jetson). The bypass in this post is for a narrower and worse situation: you've already applied the point-release upgrade, you've already got a watchdog restarting the daemon on stall, and the pipeline still degrades under your specific workload. The pattern is consistent across reports of this failure mode: clean recording for the first several sessions, then Argus starts producing `failed to create capture session` on session start, or the socket between the client process and `nvargus-daemon` starts throwing RPC dispatch errors, or a recording completes but leaves a zero-byte file on disk. Restarting the daemon and the application recovers it, but only for a while, and the recurrence gets more frequent the longer the device has been running. The important diagnostic point is that frame capture itself is rarely the bottleneck. The instability shows up during or right after the encode-and-write-to-disk stage at sustained high frame rates. Argus is the thing that visibly falls over, but it's downstream pressure, not a capture-path bug, that's putting it there. Once you've confirmed that (watchdog logs plus disk write timing are usually enough), the fix isn't to debug Argus harder, it's to remove it from the pipeline so there's nothing left to fall over. ## The bypass pipeline: V4L2 to CUDA to NVENC, zero-copy The replacement architecture keeps the same zero-copy discipline that made Argus attractive in the first place, it just moves the ISP work off NVIDIA's stack and onto one you own: 1. **Capture raw Bayer directly from the sensor node over V4L2** as `RG10` (RAW10), no ISP involved. This is the same raw path used to bring up a sensor before Argus is ever in the picture, `v4l2-ctl` and a bare `v4l2src` pipeline prove sensor, device tree, and CSI path all work before you build anything on top. 2. **Keep the buffer as a dmabuf**, not a CPU-mapped copy. The V4L2 capture must be configured for dmabuf export so the frame never round-trips through host memory before the GPU touches it. 3. **Debayer on the GPU** with a CUDA imaging library that operates on NVMM/dmabuf buffers directly. This is the step where FastVideo CUDA Debayer or CUVILib do the demosaic work, converting `RG10` into a usable RGB or YUV frame without leaving GPU memory. 4. **Hand the debayered frame to NVENC** through the same NVMM zero-copy path Argus output would have used. If your encoder integration already expects NVMM buffers from `nvarguscamerasrc`, the encoder side of the pipeline barely changes, only what feeds it does. 5. **Write to disk or stream out**, same as before. | Stage | Argus path | Bypass path | |---|---|---| | Sensor read | V4L2 subdev, internal to Argus | V4L2 subdev, direct | | ISP / demosaic | `nvargus-daemon`, Tegra ISP | CUDA debayer (FastVideo / CUVILib) | | Buffer handling | NVMM, managed by Argus | NVMM/dmabuf, managed by your code | | Encode | NVENC via `nvarguscamerasrc` output | NVENC, same zero-copy contract | | Failure surface | `nvargus-daemon`, single process | Your capture + CUDA code, no daemon dependency | The tradeoff is visible in that last row. You've traded a single, opaque, NVIDIA-maintained daemon for a pipeline you own end to end, which means the specific failure mode you were chasing (Argus instability under load) goes away, but debugging responsibility for everything downstream of the sensor now sits with your team. ## What you lose when you drop Argus, and who has to rebuild it This is the part of the bypass that's easy to underestimate in a planning meeting and hard to underestimate once you're staring at a washed-out or green-tinted frame in the field. Argus isn't just a demosaic step, it's a full ISP, and every function it performs has to land somewhere else once it's out of the pipeline. | ISP function (Argus path) | Who does it after the bypass | |---|---| | Demosaic | GPU CUDA debayer | | Auto-exposure (AE) | Your own GPU/software control loop | | Auto-white-balance (AWB) | Your own GPU/software implementation | | Temporal noise reduction | Your own GPU/software implementation | | Tone mapping | Your own GPU/software implementation | None of these are drop-in replacements. AE and AWB are closed-loop control problems, you need a metering strategy, a gain/exposure actuation path back to the sensor driver, and enough tuning to avoid hunting or flicker under changing light. Tone mapping and noise reduction are image-quality work that the Tegra ISP has had years of vendor tuning behind it, your first pass will not look as good, and getting it close takes real iteration time against real scenes, not a synthetic test chart. There's a second cost that's easy to miss because it isn't in the image quality column at all: the CUDA debayer now shares the GPU with anything else you're running there, stitching, other CV processing, or inference. On Argus, ISP work happens on a dedicated hardware block and doesn't touch your compute budget. On the bypass, it does. Budget GPU headroom for the debayer the same way you'd budget it for a model, because it is now competing for the same silicon. ## Licensing reality: CUDA debayer is not free software This needs to be said plainly because it gets glossed over in vendor conversations: FastVideo CUDA Debayer and CUVILib are commercial products. They are not open source, they are not bundled with JetPack or the Jetson SDK, and they are not free to deploy across a fleet. If a proposal for this architecture doesn't include a line item for per-unit or per-fleet licensing of whichever debayer library you pick, that proposal is missing a real cost, not a rounding error. The alternative, writing your own CUDA debayer kernel, is a legitimate option and some teams do it, but it is a real computer-vision and CUDA engineering project on its own, not a weekend task bolted onto a capture pipeline. Interpolation quality, edge artifacts, and performance at your target resolution and frame rate all have to be validated against real sensor output, not just a reference test pattern. Whether you buy a commercial library or build your own, the debayer step has a cost that has to be planned for; the only thing that changes is whether that cost shows up as a license fee or as engineering time. There's a cleaner way out of this entire tradeoff if it's still available to you: choose a sensor module with an onboard ISP, so Argus (and this whole bypass question) never enters the design in the first place. The module emits already-processed frames, and neither the Tegra ISP nor a from-scratch software ISP is in your critical path. If you're early enough in hardware selection that a sensor swap is on the table, it's worth putting on the list before committing to either side of the Argus-versus-bypass decision. ## Frequently Asked Questions ### What causes nvargus-daemon to fail during sustained high-fps recording on Jetson? In our experience the daemon itself is rarely the root cause. The trigger is usually the encode-and-write-to-disk stage falling behind at high frame rates, and that back-pressure surfaces as an Argus failure: `failed to create capture session` errors, socket or RPC dispatch failures, or zero-byte output files. Restarting `nvargus-daemon` and the application recovers the pipeline temporarily, but on some fleets it degrades again after a number of recording cycles. ### Is FastVideo CUDA Debayer or CUVILib free or open source? No. Both are commercial, closed-source libraries. Neither ships with the Jetson SDK or JetPack, and neither is free for production use. If you plan to deploy a fleet of devices running GPU debayer, per-unit licensing needs to be budgeted as a real line item, the same way you'd budget a sensor or a connector, not assumed to be included because it runs on Jetson. ### Can you bypass the Argus ISP on Jetson and still get zero-copy capture into the encoder? Yes, that is the point of the bypass. You capture raw Bayer over V4L2 as a dmabuf, debayer it on the GPU with a CUDA library that operates on NVMM buffers, and hand the result to NVENC without a CPU round trip. The zero-copy chain from sensor to encoder is preserved, you are only replacing what does the demosaic and ISP work, not adding a copy back to host memory. ### What do you lose when you bypass Argus and capture raw V4L2 on Jetson? You lose the entire Tegra ISP: demosaic, auto-exposure, auto-white-balance, temporal noise reduction, and tone mapping. All of that has to be reimplemented in GPU or software to reach comparable image quality. This is a real engineering project, not a configuration change, and it's the main reason the bypass is a last resort rather than a default architecture. ### Is there a way to avoid nvargus-daemon instability without building a custom GPU debayer pipeline? Sometimes. If the instability is a known `libnvscf` CaptureScheduler deadlock, a JetPack point release upgrade or a watchdog that restarts the daemon on stall can resolve it without touching your capture architecture. The GPU debayer bypass is worth it only when that class of fix has been tried and the failure keeps recurring under your specific sustained-recording workload, or when a sensor swap to a module with an onboard ISP isn't an option. --- *ProventusNova helps hardware startups solve embedded systems problems fast. [Get camera pipeline and driver development done in two weeks](/services/csi).* --- ## Whisper on MediaTek Genio NPU: Our Deployment Story URL: https://proventusnova.com/blog/whisper-mediatek-genio-npu/ Published: 2026-08-03 Author: Andres Campos Tags: mediatek genio, whisper, npu, onnx runtime, edge ai How we got OpenAI's Whisper running on the MediaTek Genio NPU, the mid-conversation reboot that nearly killed the release, and the pattern behind it. We got a call about putting OpenAI's Whisper on a MediaTek Genio board for offline speech-to-text, no cloud fallback, no exceptions. Getting Whisper running on the Genio NPU turned out faster and cleaner than anyone on the call expected. Then, a few weeks into integration testing, the board started rebooting itself in the middle of a conversation, and the release sat on hold until we found out why. ## Key Insights - Whisper-medium ran accelerated on the Genio NPU faster than our own bring-up estimate, which surprised us given how little public documentation exists for edge Whisper deployments on this class of NPU. - We picked Whisper-medium over smaller, cheaper variants because of NPU operator coverage, not because of accuracy or latency targets. Model choice was a compatibility decision first. - A demo that ran cleanly for five-minute test sessions eventually crashed the AI Processing Unit hard enough to force a full board reboot, not just an application restart. - The failure was invisible under any short demo and only surfaced after enough continuous inference cycles had run, which is exactly the kind of bug that survives a client walkthrough and fails in the field. - The standard mitigation for this class of failure has nothing to do with the model itself, it's about how the model gets compiled for the accelerator. ## Whisper on the MediaTek Genio NPU: what we set out to build The client was building a voice-driven embedded product that had to work with no network connection available, on hardware with real power and thermal limits. They already had Whisper running on the CPU as a proof of concept, and it worked, but it was too slow for anything resembling a natural back-and-forth exchange. Moving speech-to-text onto the NPU was not a nice-to-have. It was the only path to an acceptable response time without moving to a bigger, more expensive SoC that the product's cost target could not absorb. Going in, we did not know whether Whisper's architecture would map cleanly onto Genio's accelerator hardware. Whisper is an encoder-decoder transformer, heavy on attention and with more dynamic shape behavior than the CNNs most edge NPU tooling is built and tested around. Genio's [AI subsystem splits work across the NPU and MDLA](/blog/mediatek-genio-apu-npu-vpu-mdla-explained), and it was not obvious ahead of time how much of Whisper's graph would land on the fast fixed-function path versus fall back to the more general-purpose cores. We deliberately stayed on the general-purpose inference runtimes for this, ONNX Runtime with the NeuronExecutionProvider as our primary path, rather than reaching for a vendor-specific transformer toolchain that would have tied the whole deployment to a narrower, less field-tested code path. That decision mattered more than it looked like at the time. The standard ONNX and TFLite delegate paths are what MediaTek's own validation has exercised most heavily, and staying on well-worn tooling meant that whatever came up later, including the crash, there would be existing techniques and reference points to debug it with, instead of fighting an SDK nobody on the team had scar tissue with. The first bring-up went well. Whisper-medium transcribed test audio fast and accurately on the NPU, and the demo looked good enough that everyone on the call, including us, was a little surprised. Very few teams were running Whisper with NPU acceleration on this class of edge silicon at the time, so there was no playbook to compare notes against. We were writing the playbook as we went. ## Why we shipped Whisper-medium instead of a smaller model The obvious move for an edge device is to pick the smallest model that meets your accuracy bar, since every parameter you shed buys back latency and power headroom. We didn't do that, and the reason was operator coverage, not accuracy. Every Whisper size shares the same architecture shape, so the question was never "does the encoder-decoder pattern run on this NPU." It was "does this specific size, at this specific export configuration, hit only operators the delegate has already proven out." Smaller models are not automatically safer here. A smaller model still needs every operator in its graph individually validated against the NPU delegate, and a variant nobody has run before carries the same discovery risk as a custom architecture, regardless of its parameter count. | Whisper variant | Approx. parameters | Why we didn't start here | |---|---|---| | tiny | ~39M | Smallest footprint, but its operator graph had no track record on this delegate path; every layer would need its own validation pass | | base | ~74M | Same coverage risk as tiny, and the power savings over medium weren't worth the accuracy we'd be giving up during initial bring-up | | small | ~244M | A reasonable phase-two target once the delegate path itself was proven stable in production | | medium | ~769M | Matched the reference model MediaTek's own NPU tooling had already been validated against, so we shipped this first | The plan from day one was to ship the vendor-validated model, get it stable in production, and only then qualify a lighter variant against real field data. That sequencing is what let us separate two different kinds of risk, model accuracy risk and NPU delegate risk, and debug them one at a time instead of at once, which mattered once the crash showed up and we needed to know whether the model or the delegate path was the moving part. ## The demo worked. Then the board started rebooting. Short test sessions never showed a problem. Five minutes of back-and-forth, everything clean, every transcription on time. The first sign of trouble came out of the client's own QA process, once someone ran a longer soak session that pushed the device through far more consecutive utterances than any of our demo scripts had. Partway through, the board rebooted. Not the application, the whole board, mid-conversation. That distinction mattered immediately. An application crash gives you a stack trace and a core dump. A full board reboot with no corresponding application-level exception means the fault happened somewhere our own process couldn't see, most likely in the AI Processing Unit's firmware or the kernel driver managing it, not in our inference code. We had to go looking at a different layer than we're used to debugging at, closer to the accelerator's own logs than to anything our application had logged. This is a known failure signature for NPU-accelerated inference under sustained load, and it's worth knowing the general troubleshooting order before you're staring at it on a real device. Thermal throttling and memory pressure are the first things worth ruling out, a temperature log and an instrumented memory check are cheap and fast, since they're the most common causes of hardware-level instability under load. But in workloads that call an NPU delegate over and over for hours at a time, the more common root cause sits somewhere else: how often the accelerator is being asked to recompile. The usual culprit is the online compilation path. ONNX Runtime's NeuronExecutionProvider and the TFLite Neuron delegate both compile NPU subgraphs at session start by default, every time a session begins, rather than reusing a fixed, precompiled representation already resident on the accelerator. In a five-minute demo that overhead is invisible, a few milliseconds nobody notices. Under continuous use, hour after hour of back-to-back requests, that repeated compilation is the kind of thing that can accumulate into instability in the delegate's state or the firmware handling it, and it's the first hypothesis worth testing when a crash only shows up under sustained load rather than in short sessions. The standard mitigation is to stop recompiling on every session. Moving a fixed model like Whisper-medium onto the [offline, ahead-of-time compilation path](/blog/mediatek-genio-onnx-runtime-neuron) instead, compiling once on a host machine into a fixed accelerator binary and loading that same binary on every boot, removes the repeated-compile step entirely. For any NPU-accelerated model that doesn't change between runs, that's the first thing we'd check before anything else. Extended soak testing, thousands of consecutive inference calls back to back, is how you'd confirm the fix holds before it ships, not a five-minute demo. ## What we'd tell any team doing NPU-accelerated ASR on Genio A few things we'd do again without hesitation, and one thing we'd do earlier: 1. **Ship the vendor-validated model first.** Don't spend your first bring-up cycle chasing the smallest model that meets your accuracy target. Prove the delegate path with something known to work, then optimize model size once that path is stable. 2. **Do not trust demo-length testing.** A clean five-minute session tells you almost nothing about a workload meant to run for hours. Any accelerator-backed inference path needs a soak test long before it needs a benchmark. 3. **Default to offline compilation for any fixed model running continuously.** If the model isn't changing between sessions, there is little reason to pay a compile cost, and apparently some risk, on every single inference call. 4. **Budget review time at the accelerator layer, not just the application layer.** When a symptom looks like "the board rebooted" instead of "the app crashed," you're debugging firmware and driver behavior, and that takes different tools and a different kind of patience than a normal application bug does. ## Frequently Asked Questions ### Can OpenAI's Whisper run on an NPU? Yes. Whisper's encoder and decoder are standard transformer blocks, convolutions, matrix multiplies, layer norms, and attention, and most of those operators map onto general-purpose NPU inference paths like ONNX Runtime's NeuronExecutionProvider or a TFLite Neuron delegate. Operator coverage varies by NPU generation and toolchain version, so validate the specific model size against the specific silicon before committing to it in a product. ### Which Whisper model size works best on an edge NPU like MediaTek Genio's? Start with whichever size the vendor's own NPU delegate has already been validated against, even if a smaller model would fit your latency budget better on paper. We shipped Whisper-medium first because its operator graph matched what MediaTek's tooling had already been exercised on, and planned to qualify lighter variants only after the delegate path itself proved stable. ### Why would NPU-accelerated inference cause a full device reboot instead of an application crash? Because the fault happens below your application, in the accelerator's firmware or driver stack, not in your process. A hard enough fault in the NPU or its supporting co-processor can trigger a watchdog-driven full system reset rather than an isolated, catchable crash. That's why the symptom shows up as a random reboot with no application-level stack trace to chase. ### What is the difference between the online and offline NPU compilation paths on MediaTek Genio? The online path, used by ONNX Runtime's NeuronExecutionProvider or a TFLite Neuron delegate, compiles NPU subgraphs at model load time, every time the session starts. The offline path compiles the model ahead of time on a host machine into a fixed hardware binary that the device loads once and reuses. For a model that doesn't change between runs, the offline path skips repeating that compile step and tends to be the more stable choice under continuous, production load. ### Does MediaTek Genio support fully offline speech-to-text with no cloud API? Yes. Whisper can run entirely on-device on Genio hardware with an NPU, through either ONNX Runtime or TFLite, with no network round trip needed for transcription. The tradeoff is engineering time spent on model size selection, delegate configuration, and validation under sustained load, not a fundamental limitation of the platform. --- *ProventusNova helps hardware startups solve embedded systems problems fast. Need Whisper, or another model, running reliably on Genio's NPU? [See our edge AI deployment service](/services/edgeai-deployment).* --- ## Jetson Thor T3000 and T2000 vs T5000: which module to pick URL: https://proventusnova.com/blog/jetson-thor-t3000-t2000-vs-t5000-t4000/ Published: 2026-07-20 Author: Andres Campos Tags: jetson, thor, t3000, t2000, t5000, blackwell, edge ai, robotics, jetpack 7 NVIDIA's new Jetson Thor T3000 and T2000 compared with T5000 and T4000: FP4 TFLOPS, memory, power, why bandwidth matters, and when Orin still wins. NVIDIA has filled out the Jetson Thor lineup downward: the **T3000** (865 FP4 TFLOPS, 32GB) and **T2000** (400 FP4 TFLOPS, 16GB) bring the Blackwell-based Thor architecture into mainstream robotics and edge-AI price and power territory, joining the existing T5000 flagship and T4000. Hardware ships **Q1 2027**, with T3000 emulation available in **JetPack 7.2.1 from late July 2026**. The headline engineering fact: the T3000 keeps the T5000's full **273GB/s memory bandwidth**, which is why NVIDIA can claim similar multimodal inference performance at roughly half the size and power. ## Key Insights - **T3000 = the LLM value pick on paper**: 865 FP4 TFLOPS but the same 273GB/s bandwidth as the T5000, and token generation is bandwidth-bound, not TFLOPS-bound - **T2000 is the volume play**: 400 FP4 TFLOPS / 16GB for visual AI agents, AMRs, and industrial manipulators, but its full datasheet is not published yet - **Nothing ships until Q1 2027**: plan current designs on Orin or T5000/T4000; start Thor software work in emulation on JetPack 7.2.1 - **Jetson Agent Skills are real repos, not marketing**: `jetson-device-skills` and `jetson-bsp-skills` on GitHub, executable by coding agents for Linux customization, memory optimization, and benchmarking - **The memory-optimization story is an Orin story today**: published results reclaim 4–15GB on Orin-class devices, letting designs drop one memory SKU ## What did NVIDIA announce? Two new modules on the Thor (Blackwell GPU + Arm Neoverse) architecture, positioned below the T5000 and T4000: - **Jetson T3000**: 865 FP4 teraflops, 32GB LPDDR5X at 273GB/s, eight-core Arm Neoverse CPU, 25GbE, at "roughly half the size and power of the T5000." NVIDIA states it achieves similar inference performance to the T5000 for multimodal workloads: LLMs, VLMs, vision-language-action models, and world foundation models. - **Jetson T2000**: 400 FP4 teraflops and 16GB of memory, the entry point for visual AI agents, autonomous mobile robots, and industrial manipulators. Detailed specifications (CPU configuration, bandwidth, power range) have not been published; NVIDIA says the datasheet and design guide come closer to release. With these, the Jetson platform now spans "70 TOPS to 2,000 teraflops", from Orin Nano to T5000, on one software stack. ## Jetson Thor lineup: full spec comparison | | T2000 | T3000 | T4000 | T5000 | |---|---|---|---|---| | **AI compute (FP4)** | 400 TFLOPS | 865 TFLOPS | 1,200 TFLOPS (sparse) | 2,070 TFLOPS (sparse) | | **GPU** | Blackwell | Blackwell | 1,536-core Blackwell, 5th-gen Tensor Cores | 2,560-core Blackwell, 5th-gen Tensor Cores | | **CPU** | not yet published | 8-core Arm Neoverse | 12-core Neoverse-V3AE | 14-core Neoverse-V3AE | | **Memory** | 16GB | 32GB LPDDR5X | 64GB 256-bit LPDDR5X | 128GB 256-bit LPDDR5X | | **Memory bandwidth** | not yet published | 273GB/s | 273GB/s | 273GB/s | | **Power** | not yet published | ~half of T5000 | 40–70W | 40–130W | | **Networking** | not yet published | 25GbE | 3x 25GbE | 4x 25GbE | | **Availability** | Q1 2027 | Q1 2027 | shipping | shipping | Specs as published by NVIDIA at announcement; T2000 rows marked "not yet published" are exactly that: treat any number you see elsewhere for them as speculation. ## Why can the T3000 match the T5000 on LLM inference? This is the part of the announcement worth an engineer's attention. The T3000 has well under half the T5000's FP4 compute, yet NVIDIA claims similar inference performance on multimodal workloads. That claim is credible for a specific reason: **LLM token generation is memory-bandwidth-bound**. Every generated token walks the full weight set, so decode throughput scales with GB/s, not TFLOPS, and NVIDIA lists both modules at the same **273GB/s**. We see the same physics constantly in [LLM work on Jetson Orin](/blog/llm-inference-jetson-orin-llamacpp-ollama): quantization tiers and memory bandwidth dominate tokens-per-second; raw compute sits idle during decode. What the T5000's extra compute (and 128GB) buys you instead: - **Prefill speed**: long-context ingestion is compute-heavy - **Vision encoders and CNN pipelines**: genuinely TFLOPS-bound - **Bigger models**: 128GB holds weight sets the T3000's 32GB simply cannot - **Concurrency**: batching multiple streams shifts the bottleneck back toward compute If your workload is "run a quantized multimodal model and stream tokens," the T3000 is the rational pick. If it's "many camera streams through heavy vision models plus an LLM," the T4000/T5000 tiers still earn their price. ## What are Jetson Agent Skills? This is the software half of the announcement, and it's more concrete than the name suggests. NVIDIA published **agent-executable skill repositories** on GitHub: [`jetson-device-skills`](https://github.com/NVIDIA-AI-IOT/jetson-device-skills) and [`jetson-bsp-skills`](https://github.com/NVIDIA-AI-IOT/jetson-bsp-skills). Each skill is a repeatable recipe (which tools to call, what outputs to produce, how to validate) that a coding agent executes against your Jetson. Three categories shipped: 1. **Linux customization** for custom carrier boards 2. **Memory optimization** across the stack: bootloader memory carveouts, kernel memory reservation, redundant userspace process cleanup 3. **Model benchmarking** and inference optimization The published results are specific: up to **15GB reclaimed** (moving a UBTech/Agile Robots/Connect Tech design from Orin 64GB to Orin 32GB), **4GB reclaimed** (SandStar, Orin NX 16GB → 8GB), and a **30% reduction on Jetson TX2 NX** (NoTraffic). The pattern NVIDIA is selling: reclaim memory in software, drop one memory SKU in hardware, keep the performance tier. Skills-driven agent automation is the same pattern we run internally for BSP work. It's a genuine productivity shift when the skills encode real platform knowledge, and NVIDIA encoding its own platform knowledge this way is the strongest endorsement of the approach yet. Worth noting: **these support the entire Jetson portfolio, Orin included**: you don't need Thor hardware to benefit. On JetPack 7.2, Orin and Thor also converge on one stack: Ubuntu 24.04, kernel 6.8, CUDA 13.0. ## Which module should you pick? - **Shipping a product in the next 12 months** → this announcement changes nothing for you: Orin (Nano/NX/AGX) and the shipping T5000/T4000 are the options. See our [Orin module comparison](/blog/jetson-orin-agx-vs-orin-nx-vs-orin-nano) for that decision. - **Designing for 2027 with multimodal/LLM workloads** → T3000 is the default candidate: T5000-class decode throughput, half the power and size, 32GB. Start software now in T3000 emulation on JetPack 7.2.1. - **Cost-sensitive volume robotics for 2027** → T2000 on paper, but hold hardware decisions until the datasheet lands. 400 FP4 TFLOPS and 16GB is a real tier jump over Orin NX, but power, bandwidth, and I/O are still unpublished. - **Maximum models, maximum streams** → T5000 remains the flagship: 128GB and 2,070 TFLOPS have no substitute in the lineup. - **Existing Orin fleet** → the immediately actionable part is Agent Skills: if a memory-optimization pass reclaims even 4GB, moving down one memory SKU at fleet scale is real money, available today, no Thor required. One BSP-side note for Yocto users: Thor support lives in the `master-l4t-r38.x` line of meta-tegra (JetPack 7.x), separate from the `scarthgap` (JetPack 6 / Orin) branch most production builds use today. Budget for that branch split when planning a mixed Orin + Thor fleet. ## What should you do before Q1 2027? 1. **Prototype in emulation**: T3000 emulation on JetPack 7.2.1 means the software stack, memory budget, and model selection can be validated months before silicon. 2. **Run the memory-optimization skills on your current Orin design**: the savings are useful regardless of whether you ever move to Thor. 3. **Re-run your model-selection math against 273GB/s**: if you sized your module tier by TFLOPS, the bandwidth-bound reality of LLM decode may move you a tier down. If you're planning a Jetson platform decision (Orin now, Thor later, or a migration between them), [our Jetson engineering team](/get-expert-nvidia-jetson-support/) does this for a living: BSP bring-up, camera pipelines, and model deployment under real memory budgets, on a fixed bid. *Sources: [NVIDIA's announcement](https://blogs.nvidia.com/blog/jetson-thor-robotics-edge-ai-agent/), the [Jetson Thor product page](https://www.nvidia.com/en-us/autonomous-machines/embedded-systems/jetson-thor/), and the [JetPack 7.2 memory-efficiency deep dive](https://developer.nvidia.com/blog/deploy-agentic-ready-ai-at-the-edge-with-memory-efficiency-in-nvidia-jetpack-7-2/).* --- ## Chromium hardware video acceleration on MediaTek Genio URL: https://proventusnova.com/blog/mediatek-genio-chromium-video-acceleration/ Published: 2026-07-17 Author: Aaron Angulo Tags: mediatek genio, chromium, video acceleration, kiosk, yocto, wayland, hmi Run Chromium on MediaTek Genio with hardware video decode: which Yocto image ships it, how to verify decode is on the VPU, kiosk mode setup, and WebRTC limits. A browser is the quickest UI stack an embedded product can adopt: your HMI is a web app, your kiosk is a URL, and your frontend team already knows the tools. On MediaTek Genio that browser is Chromium, MediaTek ships an image that includes it, and the difference between a smooth product and a hot, throttling one usually comes down to one question: is video actually decoding in hardware? Here is how the pieces fit on Genio and how to verify each one. ## Key Insights - Chromium ships in MediaTek's `rity-browser-image`, the top of the RITY image hierarchy, with proprietary codec support included. - Genio's hardware decoders (H.264/H.265) are exposed through V4L2 by the mtk-vcodec drivers; Chromium reaches them through its V4L2 video decode path on Wayland/Ozone. - Never assume acceleration: `chrome://gpu` and `chrome://media-internals` tell you what your specific build is doing, and CPU load while playing video is the honest cross-check. - Treat browser WebRTC encode as software: for camera-out streaming, a native GStreamer pipeline with the hardware encoder is the dependable path. ## Which image gives you Chromium, and what does it cost? MediaTek's IoT Yocto (RITY) images stack in a strict hierarchy, and Chromium arrives at the top: | Image | Approx. size | What it adds | |---|---|---| | `rity-bringup-image` | ~150 MB | Core tools, SSH, debug | | `rity-bsp-image` | ~500 MB | Weston, GStreamer, camera, audio | | `rity-demo-image` | ~1 GB | AI/ML stack (TFLite, ONNX, NeuroPilot) | | `rity-browser-image` | ~2 GB | Chromium with proprietary codecs | That 2 GB figure is the honest price of a browser UI, and it buys you a Chromium that MediaTek has already integrated against the platform's Wayland compositor and codec stack, so you are not patching a browser yourself. For products, you have two sane starting points: ship the browser image and strip what you do not need, or derive from `rity-bsp-image` and pull the Chromium recipe in, owning the integration choices explicitly. Prototype on the first; decide deliberately before the second. Chromium runs on Weston through the Wayland Ozone backend on these images. If you are composing your own launch command, `--ozone-platform=wayland` is the flag that puts Chromium on the compositor properly instead of through legacy paths. ## How does hardware video decode work under Chromium here? Two layers have to cooperate. The kernel side is solid ground: Genio SoCs expose their hardware H.264 and H.265 decoders as V4L2 devices through the mtk-vcodec drivers, the same decoders our GStreamer pipelines use in [Genio video work](/blog/mediatek-genio-gstreamer-real-time-video). The browser side is Chromium's V4L2 video decode support, which turns a `