blob: 6da0e8a2553f58781508826422bd77a6d5aa8bb5 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2023 Google LLC
#
# 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
#
# https://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.
import time
from robot.api.logger import info
import serial
# Ensure this class gets re-instantiated for every test.
ROBOT_LIBRARY_SCOPE = "TEST"
def OpenSerialPort(device, baudrate, timeout, write_timeout) -> serial.Serial:
"""
Helper function to open a pyserial device using the given settings.
Throws an AssertionError if it could not open the given device.
"""
info(f"OpenSerialPort(dev={device}, baud={baudrate}, timeout={timeout}, wrtimeout={write_timeout}")
ser = serial.Serial(port=device,
timeout=timeout,
write_timeout=write_timeout,
baudrate=baudrate)
if not ser.is_open:
raise AssertionError(f"Could not open {device}!")
return ser
class FPGALibrary:
"""
Class to extend the Robot test framework
Adds Renode-compatible faked keywords as well as serial input and
output for the various tests.
"""
# Ensure this class gets re-instantiated for every test.
ROBOT_LIBRARY_SCOPE = "TEST"
def __init__(self, *args, **kwargs) -> None:
"""
Initialize the class library.
On construction, opens the serial ports for the given FPGA
board ID. Throws an AssertionError if it fails.
Arguments expected in kwargs:
board_id: string. Required. The FPGA board ID to test. Used to
construct path names to the UARTs.
timeout: string. The amount of time in seconds to wait in a read
or write to the serial ports.
"""
info(f"FPGALibrary installed ({args}, {kwargs})")
self.board_id = kwargs['board_id']
self.timeout = None
if kwargs.get('timeout'):
self.timeout = float(kwargs['timeout'])
if kwargs.get('quiesce_delay_seconds'):
self.quiesce_delay_seconds = float(kwargs['quiesce_delay_seconds'])
self.smc_uart = f"/dev/Nexus-CP210-FPGA-UART-{self.board_id}"
self.sc_uart = f"/dev/Nexus-FTDI-{self.board_id}-FPGA-UART"
self.uarts = {}
self.default_uart = None
self.open_serial_ports()
def _quiesce_input(self, port) -> None:
"""
Drains a given port of all incoming data.
Because serial has no actual "end state", it's possible for
a device to continually send data. The best we can do to sync
up with the output is delay a second each time we drain to
really wait for the output to stop.
Essentially, if no character is sent for at least one second,
we consider the port to be "quiesced". It's a bad heuristic,
but since there is no actual command and control protocol,
this is the best we get for synchronizing I/O to a command
line.
Arguments:
port: serial.Serial instance. The port to drain of data.
"""
# Loop while there is data available on the port, then delay
# for a second to make sure we've actually caught all of the
# incoming data.
result = ''
while True:
info(f"_quiesce_input: port.read({port.in_waiting})")
result += port.read(port.in_waiting).decode()
info(f"_quiesce_input: sleep({self.quiesce_delay_seconds})")
time.sleep(self.quiesce_delay_seconds)
if port.in_waiting == 0:
break
info(f"_quiesce_input read: [{result}]")
def _write_string_to_uart(self, port, s, wait_for_echo=True) -> None:
"""
Writes a given string to the given port.
Throws AssertionError if the echoed back data doesn't match, or a
read timeout occurs.
Arguments:
port: serial.Serial instance. The port to write data to.
s: string. The string to write to the port.
wait_for_echo: boolean. Whether or not to wait for echoed
data back from the port.
"""
# Flush the write buffers and drain inputs -- might not be
# needed, but do it anyway to ensure we're synced up.
port.flush()
self._quiesce_input(port)
port.write(s.encode('utf-8'))
port.flush()
info(f"wrote [{s}]")
if wait_for_echo:
result = port.read(len(s)).decode()
info(f"read [{result}]")
# We didn't get the same length string back -- likely caused by a timeout.
if len(result) < len(s):
raise AssertionError(
"Timeout when reading from UART -- did not read " +
f"{s}]({len(result)})! " +
f"Got: [{result}]({len(result)})")
if s != result:
raise AssertionError(
"Write String to UART: Echo back: Did not read " +
f"[{s}]({len(result)}) from port! " +
f"Got: [{result}]({len(result)})")
def _write_line_to_uart(self, port, line, wait_for_echo=True) -> None:
"""
Writes the given string with a newline to the given port.
Throws AssertionError if the echoed back data doesn't match, or
a read timeout occurs.
Arguments:
port: serial.Serial instance. The port to write data to.
s: string. The string to write to the port.
wait_for_echo: boolean. Whether or not to wait for echoed
data back from the port.
"""
self._write_string_to_uart(port, line + '\n', wait_for_echo)
def _wait_for_string_on_uart(self, port, s) -> None:
"""
Waits until the given string is found in the port buffer.
Throws AssertionError if a timeout occurs, or if the buffer
doesn't match up with the string to wait for.
Arguments:
port: serial.Serial instance. The port to wait for data from.
s: string. The string to look for on the port.
"""
if not port.is_open:
raise AssertionError(f"Port [{port}] not open!")
result = port.read_until(expected=s.encode('utf-8')).decode()
info(f"_wait_for_string_on_uart read: [{result}]({len(result)})")
# Short read likely resulting from a timeout.
if len(result) < len(s):
raise AssertionError(
f"Timeout while reading on UART: Did not find string [{s}]!")
if not result.endswith(s):
raise AssertionError(
"Wait for String on UART: " +
f"Did not get string [{s}] from port! Got [{result}]")
def set_timeout(self, timeout) -> None:
"""
Sets the read/write timeouts for serial operations.
Robot keyword. Can be called as Set Timeout.
Arguments:
timeout: float. The amount of time in seconds to wait for a
read or write to complete.
"""
self.timeout = float(timeout)
if self.uarts['sc']:
self.uarts['sc'].timeout = self.timeout
self.uarts['sc'].write_timeout = self.timeout
if self.uarts['smc']:
self.uarts['smc'].timeout = self.timeout
self.uarts['smc'].write_timeout = self.timeout
def open_serial_ports(self) -> None:
"""
Opens the UART ports to the FPGA and sets the default UART.
For now, sets the default UART to the SMC side only.
Robot keyword. Can be called as Open Serial Ports.
"""
self.uarts['sc'] = OpenSerialPort(self.sc_uart,
timeout=self.timeout,
write_timeout=self.timeout,
baudrate=115200)
self.uarts['smc'] = OpenSerialPort(self.smc_uart,
timeout=self.timeout,
write_timeout=self.timeout,
baudrate=115200)
self.default_uart = self.uarts['smc']
def close_serial_ports(self) -> None:
"""
Closes previously opened UARTs to the FPGA.
Robot keyword. Can be called as Close Serial Ports.
Throws AssertionError if it was unable to close a port.
"""
for name, port in self.uarts.items():
port.close()
if port.is_open:
raise AssertionError(f"Port {name} did not close.")
def write_line_to_uart(self, line, **kwargs) -> None:
"""
Writes a given string to the default UART.
Throws AssertionError if a timeout occurs, or if the echoed back
characters don't match the given string.
Robot keyword. Can be called as Write Line To UART.
Arguments expected in kwargs:
waitForEcho: boolean. Deafults to True. Whether or not to check
echoed back characters against the given string.
"""
if 'waitForEcho' in kwargs:
self._write_line_to_uart(self.default_uart,
line,
wait_for_echo=kwargs['waitForEcho'])
else:
self._write_line_to_uart(self.default_uart, line)
def wait_for_line_on_uart(self, s, **kwargs) -> None:
"""
Waits for a given string on the default UART.
This does not actually look for a newline in the output. The
name is a misnomer and holdover from Renode terms.
Throws AssertionError if a timeout occurs, or if the read
characters don't match the given string.
Robot keyword. Used as Wait For Line On UART.
"""
self._wait_for_string_on_uart(self.default_uart, s)
def wait_for_prompt_on_uart(self, prompt, *args) -> None:
"""
Waits for the given prompt on the default UART.
Robot keyword. Can be called as Wait For Prompt On UART.
"""
self._wait_for_string_on_uart(self.default_uart, prompt)
def execute_command(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding command `{args}`")
def execute_script(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding script `{args}`")
def set_default_uart_timeout(self, timeout, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding set default uart timeout `{args}`")
def create_log_tester(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding log tester `{args}`")
def run_process(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding run process `{args}`")
def requires(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding requires `{args}`")
def provides(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding provides `{args}`")
def start_emulation(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding start emulation `{args}`")
def create_terminal_tester(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding create terminal tester `{args}`")
def wait_for_logentry(self, *args) -> None:
"""Renode-compatible do nothing keyword."""
info(f"Eliding wait for logentry `{args}`")