blob: cadfa18ea7362780eeae489f8753b82f6f82a314 [file]
############################################################################
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you 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.
#
############################################################################
"""Product core class implementation."""
import re
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Optional,
Tuple,
Union,
)
from ntfc.coreconfig import CoreConfig
from ntfc.device.common import CmdReturn, CmdStatus
from ntfc.logger import logger
if TYPE_CHECKING:
from ntfc.device.common import DeviceCommon
###############################################################################
# Class: ProductCore
###############################################################################
class ProductCore:
"""This class implements product core under test."""
def __init__(self, device: "DeviceCommon", conf: "CoreConfig") -> None:
"""Initialize product core under test.
:param device: DeviceCommon instance
:param conf: ProductConfig instance
"""
if not device:
raise TypeError("Device instance is required")
if not isinstance(conf, CoreConfig):
raise TypeError("Config instance is required")
self._device = device
self._uptime = conf.uptime
self._name = conf.name
self._conf = conf
self._prompt = device.prompt
self._main_prompt = self._prompt
self._cur_prompt = (
self._main_prompt
if isinstance(self._main_prompt, str)
else self._main_prompt.decode("utf-8", errors="ignore")
)
# cores info not ready yet, done in self.init() method called when
# device is ready
self._core0: Optional[str] = None
self._cur_core: Optional[str] = None
self._cores: Tuple[str, ...] = ()
def __str__(self) -> str:
"""Get string for object."""
return f"ProductCore: {self._name}"
def _prepare_command(
self, cmd: str, args: Optional[Union[str, List[str]]]
) -> str:
"""Ensure command is valid and include arguments."""
if not cmd:
raise ValueError("Command cannot be empty.")
if args:
if not isinstance(args, (list, tuple)):
args = [args]
cmd = f"{cmd} {' '.join(args)}"
return cmd
def _prepare_pattern(
self,
cmd: str,
expects: Optional[Union[str, List[str]]],
flag: str,
match_all: bool,
regexp: bool,
) -> str:
"""Build a single regex pattern string."""
if expects:
expects_list = (
expects if isinstance(expects, (list, tuple)) else [expects]
)
pattern = self._build_expect_pattern(
expects_list, match_all, regexp
)
else:
pattern = self._default_prompt_pattern(cmd, flag)
# Add 'command not found' alternative
notfound_pattern = re.escape(
f"{cmd.split(' ')[0]}: {self._device.no_cmd}"
)
# Wrap everything and make sure dot matches newlines
full_pattern = f"(?s)({pattern}|{notfound_pattern})"
return full_pattern
def _build_expect_pattern(
self, expects: List[str], match_all: bool, regexp: bool
) -> str:
"""Return a single regex string."""
items = expects if regexp else [re.escape(e) for e in expects]
if match_all:
# Build stacked lookaheads for AND semantics
return "".join(f"(?=.*{item})" for item in items)
# OR semantics
return rf"({'|'.join(items)})"
def _default_prompt_pattern(self, cmd: str, flag: str = "") -> str:
"""Return prompt-based fallback pattern (string)."""
prompt = flag if flag else self._prompt.decode()
return (
f"{re.escape(prompt)}(?!.*{cmd})"
if cmd not in ["\n"]
else re.escape(prompt)
)
def _encode_for_device(
self, cmd: str, pattern: Union[str, bytes, List[Union[str, bytes]]]
) -> Tuple[bytes, bytes]:
"""Encode command and pattern to bytes, merging lists if needed."""
cmd_bytes = cmd.encode("utf-8")
if isinstance(pattern, list):
# Combine list into one string pattern (mainly for compatibility)
pattern_str = "".join(
(
p.decode("utf-8")
if isinstance(p, (bytes, bytearray))
else str(p)
)
for p in pattern
)
pattern_bytes = pattern_str.encode("utf-8")
elif isinstance(pattern, (bytes, bytearray)):
pattern_bytes = bytes(pattern)
else:
pattern_bytes = str(pattern).encode("utf-8")
return cmd_bytes, pattern_bytes
def _match_not_found(self, rematch: Optional[re.Match[Any]]) -> bool:
"""Check for 'command not found' message."""
if not rematch:
return False
matched = rematch.group().strip()
if isinstance(matched, (bytes, bytearray)):
matched = matched.decode("utf-8", errors="ignore")
return self._device.no_cmd in matched
def sendCommand( # noqa: N802
self,
cmd: str,
expects: Optional[Union[str, List[str]]] = None,
args: Optional[Union[str, List[str]]] = None,
timeout: int = 30,
flag: str = "",
match_all: bool = True,
regexp: bool = False,
) -> "CmdStatus":
"""Send command and wait for expected response.
:param cmd: Command to send
:param expects: List of expected responses or None
:param args: List of additional arguments to append to the command
or None
:param timeout: Timeout in seconds
:param flag: Default _prompt
:param match_all: "True" for all responses to match, "False" for any
response to match
:param regexp: "False" for str to match, "True" for regular
expression to match
:raises ValueError:
:raises DeviceError: Communication error with device
:raises TimeoutError: Response timeout
:return: status : command execution status
"""
cmd = self._prepare_command(cmd, args)
pattern = self._prepare_pattern(cmd, expects, flag, match_all, regexp)
cmd_bytes, pattern_bytes = self._encode_for_device(cmd, pattern)
logger.debug(
f"Sending command: {cmd}, expecting: "
f"{pattern} (timeout={timeout}s)"
)
cmdret = self._device.send_cmd_read_until_pattern(
cmd_bytes, pattern=pattern_bytes, timeout=timeout
)
if cmdret.valid_match() and self._match_not_found(cmdret.rematch):
return CmdStatus.NOTFOUND
return cmdret.status
def sendCommandReadUntilPattern( # noqa: N802
self,
cmd: str,
pattern: Optional[Union[str, bytes, List[Union[str, bytes]]]] = None,
args: Optional[Union[str, List[str]]] = None,
timeout: int = 30,
) -> CmdReturn:
"""Send command to device and read until a specific pattern.
:param cmd: (str or list of strs) command to send to device
:param pattern: (str, bytes, or list of (str, bytes)) String or regex
pattern to look for. If a list, patterns will be concatenated
with '.*'. The pattern will be converted to bytes for matching.
:param args: List of additional arguments to append to the command
or None
:param timeout: (int) timeout value in seconds, default 30s.
:return: CmdReturn : command return data
"""
cmd = self._prepare_command(cmd, args)
pattern = self._default_prompt_pattern(cmd) if not pattern else pattern
cmd_bytes, pattern_bytes = self._encode_for_device(cmd, pattern)
logger.debug(
f"Sending command: {cmd}, expecting pattern: "
f"{pattern.decode() if isinstance(pattern, bytes) else pattern} "
f"(timeout={timeout}s)"
)
return self._device.send_cmd_read_until_pattern(
cmd_bytes, pattern=pattern_bytes, timeout=timeout
)
def sendCtrlCmd(self, ctrl_char: str) -> None: # noqa: N802
"""Send a control character command (e.g., Ctrl+C).
:param ctrl_char: Control character to send
"""
if len(ctrl_char) != 1:
raise ValueError(
"ctrl_char must be a single alphabetic character."
)
if self._device.send_ctrl_cmd(ctrl_char) == CmdStatus.SUCCESS:
logger.info(f"Successfully sent Ctrl+{ctrl_char}")
else:
logger.warning(f"Failed to send Ctrl+{ctrl_char}")
# REVISIT: no proc/rpmsg in nuttx/upstream!
def get_core_info(self) -> Tuple[str, ...]:
"""Retrieve CPU core information from the device.
:return: Tuple containing Local CPU as first element followed by Remote
CPUs. Returns empty tuple on failure.
"""
cmd_rpmsg = b"cat proc/rpmsg"
timeout = 5
nonuttx_core = ["dsp"] # Non-NuttX core (extend this list as needed)
# Send command and get raw output
cmdret = self._device.send_cmd_read_until_pattern(
cmd_rpmsg, pattern=self._main_prompt, timeout=timeout
)
if cmdret.status != 0:
logger.error(f"Command failed with return code: {cmdret.status}")
return ()
# Parse output
decoded_output = cmdret.output
lines = [line.strip() for line in decoded_output.splitlines()]
# Find header location
header_index = next(
(
i
for i, line in enumerate(lines)
if "Local CPU" in line and "Remote CPU" in line
),
-1,
)
# Early return if header not found
if header_index == -1:
logger.warning("CPU information header not found in output")
return ()
# Process data rows
core_data = []
for line in lines[header_index + 1 :]: # pragma: no cover
prompt = self._device.prompt.decode()
if line.startswith(prompt):
break # Stop at next prompt
parts = line.split()
if len(parts) >= 2:
core_data.append((parts[0], parts[1]))
# Validate and format results
if not core_data:
logger.warning("No valid CPU data found after header")
return ()
# Extract unique Local CPU (default single value)
local_cpu = core_data[0][0]
# Create result tuple (Local CPU + all Remote CPUs)
return (
local_cpu,
*(cpu[1] for cpu in core_data if cpu[1] not in nonuttx_core),
)
def init(self) -> None:
"""Finish product initialization."""
cores = self.get_core_info()
self._core0 = cores[0] if cores else "core0"
self._cur_core = self._core0
self._cores = cores if cores else ("core0",)
logger.info(f"Current product support cores: {self._cores}")
@property
def cur_core(self) -> Optional[str]:
"""Get current core."""
return self._cur_core
@property
def cores(self) -> Tuple[str, ...]:
"""Get cores."""
return self._cores
def switch_core(self, target_core: str = "") -> int:
"""Switch the target core of the device.
:param target_core: Core to switch to.
:return: 0 on success, -1 on failure
"""
if not self._core0 and not self._cur_core and not self._cores:
raise ValueError("Product not initialized!")
if not target_core:
logger.debug(
"The target_core has no value and cannot be switched."
)
return CmdStatus.NOTFOUND
if self._core0 and target_core.lower() == self._core0.lower():
logger.warning(
"The target core is the main core, and the core"
" is not switched."
)
return CmdStatus.SUCCESS
logger.info(f"Attempting to switch to core: {target_core}")
if target_core.lower() not in tuple(
core.lower() for core in self._cores
):
logger.debug(f"There is no {target_core} core in the device")
return CmdStatus.NOTFOUND
cmd = f"cu -l /dev/tty{target_core.upper()}\n\n"
pattern = f"{target_core}>"
rc = self.sendCommand(cmd, pattern, match_all=False, timeout=5)
if rc == CmdStatus.SUCCESS:
logger.info(f"Core switch to {target_core} succeeded")
return rc
def get_current_prompt(self) -> str:
"""Dynamically obtain the device current prompt.
:return: The current prompt (e.g., ap>) (str)
or None: If the current prompt cannot be determined
"""
core_pattern = rb"(\S+)>"
matches: List[str] = []
for _ in range(5):
cmdret = self._device.send_cmd_read_until_pattern(
b"\n", core_pattern, 1
)
if cmdret.valid_match() and cmdret.rematch:
# Extract the matched core name
core_name = (
cmdret.rematch.group(1).decode("utf-8", errors="ignore")
+ ">"
)
matches.append(core_name)
# Check whether two or more of the three results are consistent
if len(matches) >= 2 and len(set(matches)) == 1:
logger.info(f"The current prompt is {matches[0]}")
return matches[0]
logger.error("Failed to get current prompt, use default prompt.")
return ">"
def reboot(self, timeout: int = 30) -> bool:
"""Reboot the device by calling the device's reboot function.
:param timeout: (int) Timeout in seconds for the reboot operation.
Default is 30 seconds.
:return: (bool) True if the reboot was successful, False otherwise.
"""
logger.info(
f"Attempting to reboot the device with " f"timeout: {timeout}s"
)
success = self._device.reboot(timeout=timeout)
if success:
logger.info("Device rebooted successfully.")
else:
logger.error("Failed to reboot the device.")
return success
@property
def busyloop(self) -> bool:
"""Check if the device is in busy loop."""
return self._device.busyloop
@property
def flood(self) -> bool:
"""Check if flood condition was detected."""
return self._device.flood
@property
def crash(self) -> bool:
"""Check if the device is crashed."""
return self._device.crash
@property
def notalive(self) -> bool:
"""Check if the device is dead."""
return self._device.notalive
@property
def status(self) -> str:
"""Check core status with all failure mode detection.
:return: "CRASH", "BUSYLOOP", "NORMAL", "NOTALIVE"
"""
if self.crash:
return "CRASH"
if self.busyloop:
return "BUSYLOOP"
if self.notalive:
return "NOTALIVE"
return "NORMAL"
@property
def device(self) -> "DeviceCommon":
"""Get underlying device."""
return self._device
@property
def name(self) -> Any:
"""Get product name."""
return self._name
@property
def prompt(self) -> bytes:
"""Get core prompt."""
return self._prompt
@property
def conf(self) -> "CoreConfig":
"""Get core configuration."""
return self._conf
def start_log_collect(self, logs: Dict[str, Any]) -> None:
"""Start device log collector."""
self._device.start_log_collect(logs)
def stop_log_collect(self) -> None:
"""Stop device log collector."""
self._device.stop_log_collect()
def start(self) -> None:
"""Start device."""
self._device.start()