SWiface-PHP is the server-side interface between the Open Glider Network (OGN) and Silent Wings Studio / SWviewer. It is a set of PHP endpoints (thin wrappers, run by Apache2) backed by Python scripts (all the real work) that:
- serve live and archived glider fixes to SWviewer using the
Silent Wings Tracking Protocol
(protocol version
1.3); - convert competition data from SoaringSpot, SGP / crosscountry.aero and Strepla into the JSON/task files that Silent Wings and the FAI glider-tracking front ends consume;
- provide competition support utilities — IGC file rebuilding from FLARM records (SAR), FAI Sporting Licence lookup, OGN-tracker/FLARM pairing, and results extraction.
It provides real-time scoring data to sgp.aero.
- How it fits together
- Requirements
- Silent Wings protocol endpoints
- Competition feed converters
- IGC / SAR utilities
- FAI and OGN utilities
- Shared Python modules
- Configuration
- Database
- Installation
- Directories and assets
- Operational notes
- Known rough edges
- Documentation
- Author and licence
OGN APRS feed ──► collector (separate repo) ──► MySQL / SQLite3 (OGNDATA, GLIDERS, …)
│
SoaringSpot API ─┐ │
SGP (crosscountry.aero) ─┼─► *2sws.py converters ──► cuc/*.json *.tsk *.csv
Strepla (strepla.de) ─────┘ │
▼
PHP endpoints (Apache2) ──► SWviewer / Silent Wings Studio
event.php, trackpoints.php,
eventgroups.php, gettrackerdata.php, …
Two independent data paths meet in this repo:
- Live tracking — fixes previously collected from OGN into the database are served as
Silent Wings JSON by
event.py/trackpoints.py/eventgroups.py/sgpgetfixes.py. - Competition metadata — pilots, gliders, tasks and turnpoints are pulled from the
scoring platforms by the
*2sws.pyconverters and written as.json(Silent Wings event),.tsk(task) and_filter.csv(glidertracker filter) files intocucFileLocation(cuc/by default), whereevent.pyandeventgroups.pypick them up.
The PHP layer never contains logic beyond argument sanitising: each endpoint passthru()s
/usr/bin/python3 <script>.py and echoes the output. The one exception is
gettrackerdata.php, which queries the database directly.
- Linux with Apache2,
mod_rewrite, and CGI enabled (historically a Raspberry Pi; the MySQL variant runs against a NAS/NFS-hosted server). - PHP with
mysqli/SQLite3andzlib(forgzipcompression of fix streams). - Python 3 with the packages in
requirements.txt— notablypandas,geopy,pycountry,beautifulsoup4,requests,httpx,ogn_client,MySQL-python(i.e.MySQLdb),airportsdata,suntime,termcolor,GitPython,playwright. - A MySQL/MariaDB or SQLite3 database populated by the OGN collector.
These implement the tracking protocol consumed by SWviewer. All are GET unless noted.
| Endpoint | Query parameters | Backed by | Returns |
|---|---|---|---|
getprotocolinfo.php |
username, cpassword, version |
— | {version}1.3{/version} plus server date/time |
getactivecontests.php |
— | — | Static contest descriptors (QSGP, LIVE) with site, lat/lon/alt |
getcontestinfo.php |
contestname, date, username, cpassword |
gencuc.py (for LIVE + today) |
Contest days, or the contents of cuc/<contest><date>.cuc |
gettrackerdata.php |
contestname, querytype=getintfixes, trackerid, starttime, endtime, compression |
direct SQL on OGNDATA |
CSV fixes flarmid,YYYYMMDDHHMMSS,lat,lon,alt,1, optionally gzipped |
getbannerinfo.php |
— | — | Banner image list |
event.php |
eventid |
event.py |
Silent Wings event JSON (turnpoints + tracks) for one event |
eventgroups.php |
— | eventgroups.py |
Event list built by scanning cuc/ for .cuc / .json files |
trackpoints.php |
trackid, begin |
trackpoints.py |
{"trackId":…, "live":…, "track":[{"t":ts,"e":lon,"n":lat,"a":alt}, …]} |
sgpgetfixes.php |
id, since, compression |
sgpgetfixes.py |
Fixes in the SGP scoring JSON format, optionally gzipped |
info.php |
— | — | phpinfo() (not tracked; see .gitignore) |
eventid is of the form <4-char initials><YYYY>MMDD…; the first four characters select the
event kind (LIVE for the live feed, otherwise a competition prefix such as QSGP), and
characters 7–12 carry the date used to query the fixes table.
Each converter has an HTML form, a PHP wrapper and a Python implementation.
| Source | Form | PHP | Python | Arguments |
|---|---|---|---|---|
SoaringSpot (api.soaringspot.com) |
soa2sws.html |
soa2sws.php (POST class, indexday) |
soa2sws.py |
indexday [class] |
SGP (crosscountry.aero) |
sgp2sws.html |
sgp2sws.php (POST compid, indexday) |
sgp2sws.py |
compid indexday [ip] [print] |
Strepla (strepla.de) |
str2sws.html |
str2sws.php (POST compid, indexday) |
str2sws.py |
compid indexday |
Index day is 0 for the first/most recent day, 1, 2, … for previous days;
sgp2sws.py also accepts today. sgp2sws.py 0 (or --list) prints the list of SGP
competitions instead of converting one.
Output files, written to config.cucFileLocation:
| File | Description |
|---|---|
<Initials><YYYYMMDD>[-<class>].json |
Silent Wings event: turnpoints + per-pilot tracks (pilot name, CN, country, aircraft, registration, 3D model, ribbon colours, portrait URL) |
<Initials><YYYYMMDD>[-<class>].tsk |
Task file; a <class>-latest.tsk symlink/copy is kept per class |
<class>_filter.csv / competitiongliders.csv |
ID,CALL,CN,TYPE,INDEX filter list for glidertracker.org |
competitiongliders.lst |
Flat list of the competing gliders |
SoaringSpot access is HMAC-signed: credentials come from config.clientid / config.secretkey,
or, when those are empty, from SoaringSpot/clientid and SoaringSpot/secretkey
(the SoaringSpot/ directory is git-ignored). SoaringSpot credentials are issued per
competition and are not valid for other competitions.
Related:
soa2pil.py— extract pilot information from SoaringSpot for FlyTool.ccucxtocuc.py— convert SeeYou.cucxfiles into the pseudo-.cucformat Silent Wings uses.gencuc.py— scan the database for gliders currently flying and build a live.cucfile (invoked bygetcontestinfo.phpfor theLIVEcontest).show_competition.py/sgp_api.py— SGP REST client;sgp_api.pydecodes the terse single-letter API keys into readable dicts (events, comp + pilots + days, day task, FAI ranking entry) and itsdecode_*functions are pure, so they can be tested against fixtures.gen_swsconfig.py— build a per-venueSWSconfig.<location>from an SGP competition (--list,--comp-id,--config-dir) and pointSWSconfig.iniat it via symlink.SSextractresults.py/SSextractresults.php/.html— scrape a SoaringSpot competition (-c <competition-slug>) and write the final results to CSV.
FLARM radio messages recorded inside other pilots' IGC files (LLXV FLARM… records) can be
used to reconstruct the track of a glider whose own logger file is missing — the basis of the
search-and-rescue tooling here.
| Script | Purpose |
|---|---|
SAR4comp.py (SAR4comp.php, SAR4comp.html) |
Front end for all three sources: -t SOA|SGP|DIR, -f FLARMID, -r registration, -g sgpid, -c clientid, -s secretkey, -i indexday, -w (web output). Rebuilds the flight and links to cunimb.net/igc2map.php |
soa2fil.py / soa2filfuncs.py |
Download every IGC file of a SoaringSpot day; soa2fil.py <indexday> [-e FLARMID] extracts the FLARM traces |
sgp2fil.py / sgp2filfuncs.py |
Same for an SGP competition: sgp2fil.py <compid> <indexday> [-e FLARMID] [print] |
dir2fil.py / dir2filfuncs.py |
Same over a local directory of IGC files (SARpath/IGCfiles/DIR/ → …/TMP/) |
genIGC.py |
Rebuild a valid IGC file from collected FLARM messages; filters implausible fixes using DISTHOME, DIFFALT, DIFFAVG |
buildigc.sh |
Shell wrapper: buildigc.sh <day> <flarmid> <name> — greps the FLARM records and pipes them through genIGC.py |
soa2fil.sh, sgp2fil.sh |
Batch a whole competition, one run per day index |
igc2geo.py |
Convert an .IGC file to JSON |
| Script | Purpose |
|---|---|
SearchFAISL.py (.php, .html) |
Search/validate a pilot's FAI Sporting Licence on the FAI extranet: -c IOC-country, -n surname, -f firstname, -s licence-number, -w |
validate_fai_sl.py |
The underlying FAI extranet client (licences per country, full licence details); uses config.FAIPWD |
iso2ioc.py |
ISO 3166-1 alpha-3 → IOC country-code mapping (mismatches only) |
pairtrk.py (pairtrk.php, pairtrkadd.html) |
Pair an OGN tracker with the FLARM on the same glider so the two appear as one device. Actions: list, add, update, with trk, flarmid, owner, deleteyn, active (table TRKDEVICES) |
pairsynch.php |
?synch=synch — replicate TRKDEVICES between servers with pt-table-sync |
ognddbfuncs.py |
OGN DDB lookups (registration, CN, aircraft type, FLARM id) with host failover |
ogntfuncs.py, flarmfuncs.py |
OGN-tracker ↔ FLARM pairing tables and GLIDERS lookups |
| Module | Contents |
|---|---|
config.py |
Generated — see Configuration |
parserfuncs.py |
OGN APRS parsing helpers, airport/sunrise data (airportsdata, suntime) |
geofuncs.py |
Geodesy: degrees↔DMS, geodesic distances, coordinate line conversion |
dtfuncs.py |
Timezone-aware / naive UTC helpers |
gistfuncs.py |
GitHub Gist publishing, plus obscure() / unobscure() (zlib + base64) used for stored passwords |
simplehal.py |
Minimal HAL+JSON document walker for the SoaringSpot API |
dummyfile.py |
fixcoding() — strip accents/non-ASCII from names |
web_scraper.py |
WebScraper — static (requests/BeautifulSoup) and dynamic scraping |
kpilot.py, kglider.py, ksta.py |
Local tables of known pilots, gliders to display, and OGN receiver stations |
Nothing is configured by editing Python. The single source of truth is an INI file,
$CONFIGDIR/SWSconfig.ini (default CONFIGDIR=/etc/local/):
python3 genconfig.py # reads SWSconfig.ini (+ configtail.txt) → config.py and config.phpgenconfig.py writes both config.py (imported by every Python script) and config.php
(included by the PHP that needs credentials), then chmod 740 / chown :www-data on them.
Passwords and the Gist token are stored obscured (gistfuncs.obscure) and read back with
unobscure(). Both generated files are git-ignored. configtail.template is a sample of the
configtail.txt tail (accent-stripping helper plus the fallback event/turnpoint/track
definitions used when no JSON file is found).
SWSconfig.ini keys, by section:
[server] — cucFileLocation, DBpath, SARpath, MySQL, DBhost, DBname,
DBtable, SQLite3, DBuser, DBpasswd, DBuserread, DBpasswdread, Initials,
SWSserver, TPTserver, DDBhost, DDBport, DDBurl1, DDBurl2, GIST, GIST_USER,
GIST_TOKEN, clientid, secretkey, OGNTRACKERS, DISTHOME, DIFFALT, DIFFAVG,
FAIPWD.
[location] — location_name, location_latitude, location_longitud, eventname1,
eventname2, eventdesc1, eventdesc2, PicPilots.
Only DBpath, MySQL, DBhost, DBuser, DBpasswd, DBname, SQLite3, Initials and
the [location] names/descriptions are mandatory; the rest have defaults.
Both back ends are supported and selected by config.MySQL:
- MySQL/MariaDB —
MySQLdb.connect(host=DBhost, user=DBuserread, passwd=…, db=DBname). - SQLite3 — the file
DBpath + SQLite3(e.g./nfs/OGN/SWdata/SWiface.db), opened read-only.
Tables read by this repo (populated by the OGN collector, not by these scripts):
| Table | Used for |
|---|---|
OGNDATA (name from DBtable) |
The fixes: idflarm, date (YYMMDD), time (HHMMSS), latitude, longitude, altitude |
GLIDERS |
idglider, registration, cn, type, flarmtype |
TRKDEVICES |
OGN-tracker ↔ FLARM pairing (id, flarmid, registration) |
CONTEST, CONTESTANT, PILOT, POINT, LOCATION |
Competition metadata mirrored from the scoring platform |
# 1. Get the code into the Apache document root
cd /var/www/html
git clone https://github.com/acasadoalonso/SWiface-PHP.git SWS
cd SWS
# 2. Python dependencies
pip install -r requirements.txt
# 3. Working directories
mkdir -p cuc cucfiles
sudo chown -R $USER:www-data . cuc cucfiles
# 4. Configuration
sudo vi /etc/local/SWSconfig.ini # see the Configuration section
cp configtail.template configtail.txt # adjust the fallback event/turnpoints if needed
python3 genconfig.py # → config.py and config.php
# 5. Competition data (pick the one that applies)
cp ../*.cucx cucfiles/ && (cd cucfiles && unzip '*.cucx')
python3 ccucxtocuc.py # SeeYou .cucx → .cuc
python3 soa2sws.py 0 # SoaringSpot → .json/.tsk
python3 sgp2sws.py <compid> today # SGP → .json/.tsk
# 6. Local pilot/glider tables, if you use them
vi kpilot.py kglider.py
sudo service apache2 restartFor SoaringSpot without credentials in the INI file, create SoaringSpot/clientid and
SoaringSpot/secretkey in the repo root (git-ignored).
The bundled .htaccess enables mod_rewrite (so event resolves to event.php), runs
.py as CGI, sets Access-Control-Allow-Origin: *, and requires HTTP Basic auth against
/etc/apache2/.htpasswd.
| Path | Contents |
|---|---|
cuc/ |
Generated .cuc / .json / .tsk / .csv per event and day, plus the LIVEhdr.txt / LIVEtail.txt / LIVEtail2.txt templates used to assemble live .cuc files |
cucfiles/ |
Original SeeYou .cucx files (git-ignored) |
doc/ |
Protocol and API documentation (see below) |
banners/, gif/ |
Logos and banners served to SWviewer |
tptextures/ |
Start/finish/turnpoint textures used in the 3-D rendering |
SoaringSpot/, LT24/, PilotImages/, pic/, utils/, files/ |
Site-local, git-ignored |
- Auto-deploy —
ogn.pull.phpis a GitHub push webhook: it runsgit pull origin master, fixes permissions and touchesUPDATED.by.GIT. - Vendored modules —
sscommit.shcopies the shared*funcs.pyandksta.pyfrom the author's/nfs/OGN/srctree into the repo, regeneratesrequirements.txtwithpipreqs, and pushes to both theoriginandglidernetremotes.sslink.shsymlinks them instead, for development on the source machine. - Quick check —
test.shfetches the SGP event list (data.crosscountry.aero/public/get/events). - CI —
.github/workflows/label.ymllabels pull requests by modified path.
Worth knowing before you deploy or change things:
getflarm.pyis imported bysoa2filfuncs.py,sgp2filfuncs.pyanddir2filfuncs.pybut is git-ignored and not in the repo; the IGC/FLARM extraction path will not run without it.gettrackerdata.phpcarries hard-coded database host/user/password and a hard-codedEurope/Madridtimezone, and its$mysqlflag is commented out — it therefore always takes the SQLite3 branch. It does not useconfig.php.getactivecontests.phpandgetbannerinfo.phpreturn static strings (a 2016 contest and fixed banner paths) rather than reading the configuration.config.bkup, a committed sample configuration that carried a GitHub token and obscured DB passwords, has been deleted — but it remains in the git history, so those credentials must be treated as exposed and rotated.config.pyis generated bygenconfig.py; do not commit it.soa2fil.pyreferencesFalrmIDin its 9-character FLARM-ID branch (typo forFlarmID), which raisesNameErrorforICAxxxxxx-form ids.requirements.txtis generated bypipreqsand listsGitPythonandpyOpenSSLtwice with different versions.setup.pyis a one-line placeholder; the project is not installable as a package.
In doc/:
| File | Contents |
|---|---|
README.SWS.PHP.INTERFACE, INSTALL.SWS.PHP.INTERFACE |
The original interface notes and install steps |
SWS interface for real time scoring.pdf |
Real-time scoring interface specification |
SGP API.txt, sgpcall.txt, sgp_json_specs.txt, sgpoutput.txt |
SGP REST API notes, sample calls and payloads |
Ranking list REST api v.0.23.pdf |
FAI ranking-list REST API |
LECD.doc.json |
Sample event document (La Cerdanya) |
External references:
- Silent Wings Tracking Protocol and the Silent Wings wiki
- SGP — Sailplane Grand Prix · crosscountry.aero
- SoaringSpot · Open Glider Network
Angel Casado — acasado (at) acm.org.
Licensed under the Apache License, Version 2.0 — see LICENSE. Releases up to
and including commit 29fea2c were distributed under the GNU GPL v2.
Bug reports and questions: please open an issue on GitHub.