Skip to content

2.21.0: lewei:// slots for toy drones with a Lewei camera (#262) - #267

Merged
compot-dev merged 6 commits into
v2from
feat/lewei-262
Nov 5, 2026
Merged

compot-dev merged 6 commits into
v2from
feat/lewei-262

Conversation

@compot-dev

Copy link
Copy Markdown
Collaborator

Refs #262 (stays open until the reporter has tried it on the toy).

lewei://192.168.0.1 is for the VISUO drone of #262 (vendor app WiFi UFO). The protocol comes from a PCAPdroid capture of the vendor app; pylwdrone (Apache-2.0), used as a description only, names the fields.

  • New Lewei.java: URL, a message reader that resynchronises after damage, one session (start, a heartbeat every second, stop, a new session after 5 s without video).
  • Each H.264 frame goes to the normal path as RTP (single NAL units and FU-A), so the single view and the grid need no change.
  • Stream type 1: the camera inverts one byte of each frame and the app restores it. Without that, ffmpeg conceals errors throughout the capture; with it, in none of the 184 frames. Type 129 is not handled: the log says so.
  • No flight command is sent; TCP 8060 and the vendor app's UDP ports are not used.

Checked by replaying the 184 frames of the capture from a fake toy on the development machine, not on a real toy:

  • API 25 emulator: plays 1280×720, heartbeats go out, no codec errors; the picture has no artefacts and matches ffmpeg's.
  • API 21 emulator: OMX.google.h264.decoder fails with Decoder failed: -2; the cause is not found yet, and it is not known whether other H.264 streams fail there too.
  • 14 new unit tests, with values from the capture; the full suite and lint pass in the CI image.

Version 2.21.0. Both this and #266 move versionCode to 51: whichever merges second takes the next number.

H.264 over TCP port 7060, with the one byte the camera inverts in each frame
of stream type 1 restored. Only video is asked for: no flight commands.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add lewei:// video slots for Lewei toy drones

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add Lewei toy-drone video slots without sending flight commands.
• Restore altered H.264 frames and route them through the existing RTP decoder path.
• Document capture-based validation and release the feature as 2.21.0.
Diagram

graph TD
  Slot["Lewei slot"] --> Session["RTSP session"] --> Client["Lewei client"] --> Toy["Toy camera"] --> Reader["Message reader"] --> Packetizer["RTP packetizer"] --> Decoder["Existing video path"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Feed Annex B frames directly to the decoder
  • ➕ Avoids RTP packetization and subsequent NAL reassembly.
  • ➖ Requires a separate decoder integration path and risks different behavior between single and grid views.

Recommendation: Keep the PR's RTP adapter: it contains the toy-specific protocol in one client and reuses the established H.264 viewing path. Direct frame delivery would save conversion work but introduce broader decoder changes. Real-toy validation remains the key follow-up.

Files changed (12) +968 / -12

Enhancement (4) +559 / -2
Decoder.javaIdentify Lewei slots in transport status +1/-0

Identify Lewei slots in transport status

• Displays “Lewei TCP” for lewei:// slots rather than a generic transport check.

android/app/src/main/java/com/openipc/decoder/Decoder.java

Lewei.javaImplement the Lewei video transport +491/-0

Implement the Lewei video transport

• Adds URL parsing, TCP session commands and heartbeats, resynchronizing message framing, and restoration of the inverted byte in stream-type-1 frames. Converts Annex B H.264 into single-NAL or FU-A RTP packets and ends silent sessions for reconnection; it sends no flight commands.

android/app/src/main/java/com/openipc/decoder/Lewei.java

RtspSession.javaRoute Lewei slots through the existing video path +66/-1

Route Lewei slots through the existing video path

• Adds Lewei client ownership, shutdown, status, and packet forwarding with an H.264 SDP. Excludes Lewei sessions from the generic watchdog and uses the session retry loop after a requested stream ends.

android/app/src/main/java/com/openipc/decoder/RtspSession.java

StreamProbes.javaAvoid probing active Lewei toy streams +1/-1

Avoid probing active Lewei toy streams

• Skips single-view stream checks for lewei:// slots, as for other toy transports that serve one client.

android/app/src/main/java/com/openipc/decoder/StreamProbes.java

Tests (1) +365 / -0
LeweiTest.javaTest Lewei framing, RTP conversion, and sessions +365/-0

Test Lewei framing, RTP conversion, and sessions

• Adds 14 tests covering URLs, captured command bytes, message recovery, byte restoration, RTP assembly and timing, and TCP behavior against a fake toy. These tests do not replace validation on a physical drone.

android/app/src/test/java/com/openipc/decoder/LeweiTest.java

Documentation (6) +42 / -8
CHANGELOG.mdRecord the 2.21.0 Lewei release +6/-0

Record the 2.21.0 Lewei release

• Adds a release entry for Lewei video slots, byte restoration, and the video-only scope.

CHANGELOG.md

README.mdList Lewei in the toy-drone guide +1/-1

List Lewei in the toy-drone guide

• Updates the English guide index to include Lewei drones.

README.md

README.ru.mdList Lewei in the Russian guide index +1/-1

List Lewei in the Russian guide index

• Updates the Russian guide index to include Lewei drones.

README.ru.md

README.zh-CN.mdList Lewei in the Chinese guide index +1/-1

List Lewei in the Chinese guide index

• Updates the Chinese guide index to include Lewei drones.

README.zh-CN.md

toys.mdDocument Lewei toy setup and protocol limits +30/-5

Document Lewei toy setup and protocol limits

• Adds the slot URL, observed video protocol, stream-type behavior, and video-only safety boundary. Marks the VISUO drone as capture-tested but not yet tested on the toy.

docs/toys.md

transports.mdDocument the lewei:// transport +3/-0

Document the lewei:// transport

• Adds lewei:// to the supported transport table and describes its default address and TCP H.264 video source.

docs/transports.md

Other (1) +2 / -2
build.gradleSet the app version to 2.21.0 +2/-2

Set the app version to 2.21.0

• Raises versionCode from 50 to 51 and versionName from 2.20.0 to 2.21.0.

android/app/build.gradle

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

qodo-free-for-open-source-projects Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Remediation recommended

1. A stopped toy stream can reset a new one ✓ Resolved
Description
lewei() checks the session generation under lock but calls callback.onSdp() after releasing
it, without checking again. If the slot stops and restarts between those operations, the old session
can deliver its SDP to the new stream, resetting its packet assembler and codec state.

2. Reconnecting toy slots appear disconnected 🐞 Bug ≡ Correctness
Description
RtspSession.lewei() reports buffering only when c.streamRequested is true, but
Lewei.Client.run() reports its connecting state before setting that flag. After a session ends,
networkLoop() marks the slot disconnected, so the next connection attempt keeps that status until
the start command has been sent or a packet arrives.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R972-974]

+                        } else if (c.streamRequested) {
+                            // After a lost connection: the slot waits again (Qodo on #267)
+                            status(gen, "buffering");
Evidence
The network loop sets disconnected between attempts; the new client emits a connecting state before
setting streamRequested, and the new callback suppresses buffering for that state.

android/app/src/main/java/com/openipc/decoder/RtspSession.java[267-289]
android/app/src/main/java/com/openipc/decoder/Lewei.java[398-415]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[968-988]

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

## Issue description
A reconnecting Lewei slot remains marked disconnected while it resolves and connects to the toy because the connecting-state callback occurs before `streamRequested` becomes true.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[968-976]
- android/app/src/main/java/com/openipc/decoder/Lewei.java[398-415]
## Recommended Fix
Report buffering for the connecting state as well as the post-request waiting state. Keep the existing generation guard and packet-driven connected status.

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


3. Release notes state untested video works ✓ Resolved
Description
The 2.21.0 CHANGELOG entry says the new lewei:// slot `plays their H.264 video from TCP port
7060, stated as a working feature. The PR description and the docs/toys.md` status both say it has
not been tried on a toy, and it was only replayed from a capture. The docs/toys.md capture section
also says The tested drone sends 1280×720 at 20 fps, which reads as if a drone was tested. Users
reading the release notes will take the support as device-verified, which the ticket's device-test
criterion has not yet confirmed.

View review recommended (3)
4. Toy docs leave the Wi‑Fi name unknown ✓ Resolved
Description
The new Lewei section of docs/toys.md records the Wi‑Fi name as not known yet and gives no Wi‑Fi
name prefix, no WIFI_UFO app version and no Google Play link, so the identification the ticket asks
for is absent from the documentation this PR adds. A later reader trying to tell a Lewei toy from
another WIFI_UFO-named toy has only the package name com.lewei.multiple.lewei and the address
192.168.0.1 to go on. This is a process gap for the reporter rather than a code defect, and it
stays open until the toy is identified.

5. Damaged headers block later video frames ✓ Resolved
Description
Reader.next() accepts any length up to 4 MiB after the magic bytes and waits for that entire body
before looking for another header. If a damaged video header advertises a plausible but incorrect
length, subsequent valid messages remain trapped behind it until the client declares five seconds
without video and reconnects.

6. Toy reconnects remain marked disconnected ✓ Resolved
Description
RtspSession.lewei() forwards the client's post-request waiting text through onDetail() but does
not update the session to buffering. After a prior connection ends, the network loop marks the
slot disconnected, and that status remains while the next TCP connection has requested video, until
an RTP packet is accepted.


Informational

7. The toy is never told to stop its video 🐞 Bug ≡ Correctness
Description
Lewei.Client.run() writes CMD_STOP only when its loop ends normally, but every session ends
through close() (called by closeSocketLocked() in stop()) or an exception, so the socket is
already closed or the write is skipped. The stop command that docs/toys.md describes ("command 3
ends it") therefore never reaches the toy, which learns the session is over only from the TCP reset.
Code

android/app/src/main/java/com/openipc/decoder/Lewei.java[R476-479]

+                out.write(command(CMD_STOP));
+            } catch (IOException e) {
+                if (!closed && keepGoing.check()) throw e;
+            }
Evidence
The loop runs while keepGoing.check() && !closed, and keepGoing is active && gen == generation.
Only stop() changes the generation, and it also calls closeSocketLocked(), which closes the
Lewei client and its socket. networkLoop sets active = false only after connect() returns, so
it cannot end the loop while run() is running. In both cases the blocked read throws on the closed
socket. The catch swallows the exception because closed is set, so the
out.write(command(CMD_STOP)) after the loop never runs.

android/app/src/main/java/com/openipc/decoder/Lewei.java[431-480]
android/app/src/main/java/com/openipc/decoder/Lewei.java[497-505]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[260-280]

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

## Issue description
`Lewei.Client` writes `CMD_STOP` only when its loop ends normally, but sessions always end through `close()`, which closes the socket first. The toy never receives the stop command.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Lewei.java[476-479]
- android/app/src/main/java/com/openipc/decoder/Lewei.java[497-505]
## Recommended Fix
In `close()`, when `streamRequested` is set and the socket is connected, try to write `command(CMD_STOP)` before `socket.close()` and ignore any IOException. Make sure this write cannot block the UI thread: `stop()` may call `close()` from there, so have `close()` set a flag that the run loop acts on, or use a short timeout. If the stop command is not wanted, remove the unreachable write and update the docs to match.

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


8. Malformed toy addresses still connect ✓ Resolved
Description
Lewei.Url.parse() extracts a bracketed host but does not check for stray characters after ]
before accepting a later colon or the default port. An address such as
lewei://[192.168.0.1]garbage:1234 therefore reaches the connection attempt as 192.168.0.1:1234
instead of failing validation.

9. Toy table says Lewei is untested, unlike the rest ✓ Resolved
Description
The Lewei family-table row in docs/toys.md says “since 2.21.0, not yet tried on a toy” despite the
documented VISUO drone check. Readers using that row to assess support get the opposite status from
the Lewei section, the #187 tested-drones row and the 2.21.0 CHANGELOG entry.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Fixed in ab8ed71:

  1. Reconnecting slot stays disconnected: once the stream has been asked for, the session reports buffering, so a slot waiting again after a lost connection no longer shows disconnected.
  2. Damaged headers: a video message says its length twice; if they differ the header is skipped as soon as those bytes have arrived, without waiting for the claimed body. New test wrongVideoLengthIsSeenBeforeItsBodyArrives.
  3. Toy identification in the docs: the app version and source are added. The Wi‑Fi name is not known; only the reporter can give it, so that row stays until they do.

Full build, 395 unit tests and lint pass in the CI image.

@compot-dev

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 ab8ed71

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Fixed in 8a64898: CHANGELOG.md now says the transport was checked with a capture of the vendor app and not yet on a toy. The Wi‑Fi name stays open until the reporter gives it.

@compot-dev

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 8a64898

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Fixed in dee9922, with unit tests, lint and the debug build passing in the container (395 tests).

  1. A stopped toy stream can reset a new one: lewei() now checks the generation again right before onSdp(). The call stays outside the lock, on purpose: onSdp() can close decoders and stop() runs on the UI thread, so holding the lock there could stall the UI. This narrows the window, it does not close it; the same pattern is in the other transports.
  2. Reconnecting toy slots appear disconnected: left as it is. While the TCP connection is being made the toy is not reachable yet, so disconnected is true. The slot shows buffering as soon as the video is requested.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Fixed in e4cf651, with unit tests, lint and the debug build passing in the container (396 tests).

  • Malformed toy addresses still connect: text after ] other than :port is now refused. New cases are in LeweiTest, including lewei://[192.168.0.1]garbage:1234.
  • Toy table says Lewei is untested: the row now reads "works since 2.21.0, checked with a VISUO drone", like the other families.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@compot-dev

Copy link
Copy Markdown
Collaborator Author

The toy is never told to stop its video: correct, the stop command is not reached today, and the toy learns of the end from the TCP close. Left as it is for this release, on purpose: that is the behaviour checked on a real VISUO drone. Sending it from a later point could reach the toy after a new session has started, and that can't be checked without the toy. It stays open as a follow-up.

@compot-dev
compot-dev merged commit b5f6f7b into v2 Nov 5, 2026
7 checks passed
@compot-dev
compot-dev deleted the feat/lewei-262 branch November 5, 2026 09:55
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