knapsack scheduling algorithm
@@ -7,7 +7,7 @@ A test scheduler for the NJTH which compiles a best schedule according to the ru
|
||||
|
||||
- DUT test result directory path, REF test result directory path
|
||||
- Estimated runtime for each type of test for calculations
|
||||
- Rotation definition
|
||||
- Test config definition
|
||||
- Test exclusions
|
||||
- Top priority tests
|
||||
- Allow user to manually override and enter tests that needs to be run tonight
|
||||
@@ -15,23 +15,30 @@ A test scheduler for the NJTH which compiles a best schedule according to the ru
|
||||
|
||||
## Output
|
||||
### Backend
|
||||
- Compile a schedule that would require the lowest amount of time in total according to the following rules and priority:
|
||||
- Compile a schedule that would require the lowest amount of time and least amount of wasted time in total according to the following rules and priority:
|
||||
- **Rules**
|
||||
- Can be run in the same night
|
||||
- Same rotation and same test point P2P/COE tests
|
||||
- COERXBE002: T1F -> STA63, T2A -> STA56, T3E -> STA4, R3
|
||||
- COERXBE003: T1F -> STA63, T2A -> STA56, T3E -> STA4, R3
|
||||
|
||||
- Different rotations but all stations are at same testpoints
|
||||
- R2 P2PRXAC003 5G T1C -> STA4
|
||||
- R1 P2PRXAC012 2G T1C -> STA4
|
||||
|
||||
- Same rotation and same test point P3P tests
|
||||
- Needs to be run back to back
|
||||
- Test Windows
|
||||
- Weekday night time test window: Monday - Thursday
|
||||
- Starts 5PM of that day, ends 9AM the next day
|
||||
- Capcity: 17 hrs (16hrs + 1hr margin)
|
||||
- Ex. Monday 6/29 5PM - Tuesday 6/30 9AM
|
||||
- Weekday day time test window: Monday-Thursday
|
||||
- Starts 9AM - 5PM of that day
|
||||
- Capacity: 8 hrs
|
||||
- Weekend test window: Friday - Sunday
|
||||
- Starts Friday 5PM, ends Monday 9AM
|
||||
- Capcity: 65 hrs (64hrs + 1hr margin)
|
||||
- Holiday test window
|
||||
- All tests in the same test window needs to have the same test config
|
||||
- Tests are performed on one device in each test window, unless overrides by user
|
||||
- Some tests needs to be run back to back
|
||||
- P2P COE pairings
|
||||
- Run P2P first then its COE pairings
|
||||
- P2P up link (RX) and down link (TX)
|
||||
- Ex. P2PRXAX001 and P2PTXAX001
|
||||
- COE P2P bundle
|
||||
- DUT and REF tests
|
||||
- P3P throttled(TH) and unthrottled(UT)
|
||||
- Same tests, different devices(DUT, REF) needs to be run in back to back test windows
|
||||
- Ex. Monday
|
||||
- **Priority**
|
||||
- Default priority: COE P2P pair -> P2P only -> P3P
|
||||
- 1: highest priority (must run tonight)
|
||||
@@ -40,8 +47,6 @@ A test scheduler for the NJTH which compiles a best schedule according to the ru
|
||||
- 4: P3P
|
||||
- 5: lowest priority
|
||||
- Priority 1 and 5 needs to be entered manually
|
||||
- Same test points and same rotation will be run first
|
||||
- Same test points but different will run if there is still remaining testing time for the day
|
||||
- **Calculation**
|
||||
- Use the estimate run time for each type of test to fit as many tests as possible in a shift of testing.
|
||||
- Weekdays are 16hrs, weekends are 24 hours with 1hr margin
|
||||
@@ -137,7 +142,7 @@ build_test_bundles():
|
||||
- Create bundles of tests that needs to be run back to back
|
||||
- Types of test bundles and their order
|
||||
1. Failed tests rerun if exist
|
||||
2. The base is P2P and has COE pairs: P2P on DUT -> COE pairings on DUT -> P2P on REF -> COE pairings on REF
|
||||
2. The P2P and COE pairs: P2P -> COE pairings
|
||||
3. P2P tests without COE pairs: P2PRX on DUT -> P2PTX on DUT -> P2PRX on REF -> P2PTX on REF
|
||||
4. P3P tests (ALL ROT's)
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
@@ -9,9 +9,15 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
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
|
||||
|
||||
# Ensure new_scheduler resolves the same DUT/REF labels as db records.
|
||||
os.environ.setdefault("DUT", db.DEVICE_DUT)
|
||||
os.environ.setdefault("REF", db.DEVICE_REF)
|
||||
|
||||
from scheduler import Scheduler, Test as SchedulerTest
|
||||
from test_config import build_bundle_test_configs, build_config_rows
|
||||
from test_window import get_shift_capacity_for_date, get_shift_sequence, is_off_day
|
||||
from watcher import configure_result_watcher, stop_result_watcher
|
||||
|
||||
|
||||
@@ -21,6 +27,7 @@ DUT = os.getenv("DUT", "CGW453").strip()
|
||||
REF = os.getenv("REF", "CGW452").strip()
|
||||
|
||||
|
||||
|
||||
class LoadTestsRequest(BaseModel):
|
||||
csv_path: str = Field(..., description="Absolute or backend-relative path to target CSV")
|
||||
|
||||
@@ -80,10 +87,179 @@ class SaveHolidaysRequest(BaseModel):
|
||||
dates: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
SHIFT_LABELS = {
|
||||
1: "12AM–9AM",
|
||||
2: "9AM–5PM",
|
||||
3: "5PM–12AM",
|
||||
}
|
||||
|
||||
SHIFT_BOUNDARIES = {
|
||||
1: ((0, 0), (9, 0)),
|
||||
2: ((9, 0), (17, 0)),
|
||||
3: ((17, 0), (24, 0)),
|
||||
}
|
||||
|
||||
|
||||
def _serialize_schedule_row(row: db.ScheduleRow) -> dict[str, Any]:
|
||||
return {
|
||||
"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,
|
||||
"config": row.config,
|
||||
"status": row.status,
|
||||
"priority": row.priority,
|
||||
"estimated_minutes": row.estimated_minutes,
|
||||
}
|
||||
|
||||
|
||||
def _window_segment_key(day: date, shift_index: int) -> str:
|
||||
return f"{day.isoformat()}::shift{shift_index}"
|
||||
|
||||
|
||||
def _combine_date_time(day: date, hour: int, minute: int) -> str:
|
||||
if hour == 24:
|
||||
return datetime.combine(day + timedelta(days=1), datetime.min.time()).isoformat(timespec="minutes")
|
||||
return datetime.combine(day, datetime.min.time()).replace(hour=hour, minute=minute).isoformat(timespec="minutes")
|
||||
|
||||
|
||||
def _window_intersects_week(segments: list[tuple[date, int]], week_start: date, week_end: date) -> bool:
|
||||
for day, _shift_index in segments:
|
||||
if week_start <= day < week_end:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _build_schedule_windows(week_start: date, all_rows: list[db.ScheduleRow], holiday_dates: set[str]) -> list[dict[str, Any]]:
|
||||
if not all_rows:
|
||||
return []
|
||||
|
||||
all_dates = [datetime.strptime(row.scheduled_date, "%Y-%m-%d").date() for row in all_rows]
|
||||
first_scheduled_date = min(all_dates)
|
||||
last_scheduled_date = max(all_dates)
|
||||
week_end = week_start + timedelta(days=7)
|
||||
|
||||
rows_by_segment: dict[tuple[str, int], list[dict[str, Any]]] = {}
|
||||
weekday_shift2_dates: set[str] = set()
|
||||
for row in all_rows:
|
||||
serialized = _serialize_schedule_row(row)
|
||||
rows_by_segment.setdefault((row.scheduled_date, row.shift_index), []).append(serialized)
|
||||
row_date = datetime.strptime(row.scheduled_date, "%Y-%m-%d").date()
|
||||
if row.shift_index == 2 and not is_off_day(row_date, holiday_dates):
|
||||
weekday_shift2_dates.add(row.scheduled_date)
|
||||
|
||||
simulation_start = min(first_scheduled_date, week_start) - timedelta(days=7)
|
||||
while is_off_day(simulation_start, holiday_dates):
|
||||
simulation_start -= timedelta(days=1)
|
||||
|
||||
simulation_end = max(last_scheduled_date, week_end) + timedelta(days=7)
|
||||
current_date = simulation_start
|
||||
windows: list[dict[str, Any]] = []
|
||||
seen_window_ids: set[str] = set()
|
||||
|
||||
while current_date <= simulation_end:
|
||||
current_iso = current_date.isoformat()
|
||||
|
||||
if current_iso in weekday_shift2_dates:
|
||||
segments = get_shift_sequence(current_date, holiday_dates, daytime_shift2_only=True)
|
||||
window_id = f"{current_iso}-shift2"
|
||||
if window_id not in seen_window_ids and _window_intersects_week(segments, week_start, week_end):
|
||||
seen_window_ids.add(window_id)
|
||||
shift_tests = [
|
||||
test
|
||||
for day, shift_index in segments
|
||||
for test in rows_by_segment.get((day.isoformat(), shift_index), [])
|
||||
]
|
||||
available_runtime_minutes = sum(
|
||||
get_shift_capacity_for_date(day, holiday_dates, daytime_shift2_only=True).get(shift_index, 0)
|
||||
for day, shift_index in segments
|
||||
)
|
||||
start_hour, start_minute = SHIFT_BOUNDARIES[segments[0][1]][0]
|
||||
end_hour, end_minute = SHIFT_BOUNDARIES[segments[-1][1]][1]
|
||||
windows.append({
|
||||
"window_id": window_id,
|
||||
"window_type": "daytime",
|
||||
"start_date": segments[0][0].isoformat(),
|
||||
"start_shift_index": segments[0][1],
|
||||
"end_date": segments[-1][0].isoformat(),
|
||||
"end_shift_index": segments[-1][1],
|
||||
"start_at": _combine_date_time(segments[0][0], start_hour, start_minute),
|
||||
"end_at": _combine_date_time(segments[-1][0], end_hour, end_minute),
|
||||
"available_runtime_minutes": available_runtime_minutes,
|
||||
"estimated_runtime_minutes": sum(test["estimated_minutes"] for test in shift_tests),
|
||||
"bundle_test_configs": build_bundle_test_configs(shift_tests),
|
||||
"segments": [
|
||||
{
|
||||
"date": day.isoformat(),
|
||||
"shift_index": shift_index,
|
||||
"label": SHIFT_LABELS[shift_index],
|
||||
"segment_key": _window_segment_key(day, shift_index),
|
||||
}
|
||||
for day, shift_index in segments
|
||||
],
|
||||
"configRows": build_config_rows(shift_tests),
|
||||
"tests": shift_tests,
|
||||
})
|
||||
|
||||
segments = get_shift_sequence(current_date, holiday_dates, daytime_shift2_only=False)
|
||||
last_sequence_date = segments[-1][0]
|
||||
last_shift_index = segments[-1][1]
|
||||
window_kind = "offday" if is_off_day(current_date, holiday_dates) else "overnight"
|
||||
window_id = f"{current_iso}-shift{segments[0][1]}"
|
||||
|
||||
if window_id not in seen_window_ids and _window_intersects_week(segments, week_start, week_end):
|
||||
seen_window_ids.add(window_id)
|
||||
window_tests = [
|
||||
test
|
||||
for day, shift_index in segments
|
||||
for test in rows_by_segment.get((day.isoformat(), shift_index), [])
|
||||
]
|
||||
available_runtime_minutes = sum(
|
||||
get_shift_capacity_for_date(day, holiday_dates, daytime_shift2_only=False).get(shift_index, 0)
|
||||
for day, shift_index in segments
|
||||
)
|
||||
start_hour, start_minute = SHIFT_BOUNDARIES[segments[0][1]][0]
|
||||
end_hour, end_minute = SHIFT_BOUNDARIES[segments[-1][1]][1]
|
||||
windows.append({
|
||||
"window_id": window_id,
|
||||
"window_type": window_kind,
|
||||
"start_date": segments[0][0].isoformat(),
|
||||
"start_shift_index": segments[0][1],
|
||||
"end_date": segments[-1][0].isoformat(),
|
||||
"end_shift_index": segments[-1][1],
|
||||
"start_at": _combine_date_time(segments[0][0], start_hour, start_minute),
|
||||
"end_at": _combine_date_time(segments[-1][0], end_hour, end_minute),
|
||||
"available_runtime_minutes": available_runtime_minutes,
|
||||
"estimated_runtime_minutes": sum(test["estimated_minutes"] for test in window_tests),
|
||||
"bundle_test_configs": build_bundle_test_configs(window_tests),
|
||||
"segments": [
|
||||
{
|
||||
"date": day.isoformat(),
|
||||
"shift_index": shift_index,
|
||||
"label": SHIFT_LABELS[shift_index],
|
||||
"segment_key": _window_segment_key(day, shift_index),
|
||||
}
|
||||
for day, shift_index in segments
|
||||
],
|
||||
"configRows": build_config_rows(window_tests),
|
||||
"tests": window_tests,
|
||||
})
|
||||
|
||||
if last_shift_index in {1, 2}:
|
||||
current_date = last_sequence_date
|
||||
else:
|
||||
current_date = last_sequence_date + timedelta(days=1)
|
||||
|
||||
windows.sort(key=lambda window: (window["start_at"], window["window_id"]))
|
||||
return windows
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(application: FastAPI):
|
||||
db.init_db(DB_PATH)
|
||||
graph.reset_graph_state()
|
||||
settings = db.read_settings(DB_PATH)
|
||||
configure_result_watcher(settings)
|
||||
try:
|
||||
@@ -141,21 +317,17 @@ def load_tests(request: LoadTestsRequest) -> dict[str, Any]:
|
||||
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_and_persist_graph(all_dut_tests, DB_PATH)
|
||||
|
||||
print(f"Loaded {count} tests from {csv_path}, with {len(parsed.warnings)} warnings.")
|
||||
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()})
|
||||
# new_scheduler does not keep global mutable active state in app lifecycle.
|
||||
# Keep endpoint for compatibility with frontend calls.
|
||||
return {"status": "ok", "removed": len(request.test_ids)}
|
||||
|
||||
|
||||
@@ -167,7 +339,6 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail="start_date must be YYYY-MM-DD") from exc
|
||||
|
||||
reset_scheduler_state()
|
||||
stored_tests = db.list_schedulable_tests(DB_PATH, rule=request.rule)
|
||||
if not stored_tests:
|
||||
version = db.create_schedule_version([], DB_PATH)
|
||||
@@ -189,29 +360,36 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]
|
||||
config=t.config,
|
||||
throttled=t.throttled,
|
||||
estimated_minutes=t.estimated_minutes,
|
||||
priority=t.priority,
|
||||
raw_payload=t.raw_payload or {},
|
||||
)
|
||||
for t in stored_tests
|
||||
]
|
||||
|
||||
# DB-backed graph retrieval ensures compile works after restart without manual save/load.
|
||||
all_dut_tests = db.list_tests_for_device(DUT, DB_PATH)
|
||||
graph.get_graph(DB_PATH, all_dut_tests)
|
||||
|
||||
holiday_dates = db.list_holidays(DB_PATH)
|
||||
try:
|
||||
entries, completion_date = compile_schedule(
|
||||
top_priority_pairs: set[tuple[str, str]] = set()
|
||||
for test_id in {item.strip() for item in request.top_priority_tests if item.strip()}:
|
||||
if any(t.test_id == test_id and t.device == DUT for t in stored_tests):
|
||||
top_priority_pairs.add((test_id, DUT))
|
||||
if any(t.test_id == test_id and t.device == REF for t in stored_tests):
|
||||
top_priority_pairs.add((test_id, REF))
|
||||
|
||||
scheduler = Scheduler(
|
||||
tests=scheduler_tests,
|
||||
top_priority_tests=top_priority_pairs,
|
||||
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:
|
||||
|
||||
try:
|
||||
schedule_error = scheduler.compile_schedule()
|
||||
entries = scheduler.get_schedule()
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
completion_date = max((e.scheduled_date for e in entries), default=None)
|
||||
if schedule_error:
|
||||
raise HTTPException(status_code=409, detail=schedule_error)
|
||||
|
||||
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,
|
||||
@@ -252,25 +430,14 @@ def get_schedule_week(start: str | None = None) -> dict[str, Any]:
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail="start must be YYYY-MM-DD") from exc
|
||||
|
||||
week_start_date = datetime.strptime(week_start, "%Y-%m-%d").date()
|
||||
rows = db.get_schedule_week(week_start, DB_PATH)
|
||||
all_rows = db.get_schedule_rows_for_latest_version(DB_PATH)
|
||||
holiday_dates = db.list_holidays(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,
|
||||
"config": row.config,
|
||||
"status": row.status,
|
||||
"priority": row.priority,
|
||||
"estimated_minutes": row.estimated_minutes,
|
||||
}
|
||||
for row in rows
|
||||
],
|
||||
"items": [_serialize_schedule_row(row) for row in rows],
|
||||
"windows": _build_schedule_windows(week_start_date, all_rows, holiday_dates),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,251 +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
|
||||
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
|
||||
P1,1,No,No,R3,P2PRXBE001,5G,LPI,T1F,36,80,-76,UL,STA63,,,,,,,,,,,,
|
||||
P1,1,No,No,R3,P2PRXBE002,5G,LPI,T1B,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,T1B,100,80,-67,UL,STA63,,,,,,,,,,,,
|
||||
P1,2,No,No,R3,P2PRXBE006,5G,LPI,T1C,100,80,-45,UL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
P1,5,No,Yes,R3,P2PRXBE007,5G,LPI,T1F,161,80,-76,UL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,No,R3,P2PRXBE008,5G,LPI,T1B,161,80,-67,UL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,No,R3,P2PRXBE009,5G,LPI,T1C,161,80,-45,UL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
P1,10,No,Yes,R3,P2PRXBE010,5G,LPI,T1F,100,160,-76,UL,STA63,,,,,,,,,,,,
|
||||
P1,10,No,No,R3,P2PRXBE011,5G,LPI,T1B,100,160,-67,UL,STA63,,,,,,,,,,,,
|
||||
P1,10,No,No,R3,P2PRXBE012,5G,LPI,T1C,100,160,-45,UL,STA63,,,,,,,,,,,,
|
||||
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,P2PRXBE014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA63,,,,,,
|
||||
P1,13,No,No,R1,P2PRXBE015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA63,,,,,,
|
||||
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
|
||||
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,P2PRXBE017,6G,LPI,,,,,,,T1B,133,160,-70,UL,STA63,,,,,,
|
||||
P1,15,No,No,R1,P2PRXBE018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,,
|
||||
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
|
||||
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,P2PRXBE020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA63,,,,,,
|
||||
P1,17,No,No,R1,P2PRXBE021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA63,,,,,,
|
||||
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
|
||||
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,P2PRXBE023,6G,SP,,,,,,,T1B,5,320,-70,UL,STA63,,,,,,
|
||||
P1,19,No,No,R1,P2PRXBE024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA63,,,,,,
|
||||
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
|
||||
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,P2PRXBE026,6G,LPI,,,,,,,T1B,133,320,-70,UL,STA63,,,,,,
|
||||
P1,21,No,No,R1,P2PRXBE027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA63,,,,,,
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
P1,1,No,No,R1,P2PRXAX001,5G,LPI,T1F,36,80,-76,UL,STA56,,,,,,,,,,,,
|
||||
P1,1,No,No,R1,P2PRXAX002,5G,LPI,T1B,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,T1B,100,80,-67,UL,STA56,,,,,,,,,,,,
|
||||
P1,2,No,No,R1,P2PRXAX006,5G,LPI,T1C,100,80,-45,UL,STA56,,,,,,,,,,,,
|
||||
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
|
||||
P1,5,No,Yes,R1,P2PRXAX007,5G,LPI,T1F,161,80,-76,UL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,No,R1,P2PRXAX008,5G,LPI,T1B,161,80,-67,UL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,No,R1,P2PRXAX009,5G,LPI,T1C,161,80,-45,UL,STA56,,,,,,,,,,,,
|
||||
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
|
||||
P1,10,No,Yes,R1,P2PRXAX010,5G,LPI,T1F,100,160,-76,UL,STA56,,,,,,,,,,,,
|
||||
P1,10,No,No,R1,P2PRXAX011,5G,LPI,T1B,100,160,-67,UL,STA56,,,,,,,,,,,,
|
||||
P1,10,No,No,R1,P2PRXAX012,5G,LPI,T1C,100,160,-45,UL,STA56,,,,,,,,,,,,
|
||||
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
|
||||
P1,13,No,Yes,R2,P2PRXAX013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA56,,,,,,
|
||||
P1,13,No,No,R2,P2PRXAX014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA56,,,,,,
|
||||
P1,13,No,No,R2,P2PRXAX015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA56,,,,,,
|
||||
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
|
||||
P1,15,No,Yes,R2,P2PRXAX016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA56,,,,,,
|
||||
P1,15,No,No,R2,P2PRXAX017,6G,LPI,,,,,,,T1B,133,160,-70,UL,STA56,,,,,,
|
||||
P1,15,No,No,R2,P2PRXAX018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,,
|
||||
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
|
||||
P1,17,No,Yes,R2,P2PRXAX019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA56,,,,,,
|
||||
P1,17,No,No,R2,P2PRXAX020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA56,,,,,,
|
||||
P1,17,No,No,R2,P2PRXAX021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA56,,,,,,
|
||||
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
|
||||
P1,19,No,Yes,R2,P2PRXAX022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA56,,,,,,
|
||||
P1,19,No,No,R2,P2PRXAX023,6G,SP,,,,,,,T1B,5,320,-70,UL,STA56,,,,,,
|
||||
P1,19,No,No,R2,P2PRXAX024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA56,,,,,,
|
||||
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
|
||||
P1,21,No,Yes,R2,P2PRXAX025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA56,,,,,,
|
||||
P1,21,No,No,R2,P2PRXAX026,6G,LPI,,,,,,,T1B,133,320,-70,UL,STA56,,,,,,
|
||||
P1,21,No,No,R2,P2PRXAX027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA56,,,,,,
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
P1,1,No,No,R2,P2PRXAC001,5G,LPI,T1F,36,80,-76,UL,STA4,,,,,,,,,,,,
|
||||
P1,1,No,No,R2,P2PRXAC002,5G,LPI,T1B,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,T1B,100,80,-67,UL,STA4,,,,,,,,,,,,
|
||||
P1,2,No,No,R2,P2PRXAC006,5G,LPI,T1C,100,80,-45,UL,STA4,,,,,,,,,,,,
|
||||
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
|
||||
P1,5,No,Yes,R2,P2PRXAC007,5G,LPI,T1F,161,80,-76,UL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,No,R2,P2PRXAC008,5G,LPI,T1B,161,80,-67,UL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,No,R2,P2PRXAC009,5G,LPI,T1C,161,80,-45,UL,STA4,,,,,,,,,,,,
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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,P2PTXBE002,5G,LPI,T1B,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,P2PTXBE005,5G,LPI,T1B,100,80,-67,DL,STA63,,,,,,,,,,,,
|
||||
P1,2,No,No,R3,P2PTXBE006,5G,LPI,T1C,100,80,-45,DL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
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,P2PTXBE008,5G,LPI,T1B,161,80,-67,DL,STA63,,,,,,,,,,,,
|
||||
P1,5,No,No,R3,P2PTXBE009,5G,LPI,T1C,161,80,-45,DL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
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,P2PTXBE011,5G,LPI,T1B,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,,,,,,,,,,,,
|
||||
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
|
||||
P1,11,No,Yes,R1,P2PTXBE013,6G,LPI,T1F,5,160,-79,DL,STA63,,,,,,,,,,,,
|
||||
P1,11,No,No,R1,P2PTXBE014,6G,LPI,T1B,5,160,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,11,No,No,R1,P2PTXBE015,6G,LPI,T1C,5,160,-45,DL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
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,P2PTXBE017,6G,LPI,T1B,133,160,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,13,No,No,R1,P2PTXBE018,6G,LPI,T1C,133,160,-45,DL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
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,P2PTXBE020,6G,LPI,T1B,197,160,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,15,No,No,R1,P2PTXBE021,6G,LPI,T1C,197,160,-45,DL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
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,P2PTXBE023,6G,SP,T1B,5,320,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,17,No,No,R1,P2PTXBE024,6G,SP,T1C,5,320,-45,DL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
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,P2PTXBE026,6G,LPI,T1B,133,320,-70,DL,STA63,,,,,,,,,,,,
|
||||
P1,19,No,No,R1,P2PTXBE027,6G,LPI,T1C,133,320,-45,DL,STA63,,,,,,,,,,,,
|
||||
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
|
||||
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,,,,,,,,,,,,
|
||||
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
|
||||
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,,,,,,,,,,,,
|
||||
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
|
||||
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,,,,,,,,,,,,
|
||||
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,R1,P2PTXAX001,5G,LPI,T1F,36,80,-76,DL,STA56,,,,,,,,,,,,
|
||||
P1,1,No,No,R1,P2PTXAX002,5G,LPI,T1B,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,T1B,100,80,-67,DL,STA56,,,,,,,,,,,,
|
||||
P1,2,No,No,R1,P2PTXAX006,5G,LPI,T1C,100,80,-45,DL,STA56,,,,,,,,,,,,
|
||||
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
|
||||
P1,5,No,Yes,R1,P2PTXAX007,5G,LPI,T1F,161,80,-76,DL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,No,R1,P2PTXAX008,5G,LPI,T1B,161,80,-67,DL,STA56,,,,,,,,,,,,
|
||||
P1,5,No,No,R1,P2PTXAX009,5G,LPI,T1C,161,80,-45,DL,STA56,,,,,,,,,,,,
|
||||
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
|
||||
P1,8,No,Yes,R1,P2PTXAX010,5G,LPI,T1F,100,160,-76,DL,STA56,,,,,,,,,,,,
|
||||
P1,8,No,No,R1,P2PTXAX011,5G,LPI,T1B,100,160,-67,DL,STA56,,,,,,,,,,,,
|
||||
P1,8,No,No,R1,P2PTXAX012,5G,LPI,T1C,100,160,-45,DL,STA56,,,,,,,,,,,,
|
||||
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
|
||||
P1,11,No,Yes,R2,P2PTXAX013,6G,LPI,,,,,,,T2A,5,160,-79,DL,STA56,,,,,,
|
||||
P1,11,No,No,R2,P2PTXAX014,6G,LPI,,,,,,,T1B,5,160,-70,DL,STA56,,,,,,
|
||||
P1,11,No,No,R2,P2PTXAX015,6G,LPI,,,,,,,T1C,5,160,-45,DL,STA56,,,,,,
|
||||
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
|
||||
P1,13,No,Yes,R2,P2PTXAX016,6G,LPI,,,,,,,T2A,133,160,-79,DL,STA56,,,,,,
|
||||
P1,13,No,No,R2,P2PTXAX017,6G,LPI,,,,,,,T1B,133,160,-70,DL,STA56,,,,,,
|
||||
P1,13,No,No,R2,P2PTXAX018,6G,LPI,,,,,,,T1C,133,160,-45,DL,STA56,,,,,,
|
||||
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
|
||||
P1,15,No,Yes,R2,P2PTXAX019,6G,LPI,,,,,,,T2A,197,160,-79,DL,STA56,,,,,,
|
||||
P1,15,No,No,R2,P2PTXAX020,6G,LPI,,,,,,,T1B,197,160,-70,DL,STA56,,,,,,
|
||||
P1,15,No,No,R2,P2PTXAX021,6G,LPI,,,,,,,T1C,197,160,-45,DL,STA56,,,,,,
|
||||
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
|
||||
P1,17,No,Yes,R2,P2PTXAX022,6G,SP,,,,,,,T2A,5,320,-79,DL,STA56,,,,,,
|
||||
P1,17,No,No,R2,P2PTXAX023,6G,SP,,,,,,,T1B,5,320,-70,DL,STA56,,,,,,
|
||||
P1,17,No,No,R2,P2PTXAX024,6G,SP,,,,,,,T1C,5,320,-45,DL,STA56,,,,,,
|
||||
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
|
||||
P1,19,No,Yes,R2,P2PTXAX025,6G,LPI,,,,,,,T2A,133,320,-79,DL,STA56,,,,,,
|
||||
P1,19,No,No,R2,P2PTXAX026,6G,LPI,,,,,,,T1B,133,320,-70,DL,STA56,,,,,,
|
||||
P1,19,No,No,R2,P2PTXAX027,6G,LPI,,,,,,,T1C,133,320,-45,DL,STA56,,,,,,
|
||||
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
|
||||
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
|
||||
P2,22,Yes,Yes,R3,COETXAX012,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,DL,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
|
||||
P2,24,Yes,Yes,R3,COETXAX013,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,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
|
||||
P2,26,Yes,Yes,R3,COETXAX014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,DL,STA56
|
||||
P1,1,No,No,R2,P2PTXAC001,5G,LPI,T1F,36,80,-76,DL,STA4,,,,,,,,,,,,
|
||||
P1,1,No,No,R2,P2PTXAC002,5G,LPI,T1B,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,T1B,100,80,-67,DL,STA4,,,,,,,,,,,,
|
||||
P1,2,No,No,R2,P2PTXAC006,5G,LPI,T1C,100,80,-45,DL,STA4,,,,,,,,,,,,
|
||||
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
|
||||
P1,5,No,Yes,R2,P2PTXAC007,5G,LPI,T1F,161,80,-76,DL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,No,R2,P2PTXAC008,5G,LPI,T1B,161,80,-67,DL,STA4,,,,,,,,,,,,
|
||||
P1,5,No,No,R2,P2PTXAC009,5G,LPI,T1C,161,80,-45,DL,STA4,,,,,,,,,,,,
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
@@ -1,81 +1,89 @@
|
||||
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
|
||||
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,R3,P3PTXBETH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA63,T1I,36,80,-76,DL,22Mbps,STA64,T2J,36,80,-76,DL,11Mbps,STA65
|
||||
P2,2,Yes,R3,P3PTXBETH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA63,T1I,100,80,-76,DL,22Mbps,STA64,T2J,100,80,-76,DL,11Mbps,STA65
|
||||
P2,3,Yes,R3,P3PTXBETH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA63,T1I,161,80,-76,DL,22Mbps,STA64,T2J,161,80,-76,DL,11Mbps,STA65
|
||||
P2,4,Yes,R1,P3PTXBETH004,6G,LPI,T2A,5,160,-79,DL,22Mbps,STA63,T1L,5,160,-79,DL,22Mbps,STA64,T2O,5,160,-79,DL,11Mbps,STA65
|
||||
P2,5,Yes,R1,P3PTXBETH005,6G,LPI,T2A,133,160,-79,DL,22Mbps,STA63,T1L,133,160,-79,DL,22Mbps,STA64,T2O,133,160,-79,DL,11Mbps,STA65
|
||||
P2,6,Yes,R1,P3PTXBETH006,6G,LPI,T2A,197,160,-79,DL,22Mbps,STA63,T1L,197,160,-79,DL,22Mbps,STA64,T2O,197,160,-79,DL,11Mbps,STA65
|
||||
P2,7,Yes,R2,P3PTXBETH007,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA63,T1P,6,20,-70,DL,22Mbps,STA64,T2Q,6,20,-70,DL,11Mbps,STA65
|
||||
P2,8,Yes,R1,P3PTXBETH008,6G,SP,T2A,5,320,-79,DL,22Mbps,STA63,T1L,5,320,-79,DL,22Mbps,STA64,T2O,5,320,-79,DL,11Mbps,STA65
|
||||
P2,9,Yes,R1,P3PTXBETH009,6G,LPI,T2A,133,320,-79,DL,22Mbps,STA63,T1L,133,320,-79,DL,22Mbps,STA64,T2O,133,320,-79,DL,11Mbps,STA65
|
||||
P2,1,No,R3,P3PTXBEUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA63,T1I,36,80,-76,DL,Unlimited,STA64,T2J,36,80,-76,DL,Unlimited,STA65
|
||||
P2,2,No,R3,P3PTXBEUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA63,T1I,100,80,-76,DL,Unlimited,STA64,T2J,100,80,-76,DL,Unlimited,STA65
|
||||
P2,3,No,R3,P3PTXBEUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA63,T1I,161,80,-76,DL,Unlimited,STA64,T2J,161,80,-76,DL,Unlimited,STA65
|
||||
P2,4,No,R1,P3PTXBEUT004,6G,LPI,T2A,5,160,-79,DL,Unlimited,STA63,T1L,5,160,-79,DL,Unlimited,STA64,T2O,5,160,-79,DL,Unlimited,STA65
|
||||
P2,5,No,R1,P3PTXBEUT005,6G,LPI,T2A,133,160,-79,DL,Unlimited,STA63,T1L,133,160,-79,DL,Unlimited,STA64,T2O,133,160,-79,DL,Unlimited,STA65
|
||||
P2,6,No,R1,P3PTXBEUT006,6G,LPI,T2A,197,160,-79,DL,Unlimited,STA63,T1L,197,160,-79,DL,Unlimited,STA64,T2O,197,160,-79,DL,Unlimited,STA65
|
||||
P2,7,No,R2,P3PTXBEUT007,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA63,T1P,6,20,-70,DL,Unlimited,STA64,T2Q,6,20,-70,DL,Unlimited,STA65
|
||||
P2,8,No,R1,P3PTXBEUT008,6G,SP,T2A,5,320,-79,DL,Unlimited,STA63,T1L,5,320,-79,DL,Unlimited,STA64,T2O,5,320,-79,DL,Unlimited,STA65
|
||||
P2,9,No,R1,P3PTXBEUT009,6G,LPI,T2A,133,320,-79,DL,Unlimited,STA63,T1L,133,320,-79,DL,Unlimited,STA64,T2O,133,320,-79,DL,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,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,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,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,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,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,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,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
|
||||
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,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,7,Yes,R2,P3PTXBETH007,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA63,T1P,6,20,-70,DL,22Mbps,STA64,T2Q,6,20,-70,DL,11Mbps,STA65
|
||||
P2,7,No,R2,P3PTXBEUT007,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA63,T1P,6,20,-70,DL,Unlimited,STA64,T2Q,6,20,-70,DL,Unlimited,STA65
|
||||
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,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,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,1,Yes,R3,P3PTXBETH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA63,T1I,36,80,-76,DL,22Mbps,STA64,T2J,36,80,-76,DL,11Mbps,STA65
|
||||
P2,2,Yes,R3,P3PTXBETH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA63,T1I,100,80,-76,DL,22Mbps,STA64,T2J,100,80,-76,DL,11Mbps,STA65
|
||||
P2,3,Yes,R3,P3PTXBETH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA63,T1I,161,80,-76,DL,22Mbps,STA64,T2J,161,80,-76,DL,11Mbps,STA65
|
||||
P2,1,No,R3,P3PTXBEUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA63,T1I,36,80,-76,DL,Unlimited,STA64,T2J,36,80,-76,DL,Unlimited,STA65
|
||||
P2,2,No,R3,P3PTXBEUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA63,T1I,100,80,-76,DL,Unlimited,STA64,T2J,100,80,-76,DL,Unlimited,STA65
|
||||
P2,3,No,R3,P3PTXBEUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA63,T1I,161,80,-76,DL,Unlimited,STA64,T2J,161,80,-76,DL,Unlimited,STA65
|
||||
P2,7,Yes,R3,P3PRXAXTH007,2G,LPI,T1D,6,20,-70,UL,22Mbps,STA56,T1P,6,20,-70,UL,22Mbps,STA58,T2Q,6,20,-70,UL,11Mbps,STA59
|
||||
P2,7,No,R3,P3PRXAXUT007,2G,LPI,T1D,6,20,-70,UL,Unlimited,STA56,T1P,6,20,-70,UL,Unlimited,STA58,T2Q,6,20,-70,UL,Unlimited,STA59
|
||||
P2,7,Yes,R3,P3PTXAXTH007,2G,LPI,T1D,6,20,-70,DL,22Mbps,STA56,T1P,6,20,-70,DL,22Mbps,STA58,T2Q,6,20,-70,DL,11Mbps,STA59
|
||||
P2,7,No,R3,P3PTXAXUT007,2G,LPI,T1D,6,20,-70,DL,Unlimited,STA56,T1P,6,20,-70,DL,Unlimited,STA58,T2Q,6,20,-70,DL,Unlimited,STA59
|
||||
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
|
||||
|
@@ -6,7 +6,7 @@ from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
from graph import build_station_testpoint_map
|
||||
from test_config import serialize_station_testpoint_map
|
||||
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parent
|
||||
@@ -123,13 +123,6 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
|
||||
minutes INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS graph_cache (
|
||||
name TEXT PRIMARY KEY,
|
||||
payload_json TEXT NOT NULL,
|
||||
test_count INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rerun_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
detected_date TEXT NOT NULL,
|
||||
@@ -166,14 +159,38 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
|
||||
|
||||
def _serialize_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> str:
|
||||
"""Compute and serialize the station-to-testpoint map from a test config."""
|
||||
station_map = build_station_testpoint_map(config)
|
||||
return json.dumps(station_map)
|
||||
return serialize_station_testpoint_map(config)
|
||||
|
||||
|
||||
def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> int:
|
||||
if not records:
|
||||
return 0
|
||||
|
||||
values: list[tuple[Any, ...]] = []
|
||||
for r in records:
|
||||
station_testpoint_map = _serialize_station_testpoint_map(r.config)
|
||||
|
||||
values.append(
|
||||
(
|
||||
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,
|
||||
int(r.throttled),
|
||||
r.estimated_minutes,
|
||||
r.status,
|
||||
int(r.excluded),
|
||||
json.dumps(r.raw_payload or {}),
|
||||
station_testpoint_map,
|
||||
)
|
||||
)
|
||||
|
||||
with get_connection(db_path) as conn:
|
||||
conn.executemany(
|
||||
"""
|
||||
@@ -200,27 +217,7 @@ def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> in
|
||||
station_testpoint_map = excluded.station_testpoint_map,
|
||||
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,
|
||||
int(r.throttled),
|
||||
r.estimated_minutes,
|
||||
r.status,
|
||||
int(r.excluded),
|
||||
json.dumps(r.raw_payload or {}),
|
||||
_serialize_station_testpoint_map(r.config),
|
||||
)
|
||||
for r in records
|
||||
],
|
||||
values,
|
||||
)
|
||||
return len(records)
|
||||
|
||||
@@ -253,52 +250,6 @@ def read_settings(db_path: str | Path = DB_PATH) -> dict[str, Any]:
|
||||
return {row["key"]: json.loads(row["value_json"]) for row in rows}
|
||||
|
||||
|
||||
def save_graph_cache(
|
||||
name: str,
|
||||
payload: dict[str, list[str]],
|
||||
test_count: int,
|
||||
db_path: str | Path = DB_PATH,
|
||||
) -> None:
|
||||
with get_connection(db_path) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO graph_cache(name, payload_json, test_count, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
payload_json = excluded.payload_json,
|
||||
test_count = excluded.test_count,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
(name, json.dumps(payload), int(test_count)),
|
||||
)
|
||||
|
||||
|
||||
def load_graph_cache(name: str, db_path: str | Path = DB_PATH) -> dict[str, Any] | None:
|
||||
with get_connection(db_path) as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT payload_json, test_count, updated_at
|
||||
FROM graph_cache
|
||||
WHERE name = ?
|
||||
""",
|
||||
(name,),
|
||||
).fetchone()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"payload": json.loads(row["payload_json"] or "{}"),
|
||||
"test_count": int(row["test_count"]),
|
||||
"updated_at": row["updated_at"],
|
||||
}
|
||||
|
||||
|
||||
def delete_graph_cache(name: str, db_path: str | Path = DB_PATH) -> None:
|
||||
with get_connection(db_path) as conn:
|
||||
conn.execute("DELETE FROM graph_cache WHERE name = ?", (name,))
|
||||
|
||||
|
||||
def _parse_rule_tokens(rule: str | None) -> list[str]:
|
||||
if not rule:
|
||||
return []
|
||||
@@ -345,7 +296,7 @@ def _match_any_band_value(record: TestRecord, key: str, token_suffix: str) -> bo
|
||||
|
||||
target_num = _extract_signed_int(token_suffix)
|
||||
target_compact = _normalize_compact(token_suffix)
|
||||
keys = ("Station 1", "Station 2", "Station 3") if record.test_type == "P3P" else ("5G", "6G", "2G")
|
||||
keys = ("STATION 1", "STATION 2", "STATION 3") if record.test_type == "P3P" else ("5G", "6G", "2G")
|
||||
|
||||
for band in keys:
|
||||
entry = _band_entry(record.config, band)
|
||||
@@ -579,6 +530,56 @@ def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[Sc
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_schedule_rows_for_latest_version(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.config_json,
|
||||
t.status,
|
||||
t.priority,
|
||||
t.estimated_minutes
|
||||
FROM schedules s
|
||||
JOIN tests t ON t.test_id = s.test_id AND t.device = s.device
|
||||
WHERE s.schedule_version = ?
|
||||
ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift
|
||||
""",
|
||||
(latest,),
|
||||
).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"],
|
||||
config=json.loads(row["config_json"] or "{}"),
|
||||
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
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
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]] = {}
|
||||
DUT = os.getenv("DUT", "CGW453").strip()
|
||||
APP_ROOT = Path(__file__).resolve().parent
|
||||
DB_PATH = APP_ROOT / "scheduler.db"
|
||||
GRAPH_CACHE_NAME = "dut_compatibility"
|
||||
|
||||
|
||||
def reset_graph_state() -> None:
|
||||
global _TESTS_BY_ID, _GRAPH
|
||||
_TESTS_BY_ID = {}
|
||||
_GRAPH = {}
|
||||
|
||||
|
||||
def _get_station_map(test: Any) -> dict[str, str]:
|
||||
"""Get station testpoint map from test record or compute it from config."""
|
||||
# Try to get from serialized map first (cached from DB)
|
||||
if hasattr(test, 'station_testpoint_map') and test.station_testpoint_map:
|
||||
try:
|
||||
return json.loads(test.station_testpoint_map)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
# Safeguard: return empty map
|
||||
return {}
|
||||
|
||||
|
||||
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 serialize_graph(graph: dict[str, set[str]]) -> dict[str, list[str]]:
|
||||
return {str(test_id): sorted(str(neighbor) for neighbor in neighbors) for test_id, neighbors in graph.items()}
|
||||
|
||||
|
||||
def deserialize_graph(data: dict[str, list[str]]) -> dict[str, set[str]]:
|
||||
deserialized: dict[str, set[str]] = {}
|
||||
for test_id, neighbors in (data or {}).items():
|
||||
deserialized[str(test_id)] = {str(neighbor) for neighbor in (neighbors or [])}
|
||||
return deserialized
|
||||
|
||||
|
||||
def _persist_graph(db_path: str | Path = DB_PATH) -> None:
|
||||
import db as db_module
|
||||
db_module.save_graph_cache(
|
||||
name=GRAPH_CACHE_NAME,
|
||||
payload=serialize_graph(_GRAPH),
|
||||
test_count=len(_GRAPH),
|
||||
db_path=db_path,
|
||||
)
|
||||
|
||||
|
||||
def _load_graph_from_db(db_path: str | Path = DB_PATH) -> bool:
|
||||
global _GRAPH
|
||||
import db as db_module
|
||||
cached = db_module.load_graph_cache(GRAPH_CACHE_NAME, db_path)
|
||||
if cached is None:
|
||||
return False
|
||||
_GRAPH = deserialize_graph(cached.get("payload") or {})
|
||||
return True
|
||||
|
||||
|
||||
def build_and_persist_graph(tests: list[Any], db_path: str | Path = DB_PATH) -> dict[str, set[str]]:
|
||||
graph = build_graph_once(tests)
|
||||
_persist_graph(db_path)
|
||||
return graph
|
||||
|
||||
|
||||
def get_graph(
|
||||
db_path: str | Path = DB_PATH,
|
||||
dut_tests: list[Any] | None = None,
|
||||
) -> dict[str, set[str]]:
|
||||
if _GRAPH:
|
||||
return _GRAPH
|
||||
|
||||
if _load_graph_from_db(db_path):
|
||||
return _GRAPH
|
||||
|
||||
if dut_tests is None:
|
||||
import db as db_module
|
||||
dut_tests = db_module.list_tests_for_device(DUT, db_path)
|
||||
|
||||
build_and_persist_graph(dut_tests or [], db_path)
|
||||
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:
|
||||
station_map_a = _get_station_map(a)
|
||||
station_map_b = _get_station_map(b)
|
||||
if _same_station_to_testpoint(station_map_a, station_map_b, a, b) and _same_testpoint_to_station(station_map_a, station_map_b, a, b):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _same_station_to_testpoint(
|
||||
station_map_a: dict[str, str],
|
||||
station_map_b: dict[str, str], test_a: Any, test_b: Any
|
||||
) -> bool:
|
||||
overlap_stations = set(station_map_a.keys()) & set(station_map_b.keys())
|
||||
for station in overlap_stations:
|
||||
if station_map_a[station] != station_map_b[station]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _same_testpoint_to_station(
|
||||
station_map_a: dict[str, str],
|
||||
station_map_b: dict[str, str], test_a: Any, test_b: Any
|
||||
) -> bool:
|
||||
overlap_testpoints = set(station_map_a.values()) & set(station_map_b.values())
|
||||
for testpoint in overlap_testpoints:
|
||||
stations_a = {s for s, t in station_map_a.items() if t == testpoint}
|
||||
stations_b = {s for s, t in station_map_b.items() if t == testpoint}
|
||||
if stations_a != stations_b:
|
||||
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] = {}
|
||||
|
||||
def _add_entry(entry: dict[str, str | None]) -> None:
|
||||
testpoint = _norm(entry.get("test_point"))
|
||||
sta_raw = _norm(entry.get("sta"))
|
||||
if not testpoint or not sta_raw:
|
||||
return
|
||||
for sta in sta_raw.split(","):
|
||||
sta_clean = _norm(sta)
|
||||
if sta_clean:
|
||||
station_to_testpoint[sta_clean] = testpoint
|
||||
|
||||
for band in ("5G", "6G", "2G"):
|
||||
entry = config.get(band) or {}
|
||||
_add_entry(entry)
|
||||
|
||||
for station_key in ("Station 1", "Station 2", "Station 3"):
|
||||
entry = config.get(station_key) or {}
|
||||
_add_entry(entry)
|
||||
|
||||
return station_to_testpoint
|
||||
|
||||
|
||||
|
||||
def _norm(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return " ".join(str(value).strip().upper().split())
|
||||
@@ -21,24 +21,24 @@ P2P_COE_REQUIRED_COLUMNS = [
|
||||
"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",
|
||||
"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",
|
||||
]
|
||||
|
||||
P3P_REQUIRED_COLUMNS = [
|
||||
@@ -49,27 +49,27 @@ P3P_REQUIRED_COLUMNS = [
|
||||
"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",
|
||||
"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",
|
||||
]
|
||||
|
||||
RUNTIME_DEFAULTS = {
|
||||
@@ -230,7 +230,6 @@ def _detect_csv_format(normalized_fieldnames: set[str]) -> str:
|
||||
f"{', '.join(p2p_coe_missing)}; missing columns for P3P format: {', '.join(p3p_missing)}"
|
||||
)
|
||||
|
||||
|
||||
def parse_target_csv(
|
||||
csv_path: str | Path | list[str | Path] | tuple[str | Path, ...],
|
||||
smb_credentials: dict[str, Any] | None = None,
|
||||
@@ -476,11 +475,20 @@ def _victim_band_signature(row: dict[str, str]) -> tuple[str, ...] | None:
|
||||
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"))
|
||||
# Source CSVs may use either "5G_Test Point" or "5G Test Point" style headers.
|
||||
prefixes = (f"{band}_", f"{band} ")
|
||||
|
||||
def _pick_value(suffix: str) -> str:
|
||||
for prefix in prefixes:
|
||||
value = _row_get(row, f"{prefix}{suffix}")
|
||||
if _normalize_value(value):
|
||||
return _normalize_value(value)
|
||||
return ""
|
||||
|
||||
test_point = _pick_value("Test Point")
|
||||
channel = _pick_value("Channel")
|
||||
rssi = _pick_value("RSSI")
|
||||
bandwidth = _pick_value("Bandwidth")
|
||||
|
||||
# Direction is intentionally ignored for COE pairing matching.
|
||||
if not all([test_point, channel, rssi, bandwidth]):
|
||||
@@ -515,61 +523,61 @@ def _normalize_yes_no(value: str | None) -> bool:
|
||||
def _build_legacy_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")),
|
||||
"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")),
|
||||
"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")),
|
||||
"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 _build_p3p_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
|
||||
return {
|
||||
"Station 1": {
|
||||
"test_point": _empty_to_none(_row_get(row, "Station 1 Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "Station 1 Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "Station 1 Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "Station 1 RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "Station 1 Direction")),
|
||||
"rate": _empty_to_none(_row_get(row, "Station 1 Rate")),
|
||||
"sta": _empty_to_none(_row_get(row, "Station 1 STA")),
|
||||
"STATION1": {
|
||||
"test_point": _empty_to_none(_row_get(row, "STATION 1_Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "STATION 1_Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "STATION 1_Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "STATION 1_RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "STATION 1_Direction")),
|
||||
"rate": _empty_to_none(_row_get(row, "STATION 1_Rate")),
|
||||
"sta": _empty_to_none(_row_get(row, "STATION 1_STA")),
|
||||
},
|
||||
"Station 2": {
|
||||
"test_point": _empty_to_none(_row_get(row, "Station 2 Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "Station 2 Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "Station 2 Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "Station 2 RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "Station 2 Direction")),
|
||||
"rate": _empty_to_none(_row_get(row, "Station 2 Rate")),
|
||||
"sta": _empty_to_none(_row_get(row, "Station 2 STA")),
|
||||
"STATION2": {
|
||||
"test_point": _empty_to_none(_row_get(row, "STATION 2_Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "STATION 2_Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "STATION 2_Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "STATION 2_RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "STATION 2_Direction")),
|
||||
"rate": _empty_to_none(_row_get(row, "STATION 2_Rate")),
|
||||
"sta": _empty_to_none(_row_get(row, "STATION 2_STA")),
|
||||
},
|
||||
"Station 3": {
|
||||
"test_point": _empty_to_none(_row_get(row, "Station 3 Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "Station 3 Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "Station 3 Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "Station 3 RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "Station 3 Direction")),
|
||||
"rate": _empty_to_none(_row_get(row, "Station 3 Rate")),
|
||||
"sta": _empty_to_none(_row_get(row, "Station 3 STA")),
|
||||
"STATION3": {
|
||||
"test_point": _empty_to_none(_row_get(row, "STATION 3_Test Point")),
|
||||
"channel": _empty_to_none(_row_get(row, "STATION 3_Channel")),
|
||||
"bandwidth": _empty_to_none(_row_get(row, "STATION 3_Bandwidth")),
|
||||
"rssi": _empty_to_none(_row_get(row, "STATION 3_RSSI")),
|
||||
"direction": _empty_to_none(_row_get(row, "STATION 3_Direction")),
|
||||
"rate": _empty_to_none(_row_get(row, "STATION 3_Rate")),
|
||||
"sta": _empty_to_none(_row_get(row, "STATION 3_STA")),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,37 +2,12 @@ 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]]
|
||||
throttled: bool
|
||||
estimated_minutes: int
|
||||
priority: int
|
||||
raw_payload: dict[str, Any]
|
||||
from datetime import date
|
||||
from test_window import get_shift_sequence_with_capacity, next_window_start_date
|
||||
from test_bundle import Test, TestBundle, build_test_bundles, bundle_pair_lookup
|
||||
|
||||
DUT = (os.getenv("DUT") or "DUT").strip()
|
||||
REF = (os.getenv("REF") or "REF").strip()
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScheduleEntry:
|
||||
@@ -43,575 +18,276 @@ class ScheduleEntry:
|
||||
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
|
||||
@dataclass
|
||||
class ScheduleWindow:
|
||||
index: int
|
||||
shifts: list[tuple[date, int]]
|
||||
capacity_minutes: int
|
||||
remaining_minutes: int
|
||||
assigned_device: str | None = None
|
||||
assigned_config: tuple[str, ...] | None = None
|
||||
|
||||
|
||||
# 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] = {}
|
||||
class Scheduler:
|
||||
def __init__(
|
||||
self,
|
||||
tests: list[Test],
|
||||
top_priority_tests: set[tuple[str, str]],
|
||||
start_date: str | None = None,
|
||||
holiday_dates: set[str] = set(),
|
||||
daytime_testing_today: bool = False,
|
||||
priority_weight: int = 100,
|
||||
):
|
||||
self.top_priority_tests = top_priority_tests
|
||||
self.tests = tests
|
||||
self.active_dut: dict[str, Test] = {test.test_id: test for test in tests if test.device == DUT}
|
||||
self.active_ref: dict[str, Test] = {test.test_id: test for test in tests if test.device == REF}
|
||||
self.start_date = date.fromisoformat(start_date) if start_date else date.today()
|
||||
self.holiday_dates = holiday_dates
|
||||
self.daytime_testing_today = daytime_testing_today
|
||||
self.priority_weight = priority_weight
|
||||
self.pending_dut_mirror: list[TestBundle] = []
|
||||
self.pending_ref_mirror: list[TestBundle] = []
|
||||
self.schedule: list[ScheduleEntry] = []
|
||||
self.scheduled_bundle_keys: set[tuple[int, str]] = set()
|
||||
self.scheduled_test_ids: set[str] = set()
|
||||
|
||||
def compile_schedule(self) -> str | None:
|
||||
bundles = build_test_bundles(self.active_dut, self.active_ref, self.top_priority_tests)
|
||||
if not bundles:
|
||||
return f"Error: No test bundles could be created from the provided tests."
|
||||
|
||||
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
|
||||
|
||||
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] = []
|
||||
|
||||
window_start_date = _parse_date(start_date)
|
||||
current_date = window_start_date
|
||||
last_date: str | None = None
|
||||
daytime_shift2_window_pending = daytime_testing_today
|
||||
date_device_lock: dict[str, str] = {}
|
||||
|
||||
while dut_active_priority or ref_active_priority:
|
||||
is_special_daytime_shift2_window = (
|
||||
daytime_shift2_window_pending
|
||||
and current_date == window_start_date
|
||||
and current_date.weekday() < 5
|
||||
and current_date.isoformat() not in holiday_dates
|
||||
)
|
||||
|
||||
# Get the shift sequence for current date (respects Mon/Fri/weekend rules)
|
||||
shift_sequence = _get_shift_sequence(
|
||||
current_date,
|
||||
holiday_dates,
|
||||
daytime_shift2_only=is_special_daytime_shift2_window,
|
||||
)
|
||||
|
||||
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, top_priority_tests)
|
||||
shift_capacities = {
|
||||
(date_obj, shift_idx): _shift_capacity_for_date(
|
||||
current_date=date_obj,
|
||||
holiday_dates=holiday_dates,
|
||||
daytime_shift2_only=is_special_daytime_shift2_window and date_obj == current_date,
|
||||
).get(shift_idx, 0)
|
||||
for date_obj, shift_idx in shift_sequence
|
||||
}
|
||||
|
||||
forced_device: str | None = None
|
||||
for date_obj, _shift_idx in shift_sequence:
|
||||
locked_device = date_device_lock.get(date_obj.isoformat())
|
||||
if locked_device is None:
|
||||
continue
|
||||
if forced_device is None:
|
||||
forced_device = locked_device
|
||||
elif forced_device != locked_device:
|
||||
raise ValueError(f"Conflicting device locks in shift sequence for {date_obj.isoformat()}")
|
||||
|
||||
placed_entries, placed_test_ids, placed_last_date, window_device = _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,
|
||||
forced_device=forced_device,
|
||||
)
|
||||
entries.extend(placed_entries)
|
||||
if window_device is not None:
|
||||
for date_obj, _shift_idx in shift_sequence:
|
||||
date_device_lock.setdefault(date_obj.isoformat(), window_device)
|
||||
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
|
||||
|
||||
# Daytime testing special handling applies only to the first scheduling window.
|
||||
daytime_shift2_window_pending = False
|
||||
|
||||
# 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],
|
||||
forced_device: str | None = None,
|
||||
) -> tuple[list[ScheduleEntry], set[TestKey], str | None, 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
|
||||
- Device used for this window, if any bundle was placed
|
||||
|
||||
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
|
||||
window_device: 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()
|
||||
window_placed_devices: set[str] = set()
|
||||
active_dut_bundles = [b for b in bundles if b.device == DUT]
|
||||
active_ref_bundles = [b for b in bundles if b.device == REF]
|
||||
|
||||
# Group by individual TC values (from config tuples)
|
||||
all_tcs = 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
|
||||
all_tcs.update(bundle.config if bundle.config else [None])
|
||||
|
||||
if not bundle.test_ids:
|
||||
continue
|
||||
# Create dict: TC -> bundles supporting that TC
|
||||
bundles_by_tc: dict = {}
|
||||
# Sort with None last
|
||||
sorted_tcs = sorted([tc for tc in all_tcs if tc is not None]) + ([None] if None in all_tcs else [])
|
||||
for tc in sorted_tcs:
|
||||
if tc is None:
|
||||
bundles_by_tc[tc] = [b for b in bundles if not b.config]
|
||||
else:
|
||||
bundles_by_tc[tc] = [b for b in bundles if tc in b.config]
|
||||
# Sort by priority, then index
|
||||
bundles_by_tc[tc].sort(key=lambda b: (b.priority, b.index))
|
||||
|
||||
bundle_devices = {test_key[1] for test_key in bundle.test_ids}
|
||||
if len(bundle_devices) > 1:
|
||||
# A bundle cannot span devices because each window is single-device only.
|
||||
continue
|
||||
if forced_device is not None and forced_device not in bundle_devices:
|
||||
continue
|
||||
window_index = 0
|
||||
cursor_date = self.start_date
|
||||
|
||||
# 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
|
||||
|
||||
if window_placed_devices and not bundle_devices.issubset(window_placed_devices):
|
||||
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:]
|
||||
# Sort TC keys by bundle count (largest first); tie-break by TC name.
|
||||
sorted_tc_items = sorted(
|
||||
[(tc, bundles_by_tc[tc]) for tc in bundles_by_tc if tc is not None],
|
||||
key=lambda x: (-len(x[1]), x[0]),
|
||||
)
|
||||
if remaining_window < bundle.total_minutes:
|
||||
continue
|
||||
if None in bundles_by_tc:
|
||||
sorted_tc_items.append((None, bundles_by_tc[None]))
|
||||
|
||||
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'],
|
||||
for tc, tc_bundles in sorted_tc_items:
|
||||
window_device = DUT
|
||||
dut_unscheduled = [b for b in tc_bundles if b.device == DUT]
|
||||
ref_unscheduled = [b for b in tc_bundles if b.device == REF]
|
||||
|
||||
while dut_unscheduled or ref_unscheduled:
|
||||
active_unscheduled = dut_unscheduled if window_device == DUT else ref_unscheduled
|
||||
if len(active_unscheduled) == 0:
|
||||
# If no unscheduled bundles for the current device, switch to the other device
|
||||
window_device = REF if window_device == DUT else DUT
|
||||
active_unscheduled = dut_unscheduled if window_device == DUT else ref_unscheduled
|
||||
|
||||
shifts, capacity = get_shift_sequence_with_capacity(cursor_date, self.holiday_dates, self.daytime_testing_today)
|
||||
|
||||
# Evaluate both devices without mutating queue state, then commit once.
|
||||
mirrored_bundles, knapsack_bundles, remaining_time = self._select_bundles(
|
||||
window_device,
|
||||
capacity,
|
||||
active_unscheduled,
|
||||
tc,
|
||||
mutate=True,
|
||||
)
|
||||
selected_bundles = mirrored_bundles + knapsack_bundles
|
||||
if len(selected_bundles) == 0:
|
||||
window_device = REF if window_device == DUT else DUT
|
||||
mirrored_bundles, knapsack_bundles, remaining_time = self._select_bundles(
|
||||
window_device,
|
||||
capacity,
|
||||
active_unscheduled,
|
||||
tc,
|
||||
mutate=True,
|
||||
)
|
||||
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()
|
||||
selected_bundles = mirrored_bundles + knapsack_bundles
|
||||
if len(selected_bundles) == 0:
|
||||
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
|
||||
# Create a mirror of the other device for the next window
|
||||
bundle_pairs = bundle_pair_lookup(knapsack_bundles, dut_unscheduled, ref_unscheduled)
|
||||
|
||||
if failed:
|
||||
break
|
||||
if window_device == DUT:
|
||||
pending_target = self.pending_ref_mirror
|
||||
else:
|
||||
pending_target = self.pending_dut_mirror
|
||||
|
||||
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
|
||||
existing_pending = {(b.index, b.device) for b in pending_target}
|
||||
for pair_bundle in bundle_pairs:
|
||||
pair_key = (pair_bundle.index, pair_bundle.device)
|
||||
if pair_key in self.scheduled_bundle_keys or pair_key in existing_pending:
|
||||
continue
|
||||
pending_target.append(pair_bundle)
|
||||
existing_pending.add(pair_key)
|
||||
|
||||
if placed_bundle_tests:
|
||||
window_placed_test_ids.update(test_key[0] for test_key in bundle.test_ids)
|
||||
window_placed_devices.update(bundle_devices)
|
||||
if window_device is None:
|
||||
window_device = next(iter(bundle_devices))
|
||||
|
||||
return entries, placed_test_ids, last_date, window_device
|
||||
window = ScheduleWindow(
|
||||
index=window_index,
|
||||
shifts=shifts,
|
||||
capacity_minutes=capacity,
|
||||
remaining_minutes=remaining_time,
|
||||
assigned_config=tc,
|
||||
assigned_device=window_device,
|
||||
)
|
||||
|
||||
def _create_bundles(
|
||||
dut_active_test_ids: set[TestKey],
|
||||
ref_active_test_ids: set[TestKey],
|
||||
tests: dict[TestKey, SchedulerTest],
|
||||
top_priority_tests: set[str] | None = None,
|
||||
self._place_bundles_in_window(window, selected_bundles)
|
||||
|
||||
# Mark bundles as scheduled and remove from ALL TC buckets
|
||||
selected_keys = {(b.index, b.device) for b in selected_bundles}
|
||||
|
||||
# Remove scheduled bundles from ALL TC buckets globally
|
||||
for all_tc in bundles_by_tc:
|
||||
bundles_by_tc[all_tc] = [b for b in bundles_by_tc[all_tc] if (b.index, b.device) not in selected_keys]
|
||||
|
||||
# Rebuild current TC unscheduled lists
|
||||
dut_unscheduled = [b for b in bundles_by_tc[tc] if b.device == DUT]
|
||||
ref_unscheduled = [b for b in bundles_by_tc[tc] if b.device == REF]
|
||||
|
||||
window_index += 1
|
||||
window_device = REF if window_device == DUT else DUT
|
||||
cursor_date = next_window_start_date(shifts)
|
||||
|
||||
return None
|
||||
|
||||
def _select_bundles(
|
||||
self,
|
||||
device: str,
|
||||
capacity: int,
|
||||
unscheduled: list[TestBundle],
|
||||
tc,
|
||||
mutate: bool,
|
||||
) -> tuple[list[TestBundle], list[TestBundle], int]:
|
||||
# Consume pending mirrored bundles only while there is room.
|
||||
pending = self.pending_dut_mirror if device == DUT else self.pending_ref_mirror
|
||||
mirrored_bundles: list[TestBundle] = []
|
||||
still_pending: list[TestBundle] = [] # Bundles that couldn't fit in the remaining capacity
|
||||
remaining_capacity = capacity
|
||||
|
||||
for bundle in pending:
|
||||
bundle_key = (bundle.index, bundle.device)
|
||||
if bundle_key in self.scheduled_bundle_keys:
|
||||
continue
|
||||
if bundle.device != device or (tc is not None and tc not in bundle.config) or (tc is None and bundle.config):
|
||||
still_pending.append(bundle)
|
||||
continue
|
||||
if bundle.total_minutes <= remaining_capacity:
|
||||
mirrored_bundles.append(bundle)
|
||||
remaining_capacity -= bundle.total_minutes
|
||||
else:
|
||||
still_pending.append(bundle)
|
||||
|
||||
if mutate:
|
||||
pending[:] = still_pending
|
||||
|
||||
# Run knapsack selection for remaining capacity
|
||||
candidates = [
|
||||
b
|
||||
for b in unscheduled
|
||||
if b not in mirrored_bundles
|
||||
and b.device == device
|
||||
and (b.index, b.device) not in self.scheduled_bundle_keys
|
||||
]
|
||||
knapsack_bundles = self._knapsack_select(remaining_capacity, candidates)
|
||||
remaining_capacity -= sum(bundle.total_minutes for bundle in knapsack_bundles)
|
||||
|
||||
print(f"Selected {len(mirrored_bundles)} mirrored bundles and {len(knapsack_bundles)} knapsack bundles for device {device} with remaining capacity {remaining_capacity} minutes.")
|
||||
return mirrored_bundles, knapsack_bundles, remaining_capacity
|
||||
|
||||
def get_schedule(self) -> list[ScheduleEntry]:
|
||||
return self.schedule
|
||||
|
||||
def _knapsack_select(
|
||||
self,
|
||||
capacity: int,
|
||||
candidates: list[TestBundle],
|
||||
) -> list[TestBundle]:
|
||||
"""Create bundles of tests that must run together, sorted by priority tier and efficiency.
|
||||
if capacity <= 0 or not candidates:
|
||||
return []
|
||||
|
||||
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)
|
||||
# Weights are total minutes of each bundle
|
||||
weights = [bundle.total_minutes for bundle in candidates]
|
||||
# Values are based on priority, lower priority number means higher value
|
||||
values = [self.priority_weight - bundle.priority for bundle in candidates]
|
||||
|
||||
Returned list is sorted by (priority_tier, total_minutes) to place small/high-priority bundles first.
|
||||
"""
|
||||
if top_priority_tests is None:
|
||||
top_priority_tests = set()
|
||||
|
||||
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:
|
||||
for device in (DUT, REF):
|
||||
bundle_test_ids: list[TestKey] = []
|
||||
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)
|
||||
# Check if any test in bundle is top priority
|
||||
bundle_test_ids_only = {key[0] for key in bundle_test_ids}
|
||||
if bundle_test_ids_only & top_priority_tests:
|
||||
priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0)
|
||||
# Implement dynamic programming knapsack algorithm to select bundles
|
||||
n = len(candidates)
|
||||
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
|
||||
for i in range(1, n + 1):
|
||||
for w in range(capacity + 1):
|
||||
if weights[i - 1] <= w:
|
||||
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1])
|
||||
else:
|
||||
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:
|
||||
for device in (DUT, REF):
|
||||
bundle_test_ids: list[TestKey] = []
|
||||
base_key = _active_key(test_id, device)
|
||||
if base_key is None or base_key in processed:
|
||||
dp[i][w] = dp[i - 1][w]
|
||||
|
||||
# Traceback to find selected bundles
|
||||
w = capacity
|
||||
selected_indices = []
|
||||
for i in range(n, 0, -1):
|
||||
if w <= 0:
|
||||
break
|
||||
if dp[i][w] != dp[i - 1][w]:
|
||||
selected_indices.append(i - 1)
|
||||
w -= weights[i - 1]
|
||||
|
||||
# Return the selected bundles in the order they were added
|
||||
return [candidates[i] for i in reversed(selected_indices)]
|
||||
|
||||
def _place_bundles_in_window(
|
||||
self,
|
||||
window: ScheduleWindow,
|
||||
bundles: list[TestBundle],
|
||||
) -> None:
|
||||
for bundle in bundles:
|
||||
bundle_key = (bundle.index, bundle.device)
|
||||
if bundle_key in self.scheduled_bundle_keys:
|
||||
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)
|
||||
self.scheduled_bundle_keys.add(bundle_key)
|
||||
|
||||
processed.update(bundle_test_ids)
|
||||
# Check if any test in bundle is top priority
|
||||
bundle_test_ids_only = {key[0] for key in bundle_test_ids}
|
||||
if bundle_test_ids_only & top_priority_tests:
|
||||
priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0)
|
||||
else:
|
||||
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":
|
||||
for device in (DUT, REF):
|
||||
key = _active_key(test_id, device)
|
||||
if key is None or key in processed:
|
||||
for idx, test in enumerate(bundle.tests):
|
||||
schedule_key = f"{test}:{bundle.device}"
|
||||
if schedule_key in self.scheduled_test_ids:
|
||||
continue
|
||||
bundle_test_ids = [key]
|
||||
# Check if any test in bundle is top priority
|
||||
if key[0] in top_priority_tests:
|
||||
priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0)
|
||||
else:
|
||||
priority_tier = BUNDLE_PRIORITY_P3P
|
||||
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)
|
||||
self.scheduled_test_ids.add(schedule_key)
|
||||
|
||||
# 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]
|
||||
bundle_test_ids = [key]
|
||||
# Check if this test is top priority
|
||||
if key[0] in top_priority_tests:
|
||||
priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0)
|
||||
else:
|
||||
priority_tier = BUNDLE_PRIORITY_COE_ONLY
|
||||
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
|
||||
self.schedule.append(
|
||||
ScheduleEntry(
|
||||
test_id=test,
|
||||
device=bundle.device,
|
||||
scheduled_date=str(window.shifts[0][0]),
|
||||
shift_index=window.shifts[0][1],
|
||||
sequence_in_shift=idx + 1,
|
||||
)
|
||||
)
|
||||
window.remaining_minutes -= self._get_test_minutes(test, bundle.device)
|
||||
|
||||
|
||||
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 _get_test_minutes(self, test_id: str, device: str) -> int:
|
||||
if device == DUT and test_id in self.active_dut:
|
||||
return self.active_dut[test_id].estimated_minutes
|
||||
if device == REF and test_id in self.active_ref:
|
||||
return self.active_ref[test_id].estimated_minutes
|
||||
return 0
|
||||
|
||||
|
||||
def _shift_capacity_for_date(
|
||||
current_date: date,
|
||||
holiday_dates: set[str],
|
||||
daytime_shift2_only: 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 daytime_shift2_only:
|
||||
return {1: 0, 2: 480, 3: 0}
|
||||
|
||||
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 _is_off_day(day: date, holiday_dates: set[str]) -> bool:
|
||||
"""Return True for weekend days and configured holidays."""
|
||||
return day.weekday() >= 5 or day.isoformat() in holiday_dates
|
||||
|
||||
|
||||
def _get_shift_sequence(
|
||||
start_date: date,
|
||||
holiday_dates: set[str],
|
||||
daytime_shift2_only: bool = False,
|
||||
) -> list[tuple[date, int]]:
|
||||
"""Generate the sequence of (date, shift_index) tuples for a scheduling window.
|
||||
|
||||
Rules per design:
|
||||
- Daytime testing first window (weekday only): [2] (shift 2 today only)
|
||||
- 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
|
||||
|
||||
Weekday default starts on shift 3 unless daytime testing shift-2-only is enabled.
|
||||
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 daytime_shift2_only and weekday < 5 and not is_holiday:
|
||||
shifts.append((start_date, 2))
|
||||
return shifts
|
||||
|
||||
# Any off-day (weekend/holiday) uses full daytime+night shifts for that date.
|
||||
if _is_off_day(start_date, holiday_dates):
|
||||
shifts.append((start_date, 1))
|
||||
shifts.append((start_date, 2))
|
||||
shifts.append((start_date, 3))
|
||||
return shifts
|
||||
|
||||
# Working-day windows always start at shift 3.
|
||||
shifts.append((start_date, 3))
|
||||
next_day = start_date + timedelta(days=1)
|
||||
|
||||
# If tomorrow starts an off-day chain (holiday/weekend), extend the window
|
||||
# through all off-days and end at shift 1 of the next working day.
|
||||
if _is_off_day(next_day, holiday_dates):
|
||||
cursor = next_day
|
||||
while _is_off_day(cursor, holiday_dates):
|
||||
shifts.append((cursor, 1))
|
||||
shifts.append((cursor, 2))
|
||||
shifts.append((cursor, 3))
|
||||
cursor += timedelta(days=1)
|
||||
shifts.append((cursor, 1))
|
||||
return shifts
|
||||
|
||||
# Default working-day pair: tonight shift 3 + next day shift 1.
|
||||
shifts.append((next_day, 1))
|
||||
|
||||
return shifts
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from test_config import build_bundle_test_configs
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Test:
|
||||
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]]
|
||||
throttled: bool
|
||||
estimated_minutes: int
|
||||
|
||||
# Bundle priority tiers for scheduling order (lower number = higher priority)
|
||||
BUNDLE_PRIORITY_TOP = 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") or "DUT").strip()
|
||||
REF = (os.getenv("REF") or "REF").strip()
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestBundle:
|
||||
index: int
|
||||
tests: list[str]
|
||||
total_minutes: int
|
||||
device: str
|
||||
config: tuple[str, ...]
|
||||
priority: int
|
||||
|
||||
def build_test_bundles(active_dut: dict[str, Test], active_ref: dict[str, Test], top_priority_tests: set[tuple[str, str]]) -> list[TestBundle]:
|
||||
"""Build deterministic bundles for DP scheduling."""
|
||||
|
||||
processed_dut: set[str] = set()
|
||||
processed_ref: set[str] = set()
|
||||
test_bundles: list[TestBundle] = []
|
||||
bundle_index = 0
|
||||
|
||||
def dedupe_preserve_order(test_ids: list[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
deduped: list[str] = []
|
||||
for test_id in test_ids:
|
||||
if test_id in seen:
|
||||
continue
|
||||
seen.add(test_id)
|
||||
deduped.append(test_id)
|
||||
return deduped
|
||||
|
||||
dut_top_priority: list[str] = []
|
||||
ref_top_priority: list[str] = []
|
||||
|
||||
for test_id, device in top_priority_tests:
|
||||
if device == DUT and test_id in active_dut:
|
||||
dut_top_priority.append(test_id)
|
||||
elif device == REF and test_id in active_ref:
|
||||
ref_top_priority.append(test_id)
|
||||
|
||||
if dut_top_priority:
|
||||
test_bundles.append(create_bundle(dut_top_priority, DUT, BUNDLE_PRIORITY_TOP, bundle_index, active_dut, active_ref))
|
||||
processed_dut.update(dut_top_priority)
|
||||
bundle_index += 1
|
||||
|
||||
if ref_top_priority:
|
||||
test_bundles.append(create_bundle(ref_top_priority, REF, BUNDLE_PRIORITY_TOP, bundle_index, active_dut, active_ref))
|
||||
processed_ref.update(ref_top_priority)
|
||||
bundle_index += 1
|
||||
|
||||
# Phase 1: DUT P2P bundles
|
||||
for dut_test_id, dut_test in active_dut.items():
|
||||
if dut_test_id in processed_dut or dut_test.test_type != "P2P":
|
||||
continue
|
||||
|
||||
dut_bundled_tests = [dut_test_id]
|
||||
processed_dut.add(dut_test_id)
|
||||
|
||||
active_coe_pairings = [
|
||||
test_id
|
||||
for test_id in dut_test.coe_pairing
|
||||
if test_id in active_dut and test_id not in processed_dut
|
||||
]
|
||||
|
||||
if active_coe_pairings:
|
||||
dut_bundled_tests.extend(active_coe_pairings)
|
||||
processed_dut.update(active_coe_pairings)
|
||||
priority = BUNDLE_PRIORITY_P2P_WITH_COE
|
||||
else:
|
||||
rx_tx_pair_id = get_rx_tx_pair_id(dut_test, DUT, active_dut, active_ref)
|
||||
if rx_tx_pair_id and rx_tx_pair_id not in processed_dut:
|
||||
dut_bundled_tests.append(rx_tx_pair_id)
|
||||
processed_dut.add(rx_tx_pair_id)
|
||||
priority = BUNDLE_PRIORITY_P2P_ONLY
|
||||
|
||||
dut_bundled_tests = dedupe_preserve_order(dut_bundled_tests)
|
||||
test_bundles.append(
|
||||
create_bundle(dut_bundled_tests, DUT, priority, bundle_index, active_dut, active_ref)
|
||||
)
|
||||
|
||||
ref_bundled_tests = [
|
||||
test_id
|
||||
for test_id in dut_bundled_tests
|
||||
if test_id in active_ref and test_id not in processed_ref
|
||||
]
|
||||
if ref_bundled_tests:
|
||||
ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests)
|
||||
processed_ref.update(ref_bundled_tests)
|
||||
test_bundles.append(
|
||||
create_bundle(ref_bundled_tests, REF, priority, bundle_index, active_dut, active_ref)
|
||||
)
|
||||
|
||||
bundle_index += 1
|
||||
|
||||
# Phase 2: DUT P3P bundles
|
||||
for dut_test_id, dut_test in active_dut.items():
|
||||
if dut_test_id in processed_dut or dut_test.test_type != "P3P":
|
||||
continue
|
||||
|
||||
dut_bundled_tests = [dut_test_id]
|
||||
processed_dut.add(dut_test_id)
|
||||
|
||||
th_ut_pair_id = get_th_ut_pair_id(dut_test, DUT, active_dut, active_ref)
|
||||
if th_ut_pair_id and th_ut_pair_id not in processed_dut:
|
||||
dut_bundled_tests.append(th_ut_pair_id)
|
||||
processed_dut.add(th_ut_pair_id)
|
||||
|
||||
dut_bundled_tests = dedupe_preserve_order(dut_bundled_tests)
|
||||
test_bundles.append(
|
||||
create_bundle(dut_bundled_tests, DUT, BUNDLE_PRIORITY_P3P, bundle_index, active_dut, active_ref)
|
||||
)
|
||||
|
||||
ref_bundled_tests = [
|
||||
test_id
|
||||
for test_id in dut_bundled_tests
|
||||
if test_id in active_ref and test_id not in processed_ref
|
||||
]
|
||||
if ref_bundled_tests:
|
||||
ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests)
|
||||
processed_ref.update(ref_bundled_tests)
|
||||
test_bundles.append(
|
||||
create_bundle(ref_bundled_tests, REF, BUNDLE_PRIORITY_P3P, bundle_index, active_dut, active_ref)
|
||||
)
|
||||
|
||||
bundle_index += 1
|
||||
|
||||
# Phase 3: DUT leftovers (including COE-only)
|
||||
for dut_test_id in active_dut:
|
||||
if dut_test_id in processed_dut:
|
||||
continue
|
||||
test_bundles.append(
|
||||
create_bundle([dut_test_id], DUT, BUNDLE_PRIORITY_COE_ONLY, bundle_index, active_dut, active_ref)
|
||||
)
|
||||
bundle_index += 1
|
||||
processed_dut.add(dut_test_id)
|
||||
|
||||
# Phase 4: unmatched REF P2P bundles
|
||||
for ref_test_id, ref_test in active_ref.items():
|
||||
if ref_test_id in processed_ref or ref_test.test_type != "P2P":
|
||||
continue
|
||||
|
||||
ref_bundled_tests = [ref_test_id]
|
||||
processed_ref.add(ref_test_id)
|
||||
|
||||
active_coe_pairings = [
|
||||
test_id
|
||||
for test_id in ref_test.coe_pairing
|
||||
if test_id in active_ref and test_id not in processed_ref
|
||||
]
|
||||
|
||||
if active_coe_pairings:
|
||||
ref_bundled_tests.extend(active_coe_pairings)
|
||||
processed_ref.update(active_coe_pairings)
|
||||
priority = BUNDLE_PRIORITY_P2P_WITH_COE
|
||||
else:
|
||||
rx_tx_pair_id = get_rx_tx_pair_id(ref_test, REF, active_dut, active_ref)
|
||||
if rx_tx_pair_id and rx_tx_pair_id not in processed_ref:
|
||||
ref_bundled_tests.append(rx_tx_pair_id)
|
||||
processed_ref.add(rx_tx_pair_id)
|
||||
priority = BUNDLE_PRIORITY_P2P_ONLY
|
||||
|
||||
ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests)
|
||||
test_bundles.append(
|
||||
create_bundle(ref_bundled_tests, REF, priority, bundle_index, active_dut, active_ref)
|
||||
)
|
||||
bundle_index += 1
|
||||
|
||||
# Phase 5: unmatched REF P3P bundles
|
||||
for ref_test_id, ref_test in active_ref.items():
|
||||
if ref_test_id in processed_ref or ref_test.test_type != "P3P":
|
||||
continue
|
||||
|
||||
ref_bundled_tests = [ref_test_id]
|
||||
processed_ref.add(ref_test_id)
|
||||
|
||||
th_ut_pair_id = get_th_ut_pair_id(ref_test, REF, active_dut, active_ref)
|
||||
if th_ut_pair_id and th_ut_pair_id not in processed_ref:
|
||||
ref_bundled_tests.append(th_ut_pair_id)
|
||||
processed_ref.add(th_ut_pair_id)
|
||||
|
||||
ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests)
|
||||
test_bundles.append(
|
||||
create_bundle(ref_bundled_tests, REF, BUNDLE_PRIORITY_P3P, bundle_index, active_dut, active_ref)
|
||||
)
|
||||
bundle_index += 1
|
||||
|
||||
# Phase 6: REF leftovers (including COE-only)
|
||||
for ref_test_id in active_ref:
|
||||
if ref_test_id in processed_ref:
|
||||
continue
|
||||
test_bundles.append(
|
||||
create_bundle([ref_test_id], REF, BUNDLE_PRIORITY_COE_ONLY, bundle_index, active_dut, active_ref)
|
||||
)
|
||||
bundle_index += 1
|
||||
processed_ref.add(ref_test_id)
|
||||
|
||||
test_bundles.sort(key=lambda b: (b.priority, b.index, b.device, b.config))
|
||||
return test_bundles
|
||||
|
||||
def get_rx_tx_pair_id(test: Test, device: str, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> str | None:
|
||||
active_tests = active_dut if device == DUT else active_ref
|
||||
test_id = test.test_id
|
||||
pair_id = test_id.replace("RX", "TX", 1) if "RX" in test_id else test_id.replace("TX", "RX", 1)
|
||||
return pair_id if pair_id in active_tests else None
|
||||
|
||||
def get_th_ut_pair_id(test: Test, device: str, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> str | None:
|
||||
active_tests = active_dut if device == DUT else active_ref
|
||||
test_id = test.test_id
|
||||
pair_id = test_id.replace("TH", "UT", 1) if "TH" in test_id else test_id.replace("UT", "TH", 1)
|
||||
return pair_id if pair_id in active_tests else None
|
||||
|
||||
def create_bundle(tests: list[str], device: str, priority: int, index: int, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> TestBundle:
|
||||
if not tests:
|
||||
raise ValueError("Cannot create bundle with no tests")
|
||||
|
||||
if device == DUT:
|
||||
active_tests = active_dut
|
||||
else:
|
||||
active_tests = active_ref
|
||||
|
||||
total_minutes = sum(active_tests[test_id].estimated_minutes for test_id in tests)
|
||||
|
||||
bundle_tests = [active_tests[test_id] for test_id in tests]
|
||||
config = tuple(build_bundle_test_configs(bundle_tests))
|
||||
|
||||
return TestBundle(
|
||||
index=index,
|
||||
tests=tests,
|
||||
total_minutes=total_minutes,
|
||||
device=device,
|
||||
config=config,
|
||||
priority=priority,
|
||||
)
|
||||
|
||||
def bundle_pair_lookup(bundles: list[TestBundle], dut_bundles: list[TestBundle], ref_bundles: list[TestBundle]) -> list[TestBundle]:
|
||||
device = bundles[0].device if bundles else None
|
||||
active_bundle_ids = {b.index for b in dut_bundles} if device == REF else {b.index for b in ref_bundles}
|
||||
|
||||
bundle_indexes = [b.index for b in bundles if b.index in active_bundle_ids]
|
||||
if device == DUT:
|
||||
return [b for b in ref_bundles if b.index in bundle_indexes]
|
||||
elif device == REF:
|
||||
return [b for b in dut_bundles if b.index in bundle_indexes]
|
||||
return []
|
||||
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
TEST_CONFIG_DEFINITION = {
|
||||
"TC1": {"T1D": "STA5", "T1F": "STA56", "T1I": "STA58", "T1L": "STA64", "T2A":"STA63", "T2O":"STA65", "T2J": "STA59", "T2E": "STA6", "T3E": "STA4"},
|
||||
"TC2": {"T1D": "STA64", "T1F": "STA4", "T1I": "STA5", "T1L": "STA58", "T2A":"STA56", "T2O":"STA59", "T2J": "STA6", "T2E": "STA65", "T3E": "STA63"},
|
||||
"TC3": {"T1D": "STA58", "T1F": "STA63", "T1I": "STA64", "T2J": "STA65", "T2E": "STA59", "T3E": "STA56"},
|
||||
"TC4": {"T1B": "STA56", "T1C": "STA4", "T1D": "STA63"},
|
||||
"TC5": {"T1B": "STA4", "T1C": "STA63", "T1D": "STA56"},
|
||||
"TC6": {"T1B": "STA63", "T1C": "STA56", "T1D": "STA4"},
|
||||
"TC7": {"T1F": "STA4", "T2A": "STA63", "T3E": "STA56"},
|
||||
"TC8": {"T1F": "STA63", "T2A": "STA56", "T3E": "STA4"},
|
||||
"TC9": {"T1F": "STA63", "T2A": "STA64", "T3E": "STA4"},
|
||||
"TC10": {"T1F": "STA4", "T3E": "STA56", "T1K2A": "STA63"},
|
||||
"TC11": {"T1F": "STA56", "T3E": "STA63", "T1K2A": "STA4"},
|
||||
"TC12": {"T1F": "STA56", "T3E": "STA4", "T1K2A": "STA63"},
|
||||
}
|
||||
|
||||
|
||||
|
||||
def _norm(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return " ".join(str(value).strip().upper().split())
|
||||
|
||||
|
||||
def _get_test_config(test: Any) -> dict[str, dict[str, str | None]]:
|
||||
if isinstance(test, dict):
|
||||
config = test.get("config") or {}
|
||||
else:
|
||||
config = getattr(test, "config", None) or {}
|
||||
return config if isinstance(config, dict) else {}
|
||||
|
||||
|
||||
def _testpoint_to_station_sets(config: dict[str, dict[str, str | None]]) -> dict[str, set[str]]:
|
||||
result: dict[str, set[str]] = {}
|
||||
|
||||
def _add_entry(entry: dict[str, str | None]) -> None:
|
||||
testpoint = _norm(entry.get("test_point") or entry.get("Test Point"))
|
||||
sta_raw = _norm(entry.get("sta") or entry.get("STA"))
|
||||
if not testpoint or not sta_raw:
|
||||
return
|
||||
|
||||
stations = {_norm(sta) for sta in sta_raw.split(",") if _norm(sta)}
|
||||
if not stations:
|
||||
return
|
||||
|
||||
result.setdefault(testpoint, set()).update(stations)
|
||||
|
||||
for entry in config.values():
|
||||
if isinstance(entry, dict):
|
||||
_add_entry(entry)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> dict[str, str]:
|
||||
"""Build station->testpoint map used by legacy graph serialization."""
|
||||
station_to_testpoint: dict[str, str] = {}
|
||||
for testpoint, stations in _testpoint_to_station_sets(config).items():
|
||||
for station in stations:
|
||||
station_to_testpoint[station] = testpoint
|
||||
return station_to_testpoint
|
||||
|
||||
|
||||
def resolve_test_config_keys(config: dict[str, dict[str, str | None]]) -> list[str]:
|
||||
"""Return all TC keys whose definitions contain all testpoint->station mappings in config."""
|
||||
testpoint_stations = _testpoint_to_station_sets(config)
|
||||
if not testpoint_stations:
|
||||
return []
|
||||
|
||||
matches: list[str] = []
|
||||
for tc_key, tc_mapping in TEST_CONFIG_DEFINITION.items():
|
||||
is_match = True
|
||||
for testpoint, stations in testpoint_stations.items():
|
||||
# TC definitions map one testpoint to exactly one station.
|
||||
if len(stations) != 1:
|
||||
is_match = False
|
||||
break
|
||||
expected_station = tc_mapping.get(testpoint)
|
||||
if expected_station is None or expected_station not in stations:
|
||||
is_match = False
|
||||
break
|
||||
if is_match:
|
||||
matches.append(tc_key)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def build_config_rows(tests: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
by_sta: dict[str, set[str]] = {}
|
||||
|
||||
for test in tests:
|
||||
config = test.get("config") or {}
|
||||
if not isinstance(config, dict):
|
||||
continue
|
||||
for entry in config.values():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
sta_raw = entry.get("sta") or entry.get("STA")
|
||||
test_point = entry.get("test_point") or entry.get("Test Point")
|
||||
if not sta_raw or not test_point:
|
||||
continue
|
||||
|
||||
for sta_part in str(sta_raw).split(","):
|
||||
sta = sta_part.strip().upper()
|
||||
if not sta:
|
||||
continue
|
||||
by_sta.setdefault(sta, set()).add(str(test_point))
|
||||
|
||||
return [
|
||||
{
|
||||
"sta": sta,
|
||||
"testPoints": sorted(points),
|
||||
}
|
||||
for sta, points in sorted(by_sta.items())
|
||||
]
|
||||
|
||||
|
||||
def build_bundle_test_configs(tests: list[Any]) -> list[str]:
|
||||
merged_by_testpoint: dict[str, set[str]] = {}
|
||||
for test in tests:
|
||||
config = _get_test_config(test)
|
||||
for entry in config.values():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
testpoint_raw = entry.get("test_point") or entry.get("Test Point")
|
||||
sta_raw = entry.get("sta") or entry.get("STA")
|
||||
if not testpoint_raw or not sta_raw:
|
||||
continue
|
||||
|
||||
testpoint = " ".join(str(testpoint_raw).strip().upper().split())
|
||||
if not testpoint:
|
||||
continue
|
||||
|
||||
stations = {
|
||||
" ".join(str(sta).strip().upper().split())
|
||||
for sta in str(sta_raw).split(",")
|
||||
if " ".join(str(sta).strip().upper().split())
|
||||
}
|
||||
if not stations:
|
||||
continue
|
||||
|
||||
merged_by_testpoint.setdefault(testpoint, set()).update(stations)
|
||||
|
||||
merged_config: dict[str, dict[str, str | None]] = {}
|
||||
for idx, (testpoint, stations) in enumerate(sorted(merged_by_testpoint.items()), start=1):
|
||||
merged_config[f"Station {idx}"] = {
|
||||
"test_point": testpoint,
|
||||
"sta": ",".join(sorted(stations)),
|
||||
}
|
||||
|
||||
return resolve_test_config_keys(merged_config)
|
||||
|
||||
|
||||
def serialize_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> str:
|
||||
return json.dumps(build_station_testpoint_map(config))
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
def is_off_day(current_date: date, holiday_dates: set[str]) -> bool:
|
||||
return current_date.weekday() >= 5 or current_date.isoformat() in holiday_dates
|
||||
|
||||
def get_shift_sequence(
|
||||
current_date: date,
|
||||
holiday_dates: set[str],
|
||||
daytime_shift2_only: bool = False,
|
||||
) -> list[tuple[date, int]]:
|
||||
is_holiday = current_date.isoformat() in holiday_dates
|
||||
weekday = current_date.weekday()
|
||||
shifts: list[tuple[date, int]] = []
|
||||
|
||||
if daytime_shift2_only and weekday < 5 and not is_holiday:
|
||||
return [(current_date, 2)]
|
||||
|
||||
if is_off_day(current_date, holiday_dates):
|
||||
return [(current_date, 1), (current_date, 2), (current_date, 3)]
|
||||
|
||||
shifts.append((current_date, 3))
|
||||
next_day = current_date + timedelta(days=1)
|
||||
if is_off_day(next_day, holiday_dates):
|
||||
cursor = next_day
|
||||
while is_off_day(cursor, holiday_dates):
|
||||
shifts.extend([(cursor, 1), (cursor, 2), (cursor, 3)])
|
||||
cursor += timedelta(days=1)
|
||||
shifts.append((cursor, 1))
|
||||
return shifts
|
||||
|
||||
shifts.append((next_day, 1))
|
||||
return shifts
|
||||
|
||||
|
||||
def get_shift_capacity_for_date(
|
||||
current_date: date,
|
||||
holiday_dates: set[str],
|
||||
daytime_shift2_only: bool = False,
|
||||
) -> dict[int, int]:
|
||||
if daytime_shift2_only:
|
||||
return {1: 0, 2: 480, 3: 0}
|
||||
if is_off_day(current_date, holiday_dates):
|
||||
return {1: 480, 2: 480, 3: 480}
|
||||
return {1: 480, 2: 0, 3: 480}
|
||||
|
||||
|
||||
def get_shift_sequence_with_capacity(
|
||||
current_date: date,
|
||||
holiday_dates: set[str],
|
||||
daytime_shift2_only: bool = False,
|
||||
) -> tuple[list[tuple[date, int]], int]:
|
||||
shifts = get_shift_sequence(current_date, holiday_dates, daytime_shift2_only)
|
||||
capacity = sum(
|
||||
get_shift_capacity_for_date(day, holiday_dates, daytime_shift2_only).get(shift_index, 0)
|
||||
for day, shift_index in shifts
|
||||
)
|
||||
return shifts, capacity
|
||||
|
||||
|
||||
def next_window_start_date(shifts: list[tuple[date, int]]) -> date:
|
||||
last_date, last_shift = shifts[-1]
|
||||
if last_shift in (1, 2):
|
||||
return last_date
|
||||
return last_date + timedelta(days=1)
|
||||
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/data
|
||||
|
||||
@@ -4,6 +4,7 @@ import FailedBanner from './components/FailedBanner'
|
||||
import Calendar from './components/Calendar'
|
||||
import RightPanel from './components/RightPanel'
|
||||
import SettingsModal from './components/SettingsModal'
|
||||
import TestWindowDetailsPanel from './components/TestWindowDetailsPanel'
|
||||
import { api, groupScheduleItems } from './api'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -125,17 +126,35 @@ export default function App() {
|
||||
const [startDateOverride, setStartDateOverride] = useState('')
|
||||
const [failedTests, setFailedTests] = useState([])
|
||||
const [scheduleData, setScheduleData] = useState({})
|
||||
const [scheduleWindows, setScheduleWindows] = useState([])
|
||||
const [selectedWindowId, setSelectedWindowId] = useState(null)
|
||||
const [completionDate, setCompletionDate] = useState(null)
|
||||
const [weekStart, setWeekStart] = useState(() => getMondayOfWeek(new Date()))
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const tonightConfigRows = useMemo(() => toTonightConfigRows(scheduleData), [scheduleData])
|
||||
const windowLookup = useMemo(() => {
|
||||
const lookup = new Map()
|
||||
|
||||
for (const windowDetails of scheduleWindows) {
|
||||
for (const segment of windowDetails.segments ?? []) {
|
||||
lookup.set(`${segment.date}::shift${segment.shift_index}`, windowDetails)
|
||||
}
|
||||
}
|
||||
|
||||
return lookup
|
||||
}, [scheduleWindows])
|
||||
const selectedWindow = useMemo(
|
||||
() => scheduleWindows.find((windowDetails) => windowDetails.window_id === selectedWindowId) ?? null,
|
||||
[scheduleWindows, selectedWindowId],
|
||||
)
|
||||
|
||||
// Fetch schedule for the given weekStart (Monday)
|
||||
const fetchSchedule = useCallback(async (start) => {
|
||||
try {
|
||||
const data = await api.getScheduleWeek(toKey(start))
|
||||
setScheduleData(groupScheduleItems(data.items))
|
||||
setScheduleWindows(data.windows ?? [])
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch schedule:', e)
|
||||
}
|
||||
@@ -268,6 +287,20 @@ export default function App() {
|
||||
setFailedTests([])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedWindowId && !selectedWindow) {
|
||||
setSelectedWindowId(null)
|
||||
}
|
||||
}, [selectedWindow, selectedWindowId])
|
||||
|
||||
function handleWindowSelect(windowDetails) {
|
||||
setSelectedWindowId(windowDetails?.window_id ?? null)
|
||||
}
|
||||
|
||||
function handleCloseWindowDetails() {
|
||||
setSelectedWindowId(null)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
return (
|
||||
@@ -286,13 +319,15 @@ export default function App() {
|
||||
onDecision={handleRerunDecision}
|
||||
/>
|
||||
|
||||
<main className="flex flex-1 gap-4 p-4 overflow-hidden">
|
||||
<main className="relative flex flex-1 gap-4 p-4 overflow-hidden">
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
<Calendar
|
||||
scheduleData={scheduleData}
|
||||
daytimeDateKey={daytimeEnabled ? toKey(new Date()) : null}
|
||||
weekStart={weekStart}
|
||||
onWeekChange={setWeekStart}
|
||||
windowLookup={windowLookup}
|
||||
onWindowSelect={handleWindowSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -310,6 +345,12 @@ export default function App() {
|
||||
loading={loading}
|
||||
tonightConfigRows={tonightConfigRows}
|
||||
/>
|
||||
|
||||
<TestWindowDetailsPanel
|
||||
isOpen={Boolean(selectedWindow)}
|
||||
windowDetails={selectedWindow}
|
||||
onClose={handleCloseWindowDetails}
|
||||
/>
|
||||
</main>
|
||||
|
||||
<SettingsModal
|
||||
|
||||
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 419 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 212 KiB |
@@ -1,4 +1,5 @@
|
||||
import DayColumn from './DayColumn'
|
||||
import { getDeviceAccentClass } from './TestCard'
|
||||
|
||||
function addDays(date, n) {
|
||||
const d = new Date(date)
|
||||
@@ -47,7 +48,14 @@ function getNextTestWindow() {
|
||||
}
|
||||
|
||||
// scheduleData: { "YYYY-MM-DD": { shift1: [...], shift2: [...], shift3: [...] }, ... }
|
||||
export default function Calendar({ scheduleData = {}, daytimeDateKey = null, weekStart, onWeekChange }) {
|
||||
export default function Calendar({
|
||||
scheduleData = {},
|
||||
daytimeDateKey = null,
|
||||
weekStart,
|
||||
onWeekChange,
|
||||
windowLookup = new Map(),
|
||||
onWindowSelect,
|
||||
}) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
@@ -58,6 +66,18 @@ export default function Calendar({ scheduleData = {}, daytimeDateKey = null, wee
|
||||
// Reorder columns: Sun Mon Tue Wed Thu Fri Sat
|
||||
// weekStart is Monday, so days[0]=Mon ... days[6]=Sun → put Sunday first
|
||||
const orderedDays = [ ...days.slice(0, 7)]
|
||||
const deviceLegendItems = Array.from(
|
||||
new Set(
|
||||
Object.values(scheduleData)
|
||||
.flatMap((day) => [
|
||||
...(day?.shift1 ?? []),
|
||||
...(day?.shift2 ?? []),
|
||||
...(day?.shift3 ?? []),
|
||||
])
|
||||
.map((test) => test?.device)
|
||||
.filter(Boolean),
|
||||
),
|
||||
).sort((a, b) => String(a).localeCompare(String(b)))
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 h-full">
|
||||
@@ -112,16 +132,24 @@ export default function Calendar({ scheduleData = {}, daytimeDateKey = null, wee
|
||||
activeShifts={activeShifts}
|
||||
shifts={shifts}
|
||||
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
||||
windowLookup={windowLookup}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex gap-4 text-xs text-gray-400">
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-2 text-xs text-gray-400">
|
||||
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-gray-500 inline-block" />Pending</span>
|
||||
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-green-700 inline-block" />Completed</span>
|
||||
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-red-700 inline-block" />Rerun Required</span>
|
||||
{deviceLegendItems.map((device) => (
|
||||
<span key={device} className="flex items-center gap-1.5">
|
||||
<span className={`w-2.5 h-2.5 rounded-sm inline-block ${getDeviceAccentClass(device)}`} />
|
||||
{device}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,14 +2,24 @@ import ShiftSlot from './ShiftSlot'
|
||||
|
||||
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
|
||||
export default function DayColumn({ date, isToday, activeShifts = new Set(), shifts = {}, showShift2 = false }) {
|
||||
export default function DayColumn({
|
||||
date,
|
||||
isToday,
|
||||
activeShifts = new Set(),
|
||||
shifts = {},
|
||||
showShift2 = false,
|
||||
windowLookup = new Map(),
|
||||
onWindowSelect,
|
||||
}) {
|
||||
const dayName = DAY_NAMES[date.getDay()]
|
||||
const dayNum = date.getDate()
|
||||
const isWeekend = date.getDay() === 0 || date.getDay() === 6
|
||||
const hasActive = activeShifts.size > 0
|
||||
const dateKey = date.toISOString().slice(0, 10)
|
||||
|
||||
// Shift 2 is visible if: weekend, holiday (showShift2), or daytime testing on
|
||||
const shift2Visible = isWeekend || showShift2
|
||||
function getWindowForShift(shiftIndex) {
|
||||
return windowLookup.get(`${dateKey}::shift${shiftIndex}`) ?? null
|
||||
}
|
||||
|
||||
const shift2Visible = showShift2 || shifts.shift2.length > 0 || Boolean(getWindowForShift(2))
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-w-0 flex-1 rounded-lg border border-gray-700 bg-gray-800/40">
|
||||
@@ -29,9 +39,30 @@ export default function DayColumn({ date, isToday, activeShifts = new Set(), shi
|
||||
|
||||
{/* Shifts */}
|
||||
<div className="flex flex-col flex-1 px-0.5 py-1">
|
||||
<ShiftSlot label="12AM–9AM" tests={shifts.shift1} visible={true} active={activeShifts.has(1)} />
|
||||
<ShiftSlot label="9AM–5PM" tests={shifts.shift2} visible={true} active={activeShifts.has(2)} />
|
||||
<ShiftSlot label="5PM–12AM" tests={shifts.shift3} visible={true} active={activeShifts.has(3)} />
|
||||
<ShiftSlot
|
||||
label="12AM–9AM"
|
||||
tests={shifts.shift1}
|
||||
visible={true}
|
||||
active={activeShifts.has(1)}
|
||||
windowDetails={getWindowForShift(1)}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
<ShiftSlot
|
||||
label="9AM–5PM"
|
||||
tests={shifts.shift2}
|
||||
visible={true}
|
||||
active={activeShifts.has(2)}
|
||||
windowDetails={getWindowForShift(2)}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
<ShiftSlot
|
||||
label="5PM–12AM"
|
||||
tests={shifts.shift3}
|
||||
visible={true}
|
||||
active={activeShifts.has(3)}
|
||||
windowDetails={getWindowForShift(3)}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ export default function Header({ onOpenSettings }) {
|
||||
return (
|
||||
<header className="flex items-center justify-between px-6 py-3 bg-gray-900 border-b border-gray-700 shrink-0">
|
||||
<h1 className="text-white font-semibold text-lg tracking-wide">
|
||||
CGW453 Scheduler
|
||||
NJTH Scheduler
|
||||
</h1>
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
|
||||
@@ -196,11 +196,11 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
|
||||
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
|
||||
Test Exclusion
|
||||
</p>
|
||||
<Field label="Excluded Test IDs" hint="(comma-separated)">
|
||||
<Field label="Exclusions" hint="(comma-separated)">
|
||||
<input
|
||||
type="text"
|
||||
className={INPUT_CLS}
|
||||
placeholder="P2PRXAX001, COERXBE002…"
|
||||
placeholder="SP, BW320,..."
|
||||
value={form.testExclusion?? ''}
|
||||
onChange={(e) => set('testExclusion', e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,44 @@
|
||||
import TestCard from './TestCard'
|
||||
|
||||
export default function ShiftSlot({ label, tests = [], visible = true, active = false }) {
|
||||
export default function ShiftSlot({
|
||||
label,
|
||||
tests = [],
|
||||
visible = true,
|
||||
active = false,
|
||||
windowDetails = null,
|
||||
onWindowSelect,
|
||||
}) {
|
||||
if (!visible) return null
|
||||
|
||||
const clickable = Boolean(windowDetails && onWindowSelect)
|
||||
|
||||
return (
|
||||
<div className={`border-t pt-1 pb-1.5 ${
|
||||
<div
|
||||
role={clickable ? 'button' : undefined}
|
||||
tabIndex={clickable ? 0 : undefined}
|
||||
onClick={clickable ? () => onWindowSelect(windowDetails) : undefined}
|
||||
onKeyDown={clickable ? (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
onWindowSelect(windowDetails)
|
||||
}
|
||||
} : undefined}
|
||||
className={`border-t pt-1 pb-1.5 transition-colors ${
|
||||
active ? 'border-blue-500/50 bg-blue-950/20 rounded' : 'border-gray-700/60'
|
||||
}`}>
|
||||
<p className={`text-[10px] font-semibold uppercase tracking-wider mb-1 px-1 ${
|
||||
} ${clickable ? 'cursor-pointer hover:bg-gray-700/25 focus:outline-none focus:ring-1 focus:ring-cyan-400/70' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 px-1 mb-1">
|
||||
<p className={`text-[10px] font-semibold uppercase tracking-wider ${
|
||||
active ? 'text-blue-400' : 'text-gray-500'
|
||||
}`}>
|
||||
{label}
|
||||
</p>
|
||||
{clickable && (
|
||||
<span className="text-[9px] font-semibold uppercase tracking-[0.2em] text-cyan-400/80">
|
||||
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 px-1 min-h-4">
|
||||
{tests.length === 0 ? (
|
||||
<span className="text-[10px] text-gray-600 italic">—</span>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
|
||||
const STATUS_STYLES = {
|
||||
pending: 'bg-gray-600/60 text-gray-200 border-gray-500',
|
||||
completed: 'bg-green-800/60 text-green-200 border-green-600',
|
||||
rerun: 'bg-red-800/60 text-red-200 border-red-600',
|
||||
}
|
||||
|
||||
const DEVICE_ACCENT_CLASSES = [
|
||||
export const DEVICE_ACCENT_CLASSES = [
|
||||
'bg-sky-400',
|
||||
'bg-emerald-400',
|
||||
'bg-amber-400',
|
||||
@@ -15,7 +17,7 @@ const DEVICE_ACCENT_CLASSES = [
|
||||
'bg-fuchsia-400',
|
||||
]
|
||||
|
||||
function getDeviceAccentClass(device) {
|
||||
export function getDeviceAccentClass(device) {
|
||||
if (!device) return 'bg-gray-500'
|
||||
|
||||
let hash = 0
|
||||
@@ -47,9 +49,46 @@ function formatConfig(config) {
|
||||
export default function TestCard({ test }) {
|
||||
const style = STATUS_STYLES[test.status] ?? STATUS_STYLES.pending
|
||||
const deviceAccentClass = getDeviceAccentClass(test.device)
|
||||
const cardRef = useRef(null)
|
||||
const [tooltipPos, setTooltipPos] = useState({ top: '0px', left: '0px' })
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const cardElement = cardRef.current
|
||||
if (!cardElement) return
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
// Position tooltip near the cursor, with 10px offset
|
||||
const top = e.clientY + 10
|
||||
const left = e.clientX + 10
|
||||
|
||||
setTooltipPos({
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false)
|
||||
}
|
||||
|
||||
cardElement.addEventListener('mouseenter', handleMouseEnter)
|
||||
cardElement.addEventListener('mouseleave', handleMouseLeave)
|
||||
cardElement.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return () => {
|
||||
cardElement.removeEventListener('mouseenter', handleMouseEnter)
|
||||
cardElement.removeEventListener('mouseleave', handleMouseLeave)
|
||||
cardElement.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div ref={cardRef} className="relative">
|
||||
<div
|
||||
className={`relative px-1.5 py-0.5 pl-3 rounded border text-xs font-mono truncate cursor-default select-none ${style}`}
|
||||
style={{ maxWidth: '100%' }}
|
||||
@@ -57,9 +96,14 @@ export default function TestCard({ test }) {
|
||||
<span className={`absolute inset-y-0 left-0 w-1 rounded-l ${deviceAccentClass}`} aria-hidden="true" />
|
||||
{test.test_id}
|
||||
</div>
|
||||
{/* Hover tooltip */}
|
||||
<div className="absolute z-50 bottom-full left-0 mb-1 hidden group-hover:block min-w-max">
|
||||
<div className="bg-gray-800 border border-gray-600 rounded-md px-2.5 py-1.5 text-xs text-gray-200 shadow-lg max-w-md">
|
||||
{/* Hover tooltip — follows cursor */}
|
||||
{isHovered && (
|
||||
<div className="fixed z-50 bg-gray-800 border border-gray-600 rounded-md px-2.5 py-1.5 text-xs text-gray-200 shadow-lg max-w-md pointer-events-none"
|
||||
style={{
|
||||
top: tooltipPos.top,
|
||||
left: tooltipPos.left,
|
||||
}}
|
||||
>
|
||||
<p><span className="text-gray-400">Type:</span> {test.test_type ?? '—'}</p>
|
||||
<p><span className="text-gray-400">Rotation:</span> {test.rotation ?? '—'}</p>
|
||||
<p className="flex items-center gap-1.5">
|
||||
@@ -69,7 +113,7 @@ export default function TestCard({ test }) {
|
||||
</p>
|
||||
<p className="mt-1 pt-1 border-t border-gray-600"><span className="text-gray-400">Config:</span> {formatConfig(test.config)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const STATUS_STYLES = {
|
||||
pending: 'bg-gray-700 text-gray-200 border-gray-600',
|
||||
completed: 'bg-green-900/60 text-green-200 border-green-700',
|
||||
rerun: 'bg-red-900/60 text-red-200 border-red-700',
|
||||
}
|
||||
|
||||
function formatWindowHeader(windowDetails) {
|
||||
if (!windowDetails?.start_at || !windowDetails?.end_at) return 'Window details'
|
||||
|
||||
const start = new Date(windowDetails.start_at)
|
||||
const end = new Date(windowDetails.end_at)
|
||||
|
||||
const startLabel = start.toLocaleString('en-US', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
}).replace(':00', '').replace(' AM', 'am').replace(' PM', 'pm')
|
||||
|
||||
const endLabel = end.toLocaleString('en-US', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
}).replace(':00', '').replace(' AM', 'am').replace(' PM', 'pm')
|
||||
|
||||
return `${startLabel} - ${endLabel}`
|
||||
}
|
||||
|
||||
function formatHours(minutes) {
|
||||
if (!Number.isFinite(minutes) || minutes <= 0) return '0 hrs'
|
||||
const hours = minutes / 60
|
||||
const rounded = Number.isInteger(hours) ? String(hours) : hours.toFixed(1)
|
||||
return `${rounded} hrs`
|
||||
}
|
||||
|
||||
function formatShiftLabel(test) {
|
||||
const shiftLabels = {
|
||||
1: '12AM–9AM',
|
||||
2: '9AM–5PM',
|
||||
3: '5PM–12AM',
|
||||
}
|
||||
|
||||
const date = test?.scheduled_date
|
||||
const label = shiftLabels[test?.shift_index] ?? 'Unknown shift'
|
||||
return date ? `${date} ${label}` : label
|
||||
}
|
||||
|
||||
function getWindowTestConfig(windowDetails) {
|
||||
if (Array.isArray(windowDetails?.bundle_test_configs)) {
|
||||
return windowDetails.bundle_test_configs
|
||||
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const tests = windowDetails?.tests ?? []
|
||||
const allKeys = new Set()
|
||||
for (const test of tests) {
|
||||
if (Array.isArray(test?.bundle_test_configs)) {
|
||||
for (const key of test.bundle_test_configs) {
|
||||
if (typeof key === 'string' && key.trim()) {
|
||||
allKeys.add(key.trim())
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof test?.test_config === 'string' && test.test_config.trim()) {
|
||||
allKeys.add(test.test_config.trim())
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(allKeys).sort()
|
||||
}
|
||||
|
||||
function formatConfigEntries(config) {
|
||||
if (!config || typeof config !== 'object') return []
|
||||
|
||||
return Object.entries(config)
|
||||
.map(([band, data]) => {
|
||||
if (!data || typeof data !== 'object') return null
|
||||
const testPoint = data.test_point || data['Test Point'] || '—'
|
||||
const sta = data.sta || data.STA || '—'
|
||||
return { band, testPoint, sta }
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function TestRow({ test }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const statusStyle = STATUS_STYLES[test.status] ?? STATUS_STYLES.pending
|
||||
const configEntries = useMemo(() => formatConfigEntries(test.config), [test.config])
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-700 bg-gray-900/70 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
className="w-full px-4 py-3 text-left hover:bg-gray-800/80 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{test.test_id}</p>
|
||||
<p className="mt-1 text-xs text-gray-400">{test.device ?? '—'} · {formatShiftLabel(test)}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`px-2 py-1 rounded-full border text-[11px] font-semibold uppercase tracking-wide ${statusStyle}`}>
|
||||
{test.status ?? 'pending'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">{formatHours(test.estimated_minutes)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="px-4 pb-4 pt-1 border-t border-gray-800 bg-gray-950/60">
|
||||
<div className="grid grid-cols-2 gap-3 text-xs text-gray-300">
|
||||
<div>
|
||||
<p className="text-gray-500 uppercase tracking-wide text-[11px] mb-1">Type</p>
|
||||
<p>{test.test_type ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 uppercase tracking-wide text-[11px] mb-1">Rotation</p>
|
||||
<p>{test.rotation ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 uppercase tracking-wide text-[11px] mb-1">Priority</p>
|
||||
<p>{test.priority ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 uppercase tracking-wide text-[11px] mb-1">Sequence</p>
|
||||
<p>{test.sequence_in_shift ?? '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="text-gray-500 uppercase tracking-wide text-[11px] mb-2">Config</p>
|
||||
{configEntries.length === 0 ? (
|
||||
<p className="text-xs text-gray-500">No config details available.</p>
|
||||
) : (
|
||||
<div className="rounded-lg border border-gray-800 overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-900/90">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2 font-semibold text-gray-400 border-b border-gray-800">Band</th>
|
||||
<th className="text-left px-3 py-2 font-semibold text-gray-400 border-b border-gray-800">Testpoint</th>
|
||||
<th className="text-left px-3 py-2 font-semibold text-gray-400 border-b border-gray-800">STA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{configEntries.map((entry) => (
|
||||
<tr key={`${test.test_id}-${entry.band}`} className="odd:bg-gray-950 even:bg-gray-900/60">
|
||||
<td className="px-3 py-2 text-gray-200 border-b border-gray-800">{entry.band}</td>
|
||||
<td className="px-3 py-2 text-gray-300 border-b border-gray-800">{entry.testPoint}</td>
|
||||
<td className="px-3 py-2 text-gray-300 border-b border-gray-800">{entry.sta}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose }) {
|
||||
const windowTestConfig = useMemo(() => getWindowTestConfig(windowDetails), [windowDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return undefined
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [isOpen, onClose])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-0 z-40 transition-opacity duration-300 ${
|
||||
isOpen ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'
|
||||
}`}
|
||||
aria-hidden={!isOpen}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="absolute inset-0 bg-gray-950/45 backdrop-blur-[1px]"
|
||||
aria-label="Close test window details"
|
||||
/>
|
||||
|
||||
<aside
|
||||
className={`absolute inset-y-0 right-0 w-full max-w-[30rem] border-l border-gray-700 bg-gray-950/96 shadow-2xl transition-transform duration-300 ${
|
||||
isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-gray-800 px-5 py-5">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-cyan-400">Test Window</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">
|
||||
{windowDetails ? formatWindowHeader(windowDetails) : 'Window details'}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-full border border-gray-700 p-2 text-gray-400 hover:border-gray-500 hover:text-white transition-colors"
|
||||
aria-label="Close panel"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{windowDetails && (
|
||||
<div className="flex-1 overflow-y-auto px-5 py-5">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-2xl border border-gray-800 bg-gray-900/80 px-4 py-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-gray-500">Available Run Time</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{formatHours(windowDetails.available_runtime_minutes)}</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-gray-800 bg-gray-900/80 px-4 py-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-gray-500">Estimated Run Time</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{formatHours(windowDetails.estimated_runtime_minutes)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 rounded-2xl border border-gray-800 bg-gray-900/80 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold text-white">Config</p>
|
||||
<span className="rounded-full border border-cyan-700/60 bg-cyan-900/30 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-cyan-200">
|
||||
{windowTestConfig.length > 0 ? windowTestConfig.join(', ') : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
{windowDetails.configRows?.length ? (
|
||||
<div className="mt-3 overflow-hidden rounded-xl border border-gray-800">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-950/90">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold text-gray-400 border-b border-gray-800">STA</th>
|
||||
<th className="px-3 py-2 text-left font-semibold text-gray-400 border-b border-gray-800">Testpoint</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{windowDetails.configRows.map((row, index) => (
|
||||
<tr key={`${row.sta}-${index}`} className="odd:bg-gray-950 even:bg-gray-900/60">
|
||||
<td className="px-3 py-2 border-b border-gray-800 font-semibold text-gray-200">{row.sta}</td>
|
||||
<td className="px-3 py-2 border-b border-gray-800 text-gray-300">{row.testPoints.join(', ')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-xs text-gray-500">No STA placement mapping available for this window.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<p className="text-sm font-semibold text-white">Tests Scheduled In This Window</p>
|
||||
<span className="text-xs text-gray-500">{windowDetails.tests?.length ?? 0} tests</span>
|
||||
</div>
|
||||
{windowDetails.tests?.length ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
{windowDetails.tests.map((test) => (
|
||||
<TestRow key={`${test.test_id}-${test.device}-${test.scheduled_date}-${test.shift_index}`} test={test} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-gray-800 bg-gray-900/50 px-4 py-6 text-sm text-gray-500">
|
||||
No tests are currently scheduled in this window.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||