CameraControl: don't report unconfirmed firmware upgrades as success - #968
CameraControl: don't report unconfirmed firmware upgrades as success#968Cybis320 wants to merge 3 commits into
Conversation
Upgrade results looked inconsistent because success was inferred rather than checked. cam.upgrade() ends in one of two ways and they do not mean the same thing: dict with Ret 515 - the camera explicitly confirmed the upgrade None - the connection ended with no confirmation None happens both when the camera reboots before it gets round to sending 515 and when it drops the transfer partway. The old code treated None as success unconditionally, so a flash that was never confirmed printed exactly the same "Firmware upgrade successful!" as one that was. Measured on a camera here: the whole flash completes in 38 s and never sends 515 at all, so every upgrade of that unit took the unverified path. Now None is reported as unconfirmed, and either way the camera itself settles it: waitForCameraOnline() polls the DVRIP port until the application is serving again. Port 34567 is the right liveness probe precisely because it is served by the application rather than the bootloader -- when the application is dead the camera answers on 12901 and 34567 stays shut, so an open 34567 means it really did boot. Also tightened the failure paths: 512/513/514 now say plainly that the camera kept its existing firmware, and an unrecognised code is treated as failed instead of passing with a warning. If the camera never returns, the log says so and warns against power-cycling, since it may still be writing flash. On success it prompts for the SwitchMode init push, which is easy to forget and silently leaves the camera on its old stored config. Note the progress lines printed during a transfer are not a reliable status either: dvrip's completion loop prints a stale variable, so the Ret values scrolling past can repeat or show a value the camera never sent. That is upstream in dvrip.py and is not addressed here. Verified by reflashing a camera with the image it was already running: transfer completed, no confirmation arrived, the new code waited and reported it back online, and the camera's uptime confirmed the reboot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit cf2080a)
login() starts a threading.Timer that re-sends a KeepAlive every 20 s on the same socket the firmware upload uses. A transfer takes about 38 s, so the timer fires mid-upload, writes a KeepAlive into the middle of the binary stream, then tries to JSON-decode whatever comes back. Firmware packets begin with 0xFF, so this surfaced as a traceback from a background thread partway through every flash: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0 Alarming to watch, and worse than cosmetic: a second thread reading and writing the socket mid-transfer can consume replies the upload loop is waiting for. An upgrade is exactly the wrong time to be keeping a session alive, since the camera reboots at the end and the session goes with it. Verified by reflashing a camera with the image it was already running: the traceback is gone and the transfer completes cleanly. Note this did NOT restore the Ret 515 confirmation. I had suspected the keep-alive thread of consuming it; with the timer stopped it still never arrives, so these cameras genuinely do not confirm -- they reboot as soon as the image is written. That makes waiting for the camera to come back the only sound way to verify an upgrade, which is what the previous commit does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit ac5916d)
|
Correction to the analysis above, found while looking at the I wrote that these cameras "genuinely do not confirm" the upgrade. That is wrong. The camera does send
Upstream made it tolerant. Running the same flash against the newer library, the confirmation arrives every time: So the erratic results had two causes, not one: the keep-alive thread colliding with the transfer (fixed here), and the decode path dropping the completion response (fixed upstream). This PR is still worth taking as-is. It is defensive rather than cosmetic — Separately I have a branch bumping |
…yPI release
python-dvr on PyPI has exactly one release, 0.0.1, uploaded 2022-08-05, and its
stated homepage (NeiroNx/python-dvr) now 404s. Development moved to
OpenIPC/python-dvr and was never published, so ">=0.0.1" can only ever resolve
to the four-year-old sdist. Every RMS install has been getting a library that is
340 lines behind upstream with no route to anything newer.
That matters because the installed version raises on any non-JSON reply:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0
Upstream fixed this by making receive_json tolerant, and get_command now strips
non-printable bytes before parsing. Both sit in the path every camera call uses,
so a camera returning a malformed or binary-tainted response currently takes
down whatever RMS was doing.
Concretely, it also fixes firmware upgrades. With the old library the camera's
Ret 515 completion was being lost in exactly that decode path, so upgrades
looked unconfirmed and the result varied between runs. With the new one the
confirmation arrives reliably:
Camera confirmed the upgrade (Ret 515).
Firmware upgrade successful - camera confirmed it and is back online.
Note the version specifier had to go: the git tree declares 0.0.0, which is
LOWER than the PyPI 0.0.1, so ">=0.0.1" actively rejects the newer code. Pinned
to @master, matching the imreg_dft line directly above.
Verified against a live camera: GetDeviceInformation and a full firmware upgrade
both work through the new library. Of the functions RMS calls, only login,
upgrade, receive_json, connect, get_command and set_command differ; the rest are
byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 37887414ff4d6815c9d6cb96fd72570a38e6a4c6)
|
Added the dependency bump to this PR, so it now carries both halves of the fix.
-python-dvr>=0.0.1 ; python_version >='3.6'
+python-dvr @ git+https://github.com/OpenIPC/python-dvr@master ; python_version >='3.6'Rationale, in short: PyPI's Verified against a live camera: Of the functions RMS calls, only Two things a reviewer should weigh, since I can't judge them for the project:
Also sent the one remaining upstream bug as OpenIPC/python-dvr#8 — the upgrade completion loop logs a stale variable. |
Two fixes to
upgradeFirmware, found while flashing a batch of GK7205V200 cameras where the results looked inconsistent from run to run.1.
Nonewas treated as successcam.upgrade()ends in one of two ways, and they do not mean the same thing:Ret 515— the camera explicitly confirmed the upgradeNone— the connection ended with no confirmationNonehappens both when the camera reboots before it gets round to sending 515, and when it drops the transfer partway. The current code treats it as success unconditionally:So a flash that was never confirmed prints exactly the same
Firmware upgrade successful!as one that was. On the cameras here the whole flash completes in ~38 s and never sends 515 at all, so every upgrade took the unverified path and the tool always claimed success — including, presumably, when it shouldn't have.Now
Noneis reported as unconfirmed, and either way the camera settles it:waitForCameraOnline()polls the DVRIP port until the application is serving again. Port 34567 is the right liveness probe precisely because it is served by the application rather than the bootloader — when the application is dead these cameras answer on 12901 and 34567 stays shut, so an open 34567 means it really did boot.Failure paths are tightened too: 512/513/514 now state plainly that the camera kept its existing firmware, and an unrecognised code is treated as failed instead of passing with a warning. If the camera never returns, the log says so and warns against power-cycling, since it may still be writing flash.
2. The keep-alive timer runs during the upload
login()starts athreading.Timerthat re-sends a KeepAlive every 20 s on the same socket the firmware upload uses. A transfer takes far longer than that, so the timer fires mid-upload, writes a KeepAlive into the middle of the binary stream, then tries to JSON-decode whatever comes back. Firmware packets begin with0xFF, so this surfaces as a traceback from a background thread partway through every flash:Alarming to watch, and worse than cosmetic — a second thread reading and writing the socket mid-transfer can consume replies the upload loop is waiting for. An upgrade is exactly the wrong time to be keeping a session alive, since the camera reboots at the end and the session goes with it.
Testing
Verified by repeatedly reflashing a camera with the image it was already running, so the content was a no-op and only the code path under test varied: transfer completes cleanly, the traceback is gone, and the tool correctly reports the camera back online. Camera uptime confirmed the reboot each time.
Note stopping the keep-alive did not restore the 515 confirmation — I had suspected the thread of consuming it. With the timer stopped it still never arrives, so these cameras genuinely do not confirm. That is what makes waiting for the camera the only sound verification.
Not included
There is a related upstream bug in
dvrip.pythis does not address: the completion loop doesprint(reply)instead ofprint(data), so theRetvalues printed during a flash are a stale variable and can repeat or show a value the camera never sent. Worth reporting there — it makes every user's flash output misleading.Draft while I flash the rest of the fleet through this path.