Show by Label

Sunday, September 1, 2019

Upgrading & Tuning my FY6600 Waveform Generator

For a while, I had contemplated to upgrade my FeelTech FY600-30MHz Dual Channel Waveform Generator.
This is a very interesting instrument that has specifications that would be comparable to instruments hundreds of Euro's more expensive. Unfortunately, the manufacturer of the design took some short-cuts to penny-pinch some more profit out of the device, and that causes some problems.

Initially I didn't care too much, the instrument performed very well for me, but when I started to dig into high precision oscillators and the likes, see my other posts, I needed a higher precision counter function. This can be rather easily done by replacing the main oscillator but I also wanted to see what other people did and what else was possible.

There is a huge number of postings about this instrument on this blog : eevblog.com , after reading through all of them (yes really - it took me three days) I decided to tackle the three most important issues.

These are four main issues reported by the many users on the forum.
  1. A very poor switching power supply with no earth ground. This could put an AC voltage as high as 1/2 of the main voltage on the BNC outputs. The current is very little though, and is caused by the leakage of a capacitor, but still.
  2. Distortion of the waveforms at higher amplitudes. This is caused by the selection of rather inexpensive and older design opamps. Interestingly, there are provisions on the PCB to install much better opamps.
  3. A rather poor main oscillator with poor precision and significant drift. The drift is mainly caused by the fact that the oscillator is way too close to three voltage regulators that get pretty hot.
  4. The attenuation circuit in the output section has an incorrect resistor value.

4. The attenuation issue
At this moment I'm not going to address that problem.

3. The oscillator
The recommended replacement oscillator for the CTS 50MHz 50ppm CB3 version is the model D75J from Connor Winfield. This is a surface mount temperature compensated crystal controller oscillator (TCXO) in the 50Mhz version, calibrated at 25 degrees at 1ppm and a stability of 2ppm. Incredible precision and stability! Because the D75J is a temperature controlled oscillator, the effects of the nearby regulators are pretty much eliminated. This is especially important if you use the FY as a counter.

To reduce some heat hot-spots from the three on-board regulators near the oscillator, I added two small "sticky tape" heat-sinks (Raspberry Pi type) on top of the three regulators. The temperature of the heatsinks after more than two hours is now about 46 degrees C, and more evenly spread.

After I installed the new TCXO, I measured the precision against a calibrated oscillator.  I measured the frequency of this oscillator to be 5,000,001.7 Hz. This is very close to the calibrated value of 5,000,002.1 Hz, but that factory calibration was eons ago, although the oscillator was never used until very recently (see my other post: Frequency Generator with Fast Edges). Either way, that is a remarkable precision and the reading over a few hours only changed 0.1Hz. The heating of the temperature inside the FY enclosure no longer seems to have an effect. The next step is to measure the result with a GPS Disciplined Oscillator (GPSDO), a project I'm currently working on.

2. The opamps
I replaced the single dual output amplifier with two single THS3095 opamps. I also increased the +/1 12V to +/- 15V to give them more room. The performance at higher output voltages and at higher frequencies is now much improved. Still not great, but much better, at least worth the small investment and trouble to do this. The best performance is still with 5V p-p and lower output voltages. I wonder what the higher frequency models actually deliver, if mine at 30MHz is already very poor.

1. The power supply
The power supply has been talked about many times on the forum, with several options offered, but there are only a few examples for a replacement.  I decided to design a completely new supply, that would let me adjust the +/- 12V outputs to +/-15V as well. The new output opamps need this voltage to display the waveforms without distortion.

Going through my parts collection, I picked a 24VA block transformer that could be mounted on a protoboard. The transformer is a little too heavy (in VA) with 2 separate winding's of 12VA each, and an output of 15VAC. Both secondary winding's are fused with 0.8AT PTC fuses. The primary has two separate 115V winding's. Because there are seperate windings, I took advantage of that feature, and created two separate supplies. The advantage is that you can use full bridges on either one, saving on the size of the buffer cap, and you can avoid ground problems, especially easy to do when you are using perf- or protoboard instead of  a real pcb layout. I tied the supplies together at the connector going to the main board, not anywhere else. I also wanted to avoid the inevitable switching noise coming from the DC-DC regulator going to the -12V supply as much as possible. Any noise on the 12V rails can/will make it to the output signal. After measuring the final results, I added another 330nH inductor to the plus input of the DC-DC convertor, to prevent more switch noise from being injected back into the 12V supply. This added inductor is not shown on the circuit diagram below.

The weight of the transformer will prevent the instrument from sliding as much, because the instrument was so light before, a bonus.

I decided to use the LM318 and LM338 voltage regulators for both the +/- 12..15V analog supplies. The only specialty in this circuit  is around the 10-turn trimmers, because they are China quality, and therefore cannot really be relied upon. The worst case is when the runner looses contact, creating a much higher output voltage then you intended, and this could potentially blow up the output amplifiers of the FY6600. The way I selected the adjustment components is such that you can get just below 12V and just above 15V. Depending on the toleration's of your parts, you may have to adjust a resistor value here and there. I bread-boarded the circuits before I mounted them, just to be sure.

For the +5V supply used for the digital circuits, I selected a simple DC-DC Buck convertor because doing that with normal linear regulators would create too much heat (burning off the difference between 20V and 5V at 500mA). I specifically did not want to add a fan, with all it's generated high frequency switching noise inside the box. I also reused some parts from the old supply, specifically the line filter and the 5V choke.

My unit is intentionally still floating. This can be fixed easily with a BNC/USB connection to another earth grounded instrument. If I change my mind, I can still add a 4mm connector to the back, tie that to the power ground and make a connection to earth ground with a test lead that way.

Here is my design:



And here is the unit with the supply installed:



Just above the upper left hand corner of the transformer, you can see the two little heatsinks I used on top of the three regulators to disperse their heat. Just above it you can see the metal can oscillator that I'm going to replace. The person that did the layout of the board had no clue as to the heat effect of the regulators on the stability and precision of the oscillator. 👿

I did not have room on the protoboard for the DC-DC convertor, so I used double sided sticky tape to put it on top of the transformer and moved it away from the analog section of the main board. These DC-DC devices are notorious for their switching noise superimposed on the output voltage. Using the 10uH inductor from the old supply and a few capacitors on the output reduced the 25mV ripple at about 1MHz to about 7mV, which should be OK.

When I started the testing, I had to resort to a little kludge because the heatsink I used was too small. In hindsight, I should have used two separate and larger heatsinks. Doing that now would cause some major and ugly surgery. Bolting another one on it is now better with the supplies at +/- 12V, it will be significantly cooler when I change them to +/- 15V. (less of a voltage drop over the regulators) At +/- 12V, the heatsink is now just below 50 degrees C. After more than two hours, the temperature inside the case is about 35 degrees C, at a room temperature of 27 degrees C, which is a bit warmer than I like but fine for now.

After I raised the +/- 12V supplies to +/ 15V to give the output amps more headroom, the reduced voltage drop over the regulators decreased so much that the extra heatsink kludge is no longer needed.

With all the mods done, I'm happy with the results.
I am actually most impressed with the precision of the counter function now. With my GPSDO nearing completion, I will be able to really check it out.

Enjoy!


Friday, August 23, 2019

Frequency Generator with fast edges

This post describes the building of a 20MHz frequency generator with a 1-2-5 frequency selection down to 1 Hz through a rotary decoder. The frequency generator is a stable high precision 5MHz oscillator. The device also has fast rise (>=1ns) output edges that are generated through a Fast Rise assembly taken from a Tektronix PG506 Calibrator.

The main controller is a PIC-AXE 14M2, I also use a second 18M2 controller to serially drive the LCD display. The PIC-AXE chips are very easy to use and are programmed in BASIC. PIX-AXE has a great Software Development Environment (SDE), which I think is even better than the Arduino SDE. http://www.picaxe.com/

For about 40 years, I held on to a Fast Rise Board assembly from a PG506 dating back to my time at Tektronix. The unit was defective coming out of a unit that was still under warranty (actually blown up by the user) and could not be repaired without some significant surgery. With some help I was able to fix it. For a long time I wanted to reproduce part of the PG506 Calibrator to test and adjust DMM's and oscilloscopes and although I even had a prototype for the Standard Amplitude amplifier up and running, I never saw the real need to spend some significant money on the required precision parts to finish it.

When I started to investigate the DIY build of a differential probe, see one of my other posts, I needed a fast edge pulse to measure the bandwidth in lieu of using much better equipment that I don't have.

For a long time I already had the PIC-AXE program and the frequency selection designed and prototype'd so the time was right to put it all together.

Please keep in mind that this is a quick-and-dirty "put together". Dealing with fast pulses with frequencies at 20MHz on proto-board are typically not very forgiving for glitches due to the fast switching of devices, and this is no exception. When (if) I decide to build a final version, I will have to make a proper PCB with power and ground planes and do a much, much better job at decoupling.

In any case, if you're interested, here is the design that I put together.

Clock generator

Above is the circuit diagram. Below is a picture of the oscillator I'm using. It's an old unit that was in my parts collection for 40 odd years as well. It was calibrated and measured to deliver 5,000.0021 KHz at 27 degrees Celsius. That's only 2.1ppm by the way.

A clock of 5MHz does not really work well in a 1-2-5 divider sequence, so I used a PLL chip to multiply the 5MHz oscillator output to a 20MHz clock. The output of the 5MHz oscillator also goes to the front panel through a few buffers.


Because of the constraints of the enclosure, I mounted it upside on the top cover so it's still in the proper position. It's not "ovenized" I think, it draws very little current, but the designers must have designed it with certain temperature parameters in mind, so I didn't want to mount it on its side or upside down.



Frequency Selection
The 20Mhz oscillator signal comes in at the top left of this diagram. It is divided by 7 74LS390 decade counters (two per package) into the main frequencies. All the outputs go to an 8-input multiplexer, a 74LS151. The selection of the 8 inputs goes through 3 inputs (A, B and C) and are driven by the controller. The output of the multiplexer then goes through a divider section of two 74LS109 flip-flops that divide the selected frequency through 2 or 4 to get the intended 1-2-5 frequency selection sequence. The divider outputs are selected through three OR-gates, who are also driven by the controller through NOR gates. The output of the OR-section is buffered and goes to the Fast Rise board, and also the the output BNC connector. (one still to-do is to replace U11.6 with the spare F02 and add that F04 gate in parallel to the output buffer section to get a bit more diving capability)

The controller also drives transistor Q1, which is used to activate a small relay. The relay switches the positive and negative power to the Fast Rise board. The Fast Rise board gets very warm when used, and that does not work well with a precision oscillator, so I decided to switch it off when not used, similar to the operation in the PG506. The zener diode is used to lower the 16.5V to the 12V DC for the relay.


CPU Circuit
The main controller circuit is quite simple. On the top left is a stereo connector that is used to program the device in-circuit, through the PIC-AXE USB to serial cable. The main 14M2 controller sends out a serial stream to the display controller, which is an other PIC-AXE controller, a 18M2 version. The main outputs of the 14M2 controller are the ports that drive the mux frequency selection and the /1 /2 and /4 step selection, in addition to driving the power for the Fast Rise board.
The selected settings are transmitted over a serial interface to another PIC-AXE controller on the LCD board section, and that drives the LCD display.
Lastly, the controller also deals with the rotary encoder. Pushing the rotary encoder button toggles the Fast Rise board power. Turning it left or right will select the output frequency.

Display Circuit
The display circuit consists of a PIC-AXE 18M2 controller that takes the serial input data from the main processor and multiplexes that to a typical LCD display. Due to space requirements, I selected an 8x2 LCD display. The code for this controller is a standard program that can be downloaded from the PIC-AXE site (Serial LCD). I used a ribbon cable to connect the display controller to the LCD display on the front panel.


DC Power Circuit
Due to space and noise considerations, I used an external 15-0-15VAC 2x300mA transformer. I have a number of these transformers in various voltages and housings and use them to power several of my tools. They all have 4mm test lead connectors, and I use a 3-pole DIN connector to connect the AC voltages to the power board.

For this instrument, I need three different voltages, +5V for all the logic, and +16.5 and -16.5 in addition to +5V for the Fast rise board.

NOTE:
With the 1.000uF capacitor (C4) in the positive supply side, there was a mains ripple on the +16.5V output when the Fast Rise board was activated. The Fast Rise board consumes a lot of power from the +16.5V and the +5V supply. The raw AC supply sagged to just below the headroom of the LM317, creating a hum on the positive High Rise pulse. Using Schottkey diodes to create more headroom just didn't do it, so I added a 3.300uF in parallel to C4 and that fixed that problem. When (if) I decide to turn this circuit into a real PCB, I will redesign the power section and use high efficiency/low drop regulators and do a better job decoupling.


PG506 Fast Rise Board
The design of this circuit is a typical testament to the skills of the Tektronix analog designers, that were among the best in the industry in those days.

 
The code
The code for the main controller and the serial LCD driver can be found on my Github site: Frequency-Generator

The various parts as I built them look like this:

This is the power board. The little black square is the switching 5V regulator. I don't show the extra 3.300uF cap here, that was added much later.


The Fast Rise board is at the very top. To the left is the front panel with the 8x2 LCD.
On the bottom of th case (in the middle here) is the main board. On the top left of that board is the PLL on an SMD carrier and the 4 LS390 decade counters. Next to them the 74LS151 mux and the LS109 flip-flops. Top right is the programming connector for the main controller. In the middle to the left is the 18M2 display controller and the adjustment for the display contrast. Next to that the 3 pins for the oscillator connection. To the right are the F32 OR gates and F02 NOR gates.
On the bottom row from left are the 14M2 main controller, the 74F04 buffers and in yellow the relay.


The standard Power-up screen after de device has booted up, displaying the version number of the software.

On the left side are the two Fast Rise output BNC's. On the top the positive edge, and below it the negative edge.
Below the LCD display are the main output BNC and to the right the 5MHz output of the oscillator. Top right is the rotary encoder with push button, and below it the output voltage adjust for the Fast Rise pulses.


Performance
How does it perform?

I'm using a 74F04 as the output drivers for my clock signals, and use two gates with a series resistor of 50Ohm. They produce a nice and steady 4ns rise time, using a 1mtr coax terminated in 50Ohm at the scope input.

I think I can improve on the cross talk and glitches, but that will be when (if) I design a proper PCB.


Above is the 5MHz oscillator signal taken from the BNC connector on the front panel. Note that the end of the BNC cable is terminated into a 50Ohm coax terminator before it goes to the scope input. Because of the poor decoupling on the main circuit board, the switching transitions of the various gates and the PLL are clearly visible. I don't care too much at the moment, more important for me right now is the frequency selection, the timing precision and the Fast Rise outputs.



Above is the 20MHz pulse, again terminated into 50Ohm.



Above is the 100KHz signal terminated into 50Ohm. The two gates of the 74F04 driver for the output are doing a good job, note the rise-time of 3.7ns.


Pushing the rotary encoder toggles the Fast Rise board power.




And here is the beautiful positive edge of the Fast Rise board. According to the PG506 specification, the Fast Rise edge should be <=1ns. This is at the limit of my RIGOL DS2072A (with the 300MHz hack - it reports that it is now a DS2302A).
The rise-time is not a steady 1.250ns, it varies a little. I was lucky when I pressed the print button on the scope to get this picture.



The pulse of the Fast Rise at 100KHz looks like this:



Calculating bandwidth from rise time
The "rule of thumb" calculation to infer bandwidth from rise time is BW = 0.35 / rise time for (Gaussian type) analog scopes and for DSO's it should be BW = 0.40  / rise time.

The RIGOL specifications for the DS2302A list a 300MHz bandwidth (@ 3dB) with a typical rise time of 1.2ns.
If we use the rise time specification of 1.2ns for the RIGOL with the commonly used factor of 0.40, we get 333MHz, on paper...

In the above measured case, the calculated bandwidth is 0.40 / 1.3ns (max) = approx. 307MHz. Spot on!

Master clock precision
After I upgraded my FY6600 Function Generator & Counter with a 1ppm precision TCXO oscillator, I measured the frequency of the master oscillator to be 5,000,001.7 Hz. This seems to be a little better that the part got after calibration (5,000,002.1 Hz). Either way, that is a remarkable precision for such an old and never used part.

The next step is to measure it again as soon as I have my GPS driven oscillator ready, which is a project I'm starting right now. If all goes as planned, this should allow me to verify the clock precision of my counter within 1ppm, and also allow me to calibrate the master clock again.

Todo's?
As mentioned earlier, there are some things I could do to get rid of the noise, glitches and cross talk, but right now, I'm very happy with the results.

Enjoy!

Thursday, May 30, 2019

Keeping an eye on my dad

It's probably not what you think...

My dad lives alone in an apartment all by himself. He was born in 1925 and now a little wobbly on his feet. We already lost my mom a few years ago, after she wondered around (advanced stage of Alzheimer's) during the night and fell en broke her hip while in a nursing home. Recently, a lady next door to him was found after 14 hours while lying on the floor. She fell and could not get to the alarm button or could not operate it. My dad has one of those buttons too, en he claims to have it on him all the time, certainly after the mishap to his neighbor. A number of years ago, an aunt from my wife fell badly in her home, and broke her arm and ankle. She could not get to the telephone, and was only found after almost two days. Somebody I know from our tennis club had a hart attack while sitting in his chair, also living alone, and it took 4 days before they found out. I want to spare my dad (and us) the same ordeal.

A little while ago, I stumbled on a website from a guy that is an expert on Computer Vision. He publishes many applications that use a camera of software to do recognition and tracking of objects. This peaked my interest. I had a RPi camera already in my stash for at least two years, with the intend to fabricate a burglar alarm. The applications I found were using Motion, but I didn't think much of it, so the camera was left in the box.

Here is the website where I found many interesting applications where I learned a lot from : https://www.pyimagesearch.com

In the process of learning I downloaded the code and tried many posts. After a while I zoomed in on a post that counted people.
https://www.pyimagesearch.com/2018/08/13/opencv-people-counter/

It looked the right thing for me. The reason I picked this particular application is as follows.
I want to measure movement, not presence. Image the situation where my dad sits in a chair, or falls and lies on the ground. Knowing that he's there does not help him, nor me. I want to know that he is moving around in the apartment.

So tracking him crossing an imaginary line seemed to be the right approach. I changed to code from the application to have a vertical line he needs to cross, and I tried several iterations to get it to work. The problem I ran into was speed. I'm using an RPi Model3 B+ and that is simply not fast enough to reliably recognize my dad as a "person" and track him. When the RPi Model 4 came out, I tried that and sadly, even that is too slow for this appllication as well.

I tried many things to speed things up, but the RPi was already running flat out on all cylinders (4 cores). Just when I was about to shelve the project, I found another post on the website that rejuvenated my interest. This post was about the recently launched device from Google, the Coral TPU USB accelerator.
Here is that post : https://www.pyimagesearch.com/2019/04/22/getting-started-with-google-corals-tpu-usb-accelerator/

I read all I could find about this device on the internet, which is not much at the moment. There was a very impressive Youtube video where Google folks showed what this device could do, and so I bit the bullet and ordered one.

It arrived after a few days, and I quickly installed the driver software and tried the demo programs, they all worked flawlessly, so my initial doubts where quickly diminishing.

I then started a new app using this device, while retaining some of the structure and elements I used before. I was amazed at the speed. There is no longer a need to do recognition every 40 frames and use the cpu lighter tracking method in between. The Coral runs so fast that I can use recognition on every frame and have a very smooth display of movement. I use htops version 2.2 to track the activity on the cores, and also track the core temperature and speed. It now runs with less than 40% load and the core temperature never goes much above 60-70 degrees C. When my kit is installed in a cabinet, it may get warmer, so I'm using a temperature driven fan. This is described on this forum as well. By the way, I'm running the Coral at the normal speed, there is no need to go faster.




Needless to say, I am very enthusiastic about this solution.

As you can image, I softly introduced my dad to the idea of using a device that would track his movement. He knows I'm not taking a video and invade his personal space. He was fine with the idea, and I have shown him a few early prototypes to get him used to the idea. After I finished the version with the Coral TPU, I took the kit over to his apartment to do a first tryout there, and see if I could really do what I wanted to do. I also needed to find a nice spot for the RPi and the camera.

Thinks worked very well right from the start, so we were both enthusiastic. I also found a nice place to put the stuff. He has a flat screen TV that is located in a cabinet. On the back is an unused screw hole that is there to mount a wall frame to the TV. I can use that to mount the RPi case and the Coral TPU. I can then use double sided tape to attach the camera enclosure on top of the TV. The location is perfect. The camera is facing an area in his living room where he needs to walk through to get to the kitchen, front door, or bathroom.

I'm currently running full-blown tests in my own "lab", to make sure everything works.
I'll be updating things as I get further into the deployment.

Because the RPi is located in a cabinet, together with some other equimpent, mainly video, it gets a little warmer than I like. I concocted a little board that sits on top of the RPi P1 GPIO connector, and drives the fan, the buzzer, the shutdown button (just for emergencies), the stop_app button and the stop_app LED.





The fan I use is a favourite of mine. I use it on all my RPi's that need it. It's small and very silent. Not cheap though! The model is the Noctua NF-A4x10 5V. It comes with silent mounting parts and a number of cables, that I don't use. The buzzer is one of those small round things that can be had for a very small price.



I did not take the time to find the 3D model for the P1-connector. It is obviously the 20 pin female part. I have a number of them in my stash, so I used that. It fits on all RPi models.

To make sure the RPi does not get hung-up, I use the watchdog. Have a look here for more details :
https://www.raspberrypi.org/forums/viewtopic.php?f=29&t=147501
It's a post that I just recently upgraded with the latest information.

After a few weeks of using, I made a number of adjustments and improvements. In the meantime, I also put the code up on Github.  Only the code posted there shows the latest versions.

https://github.com/paulvee/dad-watcher

Have fun!

********************************************************************************Here are some explanations of the program and it's features.

Movements are tracked, and when there is no movement for a longer period, I sound a beeper with an increasing loudness to alert my dad that he needs cross the eye, or I will get a message. The reason I'm doing this is to make sure I limit the number of messages, and if something happens to him, he knows that help will be on the way pretty soon.
The message I get will be an SMS to my smartphone. I can then first call him to see what is going on, or alert the local help desk in the apartment building.  Next step is for myself to get over there as fast as I can. I can be there in 10-15 minutes. If I can't, I can call my brother or sister and let them know what is going on.

The tiny beeper is connected through a transistor to a GPIO pin and is fed by the 5V supply of the P1 connector.

There is no need to run this program the whole day. I decided to activate the application several times during the day when he is most likely to be home. I use cron to start and stop the application. When the app is running, activities get logged into a file. At the end of every day this logfile is emailed to me. Initially I will collect a lot of data in this file, but can reduce it (set DEBUG to False in the configuration file) when the test phase is over.

I use a configuration file for key parameters that make it easy for me to change them while I'm at his place.
The file is a json formatted structure that allows comments.
The name of the file is conf.json
{
    "DEBUG": true,      // (true) add more information to the log file or (false)
    "DAEMON": false,    // if false, systemd is in control, otherwise use the console
    "SMS": false,        // if true, send SMS
    "TEST": true,       // if true, beeps at recognition of person
    "alarm_time": 60,   // time the alarm beeper is running in seconds
    "window": 3500,     // time movement detection window in seconds (58 minutes)
    "confidence": 0.8,  // recognition value, the higher the more reliable
    "twilio_account_sid": "", // for Twilio
    "twilio_auth_token":  "",   // for Twilio
    "from_cell": "",        // Twilio cell number
    "to_cell": "",          // your target cell number
}


DEBUG = True : activate a lot of logging
DAEMON = True : when the program runs normally, activated by cron. There is no screen output. With DEAMON = False, you can see the activity on the console used during testing.
SMS = True : will send out SMS messages, False will turn that off
alarm_time = number of seconds the beeper will sound just before the SMS goes out. Crossing the eye will reset the counter
window = number of seconds in which a movement must be registered, otherwise the alarm phase will start.
confidence = is used for the recognition factor for the model. I want it to be pretty high to avoid stray recognitions.

********************************************************************************

I use systemd and cron to start and stop the dad_watch script and just cron for the send email scripts and app_stop.
Note that I use the root version of cron!
In order to do that, use sudo su and than crontab -e
Here is a list of the cron entries:

0 8 * * * /bin/systemctl start dad_watch.service
5 9 * * * /bin/systemctl stop dad_watch.service

0 11 * * * /bin/systemctl start dad_watch.service
5 12 * * * /bin/systemctl stop dad_watch.service

0 13 * * * /bin/systemctl start dad_watch.service
5 14 * * * /bin/systemctl stop dad_watch.service

0 16 * * * /bin/systemctl start dad_watch.service
5 17 * * * /bin/systemctl stop dad_watch.service

0 20 * * * /bin/systemctl start dad_watch.service
5 21 * * * /bin/systemctl stop dad_watch.service

0 22 * * * /usr/bin/python3 /home/pi/send_mail.py

0 23 * * * /usr/bin/python3 /home/pi/clear_stop_flag.py

********************************************************************************
The following is a script that will allow the user to disable the running of the watch program in case he or she is not in the room
for a longer period. An entry in cron will reset the state late in the evening just in case it is forgotten

#!/usr/bin/python3
#-------------------------------------------------------------------------------
# Name:        check_app_stop.py
# Purpose:     Check if a pushbutton was pressed to prevent services from running.
#               If the button is pressed, a file in the home directory is
#               created that systemd will check before starting the application.
#
# Author:      paulv
#
# Created:     10-06-2019
# Copyright:
# Licence:     <your licence>
#-------------------------------------------------------------------------------

import RPi.GPIO as GPIO
import subprocess
import time
import os

DEBUG = False

GPIO.setmode(GPIO.BCM) # use GPIO numbering
GPIO.setwarnings(False)

file_name= "/home/pi/do_not_run"

BUTTON = 24 # GPIO-24
# pulled-up to create an active low signal
GPIO.setup(BUTTON, GPIO.IN, pull_up_down=GPIO.PUD_UP)
LED = 23    # GPIO 23
GPIO.setup(LED, GPIO.OUT, initial=GPIO.LOW) # active high


def main():

    # when the program starts (after a boot), remove the file flag
    # if the file is still there
    if os.path.isfile(file_name):
        if DEBUG : print("@boot: removing file flag")
        subprocess.call(["rm {}".format(file_name)], shell=True, \
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    try:
        if DEBUG : i = 0
        while True:
            # set an interrupt on a falling edge and wait for it to happen
            GPIO.wait_for_edge(BUTTON, GPIO.FALLING)

            # if the interrupt happends, we'll land here
            if DEBUG:
                i += 1
                print ("Button press detected {} {}".format(i, GPIO.input(BUTTON)))

            #filter out short presses and glitches
            time.sleep(0.5)
            if GPIO.input(BUTTON) == 0:
                if DEBUG : print("button is really pressed")
                if not os.path.isfile(file_name):
                    if DEBUG : print("Stop application is requested, creating file flag")
                    subprocess.call(["touch {}".format(file_name)], shell=True, \
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    # turn the warning LED on
                    GPIO.output(LED, GPIO.HIGH)
                else:
                    if DEBUG : print("file is there, remove it")
                    subprocess.call(["rm {}".format(file_name)], shell=True, \
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    # turn the warning LED off
                    GPIO.output(LED, GPIO.LOW)

    except KeyboardInterrupt:
        if DEBUG : print("\nCtrl-C - Terminating")
    finally:
        # remove the file if still there
        if os.path.isfile(file_name):
            if DEBUG : print("shutdown: removing file flag")
            subprocess.call(["rm {}".format(file_name)], shell=True, \
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        GPIO.cleanup()

if __name__ == '__main__':
    main()



********************************************************************************
This is the routine called by cron to clear the flag if it was set.


#!/usr/bin/python3
#-------------------------------------------------------------------------------
# Name:        clear_stop_flag.py
# Purpose:     Clear the manually set file flag that stops the application from
#              running.
#              This will be executed at cron around 23:00 hrs.
#
# Author:      paulv
#
# Created:     10-06-2019
# Copyright:
# Licence:     <your licence>
#-------------------------------------------------------------------------------


import subprocess
import os

DEBUG = False

file_name= "/home/pi/do_not_run"


def main():

    # remove the file flag if the file is still there
    if os.path.isfile(file_name):
        if DEBUG : print("removing file flag")
        subprocess.call(["rm {}".format(file_name)], shell=True, \
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)

if __name__ == '__main__':
    main()



********************************************************************************

Here is a quick and dirty email program that I use to send me the log file

#!/usr/bin/python3
#-------------------------------------------------------------------------------
# Name:        send_mail.py
# Purpose:     send an email with a file attachment
#
# Author:      paulv
#
# Created:     25-04-2019
# Copyright:   (c) paulv 2019
# Licence:     <your licence>
#-------------------------------------------------------------------------------

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

def send_mail():
    fromaddr = "email address"
    toaddr = "your email address"
    msg = MIMEMultipart()
    msg['From'] = fromaddr
    msg['To'] = toaddr
    msg['Subject'] = "Dad_watch activity log file"
    body = "Today's log file"

    filename = "/home/pi/dad_watch.log"
    with open(filename, 'r') as f:
        attachment = MIMEText(f.read())
    attachment.add_header('Content-Disposition', 'attachment', filename=filename)
    msg.attach(attachment)

    msg.attach(MIMEText(body, 'plain'))
    server = smtplib.SMTP('smtp.gmail.com:587')
    server.starttls()
    server.login(fromaddr, "your password")
    text = msg.as_string()
    server.sendmail(fromaddr, toaddr, text)
    server.quit()

def main():
    send_mail()

if __name__ == '__main__':
    main()


********************************************************************************

Below is the script that controls the fan to cool the SOC of the RPi.
It uses the actual temperature coming from tne SOC and uses PWM to control the 
speed of the fan.

#!/usr/bin/python3
#-------------------------------------------------------------------------------
# Name:        run_fan.py
# Purpose:     Use PWM to run a fan to keep the core temperature in check
#                    This program is managed by a systemd script
#
# Author:      Paul Versteeg
#
# Created:     01-12-2013, modified june 2019
# Copyright:   (c) Paul 2013, 2019
# Licence:     <your licence>
#-------------------------------------------------------------------------------

import RPi.GPIO as GPIO
from time import sleep, strftime
import subprocess
import shlex
import string
import sys, os
import traceback

DEBUG = True

LOG_FILENAME = "/home/pi/dad_watch.log"

FAN_PIN = 17 # GPIO 17

GPIO.setwarnings(False) # when everything is working you could turn warnings off
GPIO.setmode(GPIO.BCM)  # choose BCM numbering scheme.
# set GPIO port as output driver for the Fan
GPIO.setup(FAN_PIN, GPIO.OUT)


Fan = GPIO.PWM(FAN_PIN, 100) # create object Fan for PWM on port 22 at 100 Hertz
Fan.start(0)            # start Fan with 0 percent duty cycle (off)

delay = 30              # seconds of delay, testing the core every 30 seconds is OK
cool_baseline = 60      # start cooling from this temp in Celcius onwards
pwm_baseline = 40       # lowest PWM to keep the fan running
factor = 3              # multiplication factor
max_pwm = 100           # maximum PWM value
fan_running = False     # helps to kick-start the fan
log_fan = False         # used to limit the fan status conditions

def main():
    global fan_running, log_fan

    '''
    This program controls a Fan by using PWM.
    The Fan will probably not work below 40% duty-cycle, so that is the
    fan PWM baseline. The maximum PWM cannot be more than 100%.

    When the core temperature is above 60 'C, we will start to cool.
    When the core reaches 70 degrees, we would like to run the fan at max speed.

    To make the PWM related to the temperature, strip the actual temp from the
    cool baseline, multiply the delta with 3 and add that to the the baseline
    PWM to get 100% at 70 degrees.

    I have selected a PWM frequency of 100Hz to avoid high frequency noise, but
    you can change that.
    '''
    try:
        while True:
            # get the cpu temperature
            # need to use the full path otherwise root cannot find it
            cmd = "/opt/vc/bin/vcgencmd measure_temp"
            args = shlex.split(cmd)
            output, error = subprocess.Popen(args, stdout = subprocess.PIPE, \
                            stderr= subprocess.PIPE).communicate()

            # strip the temperature out of the returned string
            # the returned string is in the form : b"temp=43.9'C\n"
            # if your localization is set to US, you get the temp in Fahrenheit,
            # so you need to adapt the stripping somewhat
            cpu_temp =float(output[5:9]) # for Celcius

            if cpu_temp < cool_baseline :
                Fan.ChangeDutyCycle(0) # turn Fan off
                fan_running = False
                if log_fan :
                    tstamp = strftime("%H:%M:%S")
                    subprocess.call(["echo '           {}   *** Stopping fan' >> {}"\
                        .format(tstamp, LOG_FILENAME)], shell=True, \
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    log_fan = False

            if cpu_temp > cool_baseline :
                if fan_running :
                    duty_cycle = ((cpu_temp-cool_baseline)*factor)+pwm_baseline
                    if duty_cycle > max_pwm : duty_cycle = max_pwm # max = 100%
                else:
                    if not log_fan :
                        tstamp = strftime("%H:%M:%S")
                        subprocess.call(["echo '           {}   *** Starting fan' >> {}"\
                            .format(tstamp, LOG_FILENAME)], shell=True, \
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                        log_fan = True
                    # kick-start the fan for one cycle
                    duty_cycle = 70
                    fan_running = True

                Fan.ChangeDutyCycle(duty_cycle)   # output the pwm

                if DEBUG : print("core temp= {} pwm = {:.2f}".format(cpu_temp, duty_cycle))

            sleep(delay)

    # the following will allow you to kill the program, you can delete these lines if you want
    except KeyboardInterrupt:
        Fan.stop()      # stop the PWM output
        GPIO.cleanup()  # clean up GPIO on CTRL+C exit()

    except Exception as e:
        sys.stderr.write("Got exception: %s" % e)
        if DEBUG :
            print(traceback.format_exc())
            print("GPIO.cleanup")
        GPIO.output(FAN_PIN, GPIO.LOW)
        GPIO.cleanup(FAN_PIN) # leave the other GPIO-pins alone!
        os._exit(1)


if __name__ == '__main__':
    main()



********************************************************************************

Here is the current version of the main Python script.
Note that you must install the imutil and the twilio modules both as user pi (or whatever you use), and also as root. Otherwise Python cannot find the modules.
I use Twilio to send an SMS to my mobile. You can sign-up for a free account here : https://www.twilio.com/

#!/usr/bin/python3
#-------------------------------------------------------------------------------
# Name:        dad_watch.py
# Purpose:     Watching the activity of my dad, and send an SMS as warning when
#              no activity has been detected for a while.
#              This program is managed by a systemd script
#
# Author:      Paul Versteeg, based on idea and blog from Adrian Rosebrock
#              https://www.pyimagesearch.com/2018/08/13/opencv-people-counter/
# Modified:
#
# Created:     26-05-2019
# Copyright:   (c) Paul Versteeg
# Licence:     <your licence>
#-------------------------------------------------------------------------------

# import the necessary packages
from edgetpu.detection.engine import DetectionEngine
from imutils.video import VideoStream
from imutils.video import FPS
from PIL import Image
import argparse
import imutils    #pip3 install imutils @ sudo for running as root
import warnings
import datetime
# pip3 install json-minify and also sudo
from json_minify import json_minify
import json
import time
import cv2
import numpy as np
import os
import os.path
import sys
import shlex
import subprocess
import traceback
from threading import Thread
import logging
import logging.handlers
# https://www.twilio.com/
from twilio.rest import Client  # pip3 install twilio & sudo for running as root
import RPi.GPIO as GPIO
from multiprocessing import Process, Queue
import signal

VERSION ="2.2"  # added json_minify to allow comments in the configuration file
                # added Twilio vars to the conf file, load all conf varts at start
                # change all references to static vars in the code.
#VERSION ="2.1" # removed gpio.cleanup's to avoid exception in sound_alarm at shutdown
                # added ALARM check in timing deadline if statement

#filter warnings, load the configuration file
warnings.filterwarnings("ignore")
conf = json.loads(json_minify(open("/home/pi/conf.json").read()))

# load the configuration variables
DEBUG = conf["DEBUG"]
DAEMON = conf["DAEMON"]
SMS = conf["SMS"]
TEST = conf["TEST"]
alarm_time = conf["alarm_time"]
window = conf["window"]
confidence = conf["confidence"]
twilio_account_sid = conf["twilio_account_sid"] # for Twilio
twilio_auth_token = conf["twilio_auth_token"] # for Twilio
from_cell= conf["from_cell"] # Twilio cell number
to_cell = conf["to_cell"] # target cell number
whatsapp_from = conf["whatsapp_from"] # Twilio whatsapp cell number

if DEBUG :
    print ("DEBUG is {}".format(DEBUG))
    print ("DAEMON is {}".format(DAEMON))
    print("SMS is {}".format(SMS))
    print("TEST is {}".format(TEST))
    print("confidence {}".format(confidence))
    print("alarm_time is {}".format(alarm_time))
    print("window is {}".format(window))
    print("twilio_account_sid is {}".format(twilio_account_sid))
    print("twilio_auth_token is {}".format(twilio_auth_token))
    print("from_cell is {}".format(from_cell))
    print("to_cell is {}".format(to_cell))
    print("whatsapp_from is {}".format(whatsapp_from))


BEEPER = 4  # GPIO 4
ALARM = False      # alarm flag
ALARM_RUNNING = False


#--logger definitions
# save daily logs for 7 days
# the logfile will be mailed daily
LOG_FILENAME = "/home/pi/dad_watch.log"
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, when="midnight", backupCount=7)
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)


class MyLogger():
    '''
    A class that can be used to capture stdout and sterr to put it in the log

    '''
    def __init__(self, level, logger):
            '''Needs a logger and a logger level.'''
            self.logger = logger
            self.level = level

    def write(self, message):
        # Only log if there is a message (not just a new line)
        if message.rstrip() != "":
                self.logger.log(self.level, message.rstrip())

    def flush(self):
        pass  # do nothing -- just to handle the attribute for now


# --Replace stdout and stderr with logging to file so we can run it as a daemon
# and still see what is going on
if DAEMON :
    sys.stdout = MyLogger(logging.INFO, logger)
    sys.stderr = MyLogger(logging.ERROR, logger)


def get_cpu_temp():
    # get the cpu temperature
    # need to use the full path, otherwise root cannot find it
    cmd = "/opt/vc/bin/vcgencmd measure_temp"
    args = shlex.split(cmd)
    output, error = subprocess.Popen(args, stdout = subprocess.PIPE, \
                    stderr= subprocess.PIPE).communicate()

    # strip the temperature out of the returned string
    # the returned string is in the form : b"temp=43.9'C\n"
    # if your localization is set to US, you get the temp in Fahrenheit,
    # so you need to adapt the stripping somewhat
    #
    cpu_temp =float(output[5:9]) # for Celcius
    return(cpu_temp)


def init():
    global labels, model, vs, fps

    # set the GPIO mode
    GPIO.setmode(GPIO.BCM)
    if DEBUG:
        GPIO.setwarnings(True)
    else:
        GPIO.setwarnings(False)

    # set GPIO port as output driver for the beeper
    GPIO.setup(BEEPER, GPIO.OUT)

    # the core temp must be at the lowest at startup, so report it.
    logger.info("Core temp is {} degrees C".format(get_cpu_temp()))

    # initialize the labels dictionary
    print("parsing class labels...")
    labels = {}
    # loop over the class labels file
    for row in open("/home/pi/edgetpu_models/coco_labels.txt"):
        # unpack the row and update the labels dictionary
        (classID, label) = row.strip().split(maxsplit=1)
        labels[int(classID)] = label.strip()

    # load the Google Coral tpu object detection model
    print("loading Coral model...")
    model = DetectionEngine("/home/pi/edgetpu_models/mobilenet_ssd_v2_coco_quant_postprocess_edgetpu.tflite")

    # initialize the video stream and allow the camera sensor to warmup
    print("starting video stream...")
    vs = VideoStream(src=0).start() # webcam
#    vs = VideoStream(usePiCamera=True).start()  # Picam
    # let the camera sensor warm-up
    time.sleep(2.0)

    if not DAEMON :
        # start the frames per second throughput estimator
        fps = FPS().start()

    beep()


def send_sms(msg='no txt'):

    account_sid = twilio_account_sid
    auth_token = twilio_auth_token

    try:
        client = Client(account_sid, auth_token)

        message = client.messages.create(
                 body = msg,
                 from_= from_cell,
                 to = to_cell)

        logger.debug("sid = {}".format(message.sid))

    except Exception as e:
        logger.error("Unexpected Exception in send_sms() : \n{}".format(e))
        return


def beep():
    GPIO.output(BEEPER, GPIO.HIGH)
    time.sleep(0.05)
    GPIO.output(BEEPER, GPIO.LOW)
    return


def sound_alarm():
    '''
    Function to create a seperate thread to start an alarm beep.
    The alarm will be stoppen when movement has been detected again or
    when the total alarm time has been exceeded.

    '''
    global alarm_thread, ALARM_RUNNING, ALARM, SMS

    try:
        class alarm_threadclass(Thread):

            def run(self):
                global alarm_thread, ALARM_RUNNING, ALARM, SMS
                try:
                    start_time = datetime.datetime.now()
                    i = 1
                    while ALARM :
                        ALARM_RUNNING = True
                        GPIO.output(BEEPER, GPIO.HIGH)
                        time.sleep(0.1*i)
                        GPIO.output(BEEPER, GPIO.LOW)
                        time.sleep(5)
                        i += 1
                        max_alarm = (datetime.datetime.now() - start_time).seconds

                        # add ALARM in the test to catch a change during the sleep period
                        if (max_alarm > alarm_time and ALARM) :
                            logger.info("*** no movement during alarm, sending SMS")
                            if SMS :
                                send_sms("no movement during alarm fase")
                                SMS = False # send only one SMS per session
                            break

                    ALARM_RUNNING = False
                    logger.info("alarm thread ended")
                    ALARM = False
                    return

                except Exception as e:
                    logger.error("Unexpected Exception in sound_alarm :\n{0} ".format(e))

        alarm_thread = alarm_threadclass()
        alarm_thread.setDaemon(True) # so a ctrl-C can terminate it
        if not ALARM_RUNNING :
            alarm_thread.start() # start the thread
            logger.info("sound_alarm thread started")

    except Exception as e:
        logger.error("Unexpected Exception in sound_alarm() : \n{0}".format(e))
        return


def sig_handler (signum=None, frame = None):
    '''
    This function will catch the most important system signals, but NOT not a shutdown!

    This handler catches the following signals from the OS:
        SIGHUB = (1) SSH Terminal logout
        SIGINT = (2) Ctrl-C
        SIGQUIT = (3) ctrl-\
        IOerror = (5) when terminating the SSH connection (input/output error)
        SIGTERM = (15) Deamon terminate (deamon --stop): is coming from systemd
    However, it cannot catch SIGKILL = (9), the kill -9 or the shutdown procedure

    '''

    try:
        logger.info("Sig_handler called with signal : {}".format(signum))
        if signum == 1 :
            return # ignore SSH logout termination

        # the core temp must be at the highest now, so report it.
        logger.info("Core temp is {} degrees C".format(get_cpu_temp()))
        logger.info("Terminating \n")

        beep()
        time.sleep(0.5)
        beep()

        if not DAEMON:
            # stop the fps timer and display the collected results
            fps.stop()
            print("[INFO] elapsed time: {:.2f}".format(fps.elapsed()))
            print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))

        # do a bit of cleanup
        GPIO.output(BEEPER, GPIO.LOW) # force the beeper to quit
        if not DAEMON :
            cv2.destroyAllWindows()
            vs.stop()
        time.sleep(1)
        os._exit(0) # force the exit to the OS

    except Exception as e: # IOerror 005 when terminating the SSH connection
        logger.error("Unexpected Exception in sig_handler() : \n{0}".format(e))
        return



def main():
    global ALARM

    logger.info('\n\n dad_watch version %s' % VERSION)

    # initialize the frame dimensions (we'll set them as soon as we read
    # the first frame from the video)
    W = None
    H = None
    startX = 0
    startY = 0
    endX = 0
    endY = 0

    dir = None  # direction

    side = None
    old_side = None
    movement = 0

    init()
    boot_up= True # to avoide a false positive at startup

    # setup a catch for the following signals: signal.SIGINT = ctrl-c
    for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT):
        signal.signal(sig, sig_handler)

    # setup the movement timers
    lastSeen = datetime.datetime.now()
    lastSeen_ts = datetime.datetime.now()

    try:
        # loop over the frames from the video stream
        while True:
            # setup the presence timers
            timestamp = datetime.datetime.now()
            lastSeen_ts = datetime.datetime.now()
            interval = (lastSeen_ts - lastSeen).seconds

            # grab the frame from the threaded video stream and resize it
            # to have a maximum width of 500 or 800 pixels during testing
            frame = vs.read()

            if DAEMON :
                frame = imutils.resize(frame, width=500)
            else:
                frame = imutils.resize(frame, width=800)

            orig = frame.copy()

            # if the frame dimensions are empty, set them
            if W is None or H is None:
                (H, W) = frame.shape[:2]
                logger.debug("[INFO] H= {} W= {}".format(H,W))
                # Create a dividing line in the center of the frame
                # It is used to determine movement of the objects
                centerline = W // 2

            # make sure the alarm does not run indefinetely
            if (interval > (window + 100) and ALARM == True) :
                logger.info("reset the timer interval")
                lastSeen = datetime.datetime.now()
                ALARM = False

            # check if we went past the movement timing deadline...
            if ((interval > window) and (ALARM == False)):
                ts =  timestamp.strftime("%A %d %B %Y %H:%M:%S")
                logger.info("*** no movement alarm")
                ALARM = True
                sound_alarm()

            # prepare the frame for object detection by converting it
            # from BGR to RGB channel ordering and then from a NumPy
            # array to PIL image format
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frame = Image.fromarray(frame)

            # do the heavy lifting on the Coral USB tpu
            results = model.DetectWithImage(frame, threshold=confidence,
                keep_aspect_ratio=True, relative_coord=False)


            # loop over the results
            for r in results:
                # use the index to get at the properlabel
                label = labels[r.label_id]
                # we're only interested in persons
                if label != "person" :
                    continue

                # extract the bounding box and predicted class label
                box = r.bounding_box.flatten().astype("int")
                (startX, startY, endX, endY) = box

                # calculate the middle of the object
                position = (startX + endX) // 2
                # The centerline line is in the middle of the frame (W // 2)
                # Determine if the object movement is accross the centerline
                # If the middle of the object AND the right-hand side (endX) has
                # crossed, there was a complete move to the left.
                # If the middle of the object AND the left-hand side (startX) has
                # crossed, there was a complete move to the right.

                if position < centerline and endX < centerline :
                    side = "left"
                    # print("moving left")
                if position > centerline and startX > centerline :
                    side = "right"
                    # print("moving right")

                # if there is a change in the side, record it as a movement
                # avoid a false positive at startup
                if boot_up :
                    old_side = side
                    boot_up = False
                if side is not old_side:
                    movement += 1
                    if TEST : beep()
                    logger.info("movement {}".format(movement))

                    # reset the counter & alarm flag
                    lastSeen = datetime.datetime.now()
                    if (ALARM == True) :
                        ALARM = False

                old_side = side

                if not DAEMON :
                    # draw the bounding box and label on the image
                    cv2.rectangle(orig, (startX, startY), (endX, endY),
                        (0, 255, 0), 2)
                    y = startY - 15 if startY - 15 > 15 else startY + 15
                    text = "{}: {:.2f}%".format(label, r.score * 100)
                    cv2.putText(orig, text, (startX, y),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

            if not DAEMON :
                # show the output frame and wait for a key press
                cv2.imshow("Frame", orig)
                key = cv2.waitKey(1) & 0xFF

                # if the `q` key was pressed, break from the loop
                if key == ord("q"):
                    break
                # update the FPS counter
                fps.update()

    except KeyboardInterrupt:
        if (DEBUG) and (not DAEMON): print ("\nCtrl-C Terminating")

    except Exception as e:
        sys.stderr.write("Got exception: %s" % e)
        if (DEBUG) and (not DAEMON): print(traceback.format_exc())
        logger.error("exception in main() \n {}".format(traceback.format_exc()))

    finally:
        if not DAEMON :
            # stop the timer and display FPS information
            fps.stop()
            print("[INFO] elapsed time: {:.2f}".format(fps.elapsed()))
            print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))
        # do a bit of cleanup
        print("GPIO.cleanup")
        GPIO.output(BEEPER, GPIO.LOW)  # force beeper to quit
        GPIO.cleanup()     # cleanup does not really force the output to zero
        cv2.destroyAllWindows()
        vs.stop()

if __name__ == '__main__':
    main()



********************************************************************************

Here is the systemd profile. There is one special command that checks the presence of a file to decide to start the process. I use this to allow the user to prevent the system from running and possibly send out bogus SMS messages, if the user is not in the room for a longer period. If the file is present, the program will not start.

The service file is located here :  /etc/systemd/system/dad_watch.service
 

# This service installs a python script that runs the dad_watch python script.
# The execution of the target program is depending on the absence of a file.
# When the script crashes, it is automatically restarted.
# If it crashes too many times, it will be forced to fail, or you can let systemd reboot
#
# use systemctl daemon-reload if you made changes to this file.

[Unit]
Description=Installing dad_watch script
Requires=basic.target
After=multi-user.target

# a file existence is checked before the unit is started
ConditionPathExists=/home/pi/do_not_run


[Service]
ExecStart=/usr/bin/python3 /home/pi/dad_watch.py
Restart=always

# The number of times the service is restarted within a time period can be set
# If that condition is met, the RPi can be rebooted
#
StartLimitBurst=4
StartLimitInterval=180s
# actions can be none|reboot|reboot-force|reboot-immidiate
StartLimitAction=none

# The following are defined in the /etc/systemd/system.conf file and are
# global for all services
#
#DefaultTimeoutStartSec=90s
#DefaultTimeoutStopSec=90s
#
# They can also be set on a per process here:
# if they are not defined here, they fall back to the system.conf values
#TimeoutStartSec=2s
#TimeoutStopSec=2s

[Install]
WantedBy=multi-user.target
 

********************************************************************************