Repository navigation
Releases: vakaobr/atalaia-project
Release list
v0.7.0 (pre-alpha)
Pre-alpha. Not field-tested. This is a pre-release for builders and testers. The software is
tested in simulation and on a bench with real Heltec V4.3 boards, not in a real game. Do not rely on it
for a game that matters, and expect things to change.
Which file? Field Box (also the Satellite), USB radio modem and I/O pod are separate images (atalaia-field-box-,
atalaia-modem-, atalaia-io-pod-). Each comes as -usb.zip (flash it with esptool over USB, a README inside says
how) and -merged.bin (one file for a browser flasher, written at address 0x0). The Field Box also has the plain
.bin with its signed .release.json, what a controller or a box checks for an update over the air (the modem and
the pod are not updated that way). Check what you downloaded against SHA256SUMS.
USB flash required once when you come from v0.4.0 or earlier: the Field Box, modem and pod images of this release have a new partition table (4 MB OTA slots instead of 2 MB), and an update over the air cannot change the partition table. Flash the -usb.zip over USB once (write, no erase: the settings and the adoption stay; the cached game modes and the field's logo are fetched again). A box still on the old table refuses to run the new firmware and shows "Flash over USB" on its screen; one that gets it over the air from an older firmware goes back to the old one by itself. Later updates over the air work as before.
Signed with a CI release key. The firmware trusts the project's release keys: root 424d053af5cdbc35
(held offline by the maintainer), CI 32b3c815ac30ad43 and the next CI key a4f6002b0122b131, which
takes over from it. The .release.json names the key that signed this release (key_id). A controller
whose [firmware] release_keys lists keys by hand needs the next CI key,
76eY5BoMQ4RHohhX/8iSUlIUA5o5Entfk7EFBUxpbSY=, before the releases it signs (Release signing).
Guides: Firmware guide (download, flash, build it yourself) ·
Release signing ·
Controller setup ·
Set up the radio modem
What is in this release
Features
- controller: box modules, the capability start check and the GM's outputs on both controllers and the console (#47) (a652cf2)
- core: scenario triggers switch the boxes' outputs; Triggers blocks in the console (#49) (6010dbe)
- firmware: an optional passive piezo buzzer; the speaker plays the game's sounds again (#40) (c234ca6)
- firmware: the simple output and the box's output layer (#45) (39e08d6)
- modes: Bomb's smoke and siren, the master's siren, and a role's outputs (#50) (a14a5e8)
- protocol: the expansion port's protocol, vectors and capability type ids (#41) (3f30ef5)
- release: trust the next CI release key a4f6002b0122b131, sign with it behind a switch (#38) (ee56036)
Bug Fixes
- ci: link box_host's mbedtls and mbedx509 statically for the app job (#48) (bba5f69)
- console: forget a saved access code the controller now refuses (#52) (1741a21)
- preview: the live demo starts again and shows the players' positions (#43) (9030fe6)
Documentation
- phase 5 notes the HTTPS host-box fetch and the registry keys as done (#34) (dee8722)
- point the guides to v0.6.1 and describe the release gate as configured (#31) (8e3ded9)
- RFC draft for the expansion port and the single-GPIO simple output (target addon, relays, sirens) (#36) (c0962a9)
- the expansion port's decisions EXP-1 to EXP-38 in the decisions log (#42) (c8a3637)
- the maintainer's review of the open P5F/P5J rows, httpx2 and the release key rotation (#35) (16abd9f)
Details
- Scenario triggers switch the boxes' outputs (increment 5 of the draft RFC "The expansion port") (decisions EXPI5-1 to EXPI5-15, following EXP-25, EXP-29 and EXP-30;
core/scenario/manifest.lua,validator.lua,luagen.lua,runtime.lua,controller/atalaia_controller/engine.py,app/lib/controller/controller_engine.dart,controller/console/src/components/scenario-blocks.js, the console's four languages,protocol/vectors/scenario.json,protocol/vectors/scenario_compile.json,tools/scenario_compile_vectors.py): a scenario manifest's new optionaltriggers:list switches an output of a box when every objective of a list is completed (completed), the first of them is (any_completed), one fails (failed), or the game starts or ends (game: start,game: end):output: { node, capability, act, seconds }with the acton,pulse,pattern(withon_msandoff_ms, 500-10000) oroff, the box one of the scenario'snodes, the functionsiren,strobe,smoke,light,horn(siren.2),out.1-out.9or*withoff, and 1-600 s (10 s at most forgame: end, the box's end grace). The validator says what is wrong in the style of the other rules (a bareonoroff, which YAML 1.1 reads as a boolean, asks for quotes); the compiler writesSCENARIO.triggersintofield.luaonly when there are triggers; the runtime fires each trigger once a game as CMD 12 withsrc: field(the same path asfield.send(node, "output", ...)and the controller's one start a box every 2 s), tries again every second for 10 s what that limit holds back, drops what waits on a pause and does not sound the start again after a restart mid-game. Both controllers now hold back the outputs a field script switches after its owngame.end(an objective that wins the game) until the end's CMD has gone, so they come within the boxes' 10 s grace instead of being switched off by the end. The GM console's scenario blocks gain a Triggers category ("when" blocks holding an output block) in pt, en, de and zh-TW, with the YAML round trip (what the blocks cannot hold as it is stays in the YAML view). Tests: the shared vectors (C core, Python and the phone's Dart), a scenario game through the Pi's engine (controller/tests/test_scenario.py), the phone's end-of-game hold (app/test/controller/controller_engine_test.dart) and the blocks in Chromium. Not tried on hardware. - The GM console forgets a saved code the controller now refuses (
controller/console/src/app.js): reopened with a code kept in the browser that has since been revoked or has expired, the console said "Wrong access code" and kept the code, sending it again at every start. It now forgets it and says the code is no longer accepted, as when a live stream is closed for it; a code just typed and refused still says it is wrong, and a controller that does not answer keeps the code. Service worker cache v19. - CI's app job runs the host box again (
.github/workflows/ci.yml,tools/docker_app.py): since the host box's HTTPS mode fetch (#32)box_hostalso links libmbedtls and libmbedx509, but the app job linked only mbedcrypto statically, so in the Flutter image (which has no mbedtls) the host box did not start and every phone-controller test that uses it timed out; main's CI app job had failed on every commit since. All three are now linked statically. - The live demo starts again (
server/preview/demo/Dockerfile): sincebox_hostfetches modes over HTTPS (#32) it links libmbedtls and libmbedx509, which the demo image's runtime stage did not install, so its simulated boxes did not start and the demo restarted i...
v0.6.1 (pre-alpha)
Pre-alpha. Not field-tested. This is a pre-release for builders and testers. The software is
tested in simulation and on a bench with real Heltec V4.3 boards, not in a real game. Do not rely on it
for a game that matters, and expect things to change.
Which file? Field Box (also the Satellite), USB radio modem and I/O pod are separate images (atalaia-field-box-,
atalaia-modem-, atalaia-io-pod-). Each comes as -usb.zip (flash it with esptool over USB, a README inside says
how) and -merged.bin (one file for a browser flasher, written at address 0x0). The Field Box also has the plain
.bin with its signed .release.json, what a controller or a box checks for an update over the air (the modem and
the pod are not updated that way). Check what you downloaded against SHA256SUMS.
USB flash required once when you come from v0.4.0 or earlier: the Field Box, modem and pod images of this release have a new partition table (4 MB OTA slots instead of 2 MB), and an update over the air cannot change the partition table. Flash the -usb.zip over USB once (write, no erase: the settings and the adoption stay; the cached game modes and the field's logo are fetched again). A box still on the old table refuses to run the new firmware and shows "Flash over USB" on its screen; one that gets it over the air from an older firmware goes back to the old one by itself. Later updates over the air work as before.
Signed with two release keys: root 424d053af5cdbc35 (held offline by the maintainer) and CI
32b3c815ac30ad43 (this release is signed with the CI key; both are built into the firmware).
Guides: Firmware guide (download, flash, build it yourself) ·
Release signing ·
Controller setup ·
Set up the radio modem
What is in this release
Bug Fixes
- controller: a scan ending as the region changes or the controller stops no longer writes after it (#29) (4813977)
- controller: close a dropped stream's socket and wake every live-feed client; test the pinned lock in CI (#28) (6ecf830)
- release: the I/O pod's package passes the boot-splash check (#26) (d82d481)
Details
- The field controller's tests run on the hash-pinned lock too, in a new CI job (decisions PLK-1 to PLK-6;
.github/workflows/ci.ymljobcontroller-pinned,controller/tests/test_scenario_api.py,controller/atalaia_controller/ws_client.py,controller/tests/test_remote_gm.py,controller/atalaia_controller/ws.py,controller/tests/test_api.py,controller/README.md,controller/requirements.txtheader): the controller installs from Ubuntu's packages (FastAPI 0.101, pydantic 1.10) or with pip fromcontroller/requirements.txt(FastAPI 0.143, Starlette 1.8, pydantic 2.14, httpx 0.28; decision D15), but its tests had only ever run on the first. On the lock, 1 of 1132 tests failed:test_check_reads_surrogate_pairs_and_refuses_half_of_onelet the test client encode a lone surrogate, and httpx 0.28 writes JSON as UTF-8 itself and refuses one before the request leaves, so the controller was never reached. The test now makes the request bodies itself and checks the controller's answer (bad_manifest) to half of a surrogate pair sent three ways a client can: as an escape inside the manifest, as JSON's own\udf09escape, and as the raw bytes ED BC 89 (not UTF-8; Python's JSON reader takes them as the surrogate on both stacks). No bug specific to the newer stack was found, and it shows no deprecation warning from FastAPI or pydantic; Starlette 1.x asks forhttpx2in its test client (left: the lock keeps httpx). AResourceWarningseen on both stacks was a real leak, fixed: the remote GM's WebSocket client (controller/atalaia_controller/ws_client.py) marked a link that was already down (the site server closed it, a send or a receive failed) as closed, andclose()then returned without closing the socket, so every reconnect after such a drop left one socket to the garbage collector; it now always closes the socket (still sending the close frame only on a live link), with a test, and the tests' fake site server closes its accepted sockets too. One run on the lock hung intest_map_features' stream test, a race in the live feed's hub (controller/atalaia_controller/ws.py) on both stacks: it filled every client's queue on the event loop of the latest connection, but FastAPI's TestClient runs each WebSocket on a loop of its own, and anasyncio.Queuefilled from another loop's thread can leave its reader asleep for good; the hub now fills each queue on its own loop (one loop on a real server, so nothing changes there), with a test that failed before (HubLoopTests), and the new job stops after 30 minutes. The new job (ubuntu-24.04,actions/setup-pythonpinned by SHA, Python 3.12,pip install --require-hashes --no-deps --only-binary :all:, the core and the host modem, box and pod built as in thecontrollerjob, read-only permissions) runs the same tests with everyDeprecationWarningan error. - The I/O pod's release package no longer fails the boot-splash check (
tools/release_package.py): the check (LOGO-7) wanted exactly onesplash_logo.cin every build, but the I/O pod has no screen and its build embeds none, so the v0.6.0 release run stopped on the io-pod package and published nothing. A build withCONFIG_ATALAIA_POD_MODE=ynow passes when it embeds no splash, and is refused if it does. - A noise scan that ends as the field's region changes or the controller stops no longer writes after it (
controller/atalaia_controller/engine.py): a scan reads "done" a moment before it reports, and in that moment the region could change (the scan no longer counted as running) and drop it without waiting for its thread. Its late report then saved the old region's result as the last scan, sent an EU868 modem the new region's channel, and afterstop()wrote to the closed database (scan listener failed ... Cannot operate on a closed database). Only the current scan's reports count now (a new scan likewise ignores the one before), andstop()waits for a dropped scan's thread too.
v0.5.0 (pre-alpha)
Pre-alpha. Not field-tested. This is a pre-release for builders and testers. The software is
tested in simulation and on a bench with real Heltec V4.3 boards, not in a real game. Do not rely on it
for a game that matters, and expect things to change.
Which file? Field Box (also the Satellite), USB radio modem and I/O pod are separate images (atalaia-field-box-,
atalaia-modem-, atalaia-io-pod-). Each comes as -usb.zip (flash it with esptool over USB, a README inside says
how) and -merged.bin (one file for a browser flasher, written at address 0x0). The Field Box also has the plain
.bin with its signed .release.json, what a controller or a box checks for an update over the air (the modem and
the pod are not updated that way). Check what you downloaded against SHA256SUMS.
USB flash required once when you come from v0.4.0 or earlier: the Field Box, modem and pod images of this release have a new partition table (4 MB OTA slots instead of 2 MB), and an update over the air cannot change the partition table. Flash the -usb.zip over USB once (write, no erase: the settings and the adoption stay; the cached game modes and the field's logo are fetched again). A box still on the old table refuses to run the new firmware and shows "Flash over USB" on its screen; one that gets it over the air from an older firmware goes back to the old one by itself. Later updates over the air work as before.
Signed with two release keys: root 424d053af5cdbc35 (held offline by the maintainer) and CI
32b3c815ac30ad43 (this release is signed with the CI key; both are built into the firmware).
Guides: Firmware guide (download, flash, build it yourself) ·
Release signing ·
Controller setup
What is in this release
A pre-alpha, not field-tested pre-release. USB flash required once: the Field Box (and the Satellite), the modem and the I/O pod get a new partition table with 4 MB app slots, which an update over the air cannot change; flash this release over USB once, without erasing, and the box keeps its adoption, keys and settings (firmware guide, section 3.3). Also new: the Sabotage game mode, box penalties and a projected score, I/O pod settings over the radio, data retention per field, the published privacy notice with consent asked again, chrono sharing across a hosted hub's fields, a database role per field in hosted mode, and a read-only live demo.
Changed
- The app: a marshal's phone no longer keeps the field join key, CA-pinned scoreboard links are remembered, and buzzes reach a closed app on Android (decisions APX-1 to APX-3;
app/). (1) The join key stays only where it is used, by a prop (its join card) and by the phone controller;AppSettings.joinKeyUserecords which, a settings migration (version 2) clears it on an upgrade for a phone that is a marshal and never ran the controller, and confirming a marshal clears it unless the controller's. (2) A scoreboard link with&ca=is kept per field by the CA's fingerprint (scoreboard_links, read code sealed) and offered under Campos guardados; it is forgotten when the CA changes. (3) While the player view is open on a live game, an Android foreground service with an ongoing notification (four languages) keeps the app's engine running, so the same event buzzes play with the app closed or the screen off; it stops when the game ends or the player leaves; no Play services; iOS is not covered. Manuals (player, marshal, en and pt) andapp/README.mdupdated.
Added
- Site server, hosted mode: a database role per field, so a SQL injection can no longer switch fields by setting
atalaia.tenant(decision S21-13, decisions S2113-1 to S2113-8;tenant_roles.py,tenancy.py,hosted.py, migration0023_tenant_roles,hosted-roles.sh,restore.sh,server/README.md,docs/DEPLOY.md, ADR-P4-4's amendment of 2026-10-28): the row-level security keyed on a setting the server's own role could set. Now every field has a database roleatalaia_t_<field id>(no login, no superuser, noBYPASSRLS, owns nothing) that alone holds the grants on the field's schema, tables and sequences and on its rows of the hub's sharing tables; the server's login role holds none (it reads the registry, adds to the quota inbox andSET ROLEs to a field's role,WITH INHERIT FALSE), and every request's transaction starts withSET LOCAL ROLEto its field's. A statement of field A that setsatalaia.tenantto B is refused B's schema outright, for reading and writing.pg_catalog.set_configis revoked fromPUBLIC(hosted-roles.sh, as the superuser) so a field's role cannot switch role or setting from inside a statement; the server refuses to start while any role can call it (andasyncpg's type introspection is told not to). The hub's chrono sharing keeps working across fields: its policy and functions read the caller's field from the role in use (atalaia_caller_tenant()), not from the setting.hosted create/migratemake and grant the roles (the owner role now needsCREATEROLE;hosted-roles.shgives it), migration0023_tenant_rolesmakes the role of every field a hub already has (and does nothing in a single-field server),hosted deletedrops it,hosted checkverifies the roles, the grants and theset_configrestriction, and a hub's restore uses--no-aclandhosted migratefor the grants (roles belong to the cluster, not to the backup). Upgrading a hub: runhosted-roles.sh, then the hub'shosted migrate, in one maintenance window (DEPLOY.md). Not closed, and said so: a second statement that is aSET ROLEcommand would still switch fields (PostgreSQL authorises it by the session user); the server's driver sends one statement per call, and a login role and pool per field would close it for good. Single-field mode is unchanged. New tests intest_tenants.py,test_hub_share.pyandtest_migrations.py. - The privacy notice is published, and accounts are asked to consent again when it changes (decisions CONS-1 to CONS-6, S17-16;
server/atalaia_server/privacy_notice.py,api/privacy.py,web/privacy_pages.py,static/staff/consent.js): the operator puts one Markdown file per language and version in a folder (ATALAIA_PRIVACY_NOTICE_DIR,server/privacy-notice/in the Compose deployment, filesprivacy-notice-<lang>-<version>.md, filled in fromdocs/templates/privacy-notice-*.md) and the server serves the files of the currentATALAIA_PRIVACY_NOTICE_VERSIONas a public page (/privacy, pt-PT, English, German, Traditional Chinese, the text rendered by a small escaping Markdown renderer) and as JSON (GET /api/v1/privacy-notice?lang=:version,lang,available,markdown,html,url; another language of the version when the one asked for has no file, 404privacy_notice_not_publishedwhen there is none).GET /api/v1/megainsconsent(statuscurrent,outdatedormissing, the version given, the current version, whether a notice is published);POST /api/v1/me/consents/accountwith{notice_version}consents again (the live consent for the older version ends, a new one is recorded,users.gdpr_consent_atmoves,consent_renewedis audited) and answers 409notice_version_changedwithcurrent_versionwhen the version shown is not the current one;POST /api/v1/auth/registertakes an optionalnotice_versionand answers the same 409. The staff pages show a panel at the top after login (read the notice, tick, confirm) in the four languages; nothing is blocked, so the account's own export and erasure stay available. The text of the notice stays the operator's: this is only the mechanism. Tests:tests.test_privacy_notice.
Removed
users.data_retention_days(decision CONS-3; migration0020_drop_user_retention): an unused per-account override that was only exported and set to null on erasure. GDPR asks for a defined period per kind of data, which the daily purge applies; a field per account promised what the code did not do. The access answer and the export'sprofile.jsonno longer have it.
Added
- A box reports the points its teams lost to penalties, so the totals subtract them (decision BSC-1, closing E6; RFC 0001 Amendment 18; core
msg_codec,firmware/esp32/main/box_game.c, both controllers,app/lib/radio/rf_codec.dart,protocol/schema/messages.yaml,protocol/vectors/state.json). STATE gets an optional key 9,lost: a per-team map of the points lost to penalties on that node, a grow-only counter merged by maximum like the gains and sent only when a team lost something (a game with no penalty sends the same STATE as before). The controller's total per team is the gains minus the boxes' penalties plus the GM's adjustments, so Kill Confirmed's penalty now lowers the team's total on the controller, the console, the scoreboards, MQTT and the app (the station's own screen already showed it net). The node'slostis inGET /api/v1/nodesand kept across a controller restart. An older box sends no key (nothing changes for it); an older Pi controller skips it; an older Android controller refuses a STATE that has it: update the app first. The Android prop link does not carry it yet. - A projected score between STATEs, for modes that opt in (decisions SL-6 and BSC-2;
mode.yamlscoring,simulator/atalaia_sim/mode.py, both controllers;modes/README.md"Projected score"). A mode that scores by holding says how (scoring: {hold: {every: tick_s, points: points_per_tick}}, in Domination) and the controller adds the ticks a held box has made since its last STATE to the total it shows:projectedonGET /api/v1/games/{id}/scoreand the stream's `sc...
v0.4.0 (pre-alpha)
Pre-alpha. Not field-tested. This is a pre-release for builders and testers. The software is
tested in simulation and on a bench with real Heltec V4.3 boards, not in a real game. Do not rely on it
for a game that matters, and expect things to change.
Which file? Field Box (also the Satellite), USB radio modem and I/O pod are separate images (atalaia-field-box-,
atalaia-modem-, atalaia-io-pod-). Each comes as -usb.zip (flash it with esptool over USB, a README inside says
how) and -merged.bin (one file for a browser flasher, written at address 0x0). The Field Box also has the plain
.bin with its signed .release.json, what a controller or a box checks for an update over the air (the modem and
the pod are not updated that way). Check what you downloaded against SHA256SUMS.
Signed with two release keys: root 424d053af5cdbc35 (held offline by the maintainer) and CI
32b3c815ac30ad43 (this release is signed with the CI key; both are built into the firmware).
Guides: Firmware guide (download, flash, build it yourself) ·
Release signing ·
Controller setup
What is in this release
The first release published on GitHub, a pre-alpha, not field-tested pre-release: firmware images for the Field Box (and the Satellite, the same image), the controller's USB radio modem and the I/O pod, as a USB flash bundle, a single merged image for browser flashers, and the app image with its signed .release.json, built and signed by the release workflow (.github/workflows/release.yml) with the CI release key. Building the firmware yourself still works and is still documented (docs/manuals/en/firmware.md).
Added
- Firmware releases on GitHub, signed with two release keys (the maintainer's decision of 2026-10-28, decisions REL-1 to REL-8): a pushed tag
v*builds the Field Box, modem and pod images with the project's two mode keys and two release keys as defaults and no bench options, and creates a GitHub pre-release with, for each image,atalaia-<variant>-<version>-usb.zip(bootloader, partition table, app,flash_args, README),atalaia-<variant>-<version>-merged.bin(for browser flashers, at offset 0),atalaia-<variant>-<version>.binwithatalaia-<variant>-<version>.bin.release.json(what a controller or a box checks,tools/firmware_release.py), andSHA256SUMS. The root release key (424d053af5cdbc35) is held offline by the maintainer; the CI release key (32b3c815ac30ad43) is a repository secret and signs the releases. Both are the defaults ofCONFIG_ATALAIA_OTA_RELEASE_KEYS(andtools/docker_fw.py --release-keys), the Pi controller's and the phone controller's[firmware] release_keys.firmware/esp32/docs/RELEASE_SIGNING.mdhas what to do if the CI key leaks;docs/community/keys.mdlists the keys. - The hardware build manuals are brought up to date, with a firmware guide for builders and a controller setup guide (documentation only; English, Portuguese follows):
docs/manuals/en/firmware.md(the four images, how to get one today, flashing with esptool on Windows, macOS and Linux withwrite_flash "@flash_args"at 921600 baud and the--after hard_reset chip_idrestart, the build and file options, first boot and the setup page, adoption by code, updates over the air, recovery, the modem board and the I/O pod) anddocs/manuals/en/controller-setup.md(parts, the modem board, install, first start, the console address, backups for a Raspberry Pi controller).hardware/field-box/BUILD.mdgains a pin reference (signal, GPIO, firmware option, wire colour, destination, voltage; section 15), checked against Kconfig bytools/check_wiring.py; the Satellite's and the pod's pin tables gain the same columns; the target addon page gains its port's pin reference. Fixed as stale against the firmware: the settings file of a Field Box build isfirmware/esp32/build/sdkconfig, notfirmware/esp32/sdkconfig; the flash baud is 921600; a PCF8574A is found by the firmware (the diagram said "not PCF8574A", re-rendered); the LED ring starts at 40 % brightness; the first screens of a new box. Things not yet tried on a real board are marked as such. - The project's mode signing keys (the maintainer's choice of 2026-10-28): an offline root key (
d9521a79030ce544, held by the maintainer) and a CI key (7275108d2e3fe583, the secrets of the registry repository, used by its new Verified badge workflow, which a maintainer starts by hand and which opens a pull request). Both are in the registry'skeys/project/and are now the defaults of the Field Box firmware'sCONFIG_ATALAIA_MODE_KEYS(andtools/docker_fw.py --mode-keys), the Android app's built-in mode keys (props) and the controllers'[modes] box_keys(Pi and phone); a field still trusts them for its own installs only by listing them in[modes] keys.docs/community/keys.mdlists them, why the CI key is a secret, and what to do if it is exposed. - A staff page for a field's settings (decisions FS-1 to FS-6;
server/)./staff/fields/<field>/settings(the field'sadmin, linked from the field's sections) sets what onlyPATCH /api/v1/fields/{field_id}reached: the public page, the check-in rules, the membership rule (accepted list or any, register, check-in, expiry, the country's shipped list and the field's own entries), chrono sharing and the country. The membership words (APD or association) follow the country and change on the page, with the offered list, when a new country is saved; every refusal has words in pt-PT, English, German and Traditional Chinese; the page tells that the controllers get the change at once. No API change; GM manual (en, pt) and the server README say where to set them. - APD membership becomes association membership, with the field's country (the maintainer's decision of 2026-10-28; package H, decisions MEM-1 to MEM-6; migration
0019_field_country;protocol/apd.md,protocol/vectors/apd.jsonandsync.json,registry/apds/): players in other countries may need a membership too (a club, a federation, an insurance body), just not an "APD". Each entry of the shipped list (registry/apds/apds.json, schema and file keep their historical names) has acountry(ISO 3166-1 alpha-2; the three shipped arePT). A field has a country: on the site server (fields.country,PATCH /api/v1/fields/{id}country, in the field's JSON,fields-by-slug, the registration list andsettings.countryofGET /api/v1/sync/field?settings=1, additive;GET /api/v1/apds?country=) and on both controllers ([checkin] country, the console's settings, thecountrycheck-in setting ofPUT /api/v1/checkin/settings, the phone controller's; 409 when the site's applies with[sync] branding = "site"); the default isPTwhere the time zone is Europe/Lisbon, Atlantic/Azores or Atlantic/Madeira (the existing fields too, by the migration), otherwise unset. Wording by country: a PT field keeps the "APD" texts; any other country or none says "association membership" / "membership number" in pt-PT (gender-neutral), English, German and Traditional Chinese, in the app (registration, check-in, marshal), the GM console, the phone controller, the site server's staff pages and the public game page. Which list a field offers: the entries of its country plus its owncustomones (none from the shipped list without a country); the accepted-list rule is unchanged. Wire names, verdict codes and error codes stay (apd_id,apd,apd_status, ...: historical, documented inprotocol/apd.md); a client treats an answer with nocountrykey asPT. Shared vectors gainedcountry_valid,country_invalid,default_country,wordingandchoices(a non-PT field) andsettings.countrycases, run by the server, the Pi controller, the phone controller and the app. Tests:server/tests/test_apd_membership.py(CountryTests),test_roles.py,test_migrations.py, the staff browser test,controller/tests/test_apd.py, the console's browser tests,app/test/protocol/apd_test.dart,app/test/controller/controller_apd_test.dart, the check-in, site and marshal widget tests,registry/tests/test_apds.py. Manuals (game master, marshal, player; en, pt) say what changes outside Portugal. - Android props fetch signed mode packages from their controller, as Field Boxes do (the item P5B-17 and P5D-1 left for "a later PE24 slice"; decisions PKF-1 to PKF-8;
app/lib/modes/prop_mode_fetch.dart,app/lib/prop/prop_setup.dart,app/lib/ui/settings_screen.dart): when a join names a mode or modifier the app does not carry, asks for a fetch (join key 16) and names the controller (keys 13 and 14), the prop downloads each missing part over HTTPS pinned to the field CA, checks it with the box's own checks (boxCheckReleaseandboxCheckArchive, the sharedbox_checksvectors: the release's exact form, the Ed25519 signature against the prop's mode keys, kind, tier, never a built-in id, size, SHA-256, the canonical archive, then its mode.yaml agreeing with the signed manifest and the script loading in the core's sandbox), keeps it (at most 8 packages and 512 KiB, the least recently used dropped, each checked again with the current keys every time it is read) and reports to the controller with the game key's HMAC, so its start guard stops waiting. An unsigned or badly signed package is refused with a message naming the reason and never runs. The trust roots are the keys built into the build (--dart-define=ATALAIA_MODE_KEYS) and a new setting, Mode keys for fetched modes (at most 4), never a key a controller sends. Texts in pt, en, de and zh-tw. - **The field's logo on the Field Box's big screen...