Skip to content

WIP: Support osmo action 1 - #11

Draft
KonradIT wants to merge 5 commits into
mainfrom
support-osmo-action-1
Draft

WIP: Support osmo action 1#11
KonradIT wants to merge 5 commits into
mainfrom
support-osmo-action-1

Conversation

@KonradIT

@KonradIT KonradIT commented Aug 7, 2026

Copy link
Copy Markdown
Owner

No description provided.

KonradIT and others added 5 commits August 7, 2026 10:48
…record layout corrected

The older Osmo Action generation answers the file list with a flat array of fixed 65-byte
DCF records instead of CompositePack — no paths, no filenames, files addressed by a packed
index. The WIP for this lived on a branch 109 commits behind, in a file the refactor has
since split; this ports it to DcfRecords, where the stride was already reserved for it.

Its first 14 bytes turn out to be laid out exactly like the drone's 94-byte record — mtime,
size, packed index, duration at the same offsets — despite the years between the two
firmwares. Only the epoch differs: unix seconds here, FAT/DOS packing on the drone.

Two fields were being read wrong, and both mattered:

  - +12 was taken for the DCF file number and used to synthesise filenames, so a clip whose
    real name on the card is DJI_0593.MP4 was displayed and addressed as DJI_..._0117_...
    It is the duration in whole seconds.
  - +10 was taken for the DCF directory. It is the high half of the u32 packed index — the
    same mis-slicing that hides internal storage on a drone. Directory and file number now
    come out of DcfIndex.

What identifies the fields is +38, previously guessed to be a size: it is the duration in
milliseconds, and it agrees with +12 on all seven records in the fixture (117 <-> 117550,
306 <-> 306606). The "photo record reads ~0.6 MB" once read there is 667 ms.

That also exposed a trap. One record's whole-second duration is 0, which the drone's
"durationSec == 0 => still" rule would misfile as a photo, but its UUID appears among the
camera's own DjiMovDmx video entries — it is a 0.667 s clip. Sub-second durations round up
to 1 s so the rule still holds downstream.

The fixture is the raw capture; the camera's own log independently states FileNumToSend: 7,
SizeToSend: 463 and both end indices, so the counts are corroborated rather than self-asserted.

Not yet run against the camera. Note that two things this needs already landed here for
other reasons: the datalink keepalive ACKs ~2x/s so the AP cannot sleep (the July run lost
the AP 27 s after the grid, which invalidated its only download attempt), and the browse
session now holds playback mode, which is the state the HTTP server was suspected of being
gated on.

(cherry picked from commit 110efc3)
A tester's Osmo Action 1 (not the unit the layout was derived from) hex-dumped
its whole manifest through the shipped build's unknown-layout diagnostic:

  0000  01000000490000002984e65cc4909e05 7800640007000610032f018aa2d2eb03
  0020  00040000000100050000000a0000ce1c 00000000000000000000000000000000
  0040  000000000000000000

73 bytes: count=1, total=73, and 8 + 1*65 == 73 exactly. decodeAction1 takes it
unmodified and yields one file — index 0x00640078 -> SD / 100MEDIA / DJI_0120,
94,277,828 bytes, 7374 ms.

That is the confirmation the layout was missing. Action1RecordsTest pins it
against the camera it was derived from, and a layout can always be fitted to the
data that produced it; holding on a different unit, with a different file count
and a different clip, is what makes it a format rather than a fit. The bitrate
falls out sane too (~12.8 MB/s, 4K) — a field mis-mapping shows up there
immediately, and the old decoder's +12 read would have named this DJI_0007.

Also settles why that camera's grid was empty on a stock build: main has no
index-list decoder at all, and the card holds exactly one file (118.4 of 119.1 GB
free), so "empty grid" and "empty card" looked identical from outside.

Playback remains unresolved — listing is not the blocker, :80 is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3ad1047)
ROADMAP #6's open question, asked directly instead of reasoned about. The Action
1 refuses :80 while WiFi and the datalink are both up, so downloads are blocked
on an unidentified transport: static lighttpd once the camera is coaxed into
playback, or DUML via DjiTransSrv — the service that already sends us the list.

The drone work makes the second cheap to test. 0x4a transfers run as a family
per transfer kind: +0 query, +1 reply, +2 proceed, +3 state, +4 release. A media
list is 0x00-0x04 and a thumbnail is 0x20-0x24 on a Mavic 3 — same machinery,
different base. The Action 1 demonstrably speaks the list half of that family, so
if its file transfer is DUML at all it is likely another base in the same family.

DcfTransferProbe queries each candidate base for a real file index and reports
what answers; a +3 state frame is the signal, since that is what a device raises
before it starts sending. Runs once per session when an Action-1 manifest
decodes, while the session is provably alive.

Every probe releases, answer or not. A leaked slot made a Mavic 3 stop answering
media queries while telemetry streamed on — the link looks healthy and serves
nothing, which is indistinguishable from "unsupported" and would send us away
from a route that works.

Read-only: query and release, nothing that writes to the card.

A silent result is deliberately reported as weak. The query payload mirrors the
list query's header with the file index where the list puts its cursor, which is
a guess — no capture of this camera transferring a file exists, and a wrong shape
looks exactly like an unsupported kind. Only a positive is strong, and the log
says so rather than letting an empty result read as "ruled out".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit bf02b18)
… preview

A tester's Action 1 started rebooting on the build that added the probe. The
probe is NOT established as the cause — that same build was also the first to
decode the media list, so preview became reachable in the very same step, and
the log cannot separate them. But sending speculative transfer frames to 2019
firmware is exactly the kind of thing that could reboot it, and running an
experiment we can't exonerate on someone else's camera is not defensible.

So the probe is now opt-in (--ez probe4a true), off by default. The next tester
build answers the question that matters: with no probe running, does opening the
preview still reboot the camera? That isolates it in one run.

The log was blind to the half we most needed. MediaPreviewActivity logged with a
bare Log.i, so "Save logs" captured the whole connect flow and went silent the
moment a preview opened — a log covering a camera that misbehaved *during
playback* contained no trace of playback. It now writes to FileLog like
MainActivity does.

Also corrects what the probe claims. On the Action 1, base 0x20 answered our
query with a `release` (4a240e1060...) and nothing else. That is a real finding:
the transfer family IS understood at 0x20, since a foreign base is ignored
outright. But the camera closed the transfer rather than raising a state and
sending, so the summary calling any reply "datalink transfer is LIVE" overstated
it. It now separates "raised STATE — a transfer started" from "replied but
refused — most likely our query payload is wrong", which is the honest reading
of a guessed payload shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6cd1c32)
… on a dead port

The probe is off and the camera no longer reboots, so the 0x4a probe is now the
prime suspect for that — one run, but the only variable that changed.

With preview finally visible in the log, what the tester saw is plain:

  preview stream http://192.168.2.1/v1?file_index=6553720&file_subtype=18...
  (30 s)
  preview ERROR what=1 extra=-2147483648
  preview falling back to file_subtype=0

what=1 / extra=0x80000000 after exactly 30 s is MediaPlayer timing out on a port
that is not listening, twice, once per candidate. That is the "endless cycling":
a minute of spinner ending in a generic failure. So a connect with a 2.5 s
timeout now runs first, and a refused port says so immediately and specifically
instead of pretending to load.

Correction to an earlier reading: `WiFi: onLost` appears in this log too, on a
run where the tester confirms the camera did NOT reboot. It is the AP going away
for its own reasons, not evidence of a restart, and I had cited it as such.

And the actual experiment. ROADMAP #6's leading theory is that :80 is enabled by
duss_proxy only once dji_media_server is in playback — every attempt so far was
made in whatever mode the camera woke up in, so the theory has never been tested.
The session now enters playback with 0x02/0x0c 01 01 00 01 — the same command we
already send every other Osmo to page its media list, not a speculative frame —
and then tries a socket to :80 three times, reporting each result.

If :80 opens, HTTP media is reachable and the download follows. If it stays
refused, that weakens playback-gating without ruling out a further activation
step, and the log says exactly that rather than either overclaiming.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 5d7891f)
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