Update flow check physical ports test
This commit is contained in:
parent
d33878c112
commit
63b264304e
|
|
@ -216,12 +216,6 @@ export default class LineConnection {
|
||||||
}
|
}
|
||||||
if (this.config.runningPhysical) {
|
if (this.config.runningPhysical) {
|
||||||
this.outputPhysicalTest += message
|
this.outputPhysicalTest += message
|
||||||
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
|
||||||
|
|
||||||
if (this.testingPortPoE)
|
|
||||||
this.debounceTimer = setTimeout(() => {
|
|
||||||
this.flushLogBuffer()
|
|
||||||
}, 1000) // 1s debounce
|
|
||||||
}
|
}
|
||||||
if (data.toString().includes('More') || data.toString().includes('MORE'))
|
if (data.toString().includes('More') || data.toString().includes('MORE'))
|
||||||
this.writeCommand(' ')
|
this.writeCommand(' ')
|
||||||
|
|
@ -1108,35 +1102,39 @@ export default class LineConnection {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.physicalTest.start(listPorts, this.config.inventory)
|
this.physicalTest.start(
|
||||||
// const interval = setInterval(async () => {
|
listPorts.map((el) => el),
|
||||||
// if (!this.physicalTest.done) {
|
this.config.inventory
|
||||||
// // const result = this.physicalTest.getResult()
|
)
|
||||||
// // console.warn('⚠️ Missing ports:', result.missingPorts)
|
const interval = setInterval(async () => {
|
||||||
// } else {
|
if (!this.config.runningPhysical) {
|
||||||
// clearInterval(interval)
|
clearInterval(interval)
|
||||||
// await this.sendReportPhysicalTest()
|
} else {
|
||||||
// this.endTesting()
|
this.flushLogBuffer()
|
||||||
// }
|
}
|
||||||
// }, 10000)
|
}, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
flushLogBuffer() {
|
async flushLogBuffer() {
|
||||||
const lines = this.outputPhysicalTest.split(/\r?\n/)
|
try {
|
||||||
|
this.writeCommand('show power inline | include on\r\n')
|
||||||
// giữ lại dòng cuối nếu chưa kết thúc hoàn chỉnh
|
this.writeCommand('\r\n')
|
||||||
this.outputPhysicalTest = lines.pop() || ''
|
await this.sleep(1000)
|
||||||
|
this.writeCommand('show interfaces status | include SFP\r\n')
|
||||||
const completeLines = lines.join('\n')
|
this.writeCommand('\r\n')
|
||||||
|
await this.sleep(2000)
|
||||||
if (completeLines.trim()) {
|
const output = this.outputPhysicalTest
|
||||||
const ports = this.physicalTest.handleLog(completeLines)
|
this.outputPhysicalTest = ''
|
||||||
if (ports?.length)
|
if (output) {
|
||||||
|
const ports = this.physicalTest.detectPorts(output)
|
||||||
this.socketIO.emit('test_port_physical', {
|
this.socketIO.emit('test_port_physical', {
|
||||||
stationId: this.config.stationId,
|
stationId: this.config.stationId,
|
||||||
lineId: this.config.id,
|
lineId: this.config.id,
|
||||||
data: ports,
|
data: ports,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('flushLogBuffer', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1164,22 +1162,31 @@ export default class LineConnection {
|
||||||
* Get list PoE ports
|
* Get list PoE ports
|
||||||
*/
|
*/
|
||||||
async getPorts(): Promise<string[]> {
|
async getPorts(): Promise<string[]> {
|
||||||
|
this.writeCommand(' terminal length 0\r\n')
|
||||||
this.writeCommand(' show power inline\r\n')
|
this.writeCommand(' show power inline\r\n')
|
||||||
this.writeCommand(' \r\n')
|
this.writeCommand(' \r\n')
|
||||||
await this.sleep(5000)
|
await this.sleep(3000)
|
||||||
|
this.writeCommand(' show interfaces status\r\n')
|
||||||
|
this.writeCommand(' \r\n')
|
||||||
|
await this.sleep(4000)
|
||||||
const statusOutput = this.outputPhysicalTest
|
const statusOutput = this.outputPhysicalTest
|
||||||
this.outputPhysicalTest = ''
|
this.outputPhysicalTest = ''
|
||||||
|
|
||||||
const lines = statusOutput.split('\n')
|
const lines = statusOutput.split('\n')
|
||||||
const ports = []
|
const ports = []
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
// Match: "Gi0/1 is up, line protocol is up"
|
// Match: "Gi0/1 is up, line protocol is up"
|
||||||
const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
|
const matchPoE = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
|
||||||
|
if (matchPoE) {
|
||||||
if (match) {
|
const name = matchPoE[1]
|
||||||
const name = match[1]
|
|
||||||
ports.push(normalizeInterface(name))
|
ports.push(normalizeInterface(name))
|
||||||
}
|
}
|
||||||
|
// Match: "Gi0/15 notconnect 1 auto auto 1000BaseSX SFP"
|
||||||
|
// Match: "Gi0/16 notconnect 1 auto auto Not Present"
|
||||||
|
const matchSFP = line.match(/^([A-Za-z0-9\/]+).*\b(SFP|Not Present)\b/i)
|
||||||
|
if (matchSFP) {
|
||||||
|
const name = matchSFP[1]
|
||||||
|
ports.push(normalizeInterface(name) + ' (SFP)')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.config.ports = [...new Set(ports)]
|
this.config.ports = [...new Set(ports)]
|
||||||
return [...new Set(ports)]
|
return [...new Set(ports)]
|
||||||
|
|
@ -1191,7 +1198,7 @@ export default class LineConnection {
|
||||||
async sendReportPhysicalTest() {
|
async sendReportPhysicalTest() {
|
||||||
const formReport = this.physicalTest.getFormReport()
|
const formReport = this.physicalTest.getFormReport()
|
||||||
await sendMessageToMail(
|
await sendMessageToMail(
|
||||||
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Physical Port Test`,
|
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Physical Ports Test`,
|
||||||
formReport
|
formReport
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import { normalizeInterface } from '../ultils/helper.js'
|
||||||
import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js'
|
import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js'
|
||||||
const LINK_REGEX =
|
const LINK_REGEX =
|
||||||
/Interface\s+((?:FastEthernet|GigabitEthernet|TenGigabitEthernet|TwentyFiveGigE|FortyGigabitEthernet|HundredGigE|Ethernet|Port-channel|Fa|Gi|Te|Hu|Eth)[\w\/.-]+),\s+changed state to\s+(up|down)/i
|
/Interface\s+((?:FastEthernet|GigabitEthernet|TenGigabitEthernet|TwentyFiveGigE|FortyGigabitEthernet|HundredGigE|Ethernet|Port-channel|Fa|Gi|Te|Hu|Eth)[\w\/.-]+),\s+changed state to\s+(up|down)/i
|
||||||
const POE_GRANTED_REGEX = /%ILPOWER-\d+-POWER_GRANTED:\s+Interface\s+([\w\/.-]+):\s+Power granted/i
|
const POE_GRANTED_REGEX =
|
||||||
|
/.*%ILPOWER-\d+-POWER_GRANTED:\s+Interface\s+([\w\/.-]+):\s+Power granted/i
|
||||||
const POE_DISCONNECT_REGEX =
|
const POE_DISCONNECT_REGEX =
|
||||||
/%ILPOWER-\d+-IEEE_DISCONNECT:\s+Interface\s+([\w\/.-]+):\s+PD removed/i
|
/%ILPOWER-\d+-IEEE_DISCONNECT:\s+Interface\s+([\w\/.-]+):\s+PD removed/i
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@ export class PhysicalPortTest {
|
||||||
match = line.match(POE_GRANTED_REGEX)
|
match = line.match(POE_GRANTED_REGEX)
|
||||||
if (match) {
|
if (match) {
|
||||||
iface = normalizeInterface(match[1])
|
iface = normalizeInterface(match[1])
|
||||||
|
state = 'up'
|
||||||
markTested = true
|
markTested = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,7 +161,7 @@ export class PhysicalPortTest {
|
||||||
const status = missing.length === 0 ? 'PASS' : 'WARNING'
|
const status = missing.length === 0 ? 'PASS' : 'WARNING'
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<b>Physical Port Test Report</b><br/>
|
<b>Physical Ports Test Report</b><br/>
|
||||||
<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
|
<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width: 50%;">
|
<td style="width: 50%;">
|
||||||
|
|
@ -169,26 +171,25 @@ export class PhysicalPortTest {
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
Total Ports : ${report.ports.length}<br/>
|
Total Ports : ${report.ports.length}<br/>
|
||||||
Ports Tested (UP) : ${tested.length}<br/>
|
Ports Tested (UP) : <b style="color: #008000;">${tested.length}</b><br/>
|
||||||
Ports Missing : ${missing.length}<br/>
|
Ports Missing : <b style="color: #ff0000;">${missing.length}</b><br/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<br/>
|
<br/>
|
||||||
────────────────────────────────<br/>
|
────────────────────────────────<br/>
|
||||||
<b>Passed Ports</b><br/>
|
<b style="color: #008000;">Passed Ports</b><br/>
|
||||||
${
|
${
|
||||||
tested.length
|
tested.length
|
||||||
? `<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${tested.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/>
|
? `<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${tested.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/>
|
||||||
`
|
`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
<br/>
|
|
||||||
${
|
${
|
||||||
missing.length
|
missing.length
|
||||||
? `
|
? `
|
||||||
────────────────────────────────<br/>
|
────────────────────────────────<br/>
|
||||||
<b>Missing Ports</b><br/>
|
<b style="color: #ff0000;">Missing Ports</b><br/>
|
||||||
<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${missing.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/>
|
<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${missing.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/>
|
||||||
`
|
`
|
||||||
: ''
|
: ''
|
||||||
|
|
@ -209,7 +210,11 @@ export class PhysicalPortTest {
|
||||||
if (!port) return ''
|
if (!port) return ''
|
||||||
|
|
||||||
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2"
|
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2"
|
||||||
const match = port.match(/^([A-Za-z]+)([\d/]+)$/)
|
const isSFP = port.includes('SFP')
|
||||||
|
const match = port
|
||||||
|
.replace('(SFP)', '')
|
||||||
|
.trim()
|
||||||
|
.match(/^([A-Za-z]+)([\d/]+)$/)
|
||||||
|
|
||||||
if (!match) return port
|
if (!match) return port
|
||||||
|
|
||||||
|
|
@ -221,6 +226,44 @@ export class PhysicalPortTest {
|
||||||
const last = parts[parts.length - 1]
|
const last = parts[parts.length - 1]
|
||||||
const preLast = parts?.length > 1 ? parts[parts.length - 2] : ''
|
const preLast = parts?.length > 1 ? parts[parts.length - 2] : ''
|
||||||
|
|
||||||
return `${type?.slice(0, 2)}${preLast ? preLast + '/' : ''}${last}`
|
return `${type?.slice(0, 2)}${preLast ? preLast + '/' : ''}${last}${isSFP ? ' (SFP)' : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function 1: Lấy danh sách các cổng có module SFP từ lệnh 'show interfaces status'
|
||||||
|
* Logic: Tìm dòng bắt đầu bằng Tên Port và có chứa từ khóa "SFP" ở cuối.
|
||||||
|
* Function 2: Lấy danh sách các cổng đang cấp nguồn (PoE on) từ lệnh 'show power inline'
|
||||||
|
* Logic: Tìm dòng bắt đầu bằng Tên Port, cột tiếp theo là Admin status, cột tiếp theo là 'on'.
|
||||||
|
*/
|
||||||
|
detectPorts(output: string): string[] {
|
||||||
|
for (const line of output.split('\n')) {
|
||||||
|
if (line?.includes('include')) continue
|
||||||
|
const ports: string[] = []
|
||||||
|
const regexPoE = /^([A-Za-z0-9\/]+).*\on\b/i
|
||||||
|
const regexSFP = /^([A-Za-z0-9\/]+).*\bSFP\b/i
|
||||||
|
|
||||||
|
let matchPoE = line.match(regexPoE)
|
||||||
|
if (matchPoE) {
|
||||||
|
ports.push(matchPoE[1])
|
||||||
|
}
|
||||||
|
let matchSFP = line.match(regexSFP)
|
||||||
|
if (matchSFP) {
|
||||||
|
ports.push(matchSFP[1] + ' (SFP)')
|
||||||
|
}
|
||||||
|
if (ports.length > 0) {
|
||||||
|
ports
|
||||||
|
.filter((el) => el)
|
||||||
|
.forEach((el) => {
|
||||||
|
const iface = normalizeInterface(el)
|
||||||
|
const port = this.ports.get(iface)
|
||||||
|
if (port) {
|
||||||
|
port.lastState = 'up'
|
||||||
|
port.tested = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getTestedPorts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Progress } from "@mantine/core";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface AutoProgressProps {
|
||||||
|
ms: number; // thời gian chạy từ 0 -> 100
|
||||||
|
start: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutoProgress({ ms, start, style }: AutoProgressProps) {
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// cleanup cũ
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!start) {
|
||||||
|
setValue(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepTime = 50; // update mỗi 50ms
|
||||||
|
const stepValue = 100 / (ms / stepTime);
|
||||||
|
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
setValue((prev) => {
|
||||||
|
if (prev + stepValue >= 100) {
|
||||||
|
return 0; // reset và chạy vòng mới
|
||||||
|
}
|
||||||
|
return prev + stepValue;
|
||||||
|
});
|
||||||
|
}, stepTime);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [start, ms]);
|
||||||
|
|
||||||
|
return <Progress style={style} value={value} animated />;
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,7 @@ import ModalSelectIOS from "./ModalSelectIOS";
|
||||||
import ModalSelectLicense from "./ModalSelectLicense";
|
import ModalSelectLicense from "./ModalSelectLicense";
|
||||||
import ModalRunScenario from "./ModalRunScenario";
|
import ModalRunScenario from "./ModalRunScenario";
|
||||||
import DrawerScenario from "./ModalScenario";
|
import DrawerScenario from "./ModalScenario";
|
||||||
|
import AutoProgress from "../Components/AutoProgress";
|
||||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
|
||||||
const INIT_TICKET = {
|
const INIT_TICKET = {
|
||||||
|
|
@ -115,6 +116,11 @@ const ModalTerminal = ({
|
||||||
const [openSelectLicense, setOpenSelectLicense] = useState<boolean>(false);
|
const [openSelectLicense, setOpenSelectLicense] = useState<boolean>(false);
|
||||||
const [openScenarioModal, setOpenScenarioModal] = useState<boolean>(false);
|
const [openScenarioModal, setOpenScenarioModal] = useState<boolean>(false);
|
||||||
const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false);
|
const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false);
|
||||||
|
const [isPhysicalTest, setIsPhysicalTest] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsPhysicalTest(line?.runningPhysical || false);
|
||||||
|
}, [line?.runningPhysical]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened && line?.tickets && line?.tickets?.length > 0) {
|
if (opened && line?.tickets && line?.tickets?.length > 0) {
|
||||||
|
|
@ -437,7 +443,10 @@ const ModalTerminal = ({
|
||||||
if (!port) return "";
|
if (!port) return "";
|
||||||
|
|
||||||
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2"
|
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2"
|
||||||
const match = port.match(/^([A-Za-z]+)([\d/]+)$/);
|
const match = port
|
||||||
|
.replace("(SFP)", "")
|
||||||
|
.trim()
|
||||||
|
.match(/^([A-Za-z]+)([\d/]+)$/);
|
||||||
|
|
||||||
if (!match) return port;
|
if (!match) return port;
|
||||||
|
|
||||||
|
|
@ -1034,16 +1043,17 @@ const ModalTerminal = ({
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
<Tabs.Panel value="physical">
|
<Tabs.Panel value="physical">
|
||||||
<fieldset
|
<AutoProgress
|
||||||
style={{
|
start={isPhysicalTest || false}
|
||||||
marginTop: "12px",
|
ms={5000}
|
||||||
}}
|
style={{ marginTop: "8px" }}
|
||||||
>
|
/>
|
||||||
|
<fieldset>
|
||||||
<legend>
|
<legend>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Text>
|
<Text>
|
||||||
List ports{" "}
|
List ports{" "}
|
||||||
{line?.runningPhysical
|
{isPhysicalTest
|
||||||
? `(${line?.listPortsPhysical?.length || 0}/${
|
? `(${line?.listPortsPhysical?.length || 0}/${
|
||||||
line?.ports?.length || 0
|
line?.ports?.length || 0
|
||||||
})`
|
})`
|
||||||
|
|
@ -1053,10 +1063,10 @@ const ModalTerminal = ({
|
||||||
</legend>
|
</legend>
|
||||||
<ScrollArea h={"45vh"} p={"4px"}>
|
<ScrollArea h={"45vh"} p={"4px"}>
|
||||||
<SimpleGrid cols={5} spacing={"xs"}>
|
<SimpleGrid cols={5} spacing={"xs"}>
|
||||||
{line?.runningPhysical && line?.ports
|
{isPhysicalTest && line?.ports
|
||||||
? line.ports.map((port, i) => (
|
? line.ports.map((port, i) => (
|
||||||
<Text
|
<Text
|
||||||
fz={"16px"}
|
fz={"15px"}
|
||||||
key={i}
|
key={i}
|
||||||
c={
|
c={
|
||||||
line?.listPortsPhysical?.includes(port)
|
line?.listPortsPhysical?.includes(port)
|
||||||
|
|
@ -1065,6 +1075,22 @@ const ModalTerminal = ({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{normalizePortName(port)}
|
{normalizePortName(port)}
|
||||||
|
{port?.includes("SFP") ? (
|
||||||
|
<Text
|
||||||
|
mt={-4}
|
||||||
|
fz={"11px"}
|
||||||
|
key={i}
|
||||||
|
c={
|
||||||
|
line?.listPortsPhysical?.includes(port)
|
||||||
|
? "#014a1a"
|
||||||
|
: "#dedede"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
SFP
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
|
|
@ -1074,7 +1100,7 @@ const ModalTerminal = ({
|
||||||
<Flex justify={"space-between"}>
|
<Flex justify={"space-between"}>
|
||||||
<Button
|
<Button
|
||||||
fw={400}
|
fw={400}
|
||||||
disabled={isDisable || !line?.runningPhysical}
|
disabled={isDisable || !isPhysicalTest}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="yellow"
|
color="yellow"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|
@ -1094,7 +1120,7 @@ const ModalTerminal = ({
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
{line?.runningPhysical ? (
|
{isPhysicalTest ? (
|
||||||
<Button
|
<Button
|
||||||
fw={400}
|
fw={400}
|
||||||
disabled={isDisable}
|
disabled={isDisable}
|
||||||
|
|
@ -1133,7 +1159,7 @@ const ModalTerminal = ({
|
||||||
setIsDisable(true);
|
setIsDisable(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsDisable(false);
|
setIsDisable(false);
|
||||||
}, 10000);
|
}, 15000);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Start Physical Test
|
Start Physical Test
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue