# ProventusNova, Full Site Content for LLM Indexing Source: https://proventusnova.com Generated: 2026-09-28 --- # 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 ## Camera Lifecycle in Docker: SIGINT vs SIGTERM URL: https://proventusnova.com/blog/docker-camera-lifecycle-sigint-ros2/ Published: 2026-09-28 Author: Andres Campos Tags: ros2, docker, mediatek genio, camera, embedded linux Our ROS 2 camera container would not restart cleanly, and the camera got stuck. The cause was the stop signal. Why SIGTERM broke it and SIGINT fixed it. A camera that will not restart cleanly is a small bug with a large blast radius, because everything downstream depends on it. Ours got stuck in a way that looked like flaky hardware and was actually a stop signal. This is one of those fixes that is one line and saves a day, so it is worth knowing before you meet it. ## Key Insights - Our ROS 2 camera container took ten seconds to stop and left the camera in a bad state - The launcher ran as PID 1 and ignored SIGTERM, so every stop rode out Docker's grace period and ended in a hard kill - The hard kill left the camera mid-operation, it self-reset, and the next open collided with that reset and never recovered - Switching the container's stop signal to SIGINT let the process close the camera cleanly in about a second, and restarts became reliable - A frame watchdog was added as a backstop to recover from any remaining stuck state ## The symptom Stopping and restarting the camera container was unreliable. A stop took a full ten seconds, and afterward the camera would often fail to open, throwing the same error over and over and never recovering until a deeper reset. It alternated between working and failing in a way that felt like a bad cable or a marginal device, which is the kind of symptom that sends teams down the hardware path for days. ## The cause: PID 1 and the stop signal Inside the container, the ROS 2 launcher ran as process ID 1. When Docker stops a container it sends the SIGTERM signal and waits a grace period before hard-killing with SIGKILL. The launcher, as PID 1, did not act on SIGTERM, so every stop waited out the entire grace period and then died by force. That is why the stop took ten seconds, and it is why the camera was left in a bad state: a forced kill does not give the process a chance to close the device cleanly. Then the two halves collided. The camera, abandoned mid-operation, would reset itself a couple of seconds later. Meanwhile the next start of the container was already trying to open the camera, and that open landed right on top of the device's self-reset. The result was a camera that never came back until something reset it harder. ## The fix The camera closed cleanly when the process received SIGINT, the interrupt signal, rather than SIGTERM. Telling the container to use SIGINT as its stop signal changed the shutdown from a ten-second forced kill into a clean close in about a second, with no device reset and reliable restarts. We also added a frame watchdog as a backstop: if the camera goes quiet unexpectedly, it recovers the device rather than sitting stuck. That is the whole fix, and it is the kind of thing that is obvious once seen and invisible until then. The general lesson is broader than cameras: any container that owns a hardware device needs its shutdown to actually reach the process, so the device gets released cleanly. When PID 1 ignores the signal Docker sends, you get hard kills and stuck devices, and the cure is to send a signal the process respects. We keep the specific launch and compose details for the projects we do, because they vary with your stack. The reusable insight, the one worth publishing, is to check the stop signal the moment a device-owning container restarts badly. If your containerized robotics or vision stack has services that will not cycle cleanly, on the MediaTek Genio or on NVIDIA Jetson, [tell us what you are building](/contact) and we will make the lifecycle reliable. For the whole robot, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## NPU vs CPU on the MediaTek Genio: the Thermal Reality URL: https://proventusnova.com/blog/genio-npu-vs-cpu-thermal-edge-ai/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, npu, edge ai, thermal, computer vision On a fanless MediaTek Genio, running a vision model on the CPU pushed our robot to 84 C and throttled. On the NPU it held 58 C. Why edge AI is a heat problem. Here is a lesson that surprises product teams: on a small fanless edge board, running AI is a heat problem before it is a speed problem. We learned it the direct way, by watching our robot's MediaTek Genio climb past its throttle temperature within a minute of turning the camera on. This is what that looks like and how to think about it. ## Key Insights - Running our vision model on the MediaTek Genio 520 CPU drove the chip to 78 to 84 C and past its passive throttle point in about a minute - The same model on the NPU held 58 to 60 C on the same board, cool enough to run fanless - Capping CPU usage does not solve it: if inference is already too slow, throttling only makes it slower - Some embedded kernels do not expose a working CPU quota control at all, so that lever may not even exist - On a fanless edge device, plan the thermal budget of your AI workload before integration, not after ## What overheating looks like We ran our object detector on the Genio's CPU cores first, because that is the path of least resistance. Within about sixty seconds of real camera frames, the board was at 78 to 84 C. The passive design wants to throttle well below that, so the chip started slowing itself down to protect itself. Every core was pinned at maximum clock the whole time, drawing six to nearly eight cores' worth of load for a detector running at roughly two frames a second. A throttling chip is the worst of both worlds. You are already too slow, and now the hardware makes you slower to keep from cooking. On a robot with no fan, in a warm room, this is not a demo you can leave running, let alone a product you can ship. ## Why you cannot throttle your way out The instinct is to cap the CPU so it runs cooler. It does not work here, for two reasons. First, the model is already slower than the frame rate we want, so limiting cores just trades heat for even lower frame rate, and you still may not get under the thermal limit. Second, and this is the embedded twist, the kernel on this board did not expose a working CPU quota control at all, so the usual container-level cap was simply unavailable. The lever you reach for may not be there. The workload is on the wrong processor. That is the actual problem, and cooling tricks only paper over it. ## The real answer Moving the detection onto the Genio's neural accelerator dropped the board to 58 to 60 C doing the same job, cool enough to run without a fan while the camera and the rest of the stack ran alongside it. That is the headline: the NPU is not only faster, it is what makes on-device vision thermally possible on a passive board. We cover the speed side in [YOLOv8 on the Genio NPU](/blog/yolov8-mediatek-genio-npu). If for some reason a workload has to stay on the CPU, then the honest options are a smaller, cheaper model and real cooling, a heatsink or a fan sized to the sustained load. Both are legitimate, and both are decisions you want to make early. ## The order of operations that saves months The expensive mistake is discovering your thermal problem at integration time. The order that avoids it is: choose the board for its accelerator, confirm your model runs on that accelerator within your frame-rate and temperature budget, then design the enclosure and cooling around the load you measured. Teams that skip the measurement and assume the CPU will cope tend to find out in the enclosure, which is the most expensive place to find out. Sizing an AI workload to a fanless board, on the MediaTek Genio or on NVIDIA Jetson, is exactly the kind of thing we characterize quickly because we have done it before. [Tell us what you are building](/contact) and we will tell you whether your model fits your board before you commit to the enclosure. For the robot this came from, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## Getting an LD06 Lidar onto the MediaTek Genio URL: https://proventusnova.com/blog/ld06-lidar-mediatek-genio-kernel-module/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, lidar, embedded linux, yocto, robotics The LD06 lidar got no device node on our MediaTek Genio: the vendor kernel had no driver for its USB-serial chip. Building the module, and the UART fallback. A lidar that shows up on the USB bus but never becomes a serial port is a classic embedded dead end. Our LD06 lidar did exactly that on the MediaTek Genio's vendor kernel, and getting it working meant either adding a driver to a kernel we could not easily rebuild, or bypassing USB entirely. Here is the situation and both routes out. ## Key Insights - The LD06 uses a CH340-class USB-serial chip that needs the ch341 driver, which the vendor kernel did not include - Without the driver the lidar enumerated on USB but never got a serial device node, so the software found nothing to open - The fix was to build the driver as an out-of-tree kernel module against the vendor kernel, which has a real gotcha around build options - The alternative is to wire the lidar to a hardware UART on the carrier and skip USB, at the cost of a possible level shifter and sourcing 5 volts - On a new board, assume some peripheral you need will be missing its driver, and plan time for it ## The dead end The LD06 is a cheap, popular 360-degree lidar, and like a lot of inexpensive serial peripherals it talks over USB through a CH340-class serial chip. On a normal Linux system the ch341 driver claims that chip and hands you a serial port. The MediaTek Genio's vendor kernel, trimmed for the platform, did not include that driver. So the lidar enumerated on the USB bus, the kernel saw a device, and no serial port ever appeared. Software that tried to open the lidar's port found nothing there. This is one of the most common surprises on a new embedded board. The hardware is fine, the sensor is fine, and a missing driver makes the whole thing invisible to your application. ## Route one: build the driver The direct fix is to build the missing driver as an out-of-tree kernel module against the exact kernel on the board, then install it so it loads automatically. That sounds routine and has two real snags. First, you need the vendor's kernel headers and configuration to build a module that will actually load, and vendors do not always hand those over without a reason, so asking with a concrete need helps. Second, the module has to match the kernel's build options, and there is a specific trap around how the kernel handles module type information that will stop an otherwise-correct module from loading if you miss it. Once those line up, the driver loads at boot and the lidar becomes a normal serial port. We keep the specifics of that build, the exact configuration and the trap, for the work we do, because it is precisely the kind of thing that turns a lost week into an afternoon when someone has hit it before. ## Route two: skip USB entirely The other route is to not use the USB-serial chip at all and wire the lidar directly to a hardware UART on the carrier board. That removes the driver problem completely, because a hardware UART is a first-class serial port with no chip in between. The cost is on the electrical side. The board's UART may run at a different logic voltage than the sensor expects, in which case you need a level shifter so you do not feed the wrong voltage into a pin, and you have to find a 5-volt source on the carrier to power the sensor, since not every header provides one. It is more wiring and a meter check, and it sidesteps the kernel entirely. Which route is right depends on whether you would rather own a kernel module or a bit of wiring, and on what your carrier board exposes. ## The takeaway The real lesson is the one at the top: on a new board, budget for the fact that some peripheral you need will arrive with no working driver, and the failure will look like dead hardware rather than a missing piece of software. Recognizing that pattern fast is most of the fix. Bringing up sensors and peripherals on a fresh board, MediaTek Genio or NVIDIA Jetson, is a large part of what we do, and it is where the schedule risk on a new platform lives. [Tell us what you are building](/contact) and we will get your sensors talking. For the whole robot, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## Driving a Mecanum Robot from ROS 2 URL: https://proventusnova.com/blog/mecanum-robot-ros2-wheel-wiring/ Published: 2026-09-28 Author: Andres Campos Tags: ros2, robotics, mecanum, mediatek genio, motor control Our mecanum robot strafed sideways when told to go forward. The cause was a diagonal wheel pair wired backwards. How mecanum drive works and how to catch it. We told our robot to drive forward and it slid sideways instead. Nothing was broken, in the sense that every motor turned and the code was correct. The wheels were simply not the wheels the software thought they were. Mecanum drives make this failure easy to hit, so here is how the motion works and how to catch the wiring when it goes wrong. ## Key Insights - Mecanum wheels use angled rollers so the robot can drive forward, strafe sideways, or spin in place - The drive code turns a desired forward, sideways, and rotational velocity into four individual wheel speeds, and that math depends on each wheel being in a known position - If a diagonal wheel pair is wired backwards, a forward command resolves into a sideways one, which is exactly what we saw - Diagnose by commanding one wheel at a time and mapping which physical wheel actually moves, then compare to what the code assumes - The symptom tells you the fault: forward becoming a strafe points to a diagonal pair inverted ## How mecanum drive works A mecanum wheel is a normal wheel with rollers set at an angle around its rim, so instead of pushing straight ahead it pushes at 45 degrees to its axle. Put four of them at the corners of a robot, each oriented correctly, and you can produce net motion in any direction by mixing their speeds. Spin them all forward and the robot goes forward. Spin one diagonal pair one way and the other pair the other way, and the robot strafes sideways without turning. Spin the left and right sides opposite each other and it rotates in place. The drive code does the inverse of this. It takes the motion you want, a forward velocity, a sideways velocity, and a rotation, and solves for how fast and which direction each of the four wheels must turn. That solution is only correct if each wheel is where the code thinks it is and turns the direction the code expects. ## Why "forward" became "sideways" Our robot had every motor working and still strafed on a forward command. That is the fingerprint of a specific fault: a diagonal pair of wheels wired backwards relative to what the code assumes. The forward and sideways cases in the mecanum math differ by exactly which diagonal is doing what, so swapping a diagonal pair rotates your commands ninety degrees. Forward comes out as a strafe. It looks like a deep control bug and it is a connector in the wrong place. In our case the cause was mundane: the robot had been reassembled after painting, and the motor connectors came back one position off. Every wheel worked, none was where the driver expected. ## How to catch it The diagnosis is simple once you suspect wiring. Command one wheel at a time and watch which physical wheel moves and in which direction. Build the real map of connector to wheel to spin direction, then compare it to the mapping the drive code assumes. The corrections are either at the connectors or in the configuration, whichever you would rather own. A useful shortcut is the symptom rule. Forward turning into a strafe means a diagonal pair is inverted. A robot that spins when told to go straight means one side is reversed. The way the motion is wrong tells you where to look before you touch anything. We keep the exact drive configuration and the wheel-mapping conventions for the projects we do, because those are specific to your chassis and motor controller. The reusable part, the one worth publishing, is that mecanum motion failures usually live in the wiring, not the math, and the symptom points straight at the fault. If you are integrating a drive base with ROS 2, mecanum or otherwise, on the MediaTek Genio or on NVIDIA Jetson, [tell us what you are building](/contact) and we will get it moving in the right direction. For the whole robot, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## MediaTek Genio vs Jetson for a ROS 2 Robot URL: https://proventusnova.com/blog/mediatek-genio-vs-jetson-ros-robot/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, nvidia jetson, ros2, robotics, edge ai We built a ROS 2 robot on the MediaTek Genio. Here is how the Genio and NVIDIA Jetson actually compare for robotics, from first-hand experience on both. We build robots on both the MediaTek Genio and NVIDIA Jetson, and our Embedded World robot ran on the Genio. So when a founder asks which one to choose for a ROS 2 robot, the answer comes from having shipped on both, not from a spec sheet. Here is the honest comparison, aimed specifically at robotics. For the broader edge-AI platform view, see our [MediaTek Genio versus NVIDIA Jetson Orin comparison](/blog/mediatek-genio-vs-nvidia-jetson). ## Key Insights - Both platforms run ROS 2 well and both have a neural accelerator, so neither is disqualified for robotics - Jetson brings the more mature robotics and AI ecosystem, with more examples, drivers, and community knowledge to lean on - The Genio can win on power, thermals, cost, and supply, which matter a great deal for a battery-powered or price-sensitive product - The two accelerators take different routes to inference, so a model that runs on one does not automatically run on the other - The decision should be validated with your real vision and speech models, not made on datasheets alone ## Both are real robotics platforms Start with the thing that is not in dispute: ROS 2 runs well on both, and both carry a dedicated AI accelerator so your vision and speech can run on-device rather than on the CPU. We run a complete ROS 2 Humble robot on the Genio, with camera, lidar, navigation, voice, and a mecanum drive, all at once. So this is not a case of one platform being for robots and the other not. It is a case of trade-offs. ## Where Jetson leads Jetson's advantage is maturity. It has been the default for edge robotics and vision for years, and that shows up as a larger body of examples, more ready driver support for common sensors, and a deeper community you can search when you hit a wall. For a team that wants to move fast on well-trodden ground, that ecosystem is worth real money, because the time you do not spend solving already-solved problems is time you spend on your product. ## Where the Genio is compelling The Genio's case is on the platform factors that a datasheet comparison of raw performance tends to undersell. Power efficiency and thermals matter enormously on a robot, where every watt is battery life and every degree is a fan you would rather not add. Cost matters when you are building more than a handful of units. And supply, the plain question of whether you can buy the part in the volume and timeframe you need, has decided more product platforms in the last few years than benchmarks have. On those axes the Genio can be the better answer, which is exactly why we built our demo robot on it. ## The accelerator is the real question The place teams get surprised is the AI accelerator. The Genio and Jetson reach on-device inference by different routes, which means a model tuned for one does not simply drop onto the other. For a robot, the practical question is not which accelerator is faster in the abstract, it is whether your specific detector and your specific speech model convert cleanly and hit your frame rate and your thermal budget on the accelerator you choose. We have written about what that took on the Genio in [YOLOv8 on the Genio NPU](/blog/yolov8-mediatek-genio-npu). The lesson that transfers is to answer that question with your real models early, because it is the one most likely to move your decision. ## How to actually decide Weigh ecosystem maturity, where Jetson leads, against power, thermals, cost, and supply, where the Genio often does, and then validate the choice by running your real workload on the real accelerator before you commit. That last step is the one teams skip and regret. We do it for clients on both platforms, so the platform decision is made on measured fact rather than a hopeful reading of a datasheet. If you are choosing a compute platform for a robotics or vision product and want that decision grounded in your actual models, [tell us what you are building](/contact) and we will help you choose with evidence. For the robot behind this, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## The WiFi Bug That Almost Killed Our Booth Demo URL: https://proventusnova.com/blog/mt76-wifi-txpower-reporting-bug/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, wifi, mt76, embedded linux, debugging Our robot's WiFi hotspot looked capped at 3 dBm. It was a cosmetic mt76 driver reporting bug: the radio runs at full power. How to verify before you panic. Two days before a trade show, our robot's built-in WiFi hotspot looked broken. The tools reported it was transmitting at 3 dBm, a level so low that a tablet a few feet away should barely see it. We nearly bought hardware to work around it. It turned out the radio was fine and the number was a lie. Here is the story, because you will hit this exact one. ## Key Insights - The tools reported the robot's WiFi transmitting at 3 dBm, which looks like a crippling power cap - It was a cosmetic reporting bug in the mt76 driver family, not a real limit; the radio transmitted at normal power the whole time - We confirmed the real transmit power through the driver's own debug interface: roughly 13 to 16 dBm while the tool said 3 - The lesson is to verify actual transmit power at the driver level before you conclude you have a hardware or firmware cap - Believing the wrong number nearly cost us money and time on a fix for a problem that did not exist ## The scare The robot runs its own WiFi hotspot so a tablet can drive it and show its camera at a booth. During setup, every tool agreed the access point was transmitting at 3 dBm. That is roughly two milliwatts, and at a busy exhibition with hundreds of competing radios, it would mean the tablet loses the robot the moment you step back. Worse, trying to raise the power with the usual command did nothing: the setting refused to stick on readback, which is exactly what a firmware-locked regulatory cap looks like. So the working theory became "this radio is region-locked to almost no power, we need a separate WiFi dongle or a travel router." That is a real cost and a real point of failure to carry to a show. Before committing to it, we checked whether the number was even true. ## The number was lying The MediaTek WiFi driver family, mt76, has a known quirk: it does not always update the transmit-power value that the standard tools read back. So the tool reports whatever stale or default number it has, often a bogus low one, while the radio is actually transmitting at the correct regulatory power. The "won't stick" behaviour when you try to set it is the same bug, not a lock. We did not take that on faith either. The driver exposes the real per-rate transmit-power table through its own debug interface, and that is the number that reflects what the hardware is actually doing. It showed the radio transmitting at roughly 13 to 16 dBm across the relevant rates, while the standard tool insisted on 3. The radio was at normal power the entire time. The hotspot worked at booth range once we stopped chasing a cap that was not there. ## The lesson When a radio reports a suspiciously low or oddly unchangeable transmit power, do not believe it and do not buy hardware to fix it until you have checked the real value at the driver level. The reported number and the transmitted number are not always the same thing, and on this driver family they routinely are not. Verifying the truth takes minutes; acting on the lie costs days and sometimes a hardware order. This is a small story with a large moral for anyone shipping on embedded WiFi: your telemetry can be wrong, and a wrong number that happens to match a scary explanation is the most dangerous kind. The habit that saves you is confirming the physical reality before you design around the readout. If you are bringing up WiFi, or any radio, on a MediaTek Genio or another embedded platform and you want someone who has already met the driver's quirks, that is the kind of thing we sort out quickly. [Tell us what you are building](/contact). For the robot this came from, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## Building a Tablet Cockpit for a Robot URL: https://proventusnova.com/blog/robot-tablet-dashboard-pwa/ Published: 2026-09-28 Author: Andres Campos Tags: robotics, mediatek genio, dashboard, pwa, debugging Our robot's tablet dashboard froze on one device: camera live, everything else dead. A single unsupported browser call had halted the page. The field-UI lesson. The dashboard that drives our robot has to run on whatever tablet is on hand, which is a harder requirement than it sounds. Two days before a show, it froze on the demo tablet in the most confusing way possible: the live camera worked and nothing else did. The cause is a great lesson in building interfaces for devices you do not control. ## Key Insights - The robot serves its own installable web dashboard: a first-person camera view, a joystick, a live lidar radar, and voice, with no cloud - On one tablet the page froze with the camera live but every button and readout dead - The cause was a single unsupported browser call early in startup that threw and halted everything scheduled after it - Parts set up before the failing call kept working, which is why the symptom looked so strange - A field-ready UI guards modern browser features, wraps its draw loops, and limits live connections so one failure cannot take the page down ## What the cockpit is We built the robot's operator interface as a web app the robot serves itself. Any tablet or phone on the robot's network loads it, and it can be installed to the home screen so it feels like a native app. It shows a first-person camera view with a joystick for driving, a live radar scope drawn from the lidar so the operator can see obstacles around the robot, and voice controls. Serving it from the device means it needs no app store and no internet, only the robot. ## The freeze At setup, the dashboard came up on the demo tablet with a working camera feed and nothing else. The drive joystick did nothing, the telemetry never updated, the command buttons were dead. It was not the network, because a laptop on the same connection showed everything working. It was not the camera streams competing for bandwidth, because the heaviest thing on the page, the video, was the part that worked. The way we found it was to log what the tablet actually requested. It asked for the page and the camera feed, and it never asked for telemetry and never sent a single button press. That pointed straight at the cause: the page's startup script had thrown an error partway through, so everything it tried to set up after that point simply never happened. ## The cause The lidar radar drew its sweep using a modern browser drawing call that the tablet's older browser did not support. The first time the page tried to draw the radar, that call threw. Because it threw during the page's initialization, everything scheduled after it, the telemetry stream, the live status, the buttons, the joystick, never got wired up. Everything before it, the camera feeds and a health check, had already been set up and kept working. That is why the symptom was so lopsided: a live camera on top of a completely dead control panel. ## The lesson: build for devices you do not control The fix was to stop assuming the browser. Guard the modern drawing call before using it so an unsupported feature is skipped rather than fatal, wrap the drawing loop so a failure inside it cannot halt the rest of the page, and be economical with live connections, because a thin wireless link and an older browser both have hard limits on how many simultaneous streams they will hold. With those in place the dashboard degrades gracefully instead of dying, and it works on whatever tablet is at the booth. The general principle is the one in the heading. A dashboard that only runs on your development machine is not done. A field UI has to survive an unknown, possibly old browser on an unknown network, and that means defensive initialization so that no single unsupported feature can take the whole interface down. We keep the implementation details for the projects we do, because they depend on your stack. The reusable insight is to treat the operator's device as hostile terrain and build the UI to tolerate it. If you are building operator interfaces for robots or edge devices that have to run anywhere, [tell us what you are building](/contact). For the robot this drives, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## ROS 2 Across Containers: the Fast DDS Gotchas URL: https://proventusnova.com/blog/ros2-fast-dds-containers-genio/ Published: 2026-09-28 Author: Andres Campos Tags: ros2, fast dds, docker, mediatek genio, robotics Our containerized ROS 2 robot froze in ways that made no sense. The cause was DDS transport across container boundaries. The two settings that fixed it. The most confusing bugs in a containerized ROS 2 system are the ones where everything looks connected and nothing works. Our robot had two of them, and both came down to how Fast DDS moves messages across container boundaries. Here is what happened and the two settings that fixed it, so you can recognize the shape before it costs you a day. ## Key Insights - ROS 2's Fast DDS prefers shared memory between nodes, and separate containers do not share that memory namespace by default - The fix is a shared IPC namespace for the containers that need to talk, which restores the fast shared-memory transport - DDS also binds to network interfaces by default, and on one board that is a liability, because a changing address staled our locators and wedged all topic traffic - Restricting discovery and transport to loopback fixed it, because every node was on the same board anyway - Commands appearing to arrive while nothing moves is a classic sign that one transport is broken while another still works ## Gotcha one: shared memory needs a shared namespace Running each ROS 2 node in its own container is the right call for a product. It also quietly breaks an assumption Fast DDS makes. For speed, DDS tries to pass messages between local nodes through shared memory rather than the network stack. Two nodes in the same container share that memory naturally. Two nodes in separate containers do not, unless you arrange for them to share the relevant namespace. Miss it, and you get a maddening state: the nodes discover each other, the topics show up, and yet data does not move the way it should. The fix is to give the containers that need the fast path a shared IPC namespace so the shared-memory transport works across the boundary. Knowing this exists is most of the battle; it is invisible once set and baffling until you know to look. ## Gotcha two: DDS binds to interfaces that move The second one bit us at the worst time. Our robot brings up its own WiFi access point for a booth tablet, which changes the address on its wireless interface. Fast DDS, by default, binds to the network interfaces it finds. When the access point came up and the interface address changed, the DDS locators went stale, and all topic traffic between the containers wedged. The tell was that the robot's command buttons still seemed to work, because a separate direct loopback path delivered them, while the camera and telemetry, which ride the DDS topics, froze solid. Commands arriving while nothing happens is the signature of one transport alive and another dead. The fix, since every node runs on the same board, was to restrict ROS 2 discovery and transport to loopback. Once DDS stopped caring about the wireless interface entirely, the changing address could not stale anything, and the traffic flowed even while the access point was up. ## The general lesson Both gotchas share a root: the DDS defaults are tuned for nodes spread across a network, and a single-board robot with containerized nodes is a different world. On one board, you want the fast local path between containers and you want DDS to ignore the network interfaces whose addresses can shift under it. Set those two things and a whole category of intermittent, transport-level failures disappears. We have kept the exact configuration out of this post, because the valuable part for your team is knowing these two failure modes exist and what causes them, not a snippet that changes with your topology. Getting the transport right for your specific system, one board or many, is the work we do. If your ROS 2 system stalls in ways that do not make sense, on the MediaTek Genio or on NVIDIA Jetson, that is exactly the kind of thing we untangle quickly. [Tell us what you are building](/contact). For the wider stack this came from, see [running ROS 2 on the MediaTek Genio](/blog/ros2-mediatek-genio) and [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## Running ROS 2 on the MediaTek Genio URL: https://proventusnova.com/blog/ros2-mediatek-genio/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, ros2, robotics, yocto, docker, edge ai ROS 2 runs well on the MediaTek Genio, and containers on a custom Yocto image make it reproducible. What the stack looks like and the gotchas to expect. People are often surprised that ROS 2 runs cleanly on a MediaTek Genio. It does. Our ROSOrin robot runs a full ROS 2 Humble stack on a Genio 520: speech, vision, lidar, navigation, and a mecanum drive, all at once. This post is the honest lay of the land, what the stack looks like and where the friction actually is, so you can plan around it. ## Key Insights - ROS 2 Humble runs well on the MediaTek Genio 520, driving a complete robot with vision, lidar, and motion together - We run each ROS 2 node as a Docker container on a custom Yocto image, which is what makes the build reproducible across boards and reflashes - ROS 2 itself is rarely the hard part; the friction is peripherals, device access into containers, and cross-container communication - Getting ROS 2 nodes in separate containers to talk efficiently needs deliberate configuration, and skipping it produces confusing failures - The Genio is a credible robotics compute platform, and the AI accelerator is a real reason to consider it over a CPU-only board ## Yes, and here is the shape of it ROS 2 Humble on the Genio behaves like ROS 2 anywhere: the nodes, topics, and tooling are the same. What differs on an embedded target is how you package and ship it. We run each part of the robot, the camera driver, the object detector, the speech pipeline, the brain that decides what to do, the motor server, and the dashboard, as its own container. Those containers are baked into a **custom Yocto image**. That choice is what turns a demo into something you can reproduce. A Yocto image rebuilds identically, boots the same way every time, and can be flashed onto the next board without a day of manual setup. For a product, that reproducibility is not a nicety, it is the whole game. ## Where the friction actually is The surprising thing about bringing ROS 2 up on a new board is how little of the trouble is ROS 2. In our build the time went to three things, and they will be the same three on your board. **Peripherals and their drivers.** Cameras, lidar, microphones, and motor controllers each have to enumerate reliably and survive reboots. On a new board that is where the days go, and the failures are rarely where you first look. We have separate write-ups on the ones that cost us the most, because they are the parts nobody documents. **Getting devices into containers.** Running nodes in containers is the right call, and it means every physical device has to be presented into the right container correctly and stay stable across restarts. This is a solvable, well-understood problem, and it is easy to get subtly wrong in a way that only shows up under load. **Cross-container communication.** This is the one that bites hardest. ROS 2 nodes in separate containers on the same board have to be able to talk to each other efficiently, and the defaults are not always what you want when everything is local. Set it up correctly and it is invisible; set it up naively and you get intermittent, baffling stalls where messages seem to arrive but nothing happens. It has a clean answer, and it is worth knowing before you hit it rather than after. ## What we are not putting in this post You will notice this is not a step-by-step. That is deliberate. The commands to stand up ROS 2 on a Genio are not the valuable part, and they change with every image and SDK version. The valuable part is knowing which choices matter, where the schedule risk is, and how to avoid spending a month learning the same three lessons. That is the work we do, and it is the part worth a conversation. ## If you are choosing a robotics platform The Genio is a serious option for a robotics or vision product, and the reason to reach for it over a CPU-only board is the neural accelerator, which is where the AI has to run to be fast and cool enough to ship. We cover that in [YOLOv8 object detection on the Genio NPU](/blog/yolov8-mediatek-genio-npu) and [Whisper speech-to-text on the Genio NPU](/blog/whisper-mediatek-genio-npu). If you are standing up ROS 2 on a MediaTek Genio or on NVIDIA Jetson and want to skip the month of integration lessons, that is exactly what we do on a fixed timeline. [Tell us what you are building](/contact) and we will tell you the fastest path to a working result. For the robot this all came from, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## When a USB Camera Browns Out Your Edge Board URL: https://proventusnova.com/blog/usb-camera-brownout-edge-board/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, usb, embedded linux, debugging, camera A depth camera kept crashing our MediaTek Genio robot. It was not the camera or the USB hub, it was current. How we found it, and the rule that saved the build. One of the most expensive kinds of bug in embedded work looks like a flaky cable and is actually physics. On our MediaTek Genio robot, a depth camera would enumerate, stream for a moment, and then take the whole board down. It looked random. It was not. It was current, and the path to proving that is worth telling because you will meet this bug on your own build. ## Key Insights - A USB camera that works on one port and dies on another is almost always a current problem, not a data or driver problem - USB power descriptors routinely lie; the kernel budgets on those numbers, so a peaky device gets no headroom and browns out - The signature was a chain reaction: the camera's illuminator turned on, a neighbouring device dropped, then the camera itself disconnected - No over-current message ever appeared, which is exactly why teams chase the wrong layer for days - The fix is a power layout that supplies the peak current cleanly, and the right layout is specific to the board ## The symptom The camera behaved differently on the two USB ports of the board, and neither was reliable in the obvious way. On one port it streamed. On the other it delivered a single frame, reset a few seconds later, then failed to open ever again with the same error repeating forever. Move it, and the pattern moved with it. Add a hub, and new variations appeared. It had every appearance of a bad cable or a driver bug, and it was neither. ## The tell The breakthrough was watching the order of events, not the error messages. The sequence was always the same. The camera's driver opened the device, which turned on its infrared projector. About a second later, the other high-speed device sharing that rail dropped off the bus. About a second after that, the camera itself disconnected and re-enumerated. Then nothing. That is not a data fault, that is a power sag. The projector's inrush pulled the rail down far enough to knock out its neighbour first, then itself. And there was never an over-current message, because nothing on the board thought it was over-current: the devices had declared tiny power needs in their descriptors, one even claiming to be self-powered while drawing from the bus, so the kernel had budgeted almost nothing and had no reason to complain. That last part is the trap. The system tells you everything is fine right up until the board reboots. ## The lesson, and the rule The fix in principle is simple: supply the peak current through a path that can actually deliver it, and keep the hungry, peaky device away from shared power with anything sensitive. In our case that meant a specific layout using an externally powered hub feeding its own 5 volts, with the camera on the strong rail and the other peripherals arranged so nothing shared a rail with the projector's inrush. Once the power path could source the peak, the whole class of failures disappeared, and the board ran for reboots on end with everything attached. The exact layout that works is specific to your board's ports, rails, and controllers, and finding it is a matter of characterizing where the current can actually flow. We keep that part, the specific working layout and how we arrive at it, for the projects we do, because it is the engineering, not the anecdote. ## Why this costs teams weeks This bug is expensive because every instinct points the wrong way. The error text implicates the camera or its driver. The intermittency implicates a cable. The absence of an over-current warning implicates anything but power. Teams rewrite drivers and swap hardware for days before they think to watch the timing and suspect the rail. If you are integrating cameras, illuminators, lidar, or any peaky USB peripheral onto an edge board, on the MediaTek Genio or on NVIDIA Jetson, the power layout is worth getting right before it costs you a sprint. That characterization is exactly the kind of thing we do quickly because we have seen it before. [Tell us what you are building](/contact) and we will save you the week. For the robot this came from, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## Voice-to-Wheels: Turning Speech into ROS 2 Motion URL: https://proventusnova.com/blog/voice-control-ros2-robot/ Published: 2026-09-28 Author: Andres Campos Tags: ros2, voice control, robotics, mediatek genio, edge ai Routing every spoken command through an LLM made our robot slow and wrong. A rule layer in front of the model fixed both. How we structured voice control. A robot you control by voice lives and dies on two numbers: how long from speaking to moving, and how often it does the wrong thing. Our first version was slow and occasionally wrong, and the fix was not a better model, it was a better structure. Here is how we ended up building voice-to-wheels. ## Key Insights - The voice chain is speech-to-text, then intent, then a ROS 2 motion command, and on our robot all of it runs on-device - Routing every command through a language model was both slower and occasionally wrong, for example mapping "move forward" to a sideways strafe - A deterministic rule layer for the known command vocabulary runs instantly and reliably, with the model kept only as a fallback for open phrasing - Safety commands like stop need their own fast path that bypasses transcription entirely - The right structure matters more than the model choice for a responsive, trustworthy voice interface ## The naive version, and why it disappoints The obvious design is a pipeline: transcribe the speech, hand the text to a small on-device language model to figure out what the person meant, and send the resulting action to the robot. It works, and it disappoints in two specific ways. It is slow, because the language step adds one to three seconds on top of transcription for even a trivial command. And it is occasionally wrong in ways that feel dumb to a user: our model would take "move forward" and produce a sideways strafe, or take "come here" and produce nothing. When a person says a plain command and the robot does something odd, the whole thing feels broken, no matter how clever the model is elsewhere. ## The fix: a rule layer in front of the model The insight is that the commands that matter most for a demo or a product are a small, known vocabulary: stop, forward, back, turn, follow, play. Those do not need a language model at all. We put a deterministic rule layer in front of the model that resolves the known vocabulary directly, in effectively zero time, and only hands the sentence to the language model when nothing in the rules matches. That single change fixed both problems. The core commands became instant and correct, because they no longer depended on a model guessing at them, and the language model was reserved for the open-ended phrasing it is actually good at. The user-facing result is a robot that responds to plain commands immediately and predictably, which is the whole point. ## Stop is special There is one more structural rule worth stating on its own: a safety command cannot wait in line. On our robot a spoken "stop" that went through the full transcription pipeline took several seconds, which is far too long when the robot is moving toward a table edge. So stop got its own path that bypasses transcription entirely and publishes the halt directly, alongside a dedicated stop control on the dashboard. The direct path responds in well under a tenth of a second versus seconds for the spoken route. When it matters, you use the fast path. The general principle: the more important and time-critical a command is, the fewer stages it should have to pass through. ## What we keep for a conversation This post is the shape of the design, not the implementation. The reason is that the interesting decisions for your product, where the rule layer ends and the model begins, how the intent maps to your specific actions, how you keep it responsive on your hardware, are judgement calls we make with you, not a snippet to paste. Structuring a voice interface that is fast and trustworthy on-device is the work we do. If you are building voice or command interfaces on an edge robot or device, on the MediaTek Genio or on NVIDIA Jetson, [tell us what you are building](/contact) and we will help you get the structure right. For the speech model behind this, see [Whisper on the Genio NPU](/blog/whisper-mediatek-genio-npu), and for the whole robot, [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## Voice-Controlled Robot on the MediaTek Genio: the ROSOrin Build URL: https://proventusnova.com/blog/voice-controlled-robot-mediatek-genio/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, robotics, ros2, edge ai, npu, embedded world We built a voice-controlled, self-driving robot on the MediaTek Genio and demoed it at Embedded World NA. The stack, the on-NPU AI, and what it took. At Embedded World North America this year we walked the floor with a robot small enough to hold in one hand. You could tell it to follow you, drive it from a tablet, or set a ball down and watch it play. Everything it did ran on a single MediaTek Genio 520, offline, with no cloud behind it. We call it ROSOrin. This post is the overview of what it is and what it took to build. The deep dives on each part link out from here. Watch the demo from the show floor: ## Key Insights - ROSOrin runs voice control, person and ball detection, navigation, and driving on one MediaTek Genio 520, fully offline - The two AI workloads that matter, speech-to-text and object detection, both run on the Genio NPU, not the CPU - Moving object detection from the CPU to the NPU took it from unusable to real-time, and dropped the board temperature enough to run fanless - The software is ROS 2 in a container stack on a custom Yocto image, which is what makes the whole thing reproducible instead of a one-off demo - The hard parts were rarely the parts you would expect, and that is the theme of every deep dive below ## What the robot does ![ROSOrin driving on the show floor at Embedded World North America, controlled from a tablet](/blog/rosorin-genio-robot-demo-floor.jpg) You speak to it. "Follow me", "move forward", "turn left", "stop", "let's play". A microphone on the robot turns your voice into text, a small on-device language step turns that text into an action, and the wheels respond. No phone app, no server, no internet. It sees. A depth camera feeds an object detector that finds people and a ball. In floor mode it can follow a person or chase the ball. In table mode it stays put and just turns to keep you in frame, so it can sit on a stand at a booth and never drive off the edge. You can also drive it yourself. A tablet loads an installable dashboard, a first-person camera view with a joystick, plus a live radar scope from the lidar so you can see what the robot sees around it. And it holds still until told otherwise. A trade-show robot that wanders on its own is a liability, so the default is passive: it does nothing until a voice command or the dashboard engages it. ## The stack, in one picture ![Close-up of the ROSOrin robot showing the MediaTek Genio 520 compute, the lidar dome, the microphone and speaker, the depth camera, and the mecanum wheels](/blog/rosorin-genio-robot-components.jpg) - **Compute:** a MediaTek Genio 520 on a MiTwell PicoITX board, our partner hardware for this build. - **AI on the NPU:** speech-to-text and object detection both run on the Genio's neural accelerator. This is the difference between a demo that works and one that overheats. See [running YOLOv8 object detection on the Genio NPU](/blog/yolov8-mediatek-genio-npu) and [Whisper speech-to-text on the Genio NPU](/blog/whisper-mediatek-genio-npu). - **Software:** ROS 2 Humble, running as a set of Docker containers on a custom Yocto image. See [running ROS 2 on the MediaTek Genio](/blog/ros2-mediatek-genio). - **Sensing and motion:** an LD06 lidar for 360-degree awareness, a depth camera for vision, and a mecanum drive that can move in any direction. ## What it actually took The interesting part of a build like this is never the block diagram. It is the week you lose to a camera that browns out the whole board, the driver bug that makes your booth WiFi look dead when it is fine, and the AI model that runs at three frames per second until you move it onto the right silicon. We wrote those stories up, because they are the parts nobody publishes and the parts that actually decide whether an edge AI product ships. Each one names the problem and the point in the build where it bites. The exact fix is the work we do for clients, so we keep that part for a conversation. ## Embedded World North America ![The ProventusNova team at the MiTwell booth at Embedded World North America with the ROSOrin robot](/blog/rosorin-genio-embedded-world-booth.jpg) We showed ROSOrin at the MiTwell booth alongside MediaTek, the two partners behind the platform. It drew a crowd, which was the point: a hand-sized robot doing real on-device speech and vision is a good way to make "edge AI on the Genio" concrete for a founder who is deciding what to build their product on. If you are building a computer-vision or robotics product on the MediaTek Genio or on NVIDIA Jetson and you want it to run real AI on-device, that is exactly the work we do. [Tell us what you are building](/contact) and we will tell you the fastest path to a working result. --- ## Whisper Speech-to-Text on the MediaTek Genio NPU URL: https://proventusnova.com/blog/whisper-mediatek-genio-npu/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, npu, whisper, speech to text, edge ai, voice control We ran Whisper on the MediaTek Genio NPU for offline robot voice control, using only the stock on-device runtime. What it takes, and the latency reality. We wanted our robot to take spoken commands with no phone, no server, and no internet. That means speech-to-text has to run on the robot. Here is what it took to put Whisper on the MediaTek Genio's neural accelerator, and an honest account of the latency, because that part is more interesting than the headline. ## Key Insights - Whisper runs on the MediaTek Genio 520 NPU using only the on-device runtime that ships in the image, with no extra runtime libraries on the target - Speech-to-text happens fully offline, which is the point for a robot, a kiosk, or any product that cannot depend on a network - With a Whisper-medium model, the encoder runs in about 1.5 seconds and a short command is 6 to 9 seconds to transcribe - End to end, from speaking to the robot moving, is about 8 to 13 seconds, and lowering that is a tuning problem, not a hardware one - The accelerator gets you on-device speech at all; the latency you actually ship is decided by model choice and how the model is served ## Why on-device speech, and why the NPU Cloud speech recognition is easy and it is the wrong choice for a lot of edge products. A robot at a trade show, a device in a vehicle, a product that handles anything sensitive: all of them need to keep working when the network does not, and some of them cannot send audio off the device at all. That pushes speech-to-text onto the device. Whisper is a strong model for this, and it is heavy. Running it on the Genio's CPU is possible but competes with everything else the robot is doing. The neural accelerator is the right home for it, the same way it is for vision. ## What we did, and what it took We run a Whisper model on the Genio NPU and it drives the robot's voice control directly: you speak, the robot transcribes on-device, an on-device step maps the words to an action, and the wheels respond. No cloud is involved at any point. The part worth calling out is that this runs on the **stock on-device runtime** already present in the Genio image. You do not need to install a separate accelerator runtime on the target to execute the model. That keeps the deployment clean: the device ships with what it needs to run inference. Getting a model into the form the accelerator loads is the engineering work, and it is the part we keep for a conversation. The reason is the same as with vision: the interesting question for your product is not the toolchain, it is whether your speech model hits a latency your users will tolerate on the board you have. That we can tell you. ## The latency reality Here is the honest picture, because it is the useful part. With a Whisper-medium model: - The encoder runs in about **1.5 seconds** per utterance. - A short spoken command transcribes in about **6 to 9 seconds**. - Add a second or two for the on-device step that turns the text into an action, and you are at roughly **8 to 13 seconds** from speaking to motion. That is real, and for a walk-up demo it is on the edge of acceptable. The levers that bring it down are model size and how the model is loaded and served between calls. A smaller Whisper model is dramatically faster and gives up some accuracy; a better serving strategy removes overhead that has nothing to do with the model at all. Which of those is right depends on the product, and choosing well is most of the latency you actually ship. We are running a direct CPU-versus-NPU speech benchmark on this exact board, and the result is more nuanced than the vision case. When it is done we will publish the numbers, because "put it on the NPU" is not automatically the fastest answer for speech, and the honest version of that story is worth more than a slogan. ## Where this fits If your product needs voice or audio understanding on-device, on the MediaTek Genio or on NVIDIA Jetson, the pattern is the same: the accelerator makes on-device speech possible, and the latency you ship is an engineering choice you want to get right early rather than discover late. That is the work we do on a fixed timeline. [Tell us what you are building](/contact) and we will tell you the fastest path to a working result. For the robot this came from, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio), and for the vision side, [YOLOv8 on the Genio NPU](/blog/yolov8-mediatek-genio-npu). --- ## Building a Yocto Image for a MediaTek Genio Robot URL: https://proventusnova.com/blog/yocto-image-mediatek-genio-robot/ Published: 2026-09-28 Author: Andres Campos Tags: yocto, mediatek genio, ros2, embedded linux, bsp A demo becomes a product when it boots identically every time. How we baked our robot's ROS 2 stack into a Yocto image, and the trap that ruins a reflash. There is a moment in every hardware project where a working demo has to become something you can build twice. For an embedded product on the MediaTek Genio, that moment is a Yocto image. Here is how we packaged our robot into one, and the single discipline that decides whether your image is trustworthy. ## Key Insights - A demo lives on one hand-tuned board; a product lives in a Yocto image that rebuilds and reflashes identically - We package the robot's ROS 2 container stack, its recipes, and a boot service into a custom layer so the whole system lands the same way every build - The boot service starts the stack in order and brings up only the nodes whose hardware is actually present - The most dangerous mistake is a fix made live on the board that never made it into the recipe, because a reflash silently loses it - Treat the recipe as the source of truth, and verify that a fresh flash reproduces the working board ## Why Yocto, and not a setup script You can bring a board up by hand. You should not ship that way. A board configured by hand is a board only one person can reproduce, and only until they forget a step. A Yocto image is the opposite: it is a build that produces the same root filesystem every time, boots the same way, and can be written onto the next unit with no bespoke setup. The first time you need a second board, or need to reflash the one you have, the image is what saves you from redoing bring-up from memory. For our robot the image carries the whole software system: the ROS 2 nodes as containers, the recipes that place them and their supporting files, and the orchestration that starts everything at boot. ## Baking in a container stack Running each ROS 2 node as a container is the right structure, and Yocto is how you make that structure durable. A custom layer packages the container images and the recipes that install them, so that a freshly flashed board comes up with the exact stack you tested. On top of that sits a boot service that starts the stack in a defined order and is smart about hardware: it brings up the camera and its consumers only when the camera is present, the lidar-dependent nodes only when the lidar is there, and so on. That way one image behaves sensibly whether a given peripheral is plugged in or not, which matters on a robot that gets reconfigured on the bench. ## The trap that bites reflashes Here is the discipline that separates a real image from a fragile one. During bring-up you will fix things live on the board, because that is faster than a rebuild. Every one of those fixes has to be folded back into the layer, or your image is quietly wrong. We hit exactly this: several pieces of the boot orchestration existed only on the board and not in the recipe, which meant a fresh flash would have come up missing part of the startup. The board worked; the image that was supposed to reproduce it did not. The rule is simple to state and easy to violate: the recipe is the source of truth, not the board. The check that enforces it is to flash a clean image and confirm the result matches your working unit, before you rely on the image for anything. If a reflash loses a fix, you have found the gap while it is cheap. We keep the specifics of our layer and recipes for the projects we do, because they are particular to the platform and the product. The reusable lesson is the one above: a Yocto image is only as trustworthy as your discipline about keeping the board and the recipe in sync. If you are turning a working prototype into a reproducible, flashable image on the MediaTek Genio or on NVIDIA Jetson, that BSP and image work is a core part of what we do. [Tell us what you are building](/contact). For the robot this image runs, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio) and [running ROS 2 on the MediaTek Genio](/blog/ros2-mediatek-genio). --- ## YOLOv8 Object Detection on the MediaTek Genio NPU URL: https://proventusnova.com/blog/yolov8-mediatek-genio-npu/ Published: 2026-09-28 Author: Andres Campos Tags: mediatek genio, npu, yolo, object detection, edge ai, computer vision Moving YOLOv8 to the MediaTek Genio NPU took our robot's detection from 426 ms to 32 ms per frame and let it run fanless. What it takes, and when it pays off. Our robot needed to find people and a ball from a live camera, on the robot itself, with no fan and no cloud. We started where most teams start, running YOLOv8 on the CPU. It technically worked and it was unusable. Here is what the numbers looked like, and what it took to fix them. ## Key Insights - YOLOv8s on the MediaTek Genio 520 CPU ran at 426 ms per frame and drove the chip to 84 C, throttling under its own load - The same model on the Genio NPU ran at 32 ms per frame at 58 to 60 C, about 13 times faster and cool enough to run fanless - The NPU output matched the CPU output on the same frames, so this was a speed and thermal win, not a change in accuracy - The real work is converting the model to the accelerator's format correctly, especially the detection head, not the runtime - On a fanless edge board, on-device vision is a thermal problem before it is a speed problem, and the NPU solves both at once ## The problem: the CPU cannot do this YOLOv8s at a normal input size on the Genio's CPU cores runs at about 426 milliseconds per frame. That is roughly two frames per second, and to get even that the board pins every core at maximum clock. The chip climbs past 80 C and sits at 84 C, above the point where it wants to throttle. On a robot with no fan, in a warm exhibition hall, that is not a demo you can leave running. You cannot cap the CPU usage your way out of it either. The inference is simply slower than the frame rate you want, so limiting cores just makes it slower. The workload is on the wrong processor. ## The fix, in shape: move it to the NPU The MediaTek Genio has a dedicated neural accelerator. Moving YOLOv8 onto it took the same model to **32 milliseconds per frame** at **58 to 60 C**, with the camera and the rest of the stack running at the same time. That is about 13 times faster, and roughly 25 degrees cooler, on the same board doing the same job. Crucially, the detections did not change. We ran the accelerator output against the CPU output on identical frames and got the same results, down to a person detected at 0.86 confidence on a reference image. This is the outcome you want: the NPU is not a lossy shortcut, it is the correct place to run the model. | YOLOv8s on the Genio 520 | Per frame | SoC temperature | Practical result | |---|---|---|---| | CPU | 426 ms | 84 C (throttling) | ~2 fps, cannot run fanless | | NPU | 32 ms | 58-60 C | ~8-9 fps, runs fanless | ## What it takes, and where it gets hard At a high level the path is what you would expect. The model has to be converted from its trained form into the format the accelerator loads. Then it runs through the on-device runtime that already ships on the Genio image, so you do not need any special runtime libraries on the target to execute it. The difficulty is in the conversion, and specifically in the parts of a detection network that do not map cleanly onto the accelerator. A YOLO model is not just a stack of convolutions; the detection head does work that needs care to land on the NPU without losing correctness or speed. Getting that conversion right, for your exact model and your exact board, is the engineering, and it is the difference between the 426 ms result and the 32 ms one. That conversion work is what we do. We have kept the step-by-step out of this post on purpose, because the interesting question for a product team is not the command line, it is whether your model can hit your frame rate and thermal budget on the silicon you have chosen. That is a question we can answer quickly. ## When this matters for your product If you are putting a camera and a vision model on a fanless or battery-powered edge device, assume from the start that the CPU is not where inference belongs. The order of operations that saves teams months is: pick the board for its accelerator, confirm your model converts and hits your numbers on it, then build the product around that. Discovering at integration time that your detector cooks the board is an expensive way to learn the same thing. We do this on the MediaTek Genio and on NVIDIA Jetson: get a real model running on the real accelerator, at the frame rate and temperature your product needs, on a fixed timeline. If that is your problem, [tell us what you are building](/contact) and we will tell you the fastest way to a working result. For the wider picture of the robot this came from, see [the ROSOrin build overview](/blog/voice-controlled-robot-mediatek-genio). --- ## GStreamer alternatives on NVIDIA Jetson: what really replaces it URL: https://proventusnova.com/blog/gstreamer-alternatives-nvidia-jetson/ Published: 2026-09-18 Author: Andres Campos Tags: jetson, gstreamer, ffmpeg, deepstream, multimedia api, pipeline optimization GStreamer alternative on NVIDIA Jetson? FFmpeg, the Multimedia API, DeepStream, OpenCV and libcamera compared by job, plus when switching is the wrong fix. Most Jetson teams searching for a GStreamer alternative do not have a GStreamer problem. They have a pipeline that was never profiled. We know because the pipelines that reach us with "we need to get off GStreamer" in the first email usually leave with the same framework, a different element graph, and the latency they wanted. That is not an argument for staying. There are real cases where GStreamer is the wrong tool on Jetson, and there are four alternatives worth knowing in detail. This guide covers what each one is, which job each wins, and a ten-minute test that tells you whether switching would change anything before you rewrite a working pipeline. ## Key Insights - There is no drop-in replacement for GStreamer on Jetson. Every accelerated path ends at NVENC, NVDEC, the VIC and the Argus ISP, and the alternatives are different wrappers around the same blocks - DeepStream is not an alternative. It is GStreamer plus NVIDIA's inference, tracking and on-screen-display plugins, and it inherits every GStreamer behavior you are trying to escape - FFmpeg on Jetson decodes in hardware with NVIDIA's patch and encodes in hardware only through the unsupported community jetson-ffmpeg project, which makes it a transcode tool, not a live multi-camera pipeline tool - The Jetson Multimedia API is the layer GStreamer's nv plugins wrap. It buys direct control of NvBufSurface buffers at the price of roughly ten times the code and all the buffer management - The problems that drive most switch attempts are diagnosable in minutes: a software videoconvert in the path, buffers leaving NVMM memory, a missing queue, sync=true on a live sink, or Python in the frame loop ## Why teams go looking for a GStreamer alternative on Jetson The complaints are consistent. Caps negotiation fails with an error that names none of the elements involved. A pipeline that ran at 30 fps on a desktop runs at 9 fps on an Orin NX. CPU sits at 100% on one core while the GPU idles. Adding a second camera halves the frame rate of the first one. The debug output is a wall of `GST_DEBUG` lines that explain everything except the problem. None of those are GStreamer being slow. Each one maps to a specific misuse of Jetson's memory and hardware model, and each has a known fix. We documented the four patterns that kill throughput in the [GStreamer pipeline performance guide](/blog/gstreamer-pipeline-performance-jetson), and the short version is that Jetson pipelines fail when buffers leave NVMM memory, when a CPU element sits where a hardware element belongs, or when several processes fight over the single VIC block. The reason this matters for the alternatives question is simple. Every alternative on this list runs on the same hardware blocks with the same memory rules. If your pipeline copies frames through system RAM in GStreamer, the same design copies them through system RAM in FFmpeg or in raw Multimedia API code. Switching frameworks does not remove the constraint. It removes the tooling that makes the constraint visible. ## What actually replaces GStreamer on Jetson Short answer: nothing replaces it outright, but four options cover real jobs better than GStreamer does, and one option that looks like an alternative is not one. | Option | What it is | When it beats GStreamer | |---|---|---| | **FFmpeg** | General media toolkit. Hardware decode on Jetson via NVIDIA's patch; hardware encode only via community `jetson-ffmpeg` (nvmpi) | File transcode jobs, batch processing, simple single-stream capture to disk. Weak for live multi-camera pipelines | | **Jetson Multimedia API** | The C++ layer (V4L2 codecs, Argus, NvBufSurface, EGL) that GStreamer's `nv` plugins wrap | Maximum control and minimum latency when you will write and maintain roughly 10x the code | | **DeepStream** | NVIDIA's AI video framework, built on top of GStreamer, not beside it | Never as an escape from GStreamer. It is the right choice for multi-stream inference, and it is still GStreamer | | **OpenCV VideoCapture** | Convenience capture API. On Jetson, the accelerated path runs through OpenCV's GStreamer backend | Quick prototypes where pipeline control does not matter | | **libcamera / raw V4L2** | Mainline Linux camera stack, or direct V4L2 capture | Sensors that bypass the ISP on purpose, such as RAW capture for your own debayer on the GPU. Not a production path for tuned CSI cameras | ### FFmpeg on Jetson: decode yes, encode maybe NVIDIA ships a patch for FFmpeg that adds a V4L2-based hardware decoder for H.264, H.265 and the other NVDEC formats. Decode-only workflows, such as pulling frames from an RTSP source to feed a model, work well with it. Encode is the gap. NVIDIA does not provide FFmpeg hardware encode on Jetson. The community `jetson-ffmpeg` project fills it with the nvmpi encoder and decoder, and it works, but it trails JetPack releases, breaks on major L4T changes, and has no vendor behind it when it does. That makes FFmpeg the right tool for transcode jobs, archival, and batch work, and the wrong tool for a live camera pipeline that needs supported hardware encode on every JetPack you will ship on. ### The Jetson Multimedia API: the layer under the nv plugins When you use `nvv4l2decoder`, `nvvidconv` or `nvarguscamerasrc`, you are calling the Multimedia API through a plugin. Calling it directly gives you the NvBufSurface buffer as a first-class object, explicit control of every DMA transfer, and no negotiation layer between you and the hardware. The gain is real but bounded. Once a GStreamer pipeline keeps every buffer in NVMM memory, the hardware is doing the same work either way. What you remove is GStreamer's scheduling overhead and its opinions about buffering, which matters when you are chasing the last few milliseconds on a single deterministic pipeline. What you take on is everything GStreamer was doing for you: threading, queueing, format conversion, error recovery, and the RTSP or WebRTC server you probably also need. Our own experience with this trade is documented in the [Python to C++ latency post](/blog/python-to-cpp-embedded-ai-device-time-to-market). The 4x latency reduction in that engagement came from moving the frame loop out of Python and into C++ with zero-copy NvBufSurface access. The GStreamer pipeline stayed. The interpreter left. ### DeepStream is GStreamer with NVIDIA's AI plugins This one deserves a direct statement because "DeepStream vs GStreamer" is a common search and a category error. DeepStream is a set of GStreamer plugins: `nvstreammux` batches streams, `nvinfer` runs TensorRT, `nvtracker` tracks objects, `nvdsosd` draws the overlay. The sample apps are GStreamer applications. If your team is fighting caps negotiation today, DeepStream adds elements to negotiate with. Choose DeepStream when the job is multi-stream inference on Jetson, because the batching and zero-copy inference path is excellent. Do not choose it to leave GStreamer. ### OpenCV VideoCapture on Jetson goes through GStreamer anyway `cv2.VideoCapture(pipeline_string, cv2.CAP_GSTREAMER)` is the standard way to get accelerated frames into OpenCV on Jetson, and the name gives it away. The V4L2 backend avoids GStreamer, and it also avoids the ISP, the hardware color conversion and NVMM memory. You get raw CPU frames, which is acceptable for a prototype and a guaranteed bottleneck in production. ### libcamera and raw V4L2: capture without the ISP libcamera is the modern Linux camera stack and it is not NVIDIA's. Jetson's ISP is reached through libargus, which is what `nvarguscamerasrc` wraps. Capturing through raw V4L2 with `v4l2src` or libcamera works for sensors where you want the RAW Bayer frames untouched, for instance to run your own debayer on the GPU. For a tuned production CSI camera, Argus is the path that delivers usable images. ## Which option wins by job | Job | Best fit on Jetson | Why | |---|---|---| | Live CSI or GMSL2 camera capture | GStreamer (`nvarguscamerasrc`) | Only supported route through the Argus ISP | | Hardware encode for streaming | GStreamer (`nvv4l2h264enc` / `nvv4l2h265enc`) | Supported NVENC path on every JetPack | | RTSP or WebRTC server | GStreamer (`gst-rtsp-server`, `webrtcbin`) | Nothing else on the list includes a server | | Multi-stream inference | DeepStream (still GStreamer) | `nvstreammux` batching and zero-copy `nvinfer` | | File transcode, archival, batch | FFmpeg | Simpler tooling, hardware decode is supported | | Single deterministic pipeline, latency budget in single-digit ms | Jetson Multimedia API | Direct NvBufSurface control, no scheduler in the way | | RAW sensor capture for custom GPU processing | Raw V4L2 | Bypasses the ISP by design | | Prototype in a notebook | OpenCV with the GStreamer backend | Fast to write, and honest about what it is | Read the table as a map of jobs, not a ranking. Most Jetson products need three or four rows at once, and the framework that covers the most rows without a rewrite is the one that already wraps every block. ## The ten-minute test before you switch Before any rewrite, measure where the frames spend their time. We released [gst-profile](/resources/gst-profile) for exactly this. It is a stdlib-only Python tool, MIT licensed, verified on a real Orin NX, and it ranks per-element time and flags the three faults that account for most "GStreamer is slow" reports: a zero-copy break where buffers leave GPU memory and come back, a software element doing work a hardware block should do, and a missing queue that pins the whole pipeline to one thread. While it runs, check the five things we find most often: 1. **A `videoconvert` anywhere in the path.** On Jetson it copies every frame through system RAM. `nvvidconv` does the same job on the VIC and stays in NVMM. 2. **Buffers leaving NVMM memory.** Look for `video/x-raw` caps without `(memory:NVMM)` between two `nv` elements. Each break costs two DMA copies per frame. 3. **`sync=true` on a live sink.** The default presentation clock adds tens to hundreds of milliseconds. Setting `sync=false` on the output sink is the single largest latency fix on a live camera pipeline, as measured in our [end-to-end latency guide](/blog/reduce-gstreamer-pipeline-latency-jetson). 4. **Several GStreamer processes sharing the VIC.** `nvvidconv` instances in separate processes serialize on one hardware block, and throughput can fall five to ten times. The fix is [one process for all pipelines](/blog/nvvidconv-performance-multiple-gstreamer-processes), not a new framework. 5. **Python in the frame loop.** On Jetson CV pipelines, interpreter overhead is typically 30 to 45 percent of end-to-end latency. Moving the loop to C++ fixes that inside GStreamer. If the profile comes back clean and the pipeline is still outside its budget, you have a real reason to consider the Multimedia API. If it comes back with any of the five, you have a pipeline fix, and it is usually a one-session fix. ## When switching is the right call **Stay on GStreamer when** the product needs live camera capture through the ISP, supported hardware encode, a streaming server, or multi-camera composition. That describes most Jetson vision products, and the [working pipeline examples](/blog/gstreamer-pipeline-examples-jetson) cover the standard topologies. **Move to the Jetson Multimedia API when** you have one fixed pipeline, a latency budget that GStreamer's scheduler measurably breaks after profiling, and an engineer who will own buffer management for the life of the product. Budget the rewrite honestly: the [live streaming pipeline](/blog/gstreamer-live-streaming-jetson-orin) is a few dozen characters in GStreamer and a few thousand lines in the Multimedia API. **Use FFmpeg when** the job is offline: transcoding recordings, extracting frames for a dataset, or decoding a file to feed a model once. Keep it away from live encode unless you accept an unsupported codec path. **Use DeepStream when** the job is inference across many streams. Go in knowing it is GStreamer, and bring the same NVMM discipline. **Use raw V4L2 when** you want the sensor's RAW output on purpose. The rest of the stack, including the [nvcompositor versus parallel pipelines](/blog/jetson-nvcompositor-vs-parallel-gstreamer) question, stays in GStreamer. ## What we see on client projects The pattern repeats often enough to state plainly. A team arrives certain that GStreamer is the bottleneck. The profile shows a `videoconvert` that nobody remembers adding, three processes queued on the VIC, or a `queue` with the default 200-frame buffer on a live feed. The fix lands in a working session, the frame rate comes back, and the framework question disappears. The rare exception is a single-pipeline product with a hard real-time budget, where the Multimedia API rewrite is justified and we scope it as its own milestone. The expensive outcome is the middle path: a partial migration to FFmpeg or hand-written V4L2 code that removes the parts of GStreamer that were working, keeps the memory copies that were not, and adds a codebase with no upstream. If you are considering that path, profile first. ## When to call a specialist Pipeline fixes are bounded work. If profiling points at a design problem, an unusual sensor combination, or a latency budget that needs the Multimedia API, the [GStreamer development service](/services/gstreamer) page describes how we scope that as a fixed-bid milestone with a working result at the end of it, rather than an open-ended rewrite. --- ## 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).* --- ## 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 `