Server modes listen on all IPv4 interfaces (0.0.0.0) using the selected TCP
port. Port 15000 is the normal default for clients and future service packaging,
so installations do not need a machine-specific bind address.
API version 1 does not yet provide authentication or transport encryption. Treat it as a trusted-LAN interface: restrict the port with the daemon host's firewall, do not create an internet port-forward, and do not expose it on an untrusted Wi-Fi network. Secure authenticated/encrypted access remains a separate roadmap item. The proposed separation between API credentials and TX ownership, including SoapySDR credential handling, is recorded in API and SoapySDR authentication notes.
The offline server cannot open the radio:
./build/flex1500d --serve-offline 15000The permission-gated live RX server is also implemented and has completed its first validation. Both modes use the same LAN listener.
An embedded receive test client is available only with the separate
--enable-test-page flag at GET /test. Normal daemon modes return 404 for
that path. The page is an API consumer, not a direct USB/radio interface; see
WEB_TEST_UI.md.
GET /v1/status HTTP/1.1The response includes software_version and git_revision for the daemon
process that actually answered the request, in addition to api_version for
the HTTP protocol contract. A development build appends -dirty to the Git
revision when CMake configures from modified tracked sources.
Response content type: application/json.
Current fields include:
serviceandapi_version- daemon
state radio_openandnetwork_listeningsample_rateandsample_format- processed and sentinel frame counts
- USB packet and packet-error counts
- ring-buffer dropped-frame count
- network frame and sample delivery counts
- network backpressure, disconnect, and write-error counts
Transmit diagnostics are also reported:
tx_startsandtx_stopscount successful Tune starts and all active-Tune stop attempts;tx_underruns,tx_clipped_frames,tx_limited_frames, andtx_dropped_microphone_framesreport live microphone-stream quality;tx_queued_framesandtx_queued_msreport meaningful audio/IQ still waiting in the DSP ring or submitted USB transfers, whiletx_peak_queued_framesrecords the largest observed queue;tx_stop_requested_frames,tx_graceful_drained_frames,tx_graceful_discarded_frames, andtx_graceful_drain_msaccumulate normal PTT-stop drain activity at the fixed 48 ksample/s transmit rate;tx_audio_meter_validand the input, post-gain, and output peak/RMS dBFS fields describe the current or most recently started microphone stream;tx_rejected_ownership_requestscounts busy starts and invalid or mismatched Tune lease operations;tx_watchdog_stopscounts lease-expiry and hard-limit stops; andtx_cleanup_failurescounts Tune or final PA/amplifier cleanup failures.
The daemon prints the transmit counters, including graceful-drain totals, during shutdown. Counters survive an automatic USB recovery within the same daemon process.
A normal HTTP/Soapy PTT stop and physical-microphone release stop accepting new samples, drain meaningful frames already held by the DSP and USB schedulers, and then unkey. The drain is bounded to one second; frames still pending at that deadline are discarded and counted. Disconnect, lease/data watchdog, maximum-key timeout, error, ownership preemption, shutdown, and emergency paths do not wait for queued audio: they invoke immediate safe-unkey cleanup.
Known diagnostic limitation: tx_underruns currently includes zero-I/Q
scheduler fills during preparation and other muted/non-audible intervals. It
can therefore grow very large even when monitored on-air audio is complete and
clean. Treat it as a scheduler activity counter until on-air underruns are
separated from harmless zero-fill frames; use the queue, dropped-frame, and
graceful-discard counters when diagnosing tail loss.
Responses include Content-Length, close the connection after one request, and
set Cache-Control: no-store.
GET /v1/radio HTTP/1.1This read-only JSON route reports the verified model, firmware, USB VID/PID,
and the daemon capability boundary. Normal startup reports receive_only: true,
transmit_enabled: false, tune_enabled: false, and tune_active: false.
The transmit-enabled startup reports Tune capability and current activity
through those fields. The route also reports whether RX tuning is enabled and
the current frequency/filter when known. It also reports
the host receive mode and nominal DSP bandwidth. The fields
physical_inputs_known, mic_ptt, flexwire_ptt, dash, and dot expose the
latest endpoint-0x83 observation. Physical microphone PTT acts on the
transmitter only in the explicitly transmit-enabled daemon mode.
The host-side mode state accepts AM, FM, USB, LSB, and CW:
PUT /v1/radio/mode/usb HTTP/1.1This endpoint does not send a USB command or change radio hardware. It records
the receive DSP selection used by API clients such as the test page. Initial
nominal bandwidths are AM 6000 Hz, USB/LSB 2700 Hz, FM 12000 Hz, and CW 500 Hz.
The SSB passbands are approximately 100–2800 Hz from the carrier. The current
bandwidth is reported as rx_bandwidth_hz. CW uses a 700 Hz beat note in the
browser test demodulator.
Bandwidth is adjustable from 100 through 20,000 Hz with:
PUT /v1/radio/bandwidth/2400 HTTP/1.1Host-side squelch uses a dBFS threshold from -120 through 0. A value of -120 is effectively open:
PUT /v1/radio/squelch/-60 HTTP/1.1Both settings are available in the web test page. SoapySDR exposes bandwidth
through its standard bandwidth interface and squelch as the squelch_db
device setting.
The armed live receive daemon sets +20 dB at startup and accepts exactly five hardware gain settings:
PUT /v1/radio/gain/20 HTTP/1.1Valid values are -10, 0, 10, 20, and 30 dB. Successful requests return the
applied rx_gain_db; other values are rejected without a USB write. The same
control is available through the web test page and SoapySDR's standard RX gain
interface.
In transmit-enabled mode, hardware-changing routes require the persistent
station-control lease returned by POST /v1/control/owner. Present it as
X-Flex1500-Control-Lease and renew it at
PUT /v1/control/owner/keepalive. The first requesting station owns hardware
frequency, mode, gain, TX settings, Tune, and general TX until explicit release,
disconnect/lease expiry, or daemon recovery. DELETE /v1/control/owner is
rejected while a transmitter operation is active. A foreign or missing lease
gets 409 station_owned and cannot alter hardware or transmit.
An adapter for a protocol that cannot communicate operating mode may include
X-Flex1500-Mode-Aware: false only when acquiring this lease. The bundled
SoapySDR adapter does so. While such an owner is primary, physical microphone
PTT derives conventional USB/LSB from the authoritative hardware frequency;
mode-aware HTTP/API owners continue to use their explicitly selected AM, USB,
or LSB mode. This capability flag does not grant additional authority.
The daemon broadcasts IQ to as many as four consumers. A non-owner implements its own tuning and demodulation inside the owner's 48 kHz IQ window; it does not send a hardware frequency request. The browser test page and SoapySDR adapter implement this as center plus/minus 24 kHz local tuning. Physical microphone PTT is the local-priority exception: it uses the current owner's station configuration and does not revoke that station lease.
RX tuning is unavailable in the offline server and in the existing initialization-only live command. Those modes return 404 for the control path. A separately armed receive-only daemon mode enables:
PUT /v1/radio/frequency/10000000 HTTP/1.1The accepted range is 100,000 through 54,000,000 Hz. A successful request sends
exactly SET_RX1_FREQ_TW, followed by the PowerSDR-mapped SET_RX1_FILTER, and
returns the applied frequency and filter as JSON. Out-of-range values return
400; a USB command failure returns 500 and increments
radio_command_errors. Successful paired operations increment
rx_tune_operations.
After both commands succeed, the daemon clears queued pre-tune IQ. An attached IQ stream remains connected, allowing the browser test receiver to tune without stopping audio. A small amount of already-buffered pre-tune audio may still be heard during the transition. SoapySDR clients may continue to reconnect after a frequency change to reset their own stream state.
Each IQ consumer has an independent socket. A stalled or disconnected consumer is removed without interrupting the others. A live test retuned an active Soapy stream through 7.1, 14.2, 21.25, and 28.475 MHz, then restored 10 MHz, with valid samples after every change and no USB packet or radio-command errors.
The new live command is deliberately distinct:
./build/flex1500d --serve-live-rx 15000 \
--initialize-radio-and-enable-rx-tuningIt initializes the radio and permits receive frequency/filter and validated
receive-gain writes from the API. It does not enable Tune or any other
transmit operation. The older --initialize-radio token does not enable
receive-frequency or receive-gain control.
The fixed, capture-matched nominal 5 W Tune carrier is available only with the transmit-enabled live daemon command:
./build/flex1500d --serve-live-rx 15000 \
--initialize-radio-and-enable-transmitThese routes are available through the LAN API:
PUT /v1/radio/tune/startreturns a lease;PUT /v1/radio/tune/keepalive/LEASErenews its 15-second watchdog; andPUT /v1/radio/tune/stop/LEASEstops immediately.
Tune start is rejected with 422 tx_frequency_not_allowed unless the current
frequency is inside the daemon's configured amateur-band allocations. The API
checks this before acquiring a lease, and the hardware-start callback checks it
again immediately before starting the carrier. This is a coarse safety guard,
not a substitute for the operator's license-class, subband, mode, or geographic
requirements.
The transmit-enabled daemon also accepts PUT /v1/radio/tx-drive/PERCENT
(1–100) and PUT /v1/radio/mic-gain/DB (0–70 dB) while unkeyed. General
voice/data TX defaults to 100% drive and 10 dB microphone gain. On this 5 W
QRP radio, the full-drive default is an intentional operator policy choice.
These configure the immutable profile
captured by the next physical PTT press; neither route keys the transmitter.
Frequency, mode, drive, and microphone-gain changes return 409 Conflict while
any TX owner is active. Filter-bandwidth, compressor, and maximum-key-timeout
changes are rejected as well. No rejected setting is staged: unkey, apply the
change, and key again. Receive squelch remains adjustable because it is
host-only and cannot affect RF or transmitted samples.
An optional shared speech compressor is disabled by default and may be changed only while unkeyed:
PUT /v1/radio/tx-compressor/on HTTP/1.1Use off to disable it. The initial implementation uses a conservative static
3:1 curve above -12 dBFS. The final complex-I/Q limiter is always enabled and
caps output at the magnitude selected by TX drive; it cannot be disabled.
Drive is an I/Q-amplitude percentage, not calibrated RF wattage. Every general
voice/data source is capped at 100% (24,890-count complex magnitude), including
raw network I/Q. The dedicated Tune operation remains the separately validated,
fixed full-drive carrier.
GET /v1/radio reports tx_compressor_enabled, while GET /v1/status reports
meter values and limiter activity. Physical-microphone gain remains a source-
specific control. Future HTTP and Soapy audio sources should have separate
source gains and then enter this same compressor, limiter, and meter path.
Physical microphone TX additionally requires AM, USB, or LSB and a known frequency inside the daemon's conservative U.S. amateur voice-allocation policy: the 160, 80, 40, 20, 17, 15, 12, 10, or 6 meter allocation, or the permitted 60-meter USB spectrum/carriers. AM requires 3 kHz of allocation on each side of the carrier and is not enabled on 60 meters. This guard is not a substitute for the control operator's license-class, subband, emission, power, and geographic obligations. The operator remains responsible for legal operation.
The shared maximum-transmit timer is configured with:
PUT /v1/radio/tx-timeout/180 HTTP/1.1It defaults to 180 seconds and accepts 30 through 1800 seconds. Zero is
rejected, so the safety timer cannot be disabled. Changes while any TX owner is
active return 409 Conflict; the API/Soapy control connection remains
connected. GET /v1/radio reports the current value as
tx_timeout_seconds, and automatic USB recovery preserves it.
Tune has an independent 60-second hard limit. Expiry, shutdown, USB failure,
or a partial startup failure invokes transition mute, SET_TR(0), receive-
frequency restoration, PA-filter reset, and endpoint-stream cancellation.
The lease is a safety/ownership mechanism, not access control. API
authentication remains a separate project item. Until then,
firewall access to the API must be limited to trusted station-control hosts.
SoapySDR does not expose the dedicated Tune operation. Its optional TX channel
uses the general leased raw-I/Q routes described below.
The transmit-enabled daemon implements leased general network-transmit session,
PTT, and sample-upload routes for mono s16le audio and cs16le I/Q. They use
a 500 ms prebuffer, 15-second lease watchdog, one-second keyed-data watchdog,
the shared maximum-key timer, mandatory output limiting, and disconnect unkey.
The route contract and SoapySDR mapping are specified in
General TX/PTT API design. These routes have offline
test coverage; USB PCM audio and raw I/Q have also completed bounded live
dummy-load tests. Individual modes, bands, and applications still require the
validation tracked in the engineering checklist. Operational responsibilities,
supported paths, limitations, and normal and emergency unkey procedures are in
the transmit operator guide.
The browser test page uses POST /v1/tx/audio for bounded mono s16le PCM
blocks under the same TX lease. The first block attaches the logical stream;
each accepted block refreshes the one-second sample-data watchdog. Bodies must
contain an even number of bytes and are limited to 9,600 bytes (100 ms at
48 kHz). This route accepts only an audio-source session and does not replace
the persistent CONNECT tunnel intended for native clients.
Browser microphone capture is enabled only in a secure browser context.
http://localhost:15000/test qualifies on the daemon host; a page opened from
another LAN computer normally requires future HTTPS support. Tune and ordinary
radio controls do not require microphone permission.
GET /v1/stream/iq HTTP/1.1The offline server returns 503 Service Unavailable. The live RX server returns
200 OK and publishes the tested binary framing format.
The permission-gated live mode will return 200 OK with
application/octet-stream and stream frames until the client disconnects. It
supports one IQ client at a time.
All multibyte header integers are big-endian. Float payload values are IEEE-754 little-endian.
| Offset | Size | Meaning |
|---|---|---|
| 0 | 4 | ASCII magic F15I |
| 4 | 1 | Protocol version: 1 |
| 5 | 1 | Format: 1 = complex float32 little-endian |
| 6 | 2 | Header size: 20 |
| 8 | 4 | Monotonic frame sequence |
| 12 | 4 | Complex sample rate: 48,000 |
| 16 | 4 | Complex sample count |
| 20 | variable | Repeated float32 I, float32 Q pairs |
The sequence number lets clients detect a lost application frame independently of USB packet counters. The sample count lets clients skip or buffer frames without assuming a fixed network chunk size.
Unknown paths return 404 JSON. General PTT/TX routes exist only in the
explicitly transmit-enabled daemon; receive-only modes keep them unavailable.
No firmware, EEPROM, antenna, or direct PA-filter route exists. The standalone
TX probes have no daemon call path.
The saved raw capture can be passed through the same DC blocker, IQ ring, and network-frame publisher without opening USB or a socket:
./build/flex1500d --frame-capture captures/rx-settled.iq16le /tmp/rx.f15iThe output is a concatenation of the binary IQ frames described above. The command uses exclusive creation and refuses to overwrite an existing file.
- Status JSON and HTTP response headers are byte-tested.
- IQ frame headers, integer byte order, and float byte order are byte-tested.
- The inactive IQ route is tested for
503. - HTTP bytes are tested through an in-process Unix socket pair where permitted.
- The default receive-only build passes 19 tests. Enabling the separately guarded TX-research build adds five offline/interlock tests for a total of 24. Coverage includes rejection of unarmed radio modes, partial HTTP-header detection, radio metadata, publisher counters, frequency parsing/filter boundaries, offline tune rejection, receive-mode TX rejection, and—only in the research build—isolated TX-probe arming strings.
Date: 2026-08-28
An offline loopback integration check on port 15001 confirmed:
/v1/statusreturned all USB, ring, and network publisher counters./v1/radioreturned modelFLEX-1500, firmware0.5.3.24, USB IDs2192:1502,receive_only: true, andtransmit_enabled: false.POST /v1/radio/pttreturned404 Not Found.- The server stopped cleanly with
SIGINT.
No USB or radio access occurred during this validation.
Date: 2026-08-29
KB1JDX approved starting the separately armed receive-only tuning daemon and a
single tune to the previously validated 10.000 MHz WWV frequency. The daemon
sent INITIALIZE; the API then successfully sent SET_RX1_FREQ_TW(10000000)
and SET_RX1_FILTER(5). The tune response was HTTP 200, and /v1/radio
reported 10,000,000 Hz, filter 5, receive_only: true, and
transmit_enabled: false.
tools/capture_network_iq.py discarded 12,000 settling samples and captured
exactly 240,000 sequence-continuous samples from 987 F15I frames. The raw
capture is captures/rx-10mhz-5s.iq16le. Offline AM demodulation produced
captures/rx-10mhz-5s-am.wav: mono PCM16, 48 kHz, exactly 240,000 frames and
5.000 seconds.
Offline analysis of the retained network-derived capture finds no literal
(-1,-1) pairs, but this is not proof that no source sentinel occurred: the
network stream is DC-corrected complex float, so converting it back to
iq_s16le does not preserve the raw sentinel signature. Across the daemon's
longer lifetime, status reported 51,325 completed USB packets, 67 packet-status
errors, 6 raw sentinel frames, one successful tune operation, and zero
radio-command errors. The bounded application stream had continuous sequence
numbers.
At 48 frames per completed 192-byte packet, 51,325 packets would contain 2,463,600 frames, but the processor reported 2,463,533: a separate 67-frame shortfall. This implies that some descriptors marked completed had short payloads. The current combined counter cannot reconstruct whether the 67 packet errors were individual isochronous errors or included a whole-transfer failure, which adds 64 at once. More granular transfer-status and packet-length counters are required before another diagnostic run.
The daemon was stopped cleanly after capture. No endpoint 0x01, PA-filter,
PTT, or TX operation occurred.
Date: 2026-08-29
After the first run's combined 67-error count, the receive scheduler gained separate whole-transfer status classes, individual packet status classes, completed-packet length checks, missing/trailing-byte totals, and first/last event timing. KB1JDX approved a directly comparable receive-only run at 10 MHz with RX filter 5 and another bounded five-second IQ capture.
The comparison reported:
- 44,544 completed packets and exactly 2,138,112 processed frames
- zero whole-transfer status errors
- zero individual packet-status errors
- zero short, zero-length, or oversized completed packets
- zero missing or trailing bytes
- zero command errors and one successful RX tune
- continuous
F15Isequence numbers for the bounded capture - 24 candidate raw
(-1,-1)sentinel frames, about 11.2 ppm
Thus the previous USB loss did not recur and is consistent with a transient rather than a deterministic defect in every run. The first implementation of daemon-relative sentinel timestamps initialized its clock during stop instead of start, producing invalid multi-hour millisecond values. That instrumentation bug—and the intended initial command index—were corrected offline afterward; all 22 tests present at that stage passed. The recorded sentinel frame positions remain valid, but a future separately approved run is needed before using their timestamps.
The comparison IQ is saved as
captures/rx-10mhz-5s-diagnostic.iq16le. No TX operation occurred.
Date: 2026-08-27
The user started the offline daemon on port 15000. Read-only HTTP checks confirmed:
- The process listened only on
127.0.0.1:15000. GET /v1/statusreturned200 OK, API version 1, stateoffline_serving,radio_open: false, andnetwork_listening: true.GET /v1/stream/iqreturned the intended503 Service UnavailableJSON.- An unknown route returned the intended
404 Not FoundJSON. - Content types, content lengths, connection-close behavior, and no-store cache headers were present.
No USB or radio access occurred during this server validation.
Date: 2026-08-27
KB1JDX explicitly approved this command:
./build/flex1500d --serve-live-rx 15000 --initialize-radioThe daemon sent the one fixed opcode-1219 INITIALIZE packet, started the
continuous endpoint-0x82 receive scheduler, and listened only on
127.0.0.1:15000.
Read-only localhost validation confirmed:
/v1/statusreported statereceiving,radio_open: true, and a 48 kHz complex-float stream.- After 63,936 USB packets, the daemon reported zero USB packet errors and zero sentinel frames.
- A bounded one-second IQ client received 735,184 bytes. Its first complete
frame had magic
F15I, protocol version 1, sequence 0, sample rate 48,000, and 256 complex samples. - The client timeout intentionally truncated its last in-flight frame; the daemon continued receiving after that disconnect.
- Ring drops accumulated while no IQ client was connected. This is the intended bounded-buffer behavior: the daemon accounts for dropped new samples instead of overwriting unread data silently.
No frequency, filter, gain, routing, PTT, TX, firmware, or EEPROM command was
sent during this validation. After the checks, the daemon was stopped cleanly
with SIGINT; it exited with status 0 after cancelling its host-side IN
transfers, releasing interface 3, and closing the listener.