p2p/coe backend implemented
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
DUT="CGW453"
|
||||
REF="CGW452"
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from datetime import date, datetime
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import db
|
||||
import graph
|
||||
from parser import CsvValidationError, parse_target_csv
|
||||
from scheduler import SchedulerTest, compile_schedule, remove_from_active, reset_scheduler_state
|
||||
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parent
|
||||
DB_PATH = APP_ROOT / "scheduler.db"
|
||||
DUT = os.getenv("DUT", "CGW453").strip()
|
||||
REF = os.getenv("REF", "CGW452").strip()
|
||||
|
||||
app = FastAPI(title="Scheduler API", version="0.1.0")
|
||||
|
||||
|
||||
class LoadTestsRequest(BaseModel):
|
||||
csv_path: str = Field(..., description="Absolute or backend-relative path to target CSV")
|
||||
|
||||
|
||||
class SaveSettingsRequest(BaseModel):
|
||||
settings: dict[str, Any]
|
||||
|
||||
|
||||
class CompileScheduleRequest(BaseModel):
|
||||
start_date: str | None = Field(default=None, description="YYYY-MM-DD")
|
||||
rule: str = ""
|
||||
daytime_testing_today: bool = False
|
||||
top_priority_tests: list[str] = Field(default_factory=list)
|
||||
lowest_priority_tests: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RemoveActiveTestsRequest(BaseModel):
|
||||
test_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup() -> None:
|
||||
db.init_db(DB_PATH)
|
||||
graph.reset_graph_state()
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/settings")
|
||||
def save_settings(request: SaveSettingsRequest) -> dict[str, str]:
|
||||
db.save_settings(request.settings, DB_PATH)
|
||||
return {"status": "saved"}
|
||||
|
||||
|
||||
@app.get("/api/settings")
|
||||
def get_settings() -> dict[str, Any]:
|
||||
return db.read_settings(DB_PATH)
|
||||
|
||||
|
||||
@app.post("/api/tests/load")
|
||||
def load_tests(request: LoadTestsRequest) -> dict[str, Any]:
|
||||
csv_path = Path(request.csv_path)
|
||||
if not csv_path.is_absolute():
|
||||
csv_path = APP_ROOT / csv_path
|
||||
|
||||
try:
|
||||
parsed = parse_target_csv(csv_path)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except CsvValidationError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
count = db.upsert_tests(parsed.tests, DB_PATH)
|
||||
reset_scheduler_state()
|
||||
graph.reset_graph_state()
|
||||
all_dut_tests = db.list_tests_for_device(DUT, DB_PATH)
|
||||
graph.build_graph_once(all_dut_tests)
|
||||
|
||||
return {
|
||||
"loaded_tests": count,
|
||||
"warnings": parsed.warnings,
|
||||
"dut_graph_nodes": len(graph.get_graph()),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/schedule/active/remove")
|
||||
def remove_active_tests(request: RemoveActiveTestsRequest) -> dict[str, Any]:
|
||||
remove_from_active({item.strip() for item in request.test_ids if item.strip()})
|
||||
return {"status": "ok", "removed": len(request.test_ids)}
|
||||
|
||||
|
||||
@app.post("/api/schedule/compile")
|
||||
def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]:
|
||||
if request.start_date:
|
||||
try:
|
||||
datetime.strptime(request.start_date, "%Y-%m-%d")
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail="start_date must be YYYY-MM-DD") from exc
|
||||
|
||||
stored_tests = db.list_schedulable_tests(DB_PATH, rule=request.rule)
|
||||
if not stored_tests:
|
||||
version = db.create_schedule_version([], DB_PATH)
|
||||
return {
|
||||
"schedule_version": version,
|
||||
"scheduled_tests": 0,
|
||||
"completion_date": None,
|
||||
}
|
||||
|
||||
scheduler_tests = [
|
||||
SchedulerTest(
|
||||
test_id=t.test_id,
|
||||
device=t.device,
|
||||
test_type=t.test_type,
|
||||
rotation=t.rotation,
|
||||
rx_tx=t.rx_tx,
|
||||
has_coe_pair=t.has_coe_pair,
|
||||
coe_pairing=t.coe_pairing or [],
|
||||
config=t.config,
|
||||
estimated_minutes=t.estimated_minutes,
|
||||
priority=t.priority,
|
||||
raw_payload=t.raw_payload or {},
|
||||
)
|
||||
for t in stored_tests
|
||||
]
|
||||
|
||||
holiday_dates = db.list_holidays(DB_PATH)
|
||||
try:
|
||||
entries, completion_date = compile_schedule(
|
||||
tests=scheduler_tests,
|
||||
start_date=request.start_date,
|
||||
holiday_dates=holiday_dates,
|
||||
top_priority_tests={item.strip() for item in request.top_priority_tests if item.strip()},
|
||||
lowest_priority_tests={item.strip() for item in request.lowest_priority_tests if item.strip()},
|
||||
daytime_testing_today=request.daytime_testing_today,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
version = db.create_schedule_version(
|
||||
[(e.test_id, e.device, e.scheduled_date, e.shift_index, e.sequence_in_shift) for e in entries],
|
||||
DB_PATH,
|
||||
)
|
||||
|
||||
return {
|
||||
"schedule_version": version,
|
||||
"scheduled_tests": len(entries),
|
||||
"completion_date": completion_date,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/schedule/week")
|
||||
def get_schedule_week(start: str | None = None) -> dict[str, Any]:
|
||||
week_start = start or date.today().isoformat()
|
||||
try:
|
||||
datetime.strptime(week_start, "%Y-%m-%d")
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail="start must be YYYY-MM-DD") from exc
|
||||
|
||||
rows = db.get_schedule_week(week_start, DB_PATH)
|
||||
return {
|
||||
"start_date": week_start,
|
||||
"items": [
|
||||
{
|
||||
"test_id": row.test_id,
|
||||
"device": row.device,
|
||||
"scheduled_date": row.scheduled_date,
|
||||
"shift_index": row.shift_index,
|
||||
"sequence_in_shift": row.sequence_in_shift,
|
||||
"test_type": row.test_type,
|
||||
"rotation": row.rotation,
|
||||
"status": row.status,
|
||||
"priority": row.priority,
|
||||
"estimated_minutes": row.estimated_minutes,
|
||||
}
|
||||
for row in rows
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
Priority,Index,Interferer,COE Pair ,Rotation,TC ID,Victim Band,6GHz Power Mode,5G Test Point,5G Channel,5G Bandwidth,5G RSSI,5G Direction,5G STA,6G Test Point,6G Channel,6G Bandwidth,6G RSSI,6G Direction,6G STA,2G Test Point,2G Channel,2G Bandwidth,2G RSSI,2G Direction,2G STA
|
||||
P2,3,Yes,Yes,R2,COERXAC001,5G,LPI,T1F,100,80,-76,UL,STA4,T2A,5,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||
P2,6,Yes,Yes,R2,COERXAC002,5G,LPI,T1F,161,80,-76,UL,STA4,T2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||
P2,15,Yes,Yes,R1,COERXAC003,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,5,160,OFF,DL,STA63,T3E,1,20,-70,UL,STA4
|
||||
P2,17,Yes,Yes,R1,COERXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,133,160,OFF,DL,STA63,T3E,6,20,-70,UL,STA4
|
||||
P2,19,Yes,Yes,R1,COERXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,197,160,OFF,DL,STA63,T3E,11,20,-70,UL,STA4
|
||||
P2,3,Yes,Yes,R1,COERXAX001,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,4,Yes,Yes,R1,COERXAX002,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,6,Yes,Yes,R1,COERXAX003,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,7,Yes,Yes,R1,COERXAX004,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,8,Yes,Yes,R1,COERXAX005,5G,SP,T1F,161,80,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,9,Yes,Yes,R1,COERXAX006,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,11,Yes,Yes,R1,COERXAX007,5G,SP,T1F,100,160,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,12,Yes,Yes,R1,COERXAX008,5G,LPI,T1F,100,160,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,14,Yes,Yes,R2,COERXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,16,Yes,Yes,R2,COERXAX010,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,18,Yes,Yes,R2,COERXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,20,Yes,Yes,R2,COERXAX012,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,22,Yes,Yes,R2,COERXAX013,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,24,Yes,Yes,R3,COERXAX014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,UL,STA56
|
||||
P2,26,Yes,Yes,R3,COERXAX015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,UL,STA56
|
||||
P2,28,Yes,Yes,R3,COERXAX016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,UL,STA56
|
||||
P2,3,Yes,Yes,R3,COERXBE001,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,4,Yes,Yes,R3,COERXBE002,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,6,Yes,Yes,R3,COERXBE003,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,7,Yes,Yes,R3,COERXBE004,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,8,Yes,Yes,R3,COERXBE005,5G,SP,T1F,161,80,-76,UL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||
P2,9,Yes,Yes,R3,COERXBE006,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||
P2,11,Yes,Yes,R3,COERXBE007,5G,SP,T1F,100,160,-76,UL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||
P2,12,Yes,Yes,R3,COERXBE008,5G,LPI,T1F,100,160,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||
P2,14,Yes,Yes,R1,COERXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,16,Yes,Yes,R1,COERXBE010,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,18,Yes,Yes,R1,COERXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,20,Yes,Yes,R1,COERXBE012,6G,SP,T1F,161,80,-76,DL,STA56,T2A,5,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,22,Yes,Yes,R1,COERXBE013,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,24,Yes,Yes,R2,COERXBE014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA56,T3E,1,20,-70,UL,STA63
|
||||
P2,26,Yes,Yes,R2,COERXBE015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,UL,STA63
|
||||
P2,28,Yes,Yes,R2,COERXBE016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA56,T3E,11,20,-70,UL,STA63
|
||||
P2,3,Yes,Yes,R2,COETXAC001,5G,LPI,T1F,100,80,-76,DL,STA4,T1K2A,5,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||
P2,6,Yes,Yes,R2,COETXAC002,5G,LPI,T1F,161,80,-76,DL,STA4,T1K2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||
P2,15,Yes,Yes,R1,COETXAC003,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,5,160,OFF,DL,STA63,T3E,1,20,-70,DL,STA4
|
||||
P2,17,Yes,Yes,R1,COETXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,19,Yes,Yes,R1,COETXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,5,320,OFF,DL,STA63,T3E,11,20,-70,DL,STA4
|
||||
P2,3,Yes,Yes,R1,COETXAX001,5G,LPI,T1F,100,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,4,Yes,Yes,R1,COETXAX002,5G,LPI,T1F,100,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,6,Yes,Yes,R1,COETXAX003,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,7,Yes,Yes,R1,COETXAX004,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,9,Yes,Yes,R1,COETXAX005,5G,SP,T1F,100,160,-76,DL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,10,Yes,Yes,R1,COETXAX006,5G,LPI,T1F,100,160,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,12,Yes,Yes,R2,COETXAX007,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,14,Yes,Yes,R2,COETXAX008,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,16,Yes,Yes,R2,COETXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,18,Yes,Yes,R2,COETXAX010,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,20,Yes,Yes,R2,COETXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,22,Yes,Yes,R3,COETXAX012,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,DL,STA56
|
||||
P2,24,Yes,Yes,R3,COETXAX013,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||
P2,26,Yes,Yes,R3,COETXAX014,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,DL,STA56
|
||||
P2,3,Yes,Yes,R3,COETXBE001,5G,LPI,T1F,100,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,4,Yes,Yes,R3,COETXBE002,5G,LPI,T1F,100,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,6,Yes,Yes,R3,COETXBE003,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,7,Yes,Yes,R3,COETXBE004,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||
P2,9,Yes,Yes,R3,COETXBE005,5G,SP,T1F,100,160,-76,DL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||
P2,10,Yes,Yes,R3,COETXBE006,5G,LPI,T1F,100,160,-76,DL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||
P2,12,Yes,Yes,R1,COETXBE007,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,14,Yes,Yes,R1,COETXBE008,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,16,Yes,Yes,R1,COETXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,18,Yes,Yes,R1,COETXBE010,6G,SP,T1F,161,80,-76,DL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,20,Yes,Yes,R1,COETXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||
P2,22,Yes,Yes,R2,COETXBE012,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA56,T3E,1,20,-70,DL,STA63
|
||||
P2,24,Yes,Yes,R2,COETXBE013,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA63
|
||||
P2,26,Yes,Yes,R2,COETXBE014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA56,T3E,11,20,-70,DL,STA63
|
||||
P1,1,No,No,R2,P2PRXAC001,5G,LPI,T1F,36,80,-76,UL,STA4,,,,,,,,,,,,
|
||||
P1,1,No,No,R2,P2PRXAC002,5G,LPI,T2B,36,80,-67,UL,STA4,,,,,,,,,,,,
|
||||
P1,1,No,No,R2,P2PRXAC003,5G,LPI,T1C,36,80,-45,UL,STA4,,,,,,,,,,,,
|
||||
P1,2,No,Yes,R2,P2PRXAC004,5G,LPI,T1F,100,80,-76,UL,STA4,,,,,,,,,,,,
|
||||
P1,2,No,No,R2,P2PRXAC005,5G,LPI,T2B,100,80,-67,UL,STA4,,,,,,,,,,,,
|
||||
P1,2,No,No,R2,P2PRXAC006,5G,LPI,T1C,100,80,-45,UL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,Yes,R2,P2PRXAC007,5G,LPI,T1F,161,80,-76,UL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,No,R2,P2PRXAC008,5G,LPI,T2B,161,80,-67,UL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,No,R2,P2PRXAC009,5G,LPI,T1C,161,80,-45,UL,STA4,,,,,,,,,,,,
|
||||
P1,14,No,Yes,R1,P2PRXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA4
|
||||
P1,14,No,No,R1,P2PRXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA4
|
||||
P1,14,No,No,R1,P2PRXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA4
|
||||
P1,16,No,Yes,R1,P2PRXAC013,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA4
|
||||
P1,16,No,No,R1,P2PRXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA4
|
||||
P1,16,No,No,R1,P2PRXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA4
|
||||
P1,18,No,Yes,R1,P2PRXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA4
|
||||
P1,18,No,No,R1,P2PRXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA4
|
||||
P1,18,No,No,R1,P2PRXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA4
|
||||
P1,1,No,No,R1,P2PRXAX001,5G,LPI,T1F,36,80,-76,UL,STA56,,,,,,,,,,,,
|
||||
P1,1,No,No,R1,P2PRXAX002,5G,LPI,T2B,36,80,-67,UL,STA56,,,,,,,,,,,,
|
||||
P1,1,No,No,R1,P2PRXAX003,5G,LPI,T1C,36,80,-45,UL,STA56,,,,,,,,,,,,
|
||||
P1,2,No,Yes,R1,P2PRXAX004,5G,LPI,T1F,100,80,-76,UL,STA56,,,,,,,,,,,,
|
||||
P1,2,No,No,R1,P2PRXAX005,5G,LPI,T2B,100,80,-67,UL,STA56,,,,,,,,,,,,
|
||||
P1,2,No,No,R1,P2PRXAX006,5G,LPI,T1C,100,80,-45,UL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,Yes,R1,P2PRXAX007,5G,LPI,T1F,161,80,-76,UL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,No,R1,P2PRXAX008,5G,LPI,T2B,161,80,-67,UL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,No,R1,P2PRXAX009,5G,LPI,T1C,161,80,-45,UL,STA56,,,,,,,,,,,,
|
||||
P1,10,No,Yes,R1,P2PRXAX010,5G,LPI,T1F,100,160,-76,UL,STA56,,,,,,,,,,,,
|
||||
P1,10,No,No,R1,P2PRXAX011,5G,LPI,T2B,100,160,-67,UL,STA56,,,,,,,,,,,,
|
||||
P1,10,No,No,R1,P2PRXAX012,5G,LPI,T1C,100,160,-45,UL,STA56,,,,,,,,,,,,
|
||||
P1,13,No,Yes,R2,P2PRXAX013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA56,,,,,,
|
||||
P1,13,No,No,R2,P2PRXAX014,6G,LPI,,,,,,,T2B,5,160,-70,UL,STA56,,,,,,
|
||||
P1,13,No,No,R2,P2PRXAX015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA56,,,,,,
|
||||
P1,15,No,Yes,R2,P2PRXAX016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA56,,,,,,
|
||||
P1,15,No,No,R2,P2PRXAX017,6G,LPI,,,,,,,T2B,133,160,-70,UL,STA56,,,,,,
|
||||
P1,15,No,No,R2,P2PRXAX018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,,
|
||||
P1,17,No,Yes,R2,P2PRXAX019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA56,,,,,,
|
||||
P1,17,No,No,R2,P2PRXAX020,6G,LPI,,,,,,,T2B,197,160,-70,UL,STA56,,,,,,
|
||||
P1,17,No,No,R2,P2PRXAX021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA56,,,,,,
|
||||
P1,19,No,Yes,R2,P2PRXAX022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA56,,,,,,
|
||||
P1,19,No,No,R2,P2PRXAX023,6G,SP,,,,,,,T2B,5,320,-70,UL,STA56,,,,,,
|
||||
P1,19,No,No,R2,P2PRXAX024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA56,,,,,,
|
||||
P1,21,No,Yes,R2,P2PRXAX025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA56,,,,,,
|
||||
P1,21,No,No,R2,P2PRXAX026,6G,LPI,,,,,,,T2B,133,320,-70,UL,STA56,,,,,,
|
||||
P1,21,No,No,R2,P2PRXAX027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA56,,,,,,
|
||||
P1,23,No,Yes,R3,P2PRXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56
|
||||
P1,23,No,No,R3,P2PRXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56
|
||||
P1,23,No,No,R3,P2PRXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56
|
||||
P1,25,No,Yes,R3,P2PRXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56
|
||||
P1,25,No,No,R3,P2PRXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56
|
||||
P1,25,No,No,R3,P2PRXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56
|
||||
P1,27,No,Yes,R3,P2PRXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56
|
||||
P1,27,No,No,R3,P2PRXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56
|
||||
P1,27,No,No,R3,P2PRXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56
|
||||
P1,1,No,No,R3,P2PRXBE001,5G,LPI,T1F,36,80,-76,UL,STA63,,,,,,,,,,,,
|
||||
P1,1,No,No,R3,P2PRXBE002,5G,LPI,T2B,36,80,-67,UL,STA63,,,,,,,,,,,,
|
||||
P1,1,No,No,R3,P2PRXBE003,5G,LPI,T1C,36,80,-45,UL,STA63,,,,,,,,,,,,
|
||||
P1,2,No,Yes,R3,P2PRXBE004,5G,LPI,T1F,100,80,-76,UL,STA63,,,,,,,,,,,,
|
||||
P1,2,No,No,R3,P2PRXBE005,5G,LPI,T2B,100,80,-67,UL,STA63,,,,,,,,,,,,
|
||||
P1,2,No,No,R3,P2PRXBE006,5G,LPI,T1C,100,80,-45,UL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,Yes,R3,P2PRXBE007,5G,LPI,T1F,161,80,-76,UL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,No,R3,P2PRXBE008,5G,LPI,T2B,161,80,-67,UL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,No,R3,P2PRXBE009,5G,LPI,T1C,161,80,-45,UL,STA63,,,,,,,,,,,,
|
||||
P1,10,No,Yes,R3,P2PRXBE010,5G,LPI,T1F,100,160,-76,UL,STA63,,,,,,,,,,,,
|
||||
P1,10,No,No,R3,P2PRXBE011,5G,LPI,T2B,100,160,-67,UL,STA63,,,,,,,,,,,,
|
||||
P1,10,No,No,R3,P2PRXBE012,5G,LPI,T1C,100,160,-45,UL,STA63,,,,,,,,,,,,
|
||||
P1,13,No,Yes,R1,P2PRXBE013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA63,,,,,,
|
||||
P1,13,No,No,R1,P2PRXBE014,6G,LPI,,,,,,,T2B,5,160,-70,UL,STA63,,,,,,
|
||||
P1,13,No,No,R1,P2PRXBE015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA63,,,,,,
|
||||
P1,15,No,Yes,R1,P2PRXBE016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA63,,,,,,
|
||||
P1,15,No,No,R1,P2PRXBE017,6G,LPI,,,,,,,T2B,133,160,-70,UL,STA63,,,,,,
|
||||
P1,15,No,No,R1,P2PRXBE018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,,
|
||||
P1,17,No,Yes,R1,P2PRXBE019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA63,,,,,,
|
||||
P1,17,No,No,R1,P2PRXBE020,6G,LPI,,,,,,,T2B,197,160,-70,UL,STA63,,,,,,
|
||||
P1,17,No,No,R1,P2PRXBE021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA63,,,,,,
|
||||
P1,19,No,Yes,R1,P2PRXBE022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA63,,,,,,
|
||||
P1,19,No,No,R1,P2PRXBE023,6G,SP,,,,,,,T2B,5,320,-70,UL,STA63,,,,,,
|
||||
P1,19,No,No,R1,P2PRXBE024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA63,,,,,,
|
||||
P1,21,No,Yes,R1,P2PRXBE025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA63,,,,,,
|
||||
P1,21,No,No,R1,P2PRXBE026,6G,LPI,,,,,,,T2B,133,320,-70,UL,STA63,,,,,,
|
||||
P1,21,No,No,R1,P2PRXBE027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA63,,,,,,
|
||||
P1,23,No,Yes,R2,P2PRXBE028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA63
|
||||
P1,23,No,No,R2,P2PRXBE029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA63
|
||||
P1,23,No,No,R2,P2PRXBE030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA63
|
||||
P1,25,No,Yes,R2,P2PRXBE031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA63
|
||||
P1,25,No,No,R2,P2PRXBE032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA63
|
||||
P1,25,No,No,R2,P2PRXBE033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA63
|
||||
P1,27,No,Yes,R2,P2PRXBE034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA63
|
||||
P1,27,No,No,R2,P2PRXBE035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA63
|
||||
P1,27,No,No,R2,P2PRXBE036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA63
|
||||
P1,1,No,No,R2,P2PTXAC001,5G,LPI,T1F,36,80,-76,DL,STA4,,,,,,,,,,,,
|
||||
P1,1,No,No,R2,P2PTXAC002,5G,LPI,T2B,36,80,-67,DL,STA4,,,,,,,,,,,,
|
||||
P1,1,No,No,R2,P2PTXAC003,5G,LPI,T1C,36,80,-45,DL,STA4,,,,,,,,,,,,
|
||||
P1,2,No,Yes,R2,P2PTXAC004,5G,LPI,T1F,100,80,-76,DL,STA4,,,,,,,,,,,,
|
||||
P1,2,No,No,R2,P2PTXAC005,5G,LPI,T2B,100,80,-67,DL,STA4,,,,,,,,,,,,
|
||||
P1,2,No,No,R2,P2PTXAC006,5G,LPI,T1C,100,80,-45,DL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,Yes,R2,P2PTXAC007,5G,LPI,T1F,161,80,-76,DL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,No,R2,P2PTXAC008,5G,LPI,T2B,161,80,-67,DL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,No,R2,P2PTXAC009,5G,LPI,T1C,161,80,-45,DL,STA4,,,,,,,,,,,,
|
||||
P1,14,No,Yes,R1,P2PTXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,DL,STA4
|
||||
P1,14,No,No,R1,P2PTXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,DL,STA4
|
||||
P1,14,No,No,R1,P2PTXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,DL,STA4
|
||||
P1,16,No,Yes,R1,P2PTXAC013,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,DL,STA4
|
||||
P1,16,No,No,R1,P2PTXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,DL,STA4
|
||||
P1,16,No,No,R1,P2PTXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,DL,STA4
|
||||
P1,18,No,Yes,R1,P2PTXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,DL,STA4
|
||||
P1,18,No,No,R1,P2PTXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,DL,STA4
|
||||
P1,18,No,No,R1,P2PTXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,DL,STA4
|
||||
P1,1,No,No,R1,P2PTXAX001,5G,LPI,T1F,36,80,-76,DL,STA56,,,,,,,,,,,,
|
||||
P1,1,No,No,R1,P2PTXAX002,5G,LPI,T2B,36,80,-67,DL,STA56,,,,,,,,,,,,
|
||||
P1,1,No,No,R1,P2PTXAX003,5G,LPI,T1C,36,80,-45,DL,STA56,,,,,,,,,,,,
|
||||
P1,2,No,Yes,R1,P2PTXAX004,5G,LPI,T1F,100,80,-76,DL,STA56,,,,,,,,,,,,
|
||||
P1,2,No,No,R1,P2PTXAX005,5G,LPI,T2B,100,80,-67,DL,STA56,,,,,,,,,,,,
|
||||
P1,2,No,No,R1,P2PTXAX006,5G,LPI,T1C,100,80,-45,DL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,Yes,R1,P2PTXAX007,5G,LPI,T1F,161,80,-76,DL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,No,R1,P2PTXAX008,5G,LPI,T2B,161,80,-67,DL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,No,R1,P2PTXAX009,5G,LPI,T1C,161,80,-45,DL,STA56,,,,,,,,,,,,
|
||||
P1,8,No,Yes,R1,P2PTXAX010,5G,LPI,T1F,100,160,-76,DL,STA56,,,,,,,,,,,,
|
||||
P1,8,No,No,R1,P2PTXAX011,5G,LPI,T2B,100,160,-67,DL,STA56,,,,,,,,,,,,
|
||||
P1,8,No,No,R1,P2PTXAX012,5G,LPI,T1C,100,160,-45,DL,STA56,,,,,,,,,,,,
|
||||
P1,11,No,Yes,R2,P2PTXAX013,6G,LPI,,,,,,,T2A,5,160,-79,DL,STA56,,,,,,
|
||||
P1,11,No,No,R2,P2PTXAX014,6G,LPI,,,,,,,T2B,5,160,-70,DL,STA56,,,,,,
|
||||
P1,11,No,No,R2,P2PTXAX015,6G,LPI,,,,,,,T1C,5,160,-45,DL,STA56,,,,,,
|
||||
P1,13,No,Yes,R2,P2PTXAX016,6G,LPI,,,,,,,T2A,133,160,-79,DL,STA56,,,,,,
|
||||
P1,13,No,No,R2,P2PTXAX017,6G,LPI,,,,,,,T2B,133,160,-70,DL,STA56,,,,,,
|
||||
P1,13,No,No,R2,P2PTXAX018,6G,LPI,,,,,,,T1C,133,160,-45,DL,STA56,,,,,,
|
||||
P1,15,No,Yes,R2,P2PTXAX019,6G,LPI,,,,,,,T2A,197,160,-79,DL,STA56,,,,,,
|
||||
P1,15,No,No,R2,P2PTXAX020,6G,LPI,,,,,,,T2B,197,160,-70,DL,STA56,,,,,,
|
||||
P1,15,No,No,R2,P2PTXAX021,6G,LPI,,,,,,,T1C,197,160,-45,DL,STA56,,,,,,
|
||||
P1,17,No,Yes,R2,P2PTXAX022,6G,SP,,,,,,,T2A,5,320,-79,DL,STA56,,,,,,
|
||||
P1,17,No,No,R2,P2PTXAX023,6G,SP,,,,,,,T2B,5,320,-70,DL,STA56,,,,,,
|
||||
P1,17,No,No,R2,P2PTXAX024,6G,SP,,,,,,,T1C,5,320,-45,DL,STA56,,,,,,
|
||||
P1,19,No,Yes,R2,P2PTXAX025,6G,LPI,,,,,,,T2A,133,320,-79,DL,STA56,,,,,,
|
||||
P1,19,No,No,R2,P2PTXAX026,6G,LPI,,,,,,,T2B,133,320,-70,DL,STA56,,,,,,
|
||||
P1,19,No,No,R2,P2PTXAX027,6G,LPI,,,,,,,T1C,133,320,-45,DL,STA56,,,,,,
|
||||
P1,21,No,Yes,R3,P2PTXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56
|
||||
P1,21,No,No,R3,P2PTXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56
|
||||
P1,21,No,No,R3,P2PTXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56
|
||||
P1,23,No,Yes,R3,P2PTXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56
|
||||
P1,23,No,No,R3,P2PTXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56
|
||||
P1,23,No,No,R3,P2PTXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56
|
||||
P1,25,No,Yes,R3,P2PTXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56
|
||||
P1,25,No,No,R3,P2PTXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56
|
||||
P1,25,No,No,R3,P2PTXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56
|
||||
P1,1,No,No,R3,P2PTXBE001,5G,LPI,T1F,36,80,-76,DL,STA63,,,,,,,,,,,,
|
||||
P1,1,No,No,R3,P2PTXBE002,5G,LPI,T2B,36,80,-67,DL,STA63,,,,,,,,,,,,
|
||||
P1,1,No,No,R3,P2PTXBE003,5G,LPI,T1C,36,80,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,2,No,Yes,R3,P2PTXBE004,5G,LPI,T1F,100,80,-76,DL,STA63,,,,,,,,,,,,
|
||||
P1,2,No,No,R3,P2PTXBE005,5G,LPI,T2B,100,80,-67,DL,STA63,,,,,,,,,,,,
|
||||
P1,2,No,No,R3,P2PTXBE006,5G,LPI,T1C,100,80,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,Yes,R3,P2PTXBE007,5G,LPI,T1F,161,80,-76,DL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,No,R3,P2PTXBE008,5G,LPI,T2B,161,80,-67,DL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,No,R3,P2PTXBE009,5G,LPI,T1C,161,80,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,8,No,Yes,R3,P2PTXBE010,5G,LPI,T1F,100,160,-76,DL,STA63,,,,,,,,,,,,
|
||||
P1,8,No,No,R3,P2PTXBE011,5G,LPI,T2B,100,160,-67,DL,STA63,,,,,,,,,,,,
|
||||
P1,8,No,No,R3,P2PTXBE012,5G,LPI,T1C,100,160,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,11,No,Yes,R3,P2PTXBE013,6G,LPI,T1F,5,160,-79,DL,STA63,,,,,,,,,,,,
|
||||
P1,11,No,No,R1,P2PTXBE014,6G,LPI,T2B,5,160,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,11,No,No,R1,P2PTXBE015,6G,LPI,T1C,5,160,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,13,No,Yes,R1,P2PTXBE016,6G,LPI,T1F,133,160,-79,DL,STA63,,,,,,,,,,,,
|
||||
P1,13,No,No,R1,P2PTXBE017,6G,LPI,T2B,133,160,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,13,No,No,R1,P2PTXBE018,6G,LPI,T1C,133,160,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,15,No,Yes,R1,P2PTXBE019,6G,LPI,T1F,197,160,-79,DL,STA63,,,,,,,,,,,,
|
||||
P1,15,No,No,R1,P2PTXBE020,6G,LPI,T2B,197,160,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,15,No,No,R1,P2PTXBE021,6G,LPI,T1C,197,160,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,17,No,Yes,R1,P2PTXBE022,6G,SP,T1F,5,320,-79,DL,STA63,,,,,,,,,,,,
|
||||
P1,17,No,No,R1,P2PTXBE023,6G,SP,T2B,5,320,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,17,No,No,R1,P2PTXBE024,6G,SP,T1C,5,320,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,19,No,Yes,R1,P2PTXBE025,6G,LPI,T1F,133,320,-79,DL,STA63,,,,,,,,,,,,
|
||||
P1,19,No,No,R1,P2PTXBE026,6G,LPI,T2B,133,320,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,19,No,No,R1,P2PTXBE027,6G,LPI,T1C,133,320,-45,DL,STA63,,,,,,,,,,,,
|
||||
P1,21,No,Yes,R2,P2PTXBE028,2G,LPI,T3E,1,20,-70,UL,STA63,,,,,,,,,,,,
|
||||
P1,21,No,No,R2,P2PTXBE029,2G,LPI,T1D,1,20,-60,UL,STA63,,,,,,,,,,,,
|
||||
P1,21,No,No,R2,P2PTXBE030,2G,LPI,T1C,1,20,-45,UL,STA63,,,,,,,,,,,,
|
||||
P1,23,No,Yes,R2,P2PTXBE031,2G,LPI,T3E,6,20,-70,UL,STA63,,,,,,,,,,,,
|
||||
P1,23,No,No,R2,P2PTXBE032,2G,LPI,T1D,6,20,-60,UL,STA63,,,,,,,,,,,,
|
||||
P1,23,No,No,R2,P2PTXBE033,2G,LPI,T1C,6,20,-45,UL,STA63,,,,,,,,,,,,
|
||||
P1,25,No,Yes,R2,P2PTXBE034,2G,LPI,T3E,11,20,-70,UL,STA63,,,,,,,,,,,,
|
||||
P1,25,No,No,R2,P2PTXBE035,2G,LPI,T1D,11,20,-60,UL,STA63,,,,,,,,,,,,
|
||||
P1,25,No,No,R2,P2PTXBE036,2G,LPI,T1C,11,20,-45,UL,STA63,,,,,,,,,,,,
|
||||
|
@@ -0,0 +1,71 @@
|
||||
Priority,Index,Throttled,Rotation,TC ID,Band,6GHz Power Mode,Station 1 Test Point,Station 1 Channel,Station 1 Bandwidth,Station 1 RSSI,Station 1 Direction,Station 1 Rate,Station 1 STA,Station 2 Test Point,Station 2 Channel,Station 2 Bandwidth,Station 2 RSSI,Station 2 Direction,Station 2 Rate,Station 2 STA,Station 3 Test Point,Station 3 Channel,Station 3 Bandwidth,Station 3 RSSI,Station 3 Direction,Station 3 Rate,Station 3 STA
|
||||
P2,1,Yes,R3,P3PRXBETH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA63,T1I,36,80,-76,UL,22Mbps,STA64,T2J,36,80,-76,UL,11Mbps,STA65
|
||||
P2,2,Yes,R3,P3PRXBETH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA63,T1I,100,80,-76,UL,22Mbps,STA64,T2J,100,80,-76,UL,11Mbps,STA65
|
||||
P2,3,Yes,R3,P3PRXBETH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA63,T1I,161,80,-76,UL,22Mbps,STA64,T2J,161,80,-76,UL,11Mbps,STA65
|
||||
P2,4,Yes,R1,P3PRXBETH004,6G,LPI,T2A,5,160,-79,UL,22Mbps,STA63,T1L,5,160,-79,UL,22Mbps,STA64,T2O,5,160,-79,UL,11Mbps,STA65
|
||||
P2,5,Yes,R1,P3PRXBETH005,6G,LPI,T2A,133,160,-79,UL,22Mbps,STA63,T1L,133,160,-79,UL,22Mbps,STA64,T2O,133,160,-79,UL,11Mbps,STA65
|
||||
P2,6,Yes,R1,P3PRXBETH006,6G,LPI,T2A,197,160,-79,UL,22Mbps,STA63,T1L,197,160,-79,UL,22Mbps,STA64,T2O,197,160,-79,UL,11Mbps,STA65
|
||||
P2,7,Yes,R2,P3PRXBETH007,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA63,T1P,6,20,-70,UL,22Mbps,STA64,T2Q,6,20,-70,UL,11Mbps,STA65
|
||||
P2,8,Yes,R1,P3PRXBETH008,6G,SP,T2A,5,320,-79,UL,22Mbps,STA63,T1L,5,320,-79,UL,22Mbps,STA64,T2O,5,320,-79,UL,11Mbps,STA65
|
||||
P2,9,Yes,R1,P3PRXBETH009,6G,LPI,T2A,133,320,-79,UL,22Mbps,STA63,T1L,133,320,-79,UL,22Mbps,STA64,T2O,133,320,-79,UL,11Mbps,STA65
|
||||
P2,1,No,R3,P3PRXBEUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA63,T1I,36,80,-76,UL,Unlimited,STA64,T2J,36,80,-76,UL,Unlimited,STA65
|
||||
P2,2,No,R3,P3PRXBEUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA63,T1I,100,80,-76,UL,Unlimited,STA64,T2J,100,80,-76,UL,Unlimited,STA65
|
||||
P2,3,No,R3,P3PRXBEUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA63,T1I,161,80,-76,UL,Unlimited,STA64,T2J,161,80,-76,UL,Unlimited,STA65
|
||||
P2,4,No,R1,P3PRXBEUT004,6G,LPI,T2A,5,160,-79,UL,Unlimited,STA63,T1L,5,160,-79,UL,Unlimited,STA64,T2O,5,160,-79,UL,Unlimited,STA65
|
||||
P2,5,No,R1,P3PRXBEUT005,6G,LPI,T2A,133,160,-79,UL,Unlimited,STA63,T1L,133,160,-79,UL,Unlimited,STA64,T2O,133,160,-79,UL,Unlimited,STA65
|
||||
P2,6,No,R1,P3PRXBEUT006,6G,LPI,T2A,197,160,-79,UL,Unlimited,STA63,T1L,197,160,-79,UL,Unlimited,STA64,T2O,197,160,-79,UL,Unlimited,STA65
|
||||
P2,7,No,R2,P3PRXBEUT007,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA63,T1P,6,20,-70,UL,Unlimited,STA64,T2Q,6,20,-70,UL,Unlimited,STA65
|
||||
P2,8,No,R1,P3PRXBEUT008,6G,SP,T2A,5,320,-79,UL,Unlimited,STA63,T1L,5,320,-79,UL,Unlimited,STA64,T2O,5,320,-79,UL,Unlimited,STA65
|
||||
P2,9,No,R1,P3PRXBEUT009,6G,LPI,T2A,133,320,-79,UL,Unlimited,STA63,T1L,133,320,-79,UL,Unlimited,STA64,T2O,133,320,-79,UL,Unlimited,STA65
|
||||
P2,1,Yes,R1,P3PRXAXTH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA56,T1I,36,80,-76,UL,22Mbps,STA58,T2J,36,80,-76,UL,11Mbps,STA59
|
||||
P2,2,Yes,R1,P3PRXAXTH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA56,T1I,100,80,-76,UL,22Mbps,STA58,T2J,100,80,-76,UL,11Mbps,STA59
|
||||
P2,3,Yes,R1,P3PRXAXTH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA56,T1I,161,80,-76,UL,22Mbps,STA58,T2J,161,80,-76,UL,11Mbps,STA59
|
||||
P2,4,Yes,R2,P3PRXAXTH004,6G,LPI,T2A,5,160,-79,UL,22Mbps,STA56,T1L,5,160,-79,UL,22Mbps,STA58,T2O,5,160,-79,UL,11Mbps,STA59
|
||||
P2,5,Yes,R2,P3PRXAXTH005,6G,LPI,T2A,133,160,-79,UL,22Mbps,STA56,T1L,133,160,-79,UL,22Mbps,STA58,T2O,133,160,-79,UL,11Mbps,STA59
|
||||
P2,6,Yes,R2,P3PRXAXTH006,6G,LPI,T2A,197,160,-79,UL,22Mbps,STA56,T1L,197,160,-79,UL,22Mbps,STA58,T2O,197,160,-79,UL,11Mbps,STA59
|
||||
P2,7,Yes,R3,P3PRXAXTH007,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA56,T1P,6,20,-70,UL,22Mbps,STA58,T2Q,6,20,-70,UL,11Mbps,STA59
|
||||
P2,8,Yes,R2,P3PRXAXTH008,6G,SP,T2A,5,320,-79,UL,22Mbps,STA56,T1L,5,320,-79,UL,22Mbps,STA58,T2O,5,320,-79,UL,11Mbps,STA59
|
||||
P2,9,Yes,R2,P3PRXAXTH009,6G,LPI,T2A,133,320,-79,UL,22Mbps,STA56,T1L,133,320,-79,UL,22Mbps,STA58,T2O,133,320,-79,UL,11Mbps,STA59
|
||||
P2,1,No,R1,P3PRXAXUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA56,T1I,36,80,-76,UL,Unlimited,STA58,T2J,36,80,-76,UL,Unlimited,STA59
|
||||
P2,2,No,R1,P3PRXAXUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA56,T1I,100,80,-76,UL,Unlimited,STA58,T2J,100,80,-76,UL,Unlimited,STA59
|
||||
P2,3,No,R1,P3PRXAXUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA56,T1I,161,80,-76,UL,Unlimited,STA58,T2J,161,80,-76,UL,Unlimited,STA59
|
||||
P2,4,No,R2,P3PRXAXUT004,6G,LPI,T2A,5,160,-79,UL,Unlimited,STA56,T1L,5,160,-79,UL,Unlimited,STA58,T2O,5,160,-79,UL,Unlimited,STA59
|
||||
P2,5,No,R2,P3PRXAXUT005,6G,LPI,T2A,133,160,-79,UL,Unlimited,STA56,T1L,133,160,-79,UL,Unlimited,STA58,T2O,133,160,-79,UL,Unlimited,STA59
|
||||
P2,6,No,R2,P3PRXAXUT006,6G,LPI,T2A,197,160,-79,UL,Unlimited,STA56,T1L,197,160,-79,UL,Unlimited,STA58,T2O,197,160,-79,UL,Unlimited,STA59
|
||||
P2,7,No,R3,P3PRXAXUT007,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA56,T1P,6,20,-70,UL,Unlimited,STA58,T2Q,6,20,-70,UL,Unlimited,STA59
|
||||
P2,8,No,R2,P3PRXAXUT008,6G,SP,T2A,5,320,-79,UL,Unlimited,STA56,T1L,5,320,-79,UL,Unlimited,STA58,T2O,5,320,-79,UL,Unlimited,STA59
|
||||
P2,9,No,R2,P3PRXAXUT009,6G,LPI,T2A,133,320,-79,UL,Unlimited,STA56,T1L,133,320,-79,UL,Unlimited,STA58,T2O,133,320,-79,UL,Unlimited,STA59
|
||||
P2,1,Yes,R1,P3PTXAXTH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA56,T1I,36,80,-76,DL,22Mbps,STA58,T2J,36,80,-76,DL,11Mbps,STA59
|
||||
P2,2,Yes,R1,P3PTXAXTH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA56,T1I,100,80,-76,DL,22Mbps,STA58,T2J,100,80,-76,DL,11Mbps,STA59
|
||||
P2,3,Yes,R1,P3PTXAXTH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA56,T1I,161,80,-76,DL,22Mbps,STA58,T2J,161,80,-76,DL,11Mbps,STA59
|
||||
P2,4,Yes,R2,P3PTXAXTH004,6G,LPI,T2A,5,160,-79,DL,22Mbps,STA56,T1L,5,160,-79,DL,22Mbps,STA58,T2O,5,160,-79,DL,11Mbps,STA59
|
||||
P2,5,Yes,R2,P3PTXAXTH005,6G,LPI,T2A,133,160,-79,DL,22Mbps,STA56,T1L,133,160,-79,DL,22Mbps,STA58,T2O,133,160,-79,DL,11Mbps,STA59
|
||||
P2,6,Yes,R2,P3PTXAXTH006,6G,LPI,T2A,197,160,-79,DL,22Mbps,STA56,T1L,197,160,-79,DL,22Mbps,STA58,T2O,197,160,-79,DL,11Mbps,STA59
|
||||
P2,7,Yes,R3,P3PTXAXTH007,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA56,T1P,6,20,-70,DL,22Mbps,STA58,T2Q,6,20,-70,DL,11Mbps,STA59
|
||||
P2,8,Yes,R2,P3PTXAXTH008,6G,SP,T2A,5,320,-79,DL,22Mbps,STA56,T1L,5,320,-79,DL,22Mbps,STA58,T2O,5,320,-79,DL,11Mbps,STA59
|
||||
P2,9,Yes,R2,P3PTXAXTH009,6G,LPI,T2A,133,320,-79,DL,22Mbps,STA56,T1L,133,320,-79,DL,22Mbps,STA58,T2O,133,320,-79,DL,11Mbps,STA59
|
||||
P2,1,No,R1,P3PTXAXUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA56,T1I,36,80,-76,DL,Unlimited,STA58,T2J,36,80,-76,DL,Unlimited,STA59
|
||||
P2,2,No,R1,P3PTXAXUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA56,T1I,100,80,-76,DL,Unlimited,STA58,T2J,100,80,-76,DL,Unlimited,STA59
|
||||
P2,3,No,R1,P3PTXAXUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA56,T1I,161,80,-76,DL,Unlimited,STA58,T2J,161,80,-76,DL,Unlimited,STA59
|
||||
P2,4,No,R2,P3PTXAXUT004,6G,LPI,T2A,5,160,-79,DL,Unlimited,STA56,T1L,5,160,-79,DL,Unlimited,STA58,T2O,5,160,-79,DL,Unlimited,STA59
|
||||
P2,5,No,R2,P3PTXAXUT005,6G,LPI,T2A,133,160,-79,DL,Unlimited,STA56,T1L,133,160,-79,DL,Unlimited,STA58,T2O,133,160,-79,DL,Unlimited,STA59
|
||||
P2,6,No,R2,P3PTXAXUT006,6G,LPI,T2A,197,160,-79,DL,Unlimited,STA56,T1L,197,160,-79,DL,Unlimited,STA58,T2O,197,160,-79,DL,Unlimited,STA59
|
||||
P2,7,No,R3,P3PTXAXUT007,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA56,T1P,6,20,-70,DL,Unlimited,STA58,T2Q,6,20,-70,DL,Unlimited,STA59
|
||||
P2,8,No,R2,P3PTXAXUT008,6G,SP,T2A,5,320,-79,DL,Unlimited,STA56,T1L,5,320,-79,DL,Unlimited,STA58,T2O,5,320,-79,DL,Unlimited,STA59
|
||||
P2,9,No,R2,P3PTXAXUT009,6G,LPI,T2A,133,320,-79,DL,Unlimited,STA56,T1L,133,320,-79,DL,Unlimited,STA58,T2O,133,320,-79,DL,Unlimited,STA59
|
||||
P2,1,Yes,R2,P3PRXACTH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA4,T1I,36,80,-76,UL,22Mbps,STA5,T2J,36,80,-76,UL,11Mbps,STA6
|
||||
P2,2,Yes,R2,P3PRXACTH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA4,T1I,100,80,-76,UL,22Mbps,STA5,T2J,100,80,-76,UL,11Mbps,STA6
|
||||
P2,3,Yes,R2,P3PRXACTH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA4,T1I,161,80,-76,UL,22Mbps,STA5,T2J,161,80,-76,UL,11Mbps,STA6
|
||||
P2,4,Yes,R1,P3PRXACTH004,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA4,T1P,6,20,-70,UL,22Mbps,STA5,T2Q,6,20,-70,UL,11Mbps,STA6
|
||||
P2,1,No,R2,P3PRXACUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA4,T1I,36,80,-76,UL,Unlimited,STA5,T2J,36,80,-76,UL,Unlimited,STA6
|
||||
P2,2,No,R2,P3PRXACUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA4,T1I,100,80,-76,UL,Unlimited,STA5,T2J,100,80,-76,UL,Unlimited,STA6
|
||||
P2,3,No,R2,P3PRXACUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA4,T1I,161,80,-76,UL,Unlimited,STA5,T2J,161,80,-76,UL,Unlimited,STA6
|
||||
P2,4,No,R1,P3PRXACUT004,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA4,T1P,6,20,-70,UL,Unlimited,STA5,T2Q,6,20,-70,UL,Unlimited,STA6
|
||||
P2,1,Yes,R2,P3PTXACTH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA4,T1I,36,80,-76,DL,22Mbps,STA5,T2J,36,80,-76,DL,11Mbps,STA6
|
||||
P2,2,Yes,R2,P3PTXACTH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA4,T1I,100,80,-76,DL,22Mbps,STA5,T2J,100,80,-76,DL,11Mbps,STA6
|
||||
P2,3,Yes,R2,P3PTXACTH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA4,T1I,161,80,-76,DL,22Mbps,STA5,T2J,161,80,-76,DL,11Mbps,STA6
|
||||
P2,4,Yes,R1,P3PTXACTH004,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA4,T1P,6,20,-70,DL,22Mbps,STA5,T2Q,6,20,-70,DL,11Mbps,STA6
|
||||
P2,1,No,R2,P3PTXACUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA4,T1I,36,80,-76,DL,Unlimited,STA5,T2J,36,80,-76,DL,Unlimited,STA6
|
||||
P2,2,No,R2,P3PTXACUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA4,T1I,100,80,-76,DL,Unlimited,STA5,T2J,100,80,-76,DL,Unlimited,STA6
|
||||
P2,3,No,R2,P3PTXACUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA4,T1I,161,80,-76,DL,Unlimited,STA5,T2J,161,80,-76,DL,Unlimited,STA6
|
||||
P2,4,No,R1,P3PTXACUT004,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA4,T1P,6,20,-70,DL,Unlimited,STA5,T2Q,6,20,-70,DL,Unlimited,STA6
|
||||
|
+511
@@ -0,0 +1,511 @@
|
||||
import json
|
||||
import sqlite3
|
||||
import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parent
|
||||
DB_PATH = APP_ROOT / "scheduler.db"
|
||||
|
||||
# Hardware device names (from environment or defaults)
|
||||
DUT = os.getenv("DUT", "CGW453").strip()
|
||||
REF = os.getenv("REF", "CGW452").strip()
|
||||
|
||||
# Device names for the test database (use hardware device names)
|
||||
DEVICE_DUT = DUT
|
||||
DEVICE_REF = REF
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestRecord:
|
||||
test_id: str
|
||||
device: str
|
||||
test_type: str
|
||||
rotation: str | None
|
||||
rx_tx: str | None
|
||||
has_coe_pair: bool
|
||||
coe_pairing: list[str]
|
||||
priority: int
|
||||
victim_band: str | None
|
||||
config: dict[str, dict[str, str | None]]
|
||||
estimated_minutes: int
|
||||
status: str = "pending"
|
||||
excluded: bool = False
|
||||
raw_payload: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScheduleRow:
|
||||
test_id: str
|
||||
device: str
|
||||
scheduled_date: str
|
||||
shift_index: int
|
||||
sequence_in_shift: int
|
||||
test_type: str
|
||||
rotation: str | None
|
||||
status: str
|
||||
priority: int
|
||||
estimated_minutes: int
|
||||
|
||||
@contextmanager
|
||||
def get_connection(db_path: str | Path = DB_PATH) -> Iterator[sqlite3.Connection]:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def init_db(db_path: str | Path = DB_PATH) -> None:
|
||||
# Validate device names are set
|
||||
if not DUT or not REF:
|
||||
raise ValueError(f"Invalid device names: DUT={DUT!r}, REF={REF!r}")
|
||||
|
||||
with get_connection(db_path) as conn:
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
test_id TEXT NOT NULL,
|
||||
device TEXT NOT NULL,
|
||||
test_type TEXT NOT NULL CHECK (test_type IN ('P2P', 'COE', 'P3P')),
|
||||
rotation TEXT,
|
||||
rx_tx TEXT CHECK (rx_tx IN ('RX', 'TX') OR rx_tx IS NULL),
|
||||
has_coe_pair INTEGER NOT NULL DEFAULT 0,
|
||||
coe_pairing_json TEXT,
|
||||
priority INTEGER NOT NULL CHECK (priority BETWEEN 1 AND 5),
|
||||
victim_band TEXT,
|
||||
config_json TEXT,
|
||||
estimated_minutes INTEGER NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'failed', 'invalid')),
|
||||
excluded INTEGER NOT NULL DEFAULT 0,
|
||||
raw_payload TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(test_id, device)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
schedule_version INTEGER NOT NULL,
|
||||
test_id TEXT NOT NULL,
|
||||
device TEXT NOT NULL,
|
||||
scheduled_date TEXT NOT NULL,
|
||||
shift_index INTEGER NOT NULL CHECK (shift_index IN (1, 2, 3)),
|
||||
sequence_in_shift INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(test_id, device) REFERENCES tests(test_id, device)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS holidays (
|
||||
date TEXT PRIMARY KEY,
|
||||
note TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS runtime_defaults (
|
||||
test_type TEXT PRIMARY KEY,
|
||||
minutes INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rerun_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
detected_date TEXT NOT NULL,
|
||||
failed_test_ids_json TEXT NOT NULL,
|
||||
estimated_rerun_minutes INTEGER NOT NULL,
|
||||
rerun_during_day INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tests_status ON tests(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tests_priority ON tests(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedules_date_shift ON schedules(scheduled_date, shift_index);
|
||||
"""
|
||||
)
|
||||
|
||||
_ensure_column(conn, "tests", "has_coe_pair", "INTEGER NOT NULL DEFAULT 0")
|
||||
_ensure_column(conn, "tests", "coe_pairing_json", "TEXT")
|
||||
_ensure_column(conn, "tests", "config_json", "TEXT")
|
||||
_ensure_column(conn, "tests", "victim_band", "TEXT")
|
||||
_ensure_column(conn, "tests", "excluded", "INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
# Seed runtime defaults for schedule estimation.
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO runtime_defaults(test_type, minutes) VALUES
|
||||
('P2P', 80),
|
||||
('COE', 115),
|
||||
('P3P', 105)
|
||||
ON CONFLICT(test_type) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> int:
|
||||
if not records:
|
||||
return 0
|
||||
|
||||
with get_connection(db_path) as conn:
|
||||
conn.executemany(
|
||||
"""
|
||||
INSERT INTO tests(
|
||||
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
||||
coe_pairing_json, config_json, priority, victim_band, estimated_minutes, status, excluded, raw_payload
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(test_id, device) DO UPDATE SET
|
||||
test_type = excluded.test_type,
|
||||
device = excluded.device,
|
||||
rotation = excluded.rotation,
|
||||
rx_tx = excluded.rx_tx,
|
||||
has_coe_pair = excluded.has_coe_pair,
|
||||
coe_pairing_json = excluded.coe_pairing_json,
|
||||
config_json = excluded.config_json,
|
||||
priority = excluded.priority,
|
||||
victim_band = excluded.victim_band,
|
||||
estimated_minutes = excluded.estimated_minutes,
|
||||
status = excluded.status,
|
||||
excluded = excluded.excluded,
|
||||
raw_payload = excluded.raw_payload,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
[
|
||||
(
|
||||
r.test_id,
|
||||
r.device,
|
||||
r.test_type,
|
||||
r.rotation,
|
||||
r.rx_tx,
|
||||
int(r.has_coe_pair),
|
||||
json.dumps(r.coe_pairing or []),
|
||||
json.dumps(r.config),
|
||||
r.priority,
|
||||
r.victim_band,
|
||||
r.estimated_minutes,
|
||||
r.status,
|
||||
int(r.excluded),
|
||||
json.dumps(r.raw_payload or {}),
|
||||
)
|
||||
for r in records
|
||||
],
|
||||
)
|
||||
return len(records)
|
||||
|
||||
|
||||
def _ensure_column(conn: sqlite3.Connection, table: str, column: str, column_type: str) -> None:
|
||||
existing = conn.execute(f"PRAGMA table_info({table})").fetchall()
|
||||
column_names = {row[1] for row in existing}
|
||||
if column not in column_names:
|
||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {column_type}")
|
||||
|
||||
|
||||
def save_settings(payload: dict[str, Any], db_path: str | Path = DB_PATH) -> None:
|
||||
with get_connection(db_path) as conn:
|
||||
for key, value in payload.items():
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO settings(key, value_json, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value_json = excluded.value_json,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
(key, json.dumps(value)),
|
||||
)
|
||||
|
||||
|
||||
def read_settings(db_path: str | Path = DB_PATH) -> dict[str, Any]:
|
||||
with get_connection(db_path) as conn:
|
||||
rows = conn.execute("SELECT key, value_json FROM settings").fetchall()
|
||||
return {row["key"]: json.loads(row["value_json"]) for row in rows}
|
||||
|
||||
|
||||
def _parse_rule_tokens(rule: str | None) -> list[str]:
|
||||
if not rule:
|
||||
return []
|
||||
return [token.strip().upper() for token in rule.split(",") if token.strip()]
|
||||
|
||||
|
||||
def _normalize_text(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return " ".join(str(value).strip().upper().split())
|
||||
|
||||
|
||||
def _normalize_compact(value: Any) -> str:
|
||||
return re.sub(r"[^A-Z0-9+\-]", "", _normalize_text(value))
|
||||
|
||||
|
||||
def _extract_signed_int(value: Any) -> str:
|
||||
match = re.search(r"[-+]?\d+", _normalize_text(value))
|
||||
return match.group(0) if match else ""
|
||||
|
||||
|
||||
def _band_entry(config: dict[str, dict[str, str | None]], band: str) -> dict[str, str | None]:
|
||||
return config.get(band) or config.get(band.lower()) or {}
|
||||
|
||||
|
||||
def _entry_value(entry: dict[str, str | None], key: str) -> str:
|
||||
if not entry:
|
||||
return ""
|
||||
return _normalize_text(entry.get(key) or entry.get(key.capitalize()) or entry.get(key.upper()))
|
||||
|
||||
|
||||
def _match_any_band_value(record: TestRecord, key: str, token_suffix: str) -> bool:
|
||||
if not token_suffix:
|
||||
return False
|
||||
|
||||
target_num = _extract_signed_int(token_suffix)
|
||||
target_compact = _normalize_compact(token_suffix)
|
||||
for band in ("5G", "6G", "2G"):
|
||||
entry = _band_entry(record.config, band)
|
||||
value = _entry_value(entry, key)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if target_num:
|
||||
if _extract_signed_int(value) == target_num:
|
||||
return True
|
||||
elif target_compact and _normalize_compact(value) == target_compact:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _matches_atomic_exclusion_token(record: TestRecord, token: str) -> bool:
|
||||
if not token:
|
||||
return False
|
||||
|
||||
device = _normalize_text(record.device)
|
||||
test_type = _normalize_text(record.test_type)
|
||||
rotation = _normalize_text(record.rotation)
|
||||
victim_band = _normalize_text(record.victim_band)
|
||||
power_mode = _entry_value(_band_entry(record.config, "6G"), "power_mode")
|
||||
known_devices = {_normalize_text(DUT), _normalize_text(REF)}
|
||||
|
||||
if token in {"LPI", "SP"}:
|
||||
return power_mode == token
|
||||
|
||||
if token in known_devices:
|
||||
return device == token
|
||||
|
||||
if token in {"P2P", "P3P", "COE"}:
|
||||
return test_type == token
|
||||
|
||||
if token in {"2G", "5G", "6G"}:
|
||||
return victim_band == token
|
||||
|
||||
if token.startswith("BW"):
|
||||
return _match_any_band_value(record, "bandwidth", token[2:])
|
||||
|
||||
if token.startswith("CH"):
|
||||
return _match_any_band_value(record, "channel", token[2:])
|
||||
|
||||
if token.startswith("RSSI"):
|
||||
return _match_any_band_value(record, "rssi", token[4:])
|
||||
|
||||
if token.startswith("R") and len(token) > 1:
|
||||
return rotation == token
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _matches_exclusion_token(record: TestRecord, token: str) -> bool:
|
||||
if not token:
|
||||
return False
|
||||
|
||||
parts = [part for part in token.split("_") if part]
|
||||
if len(parts) > 1:
|
||||
# Treat underscore as logical AND across sub-tokens.
|
||||
return all(_matches_atomic_exclusion_token(record, part) for part in parts)
|
||||
|
||||
return _matches_atomic_exclusion_token(record, token)
|
||||
|
||||
|
||||
def _should_exclude_by_rule(record: TestRecord, tokens: list[str]) -> bool:
|
||||
return any(_matches_exclusion_token(record, token) for token in tokens)
|
||||
|
||||
|
||||
def list_schedulable_tests(db_path: str | Path = DB_PATH, rule: str = "") -> list[TestRecord]:
|
||||
tokens = _parse_rule_tokens(rule)
|
||||
with get_connection(db_path) as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT
|
||||
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
||||
config_json, victim_band,
|
||||
coe_pairing_json, priority, estimated_minutes,
|
||||
excluded, status, raw_payload
|
||||
FROM tests
|
||||
WHERE excluded = 0
|
||||
AND status != 'completed'
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
results: list[TestRecord] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
TestRecord(
|
||||
test_id=row["test_id"],
|
||||
device=row["device"],
|
||||
test_type=row["test_type"],
|
||||
rotation=row["rotation"],
|
||||
rx_tx=row["rx_tx"],
|
||||
has_coe_pair=bool(row["has_coe_pair"]),
|
||||
coe_pairing=json.loads(row["coe_pairing_json"] or "[]"),
|
||||
priority=int(row["priority"]),
|
||||
victim_band=row["victim_band"],
|
||||
config=json.loads(row["config_json"] or "{}"),
|
||||
estimated_minutes=int(row["estimated_minutes"]),
|
||||
status=row["status"],
|
||||
excluded=bool(row["excluded"]),
|
||||
raw_payload=json.loads(row["raw_payload"] or "{}"),
|
||||
)
|
||||
)
|
||||
|
||||
if not tokens:
|
||||
return results
|
||||
|
||||
return [record for record in results if not _should_exclude_by_rule(record, tokens)]
|
||||
# Get all tests for a specific device, regardless of exclusion or completion status. Used for schedule display and management.
|
||||
def list_tests_for_device(device: str, db_path: str | Path = DB_PATH) -> list[TestRecord]:
|
||||
with get_connection(db_path) as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT
|
||||
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
||||
config_json, victim_band,
|
||||
coe_pairing_json, priority, estimated_minutes,
|
||||
excluded, status, raw_payload
|
||||
FROM tests
|
||||
WHERE device = ?
|
||||
""",
|
||||
(device,),
|
||||
).fetchall()
|
||||
|
||||
results: list[TestRecord] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
TestRecord(
|
||||
test_id=row["test_id"],
|
||||
device=row["device"],
|
||||
test_type=row["test_type"],
|
||||
rotation=row["rotation"],
|
||||
rx_tx=row["rx_tx"],
|
||||
has_coe_pair=bool(row["has_coe_pair"]),
|
||||
coe_pairing=json.loads(row["coe_pairing_json"] or "[]"),
|
||||
priority=int(row["priority"]),
|
||||
victim_band=row["victim_band"],
|
||||
config=json.loads(row["config_json"] or "{}"),
|
||||
estimated_minutes=int(row["estimated_minutes"]),
|
||||
status=row["status"],
|
||||
excluded=bool(row["excluded"]),
|
||||
raw_payload=json.loads(row["raw_payload"] or "{}"),
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def list_holidays(db_path: str | Path = DB_PATH) -> set[str]:
|
||||
with get_connection(db_path) as conn:
|
||||
rows = conn.execute("SELECT date FROM holidays").fetchall()
|
||||
return {row["date"] for row in rows}
|
||||
|
||||
|
||||
def create_schedule_version(
|
||||
entries: list[tuple[str, str, str, int, int]],
|
||||
db_path: str | Path = DB_PATH,
|
||||
) -> int:
|
||||
with get_connection(db_path) as conn:
|
||||
row = conn.execute("SELECT COALESCE(MAX(schedule_version), 0) AS current FROM schedules").fetchone()
|
||||
next_version = int(row["current"]) + 1
|
||||
|
||||
if entries:
|
||||
conn.executemany(
|
||||
"""
|
||||
INSERT INTO schedules(schedule_version, test_id, device, scheduled_date, shift_index, sequence_in_shift)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
(next_version, test_id, device, scheduled_date, shift_index, sequence)
|
||||
for test_id, device, scheduled_date, shift_index, sequence in entries
|
||||
],
|
||||
)
|
||||
|
||||
return next_version
|
||||
|
||||
|
||||
def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[ScheduleRow]:
|
||||
with get_connection(db_path) as conn:
|
||||
version_row = conn.execute("SELECT MAX(schedule_version) AS latest FROM schedules").fetchone()
|
||||
latest = version_row["latest"]
|
||||
if latest is None:
|
||||
return []
|
||||
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT
|
||||
s.test_id,
|
||||
s.device,
|
||||
s.scheduled_date,
|
||||
s.shift_index,
|
||||
s.sequence_in_shift,
|
||||
t.test_type,
|
||||
t.rotation,
|
||||
t.status,
|
||||
t.priority,
|
||||
t.estimated_minutes
|
||||
FROM schedules s
|
||||
JOIN tests t ON t.test_id = s.test_id
|
||||
WHERE s.schedule_version = ?
|
||||
AND s.scheduled_date >= ?
|
||||
AND s.scheduled_date < date(?, '+7 day')
|
||||
ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift
|
||||
""",
|
||||
(latest, start_date, start_date),
|
||||
).fetchall()
|
||||
|
||||
result: list[ScheduleRow] = []
|
||||
for row in rows:
|
||||
result.append(
|
||||
ScheduleRow(
|
||||
test_id=row["test_id"],
|
||||
device=row["device"],
|
||||
scheduled_date=row["scheduled_date"],
|
||||
shift_index=int(row["shift_index"]),
|
||||
sequence_in_shift=int(row["sequence_in_shift"]),
|
||||
test_type=row["test_type"],
|
||||
rotation=row["rotation"],
|
||||
status=row["status"],
|
||||
priority=int(row["priority"]),
|
||||
estimated_minutes=int(row["estimated_minutes"]),
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def mark_tests_completed(test_ids_with_device: list[tuple[str, str]], db_path: str | Path = DB_PATH) -> None:
|
||||
if not test_ids_with_device:
|
||||
return
|
||||
|
||||
with get_connection(db_path) as conn:
|
||||
conn.executemany(
|
||||
"""
|
||||
UPDATE tests
|
||||
SET status = 'completed', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE test_id = ?
|
||||
AND device = ?
|
||||
AND status != 'completed'
|
||||
""",
|
||||
test_ids_with_device,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# Immutable-in-practice graph state for the loaded DUT test set.
|
||||
_TESTS_BY_ID: dict[str, Any] = {}
|
||||
_GRAPH: dict[str, set[str]] = {}
|
||||
|
||||
|
||||
def reset_graph_state() -> None:
|
||||
global _TESTS_BY_ID, _GRAPH
|
||||
_TESTS_BY_ID = {}
|
||||
_GRAPH = {}
|
||||
|
||||
|
||||
def is_graph_built() -> bool:
|
||||
return bool(_GRAPH)
|
||||
|
||||
|
||||
def build_graph_once(tests: list[Any]) -> dict[str, set[str]]:
|
||||
"""Build DUT-only compatibility adjacency graph once per load lifecycle."""
|
||||
global _TESTS_BY_ID, _GRAPH
|
||||
if _GRAPH:
|
||||
return _GRAPH
|
||||
|
||||
# Keep only one representative per DUT test_id.
|
||||
_TESTS_BY_ID = {str(t.test_id): t for t in tests if getattr(t, "test_id", None)}
|
||||
_GRAPH = _build_graph(_TESTS_BY_ID)
|
||||
return _GRAPH
|
||||
|
||||
|
||||
def get_graph() -> dict[str, set[str]]:
|
||||
return _GRAPH
|
||||
|
||||
|
||||
def _build_graph(tests: dict[str, Any]) -> dict[str, set[str]]:
|
||||
test_ids = list(tests.keys())
|
||||
graph = {test_id: set() for test_id in test_ids}
|
||||
for i in range(len(test_ids)):
|
||||
for j in range(i + 1, len(test_ids)):
|
||||
a_id = test_ids[i]
|
||||
b_id = test_ids[j]
|
||||
a = tests[a_id]
|
||||
b = tests[b_id]
|
||||
if _compatible(a, b):
|
||||
graph[a_id].add(b_id)
|
||||
graph[b_id].add(a_id)
|
||||
return graph
|
||||
|
||||
|
||||
def _compatible(a: Any, b: Any) -> bool:
|
||||
# Compatible when same rotation, or both non-P3P and overlap has identical testpoints.
|
||||
if a.rotation == b.rotation:
|
||||
return True
|
||||
if a.test_type != "P3P" and b.test_type != "P3P" and _same_test_points_for_overlap(a.config, b.config):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _same_test_points_for_overlap(
|
||||
config_a: dict[str, dict[str, str | None]],
|
||||
config_b: dict[str, dict[str, str | None]],
|
||||
) -> bool:
|
||||
station_map_a = _build_station_testpoint_map(config_a)
|
||||
station_map_b = _build_station_testpoint_map(config_b)
|
||||
|
||||
overlap = set(station_map_a.keys()) & set(station_map_b.keys())
|
||||
for station in overlap:
|
||||
if station_map_a[station] != station_map_b[station]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> dict[str, str]:
|
||||
station_to_testpoint: dict[str, str] = {}
|
||||
for band in ("5G", "6G", "2G"):
|
||||
entry = config.get(band) or {}
|
||||
testpoint = _norm(entry.get("test_point") or entry.get("Testpoint"))
|
||||
sta_raw = _norm(entry.get("sta") or entry.get("STA"))
|
||||
if not testpoint or not sta_raw:
|
||||
continue
|
||||
for sta in sta_raw.split(","):
|
||||
sta_clean = _norm(sta)
|
||||
if sta_clean:
|
||||
station_to_testpoint[sta_clean] = testpoint
|
||||
return station_to_testpoint
|
||||
|
||||
|
||||
def _norm(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return " ".join(str(value).strip().upper().split())
|
||||
+2515
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,301 @@
|
||||
import csv
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from db import DEVICE_DUT, DEVICE_REF, TestRecord
|
||||
|
||||
REQUIRED_COLUMNS = [
|
||||
"Priority",
|
||||
"Index",
|
||||
"Interferer",
|
||||
"COE Pair",
|
||||
"Rotation",
|
||||
"TC ID",
|
||||
"Victim Band",
|
||||
"6GHz Power Mode",
|
||||
"5G Test Point",
|
||||
"5G Channel",
|
||||
"5G Bandwidth",
|
||||
"5G RSSI",
|
||||
"5G Direction",
|
||||
"5G STA",
|
||||
"6G Test Point",
|
||||
"6G Channel",
|
||||
"6G Bandwidth",
|
||||
"6G RSSI",
|
||||
"6G Direction",
|
||||
"6G STA",
|
||||
"2G Test Point",
|
||||
"2G Channel",
|
||||
"2G Bandwidth",
|
||||
"2G RSSI",
|
||||
"2G Direction",
|
||||
"2G STA",
|
||||
]
|
||||
|
||||
RUNTIME_DEFAULTS = {
|
||||
"P2P": 80,
|
||||
"COE": 115,
|
||||
"P3P": 105,
|
||||
}
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParseResult:
|
||||
tests: list[TestRecord]
|
||||
warnings: list[str]
|
||||
|
||||
|
||||
class CsvValidationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_target_csv(csv_path: str | Path) -> ParseResult:
|
||||
path = Path(csv_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"CSV file not found: {path}")
|
||||
|
||||
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
||||
reader = csv.DictReader(handle)
|
||||
if not reader.fieldnames:
|
||||
raise CsvValidationError("CSV is missing a header row.")
|
||||
|
||||
normalized_fieldnames = {name.strip().upper() for name in reader.fieldnames if name}
|
||||
|
||||
# Check for required columns
|
||||
missing_columns = [
|
||||
c
|
||||
for c in REQUIRED_COLUMNS
|
||||
if c.strip().upper() not in normalized_fieldnames
|
||||
]
|
||||
if missing_columns:
|
||||
raise CsvValidationError(
|
||||
f"CSV is missing required columns: {', '.join(missing_columns)}"
|
||||
)
|
||||
|
||||
tests: list[TestRecord] = []
|
||||
warnings: list[str] = []
|
||||
seen_test_ids: set[str] = set()
|
||||
|
||||
records_with_signature: list[tuple[TestRecord, tuple[str, ...] | None]] = []
|
||||
|
||||
for row_num, row in enumerate(reader, start=2):
|
||||
test_id = (row.get("TC ID") or "").strip()
|
||||
if not test_id:
|
||||
warnings.append(f"Row {row_num}: missing TC ID, row skipped.")
|
||||
continue
|
||||
|
||||
if test_id in seen_test_ids:
|
||||
warnings.append(f"Row {row_num}: duplicate TC ID '{test_id}', row skipped.")
|
||||
continue
|
||||
seen_test_ids.add(test_id)
|
||||
|
||||
test_type = _infer_test_type(test_id)
|
||||
rx_tx = _infer_rx_tx(test_id)
|
||||
rotation = _empty_to_none(_row_get(row, "Rotation"))
|
||||
has_coe_pair = _normalize_yes_no(_row_get(row, "COE Pair"))
|
||||
config = _build_config(row)
|
||||
signature = _victim_band_signature(row)
|
||||
estimated_minutes = RUNTIME_DEFAULTS.get(test_type)
|
||||
victim_band = _normalize_victim_band(_row_get(row, "Victim Band"))
|
||||
|
||||
# Default priority based on test type and COE pairing
|
||||
if test_type == "P2P":
|
||||
priority = 2 if has_coe_pair else 3
|
||||
elif test_type == "COE":
|
||||
priority = 3
|
||||
else:
|
||||
priority = 4
|
||||
|
||||
record = TestRecord(
|
||||
test_id=test_id,
|
||||
device=DEVICE_DUT,
|
||||
test_type=test_type,
|
||||
rotation=rotation,
|
||||
rx_tx=rx_tx,
|
||||
has_coe_pair=has_coe_pair,
|
||||
coe_pairing=[],
|
||||
priority=priority,
|
||||
victim_band=victim_band,
|
||||
config=config,
|
||||
estimated_minutes=estimated_minutes,
|
||||
status="pending",
|
||||
excluded=False,
|
||||
raw_payload=row,
|
||||
)
|
||||
records_with_signature.append((record, signature))
|
||||
|
||||
# Build COE pairing based on victim band signature and RX/TX+band suffix.
|
||||
# Example key suffixes: RXAX, TXAX, RXBE, TXBE.
|
||||
coe_by_signature_and_suffix: dict[tuple, list[str]] = {}
|
||||
for record, signature in records_with_signature:
|
||||
if record.test_type == "COE" and signature is not None:
|
||||
suffix = _extract_pairing_suffix(record.test_id)
|
||||
if suffix:
|
||||
key = (signature, suffix)
|
||||
coe_by_signature_and_suffix.setdefault(key, []).append(record.test_id)
|
||||
|
||||
for record, signature in records_with_signature:
|
||||
pairs = record.coe_pairing
|
||||
if record.test_type == "P2P" and signature is not None:
|
||||
suffix = _extract_pairing_suffix(record.test_id)
|
||||
key = (signature, suffix) if suffix else None
|
||||
pairs = sorted(coe_by_signature_and_suffix.get(key, [])) if key else []
|
||||
|
||||
for device in (DEVICE_DUT, DEVICE_REF):
|
||||
tests.append(
|
||||
TestRecord(
|
||||
test_id=record.test_id,
|
||||
device=device,
|
||||
test_type=record.test_type,
|
||||
rotation=record.rotation,
|
||||
rx_tx=record.rx_tx,
|
||||
has_coe_pair=bool(pairs),
|
||||
coe_pairing=pairs,
|
||||
priority=record.priority,
|
||||
victim_band=record.victim_band,
|
||||
config=record.config,
|
||||
estimated_minutes=record.estimated_minutes,
|
||||
status=record.status,
|
||||
excluded=record.excluded,
|
||||
raw_payload=record.raw_payload,
|
||||
)
|
||||
)
|
||||
|
||||
return ParseResult(tests=tests, warnings=warnings)
|
||||
|
||||
|
||||
def _infer_test_type(test_id: str) -> str:
|
||||
token = test_id.upper()
|
||||
if token.startswith("COE"):
|
||||
return "COE"
|
||||
if token.startswith("P3P"):
|
||||
return "P3P"
|
||||
return "P2P"
|
||||
|
||||
|
||||
def _infer_rx_tx(test_id: str) -> str | None:
|
||||
token = test_id.upper()
|
||||
if "RX" in token:
|
||||
return "RX"
|
||||
if "TX" in token:
|
||||
return "TX"
|
||||
return None
|
||||
|
||||
|
||||
def _extract_pairing_suffix(test_id: str) -> str | None:
|
||||
"""Extract the RX/TX+band suffix used for pairing.
|
||||
|
||||
Format: [COE|P2P][RX|TX][Band][Number]
|
||||
Example: P2PRXAC004 -> 'RXAC', COETXAX012 -> 'TXAX'
|
||||
"""
|
||||
token = test_id.upper()
|
||||
# Remove COE/P2P prefix
|
||||
if token.startswith("COE"):
|
||||
token = token[3:]
|
||||
elif token.startswith("P2P") or token.startswith("P3P"):
|
||||
token = token[3:]
|
||||
else:
|
||||
return None
|
||||
|
||||
# Extract RX/TX + band (for example RXAX, TXBE).
|
||||
if len(token) >= 4 and (token.startswith("RX") or token.startswith("TX")):
|
||||
return token[:4]
|
||||
return None
|
||||
|
||||
|
||||
def _empty_to_none(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
cleaned = value.strip()
|
||||
return cleaned if cleaned else None
|
||||
|
||||
|
||||
def _victim_band_signature(row: dict[str, str]) -> tuple[str, ...] | None:
|
||||
band = _normalize_victim_band(_row_get(row, "Victim Band"))
|
||||
if band is None:
|
||||
return None
|
||||
|
||||
prefix = f"{band} "
|
||||
test_point = _normalize_value(_row_get(row, f"{prefix}Test Point"))
|
||||
channel = _normalize_value(_row_get(row, f"{prefix}Channel"))
|
||||
rssi = _normalize_value(_row_get(row, f"{prefix}RSSI"))
|
||||
bandwidth = _normalize_value(_row_get(row, f"{prefix}Bandwidth"))
|
||||
|
||||
# Direction is intentionally ignored for COE pairing matching.
|
||||
if not all([test_point, channel, rssi, bandwidth]):
|
||||
return None
|
||||
|
||||
return (band, test_point, channel, rssi, bandwidth)
|
||||
|
||||
|
||||
def _normalize_victim_band(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = value.strip().upper().replace("GHZ", "G")
|
||||
if normalized.startswith("5"):
|
||||
return "5G"
|
||||
if normalized.startswith("6"):
|
||||
return "6G"
|
||||
if normalized.startswith("2"):
|
||||
return "2G"
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_value(value: str | None) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return " ".join(value.strip().upper().split())
|
||||
|
||||
|
||||
def _normalize_yes_no(value: str | None) -> bool:
|
||||
return _normalize_value(value) == "YES"
|
||||
|
||||
|
||||
def _build_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
|
||||
return {
|
||||
"5G": {
|
||||
"test_point": _empty_to_none(_row_get(row, "5G Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "5G Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "5G Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "5G RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "5G Direction")),
|
||||
"sta": _empty_to_none(_row_get(row, "5G STA")),
|
||||
},
|
||||
"6G": {
|
||||
"power_mode": _empty_to_none(_row_get(row, "6GHz Power Mode")),
|
||||
"test_point": _empty_to_none(_row_get(row, "6G Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "6G Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "6G Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "6G RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "6G Direction")),
|
||||
"sta": _empty_to_none(_row_get(row, "6G STA")),
|
||||
},
|
||||
"2G": {
|
||||
"test_point": _empty_to_none(_row_get(row, "2G Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "2G Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "2G Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "2G RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "2G Direction")),
|
||||
"sta": _empty_to_none(_row_get(row, "2G STA")),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _row_get(row: dict[str, str], key: str) -> str | None:
|
||||
if key in row:
|
||||
return row.get(key)
|
||||
|
||||
normalized_key = key.strip().upper()
|
||||
for existing_key, value in row.items():
|
||||
if existing_key and existing_key.strip().upper() == normalized_key:
|
||||
return value
|
||||
|
||||
if normalized_key == "COE PAIR":
|
||||
for alias in ("COE PAIRING", "COE_PAIRING"):
|
||||
for existing_key, value in row.items():
|
||||
if existing_key and existing_key.strip().upper() == alias:
|
||||
return value
|
||||
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
fastapi==0.116.0
|
||||
uvicorn==0.35.0
|
||||
pydantic==2.11.7
|
||||
watchdog==6.0.0
|
||||
httpx==0.28.1
|
||||
@@ -0,0 +1,262 @@
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
|
||||
try:
|
||||
import smbclient
|
||||
except ModuleNotFoundError:
|
||||
smbclient = None
|
||||
|
||||
from db import DEVICE_DUT, DEVICE_REF, mark_tests_completed
|
||||
|
||||
_SMB_SESSIONS = set()
|
||||
_SCAN_STATE_LOCK = threading.Lock()
|
||||
_ACTIVE_SCAN_COUNT = 0
|
||||
_RESULT_TEST_ID_PATTERN = re.compile(r"(?:COE|P2P|P3P)(?:RX|TX)?[A-Z]{2}\d{3}", re.IGNORECASE)
|
||||
|
||||
|
||||
def _extract_test_id_from_result_dir_name(dir_name):
|
||||
if not dir_name:
|
||||
return None
|
||||
|
||||
match = _RESULT_TEST_ID_PATTERN.search(str(dir_name).upper())
|
||||
if match:
|
||||
return match.group(0)
|
||||
return None
|
||||
|
||||
|
||||
def _build_match_tokens(parsed):
|
||||
tokens = set()
|
||||
|
||||
def _add(value):
|
||||
if value in (None, ""):
|
||||
return
|
||||
tokens.add(str(value).upper())
|
||||
|
||||
_add(parsed.get("interference"))
|
||||
_add(parsed.get("device"))
|
||||
_add(parsed.get("rotation"))
|
||||
_add(parsed.get("test_point"))
|
||||
_add(parsed.get("rssi"))
|
||||
_add(parsed.get("station"))
|
||||
_add(parsed.get("band"))
|
||||
_add(parsed.get("channel"))
|
||||
_add(parsed.get("bandwidth"))
|
||||
_add(parsed.get("direction"))
|
||||
_add(parsed.get("throttled"))
|
||||
_add(parsed.get("test_id"))
|
||||
|
||||
for bw in parsed.get("extra_bandwidths") or []:
|
||||
_add(bw)
|
||||
|
||||
# Add numeric alias for devices, e.g. CGW453 -> 453.
|
||||
device = (parsed.get("device") or "").upper()
|
||||
device_digits = re.sub(r"\D", "", device)
|
||||
if device_digits:
|
||||
tokens.add(device_digits)
|
||||
|
||||
# SP can appear as flag or specific token variant (e.g., SP40).
|
||||
if parsed.get("sp"):
|
||||
tokens.add("SP")
|
||||
tokens.add(str(parsed.get("sp")).upper())
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
def _rule_matches_target(rule, parsed_tokens):
|
||||
# Rule parts are AND-ed: CGW453_P3P_ROT2 => device AND interference AND rotation.
|
||||
# Support _, -, or spaces as condition separators.
|
||||
parts = [p for p in re.split(r"[_\-\s]+", rule) if p]
|
||||
if not parts:
|
||||
return False
|
||||
|
||||
def _part_matches(part):
|
||||
if part in parsed_tokens:
|
||||
return True
|
||||
|
||||
# Support tag variants (e.g., BW80 should match BW80M/BW80+80 in source names).
|
||||
# Keep this conservative for very short parts to avoid overmatching.
|
||||
if len(part) >= 3:
|
||||
for token in parsed_tokens:
|
||||
if token.startswith(part) or part in token:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
return all(_part_matches(part) for part in parts)
|
||||
|
||||
|
||||
def _should_exclude_target(parsed, exclusions):
|
||||
parsed_tokens = _build_match_tokens(parsed)
|
||||
# Any matching rule excludes the testcase.
|
||||
return any(_rule_matches_target(rule, parsed_tokens) for rule in exclusions)
|
||||
|
||||
|
||||
def _scan_started():
|
||||
global _ACTIVE_SCAN_COUNT
|
||||
with _SCAN_STATE_LOCK:
|
||||
_ACTIVE_SCAN_COUNT += 1
|
||||
|
||||
|
||||
def _scan_finished():
|
||||
global _ACTIVE_SCAN_COUNT
|
||||
with _SCAN_STATE_LOCK:
|
||||
_ACTIVE_SCAN_COUNT = max(0, _ACTIVE_SCAN_COUNT - 1)
|
||||
|
||||
|
||||
def is_scan_in_progress():
|
||||
with _SCAN_STATE_LOCK:
|
||||
return _ACTIVE_SCAN_COUNT > 0
|
||||
|
||||
|
||||
def resolve_runtime_path(path_value):
|
||||
if not path_value:
|
||||
return path_value
|
||||
|
||||
raw_path = str(path_value).strip()
|
||||
if not raw_path:
|
||||
return raw_path
|
||||
|
||||
# If the path is already valid in the current runtime, keep it.
|
||||
if os.path.exists(raw_path):
|
||||
return raw_path
|
||||
|
||||
# UNC/network paths are handled separately via smbclient.
|
||||
if raw_path.startswith("\\\\") or raw_path.startswith("//"):
|
||||
return raw_path
|
||||
|
||||
# Map host paths (Windows or Linux) to the container mount point when running in a container.
|
||||
if os.name != "nt":
|
||||
mount_root = os.getenv("HOST_MOUNT_ROOT", "/host").strip() or "/host"
|
||||
host_root = os.getenv("HOST_BROWSE_ROOT", "").strip()
|
||||
|
||||
raw_norm = raw_path.replace("\\", "/")
|
||||
if host_root:
|
||||
host_norm = host_root.replace("\\", "/").rstrip("/")
|
||||
if raw_norm.lower() == host_norm.lower() or raw_norm.lower().startswith(host_norm.lower() + "/"):
|
||||
relative = raw_norm[len(host_norm):].lstrip("/")
|
||||
if relative:
|
||||
return os.path.join(mount_root, *relative.split("/"))
|
||||
return mount_root
|
||||
|
||||
return raw_path
|
||||
|
||||
|
||||
def _normalize_input_path(path_value):
|
||||
if not path_value:
|
||||
return path_value
|
||||
|
||||
path = resolve_runtime_path(path_value)
|
||||
path = str(path).strip()
|
||||
|
||||
# Accept //server/share style and normalize to UNC for smbclient.
|
||||
if path.startswith("//"):
|
||||
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||
|
||||
# Accept /<ipv4>/<share>/... and normalize to UNC for Linux-hosted inputs.
|
||||
if re.match(r"^/\d{1,3}(?:\.\d{1,3}){3}/[^/]+", path):
|
||||
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _is_unc_path(path):
|
||||
return isinstance(path, str) and path.startswith("\\\\")
|
||||
|
||||
|
||||
def _extract_unc_server(path):
|
||||
if not _is_unc_path(path):
|
||||
return None
|
||||
rest = path[2:]
|
||||
return rest.split("\\", 1)[0] if rest else None
|
||||
|
||||
|
||||
def _register_smb_session_if_needed(path):
|
||||
if not _is_unc_path(path):
|
||||
return
|
||||
|
||||
if smbclient is None:
|
||||
raise ModuleNotFoundError("smbclient is required to scan UNC result paths")
|
||||
|
||||
server = _extract_unc_server(path)
|
||||
if not server or server in _SMB_SESSIONS:
|
||||
return
|
||||
|
||||
username = os.getenv("SMB_USERNAME", "").strip()
|
||||
password = os.getenv("SMB_PASSWORD", "")
|
||||
domain = os.getenv("SMB_DOMAIN", "").strip()
|
||||
|
||||
if username and domain and "\\" not in username and "@" not in username:
|
||||
username = f"{domain}\\{username}"
|
||||
|
||||
if username:
|
||||
smbclient.register_session(server, username=username, password=password)
|
||||
else:
|
||||
smbclient.register_session(server)
|
||||
|
||||
_SMB_SESSIONS.add(server)
|
||||
|
||||
|
||||
def _iter_dir_entries(path):
|
||||
path = _normalize_input_path(path)
|
||||
if _is_unc_path(path):
|
||||
if smbclient is None:
|
||||
raise ModuleNotFoundError("smbclient is required to scan UNC result paths")
|
||||
_register_smb_session_if_needed(path)
|
||||
return list(smbclient.scandir(path))
|
||||
return list(os.scandir(path))
|
||||
|
||||
|
||||
def scan_results(results_dir_dut, results_dir_ref):
|
||||
results_dir_dut = _normalize_input_path(results_dir_dut)
|
||||
results_dir_ref = _normalize_input_path(results_dir_ref)
|
||||
|
||||
if not results_dir_dut or not results_dir_ref:
|
||||
return
|
||||
|
||||
try:
|
||||
dut_entries = [
|
||||
entry.name
|
||||
for entry in _iter_dir_entries(results_dir_dut)
|
||||
if entry.is_dir()
|
||||
]
|
||||
except OSError as exc:
|
||||
print(f"[scanner] Cannot read results dir: {exc}")
|
||||
return
|
||||
|
||||
print(f"[scanner] results: {len(dut_entries)} result dir(s) found in DUT results dir")
|
||||
|
||||
try:
|
||||
ref_entries = [
|
||||
entry.name
|
||||
for entry in _iter_dir_entries(results_dir_ref)
|
||||
if entry.is_dir()
|
||||
]
|
||||
except OSError as exc:
|
||||
print(f"[scanner] Cannot read results dir: {exc}")
|
||||
return
|
||||
|
||||
print(f"[scanner] results: {len(ref_entries)} result dir(s) found in reference results dir")
|
||||
|
||||
completed_batch = []
|
||||
unmatched_entries = []
|
||||
|
||||
for entry_name in dut_entries:
|
||||
test_id = _extract_test_id_from_result_dir_name(entry_name)
|
||||
if test_id:
|
||||
completed_batch.append((test_id, DEVICE_DUT))
|
||||
else:
|
||||
unmatched_entries.append(entry_name)
|
||||
|
||||
for entry_name in ref_entries:
|
||||
test_id = _extract_test_id_from_result_dir_name(entry_name)
|
||||
if test_id:
|
||||
completed_batch.append((test_id, DEVICE_REF))
|
||||
else:
|
||||
unmatched_entries.append(entry_name)
|
||||
|
||||
if unmatched_entries:
|
||||
print(f"[scanner] skipped {len(unmatched_entries)} result dir(s) with no recognizable test id")
|
||||
|
||||
mark_tests_completed(completed_batch)
|
||||
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import graph
|
||||
|
||||
# Bundle priority tiers for scheduling order (lower number = higher priority)
|
||||
BUNDLE_PRIORITY_FAILED = 0 # Failed tests requiring rerun
|
||||
BUNDLE_PRIORITY_P2P_WITH_COE = 1 # P2P tests with COE pairs
|
||||
BUNDLE_PRIORITY_P2P_ONLY = 2 # P2P tests without COE pairs (RX/TX bundled)
|
||||
BUNDLE_PRIORITY_COE_ONLY = 3 # COE tests without P2P pairing (should be rare/unschedulable)
|
||||
BUNDLE_PRIORITY_P3P = 4 # P3P tests
|
||||
|
||||
DUT = os.getenv("DUT", "CGW453").strip()
|
||||
REF = os.getenv("REF", "CGW452").strip()
|
||||
TestKey = tuple[str, str]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchedulerTest:
|
||||
test_id: str
|
||||
device: str
|
||||
test_type: str
|
||||
rotation: str | None
|
||||
rx_tx: str | None
|
||||
has_coe_pair: bool
|
||||
coe_pairing: list[str]
|
||||
config: dict[str, dict[str, str | None]]
|
||||
estimated_minutes: int
|
||||
priority: int
|
||||
raw_payload: dict[str, Any]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScheduleEntry:
|
||||
test_id: str
|
||||
device: str
|
||||
scheduled_date: str
|
||||
shift_index: int
|
||||
sequence_in_shift: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestBundle:
|
||||
"""A bundle of tests that must be run together in sequence."""
|
||||
test_ids: list[TestKey]
|
||||
priority_tier: int
|
||||
total_minutes: int
|
||||
|
||||
|
||||
# Global scheduler state. Graph is built once per loaded test set.
|
||||
_TESTS_BY_ID: dict[TestKey, SchedulerTest] = {}
|
||||
_ACTIVE_DUT: dict[TestKey, int] = {}
|
||||
_ACTIVE_REF: dict[TestKey, int] = {}
|
||||
|
||||
|
||||
def reset_scheduler_state() -> None:
|
||||
global _TESTS_BY_ID, _ACTIVE_DUT, _ACTIVE_REF
|
||||
_TESTS_BY_ID = {}
|
||||
_ACTIVE_DUT = {}
|
||||
_ACTIVE_REF = {}
|
||||
|
||||
|
||||
def initialize_scheduler_state(tests: list[SchedulerTest]) -> None:
|
||||
"""Initialize active scheduler state once for the current loaded schedulable dataset."""
|
||||
global _TESTS_BY_ID, _ACTIVE_DUT, _ACTIVE_REF
|
||||
if _TESTS_BY_ID:
|
||||
return
|
||||
|
||||
_TESTS_BY_ID = {(t.test_id, t.device): t for t in tests}
|
||||
_ACTIVE_DUT = {
|
||||
test_id: _derive_priority(test, set(), set(), tests)
|
||||
for test_id, test in _TESTS_BY_ID.items() if test.device == DUT
|
||||
}
|
||||
_ACTIVE_REF = {
|
||||
test_id: _derive_priority(test, set(), set(), tests)
|
||||
for test_id, test in _TESTS_BY_ID.items() if test.device == REF
|
||||
}
|
||||
|
||||
|
||||
def set_user_priorities(top_priority_tests: set[str], lowest_priority_tests: set[str]) -> None:
|
||||
"""Update active priority map in-place using user overrides."""
|
||||
if not _TESTS_BY_ID:
|
||||
return
|
||||
|
||||
all_tests = list(_TESTS_BY_ID.values())
|
||||
for key in list(_ACTIVE_DUT.keys()):
|
||||
test = _TESTS_BY_ID.get(key)
|
||||
if test is None:
|
||||
continue
|
||||
_ACTIVE_DUT[key] = _derive_priority(test, top_priority_tests, lowest_priority_tests, all_tests)
|
||||
|
||||
for key in list(_ACTIVE_REF.keys()):
|
||||
test = _TESTS_BY_ID.get(key)
|
||||
if test is None:
|
||||
continue
|
||||
_ACTIVE_REF[key] = _derive_priority(test, top_priority_tests, lowest_priority_tests, all_tests)
|
||||
|
||||
|
||||
def remove_from_active(completed_test_ids: set[str]) -> None:
|
||||
"""Remove completed/invalid tests from active list without rebuilding graph."""
|
||||
for test_id in completed_test_ids:
|
||||
_ACTIVE_DUT.pop((test_id, DUT), None)
|
||||
_ACTIVE_REF.pop((test_id, REF), None)
|
||||
|
||||
|
||||
|
||||
def compile_schedule(
|
||||
tests: list[SchedulerTest],
|
||||
start_date: str | None,
|
||||
holiday_dates: set[str],
|
||||
top_priority_tests: set[str],
|
||||
lowest_priority_tests: set[str],
|
||||
daytime_testing_today: bool,
|
||||
) -> tuple[list[ScheduleEntry], str | None]:
|
||||
"""Compile an optimized schedule for the given tests.
|
||||
|
||||
Uses the greedy algorithm respecting shift sequences per design:
|
||||
- Mon-Thu: shift 3 (5pm-1am) then shift 1 next day (1am-10am)
|
||||
- Friday: shift 3 through Monday shift 1 (4-day window)
|
||||
Returns a list of ScheduleEntry objects and the completion date.
|
||||
"""
|
||||
if not tests:
|
||||
return [], None
|
||||
if not graph.is_graph_built():
|
||||
raise RuntimeError("Graph not initialized. Load tests first to build DUT compatibility graph.")
|
||||
|
||||
initialize_scheduler_state(tests)
|
||||
set_user_priorities(top_priority_tests, lowest_priority_tests)
|
||||
|
||||
# Use a local working copy for this compile run. Global active remains until result processing removes IDs.
|
||||
dut_active_priority: dict[TestKey, int] = dict(_ACTIVE_DUT)
|
||||
ref_active_priority: dict[TestKey, int] = dict(_ACTIVE_REF)
|
||||
entries: list[ScheduleEntry] = []
|
||||
|
||||
current_date = _parse_date(start_date)
|
||||
last_date: str | None = None
|
||||
|
||||
while dut_active_priority or ref_active_priority:
|
||||
# Get the shift sequence for current date (respects Mon/Fri/weekend rules)
|
||||
shift_sequence = _get_shift_sequence(current_date, holiday_dates)
|
||||
|
||||
if not shift_sequence:
|
||||
break # No valid shift sequence
|
||||
|
||||
dut_active_test_ids: set[TestKey] = set(dut_active_priority.keys())
|
||||
ref_active_test_ids: set[TestKey] = set(ref_active_priority.keys())
|
||||
bundles = _create_bundles(dut_active_test_ids, ref_active_test_ids, _TESTS_BY_ID)
|
||||
shift_capacities = {
|
||||
(date_obj, shift_idx): _shift_capacity_for_date(
|
||||
current_date=date_obj,
|
||||
holiday_dates=holiday_dates,
|
||||
start_date_override=start_date,
|
||||
daytime_testing_today=daytime_testing_today,
|
||||
).get(shift_idx, 0)
|
||||
for date_obj, shift_idx in shift_sequence
|
||||
}
|
||||
|
||||
placed_entries, placed_test_ids, placed_last_date = _fit_bundles_to_shifts(
|
||||
bundles=bundles,
|
||||
shift_sequence=shift_sequence,
|
||||
tests=_TESTS_BY_ID,
|
||||
graph_by_test_id=graph.get_graph(),
|
||||
shift_capacities=shift_capacities,
|
||||
)
|
||||
entries.extend(placed_entries)
|
||||
for key in placed_test_ids:
|
||||
dut_active_priority.pop(key, None)
|
||||
ref_active_priority.pop(key, None)
|
||||
if placed_last_date is not None:
|
||||
last_date = placed_last_date
|
||||
|
||||
# Move to next scheduling window start date
|
||||
# After shift 1 (1am-9am), there's shift 2 if daytime testing, then shift 3 (5pm)
|
||||
# After shift 3 (5pm), shift 1 is next day (1am)
|
||||
if shift_sequence:
|
||||
last_sequence_date = shift_sequence[-1][0]
|
||||
last_shift_index = shift_sequence[-1][1]
|
||||
|
||||
if last_shift_index == 1:
|
||||
# Last shift was 1 (1am-9am); next shift 3 is same day (5pm)
|
||||
current_date = last_sequence_date
|
||||
elif last_shift_index == 2:
|
||||
# Last shift was 2 (9am-5pm); next shift 3 is same day (5pm)
|
||||
current_date = last_sequence_date
|
||||
else: # last_shift_index == 3
|
||||
# Last shift was 3 (5pm-1am); next shift 1 is next day (1am)
|
||||
current_date = last_sequence_date + timedelta(days=1)
|
||||
else:
|
||||
current_date = current_date + timedelta(days=1)
|
||||
|
||||
return entries, last_date
|
||||
|
||||
|
||||
def format_schedule_for_frontend(
|
||||
entries: list[ScheduleEntry],
|
||||
) -> dict[str, dict[int, list[SchedulerTest]]]:
|
||||
"""Convert flat ScheduleEntry list to nested format for frontend.
|
||||
|
||||
Returns: {date_string: {shift_index: [SchedulerTest, ...]}, ...}
|
||||
"""
|
||||
schedule: dict[str, dict[int, list[SchedulerTest]]] = {}
|
||||
|
||||
for entry in entries:
|
||||
if entry.scheduled_date not in schedule:
|
||||
schedule[entry.scheduled_date] = {1: [], 2: [], 3: []}
|
||||
|
||||
test = _TESTS_BY_ID.get((entry.test_id, entry.device))
|
||||
if test:
|
||||
schedule[entry.scheduled_date][entry.shift_index].append(test)
|
||||
|
||||
return schedule
|
||||
|
||||
|
||||
def _derive_priority(
|
||||
test: SchedulerTest,
|
||||
top_priority_tests: set[str],
|
||||
lowest_priority_tests: set[str],
|
||||
all_tests: list[SchedulerTest],
|
||||
) -> int:
|
||||
if test.test_id in top_priority_tests:
|
||||
return 1
|
||||
if test.test_id in lowest_priority_tests:
|
||||
return 5
|
||||
return test.priority
|
||||
|
||||
|
||||
def _fit_bundles_to_shifts(
|
||||
bundles: list[TestBundle],
|
||||
shift_sequence: list[tuple[date, int]],
|
||||
tests: dict[TestKey, SchedulerTest],
|
||||
graph_by_test_id: dict[str, set[str]],
|
||||
shift_capacities: dict[tuple[date, int], int],
|
||||
) -> tuple[list[ScheduleEntry], set[TestKey], str | None]:
|
||||
"""Fit bundles into a shift sequence window (e.g., one day or one weekend).
|
||||
|
||||
Returns:
|
||||
- List of ScheduleEntry for placed tests
|
||||
- Set of test_ids that were placed
|
||||
- Last scheduled date
|
||||
|
||||
Bundles are placed in sequence order and may span multiple shifts inside the same window.
|
||||
"""
|
||||
entries: list[ScheduleEntry] = []
|
||||
placed_test_ids: set[TestKey] = set()
|
||||
last_date: str | None = None
|
||||
|
||||
# Track state per shift
|
||||
shift_state: dict[tuple[date, int], dict] = {}
|
||||
for date_shift in shift_sequence:
|
||||
shift_state[date_shift] = {
|
||||
'remaining_minutes': shift_capacities.get(date_shift, 0),
|
||||
'sequence_counter': 1,
|
||||
}
|
||||
|
||||
shift_positions = {date_shift: idx for idx, date_shift in enumerate(shift_sequence)}
|
||||
current_shift_pos = 0
|
||||
window_placed_test_ids: set[str] = set()
|
||||
|
||||
for bundle in bundles:
|
||||
# Skip bundles that are already fully placed.
|
||||
if any(test_id in placed_test_ids for test_id in bundle.test_ids):
|
||||
continue
|
||||
|
||||
if not bundle.test_ids:
|
||||
continue
|
||||
|
||||
# Keep all tests in the same window pairwise-compatible.
|
||||
if window_placed_test_ids:
|
||||
is_compatible_with_window = True
|
||||
bundle_test_ids_only = {test_key[0] for test_key in bundle.test_ids}
|
||||
for placed_test_id in window_placed_test_ids:
|
||||
placed_neighbors = graph_by_test_id.get(placed_test_id, set())
|
||||
if not all(current_test_id in placed_neighbors for current_test_id in bundle_test_ids_only):
|
||||
is_compatible_with_window = False
|
||||
break
|
||||
if not is_compatible_with_window:
|
||||
continue
|
||||
|
||||
# Ensure the whole bundle can still fit somewhere in the remaining window.
|
||||
remaining_window = sum(
|
||||
shift_state[date_shift]['remaining_minutes'] for date_shift in shift_sequence[current_shift_pos:]
|
||||
)
|
||||
if remaining_window < bundle.total_minutes:
|
||||
continue
|
||||
|
||||
bundle_start_shift_pos = current_shift_pos
|
||||
placed_bundle_tests: list[tuple[TestKey, tuple[date, int]]] = []
|
||||
failed = False
|
||||
|
||||
for key in bundle.test_ids:
|
||||
test = tests[key]
|
||||
prev_remaining = 0
|
||||
while current_shift_pos < len(shift_sequence):
|
||||
date_shift = shift_sequence[current_shift_pos]
|
||||
state = shift_state[date_shift]
|
||||
state["remaining_minutes"] += prev_remaining # Add back any leftover from previous shift
|
||||
if state['remaining_minutes'] >= test.estimated_minutes:
|
||||
print(f"remaining minutes for {date_shift}: {state['remaining_minutes']} - placing {key} ({test.estimated_minutes}m)")
|
||||
entries.append(
|
||||
ScheduleEntry(
|
||||
test_id=test.test_id,
|
||||
device=test.device,
|
||||
scheduled_date=date_shift[0].isoformat(),
|
||||
shift_index=date_shift[1],
|
||||
sequence_in_shift=state['sequence_counter'],
|
||||
)
|
||||
)
|
||||
state['sequence_counter'] += 1
|
||||
state['remaining_minutes'] -= test.estimated_minutes
|
||||
placed_test_ids.add(key)
|
||||
placed_bundle_tests.append((key, date_shift))
|
||||
last_date = date_shift[0].isoformat()
|
||||
break
|
||||
|
||||
# Move to the next shift in the sequence and keep the bundle contiguous.
|
||||
current_shift_pos += 1
|
||||
prev_remaining = state['remaining_minutes']
|
||||
if current_shift_pos >= len(shift_sequence):
|
||||
failed = True
|
||||
break
|
||||
|
||||
if failed:
|
||||
break
|
||||
|
||||
if failed:
|
||||
# Roll back any partially placed tests from this bundle.
|
||||
for key, date_shift in reversed(placed_bundle_tests):
|
||||
state = shift_state[date_shift]
|
||||
state['remaining_minutes'] += tests[key].estimated_minutes
|
||||
state['sequence_counter'] -= 1
|
||||
entries.pop()
|
||||
placed_test_ids.discard(key)
|
||||
last_date = None
|
||||
current_shift_pos = bundle_start_shift_pos
|
||||
continue
|
||||
|
||||
if placed_bundle_tests:
|
||||
window_placed_test_ids.update(test_key[0] for test_key in bundle.test_ids)
|
||||
|
||||
return entries, placed_test_ids, last_date
|
||||
|
||||
def _create_bundles(
|
||||
dut_active_test_ids: set[TestKey],
|
||||
ref_active_test_ids: set[TestKey],
|
||||
tests: dict[TestKey, SchedulerTest],
|
||||
) -> list[TestBundle]:
|
||||
"""Create bundles of tests that must run together, sorted by priority tier and efficiency.
|
||||
|
||||
Bundling rules:
|
||||
- P2P with COE pairs: [P2P, COE1, COE2, ...]
|
||||
- P2P without COE: [P2P_RX, P2P_TX] if both active
|
||||
- COE without a linked active P2P: [COE] (standalone bundle)
|
||||
- P3P: individual test (not bundled)
|
||||
|
||||
Returned list is sorted by (priority_tier, total_minutes) to place small/high-priority bundles first.
|
||||
"""
|
||||
bundles: list[TestBundle] = []
|
||||
processed: set[TestKey] = set()
|
||||
|
||||
dut_by_id: dict[str, TestKey] = {test_id: key for test_id, _device in dut_active_test_ids for key in [(test_id, DUT)] if key in dut_active_test_ids}
|
||||
ref_by_id: dict[str, TestKey] = {test_id: key for test_id, _device in ref_active_test_ids for key in [(test_id, REF)] if key in ref_active_test_ids}
|
||||
all_ids = sorted(set(dut_by_id.keys()) | set(ref_by_id.keys()))
|
||||
|
||||
def _active_key(test_id: str, device: str) -> TestKey | None:
|
||||
key = (test_id, device)
|
||||
if key in dut_active_test_ids or key in ref_active_test_ids:
|
||||
return key
|
||||
return None
|
||||
|
||||
for test_id in all_ids:
|
||||
seed_keys = [key for key in (_active_key(test_id, DUT), _active_key(test_id, REF)) if key is not None and key not in processed]
|
||||
if not seed_keys:
|
||||
continue
|
||||
|
||||
representative_key = seed_keys[0]
|
||||
test = tests[representative_key]
|
||||
if test.test_type == "P2P":
|
||||
if test.has_coe_pair:
|
||||
bundle_test_ids: list[TestKey] = []
|
||||
for device in (DUT, REF):
|
||||
base_key = _active_key(test_id, device)
|
||||
if base_key is not None and base_key not in processed:
|
||||
bundle_test_ids.append(base_key)
|
||||
for coe_id in sorted(test.coe_pairing):
|
||||
coe_key = _active_key(coe_id, device)
|
||||
if coe_key is not None and coe_key not in processed:
|
||||
bundle_test_ids.append(coe_key)
|
||||
|
||||
if not bundle_test_ids:
|
||||
continue
|
||||
processed.update(bundle_test_ids)
|
||||
priority_tier = BUNDLE_PRIORITY_P2P_WITH_COE
|
||||
bundles.append(TestBundle(
|
||||
test_ids=bundle_test_ids,
|
||||
priority_tier=priority_tier,
|
||||
total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids)
|
||||
))
|
||||
else:
|
||||
bundle_test_ids: list[TestKey] = []
|
||||
for device in (DUT, REF):
|
||||
base_key = _active_key(test_id, device)
|
||||
if base_key is None or base_key in processed:
|
||||
continue
|
||||
bundle_test_ids.append(base_key)
|
||||
pair_id = _rx_tx_pair_id(test_id, set(k[0] for k in (dut_active_test_ids if device == DUT else ref_active_test_ids)))
|
||||
if pair_id:
|
||||
pair_key = _active_key(pair_id, device)
|
||||
if pair_key is not None and pair_key not in processed and pair_key not in bundle_test_ids:
|
||||
bundle_test_ids.append(pair_key)
|
||||
|
||||
if not bundle_test_ids:
|
||||
continue
|
||||
processed.update(bundle_test_ids)
|
||||
priority_tier = BUNDLE_PRIORITY_P2P_ONLY
|
||||
bundles.append(TestBundle(
|
||||
test_ids=bundle_test_ids,
|
||||
priority_tier=priority_tier,
|
||||
total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids)
|
||||
))
|
||||
elif test.test_type == "P3P":
|
||||
bundle_test_ids = [key for key in (_active_key(test_id, DUT), _active_key(test_id, REF)) if key is not None and key not in processed]
|
||||
if not bundle_test_ids:
|
||||
continue
|
||||
priority_tier = BUNDLE_PRIORITY_P3P
|
||||
bundles.append(TestBundle(
|
||||
test_ids=bundle_test_ids,
|
||||
priority_tier=priority_tier,
|
||||
total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids)
|
||||
))
|
||||
processed.update(bundle_test_ids)
|
||||
|
||||
# Second pass to catch any active tests left uncovered by first-pass grouping.
|
||||
all_active_keys = sorted(dut_active_test_ids | ref_active_test_ids)
|
||||
for key in all_active_keys:
|
||||
if key in processed:
|
||||
continue
|
||||
test = tests[key]
|
||||
priority_tier = BUNDLE_PRIORITY_COE_ONLY
|
||||
bundle_test_ids = [key]
|
||||
bundles.append(TestBundle(
|
||||
test_ids=bundle_test_ids,
|
||||
priority_tier=priority_tier,
|
||||
total_minutes=sum(tests[item].estimated_minutes for item in bundle_test_ids)
|
||||
))
|
||||
processed.update(bundle_test_ids)
|
||||
|
||||
# Sort by priority tier (lower first), then by total minutes (smaller first for efficiency)
|
||||
bundles.sort(key=lambda b: (b.priority_tier, b.total_minutes))
|
||||
return bundles
|
||||
|
||||
|
||||
def _rx_tx_pair_id(test_id: str, active: set[str]) -> str | None:
|
||||
if "RX" in test_id:
|
||||
candidate = test_id.replace("RX", "TX", 1)
|
||||
return candidate if candidate in active else None
|
||||
if "TX" in test_id:
|
||||
candidate = test_id.replace("TX", "RX", 1)
|
||||
return candidate if candidate in active else None
|
||||
return None
|
||||
|
||||
|
||||
def _shift_capacity_for_date(
|
||||
current_date: date,
|
||||
holiday_dates: set[str],
|
||||
start_date_override: str | None,
|
||||
daytime_testing_today: bool,
|
||||
) -> dict[int, int]:
|
||||
iso = current_date.isoformat()
|
||||
if iso in holiday_dates:
|
||||
return {1: 480, 2: 480, 3: 480}
|
||||
|
||||
is_weekend = current_date.weekday() >= 5
|
||||
if is_weekend:
|
||||
return {1: 480, 2: 480, 3: 480}
|
||||
|
||||
if start_date_override and iso == _parse_date(start_date_override).isoformat() and daytime_testing_today:
|
||||
return {1: 480, 2: 480, 3: 480}
|
||||
|
||||
return {1: 600, 2: 0, 3: 420}
|
||||
|
||||
|
||||
def _parse_date(value: str | None) -> date:
|
||||
if not value:
|
||||
return date.today()
|
||||
return datetime.strptime(value, "%Y-%m-%d").date()
|
||||
|
||||
|
||||
def _get_shift_sequence(start_date: date, holiday_dates: set[str]) -> list[tuple[date, int]]:
|
||||
"""Generate the sequence of (date, shift_index) tuples for a scheduling window.
|
||||
|
||||
Rules per design:
|
||||
- Monday-Thursday: [3, 1] (shift 3 today, shift 1 next day) = 16 hours
|
||||
- Friday: [3, 1, 2, 3, 1, 2, 3, 1] (Fri-Mon) = 24 hours continuous
|
||||
- Saturday/Sunday/Holiday weekday: all 3 shifts
|
||||
|
||||
Always starts on shift 3 for weekdays.
|
||||
Returns ordered list of (date, shift_index) pairs.
|
||||
"""
|
||||
iso = start_date.isoformat()
|
||||
is_holiday = iso in holiday_dates
|
||||
weekday = start_date.weekday() # 0=Mon, 4=Fri, 5=Sat, 6=Sun
|
||||
|
||||
shifts: list[tuple[date, int]] = []
|
||||
|
||||
# If holiday on a weekday, treat as weekend (all 3 shifts)
|
||||
if is_holiday and weekday < 5:
|
||||
shifts.append((start_date, 1))
|
||||
shifts.append((start_date, 2))
|
||||
shifts.append((start_date, 3))
|
||||
return shifts
|
||||
|
||||
# Standard weekend day (Sat/Sun)
|
||||
if weekday >= 5:
|
||||
shifts.append((start_date, 1))
|
||||
shifts.append((start_date, 2))
|
||||
shifts.append((start_date, 3))
|
||||
return shifts
|
||||
|
||||
# Friday: 4-day window (Fri-Mon)
|
||||
if weekday == 4:
|
||||
shifts.append((start_date, 3)) # Fri shift 3
|
||||
|
||||
sat = start_date + timedelta(days=1)
|
||||
shifts.append((sat, 1)) # Sat shift 1
|
||||
shifts.append((sat, 2)) # Sat shift 2
|
||||
shifts.append((sat, 3)) # Sat shift 3
|
||||
|
||||
sun = sat + timedelta(days=1)
|
||||
shifts.append((sun, 1)) # Sun shift 1
|
||||
shifts.append((sun, 2)) # Sun shift 2
|
||||
shifts.append((sun, 3)) # Sun shift 3
|
||||
|
||||
mon = sun + timedelta(days=1)
|
||||
shifts.append((mon, 1)) # Mon shift 1
|
||||
|
||||
return shifts
|
||||
|
||||
# Monday-Thursday: 2-shift window
|
||||
shifts.append((start_date, 3)) # Today shift 3
|
||||
next_day = start_date + timedelta(days=1)
|
||||
shifts.append((next_day, 1)) # Tomorrow shift 1
|
||||
|
||||
return shifts
|
||||
|
||||
|
||||
Reference in New Issue
Block a user