{"id":190,"date":"2026-09-29T17:03:22","date_gmt":"2026-09-29T15:03:22","guid":{"rendered":"https:\/\/www.waarsenburg.com\/?p=190"},"modified":"2026-09-30T16:22:12","modified_gmt":"2026-09-30T14:22:12","slug":"turning-my-zbitx-cw-nightmare-machine-into-a-cw-dream-machine","status":"publish","type":"post","link":"https:\/\/www.waarsenburg.com\/index.php\/2026\/09\/29\/turning-my-zbitx-cw-nightmare-machine-into-a-cw-dream-machine\/","title":{"rendered":"Turning my zBitx CW nightmare machine into a CW dream machine"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" class=\"alignleft size-thumbnail\" src=\"https:\/\/www.hfsignals.com\/wp-content\/uploads\/2024\/12\/zbitx_hero.jpg\" alt=\"zBitx\" width=\"150\" height=\"117\" \/>April 1st 2025 I bought my zBitx V1, which was promoted as a &#8220;CW Dream Machine&#8221;. It turned out to be a CW nightmare. CW was close to unusable. Keying was buggy, elements were missed or added and soon the zBitx ended on the shelf, where it remained for a couple of months.<\/p>\n<p><!--more--><\/p>\n<p>Eventually, Ashhar Farhan released a new version with several updates, which I tried on my zBitx. I don&#8217;t know what was improved, but not CW. The zBitx moved to its shelf again. In the meantime, I started following W9JES on the Discord server, where he an his team are working on the improvement of the zBitx firmware. Recently <a href=\"https:\/\/github.com\/drexjj\/zbitx\/\" target=\"_blank\" rel=\"noopener\">v5.11<\/a> was released and I decided to download Jesse&#8217;s fork for the first time. The software worked right away, but alas&#8230; CW was still buggy. I tried straight key, paddle and external keyer, but I was not able to make a decent CW QSO.<\/p>\n<p><span class=\"HwtZe\" lang=\"en\"><span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">In the meantime, I\u2019ve subscribed to ChatGPT.<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">Would it be possible to have ChatGPT analyze the zBitx code?<\/span><\/span><\/span><\/p>\n<p>I ZIPped the entire zBitx subdirectory and uploaded it to Codex; ChatGPT&#8217;s code plugin. I asked ChatGPT to analyze the code and find out why CW was not working as expected. Codex presented the following results:<\/p>\n<p><strong><span class=\"HwtZe\" lang=\"en\"><span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\"># Analysis of the zBitx v5.11s CW response <\/span><\/span><\/span><\/strong><\/p>\n<p><span class=\"HwtZe\" lang=\"en\"><span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">The provided source code reveals specific causes for irregular keying response.<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">The primary issue is that key sampling and transmission initiation are tied to the main loop, which also handles front-panel communication and file writing.<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">Both paddles and straight keys utilize this path.<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">Improving performance while retaining FT8 and SSB capabilities is technically feasible but requires a dedicated CW input path and careful management of shared transmission and audio functions.<\/span><\/span><\/span><\/p>\n<p><span class=\"HwtZe\" lang=\"en\"><span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">Examined: the submitted `zbitx.zip` (6,199,123 bytes; SHA-256 `96A2033C6D1CCE188BCEB3FE1B2C772017FB51052A1B1348339BCD37A3E9FB0B`).<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">`src\/sdr_ui.h:58` specifies `zbitx v5.11s`.<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">All file names and line numbers below refer to this source code, not to any potentially modified online version.<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">It has not been established that the provided executable was built exactly from this source code.<\/span><\/span><\/span><\/p>\n<p>## The current signal path<\/p>\n<p>`GPIO \u2192 key_poll() \u2192 cw_poll() \u2192 cw_key_state \u2192 cw_tx_get_sample() \u2192 tx_process() \u2192 ALSA \u2192 codec \/ RF`<\/p>\n<p>The first three steps are driven by `ui_tick()`, even in headless mode. Additionally, when transitioning from receive to transmit, `cw_poll()` calls `tx_on()`. The CW sample generator only runs via `sound_process()` and `modem_next_sample()` once the DSP transmit state is active.<\/p>\n<p>`key_poll()` reads the Raspberry Pi GPIO directly via wiringPi. Consequently, the front-panel processor does not supply the paddle events in this code. `PTT=7` and `DASH=21` are wiringPi numbers; do not treat them as BCM or physical pin numbers without conversion. In straight-key mode, only DASH is read; with paddles, PTT acts as the dash and DASH as the dot. See `sbitx_gtk.c:111`, `6082\u20136099`.<\/p>\n<p><strong>## Findings, in order of importance<\/strong><\/p>\n<p>### 1. The high polling frequency does not provide a fixed response time<\/p>\n<p>`ui_tick()` calls the modem in CW\/CWR mode on every tick (`sbitx_gtk.c:7041\u20137050`). The GUI uses `g_timeout_add(1, &#8230;)` (`6197`); however, the default Makefile builds with `JJ_HEADLESS_DAEMON=1`. In this configuration, the main loop executes `ui_tick()` first and then sleeps for 1 ms (`8544\u20138557`). Consequently, the effective period is the sum of processing time, sleep time, and scheduling overhead.<\/p>\n<p>The older comment at the top of `modem_cw.c` regarding 10\u201320 polls per second no longer describes the current implementation. Nor does the 1 ms setting imply that a GPIO measurement actually occurs every millisecond. GLib explicitly states that callbacks can be delayed by other processing tasks: [GLib timeout_add](https:\/\/docs.gtk.org\/glib\/func.timeout_add.html).<\/p>\n<p>`zbitx_poll()` runs on the same main thread. During CW reception, field and spectrum updates remain active. For each modified field, there is a `delay(10)` call, plus I\u00b2C traffic and potential retry delays (`6605\u20136617`). Just two modified fields add 20 ms to that execution cycle. A key action occurring during this execution is only detected later; a brief press-and-release sequence between two measurements might be missed entirely.<\/p>\n<p>During CW transmission, field and spectrum updates are skipped (`6595\u20136597`, `6649`), but console traffic, `IN_TX` handling, panel reads, and other processing tasks continue to occur. The read operation requests 255 bytes (`i2c.c:138\u2013162`). The actual duration depends on the bus and the driver. The I\u00b2C implementation used performs synchronous `ioctl(I2C_RDWR)` calls on `\/dev\/i2c-3`; the comments regarding bit-banging do not, in themselves, demonstrate how the kernel handles that bus.<\/p>\n<p>In CW, the panel polling interval has been reduced to every 500 ticks (`7063\u20137067`), but this primarily affects the frequency of blocking events rather than their duration. The very first character following a reception period remains vulnerable. A full synchronization may also occur when the panel is reconnected (`7084`).<\/p>\n<p>### 2. Every transmission start involves a synchronous write of settings<\/p>\n<p>`tx_on()` begins by calling `save_user_settings(1)` (`sbitx_gtk.c:5288`) before `sdr_request(&#8220;tx=on&#8221;, &#8230;)` (`5315`). The save function only skips the write operation if `forced == 0` (`1696\u20131708`). Passing argument 1 causes even unchanged settings to be reopened, overwritten, and closed (`1748\u20131776`).<\/p>\n<p>The comment accompanying the call claims that writing occurs only when changes have been made; the code does not actually do this. This represents a fundamental error in the assumption underlying the call. File system and SD card behavior can introduce variable latency here. The extent of this latency cannot be determined from the source code alone, as buffered I\/O does not necessarily result in slow performance every time.<\/p>\n<p>This applies to the initiation of a transmission, not to every individual dot within an ongoing transmission period. Consequently, it primarily explains the discrepancy between the first character and subsequent characters. Since the main loop does not poll again during the write operation, the previously read key state remains in effect for a longer duration.<\/p>\n<p>### 3. Audio samples are generated in blocks<\/p>\n<p>`sbitx_sound.c:64\u201369` sets the configuration to 96 kHz, 1024 frames per period, and four periods per buffer. One period represents 10.67 ms; four periods represent a nominal buffer capacity of 42.67 ms. `sbitx.c:1824\u20131832` generates the modem samples in blocks of `MAX_BINS\/2 = 1024`.<\/p>\n<p>Thus, the CW generator operates on a 96 kHz sample timeline but is invoked in short bursts of computation. This does not involve GPIO monitoring every 10.4 microseconds. The timing of an event relative to block processing and the already-filled playback buffer remains significant. Buffer capacity does not automatically equate to actual latency; the latter must be determined using the current ALSA parameters and `snd_pcm_delay()`. See [ALSA PCM interface](https:\/\/www.alsa-project.org\/alsa-doc\/alsa-lib\/group___p_c_m.html).<\/p>\n<p>During playback initialization, `snd_pcm_sw_params_set_start_threshold(&#8230;, 8192)` is used (`278`), but the subsequent `snd_pcm_sw_params()` call required to apply the settings is missing from this file. Therefore, do not attribute a measured or guaranteed 85 ms latency to this constant. While the configuration warrants correction based on the actual negotiated buffer size, simply adding the missing apply call could trigger an inappropriately chosen threshold given a nominal buffer of 4096 frames. Additionally, the primary audio thread employs an unbounded busy-wait for playback space (`854\u2013858`) and requests maximum SCHED_FIFO priority (`1147\u20131148`). In the event of issues, this can consume CPU time and interfere with other processing. The effects depend on scheduling, cores, and the driver; this represents an additional risk rather than a root cause measured here.<\/p>\n<p>### 4. Straight key features additional timing quantization and re-keying lockout<\/p>\n<p>For `CW_STRAIGHT`, the state machine executes only when both `keydown_count` and `keyup_count` are zero (`modem_cw.c:454\u2013463`). Pressing the key sets `keydown_count` to 480 (`519`). At 96 kHz, this means the state is re-evaluated only after 5 ms blocks. Even with perfect GPIO input, this introduces a phase-dependent delay upon key release.<\/p>\n<p>Upon release, `keyup_count` is set to `cw_envelope_pos` (`513`). With a fully formed envelope, this value is 480. However, the decay code first counts down the envelope position and only begins counting down the remaining `keyup` counter once that is complete (`475\u2013483`). Consequently, the state machine remains occupied for 959 samples\u2014approximately 9.99 ms\u2014in the event of a full envelope: roughly 5 ms for decay, followed by another ~5 ms without re-evaluating the straight key. A re-keying event within that interval may be delayed or missed.<\/p>\n<p>These figures are derived from the counters, not from hardware measurements. The same decay mechanism extends the programmed element spacing by 479 samples (approx. 4.99 ms) for standard, fully formed paddle elements. This constitutes a distinct timing issue, separate from the variable main-loop latency.<\/p>\n<p>The 5 ms envelope itself serves to ensure a smooth amplitude transition. This feature should be preserved or deliberately redesigned; simply removing it is not a suitable solution for the keying delay.<\/p>\n<p>### 5. RX\/TX transition and shared state require explicit synchronization<\/p>\n<p>Both switching variants include a `delay(10)` for the LPF relay upon initiating transmission (`sbitx.c:2578`, `2637`). In this version, the extra 20 ms applies only outside of CW\/CWR modes. Simply removing the relay delay is incorrect; it ensures the correct sequence of filter engagement and PA activation.<\/p>\n<p>The DSP flag `in_tx` is set prior to the relay delay (`2566`, `2625`). However, `cw_mode` is only set in `cw_poll()` after returning from `tx_on()` (`modem_cw.c:1389\u20131393`). Consequently, audio processing may begin before the transmission hardware or keyer mode is ready. Whether this is audible depends on thread scheduling and buffering.<\/p>\n<p>Variables such as `cw_key_state`, `cw_period`, `cw_mode`, `cw_tx_until`, and `symbol_next` are shared between the main thread and the audio thread without apparent use of atomics or locking mechanisms in this execution path. While this constitutes a synchronization issue, it does not necessarily prove that a specific observed pause is caused by it. Simply adding the `volatile` keyword does not resolve the data transfer issue. Furthermore, any redesign must coordinate the call to `fft_reset_m_bins()`\u2014currently located in the switching code\u2014with the DSP thread.<\/p>\n<p><strong>## Recommended Improvement<\/strong><\/p>\n<p>**Eliminate unpredictable input and transmission-start latency first; optimize audio only afterwards.**<\/p>\n<ol>\n<li>Move settings storage out of the critical CW transmission-start path. Have a background task save a consistent copy, while maintaining &#8220;dirty&#8221; status and standard persistence. Simply adding a &#8220;dirty&#8221; check reduces unnecessary writes but leaves a write-induced delay during the first transmission start after tuning.<\/li>\n<li>Create a dedicated CW input worker that continuously monitors both contacts during RX, TX, and mode switching. Use timestamped GPIO edge events where the installed kernel and GPIO backend support them, or\u2014as an interim measure\u2014a lightweight periodic worker using absolute monotonic deadlines. This worker handles only input and event dispatching; it performs no display, file I\/O, DSP, or transmission hardware calls. The Linux GPIO API supports edge events; check for compatibility and conflicts with existing wiringPi usage: [Linux GPIO character device API](https:\/\/docs.kernel.org\/userspace-api\/gpio\/chardev.html).<\/li>\n<li>Pass transitions via a bounded, properly synchronized queue using monotonic timestamps. Relying solely on an atomic &#8220;last state&#8221; fails to capture rapid press-release sequences. Account for contact bounce, event ordering, overflow, and resynchronization. Implement a fail-safe &#8220;key-released&#8221; state for error conditions.<\/li>\n<li>Ensure the CW request to switch to TX is also processed independently of a blocking UI. Merely moving the GPIO polling to a worker does not resolve the initial-character delay if `tx_on()` still has to wait for the UI. A single controller must manage the shared transmission hardware and serialize requests from CW, FT8, PTT, and CAT.<\/li>\n<li>Use an explicit RX \u2192 TX_PREPARE \u2192 TX_READY transition. Publish keyer mode and parameters before the generator starts, wait for the necessary relay settling time, and process the first buffered key action without silently clipping it. Also, define the behavior for a short straight-key pulse that occurs entirely during TX_PREPARE. Delayed playback must handle both the start and end with the same time offset.<\/li>\n<li>In the CW generator, decouple the element timer from the envelope position. Evaluate straight-key up\/down events on the sample timeline, avoiding the current 480-sample re-check blocks. If a new key press occurs during the decay phase, allow the envelope to resume from the current amplitude level. Let the element spacing timer count down independently of the decay phase, and explicitly handle iambic A\/B, bug, and Ultimatic modes.<\/li>\n<li>Next, measure buffer occupancy, execution time, and underruns. A GPIO event queue does not eliminate audio latency already present in the playback buffer. Implementing a smaller CW buffer or a separate CW audio path is a subsequent step carrying a higher risk of regression. Do not simply reduce `buff_size`; DSP, FFT overlap, and CW reception processes expect 1024-sample blocks in various places.<\/li>\n<\/ol>\n<p>A dedicated input worker with a target interval of 0.5\u20131 ms is a viable initial design choice to explore, though it offers no guarantee regarding Linux response times. GPIO events largely eliminate the need for periodic input polling, but the consumer and audio output processes must still execute on time. Even a real-time kernel alone does not resolve explicit sleeps, disk I\/O, and incorrect counter logic.<\/p>\n<p><strong>## Preserving FT8 and SSB<\/strong><\/p>\n<p>Modem selection is already decoupled: `modems.c:469\u2013479` selects FT8 or CW polling; `506\u2013519` selects the corresponding sample generator. Consequently, CW-specific inputs and counters can be modified without altering the FT8 encoder\/decoder or SSB modulation.<\/p>\n<p>Constraints lie within the shared `tx_on\/tx_off`, GPIO, sound thread, and DSP buffers. Pay particular attention to the following:<\/p>\n<p>&#8211; PTT must retain its existing function in USB\/LSB modes (`sbitx_gtk.c:7241\u20137248`); the same pin must not trigger keyer events outside of CW mode.<br \/>\n&#8211; Upon mode switching, legacy CW events, paddle memory, and active envelope states must be handled gracefully. Residual CW events must not trigger the start of FT8 or SSB transmission.<br \/>\n&#8211; Initially, preserve existing audio parameters and FT8 timing logic. Any new worker process must sleep or wait; it must not become a high-priority loop that consumes excessive CPU resources.<br \/>\n&#8211; Prevent simultaneous writes to the transmission hardware by FT8, CAT, PTT, and CW functions. Priority management and cancellation logic should be handled by a single shared controller.<br \/>\n&#8211; Handle display updates using consistent snapshots and serialized bus access; existing UI fields are not inherently thread-safe.&#8221;<\/p>\n<p><span class=\"HwtZe\" lang=\"en\"><span class=\"jCAhz\"><span class=\"ryNqvb\">That concludes the analysis by ChatGPT.<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">Next, I asked ChatGPT to implement the proposed improvements.<\/span><\/span> <span class=\"jCAhz ChMk0b\"><span class=\"ryNqvb\">This took well over half an hour, after which I was presented with completely overhauled source code.<\/span><\/span><\/span><\/p>\n<p><strong>## What has been implemented<\/strong><\/p>\n<p>&#8211; A dedicated GPIO worker reads the existing wiringPi key inputs. The target interval is 1 ms, using absolute monotonic deadlines and a 1 ms stability check for contact debouncing. This involves fast, independent polling\u2014not a GPIO interrupt implementation\u2014and offers no hard real-time guarantees.<br \/>\n&#8211; A separate controller manages the initiation of CW transmission and the return to RX mode. The controller is no longer directly delayed by the main loop processing screen updates or configuration file operations.<br \/>\n&#8211; Key transitions are timestamped and passed through an atomically synchronized buffer of 256 events. This ensures that a brief press and release occurring during transmission preparation remain available to the audio generator, preserving their relative timing.<br \/>\n&#8211; The relay delay mechanism is retained. The CW generator is enabled only after transmission preparation is complete. The controller and standard SDR commands serialize their access; the DSP does not consume key duration during a CW hardware transition.<br \/>\n&#8211; Initiating CW transmission no longer triggers synchronous writing of settings to disk. Existing periodic saving and saving upon normal shutdown remain in place. Non-CW transmission starts retain their existing saving behavior.<br \/>\n&#8211; Straight key operation responds to the sample timeline rather than relying on 480-sample blocks for re-evaluation. Keying can resume from the current envelope position during the decay phase.<br \/>\n&#8211; The envelope decay no longer extends the element spacing by approximately 5 ms. The existing Blackman-Harris envelope is retained.<br \/>\n&#8211; Iambic A mode now cancels the extra element upon releasing a squeeze; Iambic B mode retains this extra opposite element.<br \/>\n&#8211; CW text is passed from the UI to the audio stage via a separate buffer. For this purpose, the audio generator no longer modifies text fields and sends the TX echo back via a buffer.<br \/>\n&#8211; The transition to RX accounts for the measured playback buffer, ensuring that a short CW_DELAY does not immediately cut off the final block awaiting playback.<br \/>\n&#8211; Cancellations and mode\/frequency changes discard pending events. Following a cancellation or reactivation, a contact that is already pressed must first be released. In the event of an event overflow, CW transmission is aborted, and the contact must be released and pressed again.<br \/>\n&#8211; The new worker threads remain idle when not in CW mode. The FT8 encoder\/decoder, SSB modulation, sample rate, and audio buffer size remain unchanged.<br \/>\nNew modules: `src\/cw_runtime.c\/.h` and `src\/radio_control.c\/.h`. Integration is implemented in `modem_cw.c`, `modems.c`, `sbitx.c`, `sbitx_sound.c`, and `sbitx_gtk.c`. The version label is `zbitx v5.11s-cw1`.<\/p>\n<p>You can download the modified source code for v5.11s <a href=\"https:\/\/www.waarsenburg.com\/download\/zbitx-v5.11s-cw1-complete.zip\">HERE<\/a>.<br \/>\nYou can download the modified source code for v5.12s <a href=\"https:\/\/www.waarsenburg.com\/download\/zbitx-v5.12s-cw1-complete.zip\">HERE<\/a>.<\/p>\n<p>1. Close the running zBitx application normally.<br \/>\n2. Extract the source code overlay into the copied installation (~\/sbitx).<\/p>\n<p>3. Build and test with the development libraries already required for this software:<\/p>\n<p>&#8220;`sh<br \/>\ncd ~\/sbitx<br \/>\nmake test-cw<br \/>\nmake -C src\/ft8_lib clean<br \/>\nmake -C src\/ft8_lib libft8.a<br \/>\nmake OPT=o zbitx<br \/>\n&#8220;`<\/p>\n<p>The Makefile now explicitly uses GNU C11 for atomics. The existing wiringPi, ALSA, FFTW, GTK and other link dependencies remain necessary. The test target does not use a GPIO, sound card or RF hardware. The Windows adapter in `tests\/win` is for development testing only and is not used by the Pi-Makefile.<\/p>\n<p>4. Start using your normal starting method and check the version label &#8216;v5.11s-cw1&#8242;.<\/p>\n<p>For this local variant, do not use the included native `.\/update` script: that will retrieve the upstream version and can replace the local changes. No front panel firmware change is required for the new input path.<\/p>\n<p>Finally my zBitx is useable for CW. I haven&#8217;t tested FT8 and\/or SSB yet, since I rarely use these modes, but I explicitely asked ChatGPT to preserve these functions. I hope you&#8217;ll find this version useful.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>April 1st 2025 I bought my zBitx V1, which was promoted as a &#8220;CW Dream Machine&#8221;. It turned out to be a CW nightmare. CW was close to unusable. Keying was buggy, elements were missed or added and soon the zBitx ended on the shelf, where it remained for a couple of months.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-190","post","type-post","status-publish","format-standard","hentry","category-nieuws"],"publishpress_future_action":{"enabled":false,"date":"2026-10-08 01:39:03","action":"change-status","newStatus":"draft","terms":[],"taxonomy":"category","extraData":[]},"publishpress_future_workflow_manual_trigger":{"enabledWorkflows":[]},"_links":{"self":[{"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/posts\/190","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/comments?post=190"}],"version-history":[{"count":7,"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/posts\/190\/revisions"}],"predecessor-version":[{"id":200,"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/posts\/190\/revisions\/200"}],"wp:attachment":[{"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/media?parent=190"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/categories?post=190"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.waarsenburg.com\/index.php\/wp-json\/wp\/v2\/tags?post=190"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}