Compare commits
8 Commits
c5c5f5f18e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
f7de7debbb
|
|||
|
baeb325922
|
|||
|
4cb6412d4f
|
|||
|
3179f54681
|
|||
|
4a14bbb989
|
|||
|
989d6d6888
|
|||
|
189a1954f7
|
|||
|
c1603b1643
|
@@ -163,8 +163,11 @@ decks never collide on import.
|
||||
- `EXPLANATIONS.md` — the editorial contract for agents asked to add
|
||||
or improve per-question explanations. The schema, the workflows
|
||||
("explain everything unexplained", "improve everything below
|
||||
confidence 7"), and the source/confidence guidance live there.
|
||||
`explanations.json` is an empty `{}` until agents populate it.
|
||||
confidence 7"), the source/confidence guidance, and the **MathJax
|
||||
typesetting rules (§4a) with a verification sweep** live there. The
|
||||
`$...$` typesetting is the most error-prone part of the file — read
|
||||
§4a before touching any formula, and fix every occurrence in both the
|
||||
explanation body and the `Hilfsmittel:` note.
|
||||
- Start from `DESIGN.md` — it has the JSON schema, the question/answer
|
||||
conventions (answer A is always correct upstream → consumers shuffle
|
||||
before display), the LaTeX-in-questions caveat, the exam-structure
|
||||
|
||||
+154
-9
@@ -27,7 +27,8 @@ entry is purely additive — no regenerate ceremony beyond
|
||||
|
||||
### Per-entry schema
|
||||
|
||||
Every entry MUST have exactly these four fields:
|
||||
Every entry MUST have these four required fields, and MAY carry the one
|
||||
optional field below:
|
||||
|
||||
| Field | Type | Constraint |
|
||||
|---------------|---------|--------------------------------------------|
|
||||
@@ -35,13 +36,22 @@ Every entry MUST have exactly these four fields:
|
||||
| `explanation` | string | Non-empty. **English.** Correct & helpful, WHY-focused |
|
||||
| `source` | string | Non-empty. URL or citation like `AFuV §16(2)` |
|
||||
| `confidence` | integer | `1..10` inclusive. See scale in §5 |
|
||||
| `provenance` | string | *Optional.* Only allowed value: `"50ohm-loesungsweg"` |
|
||||
|
||||
Extra keys are rejected by `load_explanations()` — the build fails
|
||||
with `unknown fields [...]` listing them. The loader is similarly
|
||||
strict about types: a JSON `true` will not satisfy the integer
|
||||
contract for `revision` or `confidence`. If you need to track
|
||||
editorial metadata that isn't shown on the card, propose a schema
|
||||
change rather than smuggling fields in.
|
||||
`provenance` records **how the text was produced**, which `source` (a
|
||||
citation) does not. Set it to `"50ohm-loesungsweg"` only on entries
|
||||
that are genuinely a translation/condensation of a 50ohm.de worked
|
||||
solution (`contents/solutions/<ID>.md` in `DARC-e-V/50ohm-contents-dl`).
|
||||
The build uses it — and *not* the `source` domain — to decide whether
|
||||
to show the CC BY 4.0 derivative-work credit on the card (CC BY
|
||||
requires naming the author team and indicating modification). Do **not**
|
||||
add it just because an entry cites a 50ohm.de study page; merely citing
|
||||
a page is not a derivative work. Any other value, or any other extra
|
||||
field, is rejected by `load_explanations()` with
|
||||
`unknown fields [...]` / `provenance must be one of [...]`. The loader
|
||||
is also strict about types: a JSON `true` will not satisfy the integer
|
||||
contract for `revision` or `confidence`. To track other editorial
|
||||
metadata, propose a schema change rather than smuggling fields in.
|
||||
|
||||
Top-level keys (the question numbers) must also match the catalog
|
||||
exactly. An entry keyed on a number that no live question carries
|
||||
@@ -156,13 +166,137 @@ source citation.
|
||||
rule of thumb.
|
||||
- **Math.** Use inline `$...$`; it's rewritten to MathJax on the
|
||||
card, same as for question text. Don't use display math `$$...$$`
|
||||
(the deck has no MathJax support for those).
|
||||
(the deck has no MathJax support for those). Getting the *inside* of
|
||||
`$...$` right is its own discipline — see **§4a Math typesetting**
|
||||
below. This is the single most error-prone part of this file.
|
||||
- **Inline emphasis.** `<u>...</u>` is the only HTML you should
|
||||
type by hand. Everything else gets HTML-escaped.
|
||||
- **No spoilers about other answers.** If a distractor is a common
|
||||
trap, naming the trap is fine; quoting the distractor's text is
|
||||
noise.
|
||||
|
||||
### 4a. Math typesetting (MathJax) — get this right
|
||||
|
||||
Everything between `$...$` is TeX. MathJax renders **bare multi-letter
|
||||
runs as a product of italic variables**, so `ohm` shows as *o·h·m*,
|
||||
`log10` as *l·o·g·10*, `P_{PEP}` as *P·(P·E·P)*. A whole multi-round
|
||||
review on this repo was spent fixing exactly these. The rules below are
|
||||
the checklist; wrong → right.
|
||||
|
||||
**Units** — never leave a unit as bare letters; wrap in `\text{}` (or use
|
||||
the symbol) and put a thin space `\,` after the number:
|
||||
|
||||
| Wrong (in `$...$`) | Right |
|
||||
|---|---|
|
||||
| `0.01 ohm`, `10 kOhm`, `5 MOhm` | `0.01\,\Omega`, `10\,\text{k}\Omega`, `5\,\text{M}\Omega` |
|
||||
| `3 microhenry`, `3 uH` | `3\,\mu\text{H}` |
|
||||
| `1 uF`, `100 nF`, `47 pF` | `1\,\mu\text{F}`, `100\,\text{nF}`, `47\,\text{pF}` |
|
||||
| `5 mA`, `2 mV`, `1 kHz`, `28 MHz` | `5\,\text{mA}`, `2\,\text{mV}`, `1\,\text{kHz}`, `28\,\text{MHz}` |
|
||||
| `12 dB`, `8 dBi`, `46.8 bit/s` | `12\,\text{dB}`, `8\,\text{dBi}`, `46.8\,\text{bit/s}` |
|
||||
| `5 mm2`, `2.5 A/mm2` | `5\,\text{mm}^2`, `2.5\,\text{A/mm}^2` |
|
||||
| `360 degrees`, `45°` | `360^\circ`, `45^\circ` |
|
||||
| `20 A`, `12 V`, `100 W`, `5 m`, `50 s` | `20\,\text{A}`, `12\,\text{V}`, `100\,\text{W}`, `5\,\text{m}`, `50\,\text{s}` |
|
||||
|
||||
**Operators, functions, Greek, powers:**
|
||||
|
||||
| Wrong | Right | Why |
|
||||
|---|---|---|
|
||||
| `log10(x)` | `\log_{10}(x)` | bare `log` is *l·o·g* |
|
||||
| `10^(20/10)` | `10^{20/10}` | `^(` superscripts only the `(`; braces group |
|
||||
| `lambda`, `pi`, `tau`, `omega`, `rho`, `mu` | `\lambda`, `\pi`, `\tau`, … | bare = italic letters |
|
||||
| `sin`, `cos`, `ln` | `\sin`, `\cos`, `\ln` | |
|
||||
| `2 x 3`, `R1 || R2` | `2 \cdot 3`, `R_1 \parallel R_2` | `x`/`||` are not operators |
|
||||
| `62.5%` | `62.5\%` | **bare `%` is a TeX comment — it silently eats the rest of the math** |
|
||||
| `4,200,000` | `4{,}200{,}000` | bare `,` gets TeX punctuation spacing |
|
||||
|
||||
**Subscripts.** Descriptive multi-letter labels/acronyms are roman; a
|
||||
single-letter subscript stays italic:
|
||||
|
||||
| Wrong | Right |
|
||||
|---|---|
|
||||
| `P_{EIRP}`, `U_{peak}`, `f_{mod}`, `V_{BE}`, `f_{sum}` | `P_\mathrm{EIRP}`, `U_\mathrm{peak}`, `f_\mathrm{mod}`, `V_\mathrm{BE}`, `f_\mathrm{sum}` |
|
||||
| `R1/R2 = R3/R4` | `R_1/R_2 = R_3/R_4` |
|
||||
| keep italic: `U_F`, `f_c`, `X_C`, `R_g` | (single letter = variable, leave as-is) |
|
||||
|
||||
A standalone acronym used as a variable (e.g. `MUF` on a formula's LHS)
|
||||
also goes roman: `\mathrm{MUF}`.
|
||||
|
||||
**Dimensional constants keep their unit.** The field-strength constant is
|
||||
`30\,\Omega`, so write `\sqrt{30\,\Omega \cdot P_\mathrm{EIRP}}` and
|
||||
`(E\cdot d)^2/(30\,\Omega)`, not a bare `30`. Same idea for the dB-level
|
||||
exponent: `10^{g/(10\,\text{dB})}` (the `(...)` groups the denominator —
|
||||
`10^{g/10\,\text{dB}}` parses as `(g/10)·dB`).
|
||||
|
||||
**Unit vs. variable — don't blindly unit-ize a single letter.** `A` is
|
||||
amperes in `20 A` but *area* in `N^2 A/l`; `s`/`m` are seconds/metres in
|
||||
`50 s` / `5 m` but could be variables elsewhere. The tell: a *free-standing
|
||||
number* immediately before the letter (`20 A`) means a unit; an exponent
|
||||
base (`N^2 A`) or a symbolic factor (`S \cdot A`) means a variable. When
|
||||
unsure, read the sentence.
|
||||
|
||||
**What stays prose — do NOT force into `$...$`:**
|
||||
|
||||
- Band/wavelength designations: `5/8-wave`, `20/15/10 m trap dipole`,
|
||||
`the 80 m band`.
|
||||
- Conceptual word-equations: `1st overtone = 2nd harmonic`,
|
||||
`Region 1 = Europe/Africa`.
|
||||
- Units mentioned adjectivally in a sentence: "a 50 ohm antenna",
|
||||
"the 28 V/m limit", "about 11.7 V/m".
|
||||
|
||||
But a **worked computation with an `=`** (e.g. `230 V / 20 = 11.5 V`)
|
||||
belongs in `$...$`, fully typeset.
|
||||
|
||||
### Verifying it — the process lessons
|
||||
|
||||
These review rounds kept finding *peers* of an already-fixed defect.
|
||||
Avoid the repeat:
|
||||
|
||||
1. **Fix the whole record, not a substring.** The same formula usually
|
||||
appears in *both* the explanation body *and* the `Hilfsmittel:` note.
|
||||
Search the entire entry and fix every occurrence.
|
||||
2. **Fix the whole class, file-wide — not just the IDs someone listed.**
|
||||
If `P_{EIRP}` is wrong in one card it is wrong in twenty; sweep the
|
||||
whole file for the pattern.
|
||||
3. **Verify with a positive sweep, not a blacklist.** A list of
|
||||
known-bad tokens always misses a new category (it missed `degrees`,
|
||||
`bit`, `||`, bare seconds…). Instead: strip `\text{}`/`\mathrm{}` and
|
||||
the known macros, then flag *every remaining* ≥2-letter run inside
|
||||
`$...$` — each survivor must be a deliberate variable product
|
||||
(`LC`, `jX`, `RC`, `di/dt`) or it's a bug.
|
||||
4. **Don't claim "0 / exhaustive" unless the check actually covers that
|
||||
category.** Report what you checked, not a blanket adjective.
|
||||
|
||||
A ready-made sweep:
|
||||
|
||||
```sh
|
||||
python3 -c '
|
||||
import json, re
|
||||
d = json.load(open("explanations.json"))
|
||||
span = re.compile(r"\$[^$]*\$")
|
||||
def strip(s):
|
||||
s = re.sub(r"\\(?:text|mathrm|operatorname)\{[^{}]*\}", " ", s)
|
||||
return re.sub(r"\\[A-Za-z]+", " ", s) # drop macros (\sqrt, \pi, \cdot, ...)
|
||||
for k, e in d.items():
|
||||
ex = e["explanation"] if isinstance(e, dict) else ""
|
||||
if ex.count("$") % 2 or ex.count("{") != ex.count("}"):
|
||||
print("BALANCE", k)
|
||||
for m in span.finditer(ex):
|
||||
s = m.group(0)
|
||||
if re.search(r"(?<!\\)%", s) or "||" in s or "°" in s \
|
||||
or re.search(r"(?<!\\)\blog10\b|10\^\(", s) \
|
||||
or re.search(r"_\{[A-Za-z][A-Za-z,]*\}", s) \
|
||||
or re.search(r"(?<=\d),(?=\d\d\d)", s):
|
||||
print("ISSUE", k, m.group(0)[:60]); break
|
||||
for tok in re.findall(r"[A-Za-z]{2,}", strip(s)):
|
||||
# whitelist genuine products / kept tokens; investigate the rest
|
||||
if tok not in {"LC","RC","RA","UI","CU","PR","jX","fL","fC","di","dt"}:
|
||||
print("TOKEN?", k, tok, "|", m.group(0)[:60])
|
||||
'
|
||||
```
|
||||
|
||||
Anything it prints is either a real defect or a new legitimate product to
|
||||
add to the whitelist — look, don't assume.
|
||||
|
||||
### What "WHY-focused" means in practice
|
||||
|
||||
| Less useful | More useful |
|
||||
@@ -455,7 +589,18 @@ Before saving, confirm:
|
||||
- [ ] `confidence` honestly reflects how well the source supports
|
||||
the explanation.
|
||||
- [ ] Keys remain in alphabetical order in the file.
|
||||
- [ ] `python3 -m unittest test_amateurfunk_anki` passes.
|
||||
- [ ] **Math typesetting (§4a):** every formula is in `$...$`; units are
|
||||
`\text{}`/symbols with a leading `\,`; no bare `log10`, `10^(`,
|
||||
Greek words, `%`, `||`, multi-letter `_{...}` subscripts, or raw
|
||||
thousands commas; dimensional constants keep their unit. Fix every
|
||||
occurrence in **both** the body and the `Hilfsmittel:` note.
|
||||
- [ ] The §4a verification sweep prints nothing unexpected (run it after
|
||||
any batch of math edits — don't trust a per-ID fix to have caught
|
||||
the peers).
|
||||
- [ ] `python3 -m unittest test_amateurfunk_anki` passes. (The full deck
|
||||
suite is `test_amateurfunk_fetch test_amateurfunk_anki
|
||||
test_amateurfunk_shorthand test_amateurfunk_technical` — 95 tests;
|
||||
the two-module subset that validates explanation schema is 65.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+240
-11
@@ -63,6 +63,17 @@ DEFAULT_EXPLANATIONS_PATH = Path("explanations.json")
|
||||
# EXPLANATIONS.md for the full editorial contract.
|
||||
EXPLANATION_FIELDS = ("revision", "explanation", "source", "confidence")
|
||||
|
||||
# Optional, validated provenance marker. A `source` URL is only a
|
||||
# citation; it does NOT establish that the explanation is a derivative
|
||||
# work. `provenance` records *how the text was produced* so the CC BY
|
||||
# derivative-work credit is shown only for records that genuinely are
|
||||
# one. The single recognised value marks an explanation translated and
|
||||
# condensed from a 50ohm.de worked solution
|
||||
# (contents/solutions/<ID>.md in DARC-e-V/50ohm-contents-dl).
|
||||
PROVENANCE_50OHM_SOLUTION = "50ohm-loesungsweg"
|
||||
ALLOWED_PROVENANCE = frozenset({PROVENANCE_50OHM_SOLUTION})
|
||||
OPTIONAL_EXPLANATION_FIELDS = ("provenance",)
|
||||
|
||||
# Confidence values strictly below this threshold are surfaced on
|
||||
# the card as a "low confidence" badge — a learner-facing warning
|
||||
# that the explanation may be incomplete or weakly sourced. Matches
|
||||
@@ -70,6 +81,49 @@ EXPLANATION_FIELDS = ("revision", "explanation", "source", "confidence")
|
||||
# below 7 is review-worthy").
|
||||
LOW_CONFIDENCE_THRESHOLD = 7
|
||||
|
||||
# ---- Attribution (licensing) ----------------------------------------------
|
||||
# Two sources are redistributed in these decks and each carries an
|
||||
# attribution obligation:
|
||||
# * the questions/figures come from the BNetzA question catalog under
|
||||
# Datenlizenz Deutschland – Namensnennung 2.0 (DL-DE→BY-2.0); and
|
||||
# * the English explanations are translated and condensed from the
|
||||
# worked solutions on 50ohm.de, which is CC BY 4.0 and requires both
|
||||
# naming the author team and indicating that changes were made.
|
||||
# DECK_ATTRIBUTION goes in every deck's description (the whole-deck
|
||||
# notice); EXPLANATION_CC_BY_CREDIT is the per-card line appended to
|
||||
# each 50ohm-derived explanation so the "modified" indication travels
|
||||
# with the modified text itself.
|
||||
EXPLANATION_CC_BY_CREDIT = (
|
||||
"Explanation translated & condensed from the worked solution on "
|
||||
'<a href="https://50ohm.de/">50ohm.de</a> — '
|
||||
"© 50ohm.de-Autorenteam (AJW-Referat, DARC e. V.), "
|
||||
'<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>. '
|
||||
"Question © Bundesnetzagentur, "
|
||||
'<a href="https://www.govdata.de/dl-de/by-2-0">DL-DE→BY-2.0</a>.'
|
||||
)
|
||||
|
||||
DECK_ATTRIBUTION = (
|
||||
"Amateurfunk-Lernkarten / amateur-radio exam flashcards.<br><br>"
|
||||
"<b>Questions & figures:</b> official question catalog "
|
||||
""Prüfungsfragen zum Erwerb von "
|
||||
"Amateurfunkprüfungsbescheinigungen", Bundesnetzagentur, "
|
||||
"3. Auflage, März 2024 "
|
||||
"(https://www.bundesnetzagentur.de/amateurfunk). Licensed under "
|
||||
"Datenlizenz Deutschland – Namensnennung – Version 2.0 "
|
||||
"(https://www.govdata.de/dl-de/by-2-0).<br><br>"
|
||||
"<b>Explanations:</b> written for this project. Where an explanation "
|
||||
"is marked as derived from a 50ohm.de worked solution "
|
||||
"("Lösungsweg"), that text is © 50ohm.de-Autorenteam, "
|
||||
"koordiniert durch das AJW-Referat des DARC e. V., licensed under "
|
||||
"CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), and has "
|
||||
"been modified — translated from German and shortened. Other "
|
||||
"explanations cite their own sources.<br><br>"
|
||||
"<b>Exam-aid references</b> cite the BNetzA "Hilfsmittel" "
|
||||
"formula sheet.<br><br>"
|
||||
"This deck is an independent project and is not endorsed by the "
|
||||
"Bundesnetzagentur or by the DARC e. V."
|
||||
)
|
||||
|
||||
# Exit codes. The builder is much simpler than the fetcher — there is
|
||||
# no "operator must resolve local state" case here, so two codes are
|
||||
# enough.
|
||||
@@ -111,6 +165,23 @@ TECHNISCHE_SHORT_TITLE = "Technische Kenntnisse"
|
||||
# inside the package as a deck tree.
|
||||
TOPIC_SPLIT_CLASSES = frozenset({"E", "A"})
|
||||
|
||||
# The catalog's figures come in two kinds: vector SVGs (the bulk; mostly
|
||||
# small — well under ~200 px) and a handful of raster PNGs (all 1024×685). We
|
||||
# enlarge only the SVGs: they scale crisply, they're the ones that read tiny
|
||||
# on a wide desktop card, and the PNGs are already at roughly the size we're
|
||||
# aiming for. The enlargement is `MEDIA_SCALE`× the native size, with
|
||||
# `MEDIA_MAX_WIDTH` × `MEDIA_MAX_HEIGHT` acting as an *enlargement ceiling* —
|
||||
# it bounds how far a below-cap figure may grow (a figure already at/above a
|
||||
# cap is left at native size; we never shrink). The ceiling is the PNGs'
|
||||
# ballpark (1024×768) so the largest SVG schematics (up to 635×481 px in the
|
||||
# 3rd edition: the NG20x block diagrams, EB309/EB310) grow to a comparable
|
||||
# size rather than staying cramped, while folding in native *height* still
|
||||
# stops tall-but-narrow figures from growing off the bottom of the card.
|
||||
# See `MediaRegistry.image_html`.
|
||||
MEDIA_SCALE = 2
|
||||
MEDIA_MAX_WIDTH = 1024
|
||||
MEDIA_MAX_HEIGHT = 768
|
||||
|
||||
# Fallback build epoch if neither the manifest nor `--epoch` supplies
|
||||
# one. Picked as 0 so missing-metadata builds are still deterministic
|
||||
# and obviously wrong (timestamps would all show 1970).
|
||||
@@ -246,12 +317,15 @@ def load_explanations(path):
|
||||
def _validate_explanation(key, value):
|
||||
"""Raise `AnkiBuildError` if one explanation entry is malformed.
|
||||
|
||||
Strict shape check: the four documented fields are required, no
|
||||
extras allowed, and integer fields reject `bool` (which would
|
||||
pass `isinstance(..., int)` because `bool` subclasses `int` in
|
||||
Python). Extras are rejected so a stray `"note"` or `"author"`
|
||||
field can't accumulate silently and drift from the documented
|
||||
schema in EXPLANATIONS.md.
|
||||
Strict shape check: the four documented fields are required and the
|
||||
optional `provenance` field is allowed (and, if present, must be a
|
||||
string from `ALLOWED_PROVENANCE`); any *other* extra field is
|
||||
rejected, so a stray `"note"` or `"author"` field can't accumulate
|
||||
silently and drift from the documented schema in EXPLANATIONS.md.
|
||||
Integer fields reject `bool` (which would pass `isinstance(..., int)`
|
||||
because `bool` subclasses `int` in Python), and every malformed
|
||||
value — including an unhashable `provenance` — surfaces as an
|
||||
`AnkiBuildError`, never a raw `TypeError`.
|
||||
"""
|
||||
if not isinstance(value, dict):
|
||||
raise AnkiBuildError(
|
||||
@@ -262,11 +336,24 @@ def _validate_explanation(key, value):
|
||||
raise AnkiBuildError(
|
||||
f"explanation {key!r} missing required fields: {missing}"
|
||||
)
|
||||
extra = sorted(set(value) - set(EXPLANATION_FIELDS))
|
||||
extra = sorted(
|
||||
set(value) - set(EXPLANATION_FIELDS) - set(OPTIONAL_EXPLANATION_FIELDS)
|
||||
)
|
||||
if extra:
|
||||
raise AnkiBuildError(
|
||||
f"explanation {key!r}: unknown fields {extra}"
|
||||
)
|
||||
if "provenance" in value and (
|
||||
not isinstance(value["provenance"], str)
|
||||
or value["provenance"] not in ALLOWED_PROVENANCE
|
||||
):
|
||||
# The isinstance check must come first: an unhashable JSON value
|
||||
# (a list or object) would make the `not in` set test raise a raw
|
||||
# TypeError instead of the documented AnkiBuildError.
|
||||
raise AnkiBuildError(
|
||||
f"explanation {key!r}: provenance must be one of "
|
||||
f"{sorted(ALLOWED_PROVENANCE)}"
|
||||
)
|
||||
if type(value["revision"]) is not int or value["revision"] < 1:
|
||||
raise AnkiBuildError(
|
||||
f"explanation {key!r}: revision must be a positive integer"
|
||||
@@ -619,11 +706,23 @@ def render_explanation(explanation):
|
||||
'title="This explanation is weakly sourced or incomplete">'
|
||||
'low confidence</span>'
|
||||
)
|
||||
# CC BY 4.0 requires naming the author and indicating modifications.
|
||||
# Only explanations actually derived from a 50ohm.de worked solution
|
||||
# carry this credit — gated on the explicit `provenance` marker, NOT
|
||||
# on the `source` URL (a citation is not provenance; many records
|
||||
# merely cite a 50ohm study page without being a derivative work).
|
||||
credit = ""
|
||||
if explanation.get("provenance") == PROVENANCE_50OHM_SOLUTION:
|
||||
credit = (
|
||||
'<div class="af-explanation-credit">'
|
||||
f'{EXPLANATION_CC_BY_CREDIT}</div>'
|
||||
)
|
||||
return (
|
||||
'<div class="af-explanation">'
|
||||
f'<div class="af-explanation-header">Explanation{badge}</div>'
|
||||
f'<div class="af-explanation-body">{body}</div>'
|
||||
f'<div class="af-explanation-source">Source: {source_html}</div>'
|
||||
f'{credit}'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
@@ -755,6 +854,8 @@ class MediaRegistry:
|
||||
self.used_paths = set()
|
||||
# references that didn't resolve (recorded for diagnostics)
|
||||
self.missing = []
|
||||
# Path → intrinsic (w, h) in px (memoised; None when unreadable)
|
||||
self._size_cache = {}
|
||||
self._index_media_dir()
|
||||
|
||||
def _index_media_dir(self):
|
||||
@@ -796,12 +897,43 @@ class MediaRegistry:
|
||||
return None
|
||||
|
||||
def image_html(self, reference):
|
||||
"""Return an `<img>` tag for `reference`, or `""` if unresolved."""
|
||||
"""Return an `<img>` tag for `reference`, or `""` if unresolved.
|
||||
|
||||
Most catalog SVGs carry small intrinsic dimensions, so on a wide
|
||||
desktop card they render barely legible (a small fixed-px image
|
||||
occupies only a sliver of the column; `max-width: 100%` only
|
||||
ever shrinks, never enlarges). We raise the *preferred* width
|
||||
via an explicit `width` attribute so the figure grows on every
|
||||
viewport until the card's `max-width: 100%` cap engages.
|
||||
|
||||
Only SVGs are scaled — the catalog's PNGs are already large
|
||||
(1024×685), so `_intrinsic_svg_size` returns `None` for them and
|
||||
they fall through to native size. The enlargement factor is
|
||||
`scale_for_size` — `MEDIA_SCALE`×, clamped at the
|
||||
`MEDIA_MAX_WIDTH` × `MEDIA_MAX_HEIGHT` enlargement ceiling.
|
||||
`height: auto` in the CSS keeps the aspect ratio. When the size
|
||||
can't be read (non-SVG, or an unparseable SVG) we fall back to
|
||||
native.
|
||||
"""
|
||||
path = self.resolve(reference)
|
||||
if path is None:
|
||||
return ""
|
||||
filename = html.escape(path.name, quote=True)
|
||||
size = self._intrinsic_svg_size(path)
|
||||
if size is None:
|
||||
return f'<img src="{filename}" class="af-media">'
|
||||
width, height = size
|
||||
scale = scale_for_size(width, height)
|
||||
if scale <= 1.0:
|
||||
return f'<img src="{filename}" class="af-media">'
|
||||
scaled = round(width * scale)
|
||||
return f'<img src="{filename}" class="af-media" width="{scaled}">'
|
||||
|
||||
def _intrinsic_svg_size(self, path):
|
||||
"""Return the SVG's `(width, height)` in px (memoised; SVG only)."""
|
||||
if path not in self._size_cache:
|
||||
self._size_cache[path] = intrinsic_svg_size(path)
|
||||
return self._size_cache[path]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -1028,6 +1160,74 @@ def media_bytes_for_package(path):
|
||||
return svg_with_white_background(text).encode("utf-8")
|
||||
|
||||
|
||||
def scale_for_size(width, height):
|
||||
"""Return the display scale for a figure of native `width`×`height`.
|
||||
|
||||
`MEDIA_SCALE`× for legibility, but never so much that the enlarged
|
||||
figure would grow past the `MEDIA_MAX_WIDTH` / `MEDIA_MAX_HEIGHT`
|
||||
ceiling, and never below 1.0 (we only ever enlarge, never shrink —
|
||||
shrinking is the CSS `max-width`'s job). A figure already at/above
|
||||
either cap yields exactly 1.0, i.e. "render at native size" — so a
|
||||
native figure larger than the ceiling is left as-is, not shrunk.
|
||||
"""
|
||||
if width <= 0 or height <= 0:
|
||||
return 1.0
|
||||
scale = min(
|
||||
MEDIA_SCALE,
|
||||
MEDIA_MAX_WIDTH / width,
|
||||
MEDIA_MAX_HEIGHT / height,
|
||||
)
|
||||
return max(scale, 1.0)
|
||||
|
||||
|
||||
def _svg_dimension(svg_tag, name):
|
||||
"""Parse a `width`/`height` length (px or unitless) off an `<svg>` tag.
|
||||
|
||||
The numeric pattern is a well-formed decimal (`12`, `12.7`, `.5`) so
|
||||
malformed values like `1.2.3` or a lone `.` simply don't match and
|
||||
yield `None` rather than reaching `float()`. We still guard the
|
||||
conversion with `try/except` so no parse can ever abort the build.
|
||||
"""
|
||||
attr = re.search(
|
||||
rf'\b{name}\s*=\s*"(\d+(?:\.\d+)?|\.\d+)\s*(?:px)?"',
|
||||
svg_tag,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if not attr:
|
||||
return None
|
||||
try:
|
||||
return float(attr.group(1))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def intrinsic_svg_size(path):
|
||||
"""Return an SVG's intrinsic `(width, height)` in CSS px, or `None`.
|
||||
|
||||
Read from the `width`/`height` attributes on the root `<svg>` tag
|
||||
(the catalog's SVGs use unitless user units, which map 1:1 to px).
|
||||
Only `.svg` files are handled — we deliberately don't size raster
|
||||
figures, because the catalog's PNGs are already large and only the
|
||||
SVGs need enlarging. Anything we can't parse confidently — a non-SVG
|
||||
file, a percentage dimension, a missing attribute — returns `None`
|
||||
so the caller falls back to the image's native size.
|
||||
"""
|
||||
if path.suffix.lower() != ".svg":
|
||||
return None
|
||||
try:
|
||||
text = path.read_bytes().decode("utf-8-sig", errors="replace")
|
||||
except OSError:
|
||||
return None
|
||||
match = re.search(r"<svg\b[^>]*>", text, flags=re.IGNORECASE)
|
||||
if not match:
|
||||
return None
|
||||
width = _svg_dimension(match.group(0), "width")
|
||||
height = _svg_dimension(match.group(0), "height")
|
||||
if width is None or height is None:
|
||||
return None
|
||||
return (width, height)
|
||||
|
||||
|
||||
def svg_with_white_background(svg_text):
|
||||
"""Inject a white background `<rect>` after the opening `<svg>`.
|
||||
|
||||
@@ -1218,8 +1418,15 @@ def insert_collection_metadata(conn, decks, cur_deck_id, model_id, now):
|
||||
package can ship a deck tree, not just one deck. `cur_deck_id` is
|
||||
the deck made current in `col.conf`.
|
||||
"""
|
||||
# The attribution notice goes on the root (current) deck only, so it
|
||||
# shows once for the package rather than on every sub-deck.
|
||||
decks_json = {
|
||||
str(deck_id): deck_json(deck_id, deck_name, now)
|
||||
str(deck_id): deck_json(
|
||||
deck_id,
|
||||
deck_name,
|
||||
now,
|
||||
desc=DECK_ATTRIBUTION if deck_id == cur_deck_id else "",
|
||||
)
|
||||
for deck_name, deck_id in decks.items()
|
||||
}
|
||||
conn.execute(
|
||||
@@ -1278,11 +1485,14 @@ def collection_conf(deck_id):
|
||||
}
|
||||
|
||||
|
||||
def deck_json(deck_id, deck_name, now):
|
||||
def deck_json(deck_id, deck_name, now, desc=""):
|
||||
"""Return one deck entry for `col.decks`.
|
||||
|
||||
The `Amateurfunk::Foo` naming gives the user a single top-level
|
||||
Amateurfunk node in Anki's deck browser with three child decks.
|
||||
`desc` is the deck description shown in Anki's deck overview; we use
|
||||
it on the root deck to carry the source-attribution notice (see
|
||||
DECK_ATTRIBUTION).
|
||||
"""
|
||||
return {
|
||||
"id": deck_id,
|
||||
@@ -1297,7 +1507,7 @@ def deck_json(deck_id, deck_name, now):
|
||||
"browserCollapsed": False,
|
||||
"dyn": 0,
|
||||
"conf": 1,
|
||||
"desc": "",
|
||||
"desc": desc,
|
||||
"extendNew": 0,
|
||||
"extendRev": 0,
|
||||
}
|
||||
@@ -1502,6 +1712,17 @@ CARD_CSS = """
|
||||
color: #1a73e8;
|
||||
text-decoration: none;
|
||||
}
|
||||
.af-explanation-credit {
|
||||
font-family: Arial, sans-serif;
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
.af-explanation-credit a {
|
||||
color: #1a73e8;
|
||||
text-decoration: none;
|
||||
}
|
||||
.nightMode .af-explanation,
|
||||
.card.nightMode .af-explanation {
|
||||
border-top-color: #555;
|
||||
@@ -1519,6 +1740,14 @@ CARD_CSS = """
|
||||
.card.nightMode .af-explanation-source a {
|
||||
color: #8ab4f8;
|
||||
}
|
||||
.nightMode .af-explanation-credit,
|
||||
.card.nightMode .af-explanation-credit {
|
||||
color: #999;
|
||||
}
|
||||
.nightMode .af-explanation-credit a,
|
||||
.card.nightMode .af-explanation-credit a {
|
||||
color: #8ab4f8;
|
||||
}
|
||||
.nightMode .af-explanation-low-confidence,
|
||||
.card.nightMode .af-explanation-low-confidence {
|
||||
background: #4a3510;
|
||||
|
||||
+1119
-949
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,9 @@ those are memory items.
|
||||
- **S. 5 — Tabelle "Rufzeichen mit 2- oder 3-buchstabigen Suffixen":**
|
||||
the main **call-sign-series → class/use** table. DA0 = Klubstation;
|
||||
DL1–DL9 = person-bound class A; DO1–DO9 = class E; DN9 = class N;
|
||||
DP0–DP1 = exterritorial class A; etc.
|
||||
DP is intended for exterritorial-location call signs; DP0–DP1 =
|
||||
class A with use categories KS, RL/FB and SZ; DP2 = class E with the
|
||||
same use categories; DP8 = class N with the same use categories.
|
||||
- **S. 6** — club-station suffixes (1-char) table.
|
||||
- **S. 7** — club-station suffixes (4–7-digit); **§ 5** Kurzzeitzulassungen
|
||||
ausländischer Funkamateure (visitors in Germany sign **DL/** class A,
|
||||
|
||||
@@ -15,17 +15,17 @@ words. Mode names and technical abbreviations such as `CW`, `SSB`, `FM`,
|
||||
|
||||
| Code | What it means when question | What it means when not in question | Explanation, when actually used | Usage example | Exam references | Question IDs |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `QRM` | Are you being interfered with? | I am being interfered with; interference. | Used to report man-made interference or ask whether the other station has interference. | DL1ABC: `QRM?` / DO2XYZ: `QRM, pse repeat report.` | 2 | `BB201`, `BB204` |
|
||||
| `QRM` | Is my transmission being interfered with? | Your transmission is being interfered with. | Formal Q-code perspective; amateurs also commonly say "I have QRM" for interference at their receiver. | DL1ABC: `QRM?` / DO2XYZ: `QRM, pse repeat report.` | 2 | `BB201`, `BB204` |
|
||||
| `QRN` | Are you troubled by atmospheric noise? | I have atmospheric noise. | Used when static crashes or atmospheric noise make reception difficult. | DL1ABC: `QRN here, ur report?` / DO2XYZ: `RST 55 despite QRN.` | 1 | `BB201` |
|
||||
| `QRO` | Shall I increase transmitter power? | Increase transmitter power; I am increasing power. | Used when more power may be needed, usually after trying normal operating improvements first. | DL1ABC: `QRO?` / DO2XYZ: `PSE QRO, ur weak.` | 1 | `BB202` |
|
||||
| `QRP` | Shall I decrease transmitter power? | Decrease transmitter power; I am decreasing power. | Used to request or announce reduced transmitter power; also describes low-power operation. | DL1ABC: `PSE QRP.` / DO2XYZ: `QRP 5 W now.` | 1 | `BB205` |
|
||||
| `QRP` | Shall I decrease transmitter power? | Decrease transmitter power. | Formal request to reduce power; in everyday amateur use it also describes low-power operation. | DL1ABC: `PSE QRP.` / DO2XYZ: `QRP 5 W now.` | 1 | `BB205` |
|
||||
| `QRT` | Shall I stop transmitting? | Stop transmitting; I am stopping transmission. | Used to close down transmission or ask another station to stop transmitting. | DL1ABC: `I must QRT.` / DO2XYZ: `QSL, 73.` | 1 | `BB203` |
|
||||
| `QRV` | Are you ready? | I am ready. | Used to ask if a station is ready, or to say that a station is available for traffic. | DL1ABC: `QRV?` / DO2XYZ: `QRV, go ahead.` | 2 | `BB204`, `BE107` |
|
||||
| `QRX` | When will you call me again? | I will call you again; wait or stand by. | Used to ask for a later call or to tell the other station to wait. | DL1ABC: `QRX 5 min.` / DO2XYZ: `QSL, standing by.` | 1 | `BB202` |
|
||||
| `QRZ` | Who is calling me? | You are being called by ...; in pile-ups also a request for further callers. | Used when a calling station was not copied, or by a wanted station to invite the next caller. | DL1ABC: `QRZ?` / DO2XYZ: `DO2XYZ calling.` | 6 | `BB203`, `BE101`, `BE104`, `BE107`, `BE112`, `BE115` |
|
||||
| `QRX` | When will you call me again? | I will call you again at a stated time and frequency. | In normal amateur use, `QRX 5` also means to stand by for five minutes. | DL1ABC: `QRX 5 min.` / DO2XYZ: `QSL, standing by.` | 1 | `BB202` |
|
||||
| `QRZ` | Who is calling me? | You are being called by ... | Used when a caller was not copied; DX and contest stations also use the question form to invite the next caller. | DL1ABC: `QRZ?` / DO2XYZ: `DO2XYZ calling.` | 6 | `BB203`, `BE101`, `BE104`, `BE107`, `BE112`, `BE115` |
|
||||
| `QSB` | Is the strength of my signal fading? | Your signal strength is fading or fluctuating. | Used when signal strength rises and falls because of propagation. | DL1ABC: `QSB?` / DO2XYZ: `Yes, strong QSB, now 55.` | 2 | `BB201`, `BE107` |
|
||||
| `QSL` | Can you acknowledge receipt? | I acknowledge receipt; also a confirmation card or electronic confirmation for a QSO. | Used to confirm that information was received, or later for contact confirmation. | DL1ABC: `My QTH Berlin, QSL?` / DO2XYZ: `QSL, Berlin copied.` | 12 | `BB203`, `BE103`, `BE210`, `BE307`, `BG104`, `BG105`, `BG106`, `BG107`, `BG108`, `BG109`, `BG110`, `BG111` |
|
||||
| `QSO` | Can you communicate directly with ...? | I can communicate directly with ...; commonly a radio contact. | Used for the radio contact itself, or when asking whether direct contact is possible. | DL1ABC: `Tnx for QSO.` / DO2XYZ: `Tnx also, 73.` | 9 | `BB202`, `BE108`, `BE110`, `BE303`, `BE308`, `BG107`, `BG109`, `NF112`, `NG110` |
|
||||
| `QSL` | Can you acknowledge receipt? | I acknowledge receipt. | Confirms copied information; QSL cards and electronic QSLs are the later amateur contact-confirmation usage. | DL1ABC: `My QTH Berlin, QSL?` / DO2XYZ: `QSL, Berlin copied.` | 12 | `BB203`, `BE103`, `BE210`, `BE307`, `BG104`, `BG105`, `BG106`, `BG107`, `BG108`, `BG109`, `BG110`, `BG111` |
|
||||
| `QSO` | Can you communicate with ... directly or by relay? | I can communicate with ... directly or by relay through ... | In everyday amateur speech and logging, a QSO means a radio contact. | DL1ABC: `Tnx for QSO.` / DO2XYZ: `Tnx also, 73.` | 9 | `BB202`, `BE108`, `BE110`, `BE303`, `BE308`, `BG107`, `BG109`, `NF112`, `NG110` |
|
||||
| `QSY` | Shall I change to another frequency? | Change frequency; I am changing frequency. | Used to move a contact away from a calling frequency or crowded channel. | DL1ABC: `PSE QSY 145.525.` / DO2XYZ: `QSL, QSY now.` | 3 | `BB206`, `BE103`, `BE107` |
|
||||
| `QTH` | What is your position or location? | My position or location is ... | Used to ask for or give the station location, often city, locator, or portable position. | DL1ABC: `QTH?` / DO2XYZ: `QTH Munich, JN58.` | 3 | `BB204`, `BE101`, `BE111` |
|
||||
|
||||
@@ -33,7 +33,7 @@ words. Mode names and technical abbreviations such as `CW`, `SSB`, `FM`,
|
||||
|
||||
| Code | What it means when question | What it means when not in question | Explanation, when actually used | Usage example | Exam references | Question IDs |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `BK` | Not a Q-question form. | Break; signal to interrupt a running transmission, also used for informal handover. | Used in telegraphy to interrupt or quickly pass transmission without a formal over. | DL1ABC: `... antenna is BK` / DO2XYZ: `BK, pse repeat power.` | 1 | `BB108` |
|
||||
| `BK` | Not a Q-question form. | Break; signal to interrupt a running transmission, also used for informal handover. | Used in telegraphy to interrupt or quickly pass transmission without a formal over. | DL1ABC: `... running 10 W BK` / DO2XYZ: `BK, my PWR 5 W.` | 1 | `BB108` |
|
||||
| `CQ` | Not a Q-question form. | General call to any station. With an added target, it limits the desired replies, for example `CQ DX` or `CQ DL`. | Used to start a contact when no specific station is being called. | DL1ABC: `CQ CQ CQ de DL1ABC k` / DO2XYZ: `DL1ABC de DO2XYZ k` | 18 | `BB102`, `BB104`, `BB105`, `BE101`, `BE102`, `BE104`, `BE105`, `BE108`, `BE109`, `BE110`, `BE112`, `BE113`, `BE114`, `BE116`, `BE304`, `BE309`, `BE310`, `BE311` |
|
||||
| `DE` | Not a Q-question form. | From; this is. Used before the sending station's call sign in telegraphy. | Used between the called station and own call sign, especially in CW. | DL1ABC: `CQ de DL1ABC k` / DO2XYZ: `DL1ABC de DO2XYZ k` | 4 | `BE104`, `BE112`, `BE113`, `BE114` |
|
||||
| `DX` | Not a Q-question form. | Long distance. In `CQ DX`, the requested distance depends on band and context: intercontinental on HF in the exam examples, several hundred km on VHF/UHF. | Used to seek or describe long-distance contacts. | DL1ABC: `CQ DX de DL1ABC k` / PY2XYZ: `DL1ABC de PY2XYZ k` | 14 | `AH105`, `AH106`, `AH107`, `BB103`, `BB104`, `BB105`, `BE109`, `BE114`, `BE312`, `BE410`, `EH102`, `EH104`, `EH201`, `VE301` |
|
||||
@@ -43,6 +43,7 @@ words. Mode names and technical abbreviations such as `CW`, `SSB`, `FM`,
|
||||
| `R` | Not a Q-question form. | Received; everything before it was copied correctly. Sent at the start of a transmission to acknowledge the previous one. | Used in telegraphy to confirm full receipt before continuing with own traffic. | DL1ABC: `R tnx report, ur RST 579` / DO2XYZ: `QSL, 73.` | 1 | `BB110` |
|
||||
| `RST` | Not a Q-question form. | Reception report: readability, signal strength, and tone quality. In SSB only `R` and `S` are normally given. | Used to exchange signal reports during a QSO or contest. | DL1ABC: `ur RST 599` / DO2XYZ: `tnx, ur 579.` | 9 | `BE201`, `BE202`, `BE203`, `BE204`, `BE205`, `BE206`, `BE207`, `BE208`, `BE209` |
|
||||
| `TEST` | Not a Q-question form. | Contest call indicator in the catalog example `CQ FD ... TEST`. | Used in contest calls to show the call is for contest contacts. | DL1ABC: `CQ TEST de DL1ABC` / DO2XYZ: `DL1ABC de DO2XYZ 59 012` | 1 | `BE116` |
|
||||
| `UTC` | Not a Q-question form. | Coordinated Universal Time; koordinierte Weltzeit. | Used for logs, schedules and QSL confirmations so stations in different time zones record the same date and time. | Log: `2026-06-22 14:30 UTC` | 5 | `BF108`, `BG105`, `BG106`, `BG107`, `BG108` |
|
||||
|
||||
## Emergency and Safety Signals
|
||||
|
||||
|
||||
+142
-112
@@ -1,47 +1,27 @@
|
||||
{
|
||||
"q_codes": [
|
||||
{
|
||||
"code": "QRA",
|
||||
"question": "What is the name (call sign) of your station?",
|
||||
"statement": "The name of my station is ...",
|
||||
"exam": false,
|
||||
"explanation": "Asks for or gives the station's name or call sign."
|
||||
},
|
||||
{
|
||||
"code": "QRG",
|
||||
"question": "Will you tell me my exact frequency?",
|
||||
"statement": "Your exact frequency is ... kHz.",
|
||||
"statement": "Your exact frequency is ... kHz or MHz.",
|
||||
"exam": false,
|
||||
"explanation": "Used to ask for or report an exact operating frequency."
|
||||
},
|
||||
{
|
||||
"code": "QRH",
|
||||
"question": "Does my frequency vary?",
|
||||
"statement": "Your frequency varies.",
|
||||
"exam": false,
|
||||
"explanation": "Reports that a transmitter is drifting off its nominal frequency."
|
||||
},
|
||||
{
|
||||
"code": "QRK",
|
||||
"question": "What is the readability of my signals?",
|
||||
"statement": "The readability of your signals is ... (1 to 5).",
|
||||
"exam": false,
|
||||
"explanation": "Reports intelligibility on a 1 to 5 scale, the R in an RST report."
|
||||
"explanation": "Formal frequency-reporting Q-code. German-speaking amateurs also use 'QRG' informally as a noun meaning operating frequency, for example 'auf dieser QRG'.",
|
||||
"example": "DL1ABC: `QRG?` / DO2XYZ: `Your QRG is 7.042 MHz.`"
|
||||
},
|
||||
{
|
||||
"code": "QRL",
|
||||
"question": "Are you busy? / Is this frequency in use?",
|
||||
"statement": "I am busy; the frequency is in use, please do not interfere.",
|
||||
"question": "Are you busy?",
|
||||
"statement": "I am busy; please do not interfere.",
|
||||
"exam": false,
|
||||
"explanation": "Sent as QRL? before transmitting to check whether a frequency is already occupied.",
|
||||
"explanation": "In amateur operation, QRL? is the standard short CW check meaning 'Is this frequency in use?' Listen first, send it once or twice, and wait before calling CQ.",
|
||||
"example": "DL1ABC: `QRL?` / DO2XYZ: `QRL, in QSO.`"
|
||||
},
|
||||
{
|
||||
"code": "QRM",
|
||||
"question": "Are you being interfered with?",
|
||||
"statement": "I am being interfered with; man-made interference.",
|
||||
"question": "Is my transmission being interfered with?",
|
||||
"statement": "Your transmission is being interfered with.",
|
||||
"exam": true,
|
||||
"explanation": "Used to report man-made interference or ask whether the other station has interference.",
|
||||
"explanation": "Interference from other stations or man-made sources, distinct from atmospheric static (QRN). In everyday amateur speech, 'I have QRM' is also common even though the formal statement addresses the other station's transmission.",
|
||||
"example": "DL1ABC: `QRM?` / DO2XYZ: `QRM, pse repeat report.`"
|
||||
},
|
||||
{
|
||||
@@ -55,7 +35,7 @@
|
||||
{
|
||||
"code": "QRO",
|
||||
"question": "Shall I increase transmitter power?",
|
||||
"statement": "Increase transmitter power; I am increasing power.",
|
||||
"statement": "Increase transmitter power.",
|
||||
"exam": true,
|
||||
"explanation": "Used when more power may be needed, usually after trying normal operating improvements first.",
|
||||
"example": "DL1ABC: `QRO?` / DO2XYZ: `PSE QRO, ur weak.`"
|
||||
@@ -63,9 +43,9 @@
|
||||
{
|
||||
"code": "QRP",
|
||||
"question": "Shall I decrease transmitter power?",
|
||||
"statement": "Decrease transmitter power; low-power operation.",
|
||||
"statement": "Decrease transmitter power.",
|
||||
"exam": true,
|
||||
"explanation": "Used to request or announce reduced transmitter power; also describes low-power operation.",
|
||||
"explanation": "Formal request to reduce power. In everyday amateur use, QRP also describes low-power operation—typically no more than 5 W output for CW/data or 10 W PEP for SSB under the common award convention.",
|
||||
"example": "DL1ABC: `PSE QRP.` / DO2XYZ: `QRP 5 W now.`"
|
||||
},
|
||||
{
|
||||
@@ -78,7 +58,7 @@
|
||||
{
|
||||
"code": "QRS",
|
||||
"question": "Shall I send more slowly?",
|
||||
"statement": "Send more slowly.",
|
||||
"statement": "Send more slowly (... words per minute).",
|
||||
"exam": false,
|
||||
"explanation": "Requests a slower Morse sending speed; common courtesy to slower operators."
|
||||
},
|
||||
@@ -90,13 +70,6 @@
|
||||
"explanation": "Used to close down transmission or ask another station to stop transmitting.",
|
||||
"example": "DL1ABC: `I must QRT.` / DO2XYZ: `QSL, 73.`"
|
||||
},
|
||||
{
|
||||
"code": "QRU",
|
||||
"question": "Have you anything for me?",
|
||||
"statement": "I have nothing for you.",
|
||||
"exam": false,
|
||||
"explanation": "Asks whether there is traffic to pass, or reports that there is none."
|
||||
},
|
||||
{
|
||||
"code": "QRV",
|
||||
"question": "Are you ready?",
|
||||
@@ -107,27 +80,20 @@
|
||||
},
|
||||
{
|
||||
"code": "QRX",
|
||||
"question": "When will you call me again? / Shall I wait?",
|
||||
"statement": "I will call you again at ...; please wait (stand by).",
|
||||
"question": "When will you call me again?",
|
||||
"statement": "I will call you again at ... hours on ... kHz or MHz.",
|
||||
"exam": true,
|
||||
"explanation": "Used to ask for a later call or to tell the other station to wait.",
|
||||
"explanation": "The formal code schedules a later call. In normal amateur operation, QRX followed by a duration—such as 'QRX 5'—usually means 'stand by for five minutes'.",
|
||||
"example": "DL1ABC: `QRX 5 min.` / DO2XYZ: `QSL, standing by.`"
|
||||
},
|
||||
{
|
||||
"code": "QRZ",
|
||||
"question": "Who is calling me?",
|
||||
"statement": "You are being called by ...; in pile-ups also a request for further callers.",
|
||||
"statement": "You are being called by ...",
|
||||
"exam": true,
|
||||
"explanation": "Used when a calling station was not copied, or by a wanted station to invite the next caller.",
|
||||
"explanation": "Use QRZ? when a calling station was not copied. DX and contest stations also say QRZ? to invite the next caller; that widespread pile-up use is still the question form, not a separate statement meaning.",
|
||||
"example": "DL1ABC: `QRZ?` / DO2XYZ: `DO2XYZ calling.`"
|
||||
},
|
||||
{
|
||||
"code": "QSA",
|
||||
"question": "What is the strength of my signals?",
|
||||
"statement": "The strength of your signals is ... (1 to 5).",
|
||||
"exam": false,
|
||||
"explanation": "Reports signal strength on a 1 to 5 scale, related to the S in an RST report."
|
||||
},
|
||||
{
|
||||
"code": "QSB",
|
||||
"question": "Is the strength of my signal fading?",
|
||||
@@ -138,34 +104,27 @@
|
||||
},
|
||||
{
|
||||
"code": "QSK",
|
||||
"question": "Can you hear me between your signals?",
|
||||
"statement": "I can hear you between my signals; you may break in (full break-in).",
|
||||
"question": "Can you hear me between your signals, and may I break in?",
|
||||
"statement": "I can hear you between my signals; break in on my transmission.",
|
||||
"exam": false,
|
||||
"explanation": "Describes full break-in CW, where the operator hears the band between his own dots and dashes."
|
||||
"explanation": "Describes full break-in CW, where transmit/receive switching is fast enough to hear the band between one's own dots and dashes. Modern transceiver menus often label this feature QSK."
|
||||
},
|
||||
{
|
||||
"code": "QSL",
|
||||
"question": "Can you acknowledge receipt?",
|
||||
"statement": "I acknowledge receipt; also a confirmation card or electronic confirmation for a QSO.",
|
||||
"statement": "I acknowledge receipt.",
|
||||
"exam": true,
|
||||
"explanation": "Used to confirm that information was received, or later for contact confirmation.",
|
||||
"explanation": "On air it confirms that information was copied. A paper QSL card or electronic QSL is the later confirmation of a completed contact; that noun usage is practical amateur shorthand, not the formal statement text.",
|
||||
"example": "DL1ABC: `My QTH Berlin, QSL?` / DO2XYZ: `QSL, Berlin copied.`"
|
||||
},
|
||||
{
|
||||
"code": "QSO",
|
||||
"question": "Can you communicate directly with ...?",
|
||||
"statement": "I can communicate directly with ...; commonly a radio contact.",
|
||||
"question": "Can you communicate with ... directly or by relay?",
|
||||
"statement": "I can communicate with ... directly or by relay through ...",
|
||||
"exam": true,
|
||||
"explanation": "Used for the radio contact itself, or when asking whether direct contact is possible.",
|
||||
"explanation": "The formal code concerns establishing communication. In everyday amateur speech and logging, a QSO simply means a completed or ongoing radio contact.",
|
||||
"example": "DL1ABC: `Tnx for QSO.` / DO2XYZ: `Tnx also, 73.`"
|
||||
},
|
||||
{
|
||||
"code": "QSP",
|
||||
"question": "Will you relay to ...?",
|
||||
"statement": "I will relay to ...",
|
||||
"exam": false,
|
||||
"explanation": "Asks a station to pass traffic on to a third station that cannot be reached directly."
|
||||
},
|
||||
{
|
||||
"code": "QSY",
|
||||
"question": "Shall I change to another frequency?",
|
||||
@@ -188,13 +147,6 @@
|
||||
"exam": true,
|
||||
"explanation": "Used to ask for or give the station location, often city, locator, or portable position.",
|
||||
"example": "DL1ABC: `QTH?` / DO2XYZ: `QTH Munich, JN58.`"
|
||||
},
|
||||
{
|
||||
"code": "QTR",
|
||||
"question": "What is the correct time?",
|
||||
"statement": "The correct time is ...",
|
||||
"exam": false,
|
||||
"explanation": "Asks for or gives the time, usually in UTC."
|
||||
}
|
||||
],
|
||||
"abbreviations": [
|
||||
@@ -220,7 +172,8 @@
|
||||
{
|
||||
"code": "ANT",
|
||||
"meaning": "Antenna.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Common CW and logging abbreviation, for example when exchanging antenna type, height or direction."
|
||||
},
|
||||
{
|
||||
"code": "AR",
|
||||
@@ -241,7 +194,7 @@
|
||||
"meaning": "Break — interrupt or quickly hand over a running transmission.",
|
||||
"exam": true,
|
||||
"explanation": "Used in telegraphy to interrupt or quickly pass transmission without a formal over.",
|
||||
"example": "DL1ABC: `... antenna is BK` / DO2XYZ: `BK, pse repeat power.`"
|
||||
"example": "DL1ABC: `... running 10 W BK` / DO2XYZ: `BK, my PWR 5 W.`"
|
||||
},
|
||||
{
|
||||
"code": "BT",
|
||||
@@ -253,7 +206,8 @@
|
||||
{
|
||||
"code": "BTU",
|
||||
"meaning": "Back to you.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Informal voice and digital-text handover. In formal CW procedure, K or KN is clearer."
|
||||
},
|
||||
{
|
||||
"code": "CFM",
|
||||
@@ -264,7 +218,8 @@
|
||||
{
|
||||
"code": "CL",
|
||||
"meaning": "Closing down / clear (going off the air).",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Sent in CW to announce that the station is closing and will not remain available for another contact."
|
||||
},
|
||||
{
|
||||
"code": "CQ",
|
||||
@@ -273,10 +228,24 @@
|
||||
"explanation": "Used to start a contact when no specific station is being called. A target narrows it, e.g. CQ DX or CQ DL.",
|
||||
"example": "DL1ABC: `CQ CQ CQ de DL1ABC k` / DO2XYZ: `DL1ABC de DO2XYZ k`"
|
||||
},
|
||||
{
|
||||
"code": "CPY",
|
||||
"meaning": "Copy — receive or understand a signal.",
|
||||
"exam": false,
|
||||
"explanation": "Common operating shorthand in questions such as 'HW CPY?' and replies such as 'CPY 100%'. It refers to successful reception, not making a duplicate document.",
|
||||
"example": "DL1ABC: `HW CPY?` / DO2XYZ: `FB CPY, no QRM.`"
|
||||
},
|
||||
{
|
||||
"code": "CUAGN",
|
||||
"meaning": "See you again.",
|
||||
"exam": false,
|
||||
"explanation": "Compact friendly sign-off used in CW and digital text, often immediately before 73."
|
||||
},
|
||||
{
|
||||
"code": "CUL",
|
||||
"meaning": "See you later.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Friendly informal closing used mainly in CW and text modes."
|
||||
},
|
||||
{
|
||||
"code": "DE",
|
||||
@@ -315,27 +284,38 @@
|
||||
{
|
||||
"code": "GA",
|
||||
"meaning": "Good afternoon / go ahead.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Context decides the meaning: a greeting near the start of a contact, or an invitation to continue. Use K or KN for an unambiguous CW handover."
|
||||
},
|
||||
{
|
||||
"code": "GE",
|
||||
"meaning": "Good evening.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Common greeting near the start of a CW or digital-text contact."
|
||||
},
|
||||
{
|
||||
"code": "GM",
|
||||
"meaning": "Good morning.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Common greeting near the start of a CW or digital-text contact."
|
||||
},
|
||||
{
|
||||
"code": "GN",
|
||||
"meaning": "Good night.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Friendly closing, normally used at the end rather than as the formal end-of-contact prosign."
|
||||
},
|
||||
{
|
||||
"code": "GL",
|
||||
"meaning": "Good luck.",
|
||||
"exam": false,
|
||||
"explanation": "Common friendly wish in contests, award chasing and ordinary CW or digital contacts."
|
||||
},
|
||||
{
|
||||
"code": "GUD",
|
||||
"meaning": "Good.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Common CW spelling reduction, as in 'gud sig' or 'gud copy'."
|
||||
},
|
||||
{
|
||||
"code": "HI",
|
||||
@@ -346,12 +326,15 @@
|
||||
{
|
||||
"code": "HR",
|
||||
"meaning": "Here / hear.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "A context-dependent CW contraction: 'WX HR' means weather here, while 'HR U' means hear you."
|
||||
},
|
||||
{
|
||||
"code": "HW",
|
||||
"meaning": "How? (e.g. HW? = how do you copy me?).",
|
||||
"exam": false
|
||||
"meaning": "How?",
|
||||
"exam": false,
|
||||
"explanation": "Usually asks for reception or an opinion near the end of an exchange.",
|
||||
"example": "DL1ABC: `HW CPY?` / DO2XYZ: `FB CPY.`"
|
||||
},
|
||||
{
|
||||
"code": "K",
|
||||
@@ -370,17 +353,20 @@
|
||||
{
|
||||
"code": "MNI",
|
||||
"meaning": "Many.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Frequent CW contraction, for example 'mni tnx' or 'mni QSOs'."
|
||||
},
|
||||
{
|
||||
"code": "NIL",
|
||||
"meaning": "Nothing / nothing heard.",
|
||||
"exam": false,
|
||||
"explanation": "Used when no signal, traffic or result was obtained, for example 'NIL HRD' in a log or net report."
|
||||
},
|
||||
{
|
||||
"code": "NR",
|
||||
"meaning": "Number / near.",
|
||||
"exam": false
|
||||
},
|
||||
{
|
||||
"code": "OB",
|
||||
"meaning": "Old boy (familiar form of address).",
|
||||
"exam": false
|
||||
"meaning": "Number.",
|
||||
"exam": false,
|
||||
"explanation": "Used before a serial number, message number or other numbered item. Restricting the card to this common operational meaning avoids the ambiguous dictionary expansion 'near'."
|
||||
},
|
||||
{
|
||||
"code": "OM",
|
||||
@@ -391,7 +377,8 @@
|
||||
{
|
||||
"code": "OP",
|
||||
"meaning": "Operator / the operator's name.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Common when exchanging names, for example 'OP Anna', and in station descriptions such as single-op or multi-op."
|
||||
},
|
||||
{
|
||||
"code": "PSE",
|
||||
@@ -403,7 +390,8 @@
|
||||
{
|
||||
"code": "PWR",
|
||||
"meaning": "Power (transmitter output).",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Usually followed by the RF output power and unit, for example 'PWR 5 W'. Do not confuse transmitter output with ERP or EIRP."
|
||||
},
|
||||
{
|
||||
"code": "R",
|
||||
@@ -415,12 +403,26 @@
|
||||
{
|
||||
"code": "RIG",
|
||||
"meaning": "Station equipment / transceiver.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Informal term for the radio equipment in use, often exchanged together with antenna and power."
|
||||
},
|
||||
{
|
||||
"code": "RPT",
|
||||
"meaning": "Repeat / report.",
|
||||
"exam": false
|
||||
"meaning": "Repeat.",
|
||||
"exam": false,
|
||||
"explanation": "Requests retransmission of missed text. Signal report is better abbreviated RPRT, avoiding a meaning collision in the reverse card."
|
||||
},
|
||||
{
|
||||
"code": "RPRT",
|
||||
"meaning": "Report — usually a signal report.",
|
||||
"exam": false,
|
||||
"explanation": "Distinguishes the noun 'report' from RPT meaning 'repeat'. It is commonly used when asking for or sending an RST report."
|
||||
},
|
||||
{
|
||||
"code": "RR73",
|
||||
"meaning": "Report received; best regards — final FT8 acknowledgement.",
|
||||
"exam": false,
|
||||
"explanation": "In an FT8 exchange, RR73 confirms receipt of the other station's report and closes the contact in one message. It is protocol text, not a general voice or CW abbreviation."
|
||||
},
|
||||
{
|
||||
"code": "RST",
|
||||
@@ -432,7 +434,8 @@
|
||||
{
|
||||
"code": "RX",
|
||||
"meaning": "Receiver.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Equipment and diagram abbreviation paired with TX. On repeater listings, RX denotes the frequency on which the station receives."
|
||||
},
|
||||
{
|
||||
"code": "SK",
|
||||
@@ -444,7 +447,14 @@
|
||||
{
|
||||
"code": "SRI",
|
||||
"meaning": "Sorry.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Brief apology in CW or digital text, often followed by a correction or repeat."
|
||||
},
|
||||
{
|
||||
"code": "SWL",
|
||||
"meaning": "Short-Wave Listener — listener who receives but does not transmit.",
|
||||
"exam": false,
|
||||
"explanation": "SWLs monitor amateur and broadcast stations, may submit reception reports, and do not need an amateur transmitting authorization merely to receive."
|
||||
},
|
||||
{
|
||||
"code": "TEST",
|
||||
@@ -455,43 +465,63 @@
|
||||
},
|
||||
{
|
||||
"code": "TNX",
|
||||
"meaning": "Thanks (also written TKS).",
|
||||
"exam": false
|
||||
"meaning": "Thanks — three-letter telegraphic abbreviation.",
|
||||
"exam": false,
|
||||
"explanation": "Very common in CW and digital text; TKS is an equivalent spelling. The explicit three-letter wording distinguishes this reverse card from TU."
|
||||
},
|
||||
{
|
||||
"code": "TU",
|
||||
"meaning": "Thank you.",
|
||||
"exam": false
|
||||
"meaning": "Thank you — two-letter acknowledgement or sign-off.",
|
||||
"exam": false,
|
||||
"explanation": "Frequently used by contest or DX stations to acknowledge one caller and immediately continue, for example 'TU QRZ?'."
|
||||
},
|
||||
{
|
||||
"code": "TX",
|
||||
"meaning": "Transmitter.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Equipment and diagram abbreviation paired with RX. On repeater listings, TX denotes the frequency on which the station transmits."
|
||||
},
|
||||
{
|
||||
"code": "UR",
|
||||
"meaning": "Your / you're.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Context-dependent CW and text contraction, as in 'UR RST 579' or 'UR FB'."
|
||||
},
|
||||
{
|
||||
"code": "UTC",
|
||||
"meaning": "Coordinated Universal Time — koordinierte Weltzeit.",
|
||||
"exam": true,
|
||||
"explanation": "Use UTC for logs, schedules and QSL confirmations so both stations record the same date and time without daylight-saving ambiguity. Germany is UTC+1 in MEZ and UTC+2 in MESZ."
|
||||
},
|
||||
{
|
||||
"code": "VY",
|
||||
"meaning": "Very.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Common CW contraction, for example 'vy gud' or 'vy strong'."
|
||||
},
|
||||
{
|
||||
"code": "WX",
|
||||
"meaning": "Weather.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Common conversational topic in CW and digital contacts, for example 'WX HR SUNNY'."
|
||||
},
|
||||
{
|
||||
"code": "WPM",
|
||||
"meaning": "Words Per Minute — Morse sending speed.",
|
||||
"exam": false,
|
||||
"explanation": "Standard practical speed unit for CW. Send no faster than you can reliably receive, and slow down to match a station that answers at a lower speed."
|
||||
},
|
||||
{
|
||||
"code": "XYL",
|
||||
"meaning": "Wife (ex-young-lady).",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Traditional amateur shorthand that is still encountered but can sound dated; use a person's name when known."
|
||||
},
|
||||
{
|
||||
"code": "YL",
|
||||
"meaning": "Young lady — any female operator.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Traditional term for a female radio amateur regardless of age. It remains common in names of clubs, nets and awards."
|
||||
},
|
||||
{
|
||||
"code": "MAYDAY",
|
||||
|
||||
+110
-84
@@ -26,14 +26,14 @@
|
||||
"category": "Betriebsart",
|
||||
"meaning": "Doppelseitenbandmodulation — double-sideband modulation.",
|
||||
"exam": true,
|
||||
"explanation": "Both sidebands are transmitted; with the carrier suppressed it is DSB-SC, the starting point for generating SSB."
|
||||
"explanation": "Both sidebands are transmitted. The carrier may be present, as in conventional AM, or suppressed, as in DSB-SC; suppressing one of the two sidebands produces SSB."
|
||||
},
|
||||
{
|
||||
"code": "SSB",
|
||||
"category": "Betriebsart",
|
||||
"meaning": "Einseitenbandmodulation — single sideband.",
|
||||
"exam": true,
|
||||
"explanation": "Only one sideband is sent and the carrier is suppressed, making it the most bandwidth- and power-efficient analogue voice mode on HF."
|
||||
"explanation": "Only one sideband is transmitted and the carrier is normally suppressed. Compared with conventional AM, SSB uses about half the bandwidth and does not waste transmitter power on a full carrier, which is why it dominates analogue HF voice."
|
||||
},
|
||||
{
|
||||
"code": "USB",
|
||||
@@ -74,7 +74,8 @@
|
||||
"code": "ASK",
|
||||
"category": "Betriebsart",
|
||||
"meaning": "Amplitude Shift Keying — Amplitudenumtastung.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Digital symbols are represented by discrete carrier amplitudes. Simple on-off keying is the two-state special case; unlike FM or PSK, amplitude noise directly affects the keyed quantity."
|
||||
},
|
||||
{
|
||||
"code": "QAM",
|
||||
@@ -88,20 +89,21 @@
|
||||
"category": "Betriebsart",
|
||||
"meaning": "Radioteletype — Funkfernschreiben.",
|
||||
"exam": true,
|
||||
"explanation": "Classic FSK teleprinter mode using the 5-bit Baudot code."
|
||||
"explanation": "Classic teleprinter operation using frequency-shift keying and the five-bit ITA2 alphabet, commonly called Baudot. On amateur HF it is usually copied with software rather than a mechanical teleprinter."
|
||||
},
|
||||
{
|
||||
"code": "PSK31",
|
||||
"category": "Betriebsart",
|
||||
"meaning": "Phase-Shift Keying, 31 Baud — narrowband keyboard digital mode.",
|
||||
"exam": true
|
||||
"meaning": "Phase Shift Keying at 31.25 baud — narrowband keyboard digital mode.",
|
||||
"exam": true,
|
||||
"explanation": "The name is conventionally rounded: the symbol rate is 31.25 baud. This live keyboard-to-keyboard HF mode is very narrow, but overdriving an SSB transmitter produces a needlessly wide, distorted signal."
|
||||
},
|
||||
{
|
||||
"code": "FT8",
|
||||
"category": "Betriebsart",
|
||||
"meaning": "digitale Schwachsignal-Betriebsart (WSJT-X) — weak-signal digital mode.",
|
||||
"exam": true,
|
||||
"explanation": "Highly sensitive mode with fixed 15-second transmit slots; decodes signals well below the noise floor."
|
||||
"explanation": "A structured weak-signal mode with synchronized 15-second periods and short, highly constrained exchanges. It can decode signals far below the audible noise floor, but it is intended for brief contacts rather than conversation."
|
||||
},
|
||||
{
|
||||
"code": "WSPR",
|
||||
@@ -129,52 +131,57 @@
|
||||
"category": "Betriebsart",
|
||||
"meaning": "Schmalband-FM — narrowband FM.",
|
||||
"exam": true,
|
||||
"explanation": "FM with reduced deviation (e.g. a 12.5 kHz channel), the usual form for amateur FM voice."
|
||||
"explanation": "FM with limited frequency deviation and occupied bandwidth. It is the normal analogue voice mode on VHF/UHF repeaters and simplex channels; channel spacing and occupied bandwidth are related but are not the same quantity."
|
||||
},
|
||||
|
||||
{
|
||||
"code": "MF",
|
||||
"category": "Band",
|
||||
"meaning": "Mittelwelle — medium frequency, 300 kHz to 3 MHz.",
|
||||
"exam": true
|
||||
"meaning": "Medium Frequency — 300 kHz to 3 MHz (Mittelwelle).",
|
||||
"exam": true,
|
||||
"explanation": "The ITU band containing the amateur 630 m and 160 m allocations. In practice these low bands need large or electrically shortened antennas and are strongly affected by ground loss and nighttime propagation."
|
||||
},
|
||||
{
|
||||
"code": "KW",
|
||||
"category": "Band",
|
||||
"meaning": "Kurzwelle — shortwave / HF, 3 to 30 MHz.",
|
||||
"meaning": "Kurzwelle — shortwave, 3 to 30 MHz.",
|
||||
"exam": true,
|
||||
"explanation": "The classic long-distance amateur range; relies on ionospheric (skywave) propagation."
|
||||
"explanation": "The German name for the ITU HF band. This is the classic long-distance amateur range, where ionospheric propagation can support regional or worldwide contacts."
|
||||
},
|
||||
{
|
||||
"code": "UKW",
|
||||
"category": "Band",
|
||||
"meaning": "Ultrakurzwelle — VHF, 30 to 300 MHz.",
|
||||
"meaning": "Ultrakurzwelle — German name for 30 to 300 MHz.",
|
||||
"exam": true,
|
||||
"explanation": "Mostly line-of-sight propagation; home of FM voice and repeaters. In the BNetzA band names UKW is exactly VHF; 300 MHz to 3 GHz is UHF / Dezimeterwelle. Colloquially 'UKW' is sometimes stretched to mean VHF and up, but that is not the exam definition."
|
||||
},
|
||||
{
|
||||
"code": "VHF",
|
||||
"category": "Band",
|
||||
"meaning": "Very High Frequency — 30 to 300 MHz (UKW).",
|
||||
"exam": true
|
||||
"meaning": "Very High Frequency — ITU band from 30 to 300 MHz.",
|
||||
"exam": true,
|
||||
"explanation": "The international designation corresponding to German UKW. The 2 m amateur band is the main practical example, used for local SSB/CW, FM simplex, repeaters, satellites and weak-signal work."
|
||||
},
|
||||
{
|
||||
"code": "UHF",
|
||||
"category": "Band",
|
||||
"meaning": "Ultra High Frequency — 300 MHz to 3 GHz.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Includes the amateur 70 cm, 23 cm and 13 cm bands. Propagation is mainly line of sight, although repeaters, satellites, tropospheric enhancement and reflection can extend the range."
|
||||
},
|
||||
{
|
||||
"code": "SHF",
|
||||
"category": "Band",
|
||||
"meaning": "Super High Frequency — 3 to 30 GHz (Mikrowellen).",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Microwave range containing several amateur bands. Feed-line loss, connector quality and accurate antenna alignment become increasingly important, so the transverter or low-noise amplifier is often mounted near the antenna."
|
||||
},
|
||||
{
|
||||
"code": "EHF",
|
||||
"category": "Band",
|
||||
"meaning": "Extremely High Frequency — 30 to 300 GHz.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Millimetre-wave range containing experimental amateur allocations. Operation is specialized because atmospheric absorption, mechanical tolerances and feed-line losses are severe."
|
||||
},
|
||||
|
||||
{
|
||||
@@ -187,28 +194,30 @@
|
||||
{
|
||||
"code": "HF",
|
||||
"category": "Signal & Frequenz",
|
||||
"meaning": "Hochfrequenz — radio frequency (RF), above the audio range.",
|
||||
"meaning": "Hochfrequenz — German technical term for a radio-frequency signal.",
|
||||
"exam": true,
|
||||
"explanation": "In German usage HF denotes RF signals in general, not specifically the 3 to 30 MHz band (that band is called KW / Kurzwelle)."
|
||||
"explanation": "In German circuit descriptions, HF contrasts with NF and can mean radio-frequency energy generally. In the international ITU band names, however, HF specifically means 3 to 30 MHz; the German exam normally calls that range KW. Context decides which use is intended."
|
||||
},
|
||||
{
|
||||
"code": "ZF",
|
||||
"category": "Signal & Frequenz",
|
||||
"meaning": "Zwischenfrequenz — intermediate frequency (IF).",
|
||||
"meaning": "Zwischenfrequenz — German abbreviation for intermediate frequency.",
|
||||
"exam": true,
|
||||
"explanation": "The fixed frequency a superheterodyne receiver mixes the signal to, where most selective amplification and filtering happens."
|
||||
},
|
||||
{
|
||||
"code": "IF",
|
||||
"category": "Signal & Frequenz",
|
||||
"meaning": "Intermediate Frequency — englische Bezeichnung für die Zwischenfrequenz (ZF).",
|
||||
"exam": false
|
||||
"meaning": "Intermediate Frequency — English abbreviation for an intermediate frequency.",
|
||||
"exam": false,
|
||||
"explanation": "English-language manuals and schematics use this where German material uses ZF. A superheterodyne receiver converts the wanted signal to this fixed frequency for most of its filtering and gain."
|
||||
},
|
||||
{
|
||||
"code": "RF",
|
||||
"category": "Signal & Frequenz",
|
||||
"meaning": "Radio Frequency — englische Bezeichnung für Hochfrequenz (HF).",
|
||||
"exam": false
|
||||
"meaning": "Radio Frequency — English term for a radio-frequency signal.",
|
||||
"exam": false,
|
||||
"explanation": "The usual English label for signals and circuitry at radio frequencies. German schematics commonly label the same signal domain HF."
|
||||
},
|
||||
|
||||
{
|
||||
@@ -250,7 +259,8 @@
|
||||
"code": "LO",
|
||||
"category": "Baugruppe",
|
||||
"meaning": "Lokaloszillator — local (mixing) oscillator.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Provides the frequency mixed with the received or transmitted signal. The sum or difference product is selected, for example to convert an amateur band to a fixed intermediate frequency."
|
||||
},
|
||||
{
|
||||
"code": "AGC",
|
||||
@@ -270,7 +280,8 @@
|
||||
"code": "PA",
|
||||
"category": "Baugruppe",
|
||||
"meaning": "Endstufe / Leistungsverstärker — power amplifier.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "The final transmitter stage that raises the RF signal to the required output power. It must be driven within its linear range for SSB and digital modes to avoid splatter and intermodulation."
|
||||
},
|
||||
{
|
||||
"code": "LNA",
|
||||
@@ -290,27 +301,29 @@
|
||||
"code": "TRX",
|
||||
"category": "Baugruppe",
|
||||
"meaning": "Transceiver — combined transmitter and receiver.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Common shorthand in German amateur-radio diagrams and operating notes for a radio that shares controls and circuitry between transmit and receive."
|
||||
},
|
||||
{
|
||||
"code": "VOX",
|
||||
"category": "Baugruppe & Bedienung",
|
||||
"meaning": "Voice-Operated Transmit — sprachgesteuerte Sendeumschaltung.",
|
||||
"exam": true,
|
||||
"explanation": "Switches a transceiver to transmit when microphone audio exceeds a threshold. Set gain, delay and anti-VOX carefully so loudspeaker audio or room noise does not key the transmitter accidentally."
|
||||
},
|
||||
{
|
||||
"code": "Balun",
|
||||
"category": "Baugruppe",
|
||||
"meaning": "Symmetrierglied — balanced-to-unbalanced transformer.",
|
||||
"exam": true,
|
||||
"explanation": "Couples a balanced antenna (e.g. a dipole) to an unbalanced coax feed, suppressing currents on the outside of the shield."
|
||||
},
|
||||
|
||||
{
|
||||
"code": "Diode",
|
||||
"category": "Bauteil",
|
||||
"meaning": "Diode — conducts current in one direction only.",
|
||||
"exam": true
|
||||
"explanation": "Interfaces a balanced load, such as a dipole, with an unbalanced feed line such as coax. Common-mode suppression depends on the balun type: a suitable current balun or choke is needed when feed-line current must be blocked."
|
||||
},
|
||||
{
|
||||
"code": "LED",
|
||||
"category": "Bauteil",
|
||||
"meaning": "Leuchtdiode — light-emitting diode.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Emits light when forward biased and requires current limiting. In station equipment it is commonly used for status indication and display backlighting."
|
||||
},
|
||||
{
|
||||
"code": "FET",
|
||||
@@ -330,25 +343,27 @@
|
||||
"code": "BJT",
|
||||
"category": "Bauteil",
|
||||
"meaning": "Bipolartransistor — bipolar junction transistor.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "A current-controlled transistor using both electron and hole conduction. NPN and PNP devices are used in switching and amplifier stages."
|
||||
},
|
||||
{
|
||||
"code": "IC",
|
||||
"category": "Bauteil",
|
||||
"meaning": "integrierter Schaltkreis — integrated circuit.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Many components and circuit functions fabricated on one semiconductor chip, from simple voltage regulators to complete mixers, synthesizers and processors."
|
||||
},
|
||||
{
|
||||
"code": "NTC",
|
||||
"category": "Bauteil",
|
||||
"meaning": "Heißleiter — NTC thermistor (negative temperature coefficient).",
|
||||
"meaning": "Heißleiter — thermistor with a negative temperature coefficient.",
|
||||
"exam": true,
|
||||
"explanation": "Its resistance falls as temperature rises; used for temperature sensing and inrush limiting."
|
||||
},
|
||||
{
|
||||
"code": "PTC",
|
||||
"category": "Bauteil",
|
||||
"meaning": "Kaltleiter — PTC thermistor (positive temperature coefficient).",
|
||||
"meaning": "Kaltleiter — thermistor with a positive temperature coefficient.",
|
||||
"exam": true,
|
||||
"explanation": "Its resistance rises as temperature rises; used as a resettable fuse and for protection."
|
||||
},
|
||||
@@ -356,7 +371,8 @@
|
||||
"code": "LDR",
|
||||
"category": "Bauteil",
|
||||
"meaning": "Fotowiderstand — light-dependent resistor.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Its resistance decreases as illumination increases. It is useful for light sensing but is slower than a photodiode or phototransistor."
|
||||
},
|
||||
{
|
||||
"code": "VDR",
|
||||
@@ -369,15 +385,16 @@
|
||||
{
|
||||
"code": "dB",
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "Dezibel — logarithmic ratio of two powers (10·log₁₀).",
|
||||
"meaning": "Dezibel — logarithmic ratio.",
|
||||
"exam": true,
|
||||
"explanation": "+3 dB is roughly double the power, +10 dB ten times. As a power ratio it is relative, not an absolute level."
|
||||
"explanation": "For power ratios use 10·log₁₀(P₂/P₁); for voltage or current ratios under the same impedance use 20·log₁₀. +3 dB is about twice the power and +10 dB is ten times. Plain dB is relative, unlike dBm or dBW."
|
||||
},
|
||||
{
|
||||
"code": "dBi",
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "Gewinn über Isotropstrahler — gain relative to an isotropic radiator.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "An antenna gain reference, not transmitter power. Because a half-wave dipole has about 2.15 dBi gain, a value in dBi is 2.15 dB higher than the same gain stated in dBd."
|
||||
},
|
||||
{
|
||||
"code": "dBd",
|
||||
@@ -397,34 +414,29 @@
|
||||
"code": "dBW",
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "Pegel bezogen auf 1 W — power level referenced to 1 watt.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "An absolute logarithmic power level: 0 dBW = 1 W and 10 dBW = 10 W. To convert dBW to dBm, add 30 dB."
|
||||
},
|
||||
{
|
||||
"code": "SWR",
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "Stehwellenverhältnis — standing wave ratio.",
|
||||
"exam": true,
|
||||
"explanation": "Ratio of the standing wave's maximum to minimum on the feed line; 1:1 is a perfect match, higher values mean more reflected power."
|
||||
},
|
||||
{
|
||||
"code": "VSWR",
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "Spannungs-Stehwellenverhältnis — voltage standing wave ratio.",
|
||||
"exam": false
|
||||
"explanation": "Normally the ratio of maximum to minimum RF voltage along a feed line. 1:1 is a perfect match; a higher value indicates a larger reflection, but does not by itself identify whether the antenna, feed line or another component causes the mismatch."
|
||||
},
|
||||
{
|
||||
"code": "ERP",
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "effektive Strahlungsleistung — effective radiated power.",
|
||||
"exam": true,
|
||||
"explanation": "Power referenced to a half-wave dipole: transmitter power minus losses, times the antenna gain in dBd."
|
||||
"explanation": "Radiated power referenced to a half-wave dipole. In linear units, multiply power delivered to the antenna by antenna gain relative to a dipole; in decibel units, subtract feed losses and add gain in dBd."
|
||||
},
|
||||
{
|
||||
"code": "EIRP",
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "äquivalente isotrope Strahlungsleistung — equivalent isotropically radiated power.",
|
||||
"exam": true,
|
||||
"explanation": "Like ERP but referenced to an isotropic radiator; EIRP = ERP + 2.15 dB."
|
||||
"explanation": "Radiated power referenced to an ideal isotropic radiator. For the same station, EIRP is 1.64 times ERP; when both powers are expressed logarithmically, EIRP is 2.15 dB higher."
|
||||
},
|
||||
{
|
||||
"code": "PEP",
|
||||
@@ -445,26 +457,29 @@
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "Güte — quality factor of a resonant circuit.",
|
||||
"exam": true,
|
||||
"explanation": "Ratio of stored to dissipated energy; a higher Q means a narrower, sharper resonance."
|
||||
"explanation": "At resonance, Q = 2π times the energy stored divided by the energy lost per cycle; equivalently it is approximately resonant frequency divided by bandwidth. Higher Q gives a narrower response but also a smaller usable bandwidth."
|
||||
},
|
||||
{
|
||||
"code": "SNR",
|
||||
"category": "Messgröße & Leistung",
|
||||
"meaning": "Signal-Rausch-Verhältnis — signal-to-noise ratio.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Compares wanted signal power with noise power, normally in dB and for a stated bandwidth. Narrowing the receiver bandwidth reduces admitted noise and can improve readability even though the received carrier power is unchanged."
|
||||
},
|
||||
|
||||
{
|
||||
"code": "AC",
|
||||
"category": "Strom & Spannung",
|
||||
"meaning": "Wechselstrom / Wechselspannung — alternating current.",
|
||||
"exam": true
|
||||
"meaning": "Alternating Current — Wechselstrom; also used for alternating voltage.",
|
||||
"exam": true,
|
||||
"explanation": "Current whose direction changes periodically. Station mains supplies are AC; power supplies rectify and smooth it to provide the DC required by most transceivers."
|
||||
},
|
||||
{
|
||||
"code": "DC",
|
||||
"category": "Strom & Spannung",
|
||||
"meaning": "Gleichstrom / Gleichspannung — direct current.",
|
||||
"exam": true
|
||||
"meaning": "Direct Current — Gleichstrom; also used for direct voltage.",
|
||||
"exam": true,
|
||||
"explanation": "Current with constant polarity. Most mobile amateur equipment is designed for a nominal 13.8 V DC supply, where correct polarity and adequate cable size are essential."
|
||||
},
|
||||
|
||||
{
|
||||
@@ -472,21 +487,21 @@
|
||||
"category": "Ausbreitung",
|
||||
"meaning": "höchste brauchbare Frequenz — maximum usable frequency.",
|
||||
"exam": true,
|
||||
"explanation": "The highest frequency the ionosphere still reflects back for a given path; above it the signal escapes into space."
|
||||
"explanation": "The highest frequency that is refracted back to Earth for a specified path and time. Above it, that ionospheric path normally fails; the value changes with distance, solar conditions, season and time of day."
|
||||
},
|
||||
{
|
||||
"code": "LUF",
|
||||
"category": "Ausbreitung",
|
||||
"meaning": "niedrigste brauchbare Frequenz — lowest usable frequency.",
|
||||
"exam": true,
|
||||
"explanation": "Below it absorption (mainly in the D layer) makes the path unusable."
|
||||
"explanation": "The lowest frequency that still provides the required signal quality on a specified path. Below it, ionospheric absorption and atmospheric noise usually dominate; it is an operational limit, not a fixed band boundary."
|
||||
},
|
||||
{
|
||||
"code": "NVIS",
|
||||
"category": "Ausbreitung",
|
||||
"meaning": "Steilstrahlung — near-vertical incidence skywave.",
|
||||
"exam": true,
|
||||
"explanation": "Near-vertical HF radiation that comes back down over a wide area, giving gap-free regional coverage out to a few hundred km."
|
||||
"explanation": "Uses high-angle HF radiation refracted back to Earth for regional communication, typically on 160, 80, 60 or 40 m depending on conditions. It can cover the skip zone of lower-angle paths, but coverage is never guaranteed."
|
||||
},
|
||||
|
||||
{
|
||||
@@ -494,74 +509,84 @@
|
||||
"category": "Digital & Daten",
|
||||
"meaning": "Digital Mobile Radio — digitale Sprach- und Datenbetriebsart.",
|
||||
"exam": true,
|
||||
"explanation": "TDMA digital voice standard widely used on amateur repeaters and networks."
|
||||
"explanation": "An ETSI digital mobile-radio standard using two-slot TDMA on a 12.5 kHz carrier. Amateur Tier II operation is common on repeaters and hotspots; talkgroups and timeslots must be selected correctly."
|
||||
},
|
||||
{
|
||||
"code": "D-STAR",
|
||||
"category": "Digital & Daten",
|
||||
"meaning": "digitale Sprach- und Datenbetriebsart (ICOM/JARL).",
|
||||
"exam": true
|
||||
"meaning": "Digital Smart Technologies for Amateur Radio — digital voice and data system.",
|
||||
"exam": true,
|
||||
"explanation": "An open amateur-radio specification developed through JARL. Callsign routing and reflector linking are common in practice; Icom is a major equipment vendor, not part of the abbreviation."
|
||||
},
|
||||
{
|
||||
"code": "C4FM",
|
||||
"category": "Digital & Daten",
|
||||
"meaning": "Continuous 4-level FM — Yaesu System Fusion digital voice.",
|
||||
"exam": true
|
||||
"meaning": "Continuous 4-Level Frequency Modulation — four-level digital modulation.",
|
||||
"exam": true,
|
||||
"explanation": "Maps dibits to four frequency deviations. Yaesu System Fusion uses C4FM, but the modulation name and the complete network/system are not synonymous."
|
||||
},
|
||||
{
|
||||
"code": "APRS",
|
||||
"category": "Digital & Daten",
|
||||
"meaning": "Automatic Packet Reporting System — Positions- und Telemetriedaten über Packet.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Shares position, status, telemetry, messages and objects, commonly as 1200-baud packet on 144.800 MHz in Europe. Use an appropriate path and beacon rate to avoid unnecessary channel congestion."
|
||||
},
|
||||
{
|
||||
"code": "TNC",
|
||||
"category": "Digital & Daten",
|
||||
"meaning": "Terminal Node Controller — Packet-Radio-Modem.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Interfaces a computer or terminal with a radio and handles packet framing and modulation. Modern APRS stations often implement the same functions in software."
|
||||
},
|
||||
{
|
||||
"code": "AX.25",
|
||||
"category": "Digital & Daten",
|
||||
"meaning": "Amateurfunk-Datenprotokoll — amateur packet-radio link protocol.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Amateur adaptation of X.25 used for packet radio and APRS frames. It carries amateur callsigns in the link-layer addresses and can support connected or unconnected operation."
|
||||
},
|
||||
{
|
||||
"code": "CTCSS",
|
||||
"category": "Digital & Daten",
|
||||
"category": "Signalisierung & Bedienung",
|
||||
"meaning": "Continuous Tone-Coded Squelch System — subaudible tone squelch.",
|
||||
"exam": false,
|
||||
"explanation": "A low-frequency tone below the audio that opens a repeater's or receiver's squelch only for stations sending the matching tone."
|
||||
"explanation": "A continuous low audio-frequency tone, typically 67 to 254 Hz, that allows a repeater or receiver to open only for the selected tone. 'Subaudible' means it is normally filtered from the loudspeaker audio, not that it lies below the human hearing range."
|
||||
},
|
||||
{
|
||||
"code": "DTMF",
|
||||
"category": "Digital & Daten",
|
||||
"category": "Signalisierung & Bedienung",
|
||||
"meaning": "Dual-Tone Multi-Frequency — Mehrfrequenzwahlverfahren.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Each key sends one tone from a low group and one from a high group. Amateur stations use it mainly for repeater control, link commands and legacy telephone interconnects."
|
||||
},
|
||||
{
|
||||
"code": "DCS",
|
||||
"category": "Digital & Daten",
|
||||
"category": "Signalisierung & Bedienung",
|
||||
"meaning": "Digital-Coded Squelch — digitale Rauschsperrencodierung.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Sends a continuous low-rate digital code with analogue FM so a receiver opens only for the selected code. It serves a similar access-filtering purpose to CTCSS but is not a digital voice mode."
|
||||
},
|
||||
{
|
||||
"code": "PTT",
|
||||
"category": "Digital & Daten",
|
||||
"category": "Baugruppe & Bedienung",
|
||||
"meaning": "Push To Talk — Sendetaste.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Keys the transmitter while pressed. Confirm that the frequency is clear before using it, and allow a brief pause after keying a repeater so the first syllable is not clipped."
|
||||
},
|
||||
{
|
||||
"code": "SDR",
|
||||
"category": "Digital & Daten",
|
||||
"meaning": "Software Defined Radio — softwaredefiniertes Funkgerät.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Implements functions such as mixing, filtering and demodulation in software or programmable logic. An SDR still needs suitable analogue RF filtering, gain control and conversion hardware."
|
||||
},
|
||||
{
|
||||
"code": "RIT",
|
||||
"category": "Digital & Daten",
|
||||
"category": "Baugruppe & Bedienung",
|
||||
"meaning": "Receiver Incremental Tuning — Empfänger-Feinverstimmung.",
|
||||
"exam": true
|
||||
"exam": true,
|
||||
"explanation": "Shifts only the receive frequency while leaving transmit frequency unchanged. It is useful when the other station is slightly off frequency; reset it afterward to avoid later tuning confusion."
|
||||
},
|
||||
|
||||
{
|
||||
@@ -610,7 +635,8 @@
|
||||
"code": "AFu",
|
||||
"category": "Organisation & Vorschrift",
|
||||
"meaning": "Amateurfunk — amateur radio.",
|
||||
"exam": false
|
||||
"exam": false,
|
||||
"explanation": "Common German written abbreviation in terms such as AFuG and AFuV. It is mainly documentation shorthand rather than something spoken during a contact."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -420,6 +420,50 @@ class TestAnkiBuild(unittest.TestCase):
|
||||
self.assertNotIn("af-explanation-low-confidence", back_high)
|
||||
self.assertNotIn("low confidence", back_high)
|
||||
|
||||
def test_cc_by_credit_gated_on_provenance_not_source_domain(self):
|
||||
"""The CC BY derivative-work credit must follow explicit
|
||||
provenance, not the citation domain: an explanation merely
|
||||
citing a 50ohm.de page is NOT a derivative work and must not
|
||||
carry the credit, while one marked as derived from a worked
|
||||
solution must."""
|
||||
derived = {
|
||||
"revision": 2,
|
||||
"explanation": "Worked-solution port.",
|
||||
"source": "https://50ohm.de/NEA_x.html#AD106",
|
||||
"confidence": 8,
|
||||
"provenance": aa.PROVENANCE_50OHM_SOLUTION,
|
||||
}
|
||||
cited_only = {
|
||||
"revision": 1,
|
||||
"explanation": "Independently written, just cites a study page.",
|
||||
"source": "https://50ohm.de/NEA_x.html#XX101",
|
||||
"confidence": 8,
|
||||
}
|
||||
derived_html = aa.render_explanation(derived)
|
||||
cited_html = aa.render_explanation(cited_only)
|
||||
self.assertIn("af-explanation-credit", derived_html)
|
||||
self.assertIn("CC BY 4.0", derived_html)
|
||||
self.assertIn("50ohm.de-Autorenteam", derived_html)
|
||||
self.assertNotIn("af-explanation-credit", cited_html)
|
||||
self.assertNotIn("translated", cited_html)
|
||||
|
||||
def test_root_deck_description_carries_attribution(self):
|
||||
self._build_all()
|
||||
apkg = self.out_dir / "amateurfunk-betriebliche-kenntnisse.apkg"
|
||||
db_path, _media, _names = extract_collection(apkg, self.root / "b")
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
decks = json.loads(conn.execute("select decks from col").fetchone()[0])
|
||||
finally:
|
||||
conn.close()
|
||||
descs = [d["desc"] for d in decks.values() if d["desc"]]
|
||||
self.assertEqual(len(descs), 1) # only the root deck carries it
|
||||
desc = descs[0]
|
||||
self.assertIn("CC BY 4.0", desc)
|
||||
self.assertIn("govdata.de/dl-de/by-2-0", desc)
|
||||
self.assertIn("50ohm.de-Autorenteam", desc)
|
||||
self.assertIn("Bundesnetzagentur", desc)
|
||||
|
||||
def test_missing_explanation_leaves_card_unchanged(self):
|
||||
self._build_all()
|
||||
apkg = self.out_dir / "amateurfunk-technische-kenntnisse-e.apkg"
|
||||
@@ -501,6 +545,48 @@ class TestAnkiBuild(unittest.TestCase):
|
||||
self._build_all()
|
||||
self.assertIn("author", str(ctx.exception))
|
||||
|
||||
def test_schema_accepts_valid_provenance(self):
|
||||
path = self.root / "exp_provenance.json"
|
||||
path.write_text(
|
||||
json.dumps({"NA101": {
|
||||
"revision": 1, "explanation": "ok", "source": "ok",
|
||||
"confidence": 5, "provenance": aa.PROVENANCE_50OHM_SOLUTION,
|
||||
}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loaded = aa.load_explanations(path)
|
||||
self.assertEqual(
|
||||
loaded["NA101"]["provenance"], aa.PROVENANCE_50OHM_SOLUTION,
|
||||
)
|
||||
|
||||
def test_schema_rejects_unknown_provenance_value(self):
|
||||
path = self.root / "exp_badprov.json"
|
||||
path.write_text(
|
||||
json.dumps({"NA101": {
|
||||
"revision": 1, "explanation": "ok", "source": "ok",
|
||||
"confidence": 5, "provenance": "made-up",
|
||||
}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaises(aa.AnkiBuildError):
|
||||
aa.load_explanations(path)
|
||||
|
||||
def test_schema_rejects_unhashable_provenance(self):
|
||||
# A malformed but valid-JSON provenance value (list/object) is
|
||||
# unhashable; the validator must surface AnkiBuildError, never a
|
||||
# raw TypeError from the set-membership test.
|
||||
for bad in ([], {}):
|
||||
path = self.root / "exp_unhashable.json"
|
||||
path.write_text(
|
||||
json.dumps({"NA101": {
|
||||
"revision": 1, "explanation": "ok", "source": "ok",
|
||||
"confidence": 5, "provenance": bad,
|
||||
}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaises(aa.AnkiBuildError):
|
||||
aa.load_explanations(path)
|
||||
|
||||
def test_invalid_explanation_schema_raises_build_error(self):
|
||||
self.explanations_path.write_text(
|
||||
json.dumps({
|
||||
@@ -577,6 +663,162 @@ class TestAnkiBuild(unittest.TestCase):
|
||||
self.assertFalse(all(byte_equal))
|
||||
|
||||
|
||||
class TestIntrinsicSvgSize(unittest.TestCase):
|
||||
"""Parsing of native figure dimensions — SVG only."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
|
||||
def _write(self, name, body):
|
||||
path = self.root / name
|
||||
path.write_text(body, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def test_svg_unitless_width_and_height(self):
|
||||
p = self._write("a.svg", '<svg width="115.5" height="120"><g/></svg>')
|
||||
self.assertEqual(aa.intrinsic_svg_size(p), (115.5, 120.0))
|
||||
|
||||
def test_svg_px_suffix_is_tolerated(self):
|
||||
p = self._write("a.svg", '<svg width="200px" height="80px"></svg>')
|
||||
self.assertEqual(aa.intrinsic_svg_size(p), (200.0, 80.0))
|
||||
|
||||
def test_svg_percentage_dimension_is_rejected(self):
|
||||
p = self._write("a.svg", '<svg width="100%" height="100%"></svg>')
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p))
|
||||
|
||||
def test_svg_missing_dimension_returns_none(self):
|
||||
p = self._write("a.svg", '<svg width="200"></svg>')
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p))
|
||||
|
||||
def test_svg_without_root_tag_returns_none(self):
|
||||
p = self._write("a.svg", "not an svg at all")
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p))
|
||||
|
||||
def test_svg_leading_decimal_is_accepted(self):
|
||||
p = self._write("a.svg", '<svg width=".5" height="10"></svg>')
|
||||
self.assertEqual(aa.intrinsic_svg_size(p), (0.5, 10.0))
|
||||
|
||||
def test_svg_malformed_number_returns_none_not_raises(self):
|
||||
# Permissive matching used to let "1.2.3" reach float() and abort
|
||||
# the build; now it falls back to None like any other bad value.
|
||||
for bad in ('1.2.3', '.', '1.', '1..2'):
|
||||
p = self._write("a.svg", f'<svg width="{bad}" height="10"></svg>')
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p), bad)
|
||||
|
||||
def test_non_svg_is_not_sized(self):
|
||||
# A PNG with a perfectly valid-looking name is still not sized:
|
||||
# we deliberately only enlarge SVGs.
|
||||
p = self.root / "a.png"
|
||||
p.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 64)
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p))
|
||||
|
||||
|
||||
class TestScaleForSize(unittest.TestCase):
|
||||
"""The clamped enlargement factor."""
|
||||
|
||||
def test_small_figure_gets_full_multiplier(self):
|
||||
self.assertEqual(aa.scale_for_size(115, 120), aa.MEDIA_SCALE)
|
||||
|
||||
def test_wide_figure_is_clamped_by_max_width(self):
|
||||
self.assertEqual(aa.scale_for_size(aa.MEDIA_MAX_WIDTH, 50), 1.0)
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(aa.MEDIA_MAX_WIDTH / 2, 50), 2.0
|
||||
)
|
||||
# Between half-cap and cap: enlarged only until width hits the cap.
|
||||
wide = aa.MEDIA_MAX_WIDTH * 0.75
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(wide, 50), aa.MEDIA_MAX_WIDTH / wide
|
||||
)
|
||||
|
||||
def test_tall_figure_is_clamped_by_max_height(self):
|
||||
self.assertEqual(aa.scale_for_size(50, aa.MEDIA_MAX_HEIGHT), 1.0)
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(50, aa.MEDIA_MAX_HEIGHT / 2), 2.0
|
||||
)
|
||||
tall = aa.MEDIA_MAX_HEIGHT * 0.75
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(50, tall), aa.MEDIA_MAX_HEIGHT / tall
|
||||
)
|
||||
|
||||
def test_large_catalog_schematic_grows_toward_cap(self):
|
||||
# NG205-like (635x460): with the 1024x768 cap it now enlarges,
|
||||
# bounded by width — up to 1024 px wide.
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(635, 460), aa.MEDIA_MAX_WIDTH / 635
|
||||
)
|
||||
|
||||
def test_never_shrinks_below_native(self):
|
||||
self.assertEqual(aa.scale_for_size(5000, 5000), 1.0)
|
||||
|
||||
def test_degenerate_size_is_native(self):
|
||||
self.assertEqual(aa.scale_for_size(0, 0), 1.0)
|
||||
|
||||
|
||||
class TestImageHtmlScaling(unittest.TestCase):
|
||||
"""End-to-end `<img>` emission and the per-path size cache."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.media_dir = Path(self.tmp.name) / "svgs"
|
||||
self.media_dir.mkdir()
|
||||
|
||||
def _write(self, name, body):
|
||||
(self.media_dir / name).write_text(body, encoding="utf-8")
|
||||
|
||||
def test_small_figure_emits_doubled_width(self):
|
||||
self._write("S_q.svg", '<svg width="115" height="120"></svg>')
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
self.assertIn('width="230"', media.image_html("S_q"))
|
||||
|
||||
def test_figure_at_cap_emits_no_width(self):
|
||||
# Exactly at the cap → scale 1.0 → native size, no width.
|
||||
self._write(
|
||||
"L_q.svg",
|
||||
f'<svg width="{aa.MEDIA_MAX_WIDTH}" '
|
||||
f'height="{aa.MEDIA_MAX_HEIGHT}"></svg>',
|
||||
)
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
html = media.image_html("L_q")
|
||||
self.assertIn('class="af-media"', html)
|
||||
self.assertNotIn("width=", html)
|
||||
|
||||
def test_png_is_not_scaled(self):
|
||||
# Only a PNG for this stem (no .svg sibling): emitted at native
|
||||
# size with no width, because we never size raster figures.
|
||||
(self.media_dir / "P_q.png").write_bytes(
|
||||
b"\x89PNG\r\n\x1a\n" + b"\x00" * 64
|
||||
)
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
html = media.image_html("P_q")
|
||||
self.assertIn('src="P_q.png"', html)
|
||||
self.assertNotIn("width=", html)
|
||||
|
||||
def test_unreadable_size_falls_back_to_no_width(self):
|
||||
self._write("U_q.svg", '<svg width="100%" height="100%"></svg>')
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
self.assertNotIn("width=", media.image_html("U_q"))
|
||||
|
||||
def test_malformed_number_falls_back_without_raising(self):
|
||||
self._write("M_q.svg", '<svg width="1.2.3" height="10"></svg>')
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
html = media.image_html("M_q") # must not raise
|
||||
self.assertIn('src="M_q.svg"', html)
|
||||
self.assertNotIn("width=", html)
|
||||
|
||||
def test_size_is_read_once_per_path(self):
|
||||
self._write("S_q.svg", '<svg width="115" height="120"></svg>')
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
with patch.object(
|
||||
aa, "intrinsic_svg_size", wraps=aa.intrinsic_svg_size
|
||||
) as spy:
|
||||
media.image_html("S_q")
|
||||
media.image_html("S_q")
|
||||
self.assertEqual(spy.call_count, 1)
|
||||
|
||||
|
||||
class TestRealExplanationsFile(unittest.TestCase):
|
||||
"""Validate the repo's actual explanations.json against the live catalog.
|
||||
|
||||
@@ -617,6 +859,34 @@ class TestRealExplanationsFile(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual([], offenders)
|
||||
|
||||
def test_safety_distance_block_uses_transmitter_power_not_erp(self):
|
||||
"""AK106-AK112 must feed the EIRP formula with transmitter power.
|
||||
|
||||
The relation is `P_EIRP = P_S · 10^((g_d+2.15-a)/10)` with `P_S`
|
||||
the transmitter (Sender) power; writing `P_EIRP = P_ERP · 10^…`
|
||||
double-counts the antenna gain (ERP already includes it). The
|
||||
upstream 50ohm solutions carry exactly this mislabel, so guard
|
||||
the whole block against it regressing back in.
|
||||
"""
|
||||
repo = Path(__file__).resolve().parent
|
||||
explanations_path = repo / "explanations.json"
|
||||
if not explanations_path.exists():
|
||||
self.skipTest("explanations.json not available")
|
||||
explanations = aa.load_explanations(explanations_path)
|
||||
block = [f"AK1{n:02d}" for n in range(6, 13)] # AK106..AK112
|
||||
for key in block:
|
||||
if key not in explanations:
|
||||
continue
|
||||
body = explanations[key]["explanation"]
|
||||
self.assertIn(
|
||||
"P_S", body,
|
||||
f"{key}: EIRP should be computed from transmitter power P_S",
|
||||
)
|
||||
self.assertNotIn(
|
||||
r"P_\mathrm{EIRP} = P_\mathrm{ERP}", body,
|
||||
f"{key}: EIRP must not be derived by scaling ERP by the gain",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user