OXYGENHUB

0.5.0: game controller injection (POST/GET /pad) bed0f1e524fe

There is no way to test a game's controller handling without a human
holding hardware. /key covers the keyboard and /pointer the mouse; the
pad -- the input most games are actually played with -- had nothing.

POST /pad presents a VIRTUAL controller at an index and sets what is
held; GET /pad reads back {index, connected, buttons}. Backend callbacks
set_pad / get_pad, and the server advertises 0.5.0 only when both are
present, the same gate 0.4.0 applies to the pointer pair.

Three decisions worth stating.

CANONICAL LAYOUT. bit0 LEFT, 1 RIGHT, 2 UP, 3 DOWN, 4 A, 5 B, 6 X, 7 Y,
8 START, 9 SELECT, 10 L, 11 R -- the SNES/Neo6502 native order, fixed
across platforms. A backend whose hardware disagrees (the X16 KERNAL
reports RIGHT/LEFT/DOWN/UP) converts in its own floor. Same reasoning
that fixed bit0 = primary for the pointer: one layout defined once, so a
test written against one machine reads the same on another.

A LEVEL, NOT AN EVENT. The held mask persists across /step frames until
changed, unlike /key's momentary tap. That is the property that makes
this worth having rather than reusing /key, so conformance asserts it
directly rather than assuming it.

VIRTUAL, NOT SDL. A backend presents a device its existing read path
picks up; it does not drive the host's joystick API. Headless CI has no
joystick, and that is exactly the case this exists for.

Motivated by a bug that shipped: a port masked the firmware's merged
controller down to its d-pad and tried to recover the buttons from the
wrong controller index. Movement worked, fire did nothing, and nothing
could catch it without hardware in someone's hands.

Conformance covers the surface, the round trip, the hold-across-frames
property and that connected=0 is observable -- a backend that always
reports "connected" is not conformant, because a program offering
two-player only when a second pad is present needs both answers.

Core compiles clean; client and conformance parse. No backend implements
it yet, so every server keeps advertising 0.4.0 until one does.

Git-Commit: 8731e30a5faeb53a4b966e8b2605be7e91ba792d
Git-Author: Andy McCall <[email protected]>
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-07-28 20:50 UTC
Signature
◈ sealed by Andy McCall
Hash
bed0f1e524fef708c855791a30d54b455bbe957f85f87cf0879a8cf3ff44287e
Depends on
346ca3ee83f4 4c0b050e9a25 674b8447022b 8ac19619e24b 961b3c610c66 d44319b1e712
Depended on by
8fd33bc520f9 1b40a077e41f
In bookmarks
main perf-6
Edits
18 atoms, 0 tree operations, 7 files

Browse the tree at this change

modified CHANGELOG.md +21 −0
…
6 unchanged lines
7 7
advertises the highest level whose backend callbacks are all present via
8 8
`/status.contract`.
9 9
10 +
## 0.5.0
11 +
- `POST /pad?index=[&buttons=][&connected=]` — present a **virtual game
12 +
  controller** and set what is held. `buttons` is the canonical mask (bit0 LEFT,
13 +
  1 RIGHT, 2 UP, 3 DOWN, 4 A, 5 B, 6 X, 7 Y, 8 START, 9 SELECT, 10 L, 11 R —
14 +
  the SNES/Neo6502 native order, fixed across platforms). `connected=0` unplugs.
15 +
  Backend callback: `set_pad`.
16 +
- `GET /pad?index=` — read back `{index, connected, buttons}`, so a test can tell
17 +
  "the input never arrived" from "it arrived and was ignored". Backend callback:
18 +
  `get_pad`.
19 +
- A pad is a **level, not an event**: the held mask persists across `/step`
20 +
  frames until changed, unlike `/key`'s momentary tap. Conformance asserts this
21 +
  directly, because it is the property that makes `/pad` worth having rather
22 +
  than reusing `/key`.
23 +
- **Virtual, not SDL.** A backend presents a device its existing read path picks
24 +
  up rather than driving the host's joystick API. Headless CI has no joystick,
25 +
  and that is precisely the case this endpoint exists for.
26 +
- Motivation: a real bug that shipped. A port's pad read masked the firmware's
27 +
  merged controller down to its d-pad and tried to recover the buttons from the
28 +
  wrong controller index — movement worked, fire did nothing, and there was no
29 +
  way to catch it without a human holding hardware.
30 +
10 31
## 0.4.0
11 32
- `POST /pointer?x=&y=[&buttons=]` (absolute) or `?dx=&dy=[&buttons=]` (relative)
12 33
  — inject a pointer move/click through the platform's native mouse path.
…
32 unchanged lines
modified README.md +6 −2
…
18 unchanged lines
19 19
| `GET /audio` | drain synthesised PCM as a WAV | 0.3 |
20 20
| `POST /pointer?x=&y=&buttons=` | inject a pointer move/click | 0.4 |
21 21
| `GET /pointer` | read back pointer position + buttons | 0.4 |
22 +
| `POST /pad?index=&buttons=&connected=` | present a virtual controller, set what is held | 0.5 |
23 +
| `GET /pad?index=` | read back a pad's presence + held buttons | 0.5 |
22 24
23 -
Current contract: **0.4.0**. Minors are purely additive (a missing capability
25 +
Current contract: **0.5.0**. Minors are purely additive (a missing capability
24 26
returns 501 and lowers the advertised contract), so a client reads
25 27
`/status.contract` and fails fast only on a MAJOR mismatch. See [SPEC.md](SPEC.md).
26 28
…
46 unchanged lines
73 75
#include "retro_control.h"
74 76
75 77
/* Four callbacks satisfy 0.1; add inject_key/reset for 0.2,
76 -
   write_mem/capture_audio for 0.3, set_pointer/get_pointer for 0.4.
78 +
   write_mem/capture_audio for 0.3, set_pointer/get_pointer for 0.4,
79 +
   set_pad/get_pad for 0.5.
77 80
   A NULL callback 501s its endpoint. */
78 81
static const retro_control_backend_t backend = {
79 82
    .platform        = "x16",
…
7 unchanged lines
87 90
    .write_mem       = x16_write_mem,        /* 0.3 */
88 91
    .capture_audio   = x16_capture_audio,    /* 0.3 */
89 92
    .set_pointer     = x16_set_pointer,      /* 0.4 */
93 +
    .set_pad         = x16_set_pad,          /* 0.5 */
90 94
    .get_pointer     = x16_get_pointer,      /* 0.4 */
91 95
};
92 96
…
22 unchanged lines
modified SPEC.md +47 −0
…
161 unchanged lines
162 162
- Lets a test distinguish "the input never reached the platform" from "it
163 163
  arrived but the program drew the wrong thing". No pointer on the platform → 400.
164 164
165 +
### `POST /pad?index=<n>  [&buttons=<mask>] [&connected=0|1]`  *(0.5)*
166 +
Inject a game controller — the pad analogue of `/key` and `/pointer`. Query
167 +
params (no body):
168 +
- `index` — which pad, 0-based. OPTIONAL, default 0.
169 +
- `buttons` — bitmask of what is HELD, in the canonical layout below. Omitted →
170 +
  the button state is left unchanged. `buttons=0` releases everything while
171 +
  leaving the pad connected.
172 +
- `connected` — OPTIONAL. `1` (the default whenever `buttons` is given) presents
173 +
  a **virtual pad** at `index`; `0` unplugs it. Unplugging matters: a program
174 +
  that asks "is a controller present" before offering two-player must be
175 +
  testable in both states, and on most platforms the count is what it asks.
176 +
- Response: `{ "injected": true }`. No controller support on the platform → 400.
177 +
178 +
Canonical button layout, low bit first:
179 +
180 +
| bit | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
181 +
|---|---|---|---|---|---|---|---|---|---|---|---|---|
182 +
| | LEFT | RIGHT | UP | DOWN | A | B | X | Y | START | SELECT | L | R |
183 +
184 +
This is the SNES/Neo6502 native order, chosen for the same reason `/pointer`
185 +
fixed bit0 = primary: **one layout, defined once, so a test written against one
186 +
platform reads the same on another.** A backend whose hardware disagrees — the
187 +
X16 KERNAL reports RIGHT/LEFT/DOWN/UP — converts in its own floor. A test never
188 +
learns which machine it is on, which is the entire point.
189 +
190 +
Device-level, not driver-state: the injection MUST exercise the same path a real
191 +
pad takes, so what the program reads is what physical hardware would produce. On
192 +
an emulator that reads SDL joysticks, that means presenting a virtual pad the
193 +
existing read path picks up — **not** driving SDL, which would need a joystick
194 +
to exist and defeats the purpose. Headless CI has no joystick; that is the case
195 +
this endpoint is for.
196 +
197 +
Hold across frames: a pad is a LEVEL, not an event. `buttons` stays as set until
198 +
changed, so a press survives any number of `/step` frames without re-asserting —
199 +
unlike `/key`, whose TAP is deliberately momentary. Release explicitly.
200 +
201 +
### `GET /pad?index=<n>`  *(0.5)*
202 +
Read back a pad. Response:
203 +
```json
204 +
{ "index": 0, "connected": true, "buttons": 48 }
205 +
```
206 +
- `connected` — whether anything is present at that index, real or virtual.
207 +
- `buttons` — the current mask in the layout above.
208 +
- Same job as `GET /pointer`: distinguishes "the input never reached the
209 +
  platform" from "it arrived and the program ignored it". No controller support
210 +
  on the platform → 400.
211 +
165 212
### `POST /reset`  *(0.2)*
166 213
Soft/cold reset the machine. Response `{ "reset": true }`. The `frame` counter
167 214
MAY reset here (the one case `/status.frame` is allowed to go backwards).
…
145 unchanged lines
modified client/emu_control.py +41 −0
…
176 unchanged lines
177 177
        body, _ = self._get("/pointer")
178 178
        return json.loads(body)
179 179
180 +
    # Canonical pad bitmask (SPEC "Canonical button layout"). The SNES /
181 +
    # Neo6502 native order, fixed across platforms so a test written on one
182 +
    # machine reads the same on another.
183 +
    PAD_LEFT   = 0x0001
184 +
    PAD_RIGHT  = 0x0002
185 +
    PAD_UP     = 0x0004
186 +
    PAD_DOWN   = 0x0008
187 +
    PAD_A      = 0x0010
188 +
    PAD_B      = 0x0020
189 +
    PAD_X      = 0x0040
190 +
    PAD_Y      = 0x0080
191 +
    PAD_START  = 0x0100
192 +
    PAD_SELECT = 0x0200
193 +
    PAD_L      = 0x0400
194 +
    PAD_R      = 0x0800
195 +
196 +
    def pad(self, buttons: int | None = None, index: int = 0,
197 +
            connected: bool | None = None) -> None:
198 +
        """Present a virtual controller and set what is held (contract 0.5).
199 +
200 +
        `buttons` is a mask of PAD_* — omit to leave the held state alone.
201 +
        `connected=False` unplugs the pad, which is testable behaviour in its
202 +
        own right: a program that asks whether a controller is present before
203 +
        offering two-player needs both answers.
204 +
205 +
        Unlike key(), a pad is a LEVEL: the state persists across step()
206 +
        frames until changed, so a press does not need re-asserting. Release
207 +
        with pad(0)."""
208 +
        parts = [f"index={index}"]
209 +
        if buttons is not None:
210 +
            parts.append(f"buttons={buttons}")
211 +
        if connected is not None:
212 +
            parts.append(f"connected={1 if connected else 0}")
213 +
        self._post("/pad?" + "&".join(parts))
214 +
215 +
    def pad_get(self, index: int = 0) -> dict:
216 +
        """GET /pad — {index, connected, buttons} (contract 0.5). Distinguishes
217 +
        'the input never arrived' from 'it arrived and was ignored'."""
218 +
        body, _ = self._get(f"/pad?index={index}")
219 +
        return json.loads(body)
220 +
180 221
    def screenshot_ppm(self) -> bytes:
181 222
        """Return the live screen as raw PPM (P6) bytes. Stdlib-only."""
182 223
        body, _ = self._get("/screenshot")
…
29 unchanged lines
modified conformance/test_conformance.py +76 −0
…
199 unchanged lines
200 200
    code, _ = _raw(emu, "GET", "/reset")
201 201
    assert code == 405
202 202
    emu.reset()
203 +
204 +
205 +
# -- contract 0.5: pad injection (skipped on < 0.5 servers) ------------------
206 +
207 +
def test_pad_injection(emu):
208 +
    """/pad presents a virtual controller and sets what is held. The pad's
209 +
    *effect* is platform-specific and tested per-port; the contract surface is
210 +
    asserted here."""
211 +
    if _contract_minor(emu) < 5:
212 +
        pytest.skip("contract < 0.5 (no pad injection)")
213 +
    emu.pad(buttons=emu.PAD_A)                      # A held, pad plugged in
214 +
    emu.pad(buttons=emu.PAD_A | emu.PAD_LEFT)       # two at once
215 +
    emu.pad(buttons=0)                              # all released, still there
216 +
    emu.pad(index=1, buttons=emu.PAD_B)             # a second pad
217 +
    emu.pad(connected=False)                        # unplug pad 0
218 +
    emu.pad(connected=False, index=1)
219 +
220 +
221 +
def test_pad_readback(emu):
222 +
    """GET /pad reports {index, connected, buttons} (contract 0.5)."""
223 +
    if _contract_minor(emu) < 5:
224 +
        pytest.skip("contract < 0.5")
225 +
    emu.pause()
226 +
    try:
227 +
        emu.pad(buttons=emu.PAD_A | emu.PAD_RIGHT)
228 +
        emu.step(1)
229 +
        st = emu.pad_get()
230 +
        assert isinstance(st["buttons"], int)
231 +
        assert isinstance(st["connected"], bool)
232 +
        assert st["index"] == 0
233 +
        assert st["connected"] is True, "injected pad reports itself absent"
234 +
        # The mask must survive the round trip: this is the check that
235 +
        # separates "the server accepted it" from "the backend stored it".
236 +
        assert st["buttons"] == emu.PAD_A | emu.PAD_RIGHT, (
237 +
            f"held {st['buttons']:#x}, injected "
238 +
            f"{emu.PAD_A | emu.PAD_RIGHT:#x}")
239 +
    finally:
240 +
        emu.pad(buttons=0)
241 +
        emu.resume()
242 +
243 +
244 +
def test_pad_is_a_level_not_an_event(emu):
245 +
    """A pad HOLDS. Unlike a key tap, an injected press must survive stepping —
246 +
    a program that samples input once a frame has to still see it on frame 10.
247 +
    This is the difference that makes /pad worth having rather than reusing
248 +
    /key, so it is asserted rather than assumed."""
249 +
    if _contract_minor(emu) < 5:
250 +
        pytest.skip("contract < 0.5")
251 +
    emu.pause()
252 +
    try:
253 +
        emu.pad(buttons=emu.PAD_B)
254 +
        emu.step(10)
255 +
        assert emu.pad_get()["buttons"] == emu.PAD_B, (
256 +
            "the held mask did not survive 10 frames — /pad is behaving "
257 +
            "like a momentary key tap")
258 +
    finally:
259 +
        emu.pad(buttons=0)
260 +
        emu.resume()
261 +
262 +
263 +
def test_pad_unplug_is_observable(emu):
264 +
    """connected=0 must actually unplug. A program that offers two-player only
265 +
    when a second controller is present needs both answers to be reachable, so
266 +
    a backend that always reports 'connected' is not conformant."""
267 +
    if _contract_minor(emu) < 5:
268 +
        pytest.skip("contract < 0.5")
269 +
    emu.pause()
270 +
    try:
271 +
        emu.pad(buttons=0)                     # plugged in
272 +
        assert emu.pad_get()["connected"] is True
273 +
        emu.pad(connected=False)
274 +
        assert emu.pad_get()["connected"] is False, (
275 +
            "the pad still reports connected after connected=0")
276 +
    finally:
277 +
        emu.pad(connected=False)
278 +
        emu.resume()
modified core/retro_control.c +66 −1
…
47 unchanged lines
48 48
49 49
/* single-slot marshalled request. */
50 50
typedef enum { REQ_NONE = 0, REQ_MEM, REQ_REGS, REQ_SHOT, REQ_KEY, REQ_RESET,
51 -
               REQ_WRITE, REQ_AUDIO, REQ_PTR_SET, REQ_PTR_GET } req_t;
51 +
               REQ_WRITE, REQ_AUDIO, REQ_PTR_SET, REQ_PTR_GET,
52 +
               REQ_PAD_SET, REQ_PAD_GET } req_t;
52 53
static volatile req_t g_req = REQ_NONE;
53 54
static uint32_t g_req_addr, g_req_len;
54 55
static int32_t  g_req_bank;
…
4 unchanged lines
59 60
static int      g_ptr_absolute;  /* 1 => (x,y) absolute; 0 => relative deltas */
60 61
static int32_t  g_ptr_x, g_ptr_y;
61 62
static int      g_ptr_buttons;   /* button bitmask, or -1 = leave unchanged */
63 +
static int      g_pad_index;     /* which pad (0-based) */
64 +
static int      g_pad_buttons;   /* held mask, or -1 = leave unchanged */
65 +
static int      g_pad_connected; /* 1 plug, 0 unplug, -1 = unchanged */
62 66
static char     g_resp_extra[64];     /* extra response header line, or "" */
63 67
64 68
static uint8_t    *g_resp_body  = NULL;
…
199 unchanged lines
264 268
        g_resp_ctype = "application/json";
265 269
        break;
266 270
    }
271 +
    case REQ_PAD_SET: {
272 +
        const char *json;
273 +
        if (!g_be->set_pad) {
274 +
            json = "{\"error\":\"not implemented\"}"; g_resp_status = 501;
275 +
        } else if (g_be->set_pad(g_pad_index, g_pad_buttons, g_pad_connected)) {
276 +
            json = "{\"injected\":true}"; g_resp_status = 200;
277 +
        } else {
278 +
            json = "{\"error\":\"no controller\"}"; g_resp_status = 400;
279 +
        }
280 +
        size_t l = strlen(json);
281 +
        uint8_t *b = (uint8_t *)malloc(l + 1);
282 +
        if (b) memcpy(b, json, l + 1);
283 +
        g_resp_body = b; g_resp_len = b ? l : 0;
284 +
        g_resp_ctype = "application/json";
285 +
        break;
286 +
    }
287 +
    case REQ_PAD_GET: {
288 +
        char *b;
289 +
        if (!g_be->get_pad) {
290 +
            const char *json = "{\"error\":\"not implemented\"}";
291 +
            size_t l = strlen(json); b = (char *)malloc(l + 1);
292 +
            if (b) memcpy(b, json, l + 1);
293 +
            g_resp_len = b ? l : 0; g_resp_status = 501;
294 +
        } else {
295 +
            int buttons = 0, connected = 0;
296 +
            int ok = g_be->get_pad(g_pad_index, &buttons, &connected);
297 +
            b = (char *)malloc(96);
298 +
            if (!b) { g_resp_len = 0; g_resp_status = 500; }
299 +
            else if (ok) {
300 +
                g_resp_len = (size_t)snprintf(b, 96,
301 +
                    "{\"index\":%d,\"connected\":%s,\"buttons\":%d}",
302 +
                    g_pad_index, connected ? "true" : "false", buttons);
303 +
                g_resp_status = 200;
304 +
            } else {
305 +
                g_resp_len = (size_t)snprintf(b, 96,
306 +
                    "{\"error\":\"no controller\"}");
307 +
                g_resp_status = 400;
308 +
            }
309 +
        }
310 +
        g_resp_body = (uint8_t *)b;
311 +
        g_resp_ctype = "application/json";
312 +
        break;
313 +
    }
267 314
    default: break;
268 315
    }
269 316
    g_req = REQ_NONE;
…
125 unchanged lines
395 442
    /* Advertise the highest level whose defining callbacks are all present. */
396 443
    const char *contract =
397 444
        (g_be->inject_key && g_be->write_mem && g_be->capture_audio &&
445 +
         g_be->set_pointer && g_be->get_pointer &&
446 +
         g_be->set_pad && g_be->get_pad) ? "0.5.0" :
447 +
        (g_be->inject_key && g_be->write_mem && g_be->capture_audio &&
398 448
         g_be->set_pointer && g_be->get_pointer) ? "0.4.0" :
399 449
        (g_be->inject_key && g_be->write_mem && g_be->capture_audio) ? "0.3.0" :
400 450
        g_be->inject_key ? "0.2.0" : "0.1.0";
…
161 unchanged lines
562 612
            do_marshalled(fd, REQ_PTR_SET);
563 613
        } else {                              /* 0.4: read back the pointer */
564 614
            do_marshalled(fd, REQ_PTR_GET);
615 +
        }
616 +
        return;
617 +
    }
618 +
    if (!strcmp(target, "/pad")) {
619 +
        g_pad_index = (int)query_long(query, "index", 0);
620 +
        if (is_post) {                        /* 0.5: present / set a pad */
621 +
            g_pad_buttons = (int)query_long(query, "buttons", -1);
622 +
            /* `connected` defaults to plugging it in whenever buttons were
623 +
             * given -- injecting a press on an absent pad is never what the
624 +
             * caller meant -- and to leaving it alone otherwise. */
625 +
            g_pad_connected = (int)query_long(query, "connected",
626 +
                                              g_pad_buttons >= 0 ? 1 : -1);
627 +
            do_marshalled(fd, REQ_PAD_SET);
628 +
        } else {                              /* 0.5: read a pad back */
629 +
            do_marshalled(fd, REQ_PAD_GET);
565 630
        }
566 631
        return;
567 632
    }
…
57 unchanged lines
modified core/retro_control.h +35 −4
…
26 unchanged lines
27 27
#include <stddef.h>
28 28
29 29
/* Highest contract the core implements. The server advertises the highest level
30 -
 * whose backend callbacks are all present: 0.4.0 needs set_pointer + get_pointer
31 -
 * (on top of 0.3); 0.3.0 needs inject_key + write_mem + capture_audio; 0.2.0
32 -
 * needs inject_key; else 0.1.0. Keeps a partial backend honest. */
33 -
#define RETRO_CONTROL_CONTRACT "0.4.0"
30 +
 * whose backend callbacks are all present: 0.5.0 needs set_pad + get_pad (on
31 +
 * top of 0.4); 0.4.0 needs set_pointer + get_pointer (on top of 0.3); 0.3.0
32 +
 * needs inject_key + write_mem + capture_audio; 0.2.0 needs inject_key; else
33 +
 * 0.1.0. Keeps a partial backend honest. */
34 +
#define RETRO_CONTROL_CONTRACT "0.5.0"
34 35
35 36
/* Native framebuffer pixel layout; the server swizzles to PPM RGB. */
36 37
typedef enum {
…
72 unchanged lines
109 110
     * semantics as set_pointer's `buttons`). Return 1 on success (values
110 111
     * written), 0 if no pointer is available. NULL => GET /pointer returns 501. */
111 112
    int (*get_pointer)(int32_t *x, int32_t *y, int *buttons);
113 +
114 +
    /* 0.5: present a virtual game controller at `index` and set what is held.
115 +
     *
116 +
     * `buttons` is the canonical mask (SPEC "Canonical button layout"): bit0
117 +
     * LEFT, 1 RIGHT, 2 UP, 3 DOWN, 4 A, 5 B, 6 X, 7 Y, 8 START, 9 SELECT,
118 +
     * 10 L, 11 R -- the SNES/Neo6502 native order. A backend whose hardware
119 +
     * disagrees converts here, so a test never learns which machine it is on.
120 +
     * -1 leaves the mask unchanged.
121 +
     *
122 +
     * `connected` is 1 to present the pad, 0 to unplug it, -1 to leave the
123 +
     * presence unchanged. Unplugging is testable behaviour in its own right:
124 +
     * a program that asks "is a pad there" before offering two-player needs
125 +
     * both answers.
126 +
     *
127 +
     * A pad is a LEVEL, not an event -- the state persists across frames until
128 +
     * changed, unlike inject_key's TAP.
129 +
     *
130 +
     * MUST drive the same path a real pad takes. On an emulator reading SDL
131 +
     * joysticks that means presenting a virtual device the existing read path
132 +
     * picks up, NOT calling into SDL: headless CI has no joystick, which is
133 +
     * the case this exists for.
134 +
     *
135 +
     * Return 1 on success, 0 if the platform has no controller support
136 +
     * (server -> 400). NULL => POST /pad returns 501. */
137 +
    int (*set_pad)(int index, int buttons, int connected);
138 +
139 +
    /* 0.5: report a pad's presence and held mask (same layout as set_pad).
140 +
     * Return 1 on success (values written), 0 if unsupported. NULL => GET /pad
141 +
     * returns 501. */
142 +
    int (*get_pad)(int index, int *buttons, int *connected);
112 143
} retro_control_backend_t;
113 144
114 145
/* Start the server thread bound to 127.0.0.1:port. Non-blocking; 0 on success,
…
20 unchanged lines