{"id":5597,"date":"2026-09-03T21:30:32","date_gmt":"2026-09-03T12:30:32","guid":{"rendered":"https:\/\/donguri3.net\/server-tech\/pikvm-csi-camera-power-control-extras-2\/"},"modified":"2026-09-03T21:30:33","modified_gmt":"2026-09-03T12:30:33","slug":"pikvm-csi-camera-power-control-extras","status":"publish","type":"post","link":"https:\/\/donguri3.net\/en\/server-tech\/linux-server-network\/pikvm-csi-camera-power-control-extras\/","title":{"rendered":"Integrating CSI Camera ON\/OFF Control Features Utilizing PiKVM&#8217;s Standard Extension Specifications"},"content":{"rendered":"<p>In our PiKVM environment built on a Raspberry Pi 3 Model B, we added a CSI-connected camera module to check the status of the front LED indicators and physical switches on the server. After introducing ustreamer-csi as the video streaming daemon, video capture and streaming functioned normally.<\/p>\n<p>However, the Raspberry Pi 3 has limitations in CPU performance and available memory (approx. 1GB). In addition to PiKVM&#8217;s native HDMI capture processing, keeping the CSI camera&#8217;s video streaming (such as MJPEG encoding) running continuously in the background can cause SoC heating and unnecessary resource consumption.<\/p>\n<h2>Concept of On-Demand Power Control to Avoid Continuous Powering<\/h2>\n<p>For the purpose of checking the server&#8217;s physical status, there is no need to monitor the camera feed continuously. An on-demand operation\u2014starting the camera only when needed, such as during server boot-up or status checks when troubleshooting, and stopping it immediately afterwards\u2014is the most logical approach.<\/p>\n<p>To achieve this while eliminating the hassle of manually executing commands via SSH, we planned to build an integrated interface that allows starting, stopping, and checking the camera stream with a single click directly from the PiKVM browser interface.<\/p>\n<h2>Achieving Both 1.5W Standby Power Reduction and Update Resilience<\/h2>\n<p>For this implementation, we set the following three items as quantitative goals and design requirements:<\/p>\n<ul>\n<li>Reduction of Standby Power: Keeping the system-wide power consumption down to the baseline of 1.5W when the camera is stopped, cutting off extra power consumption during standby.<\/li>\n<li>Single-Screen Completeness: Completing status checks (LED lamp display), start\/stop operations, and navigation to the video stream within a single UI.<\/li>\n<li>Maintenance of Update Resilience: Eliminating the risk of configuration loss due to package updates (pacman -Syu) by avoiding any modifications to the PiKVM core code (JavaScript and HTML) and fully complying with the standard plugin structure.<\/li>\n<\/ul>\n<h2>Power Consumption When the Screen Is Not Displayed and Undocumented Extension Specifications<\/h2>\n<p>As the design progressed, hardware and software challenges that needed to be addressed became apparent.<\/p>\n<h3>Power Consumption Occurring Even When the Screen Is Closed<\/h3>\n<p>Using a USB tester to measure the power consumption of the Raspberry Pi 3 yielded the following results:<\/p>\n<ul>\n<li>When ustreamer-csi is stopped: Approx. 1.5W<\/li>\n<li>When ustreamer-csi is running (browser not connected): Approx. 2.3 to 2.4W<\/li>\n<li>While viewing the video stream (connected to port 8081): Approx. 2.9 to 3.0W<\/li>\n<\/ul>\n<p>Even without the stream window open in a browser, simply having the service enabled continuously consumed about 0.8 to 0.9W of power due to supplying power to the CMOS sensor and standby processing by the ISP (Image Signal Processor). Controlling the process itself to stop is essential to suppress heat generation and power loss.<\/p>\n<h3>Balancing Undocumented Extension Specifications with Maintainability<\/h3>\n<p>PiKVM&#8217;s official documentation does not detail development procedures for adding custom applications as extensions to the top screen. Taking the easy approach of directly modifying the frontend (main.js or index.html) carries the risk of files being overwritten and initialized during core updates.<\/p>\n<p><a href=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be.png\"><img decoding=\"async\" class=\"aligncenter wp-image-4706 size-full\" src=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be.png\" alt=\"Default UI\" width=\"843\" height=\"552\" srcset=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be.png 843w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be-300x196.png 300w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be-768x503.png 768w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be-530x347.png 530w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be-565x370.png 565w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be-710x465.png 710w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/0bdad0dada22af2b03a6a549ba2208be-725x475.png 725w\" sizes=\"(max-width: 843px) 100vw, 843px\" \/><\/a><\/p>\n<p>Therefore, upon analyzing PiKVM&#8217;s internal structure, we discovered the existence of an extras plugin mechanism similar to standard features like webterm and vnc. By following this pattern, we adopted a policy of integrating the functionality safely without modifying any core code.<\/p>\n<h2>Integrating the Web Controller Utilizing PiKVM&#8217;s Standard Extras Structure<\/h2>\n<p>The implementation is entirely self-contained within PiKVM. We configured the environment using the following steps.<\/p>\n<h3>Configuring Service Control Privileges (sudoers)<\/h3>\n<p>To allow starting, stopping, and checking the status of ustreamer-csi from the web management app without a password, we deploy a dedicated sudoers configuration.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nrw\nvim \/etc\/sudoers.d\/kvmd-camera-api\n<\/pre>\n<p>The contents of the configuration file \/etc\/sudoers.d\/kvmd-camera-api are as follows.<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nroot ALL=(root) NOPASSWD: \/usr\/bin\/systemctl start ustreamer-csi, \/usr\/bin\/systemctl stop ustreamer-csi, \/usr\/bin\/systemctl is-active ustreamer-csi\n<\/pre>\n<p>Grant the appropriate permissions.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nchmod 440 \/etc\/sudoers.d\/kvmd-camera-api\n<\/pre>\n<h3>Creating a Lightweight Web Controller (Python)<\/h3>\n<p>We create camera_manager.py using only Python standard libraries to handle web page serving and backend control in a single file. There are no dependencies on external libraries.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nvim \/usr\/local\/bin\/camera_manager.py\n<\/pre>\n<p>Write the following code into the file \/usr\/local\/bin\/camera_manager.py.<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport http.server\nimport subprocess\nimport json\n\nPORT = 8082\n\nHTML_PAGE = &quot;&quot;&quot;&lt;!DOCTYPE html&gt;\n&lt;html lang=&quot;ja&quot;&gt;\n&lt;head&gt;\n    &lt;meta charset=&quot;UTF-8&quot;&gt;\n    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;\n    &lt;title&gt;PiKVM Camera Manager&lt;\/title&gt;\n    &lt;img src=&quot;data:image\/gif;base64,R0lGODlhAQABAIAAAAAAAP\/\/\/yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; data-wp-preserve=&quot;%3Cstyle%3E%0A%20%20%20%20%20%20%20%20body%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-family%3A%20-apple-system%2C%20BlinkMacSystemFont%2C%20%22Segoe%20UI%22%2C%20Roboto%2C%20sans-serif%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%231e1e24%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3A%20%23e0e0e0%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20display%3A%20flex%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20justify-content%3A%20center%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20align-items%3A%20center%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20min-height%3A%20100vh%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin%3A%200%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.card%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%232b2b36%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20padding%3A%2030px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20border-radius%3A%2012px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20box-shadow%3A%200%208px%2024px%20rgba(0%2C0%2C0%2C0.4)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3A%20340px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20text-align%3A%20center%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20h1%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-size%3A%2020px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin-bottom%3A%2020px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3A%20%23ffffff%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.status-container%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20display%3A%20flex%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20align-items%3A%20center%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20justify-content%3A%20center%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20gap%3A%2010px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin-bottom%3A%2025px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-size%3A%2016px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-weight%3A%20bold%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.lamp%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3A%2014px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20height%3A%2014px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20border-radius%3A%2050%25%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%23555%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20box-shadow%3A%200%200%204px%20rgba(0%2C0%2C0%2C0.5)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20transition%3A%20background-color%200.3s%2C%20box-shadow%200.3s%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.lamp.active%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%2328a745%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20box-shadow%3A%200%200%2010px%20%2328a745%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.lamp.inactive%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%23dc3545%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20box-shadow%3A%200%200%2010px%20%23dc3545%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.btn-group%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20display%3A%20flex%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20gap%3A%2010px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin-bottom%3A%2020px%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20button%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20flex%3A%201%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20padding%3A%2012px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20border%3A%20none%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20border-radius%3A%206px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-size%3A%2014px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-weight%3A%20bold%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20cursor%3A%20pointer%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20transition%3A%20opacity%200.2s%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20button%3Ahover%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20opacity%3A%200.85%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.btn-on%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%2328a745%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3A%20white%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.btn-off%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%23dc3545%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3A%20white%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.btn-stream%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20display%3A%20block%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3A%20100%25%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20box-sizing%3A%20border-box%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20padding%3A%2012px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%23007acc%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3A%20white%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20text-decoration%3A%20none%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20border-radius%3A%206px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-weight%3A%20bold%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-size%3A%2014px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20transition%3A%20opacity%200.2s%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20.btn-stream%3Ahover%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20opacity%3A%200.85%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%3C%2Fstyle%3E&quot; data-mce-resize=&quot;false&quot; data-mce-placeholder=&quot;1&quot; class=&quot;mce-object mce-object-style&quot; width=&quot;20&quot; height=&quot;20&quot; alt=&quot;&amp;lt;style&amp;gt;&quot; \/&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n    &lt;div class=&quot;card&quot;&gt;\n        &lt;h1&gt;PiKVM Camera Manager&lt;\/h1&gt;\n        &lt;div class=&quot;status-container&quot;&gt;\n            &lt;div id=&quot;status-lamp&quot; class=&quot;lamp&quot;&gt;&lt;\/div&gt;\n            &lt;span id=&quot;status-text&quot;&gt;Checking...&lt;\/span&gt;\n        &lt;\/div&gt;\n        &lt;div class=&quot;btn-group&quot;&gt;\n            &lt;button class=&quot;btn-on&quot; onclick=&quot;sendAction(&#039;start&#039;)&quot;&gt;Power ON&lt;\/button&gt;\n            &lt;button class=&quot;btn-off&quot; onclick=&quot;sendAction(&#039;stop&#039;)&quot;&gt;Power OFF&lt;\/button&gt;\n        &lt;\/div&gt;\n        &lt;a id=&quot;stream-link&quot; class=&quot;btn-stream&quot; href=&quot;#&quot; target=&quot;_blank&quot;&gt;Open Camera Stream&lt;\/a&gt;\n    &lt;\/div&gt;\n\n    &lt;img src=&quot;data:image\/gif;base64,R0lGODlhAQABAIAAAAAAAP\/\/\/yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; data-wp-preserve=&quot;%3Cscript%3E%0A%20%20%20%20%20%20%20%20const%20host%20%3D%20window.location.hostname%3B%0A%20%20%20%20%20%20%20%20document.getElementById(&#039;stream-link&#039;).href%20%3D%20&#039;http%3A%2F%2F&#039;%20%2B%20host%20%2B%20&#039;%3A8081%2F&#039;%3B%0A%0A%20%20%20%20%20%20%20%20function%20updateStatus()%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20fetch(&#039;api%2Fstatus&#039;)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.then(res%20%3D%3E%20res.json())%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.then(data%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20lamp%20%3D%20document.getElementById(&#039;status-lamp&#039;)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20text%20%3D%20document.getElementById(&#039;status-text&#039;)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(data.active)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20lamp.className%20%3D%20&#039;lamp%20active&#039;%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text.innerText%20%3D%20&#039;Active%20(Streaming)&#039;%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text.style.color%20%3D%20&#039;%2328a745&#039;%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20lamp.className%20%3D%20&#039;lamp%20inactive&#039;%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text.innerText%20%3D%20&#039;Inactive%20(Stopped)&#039;%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text.style.color%20%3D%20&#039;%23dc3545&#039;%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.catch(()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20text%20%3D%20document.getElementById(&#039;status-text&#039;)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text.innerText%20%3D%20&#039;Connection%20Error&#039;%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text.style.color%20%3D%20&#039;%23aaa&#039;%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20function%20sendAction(action)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20fetch(&#039;api%2F&#039;%20%2B%20action%2C%20%7B%20method%3A%20&#039;POST&#039;%20%7D)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.then(()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20setTimeout(updateStatus%2C%20500)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20updateStatus()%3B%0A%20%20%20%20%20%20%20%20setInterval(updateStatus%2C%203000)%3B%0A%20%20%20%20%3C%2Fscript%3E&quot; data-mce-resize=&quot;false&quot; data-mce-placeholder=&quot;1&quot; class=&quot;mce-object mce-object-script&quot; width=&quot;20&quot; height=&quot;20&quot; alt=&quot;&amp;lt;script&amp;gt;&quot; \/&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n&quot;&quot;&quot;\n\nclass CameraManagerHandler(http.server.BaseHTTPRequestHandler):\n    def do_GET(self):\n        if self.path == &#039;\/&#039; or self.path == &#039;\/index.html&#039;:\n            self.send_response(200)\n            self.send_header(&#039;Content-Type&#039;, &#039;text\/html; charset=utf-8&#039;)\n            self.end_headers()\n            self.wfile.write(HTML_PAGE.encode(&#039;utf-8&#039;))\n        elif self.path == &#039;\/api\/status&#039;:\n            res = subprocess.run(&#x5B;&#039;sudo&#039;, &#039;systemctl&#039;, &#039;is-active&#039;, &#039;ustreamer-csi&#039;], stdout=subprocess.PIPE, text=True)\n            is_active = (res.stdout.strip() == &#039;active&#039;)\n            self.send_response(200)\n            self.send_header(&#039;Content-Type&#039;, &#039;application\/json&#039;)\n            self.end_headers()\n            self.wfile.write(json.dumps({&#039;active&#039;: is_active}).encode(&#039;utf-8&#039;))\n        else:\n            self.send_response(404)\n            self.end_headers()\n\n    def do_POST(self):\n        if self.path == &#039;\/api\/start&#039;:\n            subprocess.run(&#x5B;&#039;sudo&#039;, &#039;systemctl&#039;, &#039;start&#039;, &#039;ustreamer-csi&#039;])\n            self.send_response(200)\n            self.end_headers()\n        elif self.path == &#039;\/api\/stop&#039;:\n            subprocess.run(&#x5B;&#039;sudo&#039;, &#039;systemctl&#039;, &#039;stop&#039;, &#039;ustreamer-csi&#039;])\n            self.send_response(200)\n            self.end_headers()\n        else:\n            self.send_response(404)\n            self.end_headers()\n\n    def log_message(self, format, *args):\n        return\n\nif __name__ == &#039;__main__&#039;:\n    server = http.server.HTTPServer((&#039;0.0.0.0&#039;, PORT), CameraManagerHandler)\n    server.serve_forever()\n<\/pre>\n<p>Grant execution permissions.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nchmod +x \/usr\/local\/bin\/camera_manager.py\n<\/pre>\n<h3>Making the Service Persistent via systemd<\/h3>\n<p>To have the script run automatically at OS startup, define a systemd service.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nvim \/etc\/systemd\/system\/camera-manager.service\n<\/pre>\n<p>The contents of the configuration file \/etc\/systemd\/system\/camera-manager.service are as follows.<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;Unit]\nDescription=PiKVM CSI Camera Manager Web App\nAfter=network.target\n\n&#x5B;Service]\nExecStart=\/usr\/bin\/python3 \/usr\/local\/bin\/camera_manager.py\nRestart=always\nUser=root\n\n&#x5B;Install]\nWantedBy=multi-user.target\n<\/pre>\n<p>Enable and start the service.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nsystemctl daemon-reload\nsystemctl enable --now camera-manager\n<\/pre>\n<h4>Placement in PiKVM&#8217;s Standard Extras Directory<\/h4>\n<p>Create the PiKVM plugin directory \/usr\/share\/kvmd\/extras\/camera-manager and place the necessary definition files.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nmkdir -p \/usr\/share\/kvmd\/extras\/camera-manager\n<\/pre>\n<h4>Meta Information File (manifest.yaml)<\/h4>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nvim \/usr\/share\/kvmd\/extras\/camera-manager\/manifest.yaml\n<\/pre>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nname: Camera Manager\ndescription: CSI Camera Power &amp;amp;amp;amp;amp;amp; Stream Controller\nicon: extras\/camera-manager\/camera.svg\npath: extras\/camera-manager\ndaemon: camera-manager\nplace: 15\n<\/pre>\n<h3>Dedicated Icon (camera.svg)<\/h3>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nvim \/usr\/share\/kvmd\/extras\/camera-manager\/camera.svg\n<\/pre>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&lt;svg xmlns=&quot;http:\/\/www.w3.org\/2000\/svg&quot; viewBox=&quot;0 0 512 512&quot; fill=&quot;#888888&quot;&gt;\n    &lt;path d=&quot;M149.1 64.8L138.7 96H64C28.7 96 0 124.7 0 160V416c0 35.3 28.7 64 64 64H448c35.3 64 64 64 64-64V160c0-35.3-28.7-64-64-64H373.3L362.9 64.8C356.4 45.2 338.1 32 317.4 32H194.6c-20.7 0-39 13.2-45.5 32.8zM256 192a96 96 0 1 1 0 192 96 96 0 1 1 0-192z&quot;\/&gt;\n&lt;\/svg&gt;\n<\/pre>\n<h4>Nginx Reverse Proxy Configuration (nginx.ctx-server.conf)<\/h4>\n<p>Define static file serving for the SVG icon and a reverse proxy to the web management screen (port 8082).<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nvim \/usr\/share\/kvmd\/extras\/camera-manager\/nginx.ctx-server.conf\n<\/pre>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nlocation = \/extras\/camera-manager\/camera.svg {\n    alias \/usr\/share\/kvmd\/extras\/camera-manager\/camera.svg;\n    include \/etc\/kvmd\/nginx\/loc-login.conf;\n}\n\nlocation \/extras\/camera-manager {\n    rewrite ^\/extras\/camera-manager$ \/extras\/camera-manager\/ permanent;\n    proxy_pass http:\/\/127.0.0.1:8082\/;\n    include \/etc\/kvmd\/nginx\/loc-proxy.conf;\n    include \/etc\/kvmd\/nginx\/loc-login.conf;\n    include \/etc\/kvmd\/nginx\/loc-nocache.conf;\n}\n<\/pre>\n<h4>Applying and Protecting the Filesystem<\/h4>\n<p>Restart the PiKVM daemon and Nginx, and return the filesystem to read-only.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nsystemctl restart kvmd kvmd-nginx\nro\n<\/pre>\n<h2>Measured Power Reduction and UI<\/h2>\n<p>Through the above setup, the &#8220;Camera Manager&#8221; icon is now automatically rendered on PiKVM&#8217;s top screen with a design matching the standard tiles.<\/p>\n<table style=\"border-collapse: collapse; width: 78.125%;\" border=\"1\">\n<tbody>\n<tr>\n<td style=\"width: 25%;\">#<\/td>\n<td style=\"width: 25%;\">Status<\/td>\n<td style=\"width: 46.875%;\">Power Consumption (Measured)<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 25%;\">1<\/td>\n<td style=\"width: 25%;\">ustreamer-csi stopped (OFF)<\/td>\n<td style=\"width: 46.875%;\">1.5 to 1.6W<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 25%;\">2<\/td>\n<td style=\"width: 25%;\">ustreamer-csi running (ON)<\/td>\n<td style=\"width: 46.875%;\">2.3 to 2.4 W<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 25%;\">3<\/td>\n<td style=\"width: 25%;\">Stream screen displayed<\/td>\n<td style=\"width: 46.875%;\">2.9 to 3.0 W<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d.png\"><img decoding=\"async\" class=\"aligncenter wp-image-4705 size-full\" src=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d.png\" alt=\"Added camera manager\" width=\"812\" height=\"538\" srcset=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d.png 812w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d-300x199.png 300w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d-768x509.png 768w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d-530x351.png 530w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d-565x374.png 565w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d-710x470.png 710w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/70b3665d63351bb0dd04f63bc0a5200d-725x480.png 725w\" sizes=\"(max-width: 812px) 100vw, 812px\" \/><\/a><\/p>\n<p>Clicking the tile on the top screen opens the \/extras\/camera-manager\/ management panel. Pressing &#8220;Power ON&#8221; immediately changes the status lamp to green (Active), and the video stream (port 8081) can be checked from &#8220;Open Camera Stream&#8221;. After checking, pressing &#8220;Power OFF&#8221; stops the service, reducing power consumption down to the 1.5W baseline.<\/p>\n<p><a href=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/8939f506cd0a2f8d432a44a6a6e4d677.png\"><img decoding=\"async\" class=\"aligncenter size-full wp-image-4696\" src=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/8939f506cd0a2f8d432a44a6a6e4d677.png\" alt=\"PiKVM Camera Manager\" width=\"411\" height=\"281\" srcset=\"https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/8939f506cd0a2f8d432a44a6a6e4d677.png 411w, https:\/\/donguri3.net\/wp-content\/uploads\/2026\/08\/8939f506cd0a2f8d432a44a6a6e4d677-300x205.png 300w\" sizes=\"(max-width: 411px) 100vw, 411px\" \/><\/a><\/p>\n<p>Because no changes were made to PiKVM&#8217;s core JavaScript or HTML, concerns about configuration loss from future updates are eliminated, establishing a safe on-demand monitoring environment while preserving Raspberry Pi 3 resources.<\/p>\n<p><a href=\"https:\/\/amzn.to\/4gRoQ2K\" target=\"_blank\" rel=\"noopener\" data-blogcard=\"1\">Raspberry Pi 5 Supported 4GB Main Board \/ Japanese Technical Conformity Certified<\/a><\/p>\n<h2>Conclusion<\/h2>\n<p>When introducing a CSI camera to PiKVM on a Raspberry Pi 3, there was an issue where about 0.8 to 0.9W of power was continuously consumed even when the screen was not displayed. To solve this, we created a lightweight Python web controller and integrated it via PiKVM&#8217;s standard extras extension mechanism and an Nginx proxy. We built an environment that allows starting, stopping, and stream checking with a single click from the top screen without modifying core code, successfully achieving both standby power suppression down to 1.5W and high maintainability.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In our PiKVM environment built on a Raspberry Pi 3 Model B, we added a CSI-connected camera module to check th [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":4696,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_locale":"en_US","_original_post":"https:\/\/donguri3.net\/?p=4694","footnotes":""},"categories":[1170],"tags":[1191,52,63,461,47,462,1190,10,179,1192],"class_list":["post-5597","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux-server-network","tag-csi","tag-https","tag-nginx","tag-pikvm","tag-python","tag-raspberry-pi-3","tag-ustreamer","tag-server","tag-179","tag-1192","en-US"],"_links":{"self":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5597","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/comments?post=5597"}],"version-history":[{"count":1,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5597\/revisions"}],"predecessor-version":[{"id":5600,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5597\/revisions\/5600"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/media\/4696"}],"wp:attachment":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/media?parent=5597"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/categories?post=5597"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/tags?post=5597"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}