From f914c83267dcb0fded2b678f26b651fca2d9c5e7 Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Sun, 16 Aug 2020 11:57:12 -0700 Subject: [PATCH 01/20] Prototyping voice mail options --- src/app.cfg.example | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/app.cfg.example b/src/app.cfg.example index 841585a..ca3b055 100644 --- a/src/app.cfg.example +++ b/src/app.cfg.example @@ -30,14 +30,6 @@ SCREENING_MODE = ("whitelist", "blacklist") # BLOCK_ENABLED: if True calls that fail screening will be blocked BLOCK_ENABLED = True -# BLOCK_NAME_PATTERNS: A regex expression dict applied to the CID names -# Example: {"V[0-9]{15}": "Telemarketer Caller ID", "O": "Unknown caller"} -BLOCK_NAME_PATTERNS = {"V[0-9]{15}": "Telemarketer Caller ID", } - -# BLOCK_NUMBER_PATTERNS: A regx expression dict applied to the CID numbers -# Example: {"P": "Private number",} -BLOCK_NUMBER_PATTERNS = {} - # BLOCKED_ACTIONS: A tuple containing a combination of the following actions: # "greeting", "record_message", "voice_mail". # @@ -65,6 +57,15 @@ BLOCK_NUMBER_PATTERNS = {} # BLOCKED_ACTIONS = ("greeting", "voice_mail" ) # BLOCKED_ACTIONS = ("greeting", "voice_mail") +BLOCKED_RINGS_BEFORE_ANSWER = 0 + +SCREENED_ACTIONS = ("greeting", "record_message") +SCREENED_GREETING_FILE = "resources/general_greeting.wav" +SCREENED_RINGS_BEFORE_ANSWER = 0 + +PERMITTED_ACTIONS = ("greeting", "record_message") +PERMITTED_GREETING_FILE = "resources/general_greeting.wav" +PERMITTED_RINGS_BEFORE_ANSWER = 4 # BLOCKED_GREETING_FILE: The wav file to be played to blocked callers. # Example: "We're sorry, this call has been blocked by the Raspberry Pi @@ -72,6 +73,15 @@ BLOCKED_ACTIONS = ("greeting", "voice_mail") # justification to be unblocked." BLOCKED_GREETING_FILE = "resources/blocked_greeting.wav" +# BLOCK_NAME_PATTERNS: A regex expression dict applied to the CID names +# Example: {"V[0-9]{15}": "Telemarketer Caller ID", "O": "Unknown caller"} +BLOCK_NAME_PATTERNS = {"V[0-9]{15}": "Telemarketer Caller ID", } + +# BLOCK_NUMBER_PATTERNS: A regx expression dict applied to the CID numbers +# Example: {"P": "Private number",} +BLOCK_NUMBER_PATTERNS = {} + + # VOICE_MAIL_GREETING_FILE: The wav file played after answering: a general greeting # Example: "I'm sorry we missed your call..." VOICE_MAIL_GREETING_FILE = "resources/general_greeting.wav" From 07f7579ee4f573afdef97dcc8019ff682061a0b4 Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Sun, 16 Aug 2020 18:10:52 -0700 Subject: [PATCH 02/20] Prototyping ring count --- src/callattendant.py | 7 +++++-- src/hardware/modem.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/callattendant.py b/src/callattendant.py index 9076ef4..53bfa15 100755 --- a/src/callattendant.py +++ b/src/callattendant.py @@ -83,6 +83,7 @@ def __init__(self, config): # Hardware subsystem # Create (and starts) the modem with callback functions self.modem = Modem(self.config, self.phone_ringing, self.handle_caller) + self.ring_count = 0 # Messaging subsystem self.voice_mail = VoiceMail(self.db, self.config, self.modem, self.message_indicator) @@ -114,9 +115,11 @@ def phone_ringing(self, enabled): """ if enabled: self.ring_indicator.blink() + self.ring_count += 1 else: self.ring_indicator.turn_off() - + self.ring_count = 0 + print("> > > Phone ring count: {}".format(self.ring_count)) def run(self): """ @@ -201,7 +204,7 @@ def run(self): finally: # Go "on-hook" self.modem.hang_up() - + self.phone_ringing(False) except Exception as e: pprint(e) print("** Error running callattendant. Exiting.") diff --git a/src/hardware/modem.py b/src/hardware/modem.py index 89242f6..41d25e2 100644 --- a/src/hardware/modem.py +++ b/src/hardware/modem.py @@ -222,6 +222,7 @@ def pick_up(self): # Flush any existing input outout data from the buffers # self._serial.flushInput() # self._serial.flushOutput() + self.phone_ringing(False) except Exception as e: pprint(e) From 917f1f422a90f2edea8af362a06e5d3ba9bb196a Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Mon, 17 Aug 2020 08:09:33 -0700 Subject: [PATCH 03/20] More prototyping...experimenting --- src/callattendant.py | 51 +++++++++++++++++++++++++++++++++++-------- src/hardware/modem.py | 13 ++++++++++- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/callattendant.py b/src/callattendant.py index 53bfa15..176dff2 100755 --- a/src/callattendant.py +++ b/src/callattendant.py @@ -83,7 +83,6 @@ def __init__(self, config): # Hardware subsystem # Create (and starts) the modem with callback functions self.modem = Modem(self.config, self.phone_ringing, self.handle_caller) - self.ring_count = 0 # Messaging subsystem self.voice_mail = VoiceMail(self.db, self.config, self.modem, self.message_indicator) @@ -115,11 +114,8 @@ def phone_ringing(self, enabled): """ if enabled: self.ring_indicator.blink() - self.ring_count += 1 else: self.ring_indicator.turn_off() - self.ring_count = 0 - print("> > > Phone ring count: {}".format(self.ring_count)) def run(self): """ @@ -131,7 +127,11 @@ def run(self): screening_mode = self.config['SCREENING_MODE'] block = self.config.get_namespace("BLOCK_") blocked = self.config.get_namespace("BLOCKED_") + screened = self.config.get_namespace("SCREENED_") + permitted = self.config.get_namespace("PERMITTED_") blocked_greeting_file = os.path.join(root_path, blocked['greeting_file']) + screened_greeting_file = os.path.join(root_path, screened['greeting_file']) + permitted_greeting_file = os.path.join(root_path, permitted['greeting_file']) # Instruct the modem to start feeding calls into the caller queue self.modem.handle_calls() @@ -149,6 +149,7 @@ def run(self): # Perform the call screening caller_permitted = False + caller_screened = False caller_blocked = False action = "" reason = "" @@ -172,29 +173,59 @@ def run(self): self.blocked_indicator.blink() if not caller_permitted and not caller_blocked: + caller_screened = True action = "Screened" + self.approved_indicator.blink() # Log every call to the database (and console) call_no = self.logger.log_caller(caller, action, reason) print("--> {} {}: {}".format(phone_no, action, reason)) + if caller_permitted: + actions = permitted["actions"] + greeting = permitted_greeting_file + rings_before_answer = permitted["rings_before_answer"] + elif caller_screened: + actions = screened["actions"] + greeting = screened_greeting_file + rings_before_answer = screened["rings_before_answer"] + elif caller_blocked: + actions = blocked["actions"] + greeting = blocked_greeting_file + rings_before_answer = blocked["rings_before_answer"] + + # In North America, the standard ring cadence is "2-4", or two seconds + # of ringing followed by four seconds of silence (33% Duty Cycle). + if rings_before_answer > 0: + wait_secs = rings_before_answer * 6 + print("> > > Waiting {} secs for pickup").format(wait_secs) + time.sleep(wait_secs) + # Problem: What if the caller hangs up and another call + # comes in while sleeping? + # -> Issue: How to detect caller hang up? + # -} Idea: loop with 1 sec sleep interval; check for off-hook or hang-up + + # TODO here: must check for local phone off hook. + # Apply followintg actions if not off-hook. + # Apply the configured actions to blocked callers - if caller_blocked: + if len(actions) > 0: # Go "off-hook" - Acquires a lock on the modem - MUST follow with hang_up() if self.modem.pick_up(): try: # Play greeting - if "greeting" in blocked["actions"]: + if "greeting" in actions: print(">> Playing greeting...") - self.modem.play_audio(blocked_greeting_file) + self.modem.play_audio(greeting) # Record message - if "record_message" in blocked["actions"]: + if "record_message" in actions: print(">> Recording message...") self.voice_mail.record_message(call_no, caller) - elif "voice_mail" in blocked["actions"]: + # Enter voice mail menu + elif "voice_mail" in actions: print(">> Starting voice mail...") self.voice_mail.voice_messaging_menu(call_no, caller) @@ -204,7 +235,9 @@ def run(self): finally: # Go "on-hook" self.modem.hang_up() + self.phone_ringing(False) + except Exception as e: pprint(e) print("** Error running callattendant. Exiting.") diff --git a/src/hardware/modem.py b/src/hardware/modem.py index 41d25e2..b49a78b 100644 --- a/src/hardware/modem.py +++ b/src/hardware/modem.py @@ -118,7 +118,16 @@ def __init__(self, config, phone_ringing, handle_caller): # Setup and open the serial port self._serial = serial.Serial() + def get_off_hook(): + pass + + def get_is_ringing(): + pass + def handle_calls(self): + """ + Starts the thread that processes incoming data. + """ self._init_modem() self.event_thread = threading.Thread(target=self._call_handler) self.event_thread.name = "modem_call_handler" @@ -165,7 +174,7 @@ def _call_handler(self): modem_data = TEST_DATA[test_index] test_index += 1 else: - # Read a line of data from the serial port + # Wait/read a line of data from the serial port modem_data = self._serial.readline() # Process the modem data @@ -178,6 +187,8 @@ def _call_handler(self): logfile.flush() # Extract caller info + if DCE_RING in modem_data: + self.phone_ringing(True) if RING in modem_data: self.phone_ringing(True) if DATE in modem_data: From 9eff13a728adf2ae76d6c66bdcb79c78d3c8e5a5 Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Tue, 18 Aug 2020 03:36:43 -0700 Subject: [PATCH 04/20] Turned off the Flask HTML log output --- src/userinterface/webapp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/userinterface/webapp.py b/src/userinterface/webapp.py index d2105f2..8466f58 100644 --- a/src/userinterface/webapp.py +++ b/src/userinterface/webapp.py @@ -38,6 +38,7 @@ from datetime import datetime, timedelta from pprint import pprint from glob import glob +import logging import os import re import random @@ -51,6 +52,9 @@ app.config.from_pyfile('webapp.cfg') app.debug = False # debug mode prevents app from running in separate thread +# Turn off the HTML GET/POST logging +log = logging.getLogger('werkzeug') +log.disabled = True @app.before_request def before_request(): From 4f759b1b2911d8f24dc90c0bcd1eb045060a64d2 Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Tue, 18 Aug 2020 07:50:42 -0700 Subject: [PATCH 05/20] Added SevenSegmentDisplay - From https://github.com/gpiozero/gpiozero/pull/488 --- src/hardware/indicators.py | 168 ++++++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/src/hardware/indicators.py b/src/hardware/indicators.py index 146495e..6b6d3fa 100644 --- a/src/hardware/indicators.py +++ b/src/hardware/indicators.py @@ -25,7 +25,7 @@ # See: https://gpiozero.readthedocs.io/en/stable/ # See: https://gpiozero.readthedocs.io/en/stable/api_output.html#led -from gpiozero import LED, PWMLED +from gpiozero import LED, PWMLED, LEDBoard, OutputDeviceError, LEDCollection from pprint import pprint import time @@ -109,6 +109,172 @@ def __init__(self, gpio_pin=GPIO_MESSAGE): super().__init__(gpio_pin) + + +class SevenSegmentDisplay(LEDBoard): + """ + Extends :class:`LEDBoard` for a 7 segment LED display + + 7 segment displays have either 7 or 8 pins, 7 pins for the digit display + and an optional 8th pin for a decimal point. 7 segment displays + typically have either a common anode or common cathode pin, when + using a common anode display 'active_high' should be set to False. + Instances of this class can be used to display characters or control + individual leds on the display. For example:: + + from gpiozero import SevenSegmentDisplay + + seven = SevenSegmentDisplay(1,2,3,4,5,6,7,8,active_high=False) + seven.display("7") + + :param int \*pins: + Specify the GPIO pins that the 7 segment display is attached to. + Pins should be in the LED segment order A,B,C,D,E,F,G,decimal_point + (the decimal_point is optional). + + :param bool pwm: + If ``True``, construct :class:`PWMLED` instances for each pin. If + ``False`` (the default), construct regular :class:`LED` instances. This + parameter can only be specified as a keyword parameter. + + :param bool active_high: + If ``True`` (the default), the :meth:`on` method will set all the + associated pins to HIGH. If ``False``, the :meth:`on` method will set + all pins to LOW (the :meth:`off` method always does the opposite). This + parameter can only be specified as a keyword parameter. + + :param bool initial_value: + If ``False`` (the default), all LEDs will be off initially. If + ``None``, each device will be left in whatever state the pin is found + in when configured for output (warning: this can be on). If ``True``, + the device will be switched on initially. This parameter can only be + specified as a keyword parameter. + + From https://www.stuffaboutcode.com/2016/10/raspberry-pi-7-segment-display-gpiozero.html + See: https://github.com/gpiozero/gpiozero/pull/488 + """ + def __init__(self, *pins, **kwargs): + # 7 segment displays must have 7 or 8 pins + if len(pins) < 7 or len(pins) > 8: + raise ValueError('SevenSegmentDisplay must have 7 or 8 pins') + # Don't allow 7 segments to contain collections + for pin in pins: + assert not isinstance(pin, LEDCollection) + pwm = kwargs.pop('pwm', False) + active_high = kwargs.pop('active_high', True) + initial_value = kwargs.pop('initial_value', False) + if kwargs: + raise TypeError('unexpected keyword argument: %s' % kwargs.popitem()[0]) + + self._layouts = { + '1': (False, True, True, False, False, False, False), + '2': (True, True, False, True, True, False, True), + '3': (True, True, True, True, False, False, True), + '4': (False, True, True, False, False, True, True), + '5': (True, False, True, True, False, True, True), + '6': (True, False, True, True, True, True, True), + '7': (True, True, True, False, False, False, False), + '8': (True, True, True, True, True, True, True), + '9': (True, True, True, True, False, True, True), + '0': (True, True, True, True, True, True, False), + 'A': (True, True, True, False, True, True, True), + 'B': (False, False, True, True, True, True, True), + 'C': (True, False, False, True, True, True, False), + 'D': (False, True, True, True, True, False, True), + 'E': (True, False, False, True, True, True, True), + 'F': (True, False, False, False, True, True, True), + 'G': (True, False, True, True, True, True, False), + 'H': (False, True, True, False, True, True, True), + 'I': (False, False, False, False, True, True, False), + 'J': (False, True, True, True, True, False, False), + 'K': (True, False, True, False, True, True, True), + 'L': (False, False, False, True, True, True, False), + 'M': (True, False, True, False, True, False, False), + 'N': (True, True, True, False, True, True, False), + 'O': (True, True, True, True, True, True, False), + 'P': (True, True, False, False, True, True, True), + 'Q': (True, True, False, True, False, True, True), + 'R': (True, True, False, False, True, True, False), + 'S': (True, False, True, True, False, True, True), + 'T': (False, False, False, True, True, True, True), + 'U': (False, False, True, True, True, False, False), + 'V': (False, True, True, True, True, True, False), + 'W': (False, True, False, True, False, True, False), + 'X': (False, True, True, False, True, True, True), + 'Y': (False, True, True, True, False, True, True), + 'Z': (True, True, False, True, True, False, True), + '-': (False, False, False, False, False, False, True), + ' ': (False, False, False, False, False, False, False), + '=': (False, False, False, True, False, False, True) + } + + super(SevenSegmentDisplay, self).__init__(*pins, pwm=pwm, active_high=active_high, initial_value=initial_value) + + def display(self, char): + """ + Display a character on the 7 segment display + + :param string char: + A single character to be displayed + """ + char = str(char).upper() + if len(char) > 1: + raise ValueError('only a single character can be displayed') + if char not in self._layouts: + raise ValueError('there is no layout for character - %s' % char) + layout = self._layouts[char] + for led in range(7): + self[led].value = layout[led] + + def display_hex(self, hexnumber): + """ + Display a hex number (0-F) on the 7 segment display + + :param int hexnumber: + The number to be displayed in hex + """ + self.display(hex(hexnumber)[2:]) + + @property + def decimal_point(self): + """ + Represents the status of the decimal point led + """ + # does the 7seg display have a decimal point (i.e pin 8) + if len(self) > 7: + return self[7].value + else: + raise OutputDeviceError('there is no 8th pin for the decimal point') + + @decimal_point.setter + def decimal_point(self, value): + """ + Sets the status of the decimal point led + """ + if len(self) > 7: + self[7].value = value + else: + raise OutputDeviceError('there is no 8th pin for the decimal point') + + def set_char_layout(self, char, layout): + """ + Create or update a custom character layout, which can be used with the + `display` method. + + :param string char: + A single character to be displayed + + :param tuple layout: + A 7 bool tuple of LED values in the segment order A, B, C, D, E, F, G + """ + char = str(char).upper() + if len(char) != 1: + raise ValueError('only a single character can be used in a layout') + if len(layout) != 7: + raise ValueError('a character layout must have 7 segments') + self._layouts[char] = layout + + def test(): """ Unit Tests """ import os From dba648841ac5bc7e039d1a8761e77d3115d78eba Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Tue, 18 Aug 2020 09:31:09 -0700 Subject: [PATCH 06/20] Shortened the README; moved content to the Wiki --- README.md | 189 +++++++++++------------------------------------------- 1 file changed, 37 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index fdde2b3..48b2d67 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Call Attendant -Automated call attendant with call blocking and voice messaging on a Raspberry Pi. +Automated call attendant with call blocking and voice messaging running on a Raspberry Pi. Stop annoying robocalls and spammers +from interrupting your life. + +_If you're at all interested in this project, please provide some feedback by giving it a [star](https://github.com/emxsys/callattendant/stargazers), +or even better, get involved by filing [issues](https://github.com/emxsys/callattendant/issues) or [pull requests](https://github.com/emxsys/callattendant/pulls)._ #### Table of Contents - [Overview](#overview) -- [Software Architecture](#software-architecture) -- [Software Development Plan](#software-development-plan) -- [Installation](#installation) -- [Operation](#operation) +- [Quick Start](#quick-start) - [More Information](#more-information) @@ -27,76 +28,37 @@ The __callattendant__ uses the following hardware: - Raspberry Pi 3B+ or better - US Robotics 5637 Modem -For a complete description of the hardware setup see the [Installation](https://github.com/emxsys/callattendant/wiki/User-Guide#installation) -section of the [User Guide](https://github.com/emxsys/callattendant/wiki/User-Guide). +For a complete description of the hardware setup see the [Installation](https://github.com/emxsys/callattendant/wiki/Home#installation) +section of the [Wiki](https://github.com/emxsys/callattendant/wiki/Home). ##### _The required hardware components: a Raspberry Pi 3B+ and USR5637 modem_ ![Raspberry Pi and USR5637 Modem](https://github.com/emxsys/callattendant/raw/master/docs/raspberry_pi-modem.jpg) ### Web Interface -Call history, permitted numbers, blocked numbers and caller management is performed through a web interface. -Following is an example of the main screen, the Call Log, including metrics and a list of recent calls. -For a complete description see the [Web Interface](https://github.com/emxsys/callattendant/wiki/User-Guide#web-interface) -section of the [User Guide](https://github.com/emxsys/callattendant/wiki/User-Guide). - -##### _Call Log Example_ -![Call Log](https://github.com/emxsys/callattendant/blob/master/docs/call-log.png) - -### User Guide -See the [User Guide](https://github.com/emxsys/callattendant/wiki/User-Guide) in the project's wiki for installation, -configuration, operation and web interface instructions. The [Wiki](https://github.com/emxsys/callattendant/wiki) -also includes a [Developer Guide](https://github.com/emxsys/callattendant/wiki/Developer-Guide) and an -[Advanced](https://github.com/emxsys/callattendant/wiki/Advanced) page for more complex setups and situations. - -## Software Architecture -### Archtectural Viewpoints -###### _Rational Unified Process 4+1 View_ -![Alt text](https://github.com/emxsys/callattendant/blob/master/docs/images/RUP_41_View.png "RUP 4+1 View") - -### Use Case View -###### _Use Case Diagram_ -![Alt text](https://github.com/emxsys/callattendant/blob/master/docs/images/Use_Case_View.png "Use Case Diagram") +Call history, playing voice messages, permitted numbers, blocked numbers and caller management is performed through the Call Attendant's web interface. +Following is an example of the main screen, the Dashboard, including metrics and a list of recent calls. +For a complete description see the [User Guide](https://github.com/emxsys/callattendant/wiki/User-Guide). -### Logical View -###### _Class Diagram_ -![Alt text](https://github.com/emxsys/callattendant/blob/master/docs/images/Logical_View.png "Logical View Diagram") +##### _Dashboard/home page as seen on an IPad Pro and a Pixel 2 phone_ +![Dashboard - Responsive](https://github.com/emxsys/callattendant/blob/master/docs/dashboard-responsive.png) -### Process View -###### _Activity Diagram_ -![Alt text](https://github.com/emxsys/callattendant/blob/master/docs/images/Process_View.png "Process View Diagram") - -###### _Sequence Diagram_ -![Alt text](https://github.com/emxsys/callattendant/blob/master/docs/images/Main_Sequence_Diagram.png "Main Sequence Diagram") - -### Implementation View -###### _Component Diagram_ -![Alt text](https://github.com/emxsys/callattendant/blob/master/docs/images/Implementation_View.png "Implementation Diagram") - -### Deployment View -###### _Deployment Diagram_ -![Alt text](https://github.com/emxsys/callattendant/blob/master/docs/images/Deployment_View.png "Deployment Diagram") +### Setup +See the [Call Attendant Wiki](https://github.com/emxsys/callattendant/wiki/Home) for complete installation, configuration, and operation instructions. -### Data View -###### _Entity Relationship Diagram_ -![Alt text](https://github.com/emxsys/callattendant/blob/master/docs/images/Data_View.png "Entity Relationship Diagram") +### User Guide +See the [User Guide](https://github.com/emxsys/callattendant/wiki/User-Guide) in the project's wiki for web interface instructions. ---- +### Developer Guide +The [Wiki](https://github.com/emxsys/callattendant/wiki) includes a [Developer Guide](https://github.com/emxsys/callattendant/wiki/Developer-Guide) +that describes the software architecture and software development plan. -## Software Development Plan -The development plan's [phase objectives](https://github.com/emxsys/callattendant/projects?query=is%3Aopen+sort%3Acreated-asc) are captured in the GitHub projects. -### [Inception Phase](https://github.com/emxsys/callattendant/projects/1) -- [x] Iteration #I1: [v0.1](https://github.com/emxsys/callattendant/releases/tag/v0.1) -### [Elaboration Phase](https://github.com/emxsys/callattendant/projects/2) -- [x] Iteration #E1: [v0.2](https://github.com/emxsys/callattendant/releases/tag/v0.2) -### [Construction Phase](https://github.com/emxsys/callattendant/projects/3) -- [x] Iteration #C1: [v0.3](https://github.com/emxsys/callattendant/releases/tag/v0.3) -- [ ] Iteration #C2: [v0.4](https://github.com/emxsys/callattendant/issues?q=is%3Aopen+is%3Aissue+milestone%3A%22Release+0.4%22) -### [Transition Phase](https://github.com/emxsys/callattendant/projects/4) -- [ ] Iteration #T1: [v1.0](https://github.com/emxsys/callattendant/issues?q=is%3Aopen+is%3Aissue+milestone%3A%22Release+1.0%22) +### More Information +The [Wiki](https://github.com/emxsys/callattendant/wiki) has an [Advanced](https://github.com/emxsys/callattendant/wiki/Advanced) page for +more complex setups and situations. For instance, _running as a service_. --- -## Installation +## Quick Start ### Prequisites #### Hardware @@ -263,105 +225,28 @@ OK Navigate to on port 5000 and you should see the home page. Make a few calls to yourself to test the service. -### Run as a Service -###### *Optional* -We're going to define a service to automatically run the Call Attendant on the Raspberry Pi at start up. Our simple service will run the `callattendant.py` script and if by any means is aborted it is going to be restarted automatically. - -#### Create the Service - -##### Step 1. Create the Unit File -The service definition must be on the `/lib/systemd/system` folder. Our service is going to be called "callattendant.service": +--- +### Configuration +The Call Attendant's behavior can be controlled by a configuration file. To override the default configuration, +copy the `src/app.cfg.example` file to a new file, e.g. `src/app.cfg` and edit its contents. Use an editor that +provides Python syntax highlighting, like nano. Then use your configuration file when starting the callattendant. -```Shell -cd /lib/systemd/system/ -sudo nano callattendant.service +Specify the configuration file on the command line, e.g.: ``` - -Copy the following text into the `callattendant.service` unit file, using your path to `callattendant.py`: - -```text -[Unit] -Description=Call Attendant -After=multi-user.target - -[Service] -Type=simple -ExecStart=/usr/bin/python /home/pi/callattendant/src/callattendant.py -WorkingDirectory=/home/pi/callattendant/src -Restart=on-abort - -[Install] -WantedBy=multi-user.target +python3 src/callattendant.py --config app.cfg ``` -You can check more on service's options in the next wiki: https://wiki.archlinux.org/index.php/systemd. - -##### Step 2. Activate the Service -Now that we have our service we need to activate it: - -```Shell -sudo chmod 644 /lib/systemd/system/callattendant.service -chmod +x /home/pi/callattendant/src/callattendant.py -sudo systemctl daemon-reload -sudo systemctl enable callattendant.service -sudo systemctl start callattendant.service -``` - -#### Service Tasks -For every change that we do on the `/lib/systemd/system` folder we need to execute a -`daemon-reload` (third line of previous code). - -You can execute the following commands as needed to check the status, start and stop -the service, or check the logs. Several shell scripts are included in the root of this -project to perfom these tasks. - -##### Check status -`sudo systemctl status callattendant.service` - -##### Start service -`sudo systemctl start callattendant.service` - -##### Stop service -`sudo systemctl stop callattendant.service` - -##### Check service's log -`sudo journalctl -f -u callattendant.service` +See the [Configuration](https://github.com/emxsys/callattendant/wiki/Home#configuration) section in the project's +wiki for more information. --- -## Operation - -### Web Pages -A few web pages are used to monitor your installation. You can use `localhost` for the `pi_address` -when you are running the web browser on the pi. - -##### Call Log -The call log displays all the calls that have been processed by the call attendant. This list refreshes -automatically after a period of several minutes. +### Web Interface +#### URL: http://|:5000 +To view the web interface, simply point your web browser to port `5000` on your Raspberry Pi. I +For example, in your Raspberry Pi's browser, you can use: ``` http://localhost:5000/ ``` -##### Blocked List -This is the list of blocked numbers. -``` -http://localhost:5000/blocked -``` - -##### Permitted List -This is the list of permitted numbers. -``` -http://localhost:5000/permitted -``` -### Tools - -#### DB Browser for SQLite -If needed, you can use **DB Browser for SQLite** to view and edit the tables used by the **callattendant**. - -``` -sudo apt-get install sqlitebrowser -``` -``` -sqlitebrowser callattendant/src/callattendant.db -``` --- From 6b525c5251a91c75f817aa05c5e7fb9fad6ffc0c Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Tue, 18 Aug 2020 09:55:39 -0700 Subject: [PATCH 07/20] Update README.md --- README.md | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 48b2d67..5c47cc8 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,44 @@ # Call Attendant Automated call attendant with call blocking and voice messaging running on a Raspberry Pi. Stop annoying robocalls and spammers -from interrupting your life. +from interrupting your life. It intercepts robocallers and telemarketers before the first ring on your landline. It provides +voice messaging options to capture messages from humans -_If you're at all interested in this project, please provide some feedback by giving it a [star](https://github.com/emxsys/callattendant/stargazers), -or even better, get involved by filing [issues](https://github.com/emxsys/callattendant/issues) or [pull requests](https://github.com/emxsys/callattendant/pulls)._ +_If you're at all interested in this project, please provide some feedback by giving it a +__[star](https://github.com/emxsys/callattendant/stargazers)__, or even better, get involved by filing +[issues](https://github.com/emxsys/callattendant/issues) and/or [pull requests](https://github.com/emxsys/callattendant/pulls). +Thanks!_ #### Table of Contents - [Overview](#overview) - [Quick Start](#quick-start) - [More Information](#more-information) - ## Overview The Call Attendant (__callattendant__) is a python-based, automated call attendant that runs on a lightweight Raspberry Pi or other Linux-based system. Coupled with a modem, it provides a call blocker and voice messaging system that can screen callers and block robocall and scams from your landline. -Features include: +#### How it works +The Raspberry Pi and modem are connected to your home phone system in parallel with you phone handset(s). When an incoming +call is received, the call goes to both your phone and the Call Attendant software on the Pi. During the period of the first ring +the Call Attendant analyzes the caller ID, and based on your configuration, determines if the call should be blocked or allowed. +Blocked calls can be simply hung up on, or routed to the voice message system. Calls that are allowed simply ring your home +phone, if configured to do so. The Call Attendant's filtering mechanisms include an online lookup service, a blocked number list +and pattern matching on the number and/or name. + +#### Features include: - [x] A call blocker that intercepts robocallers and blocked numbers at or before the first ring - [x] Permitted numbers pass straight through to the local phone system for normal call ringing and answering - [x] Visual indicators to show whether the incoming call is from a permitted, blocked, or unknown number - [x] Call details, permitted numbers, and blocked numbers are available in a web-based user interface -- [x] Blocked callers are handled by a voice messaging system that requires human interaction, e.g, "Press 1 to leave a message" +- [x] Blocked callers are handled by a voice messaging system that optioanlly requires human interaction, e.g, "Press 1 to leave a message" + +Call history, voice messaging, permitted numbers, blocked numbers and caller management is performed through the +Call Attendant's web interface. Following is an example of the main screen, the Dashboard, including metrics and +a list of recent calls. For a complete description see the [User Guide](https://github.com/emxsys/callattendant/wiki/User-Guide). + +##### _Dashboard/home page as seen on an IPad Pro and a Pixel 2 phone_ +![Dashboard - Responsive](https://github.com/emxsys/callattendant/blob/master/docs/dashboard-responsive.png) ### Hardware The __callattendant__ uses the following hardware: @@ -34,14 +51,6 @@ section of the [Wiki](https://github.com/emxsys/callattendant/wiki/Home). ##### _The required hardware components: a Raspberry Pi 3B+ and USR5637 modem_ ![Raspberry Pi and USR5637 Modem](https://github.com/emxsys/callattendant/raw/master/docs/raspberry_pi-modem.jpg) -### Web Interface -Call history, playing voice messages, permitted numbers, blocked numbers and caller management is performed through the Call Attendant's web interface. -Following is an example of the main screen, the Dashboard, including metrics and a list of recent calls. -For a complete description see the [User Guide](https://github.com/emxsys/callattendant/wiki/User-Guide). - -##### _Dashboard/home page as seen on an IPad Pro and a Pixel 2 phone_ -![Dashboard - Responsive](https://github.com/emxsys/callattendant/blob/master/docs/dashboard-responsive.png) - ### Setup See the [Call Attendant Wiki](https://github.com/emxsys/callattendant/wiki/Home) for complete installation, configuration, and operation instructions. From cde22af33aabcf9029a74c416d0a51989bb27feb Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Tue, 18 Aug 2020 10:33:13 -0700 Subject: [PATCH 08/20] Small tweeks to the Modem class --- src/hardware/modem.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/hardware/modem.py b/src/hardware/modem.py index b49a78b..6b293c5 100644 --- a/src/hardware/modem.py +++ b/src/hardware/modem.py @@ -45,6 +45,8 @@ # ACSII codes DLE_CODE = chr(16) # Data Link Escape (DLE) code ETX_CODE = chr(3) # End Transmission (ETX) code +CR_CODE = chr(13) # Carraige return +LF_CODE = chr(10) # Line feed # Modem AT commands: # See http://support.usr.com/support/5637/5637-ug/ref_data.html @@ -86,6 +88,9 @@ DTE_END_VOICE_DATA_TX = (chr(16) + chr(3)) # DTE_END_RECIEVE_DATA_STATE = (chr(16) + chr(33)) # -! +# Return codes +CRLF = (chr(13) + chr(10)).encode() + # Record Voice Mail variables REC_VM_MAX_DURATION = 120 # Time in Seconds @@ -174,17 +179,20 @@ def _call_handler(self): modem_data = TEST_DATA[test_index] test_index += 1 else: - # Wait/read a line of data from the serial port + # Wait/read a line of data from the serial port. + # The verbose-form code is preceded and terminated by the + # sequence . The numeric-form is also terminated + # by , but it has no preceding sequence. modem_data = self._serial.readline() - # Process the modem data - if modem_data != b'': + if debugging: + print(modem_data) + if dev_mode: + logfile.write(modem_data) + logfile.flush() - if debugging: - print(modem_data) - if dev_mode: - logfile.write(modem_data) - logfile.flush() + # Process the modem data + if modem_data != b'' and modem_data != CRLF: # Extract caller info if DCE_RING in modem_data: @@ -205,8 +213,9 @@ def _call_handler(self): # Screen caller self.handle_caller(call_record) call_record = {} - # Sleep for a short duration ( secs) to allow the + # Sleep for a short duration (secs) to allow the # call attendant to screen the call before resuming + # TODO: Should wait on an Event time.sleep(2) finally: if dev_mode: From dcbd05f3c2d588c2143c084284a0f18f09ae6e35 Mon Sep 17 00:00:00 2001 From: Bruce Schubert Date: Tue, 18 Aug 2020 10:41:41 -0700 Subject: [PATCH 09/20] Changed message link to View Call instead of Manage Caller - More consistant with rest of app --- src/userinterface/templates/messages.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/userinterface/templates/messages.html b/src/userinterface/templates/messages.html index fd2a672..aff9955 100644 --- a/src/userinterface/templates/messages.html +++ b/src/userinterface/templates/messages.html @@ -39,7 +39,7 @@

None

Your browser does not support the audio element.
- from {{ item.phone_no }} - {{ item.name }} + from {{ item.phone_no }} - {{ item.name }}