Skip to content

Say how fast it is, and name what carries it - #127

Merged
openipc-ai merged 15 commits into
masterfrom
feat/low-latency-numbers
Aug 28, 2026
Merged

Say how fast it is, and name what carries it#127
openipc-ai merged 15 commits into
masterfrom
feat/low-latency-numbers

Conversation

@openipc-ai

@openipc-ai openipc-ai commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Three commits, all on the low-latency story: the numbers, the project that
carries the link, and the entry that describes that project.


1. Say how fast it is, in four bars

The latency table on /low-latency was unchanged 2022 announcement copy.
Users have pasted the exact sentence back into the chats twice asking how to
reach it. Measured against a 2026 audit of the OpenIPC and wfb-ng archives
(923,581 messages):

Configuration Page claimed Actually reported
720p60 ~60 ms 100–150 ms
1080p60 ~80 ms 95–150 ms
1080p30 ~100 ms 150–260 ms

And the hero undersold us by half: "from about 60 ms" states a floor, and
the best-measured figure on this stack is 26 ms.

Resolution was also the wrong axis — it is nearly free. The same camera at
720p45 moves 120 ms on the monitor alone.

  • Hero: 60 ms → 30 ms, here and on the homepage pillar card, which
    carried the same sentence.
  • The three-row resolution table becomes four bars keyed on receive
    path
    — dedicated ground station, goggles with a built-in VRX, phone or
    tablet, laptop or desktop — each spanning the lowest and highest figures
    users report. One measure on one shared scale, so one hue rather than
    four: the reader is comparing magnitude, not telling series apart.
  • The "measured with our own open hardware latency meter" claim is gone.
  • 56 translation keys down to 30.

It is a landing page, so it gets the claim and the evidence for it. The
audit supports a great deal more — a definition of glass-to-glass, the full
monitor sweep, the 66–99 ms an HDMI capture adapter costs before a camera is
attached — and that is wiki material.

2. Name our own link on the card that describes the radio link

The radio-link card credited wfb-ng. devourer is our own in-place
replacement for it, in the same chain: waybeam_venc on the camera,
devourer over the air, PixelPilot or Aviateur on the ground. So that is what
the card and the TX → … → RX line under it now name.

What makes it worth naming is where it runs: devourer drives the Realtek
adapter from userspace over libusb, with no kernel module in the path, which
is what lets it set rate, power and channel per frame rather than per
association. Adaptive link, frequency hopping and narrowband channels all
fall out of that.

The credits still thank both — "The radio link exists thanks to wfb-ng and
devourer" — and the provenance line under the bars still reads "the OpenIPC
and wfb-ng chats", because that is where the figures were collected.

3. Say what devourer is on the page that lists it

/ecosystem described it as "an open foundation for SDR-like receivers
built on cheap Wi-Fi hardware" and badged it R&D. It is not a foundation
for receivers, it is the link; and R&D means "we have the SDK and are
starting to study the platform", which is the wrong end of the lifecycle for
something PixelPilot ships on Android across five Realtek hardware backends.
Now The link itself: a userspace driver for Realtek Wi-Fi adapters, setting rate, power, and channel packet by packet. at DONE, and moved into
chain order so the cards read camera → air → ground.


Two things deliberately not done

No source links on the figures. Every one is a user report living at a
t.me/c/... URL, which resolves only for members of those private groups
and would 404 for a visitor. The reporters are also real people who have not
been asked whether they want their names on the marketing site. A test
asserts no t.me/c/ link ever appears.

The meter claim is removed, not restated. The meter exists, but its
design, method and raw runs are not published, and these figures are not its
output.

Verification

bin/rails test 309 runs, 1466 assertions, 0 failures
i18n-tasks missing none, across en/ru/zh
i18n-tasks unused 72 — unchanged
rendered at 1280 / 768 / 390 en/ru/zh, no page errors, no 4xx, no page overflow
deployed dev.openipc.org, 0 5xx

rubocop still cannot start on this branch — .rubocop.yml requires
rubocop-performance and the Gemfile does not list it. That is the
pre-existing defect #120 fixes.

The Chinese copy is mine and has not been read by a native speaker. The
DONE badge on devourer is my judgement from its README and its use in
PixelPilot — say the word if it should be WIP or MVP.

openipc-ai and others added 7 commits August 25, 2026 15:02
The site's CSS was one 200-line file appended to Bootstrap, and the order it
was written in hid a bug. Our theme values landed *after* the Bootstrap import,
where Bootstrap's own !default declarations had already won, so $primary stayed
at Bootstrap's #0d6efd while :root separately declared --bs-blue: #4c60d8. The
page carried two blues: buttons and links in one, headings and the navbar in the
other. Nobody had written the wrong colour anywhere -- the file just could not
express the intent in that order.

So the entry point is now only an ordering statement: fonts, Bootstrap's
functions, our variables, Bootstrap, then our own partials. --bs-primary and
--bs-btn-bg both compile to #4c60d8 now. $blue is set alongside $primary because
--bs-blue is what the pre-relaunch rules colour headings with, and leaving it at
the default would have reproduced the same split from the other side.

The partials that arrive here are the ones that style pages which exist today.
Components and page styles belonging to the new pages come with those pages,
rather than sitting in the tree as dead CSS in the meantime. pages/_legacy.scss
holds the rules that belong to the pre-relaunch pages, kept verbatim and kept
together so the cutover can delete a file instead of picking rules apart. One of
them is the CSS that hides the Russian integrators from non-Russian visitors on
/introduction: the replacement is a Ruby helper, but until that page goes this
rule is the only thing enforcing it, and dropping it early would show that logo
wall to everyone.

The other legacy rule worth naming is the h2 title style. _base.scss gives
article headings the new treatment; the legacy selector is longer and therefore
wins, and it applies exactly to pages rendered inside the .container wrapper --
which is every page that is not full-bleed. The two rules split along the same
line the layout's fullwidth switch will.

FONTS. The stylesheet opened with three @import url() lines: two to
fonts.googleapis.com and one to cdn.jsdelivr.net. Every visitor announced
themselves to two third parties before the page could paint, and first paint sat
behind DNS lookups we do not control. This is the same argument that moved the
legacy logo off a maintainer's personal CDN in d60729a. IBM Plex and
bootstrap-icons are now served from public/fonts, subset to latin, latin-ext and
cyrillic; Chinese falls back to the system stack, because IBM Plex has no CJK
glyphs and the families that do are megabytes. The files are committed, since
public/ bypasses Sprockets and a generated file would have to exist before
assets:precompile in the image and before CI renders a page, for no benefit.
tools/copy-fonts.mjs regenerates them from the @fontsource packages and fails
loudly if a file the stylesheet asks for is missing.

JAVASCRIPT. One window.onload handler becomes modules under app/javascript/src.
That handler assigned window.onload rather than adding a listener, so any second
assignment would have silently replaced all of it, and it waited on every image
-- on the Open Wall, the whole gallery -- before anything became interactive.
It is DOMContentLoaded now.

The zoom initialiser called `new bootstrap.Modal(document.getElementById(...))`
unconditionally. Most pages have no zoomable image and so no #modalZoom, the
constructor threw on null, and because every initialiser shared the one handler,
the throw took external links, timestamps and confirmations down with it on
those pages. It returns early now.

@hotwired/turbo-rails and @hotwired/stimulus are removed: neither was ever
imported. The layout's data-turbo-track goes with them, having been inert. The
lockfile is regenerated with Yarn 4 -- it must never be rewritten by a v1 yarn,
which produces a format `yarn install --immutable` in the Dockerfile cannot read.

Bootstrap is imported per component rather than wholesale. 189.4 KB to 181.8 KB
unminified, which is a small win; the point is the import list, which now states
what the markup actually depends on. Audited against the views first: dropdown,
collapse, offcanvas, carousel and Modal are used, and nothing uses tooltip,
popover, tab, alert or scrollspy.

Verified: 231 runs, 873 assertions, 0 failures. Diffing the compiled selector
set against master's leaves exactly one difference -- label.required:after,
which is now spelled ::after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The <head> carried a title and nothing else. A link to openipc.org shared in
Telegram, in a forum post, or shown in a search result therefore appeared as a
bare URL with no summary and no image -- for a project whose reach is almost
entirely word of mouth between people posting links to each other.

Added: a description, an OG/Twitter card set, a canonical, and a theme colour.
Three of those have a detail worth keeping:

The canonical carries the path and not the query string. ?locale=ru is a
rendering of the same page rather than a different one, and pointing all three
translations at one canonical is what stops them competing with each other for
the same content.

og:image is absolute, because crawlers do not resolve a relative one, and it
lives in public/ rather than the asset pipeline. OG images are fetched once and
cached by URL by every consumer that has ever seen the link, so a fingerprint in
the name would invalidate those caches for no gain.

The two font faces every page renders body text in are preloaded. crossorigin is
required even though they are same-origin -- fonts are fetched in CORS mode, and
a preload without it is simply fetched a second time.

The navbar and footer move into layouts/ partials. Nothing about them changes:
same links, same order, same locale switcher, and the footer's icons still
resolve through asset_path, which is not cosmetic -- production runs with
config.assets.compile = false, so a literal /assets/... path is a 404, and
2ec9baa fixed exactly these lines. Extracting them now means the relaunch can
rewrite the navigation in one file a reviewer can read, instead of inside a diff
that also touches <head>.

<main> gains a content_for(:fullwidth) switch for pages that lay out their own
full-bleed sections. Flash messages keep the container in both branches, or they
would run edge to edge. Pages that set nothing keep the wrapper they have always
had, and a test holds that.

The tests are the ones that would have caught each way a <head> goes wrong: a
missing or duplicated description, a description that came back as "translation
missing", a canonical that kept the query string, a relative og:image, an
og-default.png that is not there, and a preload naming a font file the
stylesheet does not generate.

238 runs, 903 assertions, 0 failures. i18n-tasks reports no missing keys and
102 unused, unchanged -- the new key being used is what keeps that number flat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
Seven pages -- home, get-started, low-latency, ecosystem, business, community,
donate -- plus the shared partials they are built from. They answer on their own
URLs from this commit, but the root route and the navigation still serve the
pre-relaunch structure, so nothing a visitor sees changes. The cutover is a
separate change, and this one can go to dev.openipc.org and be looked at first.

The copy is written in all three locales the site now serves. English and
Russian are as drafted; the Chinese was written for this commit and has not been
read by a native speaker -- it should be, before it is linked to. i18n-tasks
reports 347 keys in each of the three files and no missing translations.

Not everything from the draft survived contact with the current tree, and the
rendering tests are what found each one.

/get-started hardcoded four WebUI screenshots by filename --
webui/preview.jpg and three others. The gallery was reshot at twice the
resolution in 0f42514 and every one of those files stopped existing; the page
raised on render. It now takes them from WebuiGallery, the same manifest
/web-interface reads and the same one tools/webui-gallery photographs from, so
the two cannot disagree about what exists. Tile and full-resolution copy are
passed as a pair, which is what data-zoom in src/zoom.js is for.

Every link in the new copy that pointed at wiki.openipc.org now points at
github.com/OpenIPC/wiki. That host is retired; 2842ffa and 73c0961 removed the
last references to it and this copy would have put four of them back.

The partner wall moves out of the markup into PagesHelper, transcribed from
/introduction as it stands today rather than from the June draft, which was ten
entries out of date. Entries that page has commented out are commented out here
too, with their URLs, so nothing appears or disappears silently when
/introduction goes.

  REVIEW POINT: the Russian integrator block is currently commented out on
  /introduction, so no visitor sees any of it. Rendering it for :ru brings it
  back. That is what the relaunch plan asks for and it is gated to :ru as the
  maintainers asked, but it is a content decision -- say so if it should stay
  hidden.

Worth knowing: the CSS that was supposed to enforce that gate,
`html:not([lang="ru"]) article.introduction .col.logo.ru`, has never matched
anything. No logo on that page carries the `ru` class it selects on. The gate
being in Ruby now is what makes it testable, and there is a test.

/open-wall is uncommented. It was commented out in ed0e025, a bulk tidy-up,
while five places that redirect to it were left in place -- twice in
snapshots_controller, once in admin/snapshots_controller, and the breadcrumb on
three views. All five fell through the catch-all and answered a 302 to the
homepage. This exposes nothing new: `resources :snapshots` has served the same
gallery at /snapshots throughout.

Snapshot.latest_per_camera lifts the correlated subquery out of
SnapshotsController#index, because the homepage mosaic wants the same list and
two copies of that query is one too many.

pages.donate belonged to the band rendered under every page. It is now the
/donate page's, and the band's two keys moved to pages.donate_band -- otherwise
lazy lookup in the partial and the new page's title would have fought over the
same key, and the band would have lost its please_support string.

272 runs, 1223 assertions, 0 failures. Each page is rendered in each of the
three locales and checked for "translation missing"; every internal link on
them is followed and required not to land on the catch-all; the homepage is
rendered against an empty database, which is what a fresh checkout has. rubocop
is unchanged at 53 offences across the files this touches, and the two new files
add none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The root becomes pages#home, the navigation becomes Get Started | Hardware |
Low Latency | Ecosystem | For Business | Community, and the single copyright
line becomes a four-column footer. Everything that has been building up over the
last four changes is now what a visitor sees.

The pre-relaunch URLs redirect rather than disappear. They are in search
results, in forum posts and in the wiki, and none of that is ours to edit: 301
for the ones whose destination is settled, so what is indexed transfers instead
of competing, and 302 for /about alone, because it is meant to become a page of
its own and a 301 is cached by browsers indefinitely -- it would outlive the
decision. There is a test per redirect, and a test that every target resolves,
because a redirect to a path that has itself gone still answers 301 here and
lands the visitor on the homepage.

TWO ROUTES THAT ANSWERED 500. Building the new menu meant looking at every URL
it would link, and two of them had never worked:

  /tools/bandwidth-calculator routed to pages#bandwidth_calculator. There is no
  such action and no such template. Every request raised
  AbstractController::ActionNotFound, which in production is a 500.

  /tools/timelaps-interval-calculator routed to pages#timelaps_interval_
  calculator, which likewise does not exist -- the template is there but named
  with hyphens, so nothing could render it, and it has no translations either.

Both routes are removed, so an inbound link now reaches the homepage rather than
an error page. Neither page is linked from the new navigation, and the
unfinished timelapse template is left in the tree: finishing it is a decision
for whoever started it, not something to settle with a routing change.

The upload API keeps its own test. Cameras POST to /snapshots and the gallery
moved to /open-wall; the API did not move at all, and nothing in the relaunch
may shadow it.

That test found a third thing. Snapshot read
Rails.application.credentials.mac.blacklisted directly, so without
config/master.key -- which is what a fresh checkout and the test environment
have -- credentials.mac is nil and every upload raised NoMethodError rather than
simply having nothing to blacklist. Production has the key, so it never showed
there; it made the API impossible to exercise anywhere else. It digs now.

The pages that were replaced are deleted, along with their translations and the
navigation entries that pointed at them: i18n-tasks unused goes from 102 to 72,
below where it started, rather than up. pages/_legacy.scss loses two of its
three rules with the pages they styled -- including the CSS territory gate that
never matched anything.

Two white stripes showed up in the rendered page where one dark band met the
next: the CTA band and the footer both carried a top margin on top of their own
padding. Both dropped.

289 runs, 1276 assertions, 0 failures. i18n-tasks reports no missing
translations across en, ru and zh. rubocop is 734 offences over 114 files
against 742 over 111 on master. Every page was rendered in a browser in all
three languages before this was committed, which is how the footer stylesheet
turned out never to have been imported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
An audit of the June working tree against what the five preceding changes
actually landed found 20 files unaccounted for. Seventeen were deliberate and
are explained in those changes -- the concern upstream rewrote better, the
locale files for languages no longer served, a Yarn 1 lockfile, a schema dump.
Three were not deliberate. They were missed.

MERCHANDISE. The page advertises a single T-shirt three times: the same image,
the same .shirt_1 keys and the same link, pasted into three cards. It has read
as a catalogue of one product in triplicate on the live site for as long as the
file has existed. One card now, and a test that counts them.

THE OPEN WALL SAYS WHAT IT IS. The wall is in the navigation and the footer
since the cutover, so it now gets visitors who have never seen it and cannot
tell from a grid of stills whether these images were collected or volunteered.
A line under the heading says they were volunteered, and that they are deleted
after two days -- which is PurgeImagesJob::RETENTION, checked rather than
assumed.

THE SURVIVING PAGES HAD NO TESTS. /green_life, /merchandise,
/stages-of-firmware-development, /utilities and the partition and timer
calculators are served by the app and were rendered by no test at all. They are
the pages nobody thinks about, and they render through the layout, navbar and
footer that the relaunch rewrote -- so a mistake in any of those shows up there
first, or not at all. Ten of them are rendered now, checked for their own title
and for translation-missing, and held to the layout wrapper, which pins the
other side of the content_for(:fullwidth) switch against pages that depend on
it.

303 runs, 1390 assertions, 0 failures. i18n-tasks reports no missing
translations and 72 unused, unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The existing link test walks whatever the page renders and requires each href to
resolve. That cannot notice a link which stopped being rendered at all -- a
pillar card losing its href, or a whole section dropped in a refactor -- because
a page with fewer links still passes.

This is the one assertion from the June test file that the two replacements did
not carry between them.

304 runs, 1404 assertions, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The terminal block on /get-started ended at
"curl -L -o /tmp/ipctool https://github.com/OpenI". The rest of the line was
behind a horizontal scrollbar, which most systems do not draw until you touch
the element, so there was nothing on the page to suggest the command continued
at all.

This is the command the page asks a reader to paste into a root shell on their
camera. Being able to read it to the end before running it is the reason for
showing the command rather than linking a script.

The block was in a one-third column, and the ipctool release URL is wider than
that column. It moves under the three steps and takes the full width of the
article, where the two commands are two lines at desktop width. The stylesheet
also wraps instead of scrolling now, which is what handles the narrow viewports
where no layout change can help: at 390px the command runs to four lines, all of
them visible.

The copy button copies textContent rather than innerText. innerText is the
rendered text, and with the block now soft-wrapping a long URL there is no
guarantee an engine will not fold a visual break into the string. textContent is
the markup's own text: the command, with only the newlines that were written.

A test holds the command to the page whole -- the tempting fix for a clipped
line is to shorten what is displayed, and that would leave the page telling
people to run something other than what works. The URL was checked while
fixing this and answers 200.

Measured at 1280, 820 and 390 px: nothing clipped at any of them.
305 runs, 1411 assertions, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Replace latency claims with audited real-world measurements

📝 Documentation ✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Replaces resolution-based latency claims with audited ranges grouped by receive path.
• Explains frame-rate, display-refresh, glass-to-glass, and HDMI capture latency across three
 locales.
• Adds integration coverage preventing stale claims, private links, and unsupported meter
 attribution.
Diagram

graph TD
  Reports["Community reports"] -->|measurements| Datasets["Latency datasets"] -->|rows| View["Low latency view"] -->|renders| Page["Localized page"]
  Locales["Locale copy"] -->|translations| View
  Tests["Page tests"] -->|verifies| Page
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Store measurements in structured YAML
  • ➕ Separates editorial measurement data from Ruby helpers
  • ➕ Makes future dataset changes easier to review independently
  • ➖ Requires schema validation for a small static dataset
  • ➖ Splits rendering assumptions across additional files

Recommendation: Keep the helper constants for this bounded, static dataset: they provide a simple shared source for rendering and tests without introducing another loading layer. Move measurements to validated YAML only if the table grows or requires frequent non-code updates.

Files changed (6) +303 / -42

Enhancement (2) +135 / -12
pages_helper.rbDefine audited latency and display-refresh datasets +62/-0

Define audited latency and display-refresh datasets

• Adds receive-path-grouped glass-to-glass measurements and a controlled monitor refresh sweep. Comments document provenance, uncertainty, and why ranges replace resolution-based headline figures.

app/helpers/pages_helper.rb

low_latency.html.erbRender evidence-based latency guidance +73/-12

Render evidence-based latency guidance

• Replaces the three-row resolution table with an accessible, responsive receive-path table. Adds sections explaining latency drivers, glass-to-glass measurement, display refresh evidence, and HDMI capture overhead.

app/views/pages/low_latency.html.erb

Tests (1) +57 / -0
relaunch_pages_test.rbProtect audited latency content across locales +57/-0

Protect audited latency content across locales

• Adds integration tests ensuring the receive-path table and display sweep render, stale 2022 values remain absent, and all locales include the new guidance. Also prevents private Telegram links and unpublished latency-meter claims from appearing.

test/controllers/relaunch_pages_test.rb

Documentation (3) +111 / -30
pages.en.ymlRewrite English latency claims and guidance +37/-10

Rewrite English latency claims and guidance

• Lowers the supported-hardware floor claim to about 30 ms and adds English labels, caveats, setup notes, measurement definitions, and tuning guidance. Removes unsupported in-house meter attribution and distinguishes PixelPilot from Aviateur performance.

config/locales/pages.en.yml

pages.ru.ymlLocalize revised latency guidance in Russian +37/-10

Localize revised latency guidance in Russian

• Mirrors the audited latency claims, receive-path labels, measurement caveats, and new educational sections in Russian. Updates homepage and hero copy to the approximately 30 ms floor.

config/locales/pages.ru.yml

pages.zh.ymlLocalize revised latency guidance in Chinese +37/-10

Localize revised latency guidance in Chinese

• Mirrors the audited latency claims, receive-path labels, measurement caveats, and new educational sections in Chinese. Updates homepage and hero copy to the approximately 30 ms floor.

config/locales/pages.zh.yml

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 28, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Wrong table group scope ⊘ Outdated 🐞 Bug ≡ Correctness
Description
Each receive-path heading labels the measurements in its `, but scope="colgroup"` declares it as a
header for a group of columns. Screen readers therefore cannot reliably associate the heading with
the following measurement rows as intended; this should be scope="rowgroup".
Code

app/views/pages/low_latency.html.erb[73]

+              <th scope="colgroup" colspan="4" class="fw-semibold border-0 pt-4">
Evidence
The template states that group headings should let screen readers identify each measurement block,
then renders every path in its own `` followed by that path's measurement rows. Since the header
labels this row block rather than any set of columns, colgroup contradicts the actual structure
and intended behavior.

app/views/pages/low_latency.html.erb[52-57]
app/views/pages/low_latency.html.erb[70-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The latency receive-path heading labels a group of rows but is declared with `scope="colgroup"`, which describes a group of columns. Change it to row-group semantics so assistive technologies associate each heading with the measurements in its `<tbody>`.
## Issue Context
Each receive path is rendered as a separate `<tbody>`, and the template comment explicitly states that its heading should identify the measurement block to screen readers.
## Fix Focus Areas
- app/views/pages/low_latency.html.erb[70-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread app/views/pages/low_latency.html.erb Outdated
The latency table on /low-latency was unchanged 2022 announcement copy.
Users have pasted the exact sentence back into the chats twice asking how
to reach it. It described gk7205v200/hi3516-era cameras decoded on
laptops, while the paragraphs around it now describe Waybeam, PixelPilot
and Aviateur -- none of which existed when those numbers were measured.

An audit of 923,581 messages across the OpenIPC and wfb-ng groups found
the figures optimistic by 40-160 ms at the exact configurations they
named, and at the same time understating the floor by half: the hero said
"from about 60 ms" when the best-measured result on this stack is 26 ms.

The fix is one number and four bars, not a datasheet. Resolution turns out
to be nearly free -- the same camera at 720p45 moves 120 ms on the monitor
alone -- so the bars compare receive paths, which is what actually decides
what a reader will get. Each spans the lowest and highest figures people
report for that path; collapsing a path to a single number is how the old
table came to promise something nobody could reach.

The audit supports a great deal more than this: a definition of
glass-to-glass, a monitor sweep, the 66-99 ms an HDMI capture adapter can
cost before a camera is even attached, and the 40 ms between two receivers
listed here as equivalents. That is wiki material. This is a landing page,
so it gets the claim and the evidence for it, and the rest stays where
detail belongs.

Two things deliberately left out. The figures are not linked to their
sources: the reports live at t.me/c/... URLs that resolve only for members
of those private groups, and the reporters are real people who have not
been asked whether they want their names on the marketing site. And the
claim that these were "measured with our own open hardware latency meter"
is gone rather than restated -- the meter exists, but its design, method
and runs are not published, and these numbers are not its output.
@openipc-ai
openipc-ai force-pushed the feat/low-latency-numbers branch from 260722b to dada766 Compare August 28, 2026 07:57
@openipc-ai openipc-ai changed the title Say what people actually measure, not what we said in 2022 Say how fast it is, in four bars Aug 28, 2026
The radio-link card credited wfb-ng. devourer is OpenIPC's own replacement
for it -- a drop-in in the same chain, Waybeam on the camera, devourer over
the air, PixelPilot or Aviateur on the ground -- so that is what the card
and the TX -> ... -> RX line under it now name.

What makes it worth naming is where it runs. devourer drives the Realtek
adapter from userspace over libusb, with no kernel module in the path,
which is what lets it set rate, power and channel per frame rather than per
association. Adaptive link, frequency hopping and narrowband channels all
fall out of that, and none of them are reachable through a vendor .ko.

The credits still thank both projects, which is the one line the change
does not take away from wfb-ng.

The provenance line under the latency bars still reads "the OpenIPC and
wfb-ng chats" and stays that way: it says where the reported figures were
collected, and they were collected there.
@openipc-ai
openipc-ai force-pushed the feat/low-latency-numbers branch from c740862 to 98f5b6f Compare August 28, 2026 08:10
/ecosystem described devourer as "an open foundation for SDR-like
receivers built on cheap Wi-Fi hardware" and badged it R&D. Both undersell
it by about the same margin the 60 ms claim did.

It is not a foundation for receivers; it is the link, the piece between
waybeam_venc on the camera and PixelPilot or Aviateur on the ground. And
R&D means "we have the SDK and are starting to study the platform", which
is the wrong end of the lifecycle for something PixelPilot ships on
Android today across five Realtek hardware backends. The rest of that
section is already DONE, and so is this.

Reordered to chain order at the same time -- camera, air, ground -- so the
three read as the path a frame actually takes.
@openipc-ai openipc-ai changed the title Say how fast it is, in four bars Say how fast it is, and name what carries it Aug 28, 2026
@openipc-ai

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6a1a189

The unit was stated once, under the axis, which works for anyone who takes
the block in at a glance. A screen reader arrives at the figures one at a
time and heard "Dedicated ground station", "26 to 67", with nothing saying
what of until the end of the block -- and by then the numbers are behind it.

Each value now carries the unit in a visually-hidden span, so every row
reads complete: "Dedicated ground station, 26-67 ms glass-to-glass". The
axis and its unit label are decoration for those same numbers, so both are
now aria-hidden rather than read out a second time.

Nothing moves on screen; the sighted reader still sees the bare range and
the unit once, under the axis.
devourer earned two sentences on the middle card while TX still had the one
line it was written with, so the row read as one card with a paragraph in it
flanked by two captions -- four lines against two.

All three now run to the same length in every locale: 4 lines in English,
5 in Russian, 3 in Chinese, measured rather than eyeballed. The middle card
gave up a clause it did not need; TX gained the one fact it was missing,
which is that the frame rate chosen there sets most of the budget the rest
of the page is about.
Eight findings across #123-#127, fixed here at the tip rather than on each
branch, because the stack merges in order and this is where they all end up.

- The homepage called upcase on every snapshot's soc and sensor. Both are
  nullable and the upload endpoint permits either to be absent, so one such
  upload took the whole page down. The caption now drops what is missing,
  and disappears when both are.
- The "runs on silicon by" strip listed every Vendor, including the ones
  that only make image sensors. Vendor.soc_vendors exists for exactly this
  and is now what the homepage asks for.
- latest_per_camera called every row sharing a camera's maximum created_at
  the latest, because the anti-join only compared timestamps. Two uploads a
  second apart spent two of the homepage's five tiles on one camera. Ties
  now break on id.
- The legacy redirects used redirect('/path'), which drops the query
  string, and locale rides in the query string: /introduction?locale=ru
  landed on the homepage in the browser's language. They keep it now.
- The /snapshots regression test accepted 500 as a valid request error, so
  a controller regression that returned a server error passed as though the
  camera API still worked. An empty upload is a 415.
- The Open Wall intro said every image was "uploaded voluntarily by a
  camera running OpenIPC firmware" -- the endpoint is unauthenticated and
  verifies neither -- and promised deletion "after two days", which a
  once-nightly purge cannot meet. Both now say what is actually true.
- The ipctool command is step 1's, but rendered after all three steps, so
  below the lg breakpoint the reader met steps 2 and 3 before the command
  they needed for step 1. It sits after step 1 now and moves to the end of
  the row only on desktop, where it wants the full width anyway.

Each fix has a test, and each test was checked against the unfixed code
first: reverting the four that a request can reach fails all four.

Not fixed: the finding that the self-hosted fonts bypass the asset
pipeline. They are served from public/, which is what public/ is for, and
all 19 of them answer 200 with font/woff2 on dev.
@openipc-ai
openipc-ai changed the base branch from fix/terminal-wrapping to master August 28, 2026 09:05
…mbers

# Conflicts:
#	app/assets/stylesheets/application.bootstrap.scss
#	app/controllers/pages_controller.rb
#	app/helpers/pages_helper.rb
#	app/models/snapshot.rb
#	app/views/pages/ecosystem.html.erb
#	app/views/pages/get_started.html.erb
#	app/views/pages/home.html.erb
#	app/views/pages/low_latency.html.erb
#	config/locales/en.yml
#	config/locales/pages.en.yml
#	config/locales/pages.ru.yml
#	config/locales/pages.zh.yml
#	config/locales/ru.yml
#	config/locales/zh.yml
#	config/routes.rb
#	test/controllers/redirects_test.rb
#	test/controllers/relaunch_pages_test.rb
The comment still asserted that two days is what PurgeImagesJob::RETENTION
enforces, which is the claim the copy above it stopped making: the constant
is a cutoff for a nightly sweep, not a deadline.
@openipc-ai
openipc-ai merged commit e9c3702 into master Aug 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant