<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anguo</title>
    <description>The latest articles on DEV Community by Anguo (@anguoyang).</description>
    <link>https://dev.to/anguoyang</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4220481%2F21fcf79b-9579-4b22-b0c4-cb98d6ce8440.png</url>
      <title>DEV Community: Anguo</title>
      <link>https://dev.to/anguoyang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anguoyang"/>
    <language>en</language>
    <item>
      <title>Deploying to 20 Rockchip boards by hand is a trap — so I built rk-deploy</title>
      <dc:creator>Anguo</dc:creator>
      <pubDate>Mon, 02 Nov 2026 02:39:29 +0000</pubDate>
      <link>https://dev.to/anguoyang/deploying-to-20-rockchip-boards-by-hand-is-a-trap-so-i-built-rk-deploy-58c5</link>
      <guid>https://dev.to/anguoyang/deploying-to-20-rockchip-boards-by-hand-is-a-trap-so-i-built-rk-deploy-58c5</guid>
      <description>&lt;p&gt;If you ship software to Rockchip boards — RK3588, RK3576, RK356x — you&lt;br&gt;
know the ritual. Build a tarball. &lt;code&gt;scp&lt;/code&gt; it to the board. SSH in, untar&lt;br&gt;
it somewhere, write a systemd unit by hand, &lt;code&gt;daemon-reload&lt;/code&gt;, restart,&lt;br&gt;
tail the journal, and pray. Now do it nineteen more times. Now do it&lt;br&gt;
again for the hotfix. Now one board is still running the old version&lt;br&gt;
and nobody knows which.&lt;/p&gt;

&lt;p&gt;I kept living that ritual, so I turned it into a tool:&lt;br&gt;
&lt;a href="https://github.com/anguoyang/rk-deploy" rel="noopener noreferrer"&gt;&lt;strong&gt;rk-deploy&lt;/strong&gt;&lt;/a&gt; — manifest in,&lt;br&gt;
systemd services out, with fleet push, rollback and status reports.&lt;br&gt;
v0.1.0 is out today, Apache-2.0.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;You describe the app once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.1.0&lt;/span&gt;
&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt;
  &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app.py&lt;/span&gt;
&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;models/weights.rknn&lt;/span&gt;
&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@INSTALL_ROOT@/shared/heartbeat.json"&lt;/span&gt;
  &lt;span class="na"&gt;timeout_sec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rkdeploy pack                                        &lt;span class="c"&gt;# checksummed tarball&lt;/span&gt;
rkdeploy deploy &lt;span class="nt"&gt;--fleet&lt;/span&gt; fleet.yaml demo-1.1.0.tar.gz &lt;span class="c"&gt;# push to every board&lt;/span&gt;
rkdeploy rollback &lt;span class="nt"&gt;--fleet&lt;/span&gt; fleet.yaml &lt;span class="nt"&gt;--app&lt;/span&gt; demo      &lt;span class="c"&gt;# back, fleet-wide&lt;/span&gt;
rkdeploy status &lt;span class="nt"&gt;--fleet&lt;/span&gt; fleet.yaml &lt;span class="nt"&gt;--app&lt;/span&gt; demo        &lt;span class="c"&gt;# one probe per board&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules I was not willing to compromise on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only what the manifest declares gets packed.&lt;/strong&gt; Whatever else is
lying in your directory — notes, stray secrets — does not travel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versions are immutable, switching is atomic.&lt;/strong&gt; Each release
unpacks into its own directory; going live is one symlink swap
(a fresh symlink + &lt;code&gt;mv -Tf&lt;/code&gt; — &lt;code&gt;ln -sfn&lt;/code&gt; on a symlink-to-directory
is a trap that follows the link and creates the new one &lt;em&gt;inside&lt;/em&gt;
the target).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config and data live outside releases&lt;/strong&gt;, in &lt;code&gt;shared/&lt;/code&gt;, so
upgrades and rollbacks never touch them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The board stays dumb.&lt;/strong&gt; All parsing happens on your machine. The
board runs one POSIX shell installer and stock systemd — because
board Python stacks vary wildly between BSPs (the RK3588 I test on
has Python 3.10 and a stray PyYAML 5.4.1; the next board may have
neither), the installer reads a flat rendered &lt;code&gt;package.env&lt;/code&gt;, never
YAML, never even JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No agent, no new ports, no credential collection.&lt;/strong&gt; Plain SSH
with your keys, one upload + one execution per board per deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A failed health check rolls back automatically&lt;/strong&gt;, and the exit
code tells you exactly which world you're in: 0 ok, 4 the package
failed verification, 5 part of the fleet failed, 6 the deploy
failed &lt;em&gt;but the board is back on the previous version&lt;/em&gt;, 7 it failed
and rollback failed too.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug my own test suite caught before any board did
&lt;/h2&gt;

&lt;p&gt;The demo app writes a heartbeat file every second — the perfect&lt;br&gt;
health check. Except: during an upgrade, the &lt;em&gt;old&lt;/em&gt; version's process&lt;br&gt;
is still running until the restart, happily refreshing that same&lt;br&gt;
heartbeat file. An existence check would pass the brand-new version&lt;br&gt;
before its process wrote a single byte.&lt;/p&gt;

&lt;p&gt;So for &lt;code&gt;file&lt;/code&gt; health checks, rk-deploy also requires the file to&lt;br&gt;
have been written &lt;strong&gt;at or after this deploy's restart moment&lt;/strong&gt;. A&lt;br&gt;
one-line rule, found by a lifecycle test that runs the real board&lt;br&gt;
scripts against a fake &lt;code&gt;systemctl&lt;/code&gt; which actually starts the app —&lt;br&gt;
install, upgrade, duplicate-version refusal, rollback, broken&lt;br&gt;
release, auto-rollback, all of it, no hardware needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real board, real numbers
&lt;/h2&gt;

&lt;p&gt;Validated on an RK3588 (Ubuntu 22.04, systemd 249), driving the&lt;br&gt;
shipped CLI over a tunnel — where every SSH connection costs 5–7&lt;br&gt;
seconds just to establish, which is itself the best argument for the&lt;br&gt;
one-connection-per-operation design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploy 1.0.0 → &lt;strong&gt;ok, 21 s&lt;/strong&gt; (three connections: mkdir, scp, install)&lt;/li&gt;
&lt;li&gt;status → &lt;strong&gt;ATTENTION&lt;/strong&gt; — only one release installed, i.e. &lt;em&gt;no
rollback candidate yet&lt;/em&gt;. The warning is the spec's own rule firing.&lt;/li&gt;
&lt;li&gt;deploy 1.1.0 → &lt;strong&gt;ok, 19 s&lt;/strong&gt;; status → &lt;strong&gt;HEALTHY&lt;/strong&gt; (SoC 49 °C)&lt;/li&gt;
&lt;li&gt;deploy a deliberately broken 1.2.0 → health check fails,
&lt;strong&gt;auto-rollback to 1.1.0&lt;/strong&gt;, exit 6, 26 s&lt;/li&gt;
&lt;li&gt;explicit rollback to 1.0.0 → ok, fleet HEALTHY&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A genuine status report from that run ships in the repo under&lt;br&gt;
&lt;code&gt;examples/&lt;/code&gt;. And the board's &lt;code&gt;systemctl --failed&lt;/code&gt; afterwards?&lt;br&gt;
Identical to before the test — the tool cleans up after itself in&lt;br&gt;
validation and stays out of everything it doesn't manage in&lt;br&gt;
production. One related call: status judging uses only &lt;em&gt;your app's&lt;/em&gt;&lt;br&gt;
unit state, never the machine-wide &lt;code&gt;is-system-running&lt;/code&gt; — a board can&lt;br&gt;
sit at &lt;code&gt;degraded&lt;/code&gt; for months because of some unrelated logrotate&lt;br&gt;
unit while your app is perfectly healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately doesn't do
&lt;/h2&gt;

&lt;p&gt;No resident agent, no central server, no web UI. No automatic&lt;br&gt;
&lt;code&gt;apt install&lt;/code&gt; — dependencies are a preflight &lt;em&gt;checklist&lt;/em&gt; (binaries /&lt;br&gt;
shared libs / python modules / packages / devices), reported&lt;br&gt;
precisely, because customer boards are often offline and silently&lt;br&gt;
mutating system packages on an unattended board is worse than a&lt;br&gt;
clear report. No data migration on rollback — if your app changes&lt;br&gt;
its on-disk format incompatibly, that's your app's problem, and the&lt;br&gt;
docs say so in exactly those words, because it's precisely the thing&lt;br&gt;
people assume a deploy tool does for them.&lt;/p&gt;

&lt;p&gt;rk-deploy is the third tool in a small open family for Rockchip edge&lt;br&gt;
work, next to&lt;br&gt;
&lt;a href="https://github.com/anguoyang/rk-edge-doctor" rel="noopener noreferrer"&gt;rk-edge-doctor&lt;/a&gt;&lt;br&gt;
(read-only board / model / RTSP diagnostics) and&lt;br&gt;
&lt;a href="https://github.com/anguoyang/rk-bench" rel="noopener noreferrer"&gt;rk-bench&lt;/a&gt; (a measured RKNN&lt;br&gt;
model leaderboard). Diagnosis, measurement — and now delivery.&lt;/p&gt;

&lt;p&gt;If you deploy to RK boards for a living, I'd genuinely like to hear&lt;br&gt;
what your ritual looks like and what would break first in your&lt;br&gt;
environment: RK3576 and RK356x board reports are the most useful&lt;br&gt;
thing you can file.&lt;/p&gt;

</description>
      <category>rockchip</category>
      <category>embedded</category>
      <category>devops</category>
      <category>systemd</category>
    </item>
    <item>
      <title>RK3588 INT8, measured: 1.96 on YOLOv8n, 1.4 on YOLO11n — plus OCR joins the bench</title>
      <dc:creator>Anguo</dc:creator>
      <pubDate>Mon, 02 Nov 2026 02:38:40 +0000</pubDate>
      <link>https://dev.to/anguoyang/rk3588-int8-measured-196x-on-yolov8n-14x-on-yolo11n-plus-ocr-joins-the-bench-jbf</link>
      <guid>https://dev.to/anguoyang/rk3588-int8-measured-196x-on-yolov8n-14x-on-yolo11n-plus-ocr-joins-the-bench-jbf</guid>
      <description>&lt;p&gt;Follow-up to &lt;a href="https://dev.to/anguoyang/10-rknn-models-benchmarked-on-rk3588-and-the-fastest-one-is-cheating-onto-the-cpu-2pkh"&gt;the first rk-bench leaderboard&lt;/a&gt;: same RK3588, same frozen protocol (PERF_RUN hardware timing, per-layer CPU-fallback verdicts, median of 3 independent sessions). v0.2.0 adds the two things readers asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  INT8 vs FP, same models, same board
&lt;/h2&gt;

&lt;p&gt;Quantized with Toolkit2 2.3.2 using the RKNN Model Zoo's own recipe and its own 20-image COCO calibration subset (each entry's notes say so):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;FP p50&lt;/th&gt;
&lt;th&gt;INT8 p50&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;YOLOv8n&lt;/td&gt;
&lt;td&gt;37.50 ms&lt;/td&gt;
&lt;td&gt;19.17 ms&lt;/td&gt;
&lt;td&gt;1.96×&lt;/td&gt;
&lt;td&gt;NPU_ONLY → NPU_ONLY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLOv5s (ReLU)&lt;/td&gt;
&lt;td&gt;37.80 ms&lt;/td&gt;
&lt;td&gt;20.52 ms&lt;/td&gt;
&lt;td&gt;1.84×&lt;/td&gt;
&lt;td&gt;NPU_ONLY → NPU_ONLY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLO11n&lt;/td&gt;
&lt;td&gt;43.67 ms&lt;/td&gt;
&lt;td&gt;31.20 ms&lt;/td&gt;
&lt;td&gt;1.40×&lt;/td&gt;
&lt;td&gt;NPU_ONLY → NPU_ONLY&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So on RK3588, INT8 buys you roughly 2× on the older YOLO generations and 1.4× on YOLO11n — and in all three cases the quantized graph stays entirely on the NPU. Model files roughly halve (7.25 → 4.13 MB for YOLOv8n).&lt;/p&gt;

&lt;h2&gt;
  
  
  OCR rows, and a second CPU stowaway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PP-OCRv4-Det&lt;/strong&gt; (480×480): 47.86 ms, &lt;code&gt;NPU_ONLY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PP-OCRv4-Rec&lt;/strong&gt; (48×320): 22.68 ms, &lt;strong&gt;&lt;code&gt;MIXED&lt;/code&gt;&lt;/strong&gt; — one Reshape immediately after the softmax executes on the CPU: 579 µs, 2.58% of frame time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes two MIXED models on the board (RetinaFace was the first), and both were caught by the per-layer verdict — neither is visible from end-to-end timing. This is exactly the failure mode a leaderboard should surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug in my own tool
&lt;/h2&gt;

&lt;p&gt;Adding INT8 rows exposed a real bug in rk-bench itself: a leaderboard row's identity was model+variant, so the first INT8 report &lt;em&gt;replaced&lt;/em&gt; its FP twin instead of sitting next to it. Identity now includes precision, there's a regression test, and the published leaderboard was rebuilt from the raw reports and verified row by row (15 rows). If you cloned v0.1.0's data, re-pull.&lt;/p&gt;

&lt;p&gt;Also new in the tool: &lt;code&gt;rkbench run --note&lt;/code&gt; records provenance (calibration sets, conversion quirks) in the report, so a number's paperwork travels with it.&lt;/p&gt;

&lt;p&gt;Full table: &lt;a href="https://github.com/anguoyang/rk-bench/blob/main/docs/LEADERBOARD.md" rel="noopener noreferrer"&gt;docs/LEADERBOARD.md&lt;/a&gt; · Release: &lt;a href="https://github.com/anguoyang/rk-bench/releases/tag/v0.2.0" rel="noopener noreferrer"&gt;v0.2.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next open items: the RK3576 board table (seeded as a good first issue — if you have the board, the anchor models take ~10 minutes), and &lt;code&gt;--core-mask&lt;/code&gt; measurements. Requests welcome in the bench-report issues.&lt;/p&gt;

</description>
      <category>rockchip</category>
      <category>rknn</category>
      <category>benchmark</category>
      <category>edgeai</category>
    </item>
    <item>
      <title>Your IP camera's clock is wrong — and that's why ONVIF authentication keeps failing</title>
      <dc:creator>Anguo</dc:creator>
      <pubDate>Mon, 02 Nov 2026 02:30:17 +0000</pubDate>
      <link>https://dev.to/anguoyang/your-ip-cameras-clock-is-wrong-and-thats-why-onvif-authentication-keeps-failing-31e6</link>
      <guid>https://dev.to/anguoyang/your-ip-cameras-clock-is-wrong-and-thats-why-onvif-authentication-keeps-failing-31e6</guid>
      <description>&lt;p&gt;Every ONVIF integration eventually hits the same ghost: the password is&lt;br&gt;
correct, the user exists, the camera answers discovery — and every&lt;br&gt;
authenticated call comes back &lt;code&gt;NotAuthorized&lt;/code&gt;. People blame the vendor,&lt;br&gt;
the firmware, the client library. Most of the time, it's the clock.&lt;/p&gt;

&lt;p&gt;ONVIF authentication is a WS-Security UsernameToken whose password digest&lt;br&gt;
is &lt;code&gt;Base64(SHA1(nonce + Created + password))&lt;/code&gt;. The device checks &lt;code&gt;Created&lt;/code&gt;&lt;br&gt;
against &lt;strong&gt;its own clock&lt;/strong&gt;. A camera that has been running in a closet&lt;br&gt;
without NTP for six months can be minutes — or hours — off. Your digest is&lt;br&gt;
mathematically perfect and temporally unacceptable, so the camera says no.&lt;/p&gt;

&lt;p&gt;The fix is in the spec, hiding in plain sight: &lt;code&gt;GetSystemDateAndTime&lt;/code&gt; is&lt;br&gt;
one of the few operations a device must answer &lt;em&gt;without&lt;/em&gt; authentication,&lt;br&gt;
precisely so a client can measure the offset first and sign against&lt;br&gt;
device time.&lt;/p&gt;

&lt;p&gt;I built a small tool around that lesson, and around the pile of similar&lt;br&gt;
lessons ONVIF teaches you one deployment at a time.&lt;/p&gt;
&lt;h2&gt;
  
  
  onvif-scan
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/anguoyang/onvif-scan" rel="noopener noreferrer"&gt;onvif-scan&lt;/a&gt; discovers every&lt;br&gt;
ONVIF camera on a LAN and writes an asset inventory — JSON plus a&lt;br&gt;
single-file HTML report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;onvifscan scan &lt;span class="nt"&gt;--user&lt;/span&gt; onvif_user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;● 192.168.1.64   VendorA Cam X200   FULL ch=2 · H264 1920x1080 PTZ
● 192.168.1.65   VendorB Dome Y10   FULL ch=1 · H265 2560x1440
◐ 192.168.1.66   (unknown)          PARTIAL ch=0
— discovered 3 · full 2 · partial 1 · …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per device it records manufacturer, model, firmware, serial, channels,&lt;br&gt;
per-profile codec / resolution / frame-rate limits, PTZ presence, clock&lt;br&gt;
offset, and the RTSP stream URI for each profile (password-masked,&lt;br&gt;
ready to hand to a stream-acceptance checker —&lt;br&gt;
&lt;a href="https://github.com/anguoyang/rk-edge-doctor" rel="noopener noreferrer"&gt;rkdoctor rtsp&lt;/a&gt; if you use&lt;br&gt;
my other tool; the two are designed as a discovery → acceptance chain).&lt;/p&gt;

&lt;p&gt;It's pure Python standard library. No dependencies, nothing to install on&lt;br&gt;
the machine you carry onto a site, and it runs offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the inventory checks, beyond fields
&lt;/h2&gt;

&lt;p&gt;Fields are the easy part. The useful part is the findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CLOCK_SKEW&lt;/strong&gt; — device clock more than 60 s off. This is the finding
that explains other people's auth failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNAUTH_INFO_DISCLOSURE&lt;/strong&gt; — &lt;code&gt;GetDeviceInformation&lt;/code&gt; answered with no
credentials at all. Plenty of older firmware does this. You want to
know which cameras on a site do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XADDR_MISMATCH&lt;/strong&gt; — the address in the discovery reply is not the
address that answered (dual NICs, NAT, an IP change the ONVIF stack
didn't hear about). The tool falls back to the responder's address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MEDIA2_ONLY / LEGACY_MEDIA_ONLY&lt;/strong&gt; — which media generation a camera
actually speaks; newer devices are dropping ver10 Media entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AUTH_FAILED&lt;/strong&gt; — your credential was rejected, once. Which brings me
to the rule I'm proudest of:&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A scanner must never lock you out of your own cameras
&lt;/h2&gt;

&lt;p&gt;onvif-scan authenticates &lt;strong&gt;only&lt;/strong&gt; with credentials you explicitly&lt;br&gt;
supply — one per device from a mapping file, or one shared ONVIF user.&lt;br&gt;
Each is tried &lt;strong&gt;exactly once&lt;/strong&gt;. There is no default-password list, no&lt;br&gt;
dictionary, no "try admin/admin just in case", and no retry loop:&lt;br&gt;
several vendors lock accounts after repeated failed logins, and an&lt;br&gt;
inventory tool that brute-forces its way in — however politely — would&lt;br&gt;
be a liability on a real site, not a tool. Passwords are never written&lt;br&gt;
anywhere; stream URIs are stored masked and the finished report is&lt;br&gt;
scrubbed of every password used during the scan.&lt;/p&gt;

&lt;p&gt;The whole tool is read-only, too: &lt;code&gt;Get*&lt;/code&gt; queries only, no configuration&lt;br&gt;
changes, no PTZ movement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other quirks, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ONVIF is off by default&lt;/strong&gt; on much current firmware (Hikvision
≥ 5.5.0, Axis ≥ 5.40), and the ONVIF user is usually a &lt;em&gt;separate&lt;/em&gt;
account from the web UI user. Half of all "the scanner found nothing"
is this. A zero result prints a hint saying so instead of a conclusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes are unreliable narrators.&lt;/strong&gt; Some devices broadcast
name/hardware/MAC/location; some broadcast one Profile URI and nothing
else. Identity is assembled in layers, and what's missing is reported
as missing — never guessed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firmware XML is sloppy.&lt;/strong&gt; Unbound prefixes, missing namespaces. The
parser tolerates it (and flags it) instead of dropping the device.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a longer list in the repo's&lt;br&gt;
&lt;a href="https://github.com/anguoyang/onvif-scan/blob/main/docs/vendor-notes.md" rel="noopener noreferrer"&gt;vendor-notes&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest status
&lt;/h2&gt;

&lt;p&gt;v0.1, verified end-to-end against simulated devices that model strict,&lt;br&gt;
standard, and legacy vendor behaviours (the simulator is in the repo and&lt;br&gt;
runs in CI — including the clock-skew case both ways: signed with local&lt;br&gt;
time → rejected; signed with device time → accepted). What it is &lt;em&gt;not&lt;/em&gt;&lt;br&gt;
yet is field-verified: I'm collecting real-camera reports now. If you&lt;br&gt;
run it on a real fleet, the most valuable thing you can do is file a&lt;br&gt;
device-report issue with the (redactable) JSON attached. That's how the&lt;br&gt;
vendor matrix grows.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/anguoyang/onvif-scan" rel="noopener noreferrer"&gt;https://github.com/anguoyang/onvif-scan&lt;/a&gt; — Apache-2.0, good first&lt;br&gt;
issues are up, and if it saves you an afternoon of walking a site with a&lt;br&gt;
laptop and a spreadsheet, a star helps the next integrator find it.&lt;/p&gt;

</description>
      <category>onvif</category>
      <category>python</category>
      <category>opensource</category>
      <category>iot</category>
    </item>
    <item>
      <title>I verified 127 Rockchip projects by hand so this list wouldn't rot — awesome-rockchip is live</title>
      <dc:creator>Anguo</dc:creator>
      <pubDate>Mon, 02 Nov 2026 01:50:15 +0000</pubDate>
      <link>https://dev.to/anguoyang/i-verified-127-rockchip-projects-by-hand-so-this-list-wouldnt-rot-awesome-rockchip-is-live-450l</link>
      <guid>https://dev.to/anguoyang/i-verified-127-rockchip-projects-by-hand-so-this-list-wouldnt-rot-awesome-rockchip-is-live-450l</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I maintain awesome-rockchip, and three of my own open-source projects (rk-bench, RK Edge Doctor and onvif-scan) appear in it as ordinary entries, under the same rules as everything else.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rockchip lists rot the same way every other list does
&lt;/h2&gt;

&lt;p&gt;If you develop on RK3588, you have probably landed on a Rockchip "awesome list" that looked great in a search result and turned out to be a snapshot of the ecosystem from two years ago: dead links, archived repos presented as current, and pull requests from contributors sitting unanswered for months. The content wasn't bad on day one. Nothing about a static list keeps it true on day three hundred.&lt;/p&gt;

&lt;p&gt;I ran into the sharp end of this myself. I submitted a small, correct addition to the best-known RK3588 list for one of my tools. The list has hundreds of stars and is genuinely useful — but contributions there appear to get merged in roughly annual batches, and my PR simply sat. That's not a criticism of the maintainer; it's what happens when a list has no maintenance &lt;em&gt;system&lt;/em&gt;, only good intentions.&lt;/p&gt;

&lt;p&gt;So I built the list I wished existed: &lt;strong&gt;&lt;a href="https://github.com/anguoyang/awesome-rockchip" rel="noopener noreferrer"&gt;awesome-rockchip&lt;/a&gt;&lt;/strong&gt; — 108 entries at launch, in English and Chinese, covering the whole family rather than one chip: RK3588/3588S, RK3576, RK3566/3568 and RV1106/1126.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "verified" actually meant
&lt;/h2&gt;

&lt;p&gt;I started from 127 candidates gathered from official Rockchip sources, vendor organisations, upstream projects and existing lists — then checked &lt;strong&gt;every single one&lt;/strong&gt; before it earned a place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the link resolve, and does it point at the project's own home rather than a mirror or re-upload?&lt;/li&gt;
&lt;li&gt;When was the last commit or release (checked via the GitHub API, not by eyeballing)?&lt;/li&gt;
&lt;li&gt;Is the repository archived? If so, it is either out, or in with "(archived)" printed next to its name.&lt;/li&gt;
&lt;li&gt;Is it actually about Rockchip, or a generic project that merely mentions the chip once?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nineteen candidates didn't survive: exact duplicates (the vendor kernel was a candidate in three different sections), generic upstreams whose Rockchip story is better told by a more specific entry, and tools that turned out to be tiny, stale, or superseded. Every surviving entry got an original one-sentence description, written from what the project does — no text was lifted from any existing list. The full verification ledger (last activity, stars, archived flag, both descriptions) is how the list was built, entry by entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three findings worth knowing even if you never open the list
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The most popular Ubuntu images for RK3588 are a frozen baseline now.&lt;/strong&gt; Joshua Riek's &lt;code&gt;ubuntu-rockchip&lt;/code&gt; (4,000+ stars, the images half the community started with) is archived. It still works, but anyone building on it should know it is no longer maintained. The list keeps it — honestly labelled "(archived)" — because pretending it doesn't exist would be worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The mainline story is better than most people think, and there's one page that proves it.&lt;/strong&gt; Between Mesa's Panthor driver, upstream U-Boot booting more boards every release, and Collabora's running RK3588 mainline-status tracker, "vendor kernel forever" is increasingly a choice rather than a necessity. That tracker is in the list under OS &amp;amp; Kernel, and it's the single page I now check first when someone asks "does mainline support X yet?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The NPU ecosystem has quietly grown a long tail.&lt;/strong&gt; Beyond Rockchip's official RKNN Toolkit2 and RKLLM stack, the curation surfaced Go and Rust bindings for the RKNN runtime, OpenAI-shaped API servers for RKLLM, Docker images that save you from assembling the conversion environment by hand, and vision-language models (Qwen3-VL, SmolVLM2) already running entirely on the RK3588 NPU. A year ago most of that didn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that are supposed to keep it alive
&lt;/h2&gt;

&lt;p&gt;A list is a promise about the future, so the maintenance terms are printed at the top of the README, where breaking them is visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every PR gets a first response within 48 hours&lt;/strong&gt; — merged, commented, or changes requested. Not "merged within 48 hours"; responded to. That's the part contributors actually feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Links are checked automatically every week&lt;/strong&gt; (lychee in GitHub Actions), and failures open an issue on their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A maintenance round runs every month&lt;/strong&gt;: new entries in, dead links fixed or removed, newly archived projects marked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No paid placements, ever.&lt;/strong&gt; Entries are in on merit, in a fixed one-line format, and my own projects get no special position or wording.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Will it work? The honest answer is that the system matters more than the launch. Weekly automation removes the "I forgot to check links" failure mode, and the 48-hour pledge is cheap to keep precisely because the list is small enough to actually maintain. Check back in six months and grade it — the commit history will tell you whether the pledge held.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it / contribute
&lt;/h2&gt;

&lt;p&gt;Start at the section that matches your problem: Boards &amp;amp; Vendors if you're choosing hardware, OS &amp;amp; Kernel for images and boot, RKNN/RKLLM &amp;amp; AI Toolchain plus Models &amp;amp; Deployment if the NPU is your job, Multimedia for MPP/RGA/ISP work, Development Tools for flashing and diagnostics, and Community &amp;amp; Docs for the forums and wikis that still answer questions.&lt;/p&gt;

&lt;p&gt;If you maintain or know a Rockchip project that belongs here, the contributing guide is one page: one line per entry, link must be live, project must be Rockchip-relevant and active within the last 12 months (or a stable official baseline). You'll hear back within 48 hours.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/anguoyang/awesome-rockchip" rel="noopener noreferrer"&gt;https://github.com/anguoyang/awesome-rockchip&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rockchip</category>
      <category>rk3588</category>
      <category>opensource</category>
      <category>embedded</category>
    </item>
    <item>
      <title>10 RKNN models benchmarked on RK3588 — and the fastest one is cheating onto the CPU</title>
      <dc:creator>Anguo</dc:creator>
      <pubDate>Mon, 02 Nov 2026 01:13:57 +0000</pubDate>
      <link>https://dev.to/anguoyang/10-rknn-models-benchmarked-on-rk3588-and-the-fastest-one-is-cheating-onto-the-cpu-2pkh</link>
      <guid>https://dev.to/anguoyang/10-rknn-models-benchmarked-on-rk3588-and-the-fastest-one-is-cheating-onto-the-cpu-2pkh</guid>
      <description>&lt;p&gt;Picking a model for a Rockchip board usually means trusting a vendor table&lt;br&gt;
or timing one run yourself and hoping. I wanted a leaderboard I could&lt;br&gt;
reproduce, so I built &lt;strong&gt;rk-bench&lt;/strong&gt; and measured 10 public models on an&lt;br&gt;
RK3588 (driver 0.9.8, librknnrt 2.3.2) under one frozen protocol.&lt;/p&gt;

&lt;p&gt;Three rules make the numbers worth trusting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Latency comes from the NPU runtime's own PERF_RUN hardware counters,&lt;/strong&gt;
not wall-clock time. The board was busy (load ≈ 16) the whole time;
hardware counters don't care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every model gets a CPU-fallback verdict&lt;/strong&gt; from PERF_DETAIL per-layer
accounting — the same method as RK Edge Doctor, with the same
thresholds. Input/output staging never counts as fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No number comes from a single run.&lt;/strong&gt; More on why below; it turned
out to be the most interesting part.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  The leaderboard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;p50 (ms)&lt;/th&gt;
&lt;th&gt;p95 (ms)&lt;/th&gt;
&lt;th&gt;FPS&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;RSS Δ (MB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RetinaFace Mobile320&lt;/td&gt;
&lt;td&gt;face-detect&lt;/td&gt;
&lt;td&gt;6.43&lt;/td&gt;
&lt;td&gt;6.68&lt;/td&gt;
&lt;td&gt;155.6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MIXED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MobileNetV2&lt;/td&gt;
&lt;td&gt;classify&lt;/td&gt;
&lt;td&gt;8.63&lt;/td&gt;
&lt;td&gt;8.89&lt;/td&gt;
&lt;td&gt;115.9&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;23.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ResNet50-v2&lt;/td&gt;
&lt;td&gt;classify&lt;/td&gt;
&lt;td&gt;25.40&lt;/td&gt;
&lt;td&gt;29.34&lt;/td&gt;
&lt;td&gt;39.4&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;110.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLOv8n&lt;/td&gt;
&lt;td&gt;detect&lt;/td&gt;
&lt;td&gt;37.50&lt;/td&gt;
&lt;td&gt;38.58&lt;/td&gt;
&lt;td&gt;26.7&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;47.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLOv8n-obb&lt;/td&gt;
&lt;td&gt;detect-obb&lt;/td&gt;
&lt;td&gt;37.65&lt;/td&gt;
&lt;td&gt;38.26&lt;/td&gt;
&lt;td&gt;26.6&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;44.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLOv5s (ReLU)&lt;/td&gt;
&lt;td&gt;detect&lt;/td&gt;
&lt;td&gt;37.80&lt;/td&gt;
&lt;td&gt;38.58&lt;/td&gt;
&lt;td&gt;26.5&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;52.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLO11n&lt;/td&gt;
&lt;td&gt;detect&lt;/td&gt;
&lt;td&gt;43.67&lt;/td&gt;
&lt;td&gt;49.79&lt;/td&gt;
&lt;td&gt;22.9&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;46.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLOv8n-pose&lt;/td&gt;
&lt;td&gt;pose&lt;/td&gt;
&lt;td&gt;49.73&lt;/td&gt;
&lt;td&gt;51.27&lt;/td&gt;
&lt;td&gt;20.1&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;47.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLOv10s&lt;/td&gt;
&lt;td&gt;detect&lt;/td&gt;
&lt;td&gt;88.09&lt;/td&gt;
&lt;td&gt;90.49&lt;/td&gt;
&lt;td&gt;11.4&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;78.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YOLOv8s-seg&lt;/td&gt;
&lt;td&gt;segment&lt;/td&gt;
&lt;td&gt;104.53&lt;/td&gt;
&lt;td&gt;113.08&lt;/td&gt;
&lt;td&gt;9.6&lt;/td&gt;
&lt;td&gt;NPU_ONLY&lt;/td&gt;
&lt;td&gt;112.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All FP, official RKNN Model Zoo conversions (seven pre-converted zoo&lt;br&gt;
packages; MobileNetV2 / ResNet50-v2 / RetinaFace converted from the zoo&lt;br&gt;
ONNX with Toolkit2 2.3.2 using the zoo's own parameters). Full table with&lt;br&gt;
model sizes and per-frame memory traffic:&lt;br&gt;
&lt;a href="https://github.com/anguoyang/rk-bench/blob/main/docs/LEADERBOARD.md" rel="noopener noreferrer"&gt;docs/LEADERBOARD.md&lt;/a&gt;.&lt;br&gt;
Raw data is one JSON file — the single source of truth the tables are&lt;br&gt;
generated from.&lt;/p&gt;
&lt;h2&gt;
  
  
  Finding 1: the fastest model is the only one on the CPU
&lt;/h2&gt;

&lt;p&gt;RetinaFace Mobile320 is the quickest model on the board — and the only&lt;br&gt;
one that isn't fully on the NPU. Five &lt;code&gt;Reshape&lt;/code&gt; operators around its&lt;br&gt;
detection-head concats run on the CPU: 196 µs, 4.05% of frame time.&lt;br&gt;
Cheap in absolute terms, and you'd never see it from end-to-end timing.&lt;br&gt;
Verdict: &lt;code&gt;MIXED&lt;/code&gt;. The other nine are clean &lt;code&gt;NPU_ONLY&lt;/code&gt;, including all the&lt;br&gt;
YOLO variants.&lt;/p&gt;
&lt;h2&gt;
  
  
  Finding 2: your first benchmark run is ~15% too slow
&lt;/h2&gt;

&lt;p&gt;While validating the protocol I hit something I haven't seen documented:&lt;br&gt;
&lt;strong&gt;the first RKNN process after an idle gap measures ~15% slower across&lt;br&gt;
every single run&lt;/strong&gt; — YOLO11n at 51.4 ms instead of 44.0 ms, 50 runs in a&lt;br&gt;
row, all slow. Warmup inside that process does not fix it. Start a&lt;br&gt;
second process and the speed is back.&lt;/p&gt;

&lt;p&gt;The board uses the &lt;code&gt;rknpu_ondemand&lt;/code&gt; governor, and the effect smells like&lt;br&gt;
first-context state at the driver level; I can't prove the mechanism&lt;br&gt;
from outside, but I can prove the cure, because rk-bench now does it by&lt;br&gt;
default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;priming cycle&lt;/strong&gt; (one sacrificial load + 5 runs in a throwaway
process) before measuring, and&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;three independent sessions&lt;/strong&gt; (separate processes), publishing the
median p50/p95 and the worst verdict. The three session p50s are
printed in every entry's notes, so the spread is auditable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This also explained a nagging discrepancy: an earlier single-process&lt;br&gt;
health table (RK Edge Doctor's) had YOLO11n at 52.9 ms. It wasn't wrong —&lt;br&gt;
it was measuring the first-context state. Single-process benchmarks of&lt;br&gt;
RK3588 should be treated with suspicion, including, now, my own older&lt;br&gt;
ones; that table has moved into rk-bench and links here.&lt;/p&gt;

&lt;p&gt;A related trap: even primed, a single session can sit off the pace&lt;br&gt;
(YOLOv8s-seg produced sessions of 104.4 / 114.3 / 104.5 ms). Median of&lt;br&gt;
three is what keeps one bad minute on a busy board out of the table.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Zero dependencies — Python 3.9+ standard library and ctypes against the&lt;br&gt;
board's own &lt;code&gt;librknnrt.so&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/anguoyang/rk-bench
&lt;span class="nb"&gt;cd &lt;/span&gt;rk-bench
&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 &lt;span class="nt"&gt;-m&lt;/span&gt; rkbench run yolov8n.rknn &lt;span class="nt"&gt;--name&lt;/span&gt; YOLOv8n &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--task&lt;/span&gt; detect &lt;span class="nt"&gt;--soc&lt;/span&gt; rk3588 &lt;span class="nt"&gt;--json&lt;/span&gt; report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got a model or a board that isn't on the table? Run it and open a&lt;br&gt;
bench-report issue with the JSON — reports whose runtime/driver differ&lt;br&gt;
from a board's table start a new table instead of mixing in (RK3576 is&lt;br&gt;
the seeded next board). INT8 twins of the YOLO rows are the other big&lt;br&gt;
open item.&lt;/p&gt;

&lt;p&gt;If you benchmark RKNN models, I'd genuinely like to know: have you seen&lt;br&gt;
the first-process effect on your board, and does your methodology defend&lt;br&gt;
against it?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I maintain rk-bench and RK Edge Doctor; both are Apache-2.0&lt;br&gt;
and exist to drive traffic to my RK integration work — the data,&lt;br&gt;
however, stands on its own and every number above is reproducible from&lt;br&gt;
the repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rockchip</category>
      <category>rknn</category>
      <category>benchmark</category>
      <category>edgeai</category>
    </item>
    <item>
      <title>Before you convert that ONNX model to RKNN, check whether it can convert at all</title>
      <dc:creator>Anguo</dc:creator>
      <pubDate>Sun, 01 Nov 2026 22:47:12 +0000</pubDate>
      <link>https://dev.to/anguoyang/before-you-convert-that-onnx-model-to-rknn-check-whether-it-can-convert-at-all-31me</link>
      <guid>https://dev.to/anguoyang/before-you-convert-that-onnx-model-to-rknn-check-whether-it-can-convert-at-all-31me</guid>
      <description>&lt;p&gt;The standard RKNN conversion experience goes like this: set up the Toolkit environment, write the conversion script, run it, wait, read a wall of log, and discover — forty minutes in — that your model has a dynamic batch dimension, or an op the converter places badly, or an opset the toolchain doesn't love. None of those are conversion problems. They're &lt;em&gt;pre-flight&lt;/em&gt; problems, discovered at landing time.&lt;/p&gt;

&lt;p&gt;So I added a &lt;code&gt;convert&lt;/code&gt; trio to &lt;a href="https://github.com/anguoyang/rk-edge-doctor" rel="noopener noreferrer"&gt;RK Edge Doctor&lt;/a&gt; (zero-dependency, Python stdlib only). It deliberately does &lt;strong&gt;not&lt;/strong&gt; convert anything — the official RKNN Toolkit2 does that. It does the three things around the conversion:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;convert check&lt;/code&gt; — pre-flight.&lt;/strong&gt; A hand-rolled ONNX structure parser (raw protobuf wire format, no &lt;code&gt;onnx&lt;/code&gt; package) reads opset, input shapes and the op histogram, then checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dynamic input dimensions (fix them &lt;em&gt;before&lt;/em&gt; quantization, not after)&lt;/li&gt;
&lt;li&gt;control-flow operators — Loop / If / Scan cannot become a static RKNN graph, verdict &lt;code&gt;LIKELY_TO_FAIL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;operators known to need attention in Toolkit2 (NonMaxSuppression, Resize, GridSample, …) with the reason for each&lt;/li&gt;
&lt;li&gt;quantization-sensitive tails (Sigmoid/Softmax at the graph end under int8)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verdict: &lt;code&gt;READY / RISKY / LIKELY_TO_FAIL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;convert recipe&lt;/code&gt; — a script worth running.&lt;/strong&gt; Generates a ready-to-run Toolkit2 conversion script: target platform, int8 config + calibration dataset if you asked, and &lt;code&gt;input_size_list&lt;/code&gt; pinned from the ONNX inputs themselves. You run it in your own Toolkit environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;convert verify&lt;/code&gt; — post-conversion acceptance.&lt;/strong&gt; The existing &lt;code&gt;model&lt;/code&gt; diagnosis (CPU fallback list, PERF-counter latency, ONNX output drift) framed as the verification step.&lt;/p&gt;

&lt;p&gt;First contact with a real file was humbling in the right way. I tested the checker on the official Ultralytics YOLOv8n ONNX export (PyTorch, opset 20, 233 nodes) and it immediately failed — because my parser assumed the textbook ModelProto field layout, and real PyTorch exports arrange producer/opset fields differently. Fixed by identifying fields by content, not by faith, and the same file now gets an honest verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ rkdoctor convert check yolov8n.onnx --target rk3588
+ parse: 233 nodes (opset 20, producer pytorch)
! opset: 20 (outside the commonly used 11-19 range)
! caution_ops: Resize (some modes convert with restrictions)
+ dynamic_dims: static
+ weights: 12.78 MB
RISKY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the correct answer for that file, and it's the answer you want &lt;em&gt;before&lt;/em&gt; the Toolkit run, not after.&lt;/p&gt;

&lt;p&gt;With this release the tool's planned surface is complete: &lt;code&gt;board&lt;/code&gt; (16 hardware checks), &lt;code&gt;model&lt;/code&gt; (CPU-fallback diagnosis), &lt;code&gt;rtsp&lt;/code&gt; (stream acceptance), and now &lt;code&gt;convert&lt;/code&gt;. The &lt;a href="https://github.com/anguoyang/rk-edge-doctor/blob/main/docs/MODEL_HEALTH.md" rel="noopener noreferrer"&gt;model health table&lt;/a&gt; keeps growing weekly — custom conversions that check out RISKY, or run MIXED/CPU_HEAVY, are exactly the reports I want in issues.&lt;/p&gt;

</description>
      <category>rockchip</category>
      <category>rknn</category>
      <category>opensource</category>
      <category>embedded</category>
    </item>
    <item>
      <title>Your IP camera connects fine. Is the stream actually healthy?</title>
      <dc:creator>Anguo</dc:creator>
      <pubDate>Sun, 01 Nov 2026 22:40:48 +0000</pubDate>
      <link>https://dev.to/anguoyang/your-ip-camera-connects-fine-is-the-stream-actually-healthy-5356</link>
      <guid>https://dev.to/anguoyang/your-ip-camera-connects-fine-is-the-stream-actually-healthy-5356</guid>
      <description>&lt;p&gt;Every IP camera "works" when you open it in a player. The stream connects, a picture shows up, everyone moves on. Then the recorder has gaps, the analytics box misses frames, or the "live" view turns out to be a frozen picture from an hour ago — and nothing in your tooling can tell you which property of the stream is broken, because "it connects" was the only test.&lt;/p&gt;

&lt;p&gt;I added an &lt;code&gt;rtsp&lt;/code&gt; command to &lt;a href="https://github.com/anguoyang/rk-edge-doctor" rel="noopener noreferrer"&gt;RK Edge Doctor&lt;/a&gt; (zero-dependency, Python stdlib only) that treats a camera stream like an acceptance test instead of a preview:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reachability and SDP negotiation (codec, payload, clock) — Basic and Digest auth supported&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;time to first frame&lt;/strong&gt; after PLAY&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;measured frame rate&lt;/strong&gt;, with &lt;code&gt;--expect-fps / --expect-width / --expect-height&lt;/code&gt; assertions for acceptance runs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTP packet loss&lt;/strong&gt; from sequence numbers, &lt;strong&gt;inter-arrival jitter&lt;/strong&gt;, bitrate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTP timestamp health&lt;/strong&gt; — regressions and gaps, the silent killers of recordings and analytics&lt;/li&gt;
&lt;li&gt;an optional &lt;strong&gt;decode spot-check&lt;/strong&gt; for H.264 (when OpenCV is present): reassembles the bitstream from RTP and flags black or frozen pictures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verdict: &lt;code&gt;HEALTHY / DEGRADED / BROKEN&lt;/code&gt;, as JSON + HTML reports you can archive as acceptance evidence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ rkdoctor rtsp rtsp://user:pass@cam/stream --duration 15 --expect-fps 25
+ reachability: 8 ms
+ negotiation: H264 (payload 96, clock 90000)
+ first_frame: 15 ms
+ loss: 0.000 %
+ jitter: 0.8 ms
+ fps: 25.00 fps (measured vs expected 25.00 fps)
+ timestamps: 0 regressions, 0 gaps
+ decode: 90 frames decoded (frames decode and the picture changes over time)
HEALTHY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two implementation notes. First, there is no ffmpeg/ffprobe involved: the RTSP client is hand-rolled on stdlib sockets (Digest auth included), so the tool runs on a bare board image. Second, &lt;strong&gt;the password in the URL is redacted everywhere&lt;/strong&gt; — terminal output, JSON, HTML. An acceptance tool that leaks camera credentials into its own reports would fail its own acceptance.&lt;/p&gt;

&lt;p&gt;Verified end-to-end on an RK3588 board against a synthetic local stream (640×480 H.264 hardware-encoded on the same board, served over UDP, TCP-interleaved, and Digest auth): all three sessions HEALTHY, 0% loss, exactly 25.0 fps. That validates the client; what I really want is reports from &lt;em&gt;real&lt;/em&gt; cameras — the weird vendors, the firmware that sends timestamps from 1970, the "25 fps" cameras that send 12. If you run it against a camera you own, the JSON report (credentials already redacted) is very welcome in an issue.&lt;/p&gt;

&lt;p&gt;Also new this week: a &lt;a href="https://github.com/anguoyang/rk-edge-doctor/blob/main/docs/MODEL_HEALTH.md" rel="noopener noreferrer"&gt;model health table&lt;/a&gt; for the &lt;code&gt;model&lt;/code&gt; command — five public YOLO variants measured on RK3588, all NPU_ONLY so far. Custom conversions that come out MIXED or CPU_HEAVY are the rows I actually want to add.&lt;/p&gt;

</description>
      <category>rockchip</category>
      <category>rtsp</category>
      <category>opensource</category>
      <category>embedded</category>
    </item>
    <item>
      <title>Is your RKNN model secretly running on the CPU? I built a tool that tells you</title>
      <dc:creator>Anguo</dc:creator>
      <pubDate>Sun, 01 Nov 2026 11:38:45 +0000</pubDate>
      <link>https://dev.to/anguoyang/is-your-rknn-model-secretly-running-on-the-cpu-i-built-a-tool-that-tells-you-3n6p</link>
      <guid>https://dev.to/anguoyang/is-your-rknn-model-secretly-running-on-the-cpu-i-built-a-tool-that-tells-you-3n6p</guid>
      <description>&lt;p&gt;Converting a model to RKNN and watching it "run" on a Rockchip board proves exactly one thing: it runs. It says nothing about &lt;em&gt;where&lt;/em&gt; it runs. RKNN will happily execute unsupported or badly-quantized layers on the CPU, silently, while your benchmarks just look "a bit slower than expected." In my own testing I have seen a SAM-variant conversion whose decoder carried a dozen CPU ops — the model worked, the demos worked, and a chunk of it was never touching the NPU.&lt;/p&gt;

&lt;p&gt;Until now, checking meant spelunking through runtime perf dumps by hand. So I added a &lt;code&gt;model&lt;/code&gt; command to &lt;strong&gt;RK Edge Doctor&lt;/strong&gt; (the board health checker I released earlier this month) that answers it in one run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/anguoyang/rk-edge-doctor.git
&lt;span class="nb"&gt;cd &lt;/span&gt;rk-edge-doctor
&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 &lt;span class="nt"&gt;-m&lt;/span&gt; rkdoctor model model.rknn &lt;span class="nt"&gt;--json&lt;/span&gt; report.json &lt;span class="nt"&gt;--html&lt;/span&gt; report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output, from a public YOLOv8n (FP16) on my RK3588 board:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model: yolov8n_det.rknn | Verdict: NPU_ONLY
Latency us: p50=40933 p95=41523 mean=41096.3 (runs=20)
CPU fallback operators: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;108 layers measured. Total layer time 38.2 ms; CPU time 203 µs — and every microsecond of that is the input/output staging operators, which live on the CPU by design. The report lists them separately so the tool doesn't cry wolf: staging is structural, everything else on CPU is fallback, and the verdict comes from the fallback &lt;em&gt;share of time&lt;/em&gt;, not from op counts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NPU_ONLY&lt;/code&gt; — fallback under 1% of layer time, and no single fallback op ≥ 100 µs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MIXED&lt;/code&gt; — fallback between 1% and 20%&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CPU_HEAVY&lt;/code&gt; — fallback over 20% (threshold adjustable, and recorded in the report)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UNKNOWN&lt;/code&gt; — the tool couldn't run the model, and says why instead of guessing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works (and the trap that cost me an hour)
&lt;/h2&gt;

&lt;p&gt;No rknnlite, no toolkit install, no pip anything. The tool binds &lt;code&gt;librknnrt&lt;/code&gt; directly through ctypes — the runtime library is the one thing every RKNN board already has — runs warmup plus measured inferences with a zero input, and reads the runtime's own accounting: &lt;code&gt;PERF_RUN&lt;/code&gt; for per-run duration and &lt;code&gt;PERF_DETAIL&lt;/code&gt; for the per-layer table with each layer's target (NPU/CPU) and time.&lt;/p&gt;

&lt;p&gt;Two details worth knowing if you ever do this yourself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Latency is hardware time, not wall clock.&lt;/strong&gt; The board I measured on runs production services at load average ~16. &lt;code&gt;PERF_RUN&lt;/code&gt; comes from the runtime's counters, so p50/p95 stay meaningful on a busy board. The report still records the host load average, because honesty is kind of the theme here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PERF_DETAIL&lt;/code&gt; is empty unless you ask at init time.&lt;/strong&gt; You must pass the collect-performance flag (&lt;code&gt;RKNN_FLAG_COLLECT_PERF_MASK&lt;/code&gt;) to &lt;code&gt;rknn_init&lt;/code&gt;. Forget it and the query succeeds, returns zero bytes, and tells you nothing — no error, just silence. Ask me how I know.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;The official model-zoo conversions I tested (YOLOv8n, YOLOv8s-seg) come out clean: &lt;code&gt;NPU_ONLY&lt;/code&gt;. That's expected — they are the reference conversions. The fallback problem shows up with &lt;em&gt;your&lt;/em&gt; model: a custom architecture, an exotic op, a quantization choice the compiler didn't like. That's exactly the case where you currently have no easy way to look.&lt;/p&gt;

&lt;p&gt;So the most valuable thing you can do with this release is not run it on a zoo model — it's run it on the weird one. If you get a &lt;code&gt;MIXED&lt;/code&gt; or &lt;code&gt;CPU_HEAVY&lt;/code&gt; verdict, the JSON report (which operators, how many microseconds each) is the whole story in one file; sharing it in an issue helps everyone, and reports record the model file name and hash only, never its path.&lt;/p&gt;

&lt;p&gt;v0.2 also keeps everything from v0.1: &lt;code&gt;rkdoctor board&lt;/code&gt; runs 16 board health checks (SoC, rknpu driver, runtime version, thermals…) and emits the same style of JSON/HTML report. Reports from boards other than RK3588 — RK3588S (Orange Pi 5, ROCK 5B), RK3566/3568, RK3576 — are still the contribution I need most.&lt;/p&gt;

&lt;p&gt;Repo (Apache-2.0, Python stdlib only): &lt;a href="https://github.com/anguoyang/rk-edge-doctor" rel="noopener noreferrer"&gt;https://github.com/anguoyang/rk-edge-doctor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next up: v0.3 turns the same reporting machinery to RTSP camera acceptance — real frame rate, frame-gap jitter, freeze detection — the "attach it to the delivery document" test for cameras.&lt;/p&gt;

</description>
      <category>rockchip</category>
      <category>rk3588</category>
      <category>opensource</category>
      <category>embedded</category>
    </item>
  </channel>
</rss>
