Automatically scale max CPU speed to reduce RPi temp, Disable USB camera if RPi tempe...
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Thu, 23 May 2019 00:38:42 +0000 (17:38 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Thu, 23 May 2019 00:38:42 +0000 (17:38 -0700)
CHANGELOG.md
scripts/rc.local
src/bbserial/.gitignore [new file with mode: 0644]
src/py/bbctrl/Camera.py
src/py/bbctrl/Ctrl.py
src/py/bbctrl/MonitorTemp.py [new file with mode: 0644]
src/py/bbctrl/Web.py
src/py/bbctrl/__init__.py
src/resources/images/overtemp.jpg [new file with mode: 0644]

index 06ef92b3e44aaf857470dcbd8a6444dd1bc6bcad..c97aa735b76c168e95500846c4bf5998771a6eb3 100644 (file)
@@ -9,6 +9,8 @@ Buildbotics CNC Controller Firmware Changelog
  - Increased AVR serial and command buffers.
  - Boost HDMI signal.
  - Rewrote RPi serial driver.
+ - Automatically scale max CPU speed to reduce RPi temp.
+ - Disable USB camera if RPi temperature above 80°C, back on at 75°C.
 
 ## v0.4.7
  - Fix homing switch to motor channel mapping with non-standard axis order.
index aecc57f13d4f974289291bcea2eaf5c4ba2eab1c..43e549a2f383c32738eaf0d59f8ef71f88b8d9e2 100755 (executable)
@@ -23,9 +23,8 @@ fi
 # Reload udev
 /etc/init.d/udev restart
 
-# Stop boot splash so it does not interfere with X if GPU enabled
-grep ^dtoverlay=vc4-kms-v3d /boot/config.txt >/dev/null
-if [ $? -eq 0 ]; then plymouth quit; fi
+# Stop boot splash so it doesn't interfere with X if GPU enabled and to save CPU
+plymouth quit
 
 # Start X in /home/pi
 cd /home/pi
diff --git a/src/bbserial/.gitignore b/src/bbserial/.gitignore
new file mode 100644 (file)
index 0000000..a90b6ca
--- /dev/null
@@ -0,0 +1,8 @@
+/.*.cmd
+/.tmp_versions
+/Module.symvers
+/*.ko
+/*.mod.c
+/*.o
+/kernel
+/modules.order
index a5f0fcdef5101f8b42af99309c46ddf40fd4537d..385c3810b108ee22e1dbd4f5d090d0c7a440091f 100755 (executable)
@@ -290,16 +290,17 @@ class Camera(object):
         self.fps = args.fps
         self.fourcc = string_to_fourcc(args.fourcc)
 
-        self.offline_jpg = get_image_resource('http/images/offline.jpg')
-        self.in_use_jpg = get_image_resource('http/images/in-use.jpg')
+        self.overtemp = False
         self.dev = None
         self.clients = []
         self.path = None
+        self.have_camera = False
 
         # Find connected cameras
         for i in range(4):
             path = '/dev/video%d' % i
             if os.path.exists(path):
+                self.have_camera = True
                 self.open(path)
                 break
 
@@ -317,8 +318,13 @@ class Camera(object):
 
         path = str(device.device_node)
 
-        if action == 'add': self.open(path)
-        if action == 'remove' and path == self.path: self.close()
+        if action == 'add':
+            self.have_camera = True
+            self.open(path)
+
+        if action == 'remove' and path == self.path:
+            self.have_camera = False
+            self.close()
 
 
     def _send_frame(self, frame):
@@ -344,13 +350,21 @@ class Camera(object):
             self.log.warning('Failed to read from camera.')
             self.ioloop.remove_handler(fd)
             self.close()
-            return
 
 
+    def _update_client_image(self):
+        if self.have_camera and not self.overtemp: return
+        if self.overtemp and self.have_camera: img = 'overtemp'
+        else: img = 'offline'
+
+        if len(self.clients): self.clients[-1].write_img(img)
+
 
     def open(self, path):
         try:
+            self._update_client_image()
             self.path = path
+            if self.overtemp: return
             self.dev = VideoDevice(path)
 
             caps = self.dev.get_info()
@@ -389,34 +403,29 @@ class Camera(object):
         self.dev = None
 
 
-    def close(self):
+    def close(self, overtemp = False):
+        self._update_client_image()
         if self.dev is None: return
+
         try:
             self.ioloop.remove_handler(self.dev)
             try:
                 self.dev.stop()
             except: pass
-            self._close_dev()
 
-            for client in self.clients:
-                client.write_frame_twice(self.offline_jpg)
-
-            self.log.info('Closed camera %s' % self.path)
+            self._close_dev()
+            self.log.info('Closed camera')
 
-        except: self.log.warning('Closing camera')
+        except: self.log.exception('Exception while closing camera')
         finally: self.dev = None
 
 
     def add_client(self, client):
         self.log.info('Adding camera client: %d' % len(self.clients))
 
-        if len(self.clients):
-            self.clients[-1].write_frame_twice(self.in_use_jpg)
-
+        if len(self.clients): self.clients[-1].write_img('in-use')
         self.clients.append(client)
-
-        if self.dev is None:
-            client.write_frame_twice(self.offline_jpg)
+        self._update_client_image()
 
 
     def remove_client(self, client):
@@ -426,6 +435,14 @@ class Camera(object):
         except: pass
 
 
+    def set_overtemp(self, overtemp):
+        if self.overtemp == overtemp: return
+        self.overtemp = overtemp
+
+        if overtemp: self.close(True)
+        elif self.path is not None: self.open(self.path)
+
+
 
 class VideoHandler(web.RequestHandler):
     boundary = '---boundary---'
@@ -449,13 +466,14 @@ class VideoHandler(web.RequestHandler):
         self.set_header('Expires', 'Mon, 3 Jan 2000 12:34:56 GMT')
         self.set_header('Pragma', 'no-cache')
 
-        if self.camera is None:
-            frame = get_image_resource('http/images/offline.jpg')
-            self.write_frame_twice(frame)
-
+        if self.camera is None: self.write_img('offline')
         else: self.camera.add_client(self)
 
 
+    def write_img(self, name):
+        self.write_frame_twice(get_image_resource('http/images/%s.jpg' % name))
+
+
     def write_frame(self, frame):
         # Don't allow too many frames to queue up
         min_size = len(frame) * 2
index 2a0955a6db11b19ffc91009cb309a887e2931eb0..854f538010a3d4be49ac9934c6f642c0ff92ed81 100644 (file)
@@ -36,8 +36,6 @@ class Ctrl(object):
         self.ioloop = bbctrl.IOLoop(ioloop)
         self.id = id
         self.timeout = None # Used in demo mode
-        self.last_temp_warn = 0
-        self.temp_thresh = 80
 
         if id and not os.path.exists(id): os.mkdir(id)
 
@@ -67,8 +65,6 @@ class Ctrl(object):
             self.lcd.add_new_page(bbctrl.MainLCDPage(self))
             self.lcd.add_new_page(bbctrl.IPLCDPage(self.lcd))
 
-            if not args.demo: self.check_temp()
-
         except Exception: self.log.get('Ctrl').exception()
 
 
@@ -86,23 +82,6 @@ class Ctrl(object):
         self.timeout = self.ioloop.call_later(t, cb, *args, **kwargs)
 
 
-    def check_temp(self):
-        with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f:
-            temp = round(int(f.read()) / 1000)
-
-        # Reset temperature warning threshold after timeout
-        if time.time() < self.last_temp_warn + 60: self.temp_thresh = 80
-
-        if self.temp_thresh < temp:
-            self.last_temp_warn = time.time()
-            self.temp_thresh = temp
-
-            log = self.log.get('Ctrl')
-            log.info('Hot RaspberryPi at %d°C' % temp)
-
-        self.ioloop.call_later(15, self.check_temp)
-
-
     def get_path(self, dir = None, filename = None):
         path = './' + self.id if self.id else '.'
         path = path if dir is None else (path + '/' + dir)
diff --git a/src/py/bbctrl/MonitorTemp.py b/src/py/bbctrl/MonitorTemp.py
new file mode 100644 (file)
index 0000000..839b2e6
--- /dev/null
@@ -0,0 +1,103 @@
+################################################################################
+#                                                                              #
+#                This file is part of the Buildbotics firmware.                #
+#                                                                              #
+#                  Copyright (c) 2015 - 2018, Buildbotics LLC                  #
+#                             All rights reserved.                             #
+#                                                                              #
+#     This file ("the software") is free software: you can redistribute it     #
+#     and/or modify it under the terms of the GNU General Public License,      #
+#      version 2 as published by the Free Software Foundation. You should      #
+#      have received a copy of the GNU General Public License, version 2       #
+#     along with the software. If not, see <http://www.gnu.org/licenses/>.     #
+#                                                                              #
+#     The software is distributed in the hope that it will be useful, but      #
+#          WITHOUT ANY WARRANTY; without even the implied warranty of          #
+#      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       #
+#               Lesser General Public License for more details.                #
+#                                                                              #
+#       You should have received a copy of the GNU Lesser General Public       #
+#                License along with the software.  If not, see                 #
+#                       <http://www.gnu.org/licenses/>.                        #
+#                                                                              #
+#                For information regarding this software email:                #
+#                  "Joseph Coffland" <joseph@buildbotics.com>                  #
+#                                                                              #
+################################################################################
+
+import time
+
+
+def read_temp():
+    with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f:
+        return round(int(f.read()) / 1000)
+
+
+def set_max_freq(freq):
+    filename = '/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq'
+    with open(filename, 'w') as f: f.write('%d\n' % freq)
+
+
+class MonitorTemp(object):
+    def __init__(self, app):
+        self.app = app
+
+        ctrl = app.get_ctrl()
+        self.log = ctrl.log.get('Mon')
+        self.ioloop = ctrl.ioloop
+
+        self.last_temp_warn = 0
+        self.temp_thresh = 80
+        self.min_temp = 60
+        self.max_temp = 80
+        self.min_freq = 600000
+        self.max_freq = 1200000
+        self.low_camera_temp = 75
+        self.high_camera_temp = 80
+
+        self.callback()
+
+
+    # Scale max CPU based on temperature
+    def scale_cpu(self, temp):
+        if temp < self.min_temp: cpu_freq = self.max_freq
+        elif self.max_temp < temp: cpu_freq = self.min_freq
+        else:
+            r = 1 - float(temp - self.min_temp) / \
+                (self.max_temp - self.min_temp)
+            cpu_freq = self.min_freq + (self.max_freq - self.min_freq) * r
+
+        set_max_freq(cpu_freq)
+
+
+    def update_camera(self, temp):
+        if self.app.camera is None: return
+
+        # Disable camera if temp too high
+        if temp < self.low_camera_temp: self.app.camera.set_overtemp(False)
+        elif self.high_camera_temp < temp:
+            self.app.camera.set_overtemp(True)
+
+
+    def log_warnings(self, temp):
+        # Reset temperature warning threshold after timeout
+        if time.time() < self.last_temp_warn + 60: self.temp_thresh = 80
+
+        if self.temp_thresh < temp:
+            self.last_temp_warn = time.time()
+            self.temp_thresh = temp
+
+            self.log.info('Hot RaspberryPi at %d°C' % temp)
+
+
+    def callback(self):
+        try:
+            temp = read_temp()
+
+            self.scale_cpu(temp)
+            self.update_camera(temp)
+            self.log_warnings(temp)
+
+        except: self.log.exception()
+
+        self.ioloop.call_later(5, self.callback)
index 7a0d4992b05c67ccb83f0f3f9dffb63dc1d475c3..51e7c169d873193ea44373103aa85024c70d75cb 100644 (file)
@@ -479,14 +479,17 @@ class Web(tornado.web.Application):
         self.ioloop = ioloop
         self.ctrls = {}
 
-        # Init controller
-        if not self.args.demo: self.get_ctrl()
-
         # Init camera
         if not args.disable_camera:
             if self.args.demo: log = bbctrl.log.Log(args, ioloop, 'camera.log')
             else: log = self.get_ctrl().log
             self.camera = bbctrl.Camera(ioloop, args, log)
+        else: self.camera = None
+
+        # Init controller
+        if not self.args.demo:
+            self.get_ctrl()
+            self.monitor = bbctrl.MonitorTemp(self)
 
         handlers = [
             (r'/websocket', WSConnection),
index b8eed91cb489a6abe696ea3a2a540ab920002d81..3f54b1937227b79d27a884421cbb187f94bebacb 100644 (file)
@@ -58,6 +58,7 @@ from bbctrl.Camera import Camera, VideoHandler
 from bbctrl.AVR import AVR
 from bbctrl.AVREmu import AVREmu
 from bbctrl.IOLoop import IOLoop
+from bbctrl.MonitorTemp import MonitorTemp
 import bbctrl.Cmd as Cmd
 import bbctrl.v4l2 as v4l2
 import bbctrl.Log as log
diff --git a/src/resources/images/overtemp.jpg b/src/resources/images/overtemp.jpg
new file mode 100644 (file)
index 0000000..d39b31a
Binary files /dev/null and b/src/resources/images/overtemp.jpg differ