# pmse-to-wwb — API A FastAPI backend with two real endpoints, a health check, and a static frontend. > **This tool has a successor.** The functionality was merged into > **[RFutils](https://github.com/stoatworks-labs/RFutils)** as **Convert › Ofcom PMSE licence**, > where **the PDF parser has been validated against a real Ofcom licence.** Before adding a > feature here, check whether it belongs there instead. --- ## Endpoints | Method | Path | Purpose | |---|---|---| | `GET` | `/` | the single-page frontend | | `POST` | `/api/convert` | PDF in → parsed assignments + WWB frequency list + reference CSV | | `POST` | `/api/generate-show` | receiver layout in → a WWB `.shw` show file | | `GET` | `/health` | `{"status":"ok"}` | **There is no authentication on any of it.** --- ## `POST /api/convert` Multipart upload, field **`file`**. | Failure | Status | Meaning | |---|---|---| | Content type isn't `application/pdf` or `application/x-pdf` | **400** | rejected before reading | | Upload exceeds **20 MB** | **413** | checked *while streaming*, so a large file is cut off rather than buffered | | `parse_pdf` raised | **422** | `"Could not parse this PDF: …"` | | Parsed, but **no assignments found** | **422** | `"…may not be an Ofcom PMSE licence schedule."` | That last one is the important distinction: **a PDF that parses cleanly but yields zero assignments is a 422, not an empty success.** Ofcom schedules are typically under 1 MB, so the 20 MB ceiling is generous headroom rather than a real constraint. ### Response ```json { "metadata": { "licence_no", "notice_of_variation_no", "licensee", "licensee_address", "licence_start", "licence_end", "pmse_ref", "licensee_ref", "total_assignments" }, "warnings": [ … ], "assignment_count": 12, "assignments": [ { "frequency_mhz", "equipment_type", "model", "fee_category", "site", "suggested_name" } ], "wwb_frequency_list": "…", "reference_csv": "…", "max_channels_per_receiver": 8 } ``` Two fields deserve attention: - **`metadata.total_assignments` is the count the licence itself states**, while `assignment_count` is **how many were actually parsed.** If they disagree, the parser missed rows — check `warnings`. - **`warnings` is not decoration.** It's where the parser reports what it couldn't make sense of. A client that ignores it will present a partial licence as a complete one. `wwb_frequency_list` is the **documented** WWB6/7 import format: bare MHz values, ≤ 3 decimals, one per line, **de-duplicated**, no extra text. `reference_csv` is for humans, not for import. `suggested_name` is generated by `exporters.suggested_names()`. --- ## `POST /api/generate-show` JSON in, `{"wwb_show_file": "…"}` out. ```jsonc { "show_name": "PMSE Licence Import", // default "customer": "", "poc_name": "", "venue_name": "", "venue_address": "", "receivers": [ { "name": "", "channel_count": 4, "ip_address": null, "channels": [ { "frequency_mhz": 606.500, "name": "", "band": "G56" } ] } ] } ``` | Constraint | Enforced | |---|---| | `channel_count` | **1–8**, by the request model — outside that is a validation error, not a 422 | | `band` | **`G56` only.** Anything else → **422** `UnsupportedBandError` | | receiver config problems | **422** `ReceiverConfigError` | `show_name` is **truncated to 40 characters** when used as the band-plan name. Channels short of `channel_count` are padded with a filler entry named **`Unused`** at **470100 kHz** (the low edge of G56) and marked inactive. ### ⚠ What this format actually is > **WWB's native show-file format is undocumented and reverse-engineered.** `show_generator.py` works by **cloning real, structurally-verified XML fragments** extracted from a working **WWB7 7.8.1** show file for a **Shure AD4Q-A quad receiver in the G56 band**, and substituting only frequency, name and identity fields. Everything else is copied **verbatim**, on the theory that unedited boilerplate is far less likely to break WWB's parser than a hand-built equivalent. **This has not been validated by Shure.** Open the generated file in Wireless Workbench and check it before relying on it for a show. **The IP address fields are separately unverified.** The real sample this was reverse-engineered from **never had a device with a real IP configured**, so the `ip_mode`/`ip_address` encoding — a packed 32-bit IPv4 address with `ip_mode=1` for static — is a **best-effort guess**, not something WWB has been seen to accept. Treat any IP baked into a generated show file as a starting point to verify inside WWB. **`MAX_CHANNELS_PER_RECEIVER = 8` matches the template's `regtx1..8` slots.** It is a sanity ceiling from the template, not a verified hardware limit. --- ## Parsing `parser.py` uses `pdfplumber` with regexes for the National Grid Reference + site line, frequencies (`… MHz`), the fee block, and the licence period. Each `Assignment` carries: ``` equipment_type, model, frequency_mhz, bandwidth, max_power, emission_class, ngr_transmit, site, restrictions, period_start, period_end, fee_category, fee_type, fee_amount ``` Only a subset is surfaced by `/api/convert`; the rest reaches the reference CSV. --- ## See also - [USER-GUIDE.md](USER-GUIDE.md) — using it, and what to check - [DEVELOPING.md](DEVELOPING.md) — the risk boundary around `show_generator.py` - [README](../README.md) — what it does, deployment