From b61670b0cc972f39c57aba54c5770316c0b23bf6 Mon Sep 17 00:00:00 2001 From: TB-1993 Date: Mon, 23 Sep 2024 17:33:09 +0100 Subject: [PATCH 01/29] Upgrade #92: Creation of HDMICEC module --- framework/core/deviceManager.py | 4 + framework/core/hdmiCECController.py | 170 ++++++++++++++ framework/core/hdmicecModules/__init__.py | 30 +++ .../hdmicecModules/abstractCECController.py | 98 ++++++++ framework/core/hdmicecModules/cecClient.py | 212 ++++++++++++++++++ 5 files changed, 514 insertions(+) create mode 100644 framework/core/hdmiCECController.py create mode 100644 framework/core/hdmicecModules/__init__.py create mode 100644 framework/core/hdmicecModules/abstractCECController.py create mode 100644 framework/core/hdmicecModules/cecClient.py diff --git a/framework/core/deviceManager.py b/framework/core/deviceManager.py index 1d79026..51820e7 100644 --- a/framework/core/deviceManager.py +++ b/framework/core/deviceManager.py @@ -40,6 +40,7 @@ from framework.core.powerControl import powerControlClass from framework.core.outboundClient import outboundClientClass from framework.core.commonRemote import commonRemoteClass +from framework.core.hdmiCECController import HDMICECController from framework.core.utilities import utilities dir_path = os.path.dirname(os.path.realpath(__file__)) @@ -174,6 +175,9 @@ def __init__(self, log:logModule, logPath:str, devices:dict): config = device.get("remoteController") if config != None: self.remoteController = commonRemoteClass(log, config) + config = device.get("hdmiCECController") + if config != None: + self.hdmiCECController = HDMICECController(log, config) self.session = self.getConsoleSession() def getField(self, fieldName:str, itemsList:dict = None): diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py new file mode 100644 index 0000000..81027f1 --- /dev/null +++ b/framework/core/hdmiCECController.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +#** ***************************************************************************** +# * +# * If not stated otherwise in this file or this component's LICENSE file the +# * following copyright and licenses apply: +# * +# * Copyright 2023 RDK Management +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * +# http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# * +#* ****************************************************************************** +#* +#* ** Project : RAFT +#* ** @addtogroup : core +#* ** @date : 22/11/2021 +#* ** +#* ** @brief : HDMICECController class to differentiate into the whichever +#* ** cec controller type is specified. +#* ** +#* ****************************************************************************** + +from os import path + +import sys +MY_PATH = path.realpath(__file__) +MY_DIR = path.dirname(MY_PATH) +sys.path.append(path.join(MY_DIR,'../../')) +from framework.core.logModule import logModule +from hdmicecModules import CECClientController, MonitoringType + +class HDMICECController(): + """ + This class provides a high-level interface for controlling and monitoring + Consumer Electronics Control (CEC) devices. + """ + + def __init__(self, log: logModule, config: dict): + """ + Initializes the HDMICECController instance. + + Args: + log (logModule): An instance of a logging module for recording messages. + config (dict): A dictionary containing configuration options + """ + self._log = log + self.controllerType = config.get('type') + self.cecAdaptor = config.get('adaptor') + if self.controllerType.lower() == 'cec-client': + self.controller = CECClientController(self.cecAdaptor, self._log) + self._read_line = 0 + self._monitoringLog = path.join(self._log.logPath, 'cecMonitor.log') + + def send_message(self, message: str) -> bool: + """ + Sends a CEC message to connected devices using the configured controller. + + Args: + message (str): The CEC message to be sent. + + Returns: + bool: True if the message was sent successfully, False otherwise. + """ + self._log.debug('Sending CEC message: [%s]' % message) + return self.controller.sendMessage(message) + + def startMonitoring(self, deviceType: MonitoringType = MonitoringType.RECORDER) -> None: + """ + Starts monitoring CEC messages from the adaptor as the specified device type. + + Args: + deviceType (MonitoringType, optional): The type of device to monitor (default: MonitoringType.RECORDER). + + Raises: + RuntimeError: If monitoring is already running. + """ + if self.controller.monitoring is False: + self._log.debug('Starting monitoring on adaptor: [%s]' % self.cecAdaptor) + self._log.debug('Monitoring as device type [%s]' % deviceType.name) + return self.controller.startMonitoring(self._monitoringLog, deviceType) + else: + self._log.warn('CEC monitoring is already running') + + def stopMonitoring(self): + """ + Stops the CEC monitoring process. + + Delegates the stop task to the underlying `CECClientController`. + """ + return self.controller.stopMonitoring() + + def readUntil(self, message: str, retries: int = 5) -> bool: + """ + Reads the monitoring log until the specified message is found. + + Opens the monitoring log file and checks for the message within a specified retry limit. + + Args: + message (str): The message to search for in the monitoring log. + retries (int, optional): The maximum number of retries before giving up (default: 5). + + Returns: + bool: True if the message was found, False otherwise. + """ + self._log.debug('Starting readUntil for message as [%s] with [%s] retries' % (message,retries)) + result = False + with open(self._monitoringLog, 'r') as logFile: + retry = 0 + max_retries = retries + while retry != max_retries and not result: + logLines = logFile.readlines() + read_line = self._read_line + write_line = len(logLines) + while read_line != write_line: + if message in logLines[read_line]: + result = True + break + read_line+=1 + retry += 1 + self._read_line = read_line + return result + + def listDevices(self) -> list: + """ + Retrieves a list of discovered CEC devices with their OSD names (if available). + + Returns: + list: A list of dictionaries representing discovered devices. + """ + self._log.debug('Listing devices on CEC network') + return self.controller.listDevices() + + +if __name__ == "__main__": + import time + import json + LOG = logModule('CECTEST', logModule.DEBUG) + CONFIGS = [ + { + 'type': 'cec-client', + 'adaptor': '/dev/ttyACM0' + }, + ] + for config in CONFIGS: + LOG.setFilename('./logs/','CECTEST%s.log' % config.get('type')) + LOG.stepStart('Testing with %s' % json.dumps(config)) + CEC = HDMICECController(LOG, config) + DEVICES = CEC.listDevices() + LOG.info(json.dumps(DEVICES)) + # The user will need to check all the devices expected from their + # cec network are shown in this output. + CEC.startMonitoring() + # It's is expected that a user will send a standby command on their cec + # network during this 2 minutes. + time.sleep(120) + result = CEC.readUntil('standby') + CEC.stopMonitoring() + LOG.stepResult(result, 'The readUntil result is: [%s]' % result) + # The user should check here the monitoring log for thier type contains + # the expected information. diff --git a/framework/core/hdmicecModules/__init__.py b/framework/core/hdmicecModules/__init__.py new file mode 100644 index 0000000..df8f28f --- /dev/null +++ b/framework/core/hdmicecModules/__init__.py @@ -0,0 +1,30 @@ +#** ***************************************************************************** +# * +# * If not stated otherwise in this file or this component's LICENSE file the +# * following copyright and licenses apply: +# * +# * Copyright 2023 RDK Management +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * +# http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# * +#* ****************************************************************************** +#* +#* ** Project : RAFT +#* ** @addtogroup : core +#* ** @date : 22/11/2021 +#* ** +#* ****************************************************************************** + +from .cecClient import CECClientController +from .cecTypes import MonitoringType diff --git a/framework/core/hdmicecModules/abstractCECController.py b/framework/core/hdmicecModules/abstractCECController.py new file mode 100644 index 0000000..5a00a14 --- /dev/null +++ b/framework/core/hdmicecModules/abstractCECController.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +#** ***************************************************************************** +# * +# * If not stated otherwise in this file or this component's LICENSE file the +# * following copyright and licenses apply: +# * +# * Copyright 2023 RDK Management +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * +# http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# * +#* ****************************************************************************** +#* +#* ** Project : RAFT +#* ** @addtogroup : core +#* ** @date : 02/10/2024 +#* ** +#* ** @brief : Abstract class for CEC controller types. +#* ** +#* ****************************************************************************** + +from abc import ABCMeta, abstractmethod + +from framework.core.logModule import logModule +from .cecTypes import MonitoringType + +class CECInterface(metaclass=ABCMeta): + + def __init__(self, adaptor_path:str, logger:logModule): + self.adaptor = adaptor_path + self._log = logger + self._monitoring = False + + @property + def monitoring(self) -> bool: + return self._monitoring + + @abstractmethod + def sendMessage(cls, message:str) -> bool: + """ + Send a CEC message to the CEC network. + + Args: + message (str): The CEC message to be sent. + + Returns: + bool: True if the message was sent successfully, False otherwise. + """ + pass + + @abstractmethod + def listDevices(cls) -> list: + """ + List CEC devices on CEC network. + + The list returned contains dicts in the following format: + { + 'name': 'TV' + 'address': '0.0.0.0', + 'active source': True, + 'vendor': 'Unknown', + 'osd string': 'TV', + 'CEC version': '1.3a', + 'power status': 'on', + 'language': 'eng', + } + Returns: + list: A list of dictionaries representing discovered devices. + """ + pass + + @abstractmethod + def startMonitoring(cls, monitoringLog: str, deviceType: MonitoringType=MonitoringType.RECORDER) -> None: + """ + Starts monitoring CEC messages with a specified device type. + + Args: + deviceType (MonitoringType, optional): The type of device to monitor (default: MonitoringType.RECORDER). + monitoringLog (str) : Path to write the monitoring log out + """ + pass + + @abstractmethod + def stopMonitoring(cls) -> None: + """ + Stops the CEC monitoring process. + """ + pass diff --git a/framework/core/hdmicecModules/cecClient.py b/framework/core/hdmicecModules/cecClient.py new file mode 100644 index 0000000..2110872 --- /dev/null +++ b/framework/core/hdmicecModules/cecClient.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +#** ***************************************************************************** +# * +# * If not stated otherwise in this file or this component's LICENSE file the +# * following copyright and licenses apply: +# * +# * Copyright 2023 RDK Management +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * +# http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# * +#* ****************************************************************************** +#* +#* ** Project : RAFT +#* ** @addtogroup : core +#* ** @date : 02/10/2024 +#* ** +#* ** @brief : cecClient controller. Class for running cec-client commands on +#* ** the host PC +#* ** +#* ****************************************************************************** + +from io import IOBase +import os +import re +import subprocess +from threading import Thread + +import sys +sys.path.append('/mnt/1TB/home/toby/Documents/Scripts/python/python_raft/') + +from framework.core.logModule import logModule +from .abstractCECController import CECInterface +from .cecTypes import MonitoringType + + +class CECClientController(CECInterface): + """ + This class provides an interface for controlling Consumer Electronics Control (CEC) + devices through the `cec-client` command-line tool. + """ + + def __init__(self, adaptor_path:str, logger:logModule): + """ + Initializes the CECClientController instance. + + Args: + adaptor_path (str): Path to the CEC adaptor device. + logger (logModule): An instance of a logging module for recording messages. + + Raises: + AttributeError: If the specified CEC adaptor is not found. + """ + + self._log = logger + self.adaptor = adaptor_path + self._log.debug('Initialising CECClientController for [%s]' % self.adaptor) + if self.adaptor not in map(lambda x: x.get('com port'),self._getAdaptors()): + raise AttributeError('CEC Adaptor specified not found') + self._monitoring = False + self._m_proc = None + self._m_stdout_thread = None + self._m_log = None + + def sendMessage(self,message: str) -> bool: + exit_code, stdout = self._sendMessage(message, 0) + self._log.debug('Output of message sent: [%s]' % stdout) + if exit_code != 0: + return False + return True + + def _sendMessage(self, message: str, debug: int = 1) -> tuple: + """ + Internal method for sending a CEC message using `subprocess`. + + Args: + message (str): The CEC message to be sent. + debug (int, optional): Debug level for `cec-client` (default: 1). + + Returns: + tuple: A tuple containing the exit code of the subprocess call and the standard output. + """ + result = subprocess.run(f'echo "{message}" | cec-client {self.adaptor} -s -d {debug}', + shell=True, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout = result.stdout.decode('utf-8') + stderr = result.stderr.decode('utf-8') + exit_code = result.returncode + return exit_code, stdout + + def _getAdaptors(self) -> list: + """ + Retrieves a list of available CEC adaptors using `cec-client`. + + Returns: + list: A list of dictionaries representing available adaptors with details like COM port. + """ + result = subprocess.run(f'cec-client -l', + shell=True, + text=True, + capture_output=True, + check=True) + stdout = result.stdout + adaptor_count = re.search(r'Found devices: ([0-9]+)',stdout, re.M).group(1) + adaptors = self._splitDeviceSectionsToDicts(stdout) + return adaptors + + def _scanCECNetwork(self) -> list: + """ + Scans the CEC network for connected devices using `cec-client`. + + Sends a "scan" message and parses the response to identify connected devices. + + Returns: + list: A list of dictionaries representing discovered devices with details. + """ + _, result = self._sendMessage('scan') + self._log.debug('Output of scan on CEC Network: [%s]' % result) + devicesOnNetwork = self._splitDeviceSectionsToDicts(result) + return devicesOnNetwork + + def listDevices(self) -> list: + devices = self._scanCECNetwork() + for device_dict in devices: + device_dict['name'] = device_dict.get('osd string') + if device_dict.get('active source') == 'yes': + device_dict['active source'] = True + else: + device_dict['active source'] = False + return devices + + def _splitDeviceSectionsToDicts(self,command_output:str) -> list: + """ + Splits the output of a `cec-client` command into individual device sections and parses them into dictionaries. + + Args: + command_output (str): The output string from the `cec-client` command. + + Returns: + list: A list of dictionaries, each representing a single CEC device with its attributes. + """ + devices = [] + device_sections = re.findall(r'^device[ #0-9]{0,}:[\s\S]+?(?:type|language): +[\S ]+$', + command_output, + re.M) + if device_sections: + for section in device_sections: + device_dict = {} + for line in section.split('\n'): + line_split = re.search(r'^([\w #]+): +?(\S[\S ]{0,})$',line) + if line_split: + device_dict[line_split.group(1)] = line_split.group(2) + devices.append(device_dict) + return devices + + def startMonitoring(self, monitoringLog: str, device_type: MonitoringType = MonitoringType.RECORDER) -> None: + self._monitoring = True + try: + self._m_proc = subprocess.Popen(f'cec-client {self.adaptor} -m -d 0 -t {device_type.value}'.split(), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True) + self._m_log = open(monitoringLog, 'w+', encoding='utf-8') + self._m_stdout_thread = Thread(target=self._write_monitoring_log, + args=[self._m_proc.stdout, self._m_log], + daemon=True) + self._m_stdout_thread.start() + except Exception as e: + self.stopMonitoring() + raise + + def _write_monitoring_log(self,stream_in: IOBase, stream_out: IOBase) -> None: + """ + Writes the output of the monitoring process to a log file. + + Args: + stream_in (IOBase): The input stream from the monitoring process. + stream_out (IOBase): The output stream to the log file. + """ + while True: + chunk = stream_in.readline() + if chunk == '': + break + stream_out.write(chunk) + + def stopMonitoring(self) -> None: + self._log.debug('Stopping monitoring of adaptor [%s]' % self.adaptor) + if self.monitoring is False: + return + self._m_proc.terminate() + exit_code = self._m_proc.wait() + self._m_stdout_thread.join() + self._m_log.close() + self._monitoring = False + + def __del__(self): + """ + Destructor for the class, ensures monitoring is stopped. + """ + self.stopMonitoring() From 14437d9e77efc35b5be0d8d3cb92dd8945463f9e Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:18:50 +0100 Subject: [PATCH 02/29] Fix #92: Updated documentation and fixed bug in monitoring --- README.md | 6 +++++- examples/configs/example_rack_config.yml | 3 +++ framework/core/hdmiCECController.py | 12 ++++++------ framework/core/hdmicecModules/cecClient.py | 21 ++++++++------------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 821710b..0492df5 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,14 @@ It understands how to: - Python (=3.11.8) - Pyenv can be used to manage multiple versions of Python (please refer to the [documentation](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation)). - - All the packages listed in requirements.txt - Can be installed using `$ pip install -r requirements.txt` +#### Optional Packages + +- To use the cec-client type hdmiCECController the `cec-utils` package is required. + - `sudo apt install -y cec-utils` + ### User Installation Clone the repository and ensure that all requirements are met. diff --git a/examples/configs/example_rack_config.yml b/examples/configs/example_rack_config.yml index e011f06..fab3834 100644 --- a/examples/configs/example_rack_config.yml +++ b/examples/configs/example_rack_config.yml @@ -100,6 +100,9 @@ rackConfig: type: "HS100" ip: "192.168.1.7" port: 9999 + #hdmiCECController: Specific hdmiCECController for the slot + # supported types: + # [type: "cec-client", adaptor: "/dev/ttycec"] - pi2: ip: "192.168.99.1" description: "local pi4" diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py index 81027f1..17f4787 100644 --- a/framework/core/hdmiCECController.py +++ b/framework/core/hdmiCECController.py @@ -114,10 +114,10 @@ def readUntil(self, message: str, retries: int = 5) -> bool: """ self._log.debug('Starting readUntil for message as [%s] with [%s] retries' % (message,retries)) result = False - with open(self._monitoringLog, 'r') as logFile: - retry = 0 - max_retries = retries - while retry != max_retries and not result: + retry = 0 + max_retries = retries + while retry != max_retries and not result: + with open(self._monitoringLog, 'r') as logFile: logLines = logFile.readlines() read_line = self._read_line write_line = len(logLines) @@ -126,8 +126,8 @@ def readUntil(self, message: str, retries: int = 5) -> bool: result = True break read_line+=1 - retry += 1 - self._read_line = read_line + retry += 1 + self._read_line = read_line return result def listDevices(self) -> list: diff --git a/framework/core/hdmicecModules/cecClient.py b/framework/core/hdmicecModules/cecClient.py index 2110872..0e2c758 100644 --- a/framework/core/hdmicecModules/cecClient.py +++ b/framework/core/hdmicecModules/cecClient.py @@ -26,19 +26,16 @@ #* ** @date : 02/10/2024 #* ** #* ** @brief : cecClient controller. Class for running cec-client commands on -#* ** the host PC +#* ** the host PC. This requires the cec-utils package to be installed +#* ** on the host. #* ** #* ****************************************************************************** from io import IOBase -import os import re import subprocess from threading import Thread -import sys -sys.path.append('/mnt/1TB/home/toby/Documents/Scripts/python/python_raft/') - from framework.core.logModule import logModule from .abstractCECController import CECInterface from .cecTypes import MonitoringType @@ -70,7 +67,6 @@ def __init__(self, adaptor_path:str, logger:logModule): self._monitoring = False self._m_proc = None self._m_stdout_thread = None - self._m_log = None def sendMessage(self,message: str) -> bool: exit_code, stdout = self._sendMessage(message, 0) @@ -172,28 +168,28 @@ def startMonitoring(self, monitoringLog: str, device_type: MonitoringType = Moni stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - self._m_log = open(monitoringLog, 'w+', encoding='utf-8') self._m_stdout_thread = Thread(target=self._write_monitoring_log, - args=[self._m_proc.stdout, self._m_log], + args=[self._m_proc.stdout, monitoringLog], daemon=True) self._m_stdout_thread.start() except Exception as e: self.stopMonitoring() raise - def _write_monitoring_log(self,stream_in: IOBase, stream_out: IOBase) -> None: + def _write_monitoring_log(self,streamIn: IOBase, logFilePath: str) -> None: """ Writes the output of the monitoring process to a log file. Args: stream_in (IOBase): The input stream from the monitoring process. - stream_out (IOBase): The output stream to the log file. + logFilePath (str): File path to write the monitoring log out to. """ while True: - chunk = stream_in.readline() + chunk = streamIn.readline() if chunk == '': break - stream_out.write(chunk) + with open(logFilePath, 'a+',) as out: + out.write(chunk) def stopMonitoring(self) -> None: self._log.debug('Stopping monitoring of adaptor [%s]' % self.adaptor) @@ -202,7 +198,6 @@ def stopMonitoring(self) -> None: self._m_proc.terminate() exit_code = self._m_proc.wait() self._m_stdout_thread.join() - self._m_log.close() self._monitoring = False def __del__(self): From a09d30c732f9d6b1db4b57421e7818944a2e8d47 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:51:31 +0100 Subject: [PATCH 03/29] Update #92: Updated example_rack_config.yml Updated example_rack_config.yml to better document the usage of optional modules. --- examples/configs/example_rack_config.yml | 63 +++++++++++------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/examples/configs/example_rack_config.yml b/examples/configs/example_rack_config.yml index fab3834..5cfeddf 100644 --- a/examples/configs/example_rack_config.yml +++ b/examples/configs/example_rack_config.yml @@ -22,27 +22,22 @@ # This config file creates your environment, defaults are setup internally unless otherwise overridden. # you will need at least 1 rack and 1 slot to operate. -# each module e.g. console options, will be off by default unless otherwise stated +# Each optional module will be off by default unless otherwise stated. +# Uncomment and fill out the sections for the modules you require. -# Depreciated +# Deprecated # slotx: address: IP of device while running locally, replaced with slotx: ip # slotx: deviceConsole: IP of device while running locally, replaced with slotx: devices # Data that is global to all tests. globalConfig: includes: - # [ includes: ] - # [ deviceConfig: "required.yml file" ] deviceConfig: "example_device_config.yml" - capture: - # [capture: optional] - # [ocrEnginePath: "/usr/bin/tesseract"] # "C:\\Program Files\\Tesseract-OCR\\tesseract.exe" (For Windows) - tesseract binary - # [resolution: "1080p"] - Capture resolution - # [input: 0] - which input is connected to the video path + # [capture: optional] - This section is required for use with the videoCapture module. + # [ocrEnginePath: "/usr/bin/tesseract"] # "C:\\Program Files\\Tesseract-OCR\\tesseract.exe" (For Windows) - tesseract binary + # [resolution: "1080p"] - Capture resolution + # [input: 0] - which input is connected to the video path # Note: Video capture will not be installed unless screenRegions: is defined in deviceConfig: - ocrEnginePath: "/usr/bin/tesseract" # "C:\\Program Files\\Tesseract-OCR\\tesseract.exe" (For Windows) - resolution: "1080p" - input: 0 local: log: # log for each slot directory: "./logs" @@ -57,15 +52,16 @@ rackConfig: # [ name: "required", description: "optional"] name: "slot1" devices: - # [ devices: ] - # [ type: "serial": port: "COM7" baudRate: "(default)115200" dataBits: "optional(8)" stopBits: "optional(1)" parity: "optional(None)" FlowControl: "optional(None)" ] - # [ type: "ssh": port: 22 username: "test" password: "test" ] - # [ type: "telnet": port: 23 username: "test" password: "test" ] - dut: ip: "127.0.0.1" # IP Address of the ADA Hub description: "local PC" platform: "linux PC" consoles: + # - [ name ] - consoles should be listed here with a name. Defined as one of the types below. + # supported types: + # [ type: "serial", port: "COM7", baudRate: "(default)115200", dataBits: "optional(8)", stopBits: "optional(1)", parity: "optional(None)", FlowControl: "optional(None)", ] + # [ type: "ssh", port: 22, username: "test", password: "test" ] + # [ type: "telnet", port: 23, username: "test", password: "test" ] - default: type: "serial" port: "/dev/ttyUSB0" @@ -73,21 +69,22 @@ rackConfig: port: 22 username: "root" ip: "192.168.99.1" - remoteController: - # [ remoteController: ] - # [ type: "olimex" ip: "192.168.0.17" port: 7 map: "llama_rc6", config: "remote_commander.yml" ] - # [ type: "skyProc" map: "skyq_map", config: "remote_commander.yml" ] + + # [ remoteController: optional ] - This section is required for use with the remoteController module. + # supported types: + # [ type: "olimex", ip: "192.168.0.17", port: 7, map: "llama_rc6", config: "remote_commander.yml" ] + # [ type: "skyProc", map: "skyq_map", config: "remote_commander.yml" ] # [ type: "None" ] - type: "none" - map: "skyq_map" - config: "example_remote_commander.yml" - outbound: - download_url: "tftp://tftp-server.com/rack1/slot1/" # download location for the CPE device - upload_url: "sftp://server-address/home/workspace/tftp/rack1/slot1/" # upload location - upload_url_base_dir: "sftp://server-address/home/workspace/tftp/rack1/slot1" # alternative upload location - httpProxy: 'socks5h://localhost:1234' # Local Proxy if required - workspaceDirectory: './logs/workspace' # Local working directory - powerSwitch: # Specific power switch for each slot + + # [ outbound: optional ] - This section is used to configure paths for downloads and uploads from your test + # supported usage: + # [download_url: "url" ] - download location for the CPE device + # [ upload_url: "url" ] - upload location + # [ upload_url_base_dir: "url" ] - alternative upload location + # [ httpProxy: "uri" ] - Local Proxy if required + # [ workspaceDirectory: "path to directory on host" ] - Local working directory + + # [ powerSwitch: optional ] - Power switch for the slot # supported types, if this section is undefined this is ok # [type: "orvbioS20", ip: "", mac: "", port:"optional", relay:"optional"] # [type: "kasa", ip: "", options:"--plug" ] # <- Plug @@ -97,10 +94,8 @@ rackConfig: # [type: "olimex", ip:"", port:"optional", relay:"" ] # [type: "SLP", ip:"", username: "", password: "", outlet_id:"", port:"optional"] # [type: "none" ] if section doesn't exist then type:none will be used - type: "HS100" - ip: "192.168.1.7" - port: 9999 - #hdmiCECController: Specific hdmiCECController for the slot + + # [ hdmiCECController: optional ] - Specific hdmiCECController for the slot # supported types: # [type: "cec-client", adaptor: "/dev/ttycec"] - pi2: From bcde2964139a2a42018ed26b3d57d27026735f33 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:07:24 +0100 Subject: [PATCH 04/29] Fix #92: Added missing cecTypes.py --- framework/core/hdmicecModules/cecTypes.py | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 framework/core/hdmicecModules/cecTypes.py diff --git a/framework/core/hdmicecModules/cecTypes.py b/framework/core/hdmicecModules/cecTypes.py new file mode 100644 index 0000000..62e81f4 --- /dev/null +++ b/framework/core/hdmicecModules/cecTypes.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +#** ***************************************************************************** +# * +# * If not stated otherwise in this file or this component's LICENSE file the +# * following copyright and licenses apply: +# * +# * Copyright 2023 RDK Management +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * +# http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# * +#* ****************************************************************************** +#* +#* ** Project : RAFT +#* ** @addtogroup : core +#* ** @date : 02/10/2024 +#* ** +#* ** @brief : cecTypes defined to standardise cecController usage. +#* ** +#* ****************************************************************************** + +from enum import Enum + +class MonitoringType(Enum): + PLAYBACK = "p" + RECORDER = "r" + TUNER = "t" + AUDIO = "a" From 28e1f99d6662d5b1358dc8bd82a124bdca868ff8 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:45:03 +0000 Subject: [PATCH 05/29] Update #92 - Updated installation to install cec-client --- README.md | 14 +-- installation/activate.sh | 3 - installation/install_requirements.sh | 168 ++++++++++++++++++++++++--- 3 files changed, 160 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0492df5..0161670 100644 --- a/README.md +++ b/README.md @@ -58,19 +58,17 @@ It understands how to: ### Requirements -- Python (=3.11.8) +- Python (version 3.10+) - Pyenv can be used to manage multiple versions of Python (please refer to the [documentation](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation)). -- All the packages listed in requirements.txt - - Can be installed using `$ pip install -r requirements.txt` -#### Optional Packages +### User Installation -- To use the cec-client type hdmiCECController the `cec-utils` package is required. - - `sudo apt install -y cec-utils` +Clone the repository and run the [`install_requirements.sh`](installation/install_requirements.sh) script. -### User Installation +Administrator rights are required to install the below packages. Without these some modules may not work: +- `cec-client` - Required to use the CECClient hdmiCECController module. -Clone the repository and ensure that all requirements are met. +*If you do not have administrator rights, please ask your administrator to install the above packages for you.* ## Getting Started diff --git a/installation/activate.sh b/installation/activate.sh index da95526..5666b6d 100755 --- a/installation/activate.sh +++ b/installation/activate.sh @@ -27,6 +27,3 @@ if [ $0 != "bash" ];then fi . ${PWD}/VENV/bin/activate -export PYTHONPATH=${PWD}/VENV/lib/python3.8/site-packages/:$PYTHONPATH - - diff --git a/installation/install_requirements.sh b/installation/install_requirements.sh index 40b1352..adaec2c 100755 --- a/installation/install_requirements.sh +++ b/installation/install_requirements.sh @@ -22,26 +22,166 @@ #* ****************************************************************************** MY_PATH="`dirname \"${BASH_SOURCE[0]}\"`" -python_venv=$MY_PATH/VENV +PYTHON_VENV=$MY_PATH/VENV +SUDO=0 -venv_check=$(dpkg --list | grep python3-venv) -if [ -z "$venv_check" ]; then - echo "Please install python3-venv with the following command" - echo "sudo apt install -y python3-venv" - exit +NO_COLOR="\e[0m" +RED="\e[0;31m" +CYAN="\e[0;36m" +YELLOW="\e[1;33m" +GREEN="\e[0;32m" +RED_BOLD="\e[1;31m" +BLUE_BOLD="\e[1;34m" +YELLOW_BOLD="\e[1;33m" + +DEBUG_FLAG=0 +function ECHO() +{ + echo -e "$*" +} + +function DEBUG() +{ + # if set -x is in use debug messages are useless as whole stript will be shown + if [[ "$-" =~ "x" ]]; then + return + fi + if [[ "${DEBUG_FLAG}" == "1" ]];then + ECHO "${BLUE_BOLD}DEBUG: ${CYAN}$*${NO_COLOR}" > /dev/stderr + fi +} + +function INFO() +{ + ECHO "${GREEN}$*${NO_COLOR}" +} + +function WARNING() +{ + ECHO "${YELLOW_BOLD}Warning: ${YELLOW}$*${NO_COLOR}" > /dev/stderr +} + +function ERROR() +{ + ECHO "${RED_BOLD}ERROR: ${RED}$*${NO_COLOR}" + exit 1 +} + +function check_package_installed() +{ +# Check if a given package is installed. +# +# Arguments: +# $1: package_name: The package to check. +# +# Returns: +# 0: If package is installed. +# 1: If package is not installed. +# + DEBUG "BEGIN: ${FUNCNAME} [$*]" + local package_name="$1" + DEBUG "command -v ${package_name}" + local package_check="$(command -v ${package_name})" + if [[ -n "${package_check}" ]]; then + # If package check isn't empty + return 0 + fi + return 1 +} + +function version_check() +{ +# Check if a version is correct or not +# Arguments: +# $1: Version to check +# $2: Required version +# +: as the last character can be used to signify any version over the given number. +# -: as the last character can be used to signify any version below the given number. +# + DEBUG "BEGIN: ${FUNCNAME} [$*]" + local check_version="$1" + local required_version="$2" + local check_version_split=(${check_version//\./" "}) + DEBUG "Version split: [${version_split[0]}]" + local req_version_split=(${required_version//\./" "}) + local stop=$(("${#req_version_split[@]}"-1)) + DEBUG "LOOP STOP: [$stop]" + for i in $(seq 0 ${stop}) + do + local req_version_section="${req_version_split[$i]}" + DEBUG "Req Version Sect: [${req_version_section}]" + local check_version_section="${check_version_split[$i]}" + DEBUG "Check Version Sect: [${check_version_section}]" + case "${req_version_section}" in + *"+") + # Remove the + from the end of the string + req_version_section="${req_version_section%+}" + if [[ "$check_version_section" -ge "${req_version_section}" ]];then + return 0 + fi + return 1 + ;; + *"-") + # Remove the - from end of the string + req_version_section="${req_version_section%-}" + if [[ "$check_version_section" -le "${req_version_section}" ]];then + return 0 + fi + return 1 + ;; + *) + if [[ "${check_version_section}" != "${req_version_section}" ]];then + return 1 + fi + ;; + esac + done + return 0 +} + +##### MAIN ##### + +# Check for sudo rights. +if [[ -n "$(groups | grep sudo)" ]]; then + # If sudo is in groups. + DEBUG "SUDO=1" + SUDO=1 fi -if [ -d "$python_venv" ] && [ -e "$python_venv"/bin/activate ];then - . "$python_venv"/bin/activate +# Check python3 is installed. +check_package_installed python3 +if [[ "$?" != "0" ]];then + ERROR "Python 3 not found.\nPlease install python 3.10+" +fi + +# Python Version check. +python_version="$(python3 --version)" +# Remove the only text before the space +version_check "${python_version##* }" "3.10+" +if [[ "$?" != "0" ]];then + ERROR "Python version installed is too old. Version 3.10+ required" +fi + +if [ -d "${PYTHON_VENV}" ] && [ -e "${PYTHON_VENV}"/bin/activate ];then + . "${PYTHON_VENV}/bin/activate" pip install -qr $MY_PATH/requirements.txt else - rm -rf "$python_venv" - mkdir -p "$python_venv" - python3 -m venv "$python_venv" + rm -rf "${PYTHON_VENV}" + mkdir -p "${PYTHON_VENV}" + python3 -m venv "${PYTHON_VENV}" if [ "$?" != "0" ];then - echo "The python virtual environment could not be created" + ERROR "The python virtual environment could not be created" fi - . "$python_venv"/bin/activate - pip install -qr $MY_PATH/requirements.txt + . "${PYTHON_VENV}/bin/activate" + pip install -qr "${MY_PATH}/requirements.txt" fi +check_package_installed "cec-client" +if [[ "$?" != "0" ]];then + if [[ "${SUDO}" == "1" ]];then + sudo apt update && sudo apt install -y cec-client + else + WARNING "cec-client is not installed" + WARNING "You will not be able to use the CECClient module" + fi +fi From 71e8e15935ed3ef255dc6c0dacc6724795cb92c3 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:01:57 +0000 Subject: [PATCH 06/29] Fix #92: Removed deprecation comment from example_rack_config Removed the deprecated comment from the example_rack_config as the settings have not existed for this repository. --- examples/configs/example_rack_config.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/configs/example_rack_config.yml b/examples/configs/example_rack_config.yml index 5cfeddf..1b89f0d 100644 --- a/examples/configs/example_rack_config.yml +++ b/examples/configs/example_rack_config.yml @@ -25,10 +25,6 @@ # Each optional module will be off by default unless otherwise stated. # Uncomment and fill out the sections for the modules you require. -# Deprecated -# slotx: address: IP of device while running locally, replaced with slotx: ip -# slotx: deviceConsole: IP of device while running locally, replaced with slotx: devices - # Data that is global to all tests. globalConfig: includes: From ff614caba71a3c2ea14d60ad7ba3686262bb7d63 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Tue, 26 Nov 2024 17:42:22 +0530 Subject: [PATCH 07/29] Fix #119: Updating the remotecontrollers with keysimulator --- framework/core/commonRemote.py | 141 +++++++++++------- .../remoteControllerModules/keySimulator.py | 60 ++++++++ 2 files changed, 144 insertions(+), 57 deletions(-) create mode 100644 framework/core/remoteControllerModules/keySimulator.py diff --git a/framework/core/commonRemote.py b/framework/core/commonRemote.py index cb173f5..cf3b8ea 100644 --- a/framework/core/commonRemote.py +++ b/framework/core/commonRemote.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -#** ***************************************************************************** +# ** ***************************************************************************** # * # * If not stated otherwise in this file or this component's LICENSE file the # * following copyright and licenses apply: @@ -19,15 +19,15 @@ # * See the License for the specific language governing permissions and # * limitations under the License. # * -#* ****************************************************************************** -#* -#* ** Project : RAFT -#* ** @addtogroup : core -#* ** @date : 22/11/2021 -#* ** -#* ** @brief : commonRemote with key mapping -#* ** -#* ****************************************************************************** +# * ****************************************************************************** +# * +# * ** Project : RAFT +# * ** @addtogroup : core +# * ** @date : 22/11/2021 +# * ** +# * ** @brief : commonRemote with key mapping +# * ** +# * ****************************************************************************** import time import yaml @@ -38,9 +38,11 @@ from framework.core.remoteControllerModules.skyProc import remoteSkyProc from framework.core.remoteControllerModules.arduino import remoteArduino from framework.core.remoteControllerModules.none import remoteNone +from framework.core.remoteControllerModules.keySimulator import keySimulator + -class remoteControllerMapping(): - def __init__(self, log:logModule, mappingConfig:dict): +class remoteControllerMapping: + def __init__(self, log: logModule, mappingConfig: dict): """Initialise the remote controller key mapping class Args: @@ -54,9 +56,9 @@ def __init__(self, log:logModule, mappingConfig:dict): defaultMap = mappingConfig[0]["name"] except: defaultMap = None - self.setKeyMap( defaultMap ) + self.setKeyMap(defaultMap) - def getMappedKey(self, key:str): + def getMappedKey(self, key: str): """Get the mapped key Args: @@ -66,16 +68,20 @@ def getMappedKey(self, key:str): str: Translated key via map or None on failure """ if self.currentMap == None: - #self.log.info("No map defined") + # self.log.info("No map defined") return key if not key in self.currentMap["codes"]: - self.log.error("remoteControllerMapping.get() map=[{}] not found".format(self.currentMap["name"])) + self.log.error( + "remoteControllerMapping.get() map=[{}] not found".format( + self.currentMap["name"] + ) + ) return None - + prefix = self.currentMap.get("prefix") - returnedKey=self.currentMap["codes"].get(key) + returnedKey = self.currentMap["codes"].get(key) if prefix: - returnedKey = prefix+key + returnedKey = prefix + returnedKey return returnedKey def getKeyMap(self): @@ -85,8 +91,8 @@ def getKeyMap(self): dict: Active key map """ return self.currentMap - - def setKeyMap(self, newMapName:dict ): + + def setKeyMap(self, newMapName: dict): """Set the key map Args: @@ -111,8 +117,9 @@ def setKeyMap(self, newMapName:dict ): return False return True -class commonRemoteClass(): - def __init__(self, log:logModule, remoteConfig:dict, **kwargs:dict): + +class commonRemoteClass: + def __init__(self, log: logModule, remoteConfig: dict, **kwargs: dict): """Intialise a commonRemote Args: @@ -123,21 +130,22 @@ def __init__(self, log:logModule, remoteConfig:dict, **kwargs:dict): self.remoteConfig = remoteConfig rcMappingConfig = self.__decodeRemoteMapConfig() keyMap = remoteConfig.get("map") - self.remoteMap = remoteControllerMapping( log, rcMappingConfig ) - self.setKeyMap( keyMap ) + self.remoteMap = remoteControllerMapping(log, rcMappingConfig) + self.setKeyMap(keyMap) self.type = remoteConfig.get("type") if self.type == "olimex": - self.remoteController = remoteOlimex( self.log, remoteConfig ) + self.remoteController = remoteOlimex(self.log, remoteConfig) elif self.type == "sky_proc": - self.remoteController = remoteSkyProc( self.log, remoteConfig ) + self.remoteController = remoteSkyProc(self.log, remoteConfig) elif self.type == "arduino": - self.remoteController = remoteArduino (self.log, remoteConfig) - else: # remoteNone otherwise - self.remoteController = remoteNone( self.log, remoteConfig ) + self.remoteController = remoteArduino(self.log, remoteConfig) + elif self.type == "keySimulator": + self.remoteController = keySimulator(self.log, remoteConfig) + else: # remoteNone otherwise + self.remoteController = remoteNone(self.log, remoteConfig) def __decodeRemoteMapConfig(self): - """Decode the remote map configuration file - """ + """Decode the remote map configuration file""" configFile = self.remoteConfig.get("config") if configFile == None: return @@ -145,21 +153,20 @@ def __decodeRemoteMapConfig(self): if fullPath.startswith("."): fullPath = os.path.abspath(configFile) if os.path.exists(fullPath) == False: - print("config: file is required to run: ERROR, missing url=[{}]".format(fullPath)) + print( + "config: file is required to run: ERROR, missing url=[{}]".format( + fullPath + ) + ) os._exit(1) with open(configFile) as inputFile: inputFile.seek(0, os.SEEK_SET) config = yaml.full_load(inputFile) - keyDictionary = {} - for key, val in config.items(): - if isinstance(val, dict): - for k, v in val.items(): - keyDictionary[k] = v - else: - keyDictionary[key] = val - return keyDictionary + return config.get("remoteMaps", []) - def sendKey(self, keycode:dict, delay:int=1, repeat:int=1, randomRepeat:int=0): + def sendKey( + self, keycode: dict, delay: int = 1, repeat: int = 1, randomRepeat: int = 0 + ): """Send a key to the remoteCommander Args: @@ -168,28 +175,48 @@ def sendKey(self, keycode:dict, delay:int=1, repeat:int=1, randomRepeat:int=0): repeat (int, optional): How many key repeats. Defaults to 1. randomRepeat (int, optional): Random Key repeat value. Defaults to 0. """ - if (randomRepeat != 0): + if randomRepeat != 0: import random - repeat=random.randint(0, randomRepeat) - self.log.info( "sendKey[" + keycode.name + "] delay:[" +str(delay)+"] randomRepeat:["+str(randomRepeat)+"] -> repeat:["+str(repeat)+"]" ) + + repeat = random.randint(0, randomRepeat) + self.log.info( + "sendKey[" + + keycode.name + + "] delay:[" + + str(delay) + + "] randomRepeat:[" + + str(randomRepeat) + + "] -> repeat:[" + + str(repeat) + + "]" + ) else: - if (repeat != 1): - self.log.info( "sendKey[" + keycode.name + "] delay:[" +str(delay)+"] repeat:["+str(repeat)+"]" ) - else: - self.log.info( "sendKey[" + keycode.name + "] delay:[" +str(delay)+"]" ) + if repeat != 1: + self.log.info( + "sendKey[" + + keycode.name + + "] delay:[" + + str(delay) + + "] repeat:[" + + str(repeat) + + "]" + ) + else: + self.log.info( + "sendKey[" + keycode.name + "] delay:[" + str(delay) + "]" + ) - mappedCode = self.remoteMap.getMappedKey( keycode.name ) - result = self.remoteController.sendKey( mappedCode, repeat, delay) + mappedCode = self.remoteMap.getMappedKey(keycode.name) + result = self.remoteController.sendKey(mappedCode, repeat, delay) - def setKeyMap( self, name:dict ): + def setKeyMap(self, name: dict): """Set the Key Translation Map Args: name (dict): Translation dictionary """ - self.remoteMap.setKeyMap( name ) + self.remoteMap.setKeyMap(name) - def getKeyMap( self ): - """Get the Key Translation Map - """ - self.remoteMap.getKeyMap() + def getKeyMap(self): + """Get the Key Translation Map""" + return self.remoteMap.getKeyMap() diff --git a/framework/core/remoteControllerModules/keySimulator.py b/framework/core/remoteControllerModules/keySimulator.py new file mode 100644 index 0000000..86044af --- /dev/null +++ b/framework/core/remoteControllerModules/keySimulator.py @@ -0,0 +1,60 @@ +import os +import time +import subprocess +from framework.core.logModule import logModule +from framework.core.commandModules.sshConsole import sshConsole + + +class KeySimulator: + + def __init__(self, log: logModule, remoteConfig: dict): + """Initialize the KeySimulator class. + + Args: + log (logModule): Logging module instance. + remoteConfig (dict): Key simulator configuration. + """ + self.log = log + self.remoteConfig = remoteConfig + self.prompt = r"\$ " + + # Initialize SSH session + self.session = sshConsole( + address=self.remoteConfig.get("ip"), + username=self.remoteConfig.get("username"), + password=self.remoteConfig.get("password"), + known_hosts=self.remoteConfig.get("known_hosts"), + port=int(self.remoteConfig.get("port")), + ) + + self.firstKeyPressInTc = True + + def sendKey(self, key: str, repeat: int = 1, delay: int = 0) -> bool: + """Send a key command with specified repeats and interval. + + Args: + key (str): The key to send. + repeat (int): Number of times to send the key. + delay (int): Delay between key presses in seconds. + + Returns: + bool: Result of the command verification. + """ + result = True + verify = True + keyword = "term start init 1" + + # Send the key command + self.session.write(f"{key}") + + if verify: + output = self.session.read_until(self.prompt) + print(output) + + # Check for the presence of a keyword in the output + if keyword and keyword not in output: + result = True + else: + time.sleep(delay) + + return result From 67c28ebea944bde34f70d65fd30d006dc2067893 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Tue, 26 Nov 2024 20:59:32 +0530 Subject: [PATCH 08/29] Fix #119: Updated the commonRemote file format --- framework/core/commonRemote.py | 131 +++++++++++++-------------------- 1 file changed, 50 insertions(+), 81 deletions(-) diff --git a/framework/core/commonRemote.py b/framework/core/commonRemote.py index cf3b8ea..3d2875e 100644 --- a/framework/core/commonRemote.py +++ b/framework/core/commonRemote.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# ** ***************************************************************************** +#** ***************************************************************************** # * # * If not stated otherwise in this file or this component's LICENSE file the # * following copyright and licenses apply: @@ -19,15 +19,15 @@ # * See the License for the specific language governing permissions and # * limitations under the License. # * -# * ****************************************************************************** -# * -# * ** Project : RAFT -# * ** @addtogroup : core -# * ** @date : 22/11/2021 -# * ** -# * ** @brief : commonRemote with key mapping -# * ** -# * ****************************************************************************** +#* ****************************************************************************** +#* +#* ** Project : RAFT +#* ** @addtogroup : core +#* ** @date : 22/11/2021 +#* ** +#* ** @brief : commonRemote with key mapping +#* ** +#* ****************************************************************************** import time import yaml @@ -40,9 +40,8 @@ from framework.core.remoteControllerModules.none import remoteNone from framework.core.remoteControllerModules.keySimulator import keySimulator - -class remoteControllerMapping: - def __init__(self, log: logModule, mappingConfig: dict): +class remoteControllerMapping(): + def __init__(self, log:logModule, mappingConfig:dict): """Initialise the remote controller key mapping class Args: @@ -56,9 +55,9 @@ def __init__(self, log: logModule, mappingConfig: dict): defaultMap = mappingConfig[0]["name"] except: defaultMap = None - self.setKeyMap(defaultMap) + self.setKeyMap( defaultMap ) - def getMappedKey(self, key: str): + def getMappedKey(self, key:str): """Get the mapped key Args: @@ -68,18 +67,14 @@ def getMappedKey(self, key: str): str: Translated key via map or None on failure """ if self.currentMap == None: - # self.log.info("No map defined") + #self.log.info("No map defined") return key if not key in self.currentMap["codes"]: - self.log.error( - "remoteControllerMapping.get() map=[{}] not found".format( - self.currentMap["name"] - ) - ) + self.log.error("remoteControllerMapping.get() map=[{}] not found".format(self.currentMap["name"])) return None - + prefix = self.currentMap.get("prefix") - returnedKey = self.currentMap["codes"].get(key) + returnedKey=self.currentMap["codes"].get(key) if prefix: returnedKey = prefix + returnedKey return returnedKey @@ -91,8 +86,8 @@ def getKeyMap(self): dict: Active key map """ return self.currentMap - - def setKeyMap(self, newMapName: dict): + + def setKeyMap(self, newMapName:dict ): """Set the key map Args: @@ -117,9 +112,8 @@ def setKeyMap(self, newMapName: dict): return False return True - -class commonRemoteClass: - def __init__(self, log: logModule, remoteConfig: dict, **kwargs: dict): +class commonRemoteClass(): + def __init__(self, log:logModule, remoteConfig:dict, **kwargs:dict): """Intialise a commonRemote Args: @@ -130,22 +124,23 @@ def __init__(self, log: logModule, remoteConfig: dict, **kwargs: dict): self.remoteConfig = remoteConfig rcMappingConfig = self.__decodeRemoteMapConfig() keyMap = remoteConfig.get("map") - self.remoteMap = remoteControllerMapping(log, rcMappingConfig) - self.setKeyMap(keyMap) + self.remoteMap = remoteControllerMapping( log, rcMappingConfig ) + self.setKeyMap( keyMap ) self.type = remoteConfig.get("type") if self.type == "olimex": - self.remoteController = remoteOlimex(self.log, remoteConfig) + self.remoteController = remoteOlimex( self.log, remoteConfig ) elif self.type == "sky_proc": - self.remoteController = remoteSkyProc(self.log, remoteConfig) + self.remoteController = remoteSkyProc( self.log, remoteConfig ) elif self.type == "arduino": - self.remoteController = remoteArduino(self.log, remoteConfig) + self.remoteController = remoteArduino (self.log, remoteConfig) elif self.type == "keySimulator": - self.remoteController = keySimulator(self.log, remoteConfig) - else: # remoteNone otherwise - self.remoteController = remoteNone(self.log, remoteConfig) + self.remoteController = keySimulator (self.log, remoteConfig) + else: # remoteNone otherwise + self.remoteController = remoteNone( self.log, remoteConfig ) def __decodeRemoteMapConfig(self): - """Decode the remote map configuration file""" + """Decode the remote map configuration file + """ configFile = self.remoteConfig.get("config") if configFile == None: return @@ -153,20 +148,14 @@ def __decodeRemoteMapConfig(self): if fullPath.startswith("."): fullPath = os.path.abspath(configFile) if os.path.exists(fullPath) == False: - print( - "config: file is required to run: ERROR, missing url=[{}]".format( - fullPath - ) - ) + print("config: file is required to run: ERROR, missing url=[{}]".format(fullPath)) os._exit(1) with open(configFile) as inputFile: inputFile.seek(0, os.SEEK_SET) config = yaml.full_load(inputFile) return config.get("remoteMaps", []) - def sendKey( - self, keycode: dict, delay: int = 1, repeat: int = 1, randomRepeat: int = 0 - ): + def sendKey(self, keycode:dict, delay:int=1, repeat:int=1, randomRepeat:int=0): """Send a key to the remoteCommander Args: @@ -175,48 +164,28 @@ def sendKey( repeat (int, optional): How many key repeats. Defaults to 1. randomRepeat (int, optional): Random Key repeat value. Defaults to 0. """ - if randomRepeat != 0: + if (randomRepeat != 0): import random - - repeat = random.randint(0, randomRepeat) - self.log.info( - "sendKey[" - + keycode.name - + "] delay:[" - + str(delay) - + "] randomRepeat:[" - + str(randomRepeat) - + "] -> repeat:[" - + str(repeat) - + "]" - ) + repeat=random.randint(0, randomRepeat) + self.log.info( "sendKey[" + keycode.name + "] delay:[" +str(delay)+"] randomRepeat:["+str(randomRepeat)+"] -> repeat:["+str(repeat)+"]" ) else: - if repeat != 1: - self.log.info( - "sendKey[" - + keycode.name - + "] delay:[" - + str(delay) - + "] repeat:[" - + str(repeat) - + "]" - ) - else: - self.log.info( - "sendKey[" + keycode.name + "] delay:[" + str(delay) + "]" - ) - - mappedCode = self.remoteMap.getMappedKey(keycode.name) - result = self.remoteController.sendKey(mappedCode, repeat, delay) - - def setKeyMap(self, name: dict): + if (repeat != 1): + self.log.info( "sendKey[" + keycode.name + "] delay:[" +str(delay)+"] repeat:["+str(repeat)+"]" ) + else: + self.log.info( "sendKey[" + keycode.name + "] delay:[" +str(delay)+"]" ) + + mappedCode = self.remoteMap.getMappedKey( keycode.name ) + result = self.remoteController.sendKey( mappedCode, repeat, delay) + + def setKeyMap( self, name:dict ): """Set the Key Translation Map Args: name (dict): Translation dictionary """ - self.remoteMap.setKeyMap(name) + self.remoteMap.setKeyMap( name ) - def getKeyMap(self): - """Get the Key Translation Map""" + def getKeyMap( self ): + """Get the Key Translation Map + """ return self.remoteMap.getKeyMap() From 1cea7ad389ffb11c58a9a15e0dade6fb412357cb Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Tue, 26 Nov 2024 21:08:30 +0530 Subject: [PATCH 09/29] Fix #119: Removed the print statement --- framework/core/remoteControllerModules/keySimulator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/core/remoteControllerModules/keySimulator.py b/framework/core/remoteControllerModules/keySimulator.py index 86044af..2a40cf6 100644 --- a/framework/core/remoteControllerModules/keySimulator.py +++ b/framework/core/remoteControllerModules/keySimulator.py @@ -49,7 +49,6 @@ def sendKey(self, key: str, repeat: int = 1, delay: int = 0) -> bool: if verify: output = self.session.read_until(self.prompt) - print(output) # Check for the presence of a keyword in the output if keyword and keyword not in output: From c0ecd379f32e50daae7e4d1ad89adcc526c459f5 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Wed, 27 Nov 2024 12:58:15 +0530 Subject: [PATCH 10/29] Fix #119: Changed the code and removed the control flow issue --- .../remoteControllerModules/keySimulator.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/framework/core/remoteControllerModules/keySimulator.py b/framework/core/remoteControllerModules/keySimulator.py index 2a40cf6..1971fc9 100644 --- a/framework/core/remoteControllerModules/keySimulator.py +++ b/framework/core/remoteControllerModules/keySimulator.py @@ -40,20 +40,19 @@ def sendKey(self, key: str, repeat: int = 1, delay: int = 0) -> bool: Returns: bool: Result of the command verification. """ - result = True - verify = True + result = False keyword = "term start init 1" # Send the key command - self.session.write(f"{key}") + for _ in range(repeat): + self.session.write(f"{key}") + time.sleep(delay) - if verify: - output = self.session.read_until(self.prompt) + # Read output after sending keys + output = self.session.read_until(self.prompt) - # Check for the presence of a keyword in the output - if keyword and keyword not in output: - result = True - else: - time.sleep(delay) + # Check for the presence of a keyword in the output + if keyword in output: + result = True return result From 182c151a5d973c96e9551ef33cd6ff4faf39639e Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Wed, 27 Nov 2024 15:50:25 +0530 Subject: [PATCH 11/29] Fix #119: Changed the code --- .../remoteControllerModules/keySimulator.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/framework/core/remoteControllerModules/keySimulator.py b/framework/core/remoteControllerModules/keySimulator.py index 1971fc9..3266061 100644 --- a/framework/core/remoteControllerModules/keySimulator.py +++ b/framework/core/remoteControllerModules/keySimulator.py @@ -1,3 +1,32 @@ +#** ***************************************************************************** +# * +# * If not stated otherwise in this file or this component's LICENSE file the +# * following copyright and licenses apply: +# * +# * Copyright 2023 RDK Management +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * +# http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# * +#* ****************************************************************************** +#* +#* ** Project : RAFT +#* ** @addtogroup : core.remoteControllerModules +#* ** @date : 27/11/2024 +#* ** +#* ** @brief : remote keySimulator +#* ** +#* ****************************************************************************** import os import time import subprocess @@ -27,9 +56,8 @@ def __init__(self, log: logModule, remoteConfig: dict): port=int(self.remoteConfig.get("port")), ) - self.firstKeyPressInTc = True - def sendKey(self, key: str, repeat: int = 1, delay: int = 0) -> bool: + def sendKey(self, key: str, repeat: int , delay: int ): """Send a key command with specified repeats and interval. Args: From 18b5de1128edc578ab4f532d0d74899d43f25e35 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Thu, 28 Nov 2024 11:40:05 +0530 Subject: [PATCH 12/29] Fix #119: Added the keySimulator in the example list --- examples/configs/example_rack_config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/configs/example_rack_config.yml b/examples/configs/example_rack_config.yml index 1b89f0d..35b164d 100644 --- a/examples/configs/example_rack_config.yml +++ b/examples/configs/example_rack_config.yml @@ -71,6 +71,7 @@ rackConfig: # [ type: "olimex", ip: "192.168.0.17", port: 7, map: "llama_rc6", config: "remote_commander.yml" ] # [ type: "skyProc", map: "skyq_map", config: "remote_commander.yml" ] # [ type: "None" ] + # [ type: "keySimulator", ip: "192.168.50.99", port: 10022, username: "root", password: '', map: "keysimulator_xione", config: "keymap.yml" ] # [ outbound: optional ] - This section is used to configure paths for downloads and uploads from your test # supported usage: From 1a0b22b28a23684b6700bb8bfaf839632f072456 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Thu, 28 Nov 2024 16:29:50 +0530 Subject: [PATCH 13/29] Fix #119: Changed the file name in the example list --- examples/configs/example_rack_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/configs/example_rack_config.yml b/examples/configs/example_rack_config.yml index 35b164d..183f5c2 100644 --- a/examples/configs/example_rack_config.yml +++ b/examples/configs/example_rack_config.yml @@ -71,7 +71,7 @@ rackConfig: # [ type: "olimex", ip: "192.168.0.17", port: 7, map: "llama_rc6", config: "remote_commander.yml" ] # [ type: "skyProc", map: "skyq_map", config: "remote_commander.yml" ] # [ type: "None" ] - # [ type: "keySimulator", ip: "192.168.50.99", port: 10022, username: "root", password: '', map: "keysimulator_xione", config: "keymap.yml" ] + # [ type: "keySimulator", ip: "192.168.50.99", port: 10022, username: "root", password: '', map: "keysimulator_xione", config: "rdk_keymap.yml" ] # [ outbound: optional ] - This section is used to configure paths for downloads and uploads from your test # supported usage: From 63131b581718d45def574f2e2bbf4384739471aa Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Thu, 28 Nov 2024 17:19:38 +0530 Subject: [PATCH 14/29] Fix #119: Updated the file name --- examples/configs/example_rack_config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/configs/example_rack_config.yml b/examples/configs/example_rack_config.yml index 183f5c2..9a2cfb5 100644 --- a/examples/configs/example_rack_config.yml +++ b/examples/configs/example_rack_config.yml @@ -71,7 +71,8 @@ rackConfig: # [ type: "olimex", ip: "192.168.0.17", port: 7, map: "llama_rc6", config: "remote_commander.yml" ] # [ type: "skyProc", map: "skyq_map", config: "remote_commander.yml" ] # [ type: "None" ] - # [ type: "keySimulator", ip: "192.168.50.99", port: 10022, username: "root", password: '', map: "keysimulator_xione", config: "rdk_keymap.yml" ] + # To use keySimulator RDK Middleware is required + # [ type: "keySimulator", ip: "192.168.50.99", port: 10022, username: "root", password: '', map: "keysimulator_rdk", config: "rdk_keymap.yml" ] # [ outbound: optional ] - This section is used to configure paths for downloads and uploads from your test # supported usage: From 01cd9c6697d1de181f405329820efa380ef2b170 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Wed, 4 Dec 2024 11:25:32 +0530 Subject: [PATCH 15/29] Fix #119: Changed the key value str to rccode --- framework/core/remoteControllerModules/keySimulator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/core/remoteControllerModules/keySimulator.py b/framework/core/remoteControllerModules/keySimulator.py index 3266061..889d946 100644 --- a/framework/core/remoteControllerModules/keySimulator.py +++ b/framework/core/remoteControllerModules/keySimulator.py @@ -32,6 +32,7 @@ import subprocess from framework.core.logModule import logModule from framework.core.commandModules.sshConsole import sshConsole +from framework.core.rcCodes import rcCode as rc class KeySimulator: @@ -57,11 +58,11 @@ def __init__(self, log: logModule, remoteConfig: dict): ) - def sendKey(self, key: str, repeat: int , delay: int ): + def sendKey(self, key: rc, repeat: int , delay: int ): """Send a key command with specified repeats and interval. Args: - key (str): The key to send. + key (rc): The key to send. repeat (int): Number of times to send the key. delay (int): Delay between key presses in seconds. From 3a0125522a0a9f72be9f1b520992199233141a90 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Wed, 4 Dec 2024 20:58:38 +0530 Subject: [PATCH 16/29] Fix #119: Reverting the previous commit changes --- framework/core/remoteControllerModules/keySimulator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/framework/core/remoteControllerModules/keySimulator.py b/framework/core/remoteControllerModules/keySimulator.py index 889d946..3266061 100644 --- a/framework/core/remoteControllerModules/keySimulator.py +++ b/framework/core/remoteControllerModules/keySimulator.py @@ -32,7 +32,6 @@ import subprocess from framework.core.logModule import logModule from framework.core.commandModules.sshConsole import sshConsole -from framework.core.rcCodes import rcCode as rc class KeySimulator: @@ -58,11 +57,11 @@ def __init__(self, log: logModule, remoteConfig: dict): ) - def sendKey(self, key: rc, repeat: int , delay: int ): + def sendKey(self, key: str, repeat: int , delay: int ): """Send a key command with specified repeats and interval. Args: - key (rc): The key to send. + key (str): The key to send. repeat (int): Number of times to send the key. delay (int): Delay between key presses in seconds. From 9f58795360fd525c9fe016d0f8d61f31f999a3e8 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Thu, 5 Dec 2024 14:44:33 +0530 Subject: [PATCH 17/29] Fix #119: Updated the changes --- framework/core/commonRemote.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/framework/core/commonRemote.py b/framework/core/commonRemote.py index 3d2875e..be7ae4b 100644 --- a/framework/core/commonRemote.py +++ b/framework/core/commonRemote.py @@ -76,7 +76,7 @@ def getMappedKey(self, key:str): prefix = self.currentMap.get("prefix") returnedKey=self.currentMap["codes"].get(key) if prefix: - returnedKey = prefix + returnedKey + returnedKey = prefix + key return returnedKey def getKeyMap(self): @@ -153,7 +153,15 @@ def __decodeRemoteMapConfig(self): with open(configFile) as inputFile: inputFile.seek(0, os.SEEK_SET) config = yaml.full_load(inputFile) - return config.get("remoteMaps", []) + keyDictionary = {} + for key, val in config.items(): + if isinstance(val, dict): + for k, v in val.items(): + keyDictionary[k] = v + else: + keyDictionary[key] = val + + return keyDictionary.get("remoteMaps") def sendKey(self, keycode:dict, delay:int=1, repeat:int=1, randomRepeat:int=0): """Send a key to the remoteCommander @@ -188,4 +196,4 @@ def setKeyMap( self, name:dict ): def getKeyMap( self ): """Get the Key Translation Map """ - return self.remoteMap.getKeyMap() + self.remoteMap.getKeyMap() From 0cd320cf42b04d676053242fb35b64b2d1c4b7d3 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Thu, 5 Dec 2024 14:57:53 +0530 Subject: [PATCH 18/29] Fix #119: Fixed the changes --- framework/core/commonRemote.py | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/core/commonRemote.py b/framework/core/commonRemote.py index be7ae4b..017aca9 100644 --- a/framework/core/commonRemote.py +++ b/framework/core/commonRemote.py @@ -160,7 +160,6 @@ def __decodeRemoteMapConfig(self): keyDictionary[k] = v else: keyDictionary[key] = val - return keyDictionary.get("remoteMaps") def sendKey(self, keycode:dict, delay:int=1, repeat:int=1, randomRepeat:int=0): From ac26360effde120991aeb53a14a39dc914e4a943 Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Sat, 7 Dec 2024 21:28:07 +0530 Subject: [PATCH 19/29] Fix #119: Updated the code changes --- framework/core/commonRemote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/core/commonRemote.py b/framework/core/commonRemote.py index 017aca9..44c7950 100644 --- a/framework/core/commonRemote.py +++ b/framework/core/commonRemote.py @@ -76,7 +76,7 @@ def getMappedKey(self, key:str): prefix = self.currentMap.get("prefix") returnedKey=self.currentMap["codes"].get(key) if prefix: - returnedKey = prefix + key + returnedKey = prefix + returnedKey return returnedKey def getKeyMap(self): @@ -195,4 +195,4 @@ def setKeyMap( self, name:dict ): def getKeyMap( self ): """Get the Key Translation Map """ - self.remoteMap.getKeyMap() + return self.remoteMap.getKeyMap() From 3cbe06299bf54b24a28dc61a97a756628ffd054b Mon Sep 17 00:00:00 2001 From: tamilarasi-t12 Date: Wed, 11 Dec 2024 22:40:47 +0530 Subject: [PATCH 20/29] Fix #119: Fixed the code changes for combination of keys --- .../remoteControllerModules/keySimulator.py | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/framework/core/remoteControllerModules/keySimulator.py b/framework/core/remoteControllerModules/keySimulator.py index 3266061..08f027b 100644 --- a/framework/core/remoteControllerModules/keySimulator.py +++ b/framework/core/remoteControllerModules/keySimulator.py @@ -1,4 +1,4 @@ -#** ***************************************************************************** +# ** ***************************************************************************** # * # * If not stated otherwise in this file or this component's LICENSE file the # * following copyright and licenses apply: @@ -18,15 +18,15 @@ # * See the License for the specific language governing permissions and # * limitations under the License. # * -#* ****************************************************************************** -#* -#* ** Project : RAFT -#* ** @addtogroup : core.remoteControllerModules -#* ** @date : 27/11/2024 -#* ** -#* ** @brief : remote keySimulator -#* ** -#* ****************************************************************************** +# * ****************************************************************************** +# * +# * ** Project : RAFT +# * ** @addtogroup : core.remoteControllerModules +# * ** @date : 27/11/2024 +# * ** +# * ** @brief : remote keySimulator +# * ** +# * ****************************************************************************** import os import time import subprocess @@ -34,7 +34,7 @@ from framework.core.commandModules.sshConsole import sshConsole -class KeySimulator: +class keySimulator: def __init__(self, log: logModule, remoteConfig: dict): """Initialize the KeySimulator class. @@ -45,7 +45,6 @@ def __init__(self, log: logModule, remoteConfig: dict): """ self.log = log self.remoteConfig = remoteConfig - self.prompt = r"\$ " # Initialize SSH session self.session = sshConsole( @@ -54,10 +53,11 @@ def __init__(self, log: logModule, remoteConfig: dict): password=self.remoteConfig.get("password"), known_hosts=self.remoteConfig.get("known_hosts"), port=int(self.remoteConfig.get("port")), + prompt=self.remoteConfig.get("prompt"), + log=self.log, ) - - def sendKey(self, key: str, repeat: int , delay: int ): + def sendKey(self, key: str, repeat: int, delay: int): """Send a key command with specified repeats and interval. Args: @@ -69,18 +69,10 @@ def sendKey(self, key: str, repeat: int , delay: int ): bool: Result of the command verification. """ result = False - keyword = "term start init 1" # Send the key command for _ in range(repeat): - self.session.write(f"{key}") + result = self.session.write(f"keySimulator -k{key}", wait_for_prompt=True) time.sleep(delay) - # Read output after sending keys - output = self.session.read_until(self.prompt) - - # Check for the presence of a keyword in the output - if keyword in output: - result = True - return result From 0acbd49f3294c1e7f2cd52b4e249fa26ce6c154d Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:19:11 +0000 Subject: [PATCH 21/29] Fix #119: Corrected commonRemote to properly parse key --- framework/core/commonRemote.py | 17 +++++++---------- framework/core/hdmiCECController.py | 2 +- .../remoteControllerModules/keySimulator.py | 8 +++----- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/framework/core/commonRemote.py b/framework/core/commonRemote.py index 44c7950..ef87447 100644 --- a/framework/core/commonRemote.py +++ b/framework/core/commonRemote.py @@ -38,7 +38,7 @@ from framework.core.remoteControllerModules.skyProc import remoteSkyProc from framework.core.remoteControllerModules.arduino import remoteArduino from framework.core.remoteControllerModules.none import remoteNone -from framework.core.remoteControllerModules.keySimulator import keySimulator +from framework.core.remoteControllerModules.keySimulator import remoteKeySimulator class remoteControllerMapping(): def __init__(self, log:logModule, mappingConfig:dict): @@ -102,6 +102,8 @@ def setKeyMap(self, newMapName:dict ): self.log.error("RemoteController keyMap [{}] not found".format(newMapName)) return False found = False + if isinstance(self.maps, dict): + self.maps = list(self.maps.values()) for x in self.maps: if x["name"] == newMapName: self.currentMap = x @@ -134,7 +136,7 @@ def __init__(self, log:logModule, remoteConfig:dict, **kwargs:dict): elif self.type == "arduino": self.remoteController = remoteArduino (self.log, remoteConfig) elif self.type == "keySimulator": - self.remoteController = keySimulator (self.log, remoteConfig) + self.remoteController = remoteKeySimulator (self.log, remoteConfig) else: # remoteNone otherwise self.remoteController = remoteNone( self.log, remoteConfig ) @@ -153,14 +155,8 @@ def __decodeRemoteMapConfig(self): with open(configFile) as inputFile: inputFile.seek(0, os.SEEK_SET) config = yaml.full_load(inputFile) - keyDictionary = {} - for key, val in config.items(): - if isinstance(val, dict): - for k, v in val.items(): - keyDictionary[k] = v - else: - keyDictionary[key] = val - return keyDictionary.get("remoteMaps") + keyDictionary = config.get('remoteMaps',{}) + return keyDictionary def sendKey(self, keycode:dict, delay:int=1, repeat:int=1, randomRepeat:int=0): """Send a key to the remoteCommander @@ -183,6 +179,7 @@ def sendKey(self, keycode:dict, delay:int=1, repeat:int=1, randomRepeat:int=0): mappedCode = self.remoteMap.getMappedKey( keycode.name ) result = self.remoteController.sendKey( mappedCode, repeat, delay) + return result def setKeyMap( self, name:dict ): """Set the Key Translation Map diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py index 17f4787..0c8048a 100644 --- a/framework/core/hdmiCECController.py +++ b/framework/core/hdmiCECController.py @@ -37,7 +37,7 @@ MY_DIR = path.dirname(MY_PATH) sys.path.append(path.join(MY_DIR,'../../')) from framework.core.logModule import logModule -from hdmicecModules import CECClientController, MonitoringType +from framework.core.hdmicecModules import CECClientController, MonitoringType class HDMICECController(): """ diff --git a/framework/core/remoteControllerModules/keySimulator.py b/framework/core/remoteControllerModules/keySimulator.py index 08f027b..3c7435f 100644 --- a/framework/core/remoteControllerModules/keySimulator.py +++ b/framework/core/remoteControllerModules/keySimulator.py @@ -27,14 +27,12 @@ # * ** @brief : remote keySimulator # * ** # * ****************************************************************************** -import os import time -import subprocess from framework.core.logModule import logModule from framework.core.commandModules.sshConsole import sshConsole -class keySimulator: +class remoteKeySimulator: def __init__(self, log: logModule, remoteConfig: dict): """Initialize the KeySimulator class. @@ -48,13 +46,13 @@ def __init__(self, log: logModule, remoteConfig: dict): # Initialize SSH session self.session = sshConsole( + log=self.log, address=self.remoteConfig.get("ip"), username=self.remoteConfig.get("username"), password=self.remoteConfig.get("password"), known_hosts=self.remoteConfig.get("known_hosts"), port=int(self.remoteConfig.get("port")), - prompt=self.remoteConfig.get("prompt"), - log=self.log, + prompt=self.remoteConfig.get("prompt", ':~$ ') ) def sendKey(self, key: str, repeat: int, delay: int): From 3a28750177a45d413621f9d9f13860d620142827 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:01:57 +0000 Subject: [PATCH 22/29] Update #107: Creation of remote hdmicec client --- examples/configs/example_rack_config.yml | 1 + framework/core/hdmiCECController.py | 30 +++- framework/core/hdmicecModules/__init__.py | 1 + .../hdmicecModules/abstractCECController.py | 1 + framework/core/hdmicecModules/cecClient.py | 27 +-- .../core/hdmicecModules/remoteCECClient.py | 158 ++++++++++++++++++ framework/core/logModule.py | 58 ++++++- 7 files changed, 245 insertions(+), 31 deletions(-) create mode 100644 framework/core/hdmicecModules/remoteCECClient.py diff --git a/examples/configs/example_rack_config.yml b/examples/configs/example_rack_config.yml index 9a2cfb5..b0d9372 100644 --- a/examples/configs/example_rack_config.yml +++ b/examples/configs/example_rack_config.yml @@ -96,6 +96,7 @@ rackConfig: # [ hdmiCECController: optional ] - Specific hdmiCECController for the slot # supported types: # [type: "cec-client", adaptor: "/dev/ttycec"] + # [type: "remote-cec-client", adaptor: "/dev/ttycec", address: "192.168.99.1", username(optional): "testuser", password(optional): "testpswd", port(optional): "22"] - pi2: ip: "192.168.99.1" description: "local pi4" diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py index 0c8048a..f7ab5f1 100644 --- a/framework/core/hdmiCECController.py +++ b/framework/core/hdmiCECController.py @@ -37,11 +37,11 @@ MY_DIR = path.dirname(MY_PATH) sys.path.append(path.join(MY_DIR,'../../')) from framework.core.logModule import logModule -from framework.core.hdmicecModules import CECClientController, MonitoringType +from framework.core.hdmicecModules import CECClientController, RemoteCECClient, MonitoringType class HDMICECController(): """ - This class provides a high-level interface for controlling and monitoring + This class provides a high-level interface for controlling and monitoring Consumer Electronics Control (CEC) devices. """ @@ -58,8 +58,15 @@ def __init__(self, log: logModule, config: dict): self.cecAdaptor = config.get('adaptor') if self.controllerType.lower() == 'cec-client': self.controller = CECClientController(self.cecAdaptor, self._log) + elif self.controllerType.lower() == 'remote-cec-client': + self.controller = RemoteCECClient(self.cecAdaptor, + self._log, + address=config.get('address'), + username=config.get('username',''), + password=config.get('password',''), + port=config.get('port',22)) self._read_line = 0 - self._monitoringLog = path.join(self._log.logPath, 'cecMonitor.log') + self._monitoringLog = path.abspath(path.join(self._log.logPath, 'cecMonitor.log')) def send_message(self, message: str) -> bool: """ @@ -73,7 +80,7 @@ def send_message(self, message: str) -> bool: """ self._log.debug('Sending CEC message: [%s]' % message) return self.controller.sendMessage(message) - + def startMonitoring(self, deviceType: MonitoringType = MonitoringType.RECORDER) -> None: """ Starts monitoring CEC messages from the adaptor as the specified device type. @@ -148,16 +155,23 @@ def listDevices(self) -> list: CONFIGS = [ { 'type': 'cec-client', - 'adaptor': '/dev/ttyACM0' - }, + 'adaptor': '/dev/ttyACM0' # This is default for pulse 8 + }, + { + 'type': 'remote-cec-client', + 'adaptor': '/dev/cec0', # This is default for Raspberry Pi + 'address': '', # Needs to be be filled out with IP address + 'username': '', # Needs to be filled out with login username + 'password': '' # Needs to be filled out with login password + } ] for config in CONFIGS: - LOG.setFilename('./logs/','CECTEST%s.log' % config.get('type')) + LOG.setFilename(path.abspath('./logs/'),'CECTEST%s.log' % config.get('type')) LOG.stepStart('Testing with %s' % json.dumps(config)) CEC = HDMICECController(LOG, config) DEVICES = CEC.listDevices() LOG.info(json.dumps(DEVICES)) - # The user will need to check all the devices expected from their + # The user will need to check all the devices expected from their # cec network are shown in this output. CEC.startMonitoring() # It's is expected that a user will send a standby command on their cec diff --git a/framework/core/hdmicecModules/__init__.py b/framework/core/hdmicecModules/__init__.py index df8f28f..e3d8726 100644 --- a/framework/core/hdmicecModules/__init__.py +++ b/framework/core/hdmicecModules/__init__.py @@ -27,4 +27,5 @@ #* ****************************************************************************** from .cecClient import CECClientController +from .remoteCECClient import RemoteCECClient from .cecTypes import MonitoringType diff --git a/framework/core/hdmicecModules/abstractCECController.py b/framework/core/hdmicecModules/abstractCECController.py index 5a00a14..16509b1 100644 --- a/framework/core/hdmicecModules/abstractCECController.py +++ b/framework/core/hdmicecModules/abstractCECController.py @@ -40,6 +40,7 @@ def __init__(self, adaptor_path:str, logger:logModule): self.adaptor = adaptor_path self._log = logger self._monitoring = False + self._monitoring_log = None @property def monitoring(self) -> bool: diff --git a/framework/core/hdmicecModules/cecClient.py b/framework/core/hdmicecModules/cecClient.py index 0e2c758..ddcff07 100644 --- a/framework/core/hdmicecModules/cecClient.py +++ b/framework/core/hdmicecModules/cecClient.py @@ -59,8 +59,7 @@ def __init__(self, adaptor_path:str, logger:logModule): AttributeError: If the specified CEC adaptor is not found. """ - self._log = logger - self.adaptor = adaptor_path + super().__init__(adaptor_path=adaptor_path, logger=logger) self._log.debug('Initialising CECClientController for [%s]' % self.adaptor) if self.adaptor not in map(lambda x: x.get('com port'),self._getAdaptors()): raise AttributeError('CEC Adaptor specified not found') @@ -163,41 +162,25 @@ def _splitDeviceSectionsToDicts(self,command_output:str) -> list: def startMonitoring(self, monitoringLog: str, device_type: MonitoringType = MonitoringType.RECORDER) -> None: self._monitoring = True + self._monitoring_log = monitoringLog try: self._m_proc = subprocess.Popen(f'cec-client {self.adaptor} -m -d 0 -t {device_type.value}'.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - self._m_stdout_thread = Thread(target=self._write_monitoring_log, - args=[self._m_proc.stdout, monitoringLog], - daemon=True) - self._m_stdout_thread.start() + self._log.logStreamToFile(self._m_proc.stdout, self._monitoring_log) except Exception as e: self.stopMonitoring() raise - def _write_monitoring_log(self,streamIn: IOBase, logFilePath: str) -> None: - """ - Writes the output of the monitoring process to a log file. - - Args: - stream_in (IOBase): The input stream from the monitoring process. - logFilePath (str): File path to write the monitoring log out to. - """ - while True: - chunk = streamIn.readline() - if chunk == '': - break - with open(logFilePath, 'a+',) as out: - out.write(chunk) - def stopMonitoring(self) -> None: self._log.debug('Stopping monitoring of adaptor [%s]' % self.adaptor) if self.monitoring is False: return self._m_proc.terminate() exit_code = self._m_proc.wait() - self._m_stdout_thread.join() + self._log.stopStreamedLog(self._monitoring_log) + self._monitoring_log = None self._monitoring = False def __del__(self): diff --git a/framework/core/hdmicecModules/remoteCECClient.py b/framework/core/hdmicecModules/remoteCECClient.py new file mode 100644 index 0000000..c9b1a9a --- /dev/null +++ b/framework/core/hdmicecModules/remoteCECClient.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +#** ***************************************************************************** +# * +# * If not stated otherwise in this file or this component's LICENSE file the +# * following copyright and licenses apply: +# * +# * Copyright 2023 RDK Management +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * +# http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# * +#* ****************************************************************************** +#* +#* ** Project : RAFT +#* ** @addtogroup : core +#* ** @date : 02/10/2024 +#* ** +#* ** @brief : Abstract class for CEC controller types. +#* ** +#* ****************************************************************************** + +import re + +from framework.core.logModule import logModule +from framework.core.commandModules.sshConsole import sshConsole +from .abstractCECController import CECInterface +from .cecTypes import MonitoringType + +class RemoteCECClient(CECInterface): + + def __init__(self, adaptor: str,logger: logModule, address: str, port: int = 22, username: str = '', password: str = ''): + super().__init__(adaptor, logger) + self._console = sshConsole(self._log,address, username, password, port=port) + self._log.debug('Initialising RemoteCECClient controller') + try: + self._console.open() + except: + self._log.critical('Could not open connection to RemoteCECClient controller') + raise + if self.adaptor not in map(lambda x: x.get('com port'),self._getAdaptors()): + raise AttributeError('CEC Adaptor specified not found') + self._monitoringLog = None + + @property + def monitoring(self) -> bool: + return self._monitoring + + def _getAdaptors(self) -> list: + """ + Retrieves a list of available CEC adaptors using `cec-client`. + + Returns: + list: A list of dictionaries representing available adaptors with details like COM port. + """ + self._console.write(f'cec-client -l') + stdout = self._console.read_until('currently active source') + stdout = stdout.replace('\r\n','\n') + adaptor_count = re.search(r'Found devices: ([0-9]+)',stdout, re.M).group(1) + adaptors = self._splitDeviceSectionsToDicts(stdout) + return adaptors + + def sendMessage(self, message:str) -> bool: + """ + Send a CEC message to the CEC network. + + Args: + message (str): The CEC message to be sent. + + Returns: + bool: True if the message was sent successfully, False otherwise. + """ + return self._console.write(f'echo "{message}" | cec-client {self.adaptor}') + + def listDevices(self) -> list: + """ + List CEC devices on CEC network. + + The list returned contains dicts in the following format: + { + 'name': 'TV' + 'address': '0.0.0.0', + 'active source': True, + 'vendor': 'Unknown', + 'osd string': 'TV', + 'CEC version': '1.3a', + 'power status': 'on', + 'language': 'eng', + } + Returns: + list: A list of dictionaries representing discovered devices. + """ + self.sendMessage('scan') + output = self._console.read_until('currently active source') + devices = self._splitDeviceSectionsToDicts(output.replace('\r\n','\n')) + for device in devices: + device['name'] = device.get('osd string') + if device.get('active source') == 'yes': + device['active source'] = True + else: + device['active source'] = False + return devices + + def startMonitoring(self, monitoringLog: str, deviceType: MonitoringType=MonitoringType.RECORDER) -> None: + """ + Starts monitoring CEC messages with a specified device type. + + Args: + deviceType (MonitoringType, optional): The type of device to monitor (default: MonitoringType.RECORDER). + monitoringLog (str) : Path to write the monitoring log out + """ + self._monitoringLog = monitoringLog + self._console.write(f'cec-client -m -t{deviceType.value}') + self._console.shell.set_combine_stderr(True) + self._log.logStreamToFile(self._console.shell.makefile(), self._monitoringLog) + self._monitoring = True + + def stopMonitoring(self) -> None: + """ + Stops the CEC monitoring process. + """ + if self.monitoring is False: + return + self._console.write('\x03') + self._log.stopStreamedLog(self._monitoringLog) + + def _splitDeviceSectionsToDicts(self,command_output:str) -> list: + """ + Splits the output of a `cec-client` command into individual device sections and parses them into dictionaries. + + Args: + command_output (str): The output string from the `cec-client` command. + + Returns: + list: A list of dictionaries, each representing a single CEC device with its attributes. + """ + devices = [] + device_sections = re.findall(r'^device[ #0-9]{0,}:[\s\S]+?(?:type|language): +[\S ]+$', + command_output, + re.M) + if device_sections: + for section in device_sections: + device_dict = {} + for line in section.split('\n'): + line_split = re.search(r'^([\w #]+): +?(\S[\S ]{0,})$',line) + if line_split: + device_dict[line_split.group(1)] = line_split.group(2) + devices.append(device_dict) + return devices \ No newline at end of file diff --git a/framework/core/logModule.py b/framework/core/logModule.py index f6ab68e..bc33472 100755 --- a/framework/core/logModule.py +++ b/framework/core/logModule.py @@ -31,7 +31,9 @@ #* ****************************************************************************** import os +from io import IOBase import logging +from threading import Thread import time import datetime #from datetime import datetime @@ -110,6 +112,7 @@ def __init__(self, moduleName, level=INFO): self.path = None self.logFile = None self.csvLogFile = None + self._loggingThreads = {} def __del__(self): """Deletes the logger instance. @@ -486,4 +489,57 @@ def stepResult(self, result, message): message = "[{}]: RESULT : [{}]: {}".format(self.stepNum,resultMessage, message) self.step("=====================Step End======================",showStepNumber=False) self.stepResultMessage(message) - + + def logStreamToFile(self, inputStream: IOBase, outFileName: str) -> None: + """ + Starts a new thread to write the contents of an input stream to a file. + + Args: + inputStream (IOBase): The input stream to be read from. + outFileName (str): The path of the output file where the stream data will be written. + If only a file name is given, the file will be written in the current tests log directory. + """ + outPath = path.join(self.logPath,outFileName) + if path.isabs(outFileName): + outPath = outFileName + newThread = Thread(target=self._writeLogFile, + args=[inputStream, outPath], + daemon=True) + + self._loggingThreads.update({outFileName: newThread}) + newThread.start() + + def stopStreamedLog(self, outFileName: str) -> None: + """ + Stops a previously started thread that is writing to a log file. + + Args: + outFileName (str): The path of the output file associated with the thread to be stopped. + + Raises: + AttributeError: If the specified thread cannot be found. + """ + log_thread = self._loggingThreads.get(outFileName) + if log_thread: + log_thread.join(timeout=30) + else: + raise AttributeError(f'Could not find requested logging thread to stop. [{outFileName}]') + + def _writeLogFile(self,streamIn: IOBase, logFilePath: str) -> None: + """ + Writes the input stream to a log file. + + Args: + stream_in (IOBase): The stream from a process. + logFilePath (str): File path to write the log out to. + """ + while True: + chunk = streamIn.readline() + if chunk == '': + break + with open(logFilePath, 'a+',) as out: + out.write(chunk) + + def __del__(self): + for thread in self._loggingThreads.values(): + thread.join() \ No newline at end of file From 9e8f1dc0ff2b27c311f29da767fb416590f5061d Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:35:09 +0000 Subject: [PATCH 23/29] Fix #107: Moved the stream logging out of the logging module --- framework/core/hdmiCECController.py | 63 +++++++++++++++++++++++++++-- framework/core/logModule.py | 55 ------------------------- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py index f7ab5f1..62ac32d 100644 --- a/framework/core/hdmiCECController.py +++ b/framework/core/hdmiCECController.py @@ -30,9 +30,11 @@ #* ** #* ****************************************************************************** +from io import IOBase from os import path import sys +from threading import Thread MY_PATH = path.realpath(__file__) MY_DIR = path.dirname(MY_PATH) sys.path.append(path.join(MY_DIR,'../../')) @@ -67,6 +69,7 @@ def __init__(self, log: logModule, config: dict): port=config.get('port',22)) self._read_line = 0 self._monitoringLog = path.abspath(path.join(self._log.logPath, 'cecMonitor.log')) + self._loggingThreads = {} def send_message(self, message: str) -> bool: """ @@ -146,6 +149,60 @@ def listDevices(self) -> list: """ self._log.debug('Listing devices on CEC network') return self.controller.listDevices() + + def logStreamToFile(self, inputStream: IOBase, outFileName: str) -> None: + """ + Starts a new thread to write the contents of an input stream to a file. + + Args: + inputStream (IOBase): The input stream to be read from. + outFileName (str): The path of the output file where the stream data will be written. + If only a file name is given, the file will be written in the current tests log directory. + """ + outPath = path.join(self.logPath,outFileName) + if path.isabs(outFileName): + outPath = outFileName + newThread = Thread(target=self._writeLogFile, + args=[inputStream, outPath], + daemon=True) + + self._loggingThreads.update({outFileName: newThread}) + newThread.start() + + def stopStreamedLog(self, outFileName: str) -> None: + """ + Stops a previously started thread that is writing to a log file. + + Args: + outFileName (str): The path of the output file associated with the thread to be stopped. + + Raises: + AttributeError: If the specified thread cannot be found. + """ + log_thread = self._loggingThreads.get(outFileName) + if log_thread: + log_thread.join(timeout=30) + else: + raise AttributeError(f'Could not find requested logging thread to stop. [{outFileName}]') + + def _writeLogFile(self,streamIn: IOBase, logFilePath: str) -> None: + """ + Writes the input stream to a log file. + + Args: + stream_in (IOBase): The stream from a process. + logFilePath (str): File path to write the log out to. + """ + while True: + chunk = streamIn.readline() + if chunk == '': + break + with open(logFilePath, 'a+',) as out: + out.write(chunk) + + def __del__(self): + for thread in self._loggingThreads.values(): + thread.join() if __name__ == "__main__": @@ -160,9 +217,9 @@ def listDevices(self) -> list: { 'type': 'remote-cec-client', 'adaptor': '/dev/cec0', # This is default for Raspberry Pi - 'address': '', # Needs to be be filled out with IP address - 'username': '', # Needs to be filled out with login username - 'password': '' # Needs to be filled out with login password + 'address': '192.168.0.83', # Needs to be be filled out with IP address + 'username': 'toby', # Needs to be filled out with login username + 'password': 'test1234' # Needs to be filled out with login password } ] for config in CONFIGS: diff --git a/framework/core/logModule.py b/framework/core/logModule.py index bc33472..709d2ca 100755 --- a/framework/core/logModule.py +++ b/framework/core/logModule.py @@ -112,7 +112,6 @@ def __init__(self, moduleName, level=INFO): self.path = None self.logFile = None self.csvLogFile = None - self._loggingThreads = {} def __del__(self): """Deletes the logger instance. @@ -489,57 +488,3 @@ def stepResult(self, result, message): message = "[{}]: RESULT : [{}]: {}".format(self.stepNum,resultMessage, message) self.step("=====================Step End======================",showStepNumber=False) self.stepResultMessage(message) - - def logStreamToFile(self, inputStream: IOBase, outFileName: str) -> None: - """ - Starts a new thread to write the contents of an input stream to a file. - - Args: - inputStream (IOBase): The input stream to be read from. - outFileName (str): The path of the output file where the stream data will be written. - If only a file name is given, the file will be written in the current tests log directory. - """ - outPath = path.join(self.logPath,outFileName) - if path.isabs(outFileName): - outPath = outFileName - newThread = Thread(target=self._writeLogFile, - args=[inputStream, outPath], - daemon=True) - - self._loggingThreads.update({outFileName: newThread}) - newThread.start() - - def stopStreamedLog(self, outFileName: str) -> None: - """ - Stops a previously started thread that is writing to a log file. - - Args: - outFileName (str): The path of the output file associated with the thread to be stopped. - - Raises: - AttributeError: If the specified thread cannot be found. - """ - log_thread = self._loggingThreads.get(outFileName) - if log_thread: - log_thread.join(timeout=30) - else: - raise AttributeError(f'Could not find requested logging thread to stop. [{outFileName}]') - - def _writeLogFile(self,streamIn: IOBase, logFilePath: str) -> None: - """ - Writes the input stream to a log file. - - Args: - stream_in (IOBase): The stream from a process. - logFilePath (str): File path to write the log out to. - """ - while True: - chunk = streamIn.readline() - if chunk == '': - break - with open(logFilePath, 'a+',) as out: - out.write(chunk) - - def __del__(self): - for thread in self._loggingThreads.values(): - thread.join() \ No newline at end of file From bff15f81ac6eb5ed6f3e29d9bcebf684ee7b934e Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:41:58 +0000 Subject: [PATCH 24/29] Fix #107: Moved stream handling to it's own module. Moved stream handling to it's own module and added a receiveMessage method to hdmiCECController --- framework/core/hdmiCECController.py | 119 +++++++----------- framework/core/hdmicecModules/__init__.py | 2 +- .../hdmicecModules/abstractCECController.py | 11 +- framework/core/hdmicecModules/cecClient.py | 18 +-- framework/core/hdmicecModules/cecTypes.py | 2 +- .../core/hdmicecModules/remoteCECClient.py | 12 +- framework/core/streamToFile.py | 96 ++++++++++++++ 7 files changed, 160 insertions(+), 100 deletions(-) create mode 100644 framework/core/streamToFile.py diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py index 62ac32d..4ff15aa 100644 --- a/framework/core/hdmiCECController.py +++ b/framework/core/hdmiCECController.py @@ -29,17 +29,15 @@ #* ** cec controller type is specified. #* ** #* ****************************************************************************** - -from io import IOBase +from datetime import datetime from os import path import sys -from threading import Thread MY_PATH = path.realpath(__file__) MY_DIR = path.dirname(MY_PATH) sys.path.append(path.join(MY_DIR,'../../')) from framework.core.logModule import logModule -from framework.core.hdmicecModules import CECClientController, RemoteCECClient, MonitoringType +from framework.core.hdmicecModules import CECClientController, RemoteCECClient, CECDeviceType class HDMICECController(): """ @@ -69,9 +67,8 @@ def __init__(self, log: logModule, config: dict): port=config.get('port',22)) self._read_line = 0 self._monitoringLog = path.abspath(path.join(self._log.logPath, 'cecMonitor.log')) - self._loggingThreads = {} - def send_message(self, message: str) -> bool: + def send_message(self, message: str, deviceType: CECDeviceType=CECDeviceType.PLAYBACK) -> bool: """ Sends a CEC message to connected devices using the configured controller. @@ -82,22 +79,18 @@ def send_message(self, message: str) -> bool: bool: True if the message was sent successfully, False otherwise. """ self._log.debug('Sending CEC message: [%s]' % message) - return self.controller.sendMessage(message) - - def startMonitoring(self, deviceType: MonitoringType = MonitoringType.RECORDER) -> None: + return self.controller.sendMessage(message,deviceType) + + def startMonitoring(self) -> None: """ Starts monitoring CEC messages from the adaptor as the specified device type. - Args: - deviceType (MonitoringType, optional): The type of device to monitor (default: MonitoringType.RECORDER). - Raises: RuntimeError: If monitoring is already running. """ if self.controller.monitoring is False: self._log.debug('Starting monitoring on adaptor: [%s]' % self.cecAdaptor) - self._log.debug('Monitoring as device type [%s]' % deviceType.name) - return self.controller.startMonitoring(self._monitoringLog, deviceType) + return self.controller.startMonitoring(self._monitoringLog) else: self._log.warn('CEC monitoring is already running') @@ -122,7 +115,6 @@ def readUntil(self, message: str, retries: int = 5) -> bool: Returns: bool: True if the message was found, False otherwise. """ - self._log.debug('Starting readUntil for message as [%s] with [%s] retries' % (message,retries)) result = False retry = 0 max_retries = retries @@ -140,6 +132,38 @@ def readUntil(self, message: str, retries: int = 5) -> bool: self._read_line = read_line return result + def receiveMessage(self,messages: str|list|None=None, timeout: int=10) -> bool: + """ + This function receives messages and checks if all specified messages are found within a given + timeout period. + + Args: + messages (str|list|None): Specify the messages that expected during the monitoring process. + timeout (int): The maximum amount of time, in seconds, that the method will + wait for the messages to be received. Defaults to 10. + + Returns: + The function `receiveMessage` returns a boolean value indicating whether all the specified + messages were found within the given timeout period. + """ + end = datetime.now().timestamp() + timeout + self.startMonitoring() + found_all = False + while datetime.now().timestamp() < end: + if messages: + if isinstance(messages, str): + messages = [messages] + try: + result = self.readUntil(messages[0]) + except FileNotFoundError: + continue + if result is True: + if len(messages) > 1: + messages.pop(0) + else: + found_all = True + return found_all + def listDevices(self) -> list: """ Retrieves a list of discovered CEC devices with their OSD names (if available). @@ -149,60 +173,6 @@ def listDevices(self) -> list: """ self._log.debug('Listing devices on CEC network') return self.controller.listDevices() - - def logStreamToFile(self, inputStream: IOBase, outFileName: str) -> None: - """ - Starts a new thread to write the contents of an input stream to a file. - - Args: - inputStream (IOBase): The input stream to be read from. - outFileName (str): The path of the output file where the stream data will be written. - If only a file name is given, the file will be written in the current tests log directory. - """ - outPath = path.join(self.logPath,outFileName) - if path.isabs(outFileName): - outPath = outFileName - newThread = Thread(target=self._writeLogFile, - args=[inputStream, outPath], - daemon=True) - - self._loggingThreads.update({outFileName: newThread}) - newThread.start() - - def stopStreamedLog(self, outFileName: str) -> None: - """ - Stops a previously started thread that is writing to a log file. - - Args: - outFileName (str): The path of the output file associated with the thread to be stopped. - - Raises: - AttributeError: If the specified thread cannot be found. - """ - log_thread = self._loggingThreads.get(outFileName) - if log_thread: - log_thread.join(timeout=30) - else: - raise AttributeError(f'Could not find requested logging thread to stop. [{outFileName}]') - - def _writeLogFile(self,streamIn: IOBase, logFilePath: str) -> None: - """ - Writes the input stream to a log file. - - Args: - stream_in (IOBase): The stream from a process. - logFilePath (str): File path to write the log out to. - """ - while True: - chunk = streamIn.readline() - if chunk == '': - break - with open(logFilePath, 'a+',) as out: - out.write(chunk) - - def __del__(self): - for thread in self._loggingThreads.values(): - thread.join() if __name__ == "__main__": @@ -217,9 +187,9 @@ def __del__(self): { 'type': 'remote-cec-client', 'adaptor': '/dev/cec0', # This is default for Raspberry Pi - 'address': '192.168.0.83', # Needs to be be filled out with IP address - 'username': 'toby', # Needs to be filled out with login username - 'password': 'test1234' # Needs to be filled out with login password + 'address': '', # Needs to be be filled out with IP address + 'username': '', # Needs to be filled out with login username + 'password': '' # Needs to be filled out with login password } ] for config in CONFIGS: @@ -230,12 +200,9 @@ def __del__(self): LOG.info(json.dumps(DEVICES)) # The user will need to check all the devices expected from their # cec network are shown in this output. - CEC.startMonitoring() # It's is expected that a user will send a standby command on their cec # network during this 2 minutes. - time.sleep(120) - result = CEC.readUntil('standby') - CEC.stopMonitoring() + result = CEC.receiveMessage('standby',120) LOG.stepResult(result, 'The readUntil result is: [%s]' % result) # The user should check here the monitoring log for thier type contains # the expected information. diff --git a/framework/core/hdmicecModules/__init__.py b/framework/core/hdmicecModules/__init__.py index e3d8726..1eea55a 100644 --- a/framework/core/hdmicecModules/__init__.py +++ b/framework/core/hdmicecModules/__init__.py @@ -28,4 +28,4 @@ from .cecClient import CECClientController from .remoteCECClient import RemoteCECClient -from .cecTypes import MonitoringType +from .cecTypes import CECDeviceType diff --git a/framework/core/hdmicecModules/abstractCECController.py b/framework/core/hdmicecModules/abstractCECController.py index 16509b1..7f7b1ba 100644 --- a/framework/core/hdmicecModules/abstractCECController.py +++ b/framework/core/hdmicecModules/abstractCECController.py @@ -32,7 +32,7 @@ from abc import ABCMeta, abstractmethod from framework.core.logModule import logModule -from .cecTypes import MonitoringType +from .cecTypes import CECDeviceType class CECInterface(metaclass=ABCMeta): @@ -47,12 +47,13 @@ def monitoring(self) -> bool: return self._monitoring @abstractmethod - def sendMessage(cls, message:str) -> bool: + def sendMessage(cls, message:str, deviceType: CECDeviceType) -> bool: """ Send a CEC message to the CEC network. Args: message (str): The CEC message to be sent. + deviceType (CECDeviceType): Type of device to send the message as. Returns: bool: True if the message was sent successfully, False otherwise. @@ -81,13 +82,9 @@ def listDevices(cls) -> list: pass @abstractmethod - def startMonitoring(cls, monitoringLog: str, deviceType: MonitoringType=MonitoringType.RECORDER) -> None: + def startMonitoring(cls) -> None: """ Starts monitoring CEC messages with a specified device type. - - Args: - deviceType (MonitoringType, optional): The type of device to monitor (default: MonitoringType.RECORDER). - monitoringLog (str) : Path to write the monitoring log out """ pass diff --git a/framework/core/hdmicecModules/cecClient.py b/framework/core/hdmicecModules/cecClient.py index ddcff07..ae90ef0 100644 --- a/framework/core/hdmicecModules/cecClient.py +++ b/framework/core/hdmicecModules/cecClient.py @@ -38,7 +38,7 @@ from framework.core.logModule import logModule from .abstractCECController import CECInterface -from .cecTypes import MonitoringType +from .cecTypes import CECDeviceType class CECClientController(CECInterface): @@ -67,25 +67,25 @@ def __init__(self, adaptor_path:str, logger:logModule): self._m_proc = None self._m_stdout_thread = None - def sendMessage(self,message: str) -> bool: - exit_code, stdout = self._sendMessage(message, 0) + def sendMessage(self,message: str, deviceType: CECDeviceType = CECDeviceType.PLAYBACK) -> bool: + exit_code, stdout = self._sendMessage(message, deviceType) self._log.debug('Output of message sent: [%s]' % stdout) if exit_code != 0: return False return True - def _sendMessage(self, message: str, debug: int = 1) -> tuple: + def _sendMessage(self, message: str, deviceType: CECDeviceType) -> tuple: """ Internal method for sending a CEC message using `subprocess`. Args: message (str): The CEC message to be sent. - debug (int, optional): Debug level for `cec-client` (default: 1). + deviceType (CECDeviceType): Type of device to send the message as. Returns: tuple: A tuple containing the exit code of the subprocess call and the standard output. """ - result = subprocess.run(f'echo "{message}" | cec-client {self.adaptor} -s -d {debug}', + result = subprocess.run(f'echo "{message}" | cec-client {self.adaptor} -s -d 1 -t {deviceType.value}', shell=True, check=True, stdout=subprocess.PIPE, @@ -121,7 +121,7 @@ def _scanCECNetwork(self) -> list: Returns: list: A list of dictionaries representing discovered devices with details. """ - _, result = self._sendMessage('scan') + _, result = self._sendMessage('scan', CECDeviceType.PLAYBACK) self._log.debug('Output of scan on CEC Network: [%s]' % result) devicesOnNetwork = self._splitDeviceSectionsToDicts(result) return devicesOnNetwork @@ -160,11 +160,11 @@ def _splitDeviceSectionsToDicts(self,command_output:str) -> list: devices.append(device_dict) return devices - def startMonitoring(self, monitoringLog: str, device_type: MonitoringType = MonitoringType.RECORDER) -> None: + def startMonitoring(self, monitoringLog: str) -> None: self._monitoring = True self._monitoring_log = monitoringLog try: - self._m_proc = subprocess.Popen(f'cec-client {self.adaptor} -m -d 0 -t {device_type.value}'.split(), + self._m_proc = subprocess.Popen(f'cec-client {self.adaptor} -m -d 0'.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) diff --git a/framework/core/hdmicecModules/cecTypes.py b/framework/core/hdmicecModules/cecTypes.py index 62e81f4..9e3568e 100644 --- a/framework/core/hdmicecModules/cecTypes.py +++ b/framework/core/hdmicecModules/cecTypes.py @@ -31,7 +31,7 @@ from enum import Enum -class MonitoringType(Enum): +class CECDeviceType(Enum): PLAYBACK = "p" RECORDER = "r" TUNER = "t" diff --git a/framework/core/hdmicecModules/remoteCECClient.py b/framework/core/hdmicecModules/remoteCECClient.py index c9b1a9a..12522d0 100644 --- a/framework/core/hdmicecModules/remoteCECClient.py +++ b/framework/core/hdmicecModules/remoteCECClient.py @@ -34,7 +34,7 @@ from framework.core.logModule import logModule from framework.core.commandModules.sshConsole import sshConsole from .abstractCECController import CECInterface -from .cecTypes import MonitoringType +from .cecTypes import CECDeviceType class RemoteCECClient(CECInterface): @@ -69,17 +69,18 @@ def _getAdaptors(self) -> list: adaptors = self._splitDeviceSectionsToDicts(stdout) return adaptors - def sendMessage(self, message:str) -> bool: + def sendMessage(self, message:str, deviceType: CECDeviceType = CECDeviceType.PLAYBACK) -> bool: """ Send a CEC message to the CEC network. Args: message (str): The CEC message to be sent. + deviceType (CECDeviceType): Type of device to send the message as. Returns: bool: True if the message was sent successfully, False otherwise. """ - return self._console.write(f'echo "{message}" | cec-client {self.adaptor}') + return self._console.write(f'echo "{message}" | cec-client {self.adaptor} -s -d 1 -t {deviceType.value}') def listDevices(self) -> list: """ @@ -110,16 +111,15 @@ def listDevices(self) -> list: device['active source'] = False return devices - def startMonitoring(self, monitoringLog: str, deviceType: MonitoringType=MonitoringType.RECORDER) -> None: + def startMonitoring(self, monitoringLog: str) -> None: """ Starts monitoring CEC messages with a specified device type. Args: - deviceType (MonitoringType, optional): The type of device to monitor (default: MonitoringType.RECORDER). monitoringLog (str) : Path to write the monitoring log out """ self._monitoringLog = monitoringLog - self._console.write(f'cec-client -m -t{deviceType.value}') + self._console.write(f'cec-client -m -d 0') self._console.shell.set_combine_stderr(True) self._log.logStreamToFile(self._console.shell.makefile(), self._monitoringLog) self._monitoring = True diff --git a/framework/core/streamToFile.py b/framework/core/streamToFile.py new file mode 100644 index 0000000..1934572 --- /dev/null +++ b/framework/core/streamToFile.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +from io import IOBase +from threading import Thread +from os import path + + +class StreamToFile(): + + def __init__(self): + self._file_handles = {} + + def writeStreamToFile(self, inputStream: IOBase, outFileName: str) -> None: + """ + Starts a new thread to write the contents of an input stream to a file. + + Args: + inputStream (IOBase): The input stream to be read from. + outFileName (str): The path of the output file where the stream data will be written. + If only a file name is given, the file will be written in the current tests log directory. + """ + outFileHandle = open(outFileName, 'a+', encoding='utf-8') + newThread = Thread(target=self._writeLogFile, + args=[inputStream, outFileHandle], + daemon=True) + + self._file_handles.update({outFileName: newThread}) + newThread.start() + + def stopStreamedLog(self, outFileName: str) -> None: + """ + Stops a previously started thread that is writing to a log file. + + Args: + outFileName (str): The path of the output file associated with the thread to be stopped. + + Raises: + AttributeError: If the specified thread cannot be found. + """ + log_thread = self._loggingThreads.get(outFileName) + if log_thread: + log_thread.join(timeout=30) + else: + raise AttributeError(f'Could not find requested logging thread to stop. [{outFileName}]') + + def _writeLogFile(self,streamIn: IOBase, ioOut: IOBase) -> None: + """ + Writes the input stream to a log file. + + Args: + stream_in (IOBase): The stream from a process. + logFilePath (str): File path to write the log out to. + """ + while True: + chunk = streamIn.readline() + if chunk == '': + break + ioOut.write(chunk) + + def readUntil(self, fileName:str, searchString:str, retries: int = 5) -> None: + """ + Reads the monitoring log until the specified message is found. + + Opens the monitoring log file and checks for the message within a specified retry limit. + + Args: + message (str): The message to search for in the monitoring log. + retries (int, optional): The maximum number of retries before giving up (default: 5). + + Returns: + bool: True if the message was found, False otherwise. + """ + out_file_dict = self._file_handles.get(fileName, None) + if out_file_dict is None: + raise FileNotFoundError(fileName) + out_file_handle = out_file_dict.get('handle') + result = False + retry = 0 + max_retries = retries + while retry != max_retries and not result: + read_line = out_file_dict.get('read_line') + out_file_handle.seek(read_line) + out_lines = out_file_handle.readlines() + write_line = len(out_lines) + while read_line != write_line: + if searchString in out_lines[read_line]: + result = True + break + read_line+=1 + retry += 1 + out_file_dict['read_line'] = read_line + return result + + def __del__(self): + for handle in self._file_handles.values(): + handle.close() \ No newline at end of file From f25a9663e145068174b10c7be5e5fb8c89f95ff0 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Wed, 18 Dec 2024 19:05:39 +0000 Subject: [PATCH 25/29] Update #103 - Updated CEC controller interface to make it easier to use --- framework/core/hdmiCECController.py | 167 ++++++++---------- .../hdmicecModules/abstractCECController.py | 92 ++++++---- framework/core/hdmicecModules/cecClient.py | 107 +++++------ .../core/hdmicecModules/remoteCECClient.py | 99 ++++------- framework/core/logModule.py | 2 + framework/core/streamToFile.py | 59 +++---- 6 files changed, 247 insertions(+), 279 deletions(-) diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py index 4ff15aa..b05163f 100644 --- a/framework/core/hdmiCECController.py +++ b/framework/core/hdmiCECController.py @@ -37,6 +37,7 @@ MY_DIR = path.dirname(MY_PATH) sys.path.append(path.join(MY_DIR,'../../')) from framework.core.logModule import logModule +from framework.core.streamToFile import StreamToFile from framework.core.hdmicecModules import CECClientController, RemoteCECClient, CECDeviceType class HDMICECController(): @@ -56,140 +57,108 @@ def __init__(self, log: logModule, config: dict): self._log = log self.controllerType = config.get('type') self.cecAdaptor = config.get('adaptor') + self._streamFile = path.join(self._log.logPath, f'{self.controllerType.lower()}_{str(datetime.now().timestamp())}') + self._stream = StreamToFile(self._streamFile) if self.controllerType.lower() == 'cec-client': - self.controller = CECClientController(self.cecAdaptor, self._log) + self.controller = CECClientController(self.cecAdaptor, + self._log, + self._stream) elif self.controllerType.lower() == 'remote-cec-client': self.controller = RemoteCECClient(self.cecAdaptor, self._log, - address=config.get('address'), + self._stream, + config.get('address'), username=config.get('username',''), password=config.get('password',''), - port=config.get('port',22)) + port=config.get('port',22), + prompt=config.get('prompt', ':~')) self._read_line = 0 - self._monitoringLog = path.abspath(path.join(self._log.logPath, 'cecMonitor.log')) - def send_message(self, message: str, deviceType: CECDeviceType=CECDeviceType.PLAYBACK) -> bool: + def sendMessage(self, sourceAddress: str, destAddress: str, opCode: str, payload: list = None) -> None: """ - Sends a CEC message to connected devices using the configured controller. - - Args: - message (str): The CEC message to be sent. - - Returns: - bool: True if the message was sent successfully, False otherwise. - """ - self._log.debug('Sending CEC message: [%s]' % message) - return self.controller.sendMessage(message,deviceType) + Sends an opCode from a specified source and to a specified destination. - def startMonitoring(self) -> None: - """ - Starts monitoring CEC messages from the adaptor as the specified device type. - - Raises: - RuntimeError: If monitoring is already running. - """ - if self.controller.monitoring is False: - self._log.debug('Starting monitoring on adaptor: [%s]' % self.cecAdaptor) - return self.controller.startMonitoring(self._monitoringLog) - else: - self._log.warn('CEC monitoring is already running') - - def stopMonitoring(self): - """ - Stops the CEC monitoring process. - - Delegates the stop task to the underlying `CECClientController`. - """ - return self.controller.stopMonitoring() - - def readUntil(self, message: str, retries: int = 5) -> bool: - """ - Reads the monitoring log until the specified message is found. - - Opens the monitoring log file and checks for the message within a specified retry limit. - Args: - message (str): The message to search for in the monitoring log. - retries (int, optional): The maximum number of retries before giving up (default: 5). - - Returns: - bool: True if the message was found, False otherwise. + sourceAddress (str): The logical address of the source device (0-9 or A-F). + destAddress (str): The logical address of the destination device (0-9 or A-F). + opCode (str): Operation code to send as an hexidecimal string e.g 0x81. + payload (list): List of hexidecimal strings to be sent with the opCode. Optional. """ - result = False - retry = 0 - max_retries = retries - while retry != max_retries and not result: - with open(self._monitoringLog, 'r') as logFile: - logLines = logFile.readlines() - read_line = self._read_line - write_line = len(logLines) - while read_line != write_line: - if message in logLines[read_line]: - result = True - break - read_line+=1 - retry += 1 - self._read_line = read_line - return result - - def receiveMessage(self,messages: str|list|None=None, timeout: int=10) -> bool: + payload_string = '' + if isinstance(payload, list): + payload_string = ' '.join(payload) + self._log.debug('Sending CEC message: Source=[%s] Dest=[%s] opCode=[%s] payload=[%s]' % + (sourceAddress, destAddress, opCode, payload_string)) + self.controller.sendMessage(sourceAddress, destAddress, opCode, payload=payload) + + def receiveMessage(self, sourceAddress: str, destAddress: str, opCode: str, timeout: int = 10, payload: list = None) -> bool: """ - This function receives messages and checks if all specified messages are found within a given - timeout period. + This function checks to see if a specified opCode has been received. Args: - messages (str|list|None): Specify the messages that expected during the monitoring process. - timeout (int): The maximum amount of time, in seconds, that the method will - wait for the messages to be received. Defaults to 10. + sourceAddress (str): The logical address of the source device (0-9 or A-F). + destAddress (str): The logical address of the destination device (0-9 or A-F). + opCode (str): Operation code to send as an hexidecimal string e.g 0x81. + timeout (int): The maximum amount of time, in seconds, that the method will + wait for the message to be received. Defaults to 10. + payload (list): List of hexidecimal strings to be sent with the opCode. Optional. Returns: - The function `receiveMessage` returns a boolean value indicating whether all the specified - messages were found within the given timeout period. + boolean: True if message is received. False otherwise. """ - end = datetime.now().timestamp() + timeout - self.startMonitoring() - found_all = False - while datetime.now().timestamp() < end: - if messages: - if isinstance(messages, str): - messages = [messages] - try: - result = self.readUntil(messages[0]) - except FileNotFoundError: - continue - if result is True: - if len(messages) > 1: - messages.pop(0) - else: - found_all = True - return found_all + payload_string = '' + if isinstance(payload, list): + payload_string = ' '.join(payload) + self._log.debug('Expecting CEC message: Source=[%s] Dest=[%s] opCode=[%s] payload=[%s]' % + (sourceAddress, destAddress, opCode, payload_string)) + return self.controller.receiveMessage(sourceAddress, destAddress, opCode, timeout=timeout, payload=payload) def listDevices(self) -> list: """ - Retrieves a list of discovered CEC devices with their OSD names (if available). - + List CEC devices on CEC network. + + The list returned contains dicts in the following format: + {'active source': False, + 'vendor': 'Unknown', + 'osd string': 'TV', + 'CEC version': '1.3a', + 'power status': 'on', + 'language': 'eng', + 'physical address': '0.0.0.0', + 'name': 'TV', + 'logical address': '0'} Returns: list: A list of dictionaries representing discovered devices. """ self._log.debug('Listing devices on CEC network') return self.controller.listDevices() + def start(self): + """Start the CECContoller. + """ + self.controller.start() + + def stop(self): + """Stop the CECController. + """ + self.controller.stop() if __name__ == "__main__": import time import json LOG = logModule('CECTEST', logModule.DEBUG) CONFIGS = [ - { - 'type': 'cec-client', - 'adaptor': '/dev/ttyACM0' # This is default for pulse 8 - }, + # { + # 'type': 'cec-client', + # 'adaptor': '/dev/ttyACM0' # This is default for pulse 8 + # }, { 'type': 'remote-cec-client', 'adaptor': '/dev/cec0', # This is default for Raspberry Pi 'address': '', # Needs to be be filled out with IP address 'username': '', # Needs to be filled out with login username - 'password': '' # Needs to be filled out with login password + 'password': '', # Needs to be filled out with login password + 'prompt' : '' } ] for config in CONFIGS: @@ -198,11 +167,13 @@ def listDevices(self) -> list: CEC = HDMICECController(LOG, config) DEVICES = CEC.listDevices() LOG.info(json.dumps(DEVICES)) - # The user will need to check all the devices expected from their + CEC.sendMessage('0', '2', '0x8f', ['0x21','0x85']) + # The user will need to check all the devices expected from their # cec network are shown in this output. # It's is expected that a user will send a standby command on their cec # network during this 2 minutes. - result = CEC.receiveMessage('standby',120) + result = CEC.receiveMessage('2', '0', '0x8f') LOG.stepResult(result, 'The readUntil result is: [%s]' % result) + CEC.stop() # The user should check here the monitoring log for thier type contains # the expected information. diff --git a/framework/core/hdmicecModules/abstractCECController.py b/framework/core/hdmicecModules/abstractCECController.py index 7f7b1ba..0d2f4d7 100644 --- a/framework/core/hdmicecModules/abstractCECController.py +++ b/framework/core/hdmicecModules/abstractCECController.py @@ -30,33 +30,31 @@ #* ****************************************************************************** from abc import ABCMeta, abstractmethod +from datetime import datetime +import os from framework.core.logModule import logModule +from framework.core.streamToFile import StreamToFile from .cecTypes import CECDeviceType class CECInterface(metaclass=ABCMeta): - def __init__(self, adaptor_path:str, logger:logModule): + def __init__(self, adaptor_path:str, logger:logModule, streamLogger: StreamToFile): self.adaptor = adaptor_path self._log = logger - self._monitoring = False - self._monitoring_log = None - - @property - def monitoring(self) -> bool: - return self._monitoring + self._proc = None + self._stream = streamLogger @abstractmethod - def sendMessage(cls, message:str, deviceType: CECDeviceType) -> bool: + def sendMessage(cls, sourceAddress: str, destAddress: str, opCode: str, payload: list = None, deviceType: CECDeviceType=None) -> None: """ - Send a CEC message to the CEC network. - + Sends an opCode from a specified source and to a specified destination. + Args: - message (str): The CEC message to be sent. - deviceType (CECDeviceType): Type of device to send the message as. - - Returns: - bool: True if the message was sent successfully, False otherwise. + sourceAddress (str): The logical address of the source device (0-9 or A-F). + destAddress (str): The logical address of the destination device (0-9 or A-F). + opCode (str): Operation code to send as an hexidecimal string e.g 0x81. + payload (list): List of hexidecimal strings to be sent with the opCode. Optional. """ pass @@ -66,31 +64,65 @@ def listDevices(cls) -> list: List CEC devices on CEC network. The list returned contains dicts in the following format: - { - 'name': 'TV' - 'address': '0.0.0.0', - 'active source': True, - 'vendor': 'Unknown', - 'osd string': 'TV', - 'CEC version': '1.3a', - 'power status': 'on', - 'language': 'eng', - } + {'active source': False, + 'vendor': 'Unknown', + 'osd string': 'TV', + 'CEC version': '1.3a', + 'power status': 'on', + 'language': 'eng', + 'physical address': '0.0.0.0', + 'name': 'TV', + 'logical address': '0'} Returns: list: A list of dictionaries representing discovered devices. """ pass @abstractmethod - def startMonitoring(cls) -> None: - """ - Starts monitoring CEC messages with a specified device type. + def start(cls): + """Start the CECContoller. """ pass @abstractmethod - def stopMonitoring(cls) -> None: + def stop(cls): + """Stop the CECController. """ - Stops the CEC monitoring process. + pass + + def formatMessage(cls, sourceAddress: str, destAddress: str, opCode:str, payload: list = None) -> str: + """Format the input information into the required message string + for the CECController. + + Args: + sourceAddress (str): The logical address of the source device (0-9 or A-F). + destAddress (str): The logical address of the destination device (0-9 or A-F). + opCode (str): Operation code to send as an hexidecimal string e.g 0x81. + payload (list): List of hexidecimal strings to be sent with the opCode. Optional + + Returns: + str: Formatted message for CECController. """ pass + + def receiveMessage(self,sourceAddress: str, destAddress: str, opCode: str, timeout: int = 10, payload: list = None) -> bool: + """ + This function checks to see if a specified opCode has been received. + + Args: + sourceAddress (str): The logical address of the source device (0-9 or A-F). + destAddress (str): The logical address of the destination device (0-9 or A-F). + opCode (str): Operation code to send as an hexidecimal string e.g 0x81. + timeout (int): The maximum amount of time, in seconds, that the method will + wait for the message to be received. Defaults to 10. + payload (list): List of hexidecimal strings to be sent with the opCode. Optional. + + Returns: + boolean: True if message is received. False otherwise. + """ + end = datetime.now().timestamp() + timeout + result = False + while datetime.now().timestamp() < end and result is False: + message = self.formatMessage(sourceAddress, destAddress, opCode, payload) + result = self._stream.readUntil(message) + return result diff --git a/framework/core/hdmicecModules/cecClient.py b/framework/core/hdmicecModules/cecClient.py index ae90ef0..fb8d8df 100644 --- a/framework/core/hdmicecModules/cecClient.py +++ b/framework/core/hdmicecModules/cecClient.py @@ -31,12 +31,14 @@ #* ** #* ****************************************************************************** +from datetime import datetime from io import IOBase import re import subprocess from threading import Thread from framework.core.logModule import logModule +from framework.core.streamToFile import StreamToFile from .abstractCECController import CECInterface from .cecTypes import CECDeviceType @@ -47,7 +49,7 @@ class CECClientController(CECInterface): devices through the `cec-client` command-line tool. """ - def __init__(self, adaptor_path:str, logger:logModule): + def __init__(self, adaptor_path:str, logger:logModule, streamLogger: StreamToFile): """ Initializes the CECClientController instance. @@ -59,41 +61,33 @@ def __init__(self, adaptor_path:str, logger:logModule): AttributeError: If the specified CEC adaptor is not found. """ - super().__init__(adaptor_path=adaptor_path, logger=logger) + super().__init__(adaptor_path=adaptor_path, logger=logger, streamLogger=streamLogger) self._log.debug('Initialising CECClientController for [%s]' % self.adaptor) if self.adaptor not in map(lambda x: x.get('com port'),self._getAdaptors()): raise AttributeError('CEC Adaptor specified not found') - self._monitoring = False - self._m_proc = None - self._m_stdout_thread = None - - def sendMessage(self,message: str, deviceType: CECDeviceType = CECDeviceType.PLAYBACK) -> bool: - exit_code, stdout = self._sendMessage(message, deviceType) - self._log.debug('Output of message sent: [%s]' % stdout) - if exit_code != 0: - return False - return True - - def _sendMessage(self, message: str, deviceType: CECDeviceType) -> tuple: - """ - Internal method for sending a CEC message using `subprocess`. - - Args: - message (str): The CEC message to be sent. - deviceType (CECDeviceType): Type of device to send the message as. + self.start() - Returns: - tuple: A tuple containing the exit code of the subprocess call and the standard output. - """ - result = subprocess.run(f'echo "{message}" | cec-client {self.adaptor} -s -d 1 -t {deviceType.value}', - shell=True, - check=True, + def start(self): + self._console = subprocess.Popen(f'cec-client {self.adaptor} -d 0'.split(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout = result.stdout.decode('utf-8') - stderr = result.stderr.decode('utf-8') - exit_code = result.returncode - return exit_code, stdout + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE, + text=True) + self._stream.writeStreamToFile(self._console.stdout) + + def stop(self): + self._console.stdin.write('q\n') + self._console.stdin.flush() + try: + self._console.wait() + except subprocess.CalledProcessError: + self._console.terminate() + self._stream.stopStreamedLog() + + def sendMessage(self, sourceAddress: str, destAddress: str, opCode: str, payload: list = None) -> None: + message = self.formatMessage(sourceAddress, destAddress, opCode, payload=payload) + self._console.stdin.write(f'tx {message}\n') + self._console.stdin.flush() def _getAdaptors(self) -> list: """ @@ -121,15 +115,28 @@ def _scanCECNetwork(self) -> list: Returns: list: A list of dictionaries representing discovered devices with details. """ - _, result = self._sendMessage('scan', CECDeviceType.PLAYBACK) - self._log.debug('Output of scan on CEC Network: [%s]' % result) - devicesOnNetwork = self._splitDeviceSectionsToDicts(result) + self.stop() + result = subprocess.run(f'echo "scan" | cec-client {self.adaptor} -s -d 1', + shell=True, + check=True, + stdout=subprocess.PIPE) + self.start() + stdout = result.stdout.decode('utf-8') + self._log.debug('Output of scan on CEC Network: [%s]' % stdout) + devicesOnNetwork = self._splitDeviceSectionsToDicts(stdout) return devicesOnNetwork def listDevices(self) -> list: devices = self._scanCECNetwork() for device_dict in devices: + # Remove the 'address' from the dict and change it to 'physical address' + device_dict['physical address'] = device_dict.pop('address') device_dict['name'] = device_dict.get('osd string') + for key in device_dict.keys(): + if 'device' in key.lower(): + device_dict['logical address'] = key.rsplit('#')[-1] + device_dict.pop(key) + break if device_dict.get('active source') == 'yes': device_dict['active source'] = True else: @@ -147,7 +154,7 @@ def _splitDeviceSectionsToDicts(self,command_output:str) -> list: list: A list of dictionaries, each representing a single CEC device with its attributes. """ devices = [] - device_sections = re.findall(r'^device[ #0-9]{0,}:[\s\S]+?(?:type|language): +[\S ]+$', + device_sections = re.findall(r'^device[ #0-9A-F]{0,}:[\s\S]+?(?:type|language): +[\S ]+$', command_output, re.M) if device_sections: @@ -160,31 +167,15 @@ def _splitDeviceSectionsToDicts(self,command_output:str) -> list: devices.append(device_dict) return devices - def startMonitoring(self, monitoringLog: str) -> None: - self._monitoring = True - self._monitoring_log = monitoringLog - try: - self._m_proc = subprocess.Popen(f'cec-client {self.adaptor} -m -d 0'.split(), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True) - self._log.logStreamToFile(self._m_proc.stdout, self._monitoring_log) - except Exception as e: - self.stopMonitoring() - raise - - def stopMonitoring(self) -> None: - self._log.debug('Stopping monitoring of adaptor [%s]' % self.adaptor) - if self.monitoring is False: - return - self._m_proc.terminate() - exit_code = self._m_proc.wait() - self._log.stopStreamedLog(self._monitoring_log) - self._monitoring_log = None - self._monitoring = False + def formatMessage(self, sourceAddress: str, destAddress: str, opCode:str, payload: list = None) -> str: + message_string = f'{sourceAddress}{destAddress}:{opCode[2:]}' + if payload: + payload_string = ':'.join(map(lambda x: x[2:], payload)) + message_string += ':' + payload_string + return message_string def __del__(self): """ Destructor for the class, ensures monitoring is stopped. """ - self.stopMonitoring() + self.stop() diff --git a/framework/core/hdmicecModules/remoteCECClient.py b/framework/core/hdmicecModules/remoteCECClient.py index 12522d0..7bdad65 100644 --- a/framework/core/hdmicecModules/remoteCECClient.py +++ b/framework/core/hdmicecModules/remoteCECClient.py @@ -32,15 +32,16 @@ import re from framework.core.logModule import logModule +from framework.core.streamToFile import StreamToFile from framework.core.commandModules.sshConsole import sshConsole from .abstractCECController import CECInterface from .cecTypes import CECDeviceType class RemoteCECClient(CECInterface): - def __init__(self, adaptor: str,logger: logModule, address: str, port: int = 22, username: str = '', password: str = ''): - super().__init__(adaptor, logger) - self._console = sshConsole(self._log,address, username, password, port=port) + def __init__(self, adaptor: str,logger: logModule, streamLogger: StreamToFile, address: str, port: int = 22, username: str = '', password: str = '', prompt = ':~'): + super().__init__(adaptor, logger, streamLogger) + self._console = sshConsole(self._log,address, username, password, port=port, prompt=prompt) self._log.debug('Initialising RemoteCECClient controller') try: self._console.open() @@ -49,12 +50,16 @@ def __init__(self, adaptor: str,logger: logModule, address: str, port: int = 22, raise if self.adaptor not in map(lambda x: x.get('com port'),self._getAdaptors()): raise AttributeError('CEC Adaptor specified not found') - self._monitoringLog = None + self.start() + + def start(self): + self._console.write(f'cec-client {self.adaptor} -d 0') + self._stream.writeStreamToFile(self._console.shell.makefile()) + + def stop(self): + self._console.write('q') + self._stream.stopStreamedLog() - @property - def monitoring(self) -> bool: - return self._monitoring - def _getAdaptors(self) -> list: """ Retrieves a list of available CEC adaptors using `cec-client`. @@ -63,76 +68,37 @@ def _getAdaptors(self) -> list: list: A list of dictionaries representing available adaptors with details like COM port. """ self._console.write(f'cec-client -l') - stdout = self._console.read_until('currently active source') + stdout = self._console.read_until(self._console.prompt) stdout = stdout.replace('\r\n','\n') adaptor_count = re.search(r'Found devices: ([0-9]+)',stdout, re.M).group(1) adaptors = self._splitDeviceSectionsToDicts(stdout) return adaptors - def sendMessage(self, message:str, deviceType: CECDeviceType = CECDeviceType.PLAYBACK) -> bool: - """ - Send a CEC message to the CEC network. - - Args: - message (str): The CEC message to be sent. - deviceType (CECDeviceType): Type of device to send the message as. - - Returns: - bool: True if the message was sent successfully, False otherwise. - """ - return self._console.write(f'echo "{message}" | cec-client {self.adaptor} -s -d 1 -t {deviceType.value}') + def sendMessage(self, sourceAddress: str, destAddress: str, opCode: str, payload: list = None) -> None: + message = self.formatMessage(sourceAddress, destAddress, opCode, payload=payload) + self._console.write(f'tx {message}') def listDevices(self) -> list: - """ - List CEC devices on CEC network. - - The list returned contains dicts in the following format: - { - 'name': 'TV' - 'address': '0.0.0.0', - 'active source': True, - 'vendor': 'Unknown', - 'osd string': 'TV', - 'CEC version': '1.3a', - 'power status': 'on', - 'language': 'eng', - } - Returns: - list: A list of dictionaries representing discovered devices. - """ - self.sendMessage('scan') + self.stop() + self._console.write(f'echo "scan" | cec-client -s {self.adaptor} -d 1 > cec-test.txt') + self._console.waitForPrompt() output = self._console.read_until('currently active source') + self.start() devices = self._splitDeviceSectionsToDicts(output.replace('\r\n','\n')) for device in devices: + device['physical address'] = device.pop('address') device['name'] = device.get('osd string') + for key in device.keys(): + if 'device' in key.lower(): + device['logical address'] = key.rsplit('#')[-1] + device.pop(key) + break if device.get('active source') == 'yes': device['active source'] = True else: device['active source'] = False return devices - def startMonitoring(self, monitoringLog: str) -> None: - """ - Starts monitoring CEC messages with a specified device type. - - Args: - monitoringLog (str) : Path to write the monitoring log out - """ - self._monitoringLog = monitoringLog - self._console.write(f'cec-client -m -d 0') - self._console.shell.set_combine_stderr(True) - self._log.logStreamToFile(self._console.shell.makefile(), self._monitoringLog) - self._monitoring = True - - def stopMonitoring(self) -> None: - """ - Stops the CEC monitoring process. - """ - if self.monitoring is False: - return - self._console.write('\x03') - self._log.stopStreamedLog(self._monitoringLog) - def _splitDeviceSectionsToDicts(self,command_output:str) -> list: """ Splits the output of a `cec-client` command into individual device sections and parses them into dictionaries. @@ -144,7 +110,7 @@ def _splitDeviceSectionsToDicts(self,command_output:str) -> list: list: A list of dictionaries, each representing a single CEC device with its attributes. """ devices = [] - device_sections = re.findall(r'^device[ #0-9]{0,}:[\s\S]+?(?:type|language): +[\S ]+$', + device_sections = re.findall(r'^device[ #0-9A-F]{0,}:[\s\S]+?(?:type|language): +[\S ]+$', command_output, re.M) if device_sections: @@ -155,4 +121,11 @@ def _splitDeviceSectionsToDicts(self,command_output:str) -> list: if line_split: device_dict[line_split.group(1)] = line_split.group(2) devices.append(device_dict) - return devices \ No newline at end of file + return devices + + def formatMessage(self, sourceAddress, destAddress, opCode, payload = None): + message_string = f'{sourceAddress}{destAddress}:{opCode[2:]}' + if payload: + payload_string = ':'.join(map(lambda x: x[2:], payload)) + message_string += ':' + payload_string + return message_string \ No newline at end of file diff --git a/framework/core/logModule.py b/framework/core/logModule.py index 709d2ca..d1e76ca 100755 --- a/framework/core/logModule.py +++ b/framework/core/logModule.py @@ -112,6 +112,8 @@ def __init__(self, moduleName, level=INFO): self.path = None self.logFile = None self.csvLogFile = None + self.failedSteps = {} + def __del__(self): """Deletes the logger instance. diff --git a/framework/core/streamToFile.py b/framework/core/streamToFile.py index 1934572..7cafdac 100644 --- a/framework/core/streamToFile.py +++ b/framework/core/streamToFile.py @@ -7,10 +7,13 @@ class StreamToFile(): - def __init__(self): - self._file_handles = {} + def __init__(self, outputPath): + self._filePath = outputPath + self._fileHandle = None + self._activeThread = None + self._readLine = 0 - def writeStreamToFile(self, inputStream: IOBase, outFileName: str) -> None: + def writeStreamToFile(self, inputStream: IOBase) -> None: """ Starts a new thread to write the contents of an input stream to a file. @@ -19,15 +22,15 @@ def writeStreamToFile(self, inputStream: IOBase, outFileName: str) -> None: outFileName (str): The path of the output file where the stream data will be written. If only a file name is given, the file will be written in the current tests log directory. """ - outFileHandle = open(outFileName, 'a+', encoding='utf-8') + self._fileHandle = open(self._filePath, 'a+', encoding='utf-8') newThread = Thread(target=self._writeLogFile, - args=[inputStream, outFileHandle], + args=[inputStream, self._fileHandle], daemon=True) - self._file_handles.update({outFileName: newThread}) + self._activeThread = newThread newThread.start() - def stopStreamedLog(self, outFileName: str) -> None: + def stopStreamedLog(self) -> None: """ Stops a previously started thread that is writing to a log file. @@ -37,11 +40,10 @@ def stopStreamedLog(self, outFileName: str) -> None: Raises: AttributeError: If the specified thread cannot be found. """ - log_thread = self._loggingThreads.get(outFileName) - if log_thread: - log_thread.join(timeout=30) + if self._activeThread: + self._activeThread.join(timeout=30) else: - raise AttributeError(f'Could not find requested logging thread to stop. [{outFileName}]') + raise AttributeError(f'Could not find requested logging thread to stop. [{self._filePath}]') def _writeLogFile(self,streamIn: IOBase, ioOut: IOBase) -> None: """ @@ -57,30 +59,26 @@ def _writeLogFile(self,streamIn: IOBase, ioOut: IOBase) -> None: break ioOut.write(chunk) - def readUntil(self, fileName:str, searchString:str, retries: int = 5) -> None: + def readUntil(self, searchString:str, retries: int = 5) -> None: """ - Reads the monitoring log until the specified message is found. - - Opens the monitoring log file and checks for the message within a specified retry limit. - + Read lines from a file until a specific search string is found, with a specified + number of retries. + Args: - message (str): The message to search for in the monitoring log. - retries (int, optional): The maximum number of retries before giving up (default: 5). - + searchString (str): The string that will be search for. + retries (int): The maximum number of times the method will attempt to find the `searchString`. + Defaults to 5 + Returns: - bool: True if the message was found, False otherwise. + boolean : True when the `searchString` is found. False otherwise. """ - out_file_dict = self._file_handles.get(fileName, None) - if out_file_dict is None: - raise FileNotFoundError(fileName) - out_file_handle = out_file_dict.get('handle') result = False retry = 0 max_retries = retries while retry != max_retries and not result: - read_line = out_file_dict.get('read_line') - out_file_handle.seek(read_line) - out_lines = out_file_handle.readlines() + read_line = self._readLine + self._fileHandle.seek(read_line) + out_lines = self._fileHandle.readlines() write_line = len(out_lines) while read_line != write_line: if searchString in out_lines[read_line]: @@ -88,9 +86,10 @@ def readUntil(self, fileName:str, searchString:str, retries: int = 5) -> None: break read_line+=1 retry += 1 - out_file_dict['read_line'] = read_line + self._readLine = read_line return result def __del__(self): - for handle in self._file_handles.values(): - handle.close() \ No newline at end of file + self.stopStreamedLog() + if self._fileHandle: + self._fileHandle.close() \ No newline at end of file From a0b2b71d801b0bad1806b45dbc2e07b1db1c8298 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:06:31 +0000 Subject: [PATCH 26/29] Fix #103 - Corrected hdmiCECContoller on device manager and added stop to testControl --- framework/core/deviceManager.py | 2 ++ framework/core/testControl.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/framework/core/deviceManager.py b/framework/core/deviceManager.py index 51820e7..6a78269 100644 --- a/framework/core/deviceManager.py +++ b/framework/core/deviceManager.py @@ -150,11 +150,13 @@ def __init__(self, log:logModule, logPath:str, devices:dict): # # Telnet # # outbound # # remoteController + # # hdmiCECController self.log = log self.consoles = dict() self.powerControl = None self.outBoundClient = None self.remoteController = None + self.hdmiCECController = None self.session = None self.alive = False diff --git a/framework/core/testControl.py b/framework/core/testControl.py index 6388251..cbe230b 100644 --- a/framework/core/testControl.py +++ b/framework/core/testControl.py @@ -155,6 +155,7 @@ def __init__(self, testName="", qcId="", maxRunTime=TEST_MAX_RUN_TIME, level=log self.outboundClient = self.dut.outBoundClient self.powerControl = self.dut.powerControl self.commonRemote = self.dut.remoteController + self.hdmiCECController = self.dut.hdmiCECController self.utils = utilities(self.log) # For UI tests Initialising Video capture and decode the screen_regions.yml for the platform cpePlatform = self.slotInfo.getPlatform() @@ -300,6 +301,8 @@ def testEndFunction(self, powerOff=True): self.webpageController.closeBrowser() if self.capture is not None: self.capture.stop() + if self.hdmiCECController: + self.hdmiCECController.stop() return True def testExceptionCleanUp(self): From a8063f081ff0efbd19611d9055001954fa1637c1 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Sun, 22 Dec 2024 17:59:58 +0000 Subject: [PATCH 27/29] Update #103: Updated streamToFile's readUntil to have 1s delay between retries Corrected hdmiController code to consistently use streamToFile's readUnitl method --- framework/core/hdmiCECController.py | 12 ++--- .../hdmicecModules/abstractCECController.py | 10 ++--- framework/core/hdmicecModules/cecClient.py | 19 ++++---- .../core/hdmicecModules/remoteCECClient.py | 16 +++---- framework/core/streamToFile.py | 44 ++++++++++--------- 5 files changed, 53 insertions(+), 48 deletions(-) diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py index b05163f..9785597 100644 --- a/framework/core/hdmiCECController.py +++ b/framework/core/hdmiCECController.py @@ -148,10 +148,10 @@ def stop(self): import json LOG = logModule('CECTEST', logModule.DEBUG) CONFIGS = [ - # { - # 'type': 'cec-client', - # 'adaptor': '/dev/ttyACM0' # This is default for pulse 8 - # }, + { + 'type': 'cec-client', + 'adaptor': '/dev/ttyACM0' # This is default for pulse 8 + }, { 'type': 'remote-cec-client', 'adaptor': '/dev/cec0', # This is default for Raspberry Pi @@ -168,11 +168,13 @@ def stop(self): DEVICES = CEC.listDevices() LOG.info(json.dumps(DEVICES)) CEC.sendMessage('0', '2', '0x8f', ['0x21','0x85']) + CEC.stop() # The user will need to check all the devices expected from their # cec network are shown in this output. # It's is expected that a user will send a standby command on their cec # network during this 2 minutes. - result = CEC.receiveMessage('2', '0', '0x8f') + CEC.start() + result = CEC.receiveMessage('2', '0', '0x8f', timeout=20) LOG.stepResult(result, 'The readUntil result is: [%s]' % result) CEC.stop() # The user should check here the monitoring log for thier type contains diff --git a/framework/core/hdmicecModules/abstractCECController.py b/framework/core/hdmicecModules/abstractCECController.py index 0d2f4d7..88fbc41 100644 --- a/framework/core/hdmicecModules/abstractCECController.py +++ b/framework/core/hdmicecModules/abstractCECController.py @@ -42,7 +42,7 @@ class CECInterface(metaclass=ABCMeta): def __init__(self, adaptor_path:str, logger:logModule, streamLogger: StreamToFile): self.adaptor = adaptor_path self._log = logger - self._proc = None + self._console = None self._stream = streamLogger @abstractmethod @@ -120,9 +120,9 @@ def receiveMessage(self,sourceAddress: str, destAddress: str, opCode: str, timeo Returns: boolean: True if message is received. False otherwise. """ - end = datetime.now().timestamp() + timeout result = False - while datetime.now().timestamp() < end and result is False: - message = self.formatMessage(sourceAddress, destAddress, opCode, payload) - result = self._stream.readUntil(message) + message = self.formatMessage(sourceAddress, destAddress, opCode, payload) + output = self._stream.readUntil(message, timeout) + if len(output) > 0: + result = True return result diff --git a/framework/core/hdmicecModules/cecClient.py b/framework/core/hdmicecModules/cecClient.py index fb8d8df..879d278 100644 --- a/framework/core/hdmicecModules/cecClient.py +++ b/framework/core/hdmicecModules/cecClient.py @@ -68,7 +68,7 @@ def __init__(self, adaptor_path:str, logger:logModule, streamLogger: StreamToFil self.start() def start(self): - self._console = subprocess.Popen(f'cec-client {self.adaptor} -d 0'.split(), + self._console = subprocess.Popen(f'cec-client {self.adaptor}'.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, @@ -115,15 +115,14 @@ def _scanCECNetwork(self) -> list: Returns: list: A list of dictionaries representing discovered devices with details. """ - self.stop() - result = subprocess.run(f'echo "scan" | cec-client {self.adaptor} -s -d 1', - shell=True, - check=True, - stdout=subprocess.PIPE) - self.start() - stdout = result.stdout.decode('utf-8') - self._log.debug('Output of scan on CEC Network: [%s]' % stdout) - devicesOnNetwork = self._splitDeviceSectionsToDicts(stdout) + devicesOnNetwork = [] + self._console.stdin.write('scan') + self._console.stdin.flush() + output = self._stream.readUntil('currently active source',30) + if len(output) > 0: + output = '\n'.join(output) + self._log.debug('Output of scan on CEC Network: [%s]' % output) + devicesOnNetwork = self._splitDeviceSectionsToDicts(output) return devicesOnNetwork def listDevices(self) -> list: diff --git a/framework/core/hdmicecModules/remoteCECClient.py b/framework/core/hdmicecModules/remoteCECClient.py index 7bdad65..ecaa4f1 100644 --- a/framework/core/hdmicecModules/remoteCECClient.py +++ b/framework/core/hdmicecModules/remoteCECClient.py @@ -53,7 +53,7 @@ def __init__(self, adaptor: str,logger: logModule, streamLogger: StreamToFile, a self.start() def start(self): - self._console.write(f'cec-client {self.adaptor} -d 0') + self._console.write(f'cec-client {self.adaptor}') self._stream.writeStreamToFile(self._console.shell.makefile()) def stop(self): @@ -68,7 +68,7 @@ def _getAdaptors(self) -> list: list: A list of dictionaries representing available adaptors with details like COM port. """ self._console.write(f'cec-client -l') - stdout = self._console.read_until(self._console.prompt) + stdout = self._console.read() stdout = stdout.replace('\r\n','\n') adaptor_count = re.search(r'Found devices: ([0-9]+)',stdout, re.M).group(1) adaptors = self._splitDeviceSectionsToDicts(stdout) @@ -79,12 +79,12 @@ def sendMessage(self, sourceAddress: str, destAddress: str, opCode: str, payload self._console.write(f'tx {message}') def listDevices(self) -> list: - self.stop() - self._console.write(f'echo "scan" | cec-client -s {self.adaptor} -d 1 > cec-test.txt') - self._console.waitForPrompt() - output = self._console.read_until('currently active source') - self.start() - devices = self._splitDeviceSectionsToDicts(output.replace('\r\n','\n')) + self._console.write(f'scan') + output = self._stream.readUntil('currently active source',30) + devices = [] + if len(output) > 0: + output = '\n'.join(output) + devices = self._splitDeviceSectionsToDicts(output) for device in devices: device['physical address'] = device.pop('address') device['name'] = device.get('osd string') diff --git a/framework/core/streamToFile.py b/framework/core/streamToFile.py index 7cafdac..f0df434 100644 --- a/framework/core/streamToFile.py +++ b/framework/core/streamToFile.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -from io import IOBase +from io import IOBase, SEEK_CUR from threading import Thread from os import path +import time class StreamToFile(): @@ -12,6 +13,7 @@ def __init__(self, outputPath): self._fileHandle = None self._activeThread = None self._readLine = 0 + self._stopThread = False def writeStreamToFile(self, inputStream: IOBase) -> None: """ @@ -23,10 +25,10 @@ def writeStreamToFile(self, inputStream: IOBase) -> None: If only a file name is given, the file will be written in the current tests log directory. """ self._fileHandle = open(self._filePath, 'a+', encoding='utf-8') + self._stopThread = False newThread = Thread(target=self._writeLogFile, args=[inputStream, self._fileHandle], daemon=True) - self._activeThread = newThread newThread.start() @@ -40,10 +42,9 @@ def stopStreamedLog(self) -> None: Raises: AttributeError: If the specified thread cannot be found. """ - if self._activeThread: - self._activeThread.join(timeout=30) - else: - raise AttributeError(f'Could not find requested logging thread to stop. [{self._filePath}]') + self._stopThread = True + while self._activeThread.is_alive(): + self._activeThread.join() def _writeLogFile(self,streamIn: IOBase, ioOut: IOBase) -> None: """ @@ -53,7 +54,7 @@ def _writeLogFile(self,streamIn: IOBase, ioOut: IOBase) -> None: stream_in (IOBase): The stream from a process. logFilePath (str): File path to write the log out to. """ - while True: + while self._stopThread is False: chunk = streamIn.readline() if chunk == '': break @@ -63,33 +64,36 @@ def readUntil(self, searchString:str, retries: int = 5) -> None: """ Read lines from a file until a specific search string is found, with a specified number of retries. - + Args: searchString (str): The string that will be search for. retries (int): The maximum number of times the method will attempt to find the `searchString`. Defaults to 5 - + Returns: - boolean : True when the `searchString` is found. False otherwise. + list : list of strings including the search line. Empty list when search not found. """ - result = False + result = [] retry = 0 max_retries = retries - while retry != max_retries and not result: + while retry != max_retries and len(result) == 0: read_line = self._readLine - self._fileHandle.seek(read_line) + self._fileHandle.seek(0) out_lines = self._fileHandle.readlines() write_line = len(out_lines) - while read_line != write_line: - if searchString in out_lines[read_line]: - result = True - break - read_line+=1 + if read_line == write_line: + time.sleep(1) + else: + while read_line < write_line: + if searchString in out_lines[read_line]: + result = out_lines[:read_line] + break + read_line+=1 retry += 1 - self._readLine = read_line + self._readLine = read_line return result def __del__(self): self.stopStreamedLog() if self._fileHandle: - self._fileHandle.close() \ No newline at end of file + self._fileHandle.close() From 3b0d5024f59429192b84b6311be8813a50c4d3b2 Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:27:57 +0000 Subject: [PATCH 28/29] Update #103: Changed recieveMessage to checkMessageReceived Changed receiveMessage to checkMessageReceived to allow for a receiveMessage method that returns the received message to be implemented in the future. --- framework/core/hdmiCECController.py | 27 +++++++++---------- .../hdmicecModules/abstractCECController.py | 7 ++--- framework/core/hdmicecModules/cecClient.py | 10 +++---- .../core/hdmicecModules/remoteCECClient.py | 2 +- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/framework/core/hdmiCECController.py b/framework/core/hdmiCECController.py index 9785597..d16a0ea 100644 --- a/framework/core/hdmiCECController.py +++ b/framework/core/hdmiCECController.py @@ -77,7 +77,7 @@ def __init__(self, log: logModule, config: dict): def sendMessage(self, sourceAddress: str, destAddress: str, opCode: str, payload: list = None) -> None: """ Sends an opCode from a specified source and to a specified destination. - + Args: sourceAddress (str): The logical address of the source device (0-9 or A-F). destAddress (str): The logical address of the destination device (0-9 or A-F). @@ -87,11 +87,11 @@ def sendMessage(self, sourceAddress: str, destAddress: str, opCode: str, payload payload_string = '' if isinstance(payload, list): payload_string = ' '.join(payload) - self._log.debug('Sending CEC message: Source=[%s] Dest=[%s] opCode=[%s] payload=[%s]' % + self._log.debug('Sending CEC message: Source=[%s] Dest=[%s] opCode=[%s] payload=[%s]' % (sourceAddress, destAddress, opCode, payload_string)) self.controller.sendMessage(sourceAddress, destAddress, opCode, payload=payload) - def receiveMessage(self, sourceAddress: str, destAddress: str, opCode: str, timeout: int = 10, payload: list = None) -> bool: + def checkMessageReceived(self, sourceAddress: str, destAddress: str, opCode: str, timeout: int = 10, payload: list = None) -> bool: """ This function checks to see if a specified opCode has been received. @@ -106,12 +106,16 @@ def receiveMessage(self, sourceAddress: str, destAddress: str, opCode: str, time Returns: boolean: True if message is received. False otherwise. """ + result = False payload_string = '' if isinstance(payload, list): payload_string = ' '.join(payload) - self._log.debug('Expecting CEC message: Source=[%s] Dest=[%s] opCode=[%s] payload=[%s]' % + self._log.debug('Expecting CEC message: Source=[%s] Dest=[%s] opCode=[%s] payload=[%s]' % (sourceAddress, destAddress, opCode, payload_string)) - return self.controller.receiveMessage(sourceAddress, destAddress, opCode, timeout=timeout, payload=payload) + received_message = self.controller.receiveMessage(sourceAddress, destAddress, opCode, timeout=timeout, payload=payload) + if len(received_message) > 0: + result = True + return result def listDevices(self) -> list: """ @@ -167,15 +171,8 @@ def stop(self): CEC = HDMICECController(LOG, config) DEVICES = CEC.listDevices() LOG.info(json.dumps(DEVICES)) - CEC.sendMessage('0', '2', '0x8f', ['0x21','0x85']) - CEC.stop() - # The user will need to check all the devices expected from their - # cec network are shown in this output. - # It's is expected that a user will send a standby command on their cec - # network during this 2 minutes. - CEC.start() - result = CEC.receiveMessage('2', '0', '0x8f', timeout=20) + CEC.sendMessage('0', '2', '0x8f') + result = CEC.receiveMessage('2', '0', '0x90', payload=['0x00']) LOG.stepResult(result, 'The readUntil result is: [%s]' % result) CEC.stop() - # The user should check here the monitoring log for thier type contains - # the expected information. + diff --git a/framework/core/hdmicecModules/abstractCECController.py b/framework/core/hdmicecModules/abstractCECController.py index 88fbc41..89e5cf3 100644 --- a/framework/core/hdmicecModules/abstractCECController.py +++ b/framework/core/hdmicecModules/abstractCECController.py @@ -118,11 +118,8 @@ def receiveMessage(self,sourceAddress: str, destAddress: str, opCode: str, timeo payload (list): List of hexidecimal strings to be sent with the opCode. Optional. Returns: - boolean: True if message is received. False otherwise. + list: list of strings containing found message. Empty list if message isn't found. """ - result = False message = self.formatMessage(sourceAddress, destAddress, opCode, payload) output = self._stream.readUntil(message, timeout) - if len(output) > 0: - result = True - return result + return output diff --git a/framework/core/hdmicecModules/cecClient.py b/framework/core/hdmicecModules/cecClient.py index 879d278..9596f17 100644 --- a/framework/core/hdmicecModules/cecClient.py +++ b/framework/core/hdmicecModules/cecClient.py @@ -45,7 +45,7 @@ class CECClientController(CECInterface): """ - This class provides an interface for controlling Consumer Electronics Control (CEC) + This class provides an interface for controlling Consumer Electronics Control (CEC) devices through the `cec-client` command-line tool. """ @@ -60,7 +60,7 @@ def __init__(self, adaptor_path:str, logger:logModule, streamLogger: StreamToFil Raises: AttributeError: If the specified CEC adaptor is not found. """ - + super().__init__(adaptor_path=adaptor_path, logger=logger, streamLogger=streamLogger) self._log.debug('Initialising CECClientController for [%s]' % self.adaptor) if self.adaptor not in map(lambda x: x.get('com port'),self._getAdaptors()): @@ -96,7 +96,7 @@ def _getAdaptors(self) -> list: Returns: list: A list of dictionaries representing available adaptors with details like COM port. """ - result = subprocess.run(f'cec-client -l', + result = subprocess.run(f'cec-client -l', shell=True, text=True, capture_output=True, @@ -141,7 +141,7 @@ def listDevices(self) -> list: else: device_dict['active source'] = False return devices - + def _splitDeviceSectionsToDicts(self,command_output:str) -> list: """ Splits the output of a `cec-client` command into individual device sections and parses them into dictionaries. @@ -171,7 +171,7 @@ def formatMessage(self, sourceAddress: str, destAddress: str, opCode:str, payloa if payload: payload_string = ':'.join(map(lambda x: x[2:], payload)) message_string += ':' + payload_string - return message_string + return message_string.lower() def __del__(self): """ diff --git a/framework/core/hdmicecModules/remoteCECClient.py b/framework/core/hdmicecModules/remoteCECClient.py index ecaa4f1..399ebce 100644 --- a/framework/core/hdmicecModules/remoteCECClient.py +++ b/framework/core/hdmicecModules/remoteCECClient.py @@ -128,4 +128,4 @@ def formatMessage(self, sourceAddress, destAddress, opCode, payload = None): if payload: payload_string = ':'.join(map(lambda x: x[2:], payload)) message_string += ':' + payload_string - return message_string \ No newline at end of file + return message_string.lower() \ No newline at end of file From d529cfde3344fab8bd9179af1f4276c403720fec Mon Sep 17 00:00:00 2001 From: TB-1993 <109213741+TB-1993@users.noreply.github.com> Date: Mon, 23 Dec 2024 13:12:37 +0000 Subject: [PATCH 29/29] Updated CHANGELOG --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4493a4..82ef53d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,41 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.2.0](https://github.com/rdkcentral/python_raft/compare/1.1.2...1.2.0) + +- Update #103: Creation of remote hdmicec client [`#115`](https://github.com/rdkcentral/python_raft/pull/115) +- Fix #119: Corrected commonRemote to properly parse key [`#128`](https://github.com/rdkcentral/python_raft/pull/128) +- Adding support keySimulator in remotecontrollers [`#120`](https://github.com/rdkcentral/python_raft/pull/120) +- Fix #103 - Corrected hdmiCECContoller on device manager and added stop [`#103`](https://github.com/rdkcentral/python_raft/issues/103) +- Fix #107: Moved stream handling to it's own module. [`#107`](https://github.com/rdkcentral/python_raft/issues/107) +- Fix #107: Moved the stream logging out of the logging module [`#107`](https://github.com/rdkcentral/python_raft/issues/107) +- Merge pull request #128 from rdkcentral/feature/gh119_corrections_for_keysimulator [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Corrected commonRemote to properly parse key [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Fixed the code changes for combination of keys [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Updated the code changes [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Fixed the changes [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Updated the changes [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Reverting the previous commit changes [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Changed the key value str to rccode [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Updated the file name [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Changed the file name in the example list [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Added the keySimulator in the example list [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Changed the code [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Changed the code and removed the control flow issue [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Removed the print statement [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Updated the commonRemote file format [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #119: Updating the remotecontrollers with keysimulator [`#119`](https://github.com/rdkcentral/python_raft/issues/119) +- Fix #92: Removed deprecation comment from example_rack_config [`#92`](https://github.com/rdkcentral/python_raft/issues/92) +- Fix #92: Added missing cecTypes.py [`#92`](https://github.com/rdkcentral/python_raft/issues/92) +- Fix #92: Updated documentation and fixed bug in monitoring [`#92`](https://github.com/rdkcentral/python_raft/issues/92) +- Update #103 - Updated CEC controller interface to make it easier to use [`f25a966`](https://github.com/rdkcentral/python_raft/commit/f25a9663e145068174b10c7be5e5fb8c89f95ff0) +- Upgrade #92: Creation of HDMICEC module [`b61670b`](https://github.com/rdkcentral/python_raft/commit/b61670b0cc972f39c57aba54c5770316c0b23bf6) +- Update #107: Creation of remote hdmicec client [`3a28750`](https://github.com/rdkcentral/python_raft/commit/3a28750177a45d413621f9d9f13860d620142827) + #### [1.1.2](https://github.com/rdkcentral/python_raft/compare/1.1.1...1.1.2) +> 1 November 2024 + - GH95 - Standardise console classes [`#106`](https://github.com/rdkcentral/python_raft/pull/106) - GH93 - Upgrade ConfigParser class to work with includes [`#103`](https://github.com/rdkcentral/python_raft/pull/103) - Fix #84 - Fixed options for cli selection of slot and rack [`#84`](https://github.com/rdkcentral/python_raft/issues/84)