Skip to content

feat: FLOW-UFO toy drones over UDP (#187) - #188

Closed
compot-dev wants to merge 577 commits into
v2from
feat/flowufo-187
Closed

compot-dev wants to merge 577 commits into
v2from
feat/flowufo-187

Conversation

@compot-dev

@compot-dev compot-dev commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #187. Draft until a device test on a real drone.

What it does

A new slot type flowufo:// plays the video of cheap Wi-Fi toy drones with a FLOW-UFO-xxxxxx access point (BL60x camera), in single view and in grid cells, like an http:// MJPEG slot. Video only: no flight commands are ever sent.

  • UDP to the drone (default 192.168.169.1:8800) from an ephemeral port. Probe EF 00 04 00 every 100 ms until the first fragment; after 3 s without fragments everything is reset and probing starts again.
  • Drone packets 93 <type> <len>: type 1 frame fragments (1024 data bytes each) are collected into four slots (slot = (seq − 1) & 3, a sliding window); type 4 service messages are only logged.
  • Acknowledgement EF 02 every 25 ms with the fixed 0x58-byte header (no command, QPara 19 32 23 4B, camera 0) and per-frame records: received-fragment mask (status 0/1), "send whole" (status 3) for a slot lagging the newest frame by ≥ 5, and for the next frame after > 19 ms without fragments when all slots are complete. The frame currently arriving is left out.
  • The frames carry only the scan data: the app generates the JPEG headers itself (IJG standard tables scaled to the frame's quality, baseline, 4:4:4, Annex K Huffman tables, no restart interval; 605 bytes, size in SOF0 at 0x91/0x93) and appends EOI. Decoding/drawing reuses the existing MJPEG path (MJPEG over RTSP (RTP/JPEG, RFC 2435) — discussion #119/HTTP MJPEG slots (multipart/x-mixed-replace), e.g. http://<camera>/mjpeg #121/MJPEG: hardware decoding through MediaCodec (video/jpeg) where the device has it #132).
  • The UDP socket is bound to the network whose subnet holds the drone (ConnectivityManager, new normal permission ACCESS_NETWORK_STATE), so a Wi-Fi without internet is used even when mobile data is on.
  • Status text: "Waiting for the toy: probing 192.168.169.1:8800", "Receiving from the toy at …", "The toy went quiet: probing … again". /stats.json has a flowufo object per cell / in single (state, network, frames/fragments per second, fragments, duplicates, lost/late frames, whole/next requests, resets, last service message, header of the first fragment, send errors). Single-view stream checks skip these slots (the drone talks to one client).
  • Version 2.10 (35).

How to use

Put flowufo://192.168.169.1:8800 into a slot (flowufo:// alone means the same). Connect the phone/box to the drone's FLOW-UFO-… Wi-Fi first.

Not verified (written from a static description, no drone yet)

  • Whether the drone needs the acknowledgements at all, and whether it resends on status 0/3.
  • Frame number at 0x08: read as u32 (a u64, or a u32 plus an unknown field at 0x0C, read the same while small); base at 0x18 likewise. The log prints bytes 0x08–0x17 of the first fragments.
  • JPEG header choices: 4:4:4, Cb/Cr on the second tables, no restart interval, no JFIF marker (Android's libjpeg-turbo assumes YCbCr; a hardware MJPEG decoder might guess otherwise — wrong colours would point there). The log prints the first 32 bytes of the first frame body.
  • Whether every fragment carries the same body length (the longest is taken), and that every fragment but the last has 1024 data bytes (a short one inside the body stays missing, a short last one ends the body; short_fragments in stats.json).
  • Record order in the acknowledgement and that the paused frame is included again after 19 ms.
  • Whether Android routes to the toy without the binding on the tester's phone.

What a tester should send

  1. App version/git hash, phone model and Android version.
  2. With the slot playing (or trying for ~30 s): http://<device>:8085/stats.json (the flowufo object) and http://<device>:8085/log (lines with FLOW-UFO). The debug server must be reachable — on a phone that is only on the toy's Wi-Fi, open them on the phone itself (http://127.0.0.1:8085/...).
  3. A screenshot if a picture appears but looks wrong (colours, stripes).

Notes for testers: the phone must stay on the toy's Wi-Fi even though it has no internet; Android may ask "This network has no internet access. Stay connected?" — answer yes (and "don't ask again"). Mobile data may stay on.

- New section Transports: TCP / UDP / Auto / Multicast, what each does and its fallback;
  OpenIPC Majestic and XM reply 400/451 to RTSP multicast and play over TCP
- udp:// slots for Majestic outgoing: URL forms, codec by payload type, codec=/pt=,
  one port per slot, the outgoing destination in /etc/majestic.yaml,
  /api/v1/outgoing.json, multicast destinations (confirmed in #74)
- The transport extra of a shared link, with an adb example
- Requirements, features, quick start, the menu table, the reserved ninth cell,
  Known limitations (RTSP multicast needs Android 8.0+), Architecture (Transport,
  UdpReceiver, RtpReorder, RtpListener) and the security note updated; EN/RU/ZH
docs: transports and udp:// slots in the READMEs (#75)
…80, #82, #83)

- Each cell shows small text in its bottom-left corner, updated every second:
  'WxH codec decoded/received fps' while playing, otherwise Connecting…, Buffering…,
  Disconnected, a short connection error (Port in use, Connection refused, No answer
  from camera, Wrong login or password, …) or a decoder limit (#80, #82). The same line
  goes to logcat every 5 s.
- Received frames are counted by the RTP marker bit, decoded frames by
  releaseOutputBuffer; MediaCodecManager exposes them with the output size.
- Instance limit (#83): the first decoder of a type reports its declared limit
  (getMaxSupportedInstances, Android 6+) and throughput (logged). A cell that would exceed
  the limit shows 'Decoder limit: N × H.265' and retries with back-off until an instance
  is free. RtspSession reports connection failures through Callback.onFailure (default no-op).
- Overload (#83): judged by measurement, not by the declared budget, which is far too low on
  some boxes. When a settled cell decodes under 85 % of what it receives for 5 s, the
  highest-numbered playing cell is paused ('Paused: decoder overloaded') until the grid is
  reopened.
- Tests: CellStatsTest.
…own (#83)

On the device the first version paused cells down to two: decoded/received also drops
when frames are broken by packet loss on the network, which says nothing about the
decoder. A cell now counts as behind only when the decoder makes it lose frames: its
NAL queue overflows, or dequeueInputBuffer finds no free buffer (that NAL is dropped).
After a pause the load gets 10 s to settle before the next decision.
decoded/received stays in the corner text as information.
feat: multi-view cell stats and status, decoder limits (#80, #82, #83)
- DebugServer: a plain ServerSocket on its own thread, always on for the process,
  started by the activity. GET only (405 otherwise), one request per connection,
  3 s socket timeouts, two workers and a short queue; extra connections are closed.
  A port that cannot be bound is retried every 10 s and reported in the JSON.
- Only clients on the local network are answered: loopback, 10/8, 172.16/12,
  192.168/16, link-local (169.254/16, fe80::/10), IPv6 fc00::/7; others get 403.
- / is a static page that fetches /stats.json every 1.5 s and renders it.
  /stats.json: app version and git hash, device, SoC, Android, screen, the device
  addresses as http://<ip>:8085/ (no 127/8, no 169.254/16), mode, the single-view
  status or every cell with its corner text, in/lost fps, paused and audio.
  The state is read on the UI thread; if it does not answer within 1 s the JSON says
  "ui_thread": "not responding" (useful for the ANR in #76).
  /log: the last 500 lines of the app's own log (logcat without READ_LOGS).
- Logins and passwords never leave the device: URLs and log lines go through the same
  redaction as the log (#55), plus Authorization headers.
- NetInfo: the device addresses and the local-client test; #87 will use it for the
  U cell hint.
- README (EN/RU/ZH): the debug page and a security note.
Seen on the box: an XM slot showed user=admin, only the password was masked.
…data, U view, write deadline

- IPv6 site-local (fec0::/10) is no longer a local client: isSiteLocalAddress() counts
  for IPv4 only.
- /log is served from LogBuffer, a ring of the last 500 redacted lines fed by a logcat
  reader that follows the app's own log for the life of the process.
- Each cell reports behind_s (seconds in a row the decoder made it lose frames), with
  overload_after_s for the threshold (#83). Full rate / key frames comes with #86.
- In the U view the JSON has a "udp" object instead of the last single-view state.
- A request has a 5 s deadline, writing included: a client that stops reading has its
  socket closed and cannot hold a worker.
feat: debug HTTP server on port 8085, LAN clients only (#96)
…he screen (#98)

- /snapshot.jpg: the multi-view grid (2x2 / 3x3) or the single view, from the frames
  the app is showing. Cells are copied from their SurfaceViews with PixelCopy at
  320x180 (Android 7+), the single view with TextureView.getBitmap() at 640x360; one
  JPEG, quality 70. No extra decoder, nothing pulled from the camera.
- A cell is drawn only while it plays (not paused, frames decoded in the last second;
  single view: frames decoded since the previous snapshot). Otherwise it is black with
  its slot and status in red, never a stale frame. Labels and a timestamp on the image.
- Only on request: one capture at a time, a picture younger than 1 s is served again.
  The cells are chosen on the UI thread, copying and JPEG encoding run on the server
  thread; PixelCopy callbacks on their own HandlerThread.
- Page: the snapshot above the stats, refreshed every 3, 5 or 10 s (selector, default
  5 s, remembered), the next one requested after the previous has loaded. Stats and
  snapshots are requested only while the tab is visible.
- README (EN/RU/ZH): /snapshot.jpg.
On the AG-216 with 6 playing cells one snapshot took about 4 s with one PixelCopy
after another, and the first one ran into the 5 s request deadline. The copies are
now requested together and awaited with one 2 s budget.
…s, JPEG 60 (#98)

On the AG-216 decoding takes nearly all CPU: encoding the 960x540 JPEG took 1-2 s.
The server workers now run at background priority, so a request waits instead of
the video; cells are 256x144 (768x432 for 3x3) at quality 60, and a request may take
up to 10 s.
…per source, hidden tabs

- A cell is live in a snapshot when it is connected, not paused and showed a frame in
  the last 2 s (MediaCodecManager records when it rendered the last frame; reset when
  the decoder closes). Replaces the sampled fps, which could lag a stopped stream.
- Single view: live when the status is connected and a frame was rendered in the
  last 2 s, instead of 'frames since the previous snapshot'.
- The 1 s snapshot cache is tied to the source: a picture taken from an activity that
  was replaced or went away meanwhile is not served. No lock on the UI thread: a
  generation counter, since a capture in progress may be waiting for the UI thread.
- The page's first stats and snapshot requests also wait while the tab is hidden.
feat: debug page snapshots of what the decoder shows, grid like multi-view (#98)
…, JSON header first

- Page: a header with "OpenIPC Decoder", the version and git hash, the device time and
  uptime; under it JSON, LOG and the snapshot interval; the rest is the snapshot, kept
  at 16:9 and as large as the window allows. The stats tables are gone.
- The text of each cell is HTML over the picture, refreshed with the stats every 1.5 s:
  slot, camera address and transport on top; size, codec and decoded/received fps
  below (orange when under 90 %), lost frames and decoder lag in red. A cell without
  video is covered with its slot and status in red.
- /snapshot.jpg?labels=0 gives the frames only (no text, no grid lines) for the page;
  /snapshot.jpg keeps its labels. The 1 s reuse is per variant.
- /stats.json is indented, and starts with what the page header shows: app, version,
  git, time, uptime_s, device, android, screen, addresses, server, then ui_thread,
  mode, active_slot, overload_after_s, and the cells last. Cells and the single view
  get "video" (playing now: connected, not paused, a frame in the last 2 s); cells get
  out_fps; the single view's resolution moves to "size".
…, cache per variant

- The single view's video flag (and its snapshot) requires the app in the foreground;
  stopping the streams sets its state to disconnected.
- The page checks the paused / UDP modes before the single view or the cells.
- The no-video cover is opaque: a stale frame of the last snapshot cannot show through.
- Cell header: a bracketed IPv6 camera address is shown whole.
- The 1 s snapshot reuse keeps one picture per variant (frames only / labelled), so
  the page and a curl poller do not evict each other.
feat: debug page — snapshot fills the page with the cell text over it, JSON header first
…th a monitor), firmware and developer settings
docs: LABORATORY — Aceline-1 and Aceline-2 test boxes
feat: debug page — sharper snapshots and a header that works on phones (#103)
openipc-ai and others added 2 commits October 15, 2026 18:01
…or mappings, Telegram only for v2/basic, LABORATORY without per-version releases (Qodo on #195)
ci: only latest and test pre-releases; docs: mapping.txt (#194, #193)
…lots, dqt switch) and the KY UFO 7099 heartbeat (#187)

Compared with marshallrichards/turbodrone (Apache-2.0, wifi_uav and cooingdv backends;
no code taken over, credited in docs/toys.md and the comments):

- ACK quality parameters 32 4B 14 2D by default; ?qpara= (or ?q=) with 8 hex digits
- bytes 0x34/0x35 are main/flow camera status: shown in the stats, no family warning
- slots 5 or more frames behind are left out of the ACK instead of a seq+4 request
- port 8800 (default) probes 8801 too, one socket per port; locks onto the socket the
  first 0x93 packet arrives on (source address only, the drone sends from port 1234);
  BL618 timing on 8801: probes every 200 ms, ACKs every 50 ms only with a record
- ?dqt=zigzag|raster|q50 for the JPEG quantisation tables, shown in the stats
- no flight command in the ACK, with the reason in a comment
- RTSP: 01 01 to UDP 7099 every second for rtsp://<host>:7070/webcam, or with
  ?heartbeat=<port> (off with ?heartbeat=off; the option is not sent to the camera);
  heartbeat object in /stats.json
- subnet hint in the status when a flowufo:// slot targets the phone itself or another
  toy family's network
- docs/toys.md: families by Wi-Fi name, the new options, KY UFO RTSP, credits (#196)
jieli://192.168.80.1 for toys with an HF_xxxxxxxx Wi-Fi (Jieli AC79, vendor app
DVRunning 2), after the PCAPdroid capture in #196 and turbodrone's research notes
(Apache-2.0, behaviour reference only, no code taken over):

- CTP on TCP 3333 (CTP: + u16 topic length + topic + u32 JSON length + JSON), a reader
  that reassembles split and joined messages and skips what is not CTP
- APP_ACCESS, DATE_TIME, CTP_KEEP_ALIVE every min(timeout / 4, 7.5 s), OPEN_RT_STREAM
  with the size of VIDEO_PARAM (else 640x480 at 25), CLOSE_RT_STREAM on stop
- video expected on the phone's UDP 6666 (unverified): PT 26 into the RTP/JPEG path,
  PT 96 into the H.264 path, whole JPEG datagrams too; first packets logged with
  sender, PT and bytes; no video for 5 s: said in the status, UDP 1234 and 2224 opened
  for the log only, OPEN_RT_STREAM again every 10 s
- options w, h, fps, rtp, format; sockets bound to the toy's network (TCP from Android 6)
- jieli object in /stats.json; subnet hints both ways between flowufo:// and jieli://
- never FLIGHT_CONTROL, nothing to UDP 3456
- docs/toys.md: HF / Jieli section, credits; unit tests with a fake toy on loopback
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread android/app/src/main/java/com/openipc/decoder/FlowUfo.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/FlowUfo.java Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b898ca1

…silence reset, network binding from Android 6 (Qodo on #188)
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo review of b898ca1, addressed in 4a9245a:

  1. Network.bindSocket(DatagramSocket) on Android 5.1: valid, it is API 23. NetInfo.bindToNetworkOf now binds only from Android 6 (M) and keeps the default network below that.
  2. Silence recovery keeps the old socket: valid. probeAgain() now replaces every socket, so each one is set up and bound again for the network that reaches the toy now. The loopback test counts the setup calls across a reset.
  3. A service message locks the wrong port: valid. Only a type-1 frame fragment locks a port now. A service message is still logged and counted, and probing goes on on both ports. The two-port test sends a service message to the 8800 socket first, then video from the 8801 one, and checks that the lock goes to 8801.

The other items were already marked resolved.

compot-dev and others added 4 commits October 15, 2026 18:51
… video (Qodo on #198)

- a KEEP_ALIVE_INTERVAL announced after the first keepalive applies at once: the next
  one is due interval ms after the last one sent, not on the old schedule; test with
  the announcement after the first keepalive
- the quiet status from the CTP thread and "connected" from the UDP thread are
  serialised, and a quiet notice is dropped when video arrived meanwhile
feat: Jieli HF toy drones over CTP (#187)
…aries, so chatty graphics logs no longer push them out (#187)
…r-clockwise, ?rotate= to override (#200)

The K200's camera is mounted sideways. For :7070/webcam slots the software
MJPEG path draws the frames turned 90° CCW and reports the turned size, so the
view fits a landscape picture; ?rotate=0|90|180|270 overrides it for any RTSP
MJPEG slot and is not sent to the camera. H.264/H.265 are not turned (#201).
/stats.json gets rotate; screenshot and debug snapshot copy the turned surface.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

Comment on lines +820 to +825
private void open(int i, InetAddress target) throws IOException {
DatagramSocket s = new DatagramSocket();
try {
s.setReceiveBufferSize(1024 * 1024);
} catch (IOException ignored) {}
if (setup != null) network = setup.prepare(s, target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Failed startup leaks drone sockets 🐞 Bug ☼ Reliability

Client.open() creates a datagram socket but does not close it if preparation throws, and run()
opens both probe sockets before entering its cleanup try. If opening the second socket fails, the
first remains open across connection retries; repeated failures can exhaust available sockets.
Agent Prompt
## Issue description
A failed probe-socket startup can leave previously created sockets open.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/FlowUfo.java[820-830]
- android/app/src/main/java/com/openipc/decoder/FlowUfo.java[857-876]
## Recommended Fix
Close a newly created socket if preparation fails, and move the initial multi-socket opening inside the run method's cleanup region. Test failure while opening the second probe socket.

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

Comment on lines +652 to +658
synchronized (lock) {
for (DatagramSocket s : udpSockets) s.close();
if (tcp != null && !tcpConnected) {
try {
tcp.close();
} catch (IOException ignored) {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Stopping a toy can leave its session stuck 🐞 Bug ☼ Reliability

Jieli.Client.close() leaves the TCP socket open once tcpConnected is true, while send()
performs writes without a timeout. If the peer stops reading and a CTP write blocks, stopping the
slot cannot release that write or reach the session's finally block that closes the connection.
Agent Prompt
## Issue description
A blocked CTP write can prevent a connected toy session from stopping indefinitely.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[645-659]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[817-835]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[890-895]
## Recommended Fix
Preserve the graceful CLOSE_RT_STREAM attempt when possible, but add a bounded shutdown path that closes an established TCP socket if the session thread cannot exit, releasing a blocked write.

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

Comment thread android/app/src/main/java/com/openipc/decoder/RtspSession.java
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b93fd47

@compot-dev

Copy link
Copy Markdown
Collaborator Author

#200 (MJPEG of rtsp://…:7070/webcam turned 90° counter-clockwise, ?rotate=0|90|180|270) is in b93fd47. Qodo review of b93fd47, addressed in 05f3093:

  1. Heartbeat stats outlive the camera that used them: valid. If configure() gets a different URL, it now resets the heartbeat totals, the used/logged flags and the current heartbeat reference. The old connection still closes its own heartbeat, and it no longer adds to the new totals.

Findings 3, 4 and 11 (FlowUfo socket lifetime and reopen timing) are in the FLOW-UFO client, not in this change, and are left for the #187 work.

@compot-dev compot-dev closed this Oct 15, 2026
compot-dev pushed a commit that referenced this pull request Oct 15, 2026
The README (EN/RU/ZH) keeps the overview, screenshots, download,
requirements, quick start, a short controls summary, known limitations,
a documentation table, support and license. The details move to
English-only documents in docs/, each with its own table of contents:

- usage.md: menu, gestures and keys, remote, multi-view, Slides, U cell,
  WebUI, share intent, screenshots, Reset
- transports.md: URL formats and options per transport, codecs,
  authentication, what the servers support
- debug.md: debug server, page, snapshot, stream checks, a /stats.json
  field reference from the code, /log, corner text, bug reports
- architecture.md: data path diagram, classes, threads, decode planner
  (#85, #189), SurfaceView rendering (#61, #76), settings, layout
- testing.md: branches, CI, unit tests, versions and releases,
  branch builds, mapping.txt (#181), local builds
- security.md: network, credential store, debug server LAN rule,
  log redaction, WebUI, permissions
- devices.md: verified and not working devices, hardware decoder notes
- toys.md: drones and Wi-Fi toys, FLOW-UFO in testing (#187, #188),
  unknown toys, what to send

The #76 freeze is no longer a known limitation (fixed in 2.09, #185).
The device report form points to docs/devices.md. Docs only, no
version bump.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Continued in #215: GitHub closed this PR automatically when v2 was force-pushed to remove files from its history, and it can't be reopened. The branch feat/flowufo-187 and the test build toys-187-test2 are unchanged.

compot-dev added a commit that referenced this pull request Oct 16, 2026
…en startup fails; Jieli: only usable video counts as receiving, a stopped slot cannot hang on a blocked CTP write, diagnostic ports freed after 30 s, topic stats bounded, a dropout after the stream request reconnects without back-off
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting device test PR ready, merge only after testing on a real device

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants