#!/usr/bin/env python3 """Generate the Turkish GPS PPS theory and project-mapping report as PDF. The report is intentionally generated with matplotlib/PdfPages so it remains self-contained in the existing Python environment and all schematics stay vector-based in the PDF. """ from __future__ import annotations import argparse import os import textwrap from pathlib import Path os.environ.setdefault("MPLCONFIGDIR", "/tmp/pps_report_matplotlib") import matplotlib matplotlib.use("Agg") import matplotlib.image as mpimg import matplotlib.pyplot as plt from matplotlib.backends.backend_pdf import PdfPages from matplotlib.patches import Circle, FancyArrowPatch, FancyBboxPatch, Rectangle ROOT = Path(__file__).resolve().parent OUTPUT = ROOT / "output" DEFAULT_PDF = ROOT / "GPS_PPS_Teori_Proje_Mapping.pdf" NAVY = "#12304A" BLUE = "#176B9B" CYAN = "#3BA7C4" GREEN = "#2D8A61" ORANGE = "#E8893A" RED = "#C84C4C" INK = "#17212B" MUTED = "#596773" LIGHT = "#EEF3F6" PALE_BLUE = "#E7F2F8" PALE_GREEN = "#E9F5EF" PALE_ORANGE = "#FCF0E5" A4 = (8.27, 11.69) class Report: def __init__(self, output_pdf: Path) -> None: self.output_pdf = output_pdf self.pdf = PdfPages( output_pdf, metadata={ "Title": "GPS PPS Teorisi ve Proje Mapping'i", "Author": "Rocket GPS receiver design study", "Subject": "LNAV, WLS clock bias, cross-timestamp and PI disciplined PPS", "Keywords": "GPS PPS WLS TOW HOW ABS_SAMPLE PI APB", }, ) self.page_number = 0 def close(self) -> None: self.pdf.close() def new_page(self, title: str, section: str = "GPS PPS TASARIM RAPORU"): self.page_number += 1 fig = plt.figure(figsize=A4, facecolor="white") ax = fig.add_axes([0, 0, 1, 1]) ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.axis("off") ax.add_patch(Rectangle((0, 0.945), 1, 0.055, color=NAVY)) ax.text(0.055, 0.973, section, color="white", fontsize=8.5, va="center") ax.text(0.055, 0.915, title, color=NAVY, fontsize=19, weight="bold", va="top") ax.plot([0.055, 0.945], [0.885, 0.885], color=CYAN, linewidth=1.5) self._footer(ax) return fig, ax def _footer(self, ax) -> None: ax.plot([0.055, 0.945], [0.045, 0.045], color="#C6D0D7", linewidth=0.7) ax.text( 0.055, 0.022, "Mevcut yazılım/RTL analizi ve Python zaman modeli", fontsize=7.2, color=MUTED, va="center", ) ax.text( 0.945, 0.022, str(self.page_number), fontsize=8, color=MUTED, ha="right", va="center", ) def save(self, fig) -> None: self.pdf.savefig(fig, bbox_inches="tight", pad_inches=0) plt.close(fig) def cover(self) -> None: self.page_number += 1 fig = plt.figure(figsize=A4, facecolor="white") ax = fig.add_axes([0, 0, 1, 1]) ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.axis("off") ax.add_patch(Rectangle((0, 0), 1, 1, color=NAVY)) ax.add_patch(Rectangle((0.06, 0.08), 0.012, 0.84, color=CYAN)) ax.text(0.11, 0.83, "GPS PPS", fontsize=42, color="white", weight="bold") ax.text( 0.11, 0.755, "Teoriden mevcut projeye zaman mapping'i", fontsize=20, color="#D9EBF3", ) ax.text( 0.11, 0.68, "LNAV / HOW / TOW • ABS_SAMPLE • WLS clock bias\n" "125 MHz cross-timestamp • APB atomic commit • PI loop", fontsize=12.5, color="white", linespacing=1.55, ) self.box( ax, 0.11, 0.39, 0.77, 0.18, "Tasarım hedefi", "Navigation çözümünün ürettiği GPS zaman bilgisini, RF ölçüm epoch'unu " "kaybetmeden 125 MHz free-running zaman tabanına bağlamak; yazılım ve APB " "gecikmesini PPS fazına taşımadan düşük bant genişlikli bir PI ile kararlı " "1 PPS üretmek.", face="#173B58", edge="#4AA8C3", title_color="#8ED6E8", text_color="white", ) ax.text( 0.11, 0.19, "Kapsam: mevcut C ve RTL davranışı + sentetik event-driven doğrulama modeli", fontsize=10, color="#BFD5E1", ) ax.text( 0.11, 0.14, "Kaynak: software/pps_python", fontsize=9, color="#8FB3C6", ) ax.text(0.89, 0.06, str(self.page_number), fontsize=8, color="#8FB3C6", ha="right") self.save(fig) @staticmethod def box( ax, x: float, y: float, width: float, height: float, title: str, text: str, *, face: str = LIGHT, edge: str = "#9FB3BF", title_color: str = NAVY, text_color: str = INK, fontsize: float = 9.2, ) -> None: patch = FancyBboxPatch( (x, y), width, height, boxstyle="round,pad=0.012,rounding_size=0.012", facecolor=face, edgecolor=edge, linewidth=1.0, ) ax.add_patch(patch) ax.text( x + 0.018, y + height - 0.025, title, color=title_color, fontsize=10.2, weight="bold", va="top", ) chars = max(24, int(width * 108)) wrapped = "\n".join(textwrap.wrap(text, width=chars)) ax.text( x + 0.018, y + height - 0.063, wrapped, color=text_color, fontsize=fontsize, va="top", linespacing=1.35, ) @staticmethod def arrow(ax, start, end, color=BLUE, text: str | None = None, rad=0.0) -> None: arrow = FancyArrowPatch( start, end, arrowstyle="-|>", mutation_scale=12, linewidth=1.4, color=color, connectionstyle=f"arc3,rad={rad}", ) ax.add_patch(arrow) if text: x = (start[0] + end[0]) / 2 y = (start[1] + end[1]) / 2 + 0.018 ax.text(x, y, text, fontsize=8, color=color, ha="center") @staticmethod def paragraph(ax, x, y, text, width=92, fontsize=9.5, color=INK, spacing=1.38): paragraphs = text.split("\n") lines: list[str] = [] for paragraph in paragraphs: if paragraph.strip(): lines.extend(textwrap.wrap(paragraph, width=width)) else: lines.append("") ax.text( x, y, "\n".join(lines), fontsize=fontsize, color=color, va="top", linespacing=spacing, ) line_height = 0.0180 * (fontsize / 9.5) * spacing / 1.38 return y - len(lines) * line_height @staticmethod def bullets(ax, x, y, items, width=82, fontsize=9.2, color=INK, gap=0.014): for item in items: wrapped = textwrap.wrap(item, width=width) ax.text(x, y, "•", fontsize=fontsize + 2, color=BLUE, va="top") ax.text( x + 0.022, y, "\n".join(wrapped), fontsize=fontsize, color=color, va="top", linespacing=1.32, ) y -= len(wrapped) * 0.021 * (fontsize / 9.2) + gap return y @staticmethod def equation(ax, y, equation: str, caption: str | None = None, face=PALE_BLUE): height = 0.083 if caption else 0.06 ax.add_patch( FancyBboxPatch( (0.095, y - height), 0.81, height, boxstyle="round,pad=0.008,rounding_size=0.008", facecolor=face, edgecolor="#A9C7D7", ) ) equation_fontsize = 14.0 if len(equation) > 70: equation_fontsize = 11.5 if len(equation) > 105: equation_fontsize = 9.6 ax.text( 0.5, y - 0.027, f"${equation}$", fontsize=equation_fontsize, color=NAVY, ha="center", va="center", ) if caption: ax.text(0.5, y - 0.060, caption, fontsize=8.2, color=MUTED, ha="center") return y - height - 0.018 def page_executive(report: Report) -> None: fig, ax = report.new_page("Yönetici özeti") y = 0.85 y = report.paragraph( ax, 0.07, y, "Bu projede konum WLS'i doğru çalışırken elde edilen dördüncü state, doğrudan " "125 MHz receiver clock fazı değildir. Pseudorange'lar her epoch'ta minimum " "ABS_SAMPLE ile normalize edildiği için RF sayacının mutlak fazı observation'dan " "çıkar. WLS bias; 68.802 ms sentetik datum ile minimum kanalın gözlenen uçuş " "süresi arasındaki ortak farkı taşır.", width=100, fontsize=10.2, ) report.box( ax, 0.07, 0.61, 0.4, 0.16, "Yanlış başlangıç", "İlk HOW/TOW interrupt'ı geldiğinde PPS zamanını 'şimdi' kabul etmek. HOW transmit " "subframe sınırını etiketler; receive event bundan signal flight time kadar sonradır.", face="#FBEAEA", edge="#E1A2A2", title_color=RED, ) report.box( ax, 0.53, 0.61, 0.4, 0.16, "Doğru başlangıç", "125 MHz raw counter sürekli çalışır. İlk HOW mutlak zaman etiketini sağlar. İlk " "coherent WLS, uçuş süresini çıkarır ve GPS receive time'ı aynı epoch'un 125 MHz " "timestamp'ına bağlar.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) report.equation( ax, 0.55, r"T_{GPS,k}=T_{TX,k}+S-\frac{b_k}{c}", "Minimum corrected-ABS kanalının receive GPS zamanı; ilk delivery için TTX,k = HOW + 0.180 s", ) report.equation( ax, 0.43, r"e_k=T_{GPS,k}-T_{LOCAL}(C_{125,k})", "PI'ye girecek gerçek phase error; GPS reference − local disciplined time", face=PALE_GREEN, ) report.bullets( ax, 0.08, 0.32, [ "Minimum kanal yalnız datum'u seçer; clock bias bütün uydularla birlikte çözülür.", "WLS compute ve APB gecikmesi epoch timestamp ile taşınır; PI bu gecikmeyi oscillator hatası sanmaz.", "Raw 125 MHz counter resetlenmez. PPS, ayrı bir disciplined accumulator/NCO üzerinden üretilir.", "İlk dört uydulu çözüm zaman açısından daha gürültülüdür; coarse alignment yapılabilir fakat loop düşük bant genişlikli tutulmalıdır.", ], width=94, ) report.save(fig) def page_time_vocabulary(report: Report) -> None: fig, ax = report.new_page("Önce zamanları ayıralım") report.box( ax, 0.06, 0.70, 0.27, 0.15, "GPS system time", "Uyduların navigation mesajında etiketlenen ortak zaman. HOW Z-count bunun 6 saniyelik kaba sınırını verir.", face=PALE_BLUE, ) report.box( ax, 0.365, 0.70, 0.27, 0.15, "RF sample time", "16.368 MHz AFE kenarlarının sayımı. ABS_SAMPLE sinyalin receiver'a hangi RF sample civarında ulaştığını ölçer.", face=PALE_ORANGE, edge="#D9B58D", title_color=ORANGE, ) report.box( ax, 0.67, 0.70, 0.27, 0.15, "Tracking/PPS time", "125 MHz free-running counter ve ondan türetilen disciplined GPS accumulator. Fiziksel PPS kenarı 8 ns grid üzerindedir.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) report.arrow(ax, (0.33, 0.775), (0.365, 0.775), text="ölçüm") report.arrow(ax, (0.635, 0.775), (0.67, 0.775), text="mapping") y = 0.64 y = report.paragraph( ax, 0.075, y, "Sorunun özü üç zamanın aynı şey sanılmasıdır. GPS TOW transmit zamanını, " "ABS_SAMPLE receive olayını, 125 MHz counter ise local oscillator fazını temsil eder. " "PPS tasarımı bu üçü arasında timestamp'lı bir dönüşüm kurar.", width=99, fontsize=10, ) report.equation(ax, 0.52, r"t_{RX,i}=t_{TX,i}+\tau_i", "Signal time-of-flight ile transmit → receive dönüşümü") report.equation(ax, 0.41, r"C_{125}(t)\simeq C_0+125\,\mathrm{MHz}\,(1+\epsilon_f)t", "Free-running local oscillator; εf drift/frequency error") report.equation(ax, 0.30, r"T_{DISC}(C)=T_0+\alpha(C-C_0)", "PPS modülünün kuracağı GPS-time mapping; α PI ile yavaşça disipline edilir", face=PALE_GREEN) report.box( ax, 0.12, 0.09, 0.76, 0.10, "Temel kural", "Bir zaman değeri tek başına yeterli değildir. Her GPS zaman referansı, geçerli olduğu hardware counter değeri ve epoch sequence ile birlikte taşınmalıdır.", face="#F6F8F9", ) report.save(fig) def page_lnav_timeline(report: Report) -> None: fig, ax = report.new_page("GPS L1 C/A LNAV zaman hiyerarşisi") ax.text(0.07, 0.84, "Bir navigation bit", fontsize=9, color=MUTED) ax.add_patch(Rectangle((0.20, 0.815), 0.13, 0.045, facecolor=ORANGE, edgecolor="white")) ax.text(0.265, 0.837, "20 ms", color="white", weight="bold", ha="center", va="center", fontsize=9) report.arrow(ax, (0.34, 0.838), (0.43, 0.838), text="30 bit") ax.add_patch(Rectangle((0.45, 0.815), 0.18, 0.045, facecolor=BLUE, edgecolor="white")) ax.text(0.54, 0.837, "1 word = 600 ms", color="white", weight="bold", ha="center", va="center", fontsize=9) report.arrow(ax, (0.65, 0.838), (0.72, 0.838), text="10 word") ax.add_patch(Rectangle((0.74, 0.815), 0.18, 0.045, facecolor=GREEN, edgecolor="white")) ax.text(0.83, 0.837, "1 subframe = 6 s", color="white", weight="bold", ha="center", va="center", fontsize=9) ax.text(0.07, 0.75, "Bir GPS frame = 5 subframe = 30 s", fontsize=10, color=NAVY, weight="bold") colors = ["#367FA7", "#438EAE", "#519DB4", "#63ACB9", "#78BBC0"] for index in range(5): x = 0.08 + index * 0.17 ax.add_patch(Rectangle((x, 0.685), 0.16, 0.05, facecolor=colors[index], edgecolor="white")) ax.text(x + 0.08, 0.71, f"SF {index+1}\n6 s", color="white", ha="center", va="center", fontsize=8) ax.text(0.07, 0.62, "Bir subframe'in 10 word'ü", fontsize=10, color=NAVY, weight="bold") for index in range(10): x = 0.08 + index * 0.085 color = ORANGE if index < 2 else "#C9D7DE" ax.add_patch(Rectangle((x, 0.555), 0.078, 0.05, facecolor=color, edgecolor="white")) label = "TLM" if index == 0 else "HOW" if index == 1 else f"W{index+1}" ax.text(x + 0.039, 0.58, label, color="white" if index < 2 else INK, ha="center", va="center", fontsize=7.5, weight="bold" if index < 2 else "normal") report.arrow(ax, (0.205, 0.545), (0.86, 0.49), color=ORANGE, text="HOW Z-count: sonraki 6 s subframe sınırının transmit-time etiketi", rad=-0.12) ax.add_patch(FancyBboxPatch((0.18, 0.445), 0.64, 0.045, boxstyle="round,pad=0.006", facecolor=PALE_ORANGE, edgecolor=ORANGE)) ax.text(0.50, 0.467, "Mevcut hardware 9. takip eden bitte teslim eder: 9 × 20 ms = POSTPONE_TOW = 180 ms", fontsize=7.6, color=ORANGE, ha="center", va="center", weight="bold") report.box( ax, 0.07, 0.31, 0.41, 0.13, "Projede hardware ne yapıyor?", "TLM preamble detection, word polarity/tümleyen ve parity akışı tracking wrapper'da yapılır. Software'e 10 adet navigation word ve subframe-ready interrupt gelir.", face=PALE_BLUE, ) report.box( ax, 0.52, 0.31, 0.41, 0.13, "Software ne yapıyor?", "parse_subframe(), HOW alanını çıkarıp Z-count × 6 ile tow.sec yapar; subframe 1–3 ephemeris, SF4 page 18 iono/UTC, SF5 almanac referansını günceller.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) report.box( ax, 0.12, 0.12, 0.76, 0.10, "PPS açısından kritik anlam", "HOW zaman etiketi receiver interrupt anı değildir. Uydunun saat düzeltmesi, signal flight time ve tracking/pipeline gecikmesi eklenmeden 125 MHz local counter'a bağlanamaz.", face="#FBEAEA", edge="#E1A2A2", title_color=RED, ) report.save(fig) def page_tx_rx(report: Report) -> None: fig, ax = report.new_page("Transmit subframe sınırı receiver'a ne zaman gelir?") ax.text(0.08, 0.82, "GPS transmit time", fontsize=9, color=NAVY, weight="bold") ax.plot([0.12, 0.88], [0.76, 0.76], color=NAVY, linewidth=2) for x, label in [(0.22, "SF n başı"), (0.62, "SF n+1 başı\nHOW etiketi")]: ax.plot([x, x], [0.735, 0.785], color=NAVY, linewidth=2) ax.text(x, 0.70, label, fontsize=8, ha="center", color=NAVY) satellites = [("SV-A", 0.62, 0.67, "yakın"), ("SV-B", 0.62, 0.655, "orta"), ("SV-C", 0.62, 0.635, "uzak")] ax.text(0.08, 0.60, "Receiver events", fontsize=9, color=GREEN, weight="bold") ax.plot([0.12, 0.88], [0.54, 0.54], color=GREEN, linewidth=2) for name, tx_x, rx_x, distance in satellites: report.arrow(ax, (tx_x, 0.75), (rx_x, 0.55), color=ORANGE) ax.plot([rx_x, rx_x], [0.515, 0.565], color=GREEN, linewidth=1.6) ax.text(rx_x, 0.48, f"{name}\n{distance}", ha="center", fontsize=7.5) ax.annotate( "τi = path / c − satellite clock", xy=(0.69, 0.64), xytext=(0.73, 0.69), arrowprops=dict(arrowstyle="->", color=ORANGE), fontsize=8, color=ORANGE, ) report.equation(ax, 0.42, r"T_{RX,i}=T_{HOW}+\frac{\rho_i+I_i+T_i}{c}-\delta t_{SV,i}", "Aynı transmit sınırı her kanalda farklı receive timestamp üretir") ax.text(0.50, 0.325, r"Hardware delivery epoch: $T_{event,i}=T_{HOW}+0.180\,s+\tau_i$", fontsize=9.2, color=ORANGE, ha="center", weight="bold") report.box( ax, 0.07, 0.19, 0.41, 0.12, "Bütün kanallar zaten locked ise", "Aynı subframe sınırında en kısa gözlenen uçuş süresine sahip kanal önce event üretir. Fark birkaç ms mertebesinde olabilir.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) report.box( ax, 0.52, 0.19, 0.41, 0.12, "Cold-start sırasında", "Acquisition tamamlanması, bit lock ve hangi tam subframe'in yakalandığı 6 saniyelik adımlarla değişebilir; ilk HOW yalnız yakınlıkla seçilmez.", face=PALE_ORANGE, edge="#D9B58D", title_color=ORANGE, ) report.save(fig) def page_project_map(report: Report) -> None: fig, ax = report.new_page("Mevcut projede ölçümün uçtan uca akışı") boxes = [ (0.06, 0.73, 0.17, 0.10, "RF / AFE", "16.368 MHz\n4-bit RF data", PALE_ORANGE, ORANGE), (0.28, 0.73, 0.20, 0.10, "sample_counter.sv", "trk_clk alanında\nAFE edge sayımı", PALE_ORANGE, ORANGE), (0.54, 0.73, 0.18, 0.10, "trck_top / bit_gen", "ABS_SAMPLE\nREM_CODE_PHASE", PALE_BLUE, BLUE), (0.77, 0.73, 0.17, 0.10, "tracking_wrapper", "PCLK register\ninterrupt flags", PALE_BLUE, BLUE), (0.77, 0.54, 0.17, 0.10, "Tracking ISR", "event queue +\nshadow counter", LIGHT, NAVY), (0.54, 0.54, 0.18, 0.10, "Linked-list state", "TOW owner\nsequence + sample", LIGHT, NAVY), (0.28, 0.54, 0.20, 0.10, "Coherent snapshot", "25 ms window\nmodulo unwrap", PALE_GREEN, GREEN), (0.06, 0.54, 0.17, 0.10, "Pseudorange", "minimum +\n68.802 ms", PALE_GREEN, GREEN), (0.06, 0.34, 0.17, 0.10, "Satellite model", "TOW + 180 ms\nephemeris/clock", PALE_BLUE, BLUE), (0.32, 0.34, 0.23, 0.10, "Weighted LS", "[X Y Z b]\niono + tropo + URA/el", PALE_GREEN, GREEN), (0.64, 0.34, 0.30, 0.10, "Bugünkü çıkış", "position log + static last_solution[4]\nclock bias dışarı taşınmıyor", "#FBEAEA", RED), ] for x, y, w, h, title, text, face, color in boxes: report.box(ax, x, y, w, h, title, text, face=face, edge=color, title_color=color, fontsize=7.5) for start, end in [ ((0.23, 0.78), (0.28, 0.78)), ((0.48, 0.78), (0.54, 0.78)), ((0.72, 0.78), (0.77, 0.78)), ((0.855, 0.73), (0.855, 0.64)), ((0.77, 0.59), (0.72, 0.59)), ((0.54, 0.59), (0.48, 0.59)), ((0.28, 0.59), (0.23, 0.59)), ((0.145, 0.54), (0.145, 0.44)), ((0.23, 0.39), (0.32, 0.39)), ((0.55, 0.39), (0.64, 0.39)), ]: report.arrow(ax, start, end) report.box( ax, 0.08, 0.14, 0.84, 0.11, "PPS için eklenecek veri bağı", "ABS_SAMPLE ve REM_CODE_PHASE ile aynı logical measurement eventinde 64-bit tracking_counter_125 latch edilecek. WLS sonucu [b, GPS transmit label, reference SVID, counter timestamp, epoch sequence] olarak atomik taşınacak.", face="#E8F4F8", edge=CYAN, title_color=BLUE, ) report.save(fig) def page_nav_epoch(report: Report) -> None: fig, ax = report.new_page("Nav-solve epoch ve coherency davranışı") ax.text(0.07, 0.84, "Transmit nav bit sınırları (20 ms)", fontsize=9, color=NAVY, weight="bold") for index in range(11): x = 0.1 + index * 0.075 ax.plot([x, x], [0.785, 0.82], color=NAVY, linewidth=1) if index in (0, 10): ax.text(x, 0.76, f"{index*20} ms", fontsize=7, ha="center", color=MUTED) ax.plot([0.1, 0.85], [0.80, 0.80], color=NAVY, linewidth=1.4) ax.annotate("NAV_SOLVE_TIMER=10", xy=(0.475, 0.825), ha="center", fontsize=8.5, color=BLUE) rows = [("SV 2", 0.12, BLUE), ("SV 5", 0.145, GREEN), ("SV 7", 0.17, ORANGE), ("SV 9", 0.19, RED)] base_y = 0.66 for row, (name, delay, color) in enumerate(rows): y = base_y - row * 0.08 ax.text(0.07, y, name, fontsize=8.5, color=color, va="center") ax.plot([0.14, 0.90], [y, y], color="#C8D2D8", linewidth=1) x = 0.62 + delay ax.plot([x, x], [y - 0.022, y + 0.022], color=color, linewidth=3) ax.text(x, y + 0.03, "ABS/REM", fontsize=6.8, ha="center", color=color) ax.add_patch(Rectangle((0.72, 0.385), 0.18, 0.32, fill=False, edgecolor=CYAN, linewidth=1.5, linestyle="--")) ax.text(0.81, 0.37, "25 ms coherence window", fontsize=8, color=CYAN, ha="center") report.box( ax, 0.07, 0.20, 0.40, 0.11, "Software snapshot", "En yeni raw ABS_SAMPLE referans alınır. Diğer kanallar signed modulo-32 delta ile unwrap edilir; corrected double yalnız pseudorange'a gider.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) report.box( ax, 0.53, 0.20, 0.40, 0.11, "TOW ownership", "İlk gerçek nav-solve eventine kadar HOW günceller. Sonra her event TOW'u sabit N×20 ms artırır; arkadaki nav bits ephemeris'i günceller fakat live TOW'u yazmaz.", face=PALE_BLUE, ) ax.text( 0.08, 0.13, "PPS timestamp da aynı snapshot kuralını izlemeli; ISR'ın okuma anı ölçüm epoch'u değildir.", fontsize=9.5, color=RED, weight="bold", ) report.save(fig) def page_pseudorange(report: Report) -> None: fig, ax = report.new_page("Mevcut pseudorange datum'u nasıl kuruluyor?") y = 0.84 ax.text(0.08, y, "1 Fractional RF sample düzeltmesi", fontsize=11, color=NAVY, weight="bold") y = report.equation(ax, y - 0.025, r"A_i^{corr}=A_i^{raw}-\frac{REM_i}{1.023\times10^6}") ax.text(0.08, y, "2 Wrap-safe coherent unwrap ve minimum seçimi", fontsize=11, color=NAVY, weight="bold") y = report.equation(ax, y - 0.025, r"A_{min}=\min_i\left(A_i^{corr}\right)") ax.text(0.08, y, "3 C kodundaki sentetik propagation datum'u", fontsize=11, color=NAVY, weight="bold") y = report.equation( ax, y - 0.025, r"P'_i=c\left[\frac{A_i^{corr}-A_{min}}{16.368\,MHz}+S\right]+c\,\delta t_{SV,i}", "S = 68.802 ms; gps_ip.c içinde ms formunda hesaplanır", face=PALE_ORANGE, ) report.box( ax, 0.07, 0.17, 0.40, 0.14, "Minimum neden var?", "Raw ABS_SAMPLE'ın receiver açılışına bağlı büyük ortak sayısını çıkarır. Uydular arası arrival-time farkları korunur; konum geometrisi bu farklarda bulunur.", face=PALE_BLUE, ) report.box( ax, 0.53, 0.17, 0.40, 0.14, "68.802 ms neden var?", "Göreli range'leri fiziksel 20–26 bin km büyüklüğüne taşır ve WLS common bias'ı sıfıra yakın tutar. Yeni bilgi eklemez; datum seçer.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) ax.text( 0.10, 0.10, "Sonuç: common RF counter fazı (A0) minimum çıkarımında iptal olur. Bu nedenle WLS b doğrudan physical 125 MHz clock phase değildir.", fontsize=9.2, color=RED, weight="bold", ) report.save(fig) def page_wls_theory(report: Report) -> None: fig, ax = report.new_page("Weighted least squares ve dördüncü state") y = 0.84 y = report.paragraph( ax, 0.075, y, "Standart GNSS observation modeli receiver position ve receiver clock bias'ı aynı anda çözer. " "Bu projede uydu saat düzeltmesi pseudorange'a eklenir; ionosphere ve troposphere WLS residual modelinde çıkarılır.", width=99, fontsize=10, ) y = report.equation(ax, y - 0.01, r"P_i=\rho_i(X,Y,Z)+b+I_i+T_i+\epsilon_i") y = report.equation(ax, y, r"\mathbf{x}=[X\;Y\;Z\;b]^T,\qquad A_i=[-\hat{u}_{ix}\;-\hat{u}_{iy}\;-\hat{u}_{iz}\;1]") y = report.equation(ax, y, r"\Delta\mathbf{x}=(A^TWA)^{-1}A^TW\,\mathbf{r}") report.box( ax, 0.07, 0.30, 0.40, 0.16, "Ağırlık modeli", "URA varyansı ile elevation-dependent 3 m/sin(el) varyansı toplanır; ağırlık 1/variance olur ve maksimum ağırlığa normalize edilir. Normalize etmek çözümü değiştirmez.", face=PALE_BLUE, ) report.box( ax, 0.53, 0.30, 0.40, 0.16, "Projeye özgü b", "Matematiksel olarak common range bias state'idir. Fakat observation minimum ABS_SAMPLE ve S ile tanımlandığı için fiziksel local timer − GPS time anlamı ancak TOW ve timestamp ile handover yapıldığında oluşur.", face=PALE_ORANGE, edge="#D9B58D", title_color=ORANGE, ) report.box( ax, 0.10, 0.12, 0.80, 0.10, "C implementasyon notu", "last_solution[4] bütün state'leri bir sonraki solve'a seed eder. Convergence normVect(xVec) ile yalnız XYZ update'e bakar; final clock update Python CSV'sinde ayrıca izlenir.", face="#F6F8F9", ) report.save(fig) def page_bias_derivation(report: Report) -> None: fig, ax = report.new_page("WLS bias'tan signal flight time nasıl çıkar?") y = 0.84 ax.text(0.08, y, "Aynı transmit nav-bit sınırı için receiver arrival zamanı:", fontsize=10, color=NAVY, weight="bold") y = report.equation(ax, y - 0.025, r"t_{RX,i}=t_{TX}+\frac{\rho_i+I_i+T_i}{c}-\delta t_{SV,i}") ax.text(0.08, y, "Bu ifade minimum-normalized observation'a yerleştirilirse:", fontsize=10, color=NAVY, weight="bold") y = report.equation( ax, y - 0.025, r"b=cS-(\rho_{min}+I_{min}+T_{min})+c\,\delta t_{SV,min}", "b, bütün uydularla çözülür fakat observation origin'i minimum kanaldır", face=PALE_ORANGE, ) ax.text(0.08, y, "Dolayısıyla minimum kanalın gözlenen uçuş süresi:", fontsize=10, color=NAVY, weight="bold") y = report.equation( ax, y - 0.025, r"\tau_{min}^{obs}=S-\frac{b}{c}=\frac{\rho_{min}+I_{min}+T_{min}}{c}-\delta t_{SV,min}", "Bu değer transmit GPS time'ı receive GPS time'a taşır", face=PALE_GREEN, ) report.box( ax, 0.07, 0.16, 0.40, 0.13, "Neden raw b/c PI'ye verilmez?", "Referans uydu değişince path_min değişir ve b/c milisaniyelik basamak yapar. Bu oscillator phase step değildir; datum değişimidir.", face="#FBEAEA", edge="#E1A2A2", title_color=RED, ) report.box( ax, 0.53, 0.16, 0.40, 0.13, "Neden TOW + S − b/c süreklidir?", "Her epoch'un transmit etiketi ve seçilen kanalın timestamp'ı birlikte değişir. Doğru çift kurulduğunda receiver GPS time sürekli kalır.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) report.save(fig) def page_counter_mapping(report: Report) -> None: fig, ax = report.new_page("GPS time ile 125 MHz counter mapping'i") ax.text(0.08, 0.84, "İlk coherent WLS epoch'u k", fontsize=11, color=NAVY, weight="bold") boxes = [ (0.07, 0.68, 0.25, 0.11, "Transmit etiketi", "İlk delivery: HOW + 0.180 s\nsonra nav-solve epoch", PALE_BLUE, BLUE), (0.375, 0.68, 0.25, 0.11, "WLS flight time", "τmin = S − b/c\nall-satellite solution", PALE_GREEN, GREEN), (0.68, 0.68, 0.25, 0.11, "Receive reference", "T_GPS,k = T_TX,k + τmin", PALE_ORANGE, ORANGE), (0.375, 0.47, 0.25, 0.11, "Hardware timestamp", "C125,k\naynı ABS/REM event", PALE_BLUE, BLUE), (0.68, 0.47, 0.25, 0.11, "Mapping anchor", "(C125,k, T_GPS,k, seq)", PALE_GREEN, GREEN), ] for x, y, w, h, title, text, face, color in boxes: report.box(ax, x, y, w, h, title, text, face=face, edge=color, title_color=color, fontsize=8) report.arrow(ax, (0.32, 0.735), (0.375, 0.735)) report.arrow(ax, (0.625, 0.735), (0.68, 0.735)) report.arrow(ax, (0.805, 0.68), (0.805, 0.58)) report.arrow(ax, (0.625, 0.525), (0.68, 0.525)) ax.text( 0.50, 0.632, "Ownership: baremetal T_TX,k = eph.tow + POSTPONE_TOW'u bir kez kurar;\n" "PPS peripheral tekrar eklemez.", fontsize=7.2, color=RED, ha="center", va="top", weight="bold", linespacing=1.2, ) report.equation( ax, 0.39, r"T_{GPS}(C)=T_{GPS,k}+\alpha_k(C-C_{125,k})", "Başlangıçta α ≈ 1/125 MHz; TRACK state'inde PI ile küçük frequency düzeltmesi eklenir", face=PALE_GREEN, ) report.box( ax, 0.07, 0.14, 0.40, 0.12, "Bir başka uydu kullanılırsa", "Kendi ρ+I+T ve satellite-clock düzeltmesiyle kendi receive time'ı bulunur; kendi C125 timestamp'ına bağlandığında aynı affine mapping çıkmalıdır.", face=PALE_BLUE, ) report.box( ax, 0.53, 0.14, 0.40, 0.12, "Coherency residual", "Per-SV mapping farkları PPS'i ayrı ayrı başlatmak için değil, stale sample, yanlış TOW veya channel timing hatasını yakalamak için kullanılabilir.", face=PALE_ORANGE, edge="#D9B58D", title_color=ORANGE, ) report.save(fig) def page_state_machine(report: Report) -> None: fig, ax = report.new_page("PPS zaman ownership state machine") states = { "FREE_RUNNING": (0.08, 0.68, PALE_BLUE, BLUE), "TOW_KNOWN": (0.39, 0.68, PALE_ORANGE, ORANGE), "COARSE_ALIGNED": (0.69, 0.68, PALE_GREEN, GREEN), "TRACK": (0.69, 0.42, PALE_GREEN, GREEN), "HOLDOVER": (0.39, 0.42, LIGHT, NAVY), } descriptions = { "FREE_RUNNING": "Raw 125 MHz counter çalışır\nGPS time valid değil", "TOW_KNOWN": "İlk valid HOW/week\n6 s absolute label", "COARSE_ALIGNED": "İlk coherent WLS\nanchor atomik commit", "TRACK": "Her epoch phase error\nPI rate discipline", "HOLDOVER": "Son frequency tutulur\nPPS quality downgraded", } for name, (x, y, face, color) in states.items(): report.box(ax, x, y, 0.23, 0.12, name, descriptions[name], face=face, edge=color, title_color=color, fontsize=8) report.arrow(ax, (0.31, 0.74), (0.39, 0.74), text="ilk HOW") report.arrow(ax, (0.62, 0.74), (0.69, 0.74), text="WLS + timestamp") report.arrow(ax, (0.805, 0.68), (0.805, 0.54), text="coarse commit") report.arrow(ax, (0.69, 0.48), (0.62, 0.48), text="solution loss") report.arrow(ax, (0.51, 0.54), (0.70, 0.64), text="valid WLS returns", rad=-0.22) report.box( ax, 0.08, 0.20, 0.40, 0.12, "PPS valid politikası", "TOW_KNOWN yalnız kaba time-of-week sağlar. Accurate PPS valid bayrağı COARSE_ALIGNED sonrası açılır. Dört uydu çözümü valid olabilir fakat quality düşük raporlanır.", face=PALE_BLUE, ) report.box( ax, 0.53, 0.20, 0.40, 0.12, "Sayaç reset politikası", "Raw counter hiçbir state geçişinde resetlenmez. Coarse alignment yalnız disciplined accumulator'ın anchor/offset'ini değiştirir; TRACK sırasında phase jump yapılmaz.", face="#FBEAEA", edge="#E1A2A2", title_color=RED, ) report.save(fig) def page_cross_timestamp(report: Report) -> None: fig, ax = report.new_page("Cross-timestamp ve clock-domain sınırı") ax.add_patch(Rectangle((0.06, 0.40), 0.40, 0.40, facecolor="#FFF6EC", edgecolor=ORANGE, linewidth=1.2)) ax.add_patch(Rectangle((0.54, 0.40), 0.40, 0.40, facecolor="#EAF5F8", edgecolor=BLUE, linewidth=1.2)) ax.text(0.08, 0.77, "RF / measurement tarafı", color=ORANGE, weight="bold", fontsize=11) ax.text(0.56, 0.77, "125 MHz / PPS tarafı", color=BLUE, weight="bold", fontsize=11) report.box(ax, 0.10, 0.61, 0.31, 0.10, "sample_counter", "AFE edge count\nABS_SAMPLE raw", face=PALE_ORANGE, edge=ORANGE, title_color=ORANGE, fontsize=8) report.box(ax, 0.10, 0.46, 0.31, 0.10, "code boundary", "REM_CODE_PHASE\ncorrected ABS", face=PALE_ORANGE, edge=ORANGE, title_color=ORANGE, fontsize=8) report.box(ax, 0.59, 0.61, 0.30, 0.10, "raw_counter_125", "64-bit, always running\n8 ns integer tick", face=PALE_BLUE, edge=BLUE, title_color=BLUE, fontsize=8) report.box(ax, 0.59, 0.46, 0.30, 0.10, "source snapshot", "counter latch + event toggle\nstable bundled data", face=PALE_GREEN, edge=GREEN, title_color=GREEN, fontsize=8) report.arrow(ax, (0.41, 0.515), (0.59, 0.515), color=CYAN, text="aynı logical event") report.arrow(ax, (0.74, 0.61), (0.74, 0.56), color=BLUE) report.box( ax, 0.07, 0.23, 0.40, 0.10, "CDC kuralı", "Increment eden 64-bit counter bus'ı doğrudan PCLK'de okunmaz. Source domain latch + toggle/ack veya yeterince uzun stable bundled-data protokolü kullanılır.", face="#FBEAEA", edge="#E1A2A2", title_color=RED, ) report.box( ax, 0.53, 0.23, 0.40, 0.10, "Fractional düzeltme", "REM correction RF raw edge'den code boundary'ye geri taşır. Aynı süre 125 MHz timestamp'a da Q-format fractional tick olarak uygulanmalıdır.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) ax.text( 0.09, 0.14, "Önerilen measurement tuple: {epoch_seq, raw_abs, rem_code_phase, tracking_counter_125, SVID}", fontsize=9.3, color=NAVY, weight="bold", ) report.save(fig) def page_apb_latency(report: Report) -> None: fig, ax = report.new_page("WLS compute ve APB gecikmesi faza nasıl karışmaz?") timeline_y = 0.74 ax.plot([0.08, 0.92], [timeline_y, timeline_y], color=NAVY, linewidth=2) events = [ (0.14, "capture\nCk, ABSk", BLUE), (0.30, "son uydu\nsnapshot hazır", GREEN), (0.59, "WLS bitti", ORANGE), (0.78, "APB COMMIT", RED), (0.89, "current Cnow", NAVY), ] for x, label, color in events: ax.plot([x, x], [timeline_y - 0.03, timeline_y + 0.03], color=color, linewidth=2) ax.text(x, timeline_y - 0.065, label, fontsize=7.5, ha="center", color=color) ax.annotate("satellite arrival span", xy=(0.22, 0.79), ha="center", fontsize=7.5, color=GREEN) ax.annotate("compute + scheduler", xy=(0.445, 0.79), ha="center", fontsize=7.5, color=ORANGE) ax.annotate("APB", xy=(0.685, 0.79), ha="center", fontsize=7.5, color=RED) report.equation( ax, 0.59, r"T_{GPS,now}=T_{GPS,k}+\alpha(C_{now}-C_k)", "Reference eski epoch'a aittir; peripheral counter farkıyla commit anına taşır", face=PALE_GREEN, ) report.box( ax, 0.07, 0.34, 0.40, 0.13, "Timestamp yoksa", "T_GPS,k değeri APB geldiği anda geçerli sanılır. Capture→commit süresi doğrudan phase offset gibi görünür ve PI oscillator'ı yanlış yönde sürer.", face="#FBEAEA", edge="#E1A2A2", title_color=RED, ) report.box( ax, 0.53, 0.34, 0.40, 0.13, "Timestamp varsa", "Software latency ölçümden çıkar. Geriye feedback komutunun gecikmesi kalır; düşük bant genişlikli loop içinde yalnız phase margin'i etkiler.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, ) report.equation(ax, 0.26, r"E_{used}(s)=E(s)e^{-sD}", "D sabit feedback delay; bandwidth yükseldikçe phase margin azalır") ax.text(0.10, 0.12, "APB register'ları shadow olmalı; bütün 64-bit/fractional alanlar yazıldıktan sonra tek sequence-tagged COMMIT yapılmalı.", fontsize=9.3, color=NAVY, weight="bold") report.save(fig) def page_pi(report: Report) -> None: fig, ax = report.new_page("PI kontrollü disciplined PPS mimarisi") report.box(ax, 0.06, 0.69, 0.17, 0.11, "WLS reference", "e = GPS − local\n5 Hz update", face=PALE_BLUE, edge=BLUE, title_color=BLUE, fontsize=8) report.box(ax, 0.29, 0.69, 0.17, 0.11, "PI", "Kp e + ∫Ki e dt\nanti-windup", face=PALE_GREEN, edge=GREEN, title_color=GREEN, fontsize=8) report.box(ax, 0.52, 0.69, 0.18, 0.11, "Rate word", "nominal increment\n+ signed correction", face=PALE_ORANGE, edge=ORANGE, title_color=ORANGE, fontsize=8) report.box(ax, 0.76, 0.69, 0.18, 0.11, "GPS accumulator", "fractional phase\ninteger-second wrap", face=PALE_GREEN, edge=GREEN, title_color=GREEN, fontsize=8) report.arrow(ax, (0.23, 0.745), (0.29, 0.745)) report.arrow(ax, (0.46, 0.745), (0.52, 0.745)) report.arrow(ax, (0.70, 0.745), (0.76, 0.745)) report.arrow(ax, (0.85, 0.69), (0.85, 0.61), text="PPS") report.arrow(ax, (0.76, 0.66), (0.15, 0.66), color=CYAN, text="local time at captured counter", rad=-0.08) report.equation(ax, 0.55, r"I_k=clip(I_{k-1}+K_iT_s e_k)") report.equation(ax, 0.46, r"u_k=clip(I_k+K_p e_k),\qquad f_{disc}=f_{raw}(1+u_k)") report.box( ax, 0.07, 0.22, 0.40, 0.14, "Neden raw counter değişmez?", "Debug ve timestamp monotonic kalır; phase jump oluşmaz. Coarse alignment ve PI yalnız GPS accumulator mapping'ini değiştirir.", face=PALE_BLUE, ) report.box( ax, 0.53, 0.22, 0.40, 0.14, "125 MHz ne sağlar?", "Fiziksel PPS edge 8 ns grid'dedir. Fractional accumulator uzun dönem frequency çözünürlüğünü iyileştirir fakat tek edge'i clock kenarları arasına koyamaz.", face=PALE_ORANGE, edge="#D9B58D", title_color=ORANGE, ) ax.text(0.09, 0.13, "Python başlangıç parametreleri: natural frequency 0.035 Hz, damping 0.9, max slew 20 ppm. Final değerler kart oscillator logundan seçilmelidir.", fontsize=8.8, color=RED, weight="bold") report.save(fig) def page_control_loop_model(report: Report) -> None: fig, ax = report.new_page("Kontrol loop'unun kapalı çevrim modeli") # Main forward path: phase detector -> PI -> delayed rate actuation -> # oscillator/NCO frequency sum -> phase integrator. sum_xy = (0.16, 0.72) ax.add_patch(Circle(sum_xy, 0.027, facecolor="white", edgecolor=NAVY, linewidth=1.4)) ax.text(sum_xy[0], sum_xy[1], "Σ", fontsize=12, color=NAVY, ha="center", va="center", weight="bold") ax.text(0.105, 0.75, "+", fontsize=10, color=GREEN, weight="bold") ax.text(0.145, 0.675, "−", fontsize=11, color=RED, weight="bold") ax.text(0.055, 0.72, r"$T_{GPS,k}$", fontsize=10, color=GREEN, va="center") report.arrow(ax, (0.105, 0.72), (0.133, 0.72), color=GREEN) report.box(ax, 0.22, 0.665, 0.15, 0.11, "PI C(z)", "Kp·e + integral\nanti-windup", face=PALE_GREEN, edge=GREEN, title_color=GREEN, fontsize=7.6) report.box(ax, 0.42, 0.665, 0.16, 0.11, "Actuation delay", "z⁻ᵈ / variable D\nrate clamp", face=PALE_ORANGE, edge=ORANGE, title_color=ORANGE, fontsize=7.6) freq_sum = (0.64, 0.72) ax.add_patch(Circle(freq_sum, 0.027, facecolor="white", edgecolor=NAVY, linewidth=1.4)) ax.text(freq_sum[0], freq_sum[1], "Σ", fontsize=12, color=NAVY, ha="center", va="center", weight="bold") report.box(ax, 0.70, 0.665, 0.13, 0.11, "Plant G(z)", "frequency → phase\naccumulator", face=PALE_BLUE, edge=BLUE, title_color=BLUE, fontsize=7.4) ax.text(0.905, 0.72, r"$\theta=T_{LOCAL}-T_{GPS}$", fontsize=8.2, color=NAVY, ha="center", va="center") report.arrow(ax, (0.187, 0.72), (0.22, 0.72), text="e") report.arrow(ax, (0.37, 0.72), (0.42, 0.72), text="u") report.arrow(ax, (0.58, 0.72), (0.613, 0.72)) ax.text(0.596, 0.685, "u[k−d]", fontsize=7.0, color=BLUE, ha="center") report.arrow(ax, (0.667, 0.72), (0.70, 0.72)) report.arrow(ax, (0.83, 0.72), (0.86, 0.72)) ax.text(0.64, 0.835, r"$y_{osc}$", fontsize=9.5, color=RED, ha="center") ax.text(0.64, 0.805, "raw 125 MHz\nfractional frequency error", fontsize=7.0, color=MUTED, ha="center", va="top") report.arrow(ax, (0.64, 0.79), (0.64, 0.747), color=RED) # Timestamped measurement path. The delay remains in control application, # not in the association between reference and local time. report.box( ax, 0.40, 0.49, 0.27, 0.10, "Timestamp'lı phase detector", "TGPS,k ve TLOCAL(C125,k)\naynı measurement epoch", face="#E8F4F8", edge=CYAN, title_color=BLUE, fontsize=7.6, ) report.arrow(ax, (0.88, 0.695), (0.67, 0.545), color=CYAN, rad=-0.12) report.arrow(ax, (0.40, 0.54), (0.16, 0.693), color=CYAN, text="feedback at epoch k", rad=-0.10) ax.text(0.84, 0.62, "PPS edge\nquantizer: 8 ns", fontsize=7.2, color=BLUE, ha="center") report.arrow(ax, (0.84, 0.70), (0.84, 0.64), color=BLUE) report.equation( ax, 0.43, r"C(s)=K_p+\frac{K_i}{s},\qquad G(s)=\frac{1}{s},\qquad L(s)=\frac{C(s)e^{-sD}}{s}", "PI rate düzeltmesi üretir; accumulator frequency error'ı phase'e integre eder", face=PALE_BLUE, ) report.equation( ax, 0.31, r"I_{k+1}=clip(I_k+K_iT_s e_k),\quad u_k=clip(I_k+K_pe_k)", "Ts = NAV_SOLVE_TIMER × 20 ms = 200 ms; update rate 5 Hz", face=PALE_GREEN, ) report.equation( ax, 0.19, r"\theta_{k+1}=\theta_k+T_s\left(y_{osc,k}+u_{k-d}\right)+w_k,\qquad e_k=-\theta_k+v_k", "θ local−GPS phase; v WLS/timestamp noise, w unmodelled phase disturbance", face=PALE_ORANGE, ) ax.text( 0.075, 0.085, "\n".join(textwrap.wrap( "Coarse alignment bu lineer TRACK loop'un dışında, bir defalık anchor yüklemesidir. " "HOLDOVER'da e_k güncellenmez; integrator/rate word dondurulur.", width=118, )), fontsize=7.6, color=RED, weight="bold", va="top", linespacing=1.25, ) report.save(fig) def page_sim_method(report: Report) -> None: fig, ax = report.new_page("Python event-driven modelinde ne taklit edildi?") items = [ ("RF clock", "16.368 MHz, ortak/bağımsız ppm drift, 32-bit wrap, fractional REM correction"), ("Navigation", "Ortak 20 ms transmit-bit epoch; NAV_SOLVE_TIMER=10 → 200 ms"), ("Satellite", "Sentetik gerçekçi ECEF/az-el geometrisi, satellite clock, path delay"), ("Corrections", "Earth rotation, Klobuchar, aktif C troposfer modeli, URA/elevation noise"), ("Firmware", "Modulo unwrap, minimum + 68.802 ms, clock correction, cached four-state WLS"), ("Latency", "Satellite arrival span, WLS compute, scheduler stall, APB jitter"), ("PPS", "125 MHz raw oscillator, coarse mapping, timestamped/untagged PI, 8 ns edge grid"), ] y = 0.82 for index, (name, detail) in enumerate(items): color = [BLUE, GREEN, ORANGE][index % 3] ax.add_patch(FancyBboxPatch((0.08, y - 0.055), 0.18, 0.055, boxstyle="round,pad=0.006", facecolor=color, edgecolor=color)) ax.text(0.17, y - 0.028, name, color="white", fontsize=8.7, weight="bold", ha="center", va="center") ax.add_patch(FancyBboxPatch((0.28, y - 0.055), 0.64, 0.055, boxstyle="round,pad=0.006", facecolor="#F5F7F8", edgecolor="#C4D0D7")) ax.text(0.30, y - 0.028, detail, color=INK, fontsize=8.2, va="center") y -= 0.082 report.box( ax, 0.08, 0.12, 0.84, 0.10, "Modelin sınırı", "Gerçek broadcast ephemeris ve kart oscillator phase-noise kaydı yerine sentetik geometri/drift kullanılır. Bu rapordaki sayısal PI sonucu mimariyi doğrular; final performans garantisi değildir.", face="#FBEAEA", edge="#E1A2A2", title_color=RED, ) report.save(fig) def plot_page(report: Report, title: str, image_path: Path, caption: str) -> None: fig, ax = report.new_page(title, section="PYTHON MODEL SONUÇLARI") image = mpimg.imread(image_path) image_ax = fig.add_axes([0.07, 0.13, 0.86, 0.72]) image_ax.imshow(image) image_ax.axis("off") wrapped = "\n".join(textwrap.wrap(caption, width=120)) ax.text(0.07, 0.095, wrapped, fontsize=8.5, color=MUTED, va="top", linespacing=1.35) report.save(fig) def page_registers(report: Report) -> None: fig, ax = report.new_page("Önerilen PPS APB sözleşmesi") columns = [0.07, 0.25, 0.47, 0.91] headers = ["Register", "Yön/format", "Anlam"] for index, header in enumerate(headers): x0 = columns[index] x1 = columns[index + 1] ax.add_patch(Rectangle((x0, 0.80), x1 - x0, 0.045, facecolor=NAVY, edgecolor="white")) ax.text(x0 + 0.008, 0.822, header, color="white", fontsize=8.5, weight="bold", va="center") rows = [ ("CONTROL", "RW / bitfield", "enable, coarse_commit, track_enable, holdover, clear_status"), ("STATUS", "RO / bitfield", "tow_known, aligned, tracking, pps_valid, stale_seq, saturation"), ("RAW_COUNTER", "RO / uint64", "Değiştirilmeyen 125 MHz free-running counter"), ("EPOCH_COUNTER", "WO shadow / uint64", "WLS referansının geçerli olduğu captured counter"), ("GPS_SECONDS", "WO shadow / uint64", "Baremetal tarafından epoch-aligned edilmiş GPS reference second"), ("GPS_FRACTION", "WO shadow / Q0.32", "Receive GPS time fractional second"), ("EPOCH_SEQUENCE", "WO shadow / uint32", "Out-of-order/stale APB commit koruması"), ("KP / KI", "RW / fixed point", "PI coefficients; units register spec'te açık tanımlanmalı"), ("MAX_SLEW", "RW / signed ppm/Q", "Rate correction clamp ve anti-windup sınırı"), ("COMMIT", "WO / pulse", "Bütün shadow alanları tek tracking-clock eventinde aktive eder"), ("PPS_DIAG", "RO / signed", "Son phase error, integrator, rate word, age/quality"), ] y = 0.755 row_h = 0.057 for row_index, row in enumerate(rows): face = "#F6F8F9" if row_index % 2 == 0 else "white" for index, value in enumerate(row): x0 = columns[index] x1 = columns[index + 1] ax.add_patch(Rectangle((x0, y), x1 - x0, row_h, facecolor=face, edgecolor="#D2DADF", linewidth=0.6)) wrapped = "\n".join(textwrap.wrap(value, width=[18, 21, 53][index])) ax.text(x0 + 0.008, y + row_h - 0.012, wrapped, fontsize=7.1, color=INK, va="top", linespacing=1.18) y -= row_h report.box( ax, 0.08, 0.075, 0.84, 0.075, "Format ilkesi", "Hardware'a double taşınmaz. Counter integer tick, GPS fraction ve rate correction için açık signed/unsigned Q formatları; software conversion ve saturation kuralları birlikte tanımlanır.", face=PALE_ORANGE, edge="#D9B58D", title_color=ORANGE, fontsize=7.8, ) report.save(fig) def page_implementation(report: Report) -> None: fig, ax = report.new_page("Uygulama sırası: ölçülebilir küçük adımlar") steps = [ ("1", "Measurement contract", "WLS result struct: XYZ, b, reference SVID, corrected ABS, C125, TOW, sequence."), ("2", "Timestamp-only RTL", "64-bit raw counter ve ABS/REM ile paired per-channel capture; PPS üretme."), ("3", "Cross-check", "ΔC125 ≈ ΔABS × 125/16.368 ilişkisi, wrap ve fractional REM testi."), ("4", "Open-loop coarse", "İlk HOW + coherent WLS ile affine anchor; PI=0, yalnız PPS scheduling."), ("5", "Latency injection", "WLS/APB'ye yapay delay ekle; timestamp'lı PPS fazı değişmemeli."), ("6", "P-only", "İşaret, Q format, max slew ve phase response doğrulaması."), ("7", "PI track", "Kart drift/noise logundan Kp/Ki seç; saturation ve holdover testleri."), ("8", "System tests", "4/5/7 SV, drop/re-acq, reference change, week rollover, stale commit."), ] y = 0.82 for number, title, detail in steps: ax.add_patch(FancyBboxPatch((0.08, y - 0.055), 0.06, 0.055, boxstyle="circle,pad=0.008", facecolor=BLUE, edgecolor=BLUE)) ax.text(0.11, y - 0.028, number, color="white", fontsize=9, weight="bold", ha="center", va="center") ax.text(0.17, y - 0.006, title, fontsize=9.3, color=NAVY, weight="bold", va="top") ax.text(0.17, y - 0.030, "\n".join(textwrap.wrap(detail, 82)), fontsize=8.2, color=INK, va="top", linespacing=1.2) if number != "8": ax.plot([0.11, 0.11], [y - 0.063, y - 0.082], color=CYAN, linewidth=1.4) y -= 0.086 report.box( ax, 0.10, 0.075, 0.80, 0.075, "Go/no-go kuralı", "Bir aşamanın timestamp ve sign testi geçmeden sonraki kontrol katmanı açılmaz. İlk hardware hedefi iyi PPS değil, doğru ölçüm epoch'udur.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, fontsize=8, ) report.save(fig) def page_verification(report: Report) -> None: fig, ax = report.new_page("Doğrulama matrisi ve beklenen sonuç") tests = [ ("ABS common +5 ms", "XYZ aynı; normalized b aynı", "Observation common phase iptalini kanıtlar"), ("STARTOFFSET +1 ms", "XYZ aynı; b +c×1 ms", "Datum dönüşüm işaretini kanıtlar"), ("32-bit ABS wrap", "Pseudorange süreklidir", "Signed modulo unwrap"), ("Reference SVID değişimi", "Raw b step; mapped GPS time continuous", "Min datum ownership"), ("APB +50 ms delay", "Timestamped PPS fazı değişmez", "Epoch association"), ("Out-of-order commit", "Eski sequence reject", "Stale result koruması"), ("4 → 7 satellite", "Timing noise azalır; loop stabil kalır", "Cold-start geometri"), ("Tracking drop/re-acq", "No phase jump; quality/holdover doğru", "State transition"), ("GPS week rollover", "Continuous seconds ve PPS düzgün", "TOW normalization"), ("HOW pipeline sweep", "Sabit calibrated offset", "POSTPONE/pipeline semantiği"), ] columns = [0.06, 0.29, 0.61, 0.94] for index, header in enumerate(["Test", "Beklenen", "Kanıtlanan"]): ax.add_patch(Rectangle((columns[index], 0.81), columns[index + 1] - columns[index], 0.045, facecolor=NAVY, edgecolor="white")) ax.text(columns[index] + 0.008, 0.832, header, color="white", fontsize=8.5, weight="bold", va="center") y = 0.755 h = 0.063 for row_index, row in enumerate(tests): face = "#F6F8F9" if row_index % 2 == 0 else "white" for index, value in enumerate(row): ax.add_patch(Rectangle((columns[index], y), columns[index + 1] - columns[index], h, facecolor=face, edgecolor="#D2DADF", linewidth=0.6)) ax.text(columns[index] + 0.007, y + h - 0.012, "\n".join(textwrap.wrap(value, [22, 31, 31][index])), fontsize=7.0, color=INK, va="top", linespacing=1.16) y -= h report.box( ax, 0.08, 0.075, 0.84, 0.075, "Kart ölçümünde gerekli log", "epoch sequence, SVID set, raw/corrected ABS, C125 capture, TOW source, WLS b, mapped phase error, PI integrator/rate, compute/APB age ve PPS edge timestamp.", face=PALE_BLUE, fontsize=7.9, ) report.save(fig) def page_open_points(report: Report) -> None: fig, ax = report.new_page("Projede ölçülerek kapatılacak açık noktalar") points = [ ("HOW boundary semantiği", "parse_subframe Z-count × 6 yapıyor. Full-subframe interrupt, bit_gen ABS_SAMPLE ve gerçek transmit next-subframe sınırı arasındaki sabit pipeline cycle sayısı scope/ILA ile ölçülmeli."), ("POSTPONE_TOW = 0.180", "Mevcut RTL readout gecikmesidir: doğrulama dokuzuncu takip eden bitte yapılır; 50 bit/s hızda 180 ms. Mevcut satpos() kullanım anında bir kez ekler; PPS peripheral tekrar eklemez."), ("RF ↔ 125 MHz bağı", "Aynı PLL/reference mı, bağımsız mı? Ortak drift ve independent jitter ölçülmeli; cross-timestamp uncertainty buna göre bütçelenmeli."), ("Cold-start WLS timing", "Dört uyduda vertical/clock korelasyonu yüksek. Coarse alignment yapılabilir; PPS quality düşük işaretlenmeli ve PI bandwidth agresif seçilmemeli."), ("Atmosfer/multipath", "Iono/tropo modele girse de residual b ve position'a sızar. Timing-only mode veya sabit/smoothed position ileride clock çözümünü iyileştirebilir."), ("Hardware fixed point", "Phase, frequency word, Kp/Ki ve fractional timestamp Q formatlarının overflow, saturation ve rounding davranışı bit-accurate Python modeline eklenmeli."), ] y = 0.82 for index, (title, detail) in enumerate(points): color = [ORANGE, RED, BLUE, GREEN, ORANGE, BLUE][index] ax.add_patch(Rectangle((0.07, y - 0.085), 0.012, 0.085, color=color)) ax.text(0.10, y, title, fontsize=10, color=color, weight="bold", va="top") ax.text(0.10, y - 0.031, "\n".join(textwrap.wrap(detail, 98)), fontsize=8.6, color=INK, va="top", linespacing=1.28) y -= 0.118 report.box( ax, 0.10, 0.075, 0.80, 0.075, "Tasarım sonucu", "Bu açık noktalar PPS mimarisini değiştirmez; yalnız sabit offset, uncertainty bütçesi ve final PI katsayılarını belirler.", face=PALE_GREEN, edge="#9BC9AF", title_color=GREEN, fontsize=8.1, ) report.save(fig) def page_conclusion(report: Report) -> None: fig, ax = report.new_page("Sonuç: projeye uygun en yalın PPS zinciri") y = 0.80 chain = [ ("1", "Raw clocks", "16.368 MHz measurement + 125 MHz monotonic counter"), ("2", "Paired capture", "ABS / REM / C125 / sequence aynı event"), ("3", "Coherent WLS", "minimum datum + all-satellite [XYZ b]"), ("4", "GPS receive time", "T_TX(epoch-aligned) + 68.802 ms − b/c"), ("5", "Atomic mapping", "(C125,k, TGPS,k, seq) shadow + commit"), ("6", "Disciplined PPS", "immutable raw counter + low-bandwidth PI accumulator"), ] for index, (number, title, detail) in enumerate(chain): x = 0.09 if index % 2 == 0 else 0.53 yy = y - (index // 2) * 0.19 color = [BLUE, ORANGE, GREEN][index // 2] ax.add_patch(FancyBboxPatch((x, yy - 0.11), 0.38, 0.11, boxstyle="round,pad=0.010", facecolor="#F5F8FA", edgecolor=color, linewidth=1.2)) ax.add_patch(FancyBboxPatch((x + 0.012, yy - 0.052), 0.045, 0.045, boxstyle="circle,pad=0.002", facecolor=color, edgecolor=color)) ax.text(x + 0.0345, yy - 0.030, number, color="white", fontsize=8, weight="bold", ha="center", va="center") ax.text(x + 0.072, yy - 0.015, title, fontsize=9.5, color=NAVY, weight="bold", va="top") ax.text(x + 0.072, yy - 0.050, "\n".join(textwrap.wrap(detail, 40)), fontsize=8.1, color=INK, va="top") report.equation(ax, 0.30, r"e_k=\left(T_{TX,k}+S-b_k/c\right)-T_{LOCAL}(C_{125,k})", "Bütün raporun tek satırlık özeti", face=PALE_GREEN) report.box( ax, 0.10, 0.10, 0.80, 0.09, "En önemli mimari karar", "İlk uydu PPS'i başlatmaz. Baremetal HOW'a 180 ms readout düzeltmesini bir kez uygular; ilk coherent WLS bu etiketi free-running 125 MHz counter'a bağlar. PPS peripheral 180 ms'yi tekrar eklemez.", face=NAVY, edge=NAVY, title_color="#8ED6E8", text_color="white", fontsize=8.4, ) report.save(fig) def generate(output_pdf: Path) -> None: required_plots = [ OUTPUT / "01_wls_time_model.png", OUTPUT / "02_latency_timeline.png", OUTPUT / "03_pi_loop_response.png", OUTPUT / "04_pps_edge_error.png", OUTPUT / "05_startup_tow_alignment.png", ] missing = [str(path) for path in required_plots if not path.exists()] if missing: raise SystemExit( "Missing simulation plots. Run simulate_pps.py first:\n" + "\n".join(missing) ) output_pdf.parent.mkdir(parents=True, exist_ok=True) report = Report(output_pdf) try: report.cover() page_executive(report) page_time_vocabulary(report) page_lnav_timeline(report) page_tx_rx(report) page_project_map(report) page_nav_epoch(report) page_pseudorange(report) page_wls_theory(report) page_bias_derivation(report) page_counter_mapping(report) page_state_machine(report) page_cross_timestamp(report) page_apb_latency(report) page_pi(report) page_control_loop_model(report) page_sim_method(report) plot_page( report, "WLS datum, reference değişimi ve cold-start geometri", required_plots[0], "Raw b/c referans SVID değişince milisaniyelik basamak yaparken TOW+bias ile rekonstrükte edilen zaman hatası ns seviyesinde kalır. Dört uydulu ilk bölüm position/clock açısından belirgin şekilde daha gürültülüdür.", ) plot_page( report, "İlk HOW neden PPS receive-now değildir?", required_plots[4], "Sentetik acquisition/subframe Monte Carlo: mevcut hardware delivery, dokuzuncu takip eden bit nedeniyle HOW sınırından 180 ms ve signal flight time kadar sonradır. HOW +180 ms +68.802 ms kaba yaklaşım bırakır; WLS flight-time + paired timestamp doğru handover'dır.", ) plot_page( report, "Capture-to-commit gecikme bileşenleri", required_plots[1], "Satellite arrival span, WLS compute ve APB delay aynı epoch'un age bilgisidir. Timestamp varsa phase offset değildir; timestamp yoksa doğrudan PPS hatasına dönüşür.", ) plot_page( report, "Timestamp'lı PI loop cevabı", required_plots[2], "Timestamp'lı PI raw oscillator driftini rate command ile karşılar. PI kapalı clock yüzlerce µs sürüklenir; eski GPS referansını APB-now sanan kontrol ise ms seviyesinde yanlış faza oturur.", ) plot_page( report, "PPS edge sonucu ve 8 ns output grid", required_plots[3], "Timestamp'lı loop ilk transient sonrasında sub-µs bölgeye iner. Bu sentetik katsayı sonucu mimari karşılaştırmadır; gerçek kart noise/drift verisi olmadan final doğruluk iddiası değildir.", ) page_registers(report) page_implementation(report) page_verification(report) page_open_points(report) page_conclusion(report) finally: report.close() def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, default=DEFAULT_PDF) return parser.parse_args() def main() -> int: args = parse_args() generate(args.output.resolve()) print(args.output.resolve()) return 0 if __name__ == "__main__": raise SystemExit(main())