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
|
- DUT test result directory path, REF test result directory path
|
||||||
- Estimated runtime for each type of test for calculations
|
- Estimated runtime for each type of test for calculations
|
||||||
- Rotation definition
|
- Test config definition
|
||||||
- Test exclusions
|
- Test exclusions
|
||||||
- Top priority tests
|
- Top priority tests
|
||||||
- Allow user to manually override and enter tests that needs to be run tonight
|
- 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
|
## Output
|
||||||
### Backend
|
### 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**
|
- **Rules**
|
||||||
- Can be run in the same night
|
- Test Windows
|
||||||
- Same rotation and same test point P2P/COE tests
|
- Weekday night time test window: Monday - Thursday
|
||||||
- COERXBE002: T1F -> STA63, T2A -> STA56, T3E -> STA4, R3
|
- Starts 5PM of that day, ends 9AM the next day
|
||||||
- COERXBE003: T1F -> STA63, T2A -> STA56, T3E -> STA4, R3
|
- Capcity: 17 hrs (16hrs + 1hr margin)
|
||||||
|
- Ex. Monday 6/29 5PM - Tuesday 6/30 9AM
|
||||||
- Different rotations but all stations are at same testpoints
|
- Weekday day time test window: Monday-Thursday
|
||||||
- R2 P2PRXAC003 5G T1C -> STA4
|
- Starts 9AM - 5PM of that day
|
||||||
- R1 P2PRXAC012 2G T1C -> STA4
|
- Capacity: 8 hrs
|
||||||
|
- Weekend test window: Friday - Sunday
|
||||||
- Same rotation and same test point P3P tests
|
- Starts Friday 5PM, ends Monday 9AM
|
||||||
- Needs to be run back to back
|
- 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)
|
- P2P up link (RX) and down link (TX)
|
||||||
- Ex. P2PRXAX001 and P2PTXAX001
|
- Ex. P2PRXAX001 and P2PTXAX001
|
||||||
- COE P2P bundle
|
- P3P throttled(TH) and unthrottled(UT)
|
||||||
- DUT and REF tests
|
- Same tests, different devices(DUT, REF) needs to be run in back to back test windows
|
||||||
|
- Ex. Monday
|
||||||
- **Priority**
|
- **Priority**
|
||||||
- Default priority: COE P2P pair -> P2P only -> P3P
|
- Default priority: COE P2P pair -> P2P only -> P3P
|
||||||
- 1: highest priority (must run tonight)
|
- 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
|
- 4: P3P
|
||||||
- 5: lowest priority
|
- 5: lowest priority
|
||||||
- Priority 1 and 5 needs to be entered manually
|
- 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**
|
- **Calculation**
|
||||||
- Use the estimate run time for each type of test to fit as many tests as possible in a shift of testing.
|
- 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
|
- 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
|
- Create bundles of tests that needs to be run back to back
|
||||||
- Types of test bundles and their order
|
- Types of test bundles and their order
|
||||||
1. Failed tests rerun if exist
|
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
|
3. P2P tests without COE pairs: P2PRX on DUT -> P2PTX on DUT -> P2PRX on REF -> P2PTX on REF
|
||||||
4. P3P tests (ALL ROT's)
|
4. P3P tests (ALL ROT's)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime, timedelta
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
@@ -9,9 +9,15 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
import db
|
import db
|
||||||
import graph
|
|
||||||
from parser import CsvValidationError, parse_target_csv
|
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
|
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()
|
REF = os.getenv("REF", "CGW452").strip()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LoadTestsRequest(BaseModel):
|
class LoadTestsRequest(BaseModel):
|
||||||
csv_path: str = Field(..., description="Absolute or backend-relative path to target CSV")
|
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)
|
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
|
@asynccontextmanager
|
||||||
async def lifespan(application: FastAPI):
|
async def lifespan(application: FastAPI):
|
||||||
db.init_db(DB_PATH)
|
db.init_db(DB_PATH)
|
||||||
graph.reset_graph_state()
|
|
||||||
settings = db.read_settings(DB_PATH)
|
settings = db.read_settings(DB_PATH)
|
||||||
configure_result_watcher(settings)
|
configure_result_watcher(settings)
|
||||||
try:
|
try:
|
||||||
@@ -141,21 +317,17 @@ def load_tests(request: LoadTestsRequest) -> dict[str, Any]:
|
|||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
count = db.upsert_tests(parsed.tests, DB_PATH)
|
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 {
|
return {
|
||||||
"loaded_tests": count,
|
"loaded_tests": count,
|
||||||
"warnings": parsed.warnings,
|
"warnings": parsed.warnings,
|
||||||
"dut_graph_nodes": len(graph.get_graph()),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/schedule/active/remove")
|
@app.post("/api/schedule/active/remove")
|
||||||
def remove_active_tests(request: RemoveActiveTestsRequest) -> dict[str, Any]:
|
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)}
|
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:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail="start_date must be YYYY-MM-DD") from 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)
|
stored_tests = db.list_schedulable_tests(DB_PATH, rule=request.rule)
|
||||||
if not stored_tests:
|
if not stored_tests:
|
||||||
version = db.create_schedule_version([], DB_PATH)
|
version = db.create_schedule_version([], DB_PATH)
|
||||||
@@ -189,29 +360,36 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]
|
|||||||
config=t.config,
|
config=t.config,
|
||||||
throttled=t.throttled,
|
throttled=t.throttled,
|
||||||
estimated_minutes=t.estimated_minutes,
|
estimated_minutes=t.estimated_minutes,
|
||||||
priority=t.priority,
|
|
||||||
raw_payload=t.raw_payload or {},
|
|
||||||
)
|
)
|
||||||
for t in stored_tests
|
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)
|
holiday_dates = db.list_holidays(DB_PATH)
|
||||||
try:
|
top_priority_pairs: set[tuple[str, str]] = set()
|
||||||
entries, completion_date = compile_schedule(
|
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,
|
tests=scheduler_tests,
|
||||||
|
top_priority_tests=top_priority_pairs,
|
||||||
start_date=request.start_date,
|
start_date=request.start_date,
|
||||||
holiday_dates=holiday_dates,
|
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,
|
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
|
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(
|
version = db.create_schedule_version(
|
||||||
[(e.test_id, e.device, e.scheduled_date, e.shift_index, e.sequence_in_shift) for e in entries],
|
[(e.test_id, e.device, e.scheduled_date, e.shift_index, e.sequence_in_shift) for e in entries],
|
||||||
DB_PATH,
|
DB_PATH,
|
||||||
@@ -252,25 +430,14 @@ def get_schedule_week(start: str | None = None) -> dict[str, Any]:
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail="start must be YYYY-MM-DD") from 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)
|
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 {
|
return {
|
||||||
"start_date": week_start,
|
"start_date": week_start,
|
||||||
"items": [
|
"items": [_serialize_schedule_row(row) for row in rows],
|
||||||
{
|
"windows": _build_schedule_windows(week_start_date, all_rows, holiday_dates),
|
||||||
"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
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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
|
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
|
P1,1,No,No,R3,P2PRXBE001,5G,LPI,T1F,36,80,-76,UL,STA63,,,,,,,,,,,,
|
||||||
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,1,No,No,R3,P2PRXBE002,5G,LPI,T1B,36,80,-67,UL,STA63,,,,,,,,,,,,
|
||||||
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,1,No,No,R3,P2PRXBE003,5G,LPI,T1C,36,80,-45,UL,STA63,,,,,,,,,,,,
|
||||||
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,2,No,Yes,R3,P2PRXBE004,5G,LPI,T1F,100,80,-76,UL,STA63,,,,,,,,,,,,
|
||||||
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,2,No,No,R3,P2PRXBE005,5G,LPI,T1B,100,80,-67,UL,STA63,,,,,,,,,,,,
|
||||||
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
|
P1,2,No,No,R3,P2PRXBE006,5G,LPI,T1C,100,80,-45,UL,STA63,,,,,,,,,,,,
|
||||||
P2,4,Yes,Yes,R1,COERXAX002,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,6,Yes,Yes,R1,COERXAX003,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,7,Yes,Yes,R1,COERXAX004,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,8,Yes,Yes,R1,COERXAX005,5G,SP,T1F,161,80,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,9,Yes,Yes,R1,COERXAX006,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,11,Yes,Yes,R1,COERXAX007,5G,SP,T1F,100,160,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,12,Yes,Yes,R1,COERXAX008,5G,LPI,T1F,100,160,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,14,Yes,Yes,R2,COERXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,16,Yes,Yes,R2,COERXAX010,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,18,Yes,Yes,R2,COERXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,20,Yes,Yes,R2,COERXAX012,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,22,Yes,Yes,R2,COERXAX013,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
|
||||||
P2,24,Yes,Yes,R3,COERXAX014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,UL,STA56
|
|
||||||
P2,26,Yes,Yes,R3,COERXAX015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,UL,STA56
|
|
||||||
P2,28,Yes,Yes,R3,COERXAX016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,UL,STA56
|
|
||||||
P2,3,Yes,Yes,R3,COERXBE001,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
P2,3,Yes,Yes,R3,COERXBE001,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
P2,4,Yes,Yes,R3,COERXBE002,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
P2,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,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,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,8,Yes,Yes,R3,COERXBE005,5G,SP,T1F,161,80,-76,UL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||||
P2,9,Yes,Yes,R3,COERXBE006,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
P2,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,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,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,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,,,,,,
|
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,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,,,,,,
|
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,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,,,,,,
|
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,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,,,,,,
|
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,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,,,,,,
|
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,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,P2PRXBE029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA63
|
||||||
P1,23,No,No,R2,P2PRXBE030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,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,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,P2PRXBE032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA63
|
||||||
P1,25,No,No,R2,P2PRXBE033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,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,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,P2PRXBE035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA63
|
||||||
P1,27,No,No,R2,P2PRXBE036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,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,,,,,,,,,,,,
|
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,R2,P2PTXAC002,5G,LPI,T2B,36,80,-67,DL,STA4,,,,,,,,,,,,
|
P1,1,No,No,R1,P2PRXAX001,5G,LPI,T1F,36,80,-76,UL,STA56,,,,,,,,,,,,
|
||||||
P1,1,No,No,R2,P2PTXAC003,5G,LPI,T1C,36,80,-45,DL,STA4,,,,,,,,,,,,
|
P1,1,No,No,R1,P2PRXAX002,5G,LPI,T1B,36,80,-67,UL,STA56,,,,,,,,,,,,
|
||||||
P1,2,No,Yes,R2,P2PTXAC004,5G,LPI,T1F,100,80,-76,DL,STA4,,,,,,,,,,,,
|
P1,1,No,No,R1,P2PRXAX003,5G,LPI,T1C,36,80,-45,UL,STA56,,,,,,,,,,,,
|
||||||
P1,2,No,No,R2,P2PTXAC005,5G,LPI,T2B,100,80,-67,DL,STA4,,,,,,,,,,,,
|
P1,2,No,Yes,R1,P2PRXAX004,5G,LPI,T1F,100,80,-76,UL,STA56,,,,,,,,,,,,
|
||||||
P1,2,No,No,R2,P2PTXAC006,5G,LPI,T1C,100,80,-45,DL,STA4,,,,,,,,,,,,
|
P1,2,No,No,R1,P2PRXAX005,5G,LPI,T1B,100,80,-67,UL,STA56,,,,,,,,,,,,
|
||||||
P1,5,No,Yes,R2,P2PTXAC007,5G,LPI,T1F,161,80,-76,DL,STA4,,,,,,,,,,,,
|
P1,2,No,No,R1,P2PRXAX006,5G,LPI,T1C,100,80,-45,UL,STA56,,,,,,,,,,,,
|
||||||
P1,5,No,No,R2,P2PTXAC008,5G,LPI,T2B,161,80,-67,DL,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
|
||||||
P1,5,No,No,R2,P2PTXAC009,5G,LPI,T1C,161,80,-45,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,14,No,Yes,R1,P2PTXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,DL,STA4
|
P1,5,No,Yes,R1,P2PRXAX007,5G,LPI,T1F,161,80,-76,UL,STA56,,,,,,,,,,,,
|
||||||
P1,14,No,No,R1,P2PTXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,DL,STA4
|
P1,5,No,No,R1,P2PRXAX008,5G,LPI,T1B,161,80,-67,UL,STA56,,,,,,,,,,,,
|
||||||
P1,14,No,No,R1,P2PTXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,DL,STA4
|
P1,5,No,No,R1,P2PRXAX009,5G,LPI,T1C,161,80,-45,UL,STA56,,,,,,,,,,,,
|
||||||
P1,16,No,Yes,R1,P2PTXAC013,2G,LPI,,,,,,,,,,,,,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
|
||||||
P1,16,No,No,R1,P2PTXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,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
|
||||||
P1,16,No,No,R1,P2PTXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,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
|
||||||
P1,18,No,Yes,R1,P2PTXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,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,18,No,No,R1,P2PTXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,DL,STA4
|
P1,10,No,Yes,R1,P2PRXAX010,5G,LPI,T1F,100,160,-76,UL,STA56,,,,,,,,,,,,
|
||||||
P1,18,No,No,R1,P2PTXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,DL,STA4
|
P1,10,No,No,R1,P2PRXAX011,5G,LPI,T1B,100,160,-67,UL,STA56,,,,,,,,,,,,
|
||||||
P1,1,No,No,R1,P2PTXAX001,5G,LPI,T1F,36,80,-76,DL,STA56,,,,,,,,,,,,
|
P1,10,No,No,R1,P2PRXAX012,5G,LPI,T1C,100,160,-45,UL,STA56,,,,,,,,,,,,
|
||||||
P1,1,No,No,R1,P2PTXAX002,5G,LPI,T2B,36,80,-67,DL,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
|
||||||
P1,1,No,No,R1,P2PTXAX003,5G,LPI,T1C,36,80,-45,DL,STA56,,,,,,,,,,,,
|
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,2,No,Yes,R1,P2PTXAX004,5G,LPI,T1F,100,80,-76,DL,STA56,,,,,,,,,,,,
|
P1,13,No,Yes,R2,P2PRXAX013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA56,,,,,,
|
||||||
P1,2,No,No,R1,P2PTXAX005,5G,LPI,T2B,100,80,-67,DL,STA56,,,,,,,,,,,,
|
P1,13,No,No,R2,P2PRXAX014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA56,,,,,,
|
||||||
P1,2,No,No,R1,P2PTXAX006,5G,LPI,T1C,100,80,-45,DL,STA56,,,,,,,,,,,,
|
P1,13,No,No,R2,P2PRXAX015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA56,,,,,,
|
||||||
P1,5,No,Yes,R1,P2PTXAX007,5G,LPI,T1F,161,80,-76,DL,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,5,No,No,R1,P2PTXAX008,5G,LPI,T2B,161,80,-67,DL,STA56,,,,,,,,,,,,
|
P1,15,No,Yes,R2,P2PRXAX016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA56,,,,,,
|
||||||
P1,5,No,No,R1,P2PTXAX009,5G,LPI,T1C,161,80,-45,DL,STA56,,,,,,,,,,,,
|
P1,15,No,No,R2,P2PRXAX017,6G,LPI,,,,,,,T1B,133,160,-70,UL,STA56,,,,,,
|
||||||
P1,8,No,Yes,R1,P2PTXAX010,5G,LPI,T1F,100,160,-76,DL,STA56,,,,,,,,,,,,
|
P1,15,No,No,R2,P2PRXAX018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,,
|
||||||
P1,8,No,No,R1,P2PTXAX011,5G,LPI,T2B,100,160,-67,DL,STA56,,,,,,,,,,,,
|
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,8,No,No,R1,P2PTXAX012,5G,LPI,T1C,100,160,-45,DL,STA56,,,,,,,,,,,,
|
P1,17,No,Yes,R2,P2PRXAX019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA56,,,,,,
|
||||||
P1,11,No,Yes,R2,P2PTXAX013,6G,LPI,,,,,,,T2A,5,160,-79,DL,STA56,,,,,,
|
P1,17,No,No,R2,P2PRXAX020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA56,,,,,,
|
||||||
P1,11,No,No,R2,P2PTXAX014,6G,LPI,,,,,,,T2B,5,160,-70,DL,STA56,,,,,,
|
P1,17,No,No,R2,P2PRXAX021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA56,,,,,,
|
||||||
P1,11,No,No,R2,P2PTXAX015,6G,LPI,,,,,,,T1C,5,160,-45,DL,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,13,No,Yes,R2,P2PTXAX016,6G,LPI,,,,,,,T2A,133,160,-79,DL,STA56,,,,,,
|
P1,19,No,Yes,R2,P2PRXAX022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA56,,,,,,
|
||||||
P1,13,No,No,R2,P2PTXAX017,6G,LPI,,,,,,,T2B,133,160,-70,DL,STA56,,,,,,
|
P1,19,No,No,R2,P2PRXAX023,6G,SP,,,,,,,T1B,5,320,-70,UL,STA56,,,,,,
|
||||||
P1,13,No,No,R2,P2PTXAX018,6G,LPI,,,,,,,T1C,133,160,-45,DL,STA56,,,,,,
|
P1,19,No,No,R2,P2PRXAX024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA56,,,,,,
|
||||||
P1,15,No,Yes,R2,P2PTXAX019,6G,LPI,,,,,,,T2A,197,160,-79,DL,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,15,No,No,R2,P2PTXAX020,6G,LPI,,,,,,,T2B,197,160,-70,DL,STA56,,,,,,
|
P1,21,No,Yes,R2,P2PRXAX025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA56,,,,,,
|
||||||
P1,15,No,No,R2,P2PTXAX021,6G,LPI,,,,,,,T1C,197,160,-45,DL,STA56,,,,,,
|
P1,21,No,No,R2,P2PRXAX026,6G,LPI,,,,,,,T1B,133,320,-70,UL,STA56,,,,,,
|
||||||
P1,17,No,Yes,R2,P2PTXAX022,6G,SP,,,,,,,T2A,5,320,-79,DL,STA56,,,,,,
|
P1,21,No,No,R2,P2PRXAX027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA56,,,,,,
|
||||||
P1,17,No,No,R2,P2PTXAX023,6G,SP,,,,,,,T2B,5,320,-70,DL,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,17,No,No,R2,P2PTXAX024,6G,SP,,,,,,,T1C,5,320,-45,DL,STA56,,,,,,
|
P1,23,No,Yes,R3,P2PRXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56
|
||||||
P1,19,No,Yes,R2,P2PTXAX025,6G,LPI,,,,,,,T2A,133,320,-79,DL,STA56,,,,,,
|
P1,23,No,No,R3,P2PRXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56
|
||||||
P1,19,No,No,R2,P2PTXAX026,6G,LPI,,,,,,,T2B,133,320,-70,DL,STA56,,,,,,
|
P1,23,No,No,R3,P2PRXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56
|
||||||
P1,19,No,No,R2,P2PTXAX027,6G,LPI,,,,,,,T1C,133,320,-45,DL,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,21,No,Yes,R3,P2PTXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56
|
P1,25,No,Yes,R3,P2PRXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56
|
||||||
P1,21,No,No,R3,P2PTXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56
|
P1,25,No,No,R3,P2PRXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56
|
||||||
P1,21,No,No,R3,P2PTXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56
|
P1,25,No,No,R3,P2PRXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56
|
||||||
P1,23,No,Yes,R3,P2PTXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,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
|
||||||
P1,23,No,No,R3,P2PTXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56
|
P1,27,No,Yes,R3,P2PRXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56
|
||||||
P1,23,No,No,R3,P2PTXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56
|
P1,27,No,No,R3,P2PRXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56
|
||||||
P1,25,No,Yes,R3,P2PTXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56
|
P1,27,No,No,R3,P2PRXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56
|
||||||
P1,25,No,No,R3,P2PTXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,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,25,No,No,R3,P2PTXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,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,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,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,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,,,,,,,,,,,,
|
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,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,,,,,,,,,,,,
|
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,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,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,,,,,,,,,,,,
|
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
|
||||||
P1,11,No,No,R1,P2PTXBE014,6G,LPI,T2B,5,160,-70,DL,STA63,,,,,,,,,,,,
|
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,,,,,,,,,,,,
|
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,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,,,,,,,,,,,,
|
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,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,,,,,,,,,,,,
|
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,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,,,,,,,,,,,,
|
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,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,,,,,,,,,,,,
|
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,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,P2PTXBE029,2G,LPI,T1D,1,20,-60,UL,STA63,,,,,,,,,,,,
|
||||||
P1,21,No,No,R2,P2PTXBE030,2G,LPI,T1C,1,20,-45,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,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,P2PTXBE032,2G,LPI,T1D,6,20,-60,UL,STA63,,,,,,,,,,,,
|
||||||
P1,23,No,No,R2,P2PTXBE033,2G,LPI,T1C,6,20,-45,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,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,P2PTXBE035,2G,LPI,T1D,11,20,-60,UL,STA63,,,,,,,,,,,,
|
||||||
P1,25,No,No,R2,P2PTXBE036,2G,LPI,T1C,11,20,-45,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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
|
|
||||||
|
@@ -6,7 +6,7 @@ from contextlib import contextmanager
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterator
|
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
|
APP_ROOT = Path(__file__).resolve().parent
|
||||||
@@ -123,13 +123,6 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
|
|||||||
minutes INTEGER NOT NULL
|
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 (
|
CREATE TABLE IF NOT EXISTS rerun_events (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
detected_date TEXT NOT NULL,
|
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:
|
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."""
|
"""Compute and serialize the station-to-testpoint map from a test config."""
|
||||||
station_map = build_station_testpoint_map(config)
|
return serialize_station_testpoint_map(config)
|
||||||
return json.dumps(station_map)
|
|
||||||
|
|
||||||
|
|
||||||
def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> int:
|
def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> int:
|
||||||
if not records:
|
if not records:
|
||||||
return 0
|
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:
|
with get_connection(db_path) as conn:
|
||||||
conn.executemany(
|
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,
|
station_testpoint_map = excluded.station_testpoint_map,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
""",
|
""",
|
||||||
[
|
values,
|
||||||
(
|
|
||||||
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
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
return len(records)
|
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}
|
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]:
|
def _parse_rule_tokens(rule: str | None) -> list[str]:
|
||||||
if not rule:
|
if not rule:
|
||||||
return []
|
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_num = _extract_signed_int(token_suffix)
|
||||||
target_compact = _normalize_compact(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:
|
for band in keys:
|
||||||
entry = _band_entry(record.config, band)
|
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
|
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:
|
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:
|
if not test_ids_with_device:
|
||||||
return
|
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",
|
"TC ID",
|
||||||
"Victim Band",
|
"Victim Band",
|
||||||
"6GHz Power Mode",
|
"6GHz Power Mode",
|
||||||
"5G Test Point",
|
"5G_Test Point",
|
||||||
"5G Channel",
|
"5G_Channel",
|
||||||
"5G Bandwidth",
|
"5G_Bandwidth",
|
||||||
"5G RSSI",
|
"5G_RSSI",
|
||||||
"5G Direction",
|
"5G_Direction",
|
||||||
"5G STA",
|
"5G_STA",
|
||||||
"6G Test Point",
|
"6G_Test Point",
|
||||||
"6G Channel",
|
"6G_Channel",
|
||||||
"6G Bandwidth",
|
"6G_Bandwidth",
|
||||||
"6G RSSI",
|
"6G_RSSI",
|
||||||
"6G Direction",
|
"6G_Direction",
|
||||||
"6G STA",
|
"6G_STA",
|
||||||
"2G Test Point",
|
"2G_Test Point",
|
||||||
"2G Channel",
|
"2G_Channel",
|
||||||
"2G Bandwidth",
|
"2G_Bandwidth",
|
||||||
"2G RSSI",
|
"2G_RSSI",
|
||||||
"2G Direction",
|
"2G_Direction",
|
||||||
"2G STA",
|
"2G_STA",
|
||||||
]
|
]
|
||||||
|
|
||||||
P3P_REQUIRED_COLUMNS = [
|
P3P_REQUIRED_COLUMNS = [
|
||||||
@@ -49,27 +49,27 @@ P3P_REQUIRED_COLUMNS = [
|
|||||||
"TC ID",
|
"TC ID",
|
||||||
"Band",
|
"Band",
|
||||||
"6GHz Power Mode",
|
"6GHz Power Mode",
|
||||||
"Station 1 Test Point",
|
"STATION 1_Test Point",
|
||||||
"Station 1 Channel",
|
"STATION 1_Channel",
|
||||||
"Station 1 Bandwidth",
|
"STATION 1_Bandwidth",
|
||||||
"Station 1 RSSI",
|
"STATION 1_RSSI",
|
||||||
"Station 1 Direction",
|
"STATION 1_Direction",
|
||||||
"Station 1 Rate",
|
"STATION 1_Rate",
|
||||||
"Station 1 STA",
|
"STATION 1_STA",
|
||||||
"Station 2 Test Point",
|
"STATION 2_Test Point",
|
||||||
"Station 2 Channel",
|
"STATION 2_Channel",
|
||||||
"Station 2 Bandwidth",
|
"STATION 2_Bandwidth",
|
||||||
"Station 2 RSSI",
|
"STATION 2_RSSI",
|
||||||
"Station 2 Direction",
|
"STATION 2_Direction",
|
||||||
"Station 2 Rate",
|
"STATION 2_Rate",
|
||||||
"Station 2 STA",
|
"STATION 2_STA",
|
||||||
"Station 3 Test Point",
|
"STATION 3_Test Point",
|
||||||
"Station 3 Channel",
|
"STATION 3_Channel",
|
||||||
"Station 3 Bandwidth",
|
"STATION 3_Bandwidth",
|
||||||
"Station 3 RSSI",
|
"STATION 3_RSSI",
|
||||||
"Station 3 Direction",
|
"STATION 3_Direction",
|
||||||
"Station 3 Rate",
|
"STATION 3_Rate",
|
||||||
"Station 3 STA",
|
"STATION 3_STA",
|
||||||
]
|
]
|
||||||
|
|
||||||
RUNTIME_DEFAULTS = {
|
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)}"
|
f"{', '.join(p2p_coe_missing)}; missing columns for P3P format: {', '.join(p3p_missing)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_target_csv(
|
def parse_target_csv(
|
||||||
csv_path: str | Path | list[str | Path] | tuple[str | Path, ...],
|
csv_path: str | Path | list[str | Path] | tuple[str | Path, ...],
|
||||||
smb_credentials: dict[str, Any] | None = None,
|
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:
|
if band is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
prefix = f"{band} "
|
# Source CSVs may use either "5G_Test Point" or "5G Test Point" style headers.
|
||||||
test_point = _normalize_value(_row_get(row, f"{prefix}Test Point"))
|
prefixes = (f"{band}_", f"{band} ")
|
||||||
channel = _normalize_value(_row_get(row, f"{prefix}Channel"))
|
|
||||||
rssi = _normalize_value(_row_get(row, f"{prefix}RSSI"))
|
def _pick_value(suffix: str) -> str:
|
||||||
bandwidth = _normalize_value(_row_get(row, f"{prefix}Bandwidth"))
|
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.
|
# Direction is intentionally ignored for COE pairing matching.
|
||||||
if not all([test_point, channel, rssi, bandwidth]):
|
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]]:
|
def _build_legacy_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
|
||||||
return {
|
return {
|
||||||
"5G": {
|
"5G": {
|
||||||
"test_point": _empty_to_none(_row_get(row, "5G Test Point")),
|
"test_point": _empty_to_none(_row_get(row, "5G_Test Point")),
|
||||||
"channel": _empty_to_none(_row_get(row, "5G Channel")),
|
"channel": _empty_to_none(_row_get(row, "5G_Channel")),
|
||||||
"bandwidth": _empty_to_none(_row_get(row, "5G Bandwidth")),
|
"bandwidth": _empty_to_none(_row_get(row, "5G_Bandwidth")),
|
||||||
"rssi": _empty_to_none(_row_get(row, "5G RSSI")),
|
"rssi": _empty_to_none(_row_get(row, "5G_RSSI")),
|
||||||
"direction": _empty_to_none(_row_get(row, "5G Direction")),
|
"direction": _empty_to_none(_row_get(row, "5G_Direction")),
|
||||||
"sta": _empty_to_none(_row_get(row, "5G STA")),
|
"sta": _empty_to_none(_row_get(row, "5G_STA")),
|
||||||
},
|
},
|
||||||
"6G": {
|
"6G": {
|
||||||
"power_mode": _empty_to_none(_row_get(row, "6GHz Power Mode")),
|
"power_mode": _empty_to_none(_row_get(row, "6GHz Power Mode")),
|
||||||
"test_point": _empty_to_none(_row_get(row, "6G Test Point")),
|
"test_point": _empty_to_none(_row_get(row, "6G_Test Point")),
|
||||||
"channel": _empty_to_none(_row_get(row, "6G Channel")),
|
"channel": _empty_to_none(_row_get(row, "6G_Channel")),
|
||||||
"bandwidth": _empty_to_none(_row_get(row, "6G Bandwidth")),
|
"bandwidth": _empty_to_none(_row_get(row, "6G_Bandwidth")),
|
||||||
"rssi": _empty_to_none(_row_get(row, "6G RSSI")),
|
"rssi": _empty_to_none(_row_get(row, "6G_RSSI")),
|
||||||
"direction": _empty_to_none(_row_get(row, "6G Direction")),
|
"direction": _empty_to_none(_row_get(row, "6G_Direction")),
|
||||||
"sta": _empty_to_none(_row_get(row, "6G STA")),
|
"sta": _empty_to_none(_row_get(row, "6G_STA")),
|
||||||
},
|
},
|
||||||
"2G": {
|
"2G": {
|
||||||
"test_point": _empty_to_none(_row_get(row, "2G Test Point")),
|
"test_point": _empty_to_none(_row_get(row, "2G_Test Point")),
|
||||||
"channel": _empty_to_none(_row_get(row, "2G Channel")),
|
"channel": _empty_to_none(_row_get(row, "2G_Channel")),
|
||||||
"bandwidth": _empty_to_none(_row_get(row, "2G Bandwidth")),
|
"bandwidth": _empty_to_none(_row_get(row, "2G_Bandwidth")),
|
||||||
"rssi": _empty_to_none(_row_get(row, "2G RSSI")),
|
"rssi": _empty_to_none(_row_get(row, "2G_RSSI")),
|
||||||
"direction": _empty_to_none(_row_get(row, "2G Direction")),
|
"direction": _empty_to_none(_row_get(row, "2G_Direction")),
|
||||||
"sta": _empty_to_none(_row_get(row, "2G STA")),
|
"sta": _empty_to_none(_row_get(row, "2G_STA")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _build_p3p_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
|
def _build_p3p_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
|
||||||
return {
|
return {
|
||||||
"Station 1": {
|
"STATION1": {
|
||||||
"test_point": _empty_to_none(_row_get(row, "Station 1 Test Point")),
|
"test_point": _empty_to_none(_row_get(row, "STATION 1_Test Point")),
|
||||||
"channel": _empty_to_none(_row_get(row, "Station 1 Channel")),
|
"channel": _empty_to_none(_row_get(row, "STATION 1_Channel")),
|
||||||
"bandwidth": _empty_to_none(_row_get(row, "Station 1 Bandwidth")),
|
"bandwidth": _empty_to_none(_row_get(row, "STATION 1_Bandwidth")),
|
||||||
"rssi": _empty_to_none(_row_get(row, "Station 1 RSSI")),
|
"rssi": _empty_to_none(_row_get(row, "STATION 1_RSSI")),
|
||||||
"direction": _empty_to_none(_row_get(row, "Station 1 Direction")),
|
"direction": _empty_to_none(_row_get(row, "STATION 1_Direction")),
|
||||||
"rate": _empty_to_none(_row_get(row, "Station 1 Rate")),
|
"rate": _empty_to_none(_row_get(row, "STATION 1_Rate")),
|
||||||
"sta": _empty_to_none(_row_get(row, "Station 1 STA")),
|
"sta": _empty_to_none(_row_get(row, "STATION 1_STA")),
|
||||||
},
|
},
|
||||||
"Station 2": {
|
"STATION2": {
|
||||||
"test_point": _empty_to_none(_row_get(row, "Station 2 Test Point")),
|
"test_point": _empty_to_none(_row_get(row, "STATION 2_Test Point")),
|
||||||
"channel": _empty_to_none(_row_get(row, "Station 2 Channel")),
|
"channel": _empty_to_none(_row_get(row, "STATION 2_Channel")),
|
||||||
"bandwidth": _empty_to_none(_row_get(row, "Station 2 Bandwidth")),
|
"bandwidth": _empty_to_none(_row_get(row, "STATION 2_Bandwidth")),
|
||||||
"rssi": _empty_to_none(_row_get(row, "Station 2 RSSI")),
|
"rssi": _empty_to_none(_row_get(row, "STATION 2_RSSI")),
|
||||||
"direction": _empty_to_none(_row_get(row, "Station 2 Direction")),
|
"direction": _empty_to_none(_row_get(row, "STATION 2_Direction")),
|
||||||
"rate": _empty_to_none(_row_get(row, "Station 2 Rate")),
|
"rate": _empty_to_none(_row_get(row, "STATION 2_Rate")),
|
||||||
"sta": _empty_to_none(_row_get(row, "Station 2 STA")),
|
"sta": _empty_to_none(_row_get(row, "STATION 2_STA")),
|
||||||
},
|
},
|
||||||
"Station 3": {
|
"STATION3": {
|
||||||
"test_point": _empty_to_none(_row_get(row, "Station 3 Test Point")),
|
"test_point": _empty_to_none(_row_get(row, "STATION 3_Test Point")),
|
||||||
"channel": _empty_to_none(_row_get(row, "Station 3 Channel")),
|
"channel": _empty_to_none(_row_get(row, "STATION 3_Channel")),
|
||||||
"bandwidth": _empty_to_none(_row_get(row, "Station 3 Bandwidth")),
|
"bandwidth": _empty_to_none(_row_get(row, "STATION 3_Bandwidth")),
|
||||||
"rssi": _empty_to_none(_row_get(row, "Station 3 RSSI")),
|
"rssi": _empty_to_none(_row_get(row, "STATION 3_RSSI")),
|
||||||
"direction": _empty_to_none(_row_get(row, "Station 3 Direction")),
|
"direction": _empty_to_none(_row_get(row, "STATION 3_Direction")),
|
||||||
"rate": _empty_to_none(_row_get(row, "Station 3 Rate")),
|
"rate": _empty_to_none(_row_get(row, "STATION 3_Rate")),
|
||||||
"sta": _empty_to_none(_row_get(row, "Station 3 STA")),
|
"sta": _empty_to_none(_row_get(row, "STATION 3_STA")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,37 +2,12 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date
|
||||||
from typing import Any
|
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
|
||||||
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]
|
|
||||||
|
|
||||||
|
DUT = (os.getenv("DUT") or "DUT").strip()
|
||||||
|
REF = (os.getenv("REF") or "REF").strip()
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ScheduleEntry:
|
class ScheduleEntry:
|
||||||
@@ -43,575 +18,276 @@ class ScheduleEntry:
|
|||||||
sequence_in_shift: int
|
sequence_in_shift: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass
|
||||||
class TestBundle:
|
class ScheduleWindow:
|
||||||
"""A bundle of tests that must be run together in sequence."""
|
index: int
|
||||||
test_ids: list[TestKey]
|
shifts: list[tuple[date, int]]
|
||||||
priority_tier: int
|
capacity_minutes: int
|
||||||
total_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.
|
class Scheduler:
|
||||||
_TESTS_BY_ID: dict[TestKey, SchedulerTest] = {}
|
def __init__(
|
||||||
_ACTIVE_DUT: dict[TestKey, int] = {}
|
self,
|
||||||
_ACTIVE_REF: dict[TestKey, int] = {}
|
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:
|
active_dut_bundles = [b for b in bundles if b.device == DUT]
|
||||||
global _TESTS_BY_ID, _ACTIVE_DUT, _ACTIVE_REF
|
active_ref_bundles = [b for b in bundles if b.device == 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()
|
|
||||||
|
|
||||||
|
# Group by individual TC values (from config tuples)
|
||||||
|
all_tcs = set()
|
||||||
for bundle in bundles:
|
for bundle in bundles:
|
||||||
# Skip bundles that are already fully placed.
|
all_tcs.update(bundle.config if bundle.config else [None])
|
||||||
if any(test_id in placed_test_ids for test_id in bundle.test_ids):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not bundle.test_ids:
|
# Create dict: TC -> bundles supporting that TC
|
||||||
continue
|
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}
|
window_index = 0
|
||||||
if len(bundle_devices) > 1:
|
cursor_date = self.start_date
|
||||||
# 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
|
|
||||||
|
|
||||||
# Keep all tests in the same window pairwise-compatible.
|
# Sort TC keys by bundle count (largest first); tie-break by TC name.
|
||||||
if window_placed_test_ids:
|
sorted_tc_items = sorted(
|
||||||
is_compatible_with_window = True
|
[(tc, bundles_by_tc[tc]) for tc in bundles_by_tc if tc is not None],
|
||||||
bundle_test_ids_only = {test_key[0] for test_key in bundle.test_ids}
|
key=lambda x: (-len(x[1]), x[0]),
|
||||||
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:]
|
|
||||||
)
|
)
|
||||||
if remaining_window < bundle.total_minutes:
|
if None in bundles_by_tc:
|
||||||
|
sorted_tc_items.append((None, bundles_by_tc[None]))
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
selected_bundles = mirrored_bundles + knapsack_bundles
|
||||||
|
if len(selected_bundles) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Create a mirror of the other device for the next window
|
||||||
|
bundle_pairs = bundle_pair_lookup(knapsack_bundles, dut_unscheduled, ref_unscheduled)
|
||||||
|
|
||||||
|
if window_device == DUT:
|
||||||
|
pending_target = self.pending_ref_mirror
|
||||||
|
else:
|
||||||
|
pending_target = self.pending_dut_mirror
|
||||||
|
|
||||||
|
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
|
continue
|
||||||
|
pending_target.append(pair_bundle)
|
||||||
|
existing_pending.add(pair_key)
|
||||||
|
|
||||||
bundle_start_shift_pos = current_shift_pos
|
|
||||||
placed_bundle_tests: list[tuple[TestKey, tuple[date, int]]] = []
|
|
||||||
failed = False
|
|
||||||
|
|
||||||
for key in bundle.test_ids:
|
window = ScheduleWindow(
|
||||||
test = tests[key]
|
index=window_index,
|
||||||
prev_remaining = 0
|
shifts=shifts,
|
||||||
while current_shift_pos < len(shift_sequence):
|
capacity_minutes=capacity,
|
||||||
date_shift = shift_sequence[current_shift_pos]
|
remaining_minutes=remaining_time,
|
||||||
state = shift_state[date_shift]
|
assigned_config=tc,
|
||||||
state["remaining_minutes"] += prev_remaining # Add back any leftover from previous shift
|
assigned_device=window_device,
|
||||||
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(
|
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]:
|
||||||
|
if capacity <= 0 or not candidates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
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
|
||||||
|
self.scheduled_bundle_keys.add(bundle_key)
|
||||||
|
|
||||||
|
for idx, test in enumerate(bundle.tests):
|
||||||
|
schedule_key = f"{test}:{bundle.device}"
|
||||||
|
if schedule_key in self.scheduled_test_ids:
|
||||||
|
continue
|
||||||
|
self.scheduled_test_ids.add(schedule_key)
|
||||||
|
|
||||||
|
self.schedule.append(
|
||||||
ScheduleEntry(
|
ScheduleEntry(
|
||||||
test_id=test.test_id,
|
test_id=test,
|
||||||
device=test.device,
|
device=bundle.device,
|
||||||
scheduled_date=date_shift[0].isoformat(),
|
scheduled_date=str(window.shifts[0][0]),
|
||||||
shift_index=date_shift[1],
|
shift_index=window.shifts[0][1],
|
||||||
sequence_in_shift=state['sequence_counter'],
|
sequence_in_shift=idx + 1,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
state['sequence_counter'] += 1
|
window.remaining_minutes -= self._get_test_minutes(test, bundle.device)
|
||||||
state['remaining_minutes'] -= test.estimated_minutes
|
|
||||||
placed_test_ids.add(key)
|
|
||||||
placed_bundle_tests.append((key, date_shift))
|
|
||||||
last_date = date_shift[0].isoformat()
|
|
||||||
break
|
|
||||||
|
|
||||||
# Move to the next shift in the sequence and keep the bundle contiguous.
|
|
||||||
current_shift_pos += 1
|
|
||||||
prev_remaining = state['remaining_minutes']
|
|
||||||
if current_shift_pos >= len(shift_sequence):
|
|
||||||
failed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if failed:
|
|
||||||
break
|
|
||||||
|
|
||||||
if failed:
|
|
||||||
# Roll back any partially placed tests from this bundle.
|
|
||||||
for key, date_shift in reversed(placed_bundle_tests):
|
|
||||||
state = shift_state[date_shift]
|
|
||||||
state['remaining_minutes'] += tests[key].estimated_minutes
|
|
||||||
state['sequence_counter'] -= 1
|
|
||||||
entries.pop()
|
|
||||||
placed_test_ids.discard(key)
|
|
||||||
last_date = None
|
|
||||||
current_shift_pos = bundle_start_shift_pos
|
|
||||||
continue
|
|
||||||
|
|
||||||
if placed_bundle_tests:
|
|
||||||
window_placed_test_ids.update(test_key[0] for test_key in bundle.test_ids)
|
|
||||||
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
|
|
||||||
|
|
||||||
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,
|
|
||||||
) -> list[TestBundle]:
|
|
||||||
"""Create bundles of tests that must run together, sorted by priority tier and efficiency.
|
|
||||||
|
|
||||||
Bundling rules:
|
|
||||||
- P2P with COE pairs: [P2P, COE1, COE2, ...]
|
|
||||||
- P2P without COE: [P2P_RX, P2P_TX] if both active
|
|
||||||
- COE without a linked active P2P: [COE] (standalone bundle)
|
|
||||||
- P3P: individual test (not bundled)
|
|
||||||
|
|
||||||
Returned list is sorted by (priority_tier, total_minutes) to place small/high-priority bundles first.
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
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:
|
|
||||||
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)
|
|
||||||
|
|
||||||
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:
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
def _rx_tx_pair_id(test_id: str, active: set[str]) -> str | None:
|
def _get_test_minutes(self, test_id: str, device: str) -> int:
|
||||||
if "RX" in test_id:
|
if device == DUT and test_id in self.active_dut:
|
||||||
candidate = test_id.replace("RX", "TX", 1)
|
return self.active_dut[test_id].estimated_minutes
|
||||||
return candidate if candidate in active else None
|
if device == REF and test_id in self.active_ref:
|
||||||
if "TX" in test_id:
|
return self.active_ref[test_id].estimated_minutes
|
||||||
candidate = test_id.replace("TX", "RX", 1)
|
return 0
|
||||||
return candidate if candidate in active else None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
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
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
/data
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import FailedBanner from './components/FailedBanner'
|
|||||||
import Calendar from './components/Calendar'
|
import Calendar from './components/Calendar'
|
||||||
import RightPanel from './components/RightPanel'
|
import RightPanel from './components/RightPanel'
|
||||||
import SettingsModal from './components/SettingsModal'
|
import SettingsModal from './components/SettingsModal'
|
||||||
|
import TestWindowDetailsPanel from './components/TestWindowDetailsPanel'
|
||||||
import { api, groupScheduleItems } from './api'
|
import { api, groupScheduleItems } from './api'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -125,17 +126,35 @@ export default function App() {
|
|||||||
const [startDateOverride, setStartDateOverride] = useState('')
|
const [startDateOverride, setStartDateOverride] = useState('')
|
||||||
const [failedTests, setFailedTests] = useState([])
|
const [failedTests, setFailedTests] = useState([])
|
||||||
const [scheduleData, setScheduleData] = useState({})
|
const [scheduleData, setScheduleData] = useState({})
|
||||||
|
const [scheduleWindows, setScheduleWindows] = useState([])
|
||||||
|
const [selectedWindowId, setSelectedWindowId] = useState(null)
|
||||||
const [completionDate, setCompletionDate] = useState(null)
|
const [completionDate, setCompletionDate] = useState(null)
|
||||||
const [weekStart, setWeekStart] = useState(() => getMondayOfWeek(new Date()))
|
const [weekStart, setWeekStart] = useState(() => getMondayOfWeek(new Date()))
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const tonightConfigRows = useMemo(() => toTonightConfigRows(scheduleData), [scheduleData])
|
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)
|
// Fetch schedule for the given weekStart (Monday)
|
||||||
const fetchSchedule = useCallback(async (start) => {
|
const fetchSchedule = useCallback(async (start) => {
|
||||||
try {
|
try {
|
||||||
const data = await api.getScheduleWeek(toKey(start))
|
const data = await api.getScheduleWeek(toKey(start))
|
||||||
setScheduleData(groupScheduleItems(data.items))
|
setScheduleData(groupScheduleItems(data.items))
|
||||||
|
setScheduleWindows(data.windows ?? [])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to fetch schedule:', e)
|
console.error('Failed to fetch schedule:', e)
|
||||||
}
|
}
|
||||||
@@ -268,6 +287,20 @@ export default function App() {
|
|||||||
setFailedTests([])
|
setFailedTests([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedWindowId && !selectedWindow) {
|
||||||
|
setSelectedWindowId(null)
|
||||||
|
}
|
||||||
|
}, [selectedWindow, selectedWindowId])
|
||||||
|
|
||||||
|
function handleWindowSelect(windowDetails) {
|
||||||
|
setSelectedWindowId(windowDetails?.window_id ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloseWindowDetails() {
|
||||||
|
setSelectedWindowId(null)
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -286,13 +319,15 @@ export default function App() {
|
|||||||
onDecision={handleRerunDecision}
|
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">
|
<div className="flex-1 min-w-0 flex flex-col">
|
||||||
<Calendar
|
<Calendar
|
||||||
scheduleData={scheduleData}
|
scheduleData={scheduleData}
|
||||||
daytimeDateKey={daytimeEnabled ? toKey(new Date()) : null}
|
daytimeDateKey={daytimeEnabled ? toKey(new Date()) : null}
|
||||||
weekStart={weekStart}
|
weekStart={weekStart}
|
||||||
onWeekChange={setWeekStart}
|
onWeekChange={setWeekStart}
|
||||||
|
windowLookup={windowLookup}
|
||||||
|
onWindowSelect={handleWindowSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -310,6 +345,12 @@ export default function App() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
tonightConfigRows={tonightConfigRows}
|
tonightConfigRows={tonightConfigRows}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TestWindowDetailsPanel
|
||||||
|
isOpen={Boolean(selectedWindow)}
|
||||||
|
windowDetails={selectedWindow}
|
||||||
|
onClose={handleCloseWindowDetails}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<SettingsModal
|
<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 DayColumn from './DayColumn'
|
||||||
|
import { getDeviceAccentClass } from './TestCard'
|
||||||
|
|
||||||
function addDays(date, n) {
|
function addDays(date, n) {
|
||||||
const d = new Date(date)
|
const d = new Date(date)
|
||||||
@@ -47,7 +48,14 @@ function getNextTestWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// scheduleData: { "YYYY-MM-DD": { shift1: [...], shift2: [...], shift3: [...] }, ... }
|
// 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()
|
const today = new Date()
|
||||||
today.setHours(0, 0, 0, 0)
|
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
|
// Reorder columns: Sun Mon Tue Wed Thu Fri Sat
|
||||||
// weekStart is Monday, so days[0]=Mon ... days[6]=Sun → put Sunday first
|
// weekStart is Monday, so days[0]=Mon ... days[6]=Sun → put Sunday first
|
||||||
const orderedDays = [ ...days.slice(0, 7)]
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-3 h-full">
|
<div className="flex flex-col gap-3 h-full">
|
||||||
@@ -112,16 +132,24 @@ export default function Calendar({ scheduleData = {}, daytimeDateKey = null, wee
|
|||||||
activeShifts={activeShifts}
|
activeShifts={activeShifts}
|
||||||
shifts={shifts}
|
shifts={shifts}
|
||||||
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
||||||
|
windowLookup={windowLookup}
|
||||||
|
onWindowSelect={onWindowSelect}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legend */}
|
{/* 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-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-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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,24 @@ import ShiftSlot from './ShiftSlot'
|
|||||||
|
|
||||||
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
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 dayName = DAY_NAMES[date.getDay()]
|
||||||
const dayNum = date.getDate()
|
const dayNum = date.getDate()
|
||||||
const isWeekend = date.getDay() === 0 || date.getDay() === 6
|
const dateKey = date.toISOString().slice(0, 10)
|
||||||
const hasActive = activeShifts.size > 0
|
|
||||||
|
|
||||||
// Shift 2 is visible if: weekend, holiday (showShift2), or daytime testing on
|
function getWindowForShift(shiftIndex) {
|
||||||
const shift2Visible = isWeekend || showShift2
|
return windowLookup.get(`${dateKey}::shift${shiftIndex}`) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const shift2Visible = showShift2 || shifts.shift2.length > 0 || Boolean(getWindowForShift(2))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-w-0 flex-1 rounded-lg border border-gray-700 bg-gray-800/40">
|
<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 */}
|
{/* Shifts */}
|
||||||
<div className="flex flex-col flex-1 px-0.5 py-1">
|
<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
|
||||||
<ShiftSlot label="9AM–5PM" tests={shifts.shift2} visible={true} active={activeShifts.has(2)} />
|
label="12AM–9AM"
|
||||||
<ShiftSlot label="5PM–12AM" tests={shifts.shift3} visible={true} active={activeShifts.has(3)} />
|
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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export default function Header({ onOpenSettings }) {
|
|||||||
return (
|
return (
|
||||||
<header className="flex items-center justify-between px-6 py-3 bg-gray-900 border-b border-gray-700 shrink-0">
|
<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">
|
<h1 className="text-white font-semibold text-lg tracking-wide">
|
||||||
CGW453 Scheduler
|
NJTH Scheduler
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<button
|
||||||
onClick={onOpenSettings}
|
onClick={onOpenSettings}
|
||||||
|
|||||||
@@ -196,12 +196,12 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
|
|||||||
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
|
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
|
||||||
Test Exclusion
|
Test Exclusion
|
||||||
</p>
|
</p>
|
||||||
<Field label="Excluded Test IDs" hint="(comma-separated)">
|
<Field label="Exclusions" hint="(comma-separated)">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={INPUT_CLS}
|
className={INPUT_CLS}
|
||||||
placeholder="P2PRXAX001, COERXBE002…"
|
placeholder="SP, BW320,..."
|
||||||
value={form.testExclusion ?? ''}
|
value={form.testExclusion?? ''}
|
||||||
onChange={(e) => set('testExclusion', e.target.value)}
|
onChange={(e) => set('testExclusion', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -1,17 +1,44 @@
|
|||||||
import TestCard from './TestCard'
|
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
|
if (!visible) return null
|
||||||
|
|
||||||
|
const clickable = Boolean(windowDetails && onWindowSelect)
|
||||||
|
|
||||||
return (
|
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'
|
active ? 'border-blue-500/50 bg-blue-950/20 rounded' : 'border-gray-700/60'
|
||||||
}`}>
|
} ${clickable ? 'cursor-pointer hover:bg-gray-700/25 focus:outline-none focus:ring-1 focus:ring-cyan-400/70' : ''}`}
|
||||||
<p className={`text-[10px] font-semibold uppercase tracking-wider mb-1 px-1 ${
|
>
|
||||||
|
<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'
|
active ? 'text-blue-400' : 'text-gray-500'
|
||||||
}`}>
|
}`}>
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</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">
|
<div className="flex flex-col gap-0.5 px-1 min-h-4">
|
||||||
{tests.length === 0 ? (
|
{tests.length === 0 ? (
|
||||||
<span className="text-[10px] text-gray-600 italic">—</span>
|
<span className="text-[10px] text-gray-600 italic">—</span>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { useRef, useEffect, useState } from 'react'
|
||||||
|
|
||||||
const STATUS_STYLES = {
|
const STATUS_STYLES = {
|
||||||
pending: 'bg-gray-600/60 text-gray-200 border-gray-500',
|
pending: 'bg-gray-600/60 text-gray-200 border-gray-500',
|
||||||
completed: 'bg-green-800/60 text-green-200 border-green-600',
|
completed: 'bg-green-800/60 text-green-200 border-green-600',
|
||||||
rerun: 'bg-red-800/60 text-red-200 border-red-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-sky-400',
|
||||||
'bg-emerald-400',
|
'bg-emerald-400',
|
||||||
'bg-amber-400',
|
'bg-amber-400',
|
||||||
@@ -15,7 +17,7 @@ const DEVICE_ACCENT_CLASSES = [
|
|||||||
'bg-fuchsia-400',
|
'bg-fuchsia-400',
|
||||||
]
|
]
|
||||||
|
|
||||||
function getDeviceAccentClass(device) {
|
export function getDeviceAccentClass(device) {
|
||||||
if (!device) return 'bg-gray-500'
|
if (!device) return 'bg-gray-500'
|
||||||
|
|
||||||
let hash = 0
|
let hash = 0
|
||||||
@@ -47,9 +49,46 @@ function formatConfig(config) {
|
|||||||
export default function TestCard({ test }) {
|
export default function TestCard({ test }) {
|
||||||
const style = STATUS_STYLES[test.status] ?? STATUS_STYLES.pending
|
const style = STATUS_STYLES[test.status] ?? STATUS_STYLES.pending
|
||||||
const deviceAccentClass = getDeviceAccentClass(test.device)
|
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 (
|
return (
|
||||||
<div className="relative group">
|
<div ref={cardRef} className="relative">
|
||||||
<div
|
<div
|
||||||
className={`relative px-1.5 py-0.5 pl-3 rounded border text-xs font-mono truncate cursor-default select-none ${style}`}
|
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%' }}
|
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" />
|
<span className={`absolute inset-y-0 left-0 w-1 rounded-l ${deviceAccentClass}`} aria-hidden="true" />
|
||||||
{test.test_id}
|
{test.test_id}
|
||||||
</div>
|
</div>
|
||||||
{/* Hover tooltip */}
|
{/* Hover tooltip — follows cursor */}
|
||||||
<div className="absolute z-50 bottom-full left-0 mb-1 hidden group-hover:block min-w-max">
|
{isHovered && (
|
||||||
<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">
|
<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">Type:</span> {test.test_type ?? '—'}</p>
|
||||||
<p><span className="text-gray-400">Rotation:</span> {test.rotation ?? '—'}</p>
|
<p><span className="text-gray-400">Rotation:</span> {test.rotation ?? '—'}</p>
|
||||||
<p className="flex items-center gap-1.5">
|
<p className="flex items-center gap-1.5">
|
||||||
@@ -69,7 +113,7 @@ export default function TestCard({ test }) {
|
|||||||
</p>
|
</p>
|
||||||
<p className="mt-1 pt-1 border-t border-gray-600"><span className="text-gray-400">Config:</span> {formatConfig(test.config)}</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>
|
)}
|
||||||
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||