All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Note
Two different 0.4.0 entries once existed. master and
feat/v0.4.0-foundation forked at v0.3.0 and each described its own work
under the same version numbers. Neither was ever tagged — v0.3.0 is the last
release — so the 0.4.0 below is the only one, and the feature-surface entries
that used to claim 0.4.0/0.5.0 on master are preserved on
legacy/v0.5.0-feature-surface. Those features return one milestone at a
time, ported onto the measurement envelope rather than merged onto it.
-
masterreconciled with the foundation branch. The foundation became the mainline;master's feature surface (LTE RX/EBLER, WLAN AP signaling, WLAN throughput/DAU, BLE signaling PER, coex, IMD planner, bench profiles, test-plan engine) is preserved onlegacy/v0.5.0-feature-surfaceand returns per milestone #41–#46. Tool count 131 → 84 until that work lands. -
CI now triggers on
feat/**/fix/**pushes and no longer filterspull_requestby base branch. Apull_requestrun needs a buildable merge commit, so a branch that conflicts with its base was silently unverified — which is how the items below survived. Test matrix gains windows-latest / 3.11, the platform the instrument is driven from. -
The SCPI transport, connection registry, safety checks and simulator engine now come from
scpi-core. They were written here and were being copy-pasted into the sibling R&S servers, where the copies diverged -- two of the three could silently return a stale reading after a read timeout. Moving the code out rather than copying it again means there is one implementation of the atomicity and desync guarantees for every server to rely on. No behaviour here changes: the same 686 tests pass untouched.- The dependency resolves from git, not a path. A path dependency only works when both repos happen to sit side by side, so a single clone of this repo did not install -- precisely the situation someone hits on a bench machine with an instrument in front of them.
driver/scpi_socket.pyanddriver/lock.pyare deleted;SCPISocket,IdempotencyandTaskReentrantLockare imported fromscpi_coreand remain importable fromrs_cmw500_mcp.driver.exceptions.pyis a re-export shim overscpi_core.exceptions.CMW500Erroris now an alias ofInstrumentError-- the same class, so an error raised inside the shared transport is caught by every existingexcept CMW500Error.MeasurementErrorandSignalingErrorstay here: a signal generator has no measurement or signaling plane.safety/validators.pykeepssanitize_scpi_paramandvalidate_safe_pathwith their exact wording, as thin adapters overscpi_core.safety. The three servers phrase these refusals differently and each pins its own text, so the rule that fired travels as data and the message stays local.SafetyLimitsand the two-plane power validation are untouched.- The simulator's six engine modules are deleted. The CMW command surface is
now the
cmw500node map shipped with the core (327 nodes, 3 unverified -- identical to thebuild_nodes()table it replaces), andcmw-simulatoris a thin wrapper that preselects it, so every documented invocation and flag works unchanged.
-
validate_safe_pathrejects null bytes explicitly instead of relying on the OS. POSIX raises; Windows resolvesstate\x00.jsonwithout complaint, so the guard silently passed there. -
Text I/O pins
encoding="utf-8". The platform default is cp1252 on Windows, which broke reading the README and would have corrupted any saved template, state or limit set holding a non-Latin-1 character. Golden snapshots also pinnewline="\n"so regenerating on Windows does not rewrite all 84 with CRLF. -
TaskReentrantLockraisesNestedTransactionInSubtaskErrorinstead of deadlocking when a subtask of the lock holder tries to acquire. Before Python 3.12,asyncio.wait_forruns its argument in a new task, so a nested transaction blocked on a lock its own caller could not release. Ownership stays strictly per-task — granting the subtask would let two coroutines interleave a send and a read, the exact failure this transport prevents. -
The simulator accepts parameterless event commands.
SYSTem:GENerator:ALL:OFFandSYSTem:MEASurement:ALL:OFF— the emergency shutoff the bench fixture runs on teardown — drew a spurious-109 Missing parameterbecause "takes no parameter" was inferred from the leading mnemonic. It is now declared per node (Command.action). -
Simulator dry-run findings go to a temp dir, not
bench-results/, which the runbook tells the next person to commit and trust. -
.mcp.jsonno longer hardcodes another machine's path;serverInfo.versionreports the package version rather than the MCP SDK's; and the bench runbook no longer namescmw_rf_route_set, a tool that never existed. A test now requires everycmw_*name in the docs to be a registered tool. -
An idle connection is no longer left generating. The connection pool is now
scpi_core.ConnectionRegistrywith an idle TTL and an eviction hook, and the hook runs the same safe-state sequence ascmw_system_all_off(SYSTem:GENerator:ALL:OFF,SYSTem:MEASurement:ALL:OFF) before the handle is dropped. The previous pool had no expiry and no hook at all, so a carrier switched on by one tool call radiated until the process died.This also makes
cmw_disconnectforce the safe state on the way out, which is a visible change to what it puts on the wire -- see thetests/golden/cmw_disconnect.txtdiff. Disconnecting the control session no longer leaves the instrument transmitting.
Trustworthiness release. Everything here exists to make one claim true: a number this server returns is traceable to a synchronised acquisition, a decoded reliability verdict, and a known RF path.
cmw_measure— arm a measurement, wait for the instrument to report the acquisition finished, then fetch.INITiatereturns as soon as the measurement is armed, so separate trigger and fetch calls cannot tell whether they are reading this acquisition or the previous one. Also exposes the five statistics beyondCURRent(AVERage, MAXimum, MINimum, SDEViation, EXTReme) that the instrument has always offered and this server never surfaced.cmw_get_capabilities— which technology families this instrument is licensed and fitted for. A CMW500 is a chassis; WLAN, Bluetooth, LTE measurement and LTE signaling are each separately licensed, and asking an unlicensed application to do something gets-113 Undefined header, which reads like a client bug rather than a missing licence.cmw_connectnow reports the available families, and a tool whose family is unlicensed carries a notice. Policy viaCMW_CAPABILITY_GATE(off/warn/block, defaultwarn).cmw-simulator— a shipped CMW500 SCPI simulator (console script, stdlib only). Real command-tree parsing, error queue, modelled command latency, fault injection (dropped/surplus responses, mid-conversation close), and settable reliability codes. Binds loopback only unless--allow-remote, and its*IDN?cannot be mistaken for hardware. Makes "evaluate this without a CMW500" possible for the first time.cmw_get_errors— drain and report the instrument's SCPI error queue.cmw_set_external_attenuation/cmw_get_external_attenuation— declare the cable and fixture loss so levels and measurements refer to the DUT. Previously unreachable, which meant every absolute power number was wrong by the path loss.- Golden-SCPI snapshots for all tools, a pinned tool manifest, and a config/documentation sync test.
- Measurement tools return an envelope. Values move under
values;reliability,trustworthyandsourceare new. Field 0 of every CMW response is the R&S reliability indicator, and it was previously discarded — so a measurement timeout, an overdriven front end or a dropped call arrived as a confident-looking number. Aninvalidverdict now yields no values at all, because an agent handed both a warning and a number will report the number.degradedverdicts (overdriven/underdriven/uncal) keep their values with a warning attached. CMW_ALLOW_RAW_SCPInow defaults tofalse, matching what the README,.env.exampleand this changelog have always said. The code defaulted totrue, so a guard users believed was on was off, and raw SCPI bypasses every safety clamp.CMW500Driver.get_errors()returns structuredScpiErrorobjects rather than raw strings.- LTE fetch methods return the envelope dict rather than result dataclasses.
-
Wrong SCPI, cross-checked against R&S's own instrument drivers. R&S publishes per-technology Python drivers carrying the literal SCPI string and full response structure for ~5,100 commands. Checking ours against theirs found eleven wrong commands and a systematic parsing error:
cmw_set_signal_pathwrote to a query-only node, so the one tool that claims to select the signal path did nothing — andcmw_get_signal_paththen read back the existing setting, which looks like success. The WLAN and Bluetooth routes had the same defect;cmw_set_portemitted a node that does not exist. All now use:SCENario:SALone <connector>,<converter>with a typedRFPath.- The BLE PHY was wrong in kind.
LE1M/LE2M/LECS2/LECS8written toMEValuation:BURSt:TYPEconflates two independent settings and targets a third thing entirely. PHY isISIGnal:LENergy:PHY(LE1M|LE2M|LELR); the S2/S8 coding scheme is a separate node. Getting this wrong silently measures the wrong PHY. - LTE cell bandwidth needs a direction (
:CELL:BANDwidth:PCC:DL); LTE downlink power is RS EPRE (:DL:PCC:RSEPre:LEVel) and the node used did not exist; WLAN bandwidth is:ISIGnal:BWIDth; GPRF spectrum RBW is:SPECtrum:FSWeep:RBW, and there is no detector node at all. - Bluetooth commands addressed nodes that do not exist. BT result trees
are per-PHY:
MEValuation:MODulation:NMODe:LENergy:LE1M:CURRent?, not a genericMODulation:CURRent?. Power lives underPVTime, notPOWer. And BLE has no DEVM at all -- its modulation metric is frequency deviation (ΔF1/ΔF2); DEVM is a BR/EDR measurement -- so the old BLE results were labelled with a quantity the instrument was not measuring. - Measurement field layouts were mis-indexed nearly everywhere. For most
result views index 1 is
Out_Of_Tolerance— a percentage — not the first measured value. Every WLAN EVM number this server produced was wrong (EVM is at indices 15–17 of a 22-field response). LTE ACLR was read as minus/plus pairs when it is ordered by frequency with the carrier's own power in the middle. LTE SEM has no pass flag or margin at all; the occupied bandwidth was being reported asmargin_db. WLAN spectrum flatness returns per-trace margins where a positive margin means the limit was violated -- the opposite of the usual convention -- and we had a boolean read from the wrong slot, so it was both mis-indexed and inverted.
-
Measurements were never synchronised.
*OPC?answers "has the command queue drained", which is true immediately afterINITiate. Completion is now polled on the application's ownSTATenode, which has eight values (ACTive,ADJusted,INValid,OFF,PENDing,QUEued,RDY,RUN) —PENDingandQUEuedare exactly what a fast poll lands on. Continuous repetition never reaches a finished state and is refused with an actionable message rather than timing out and fetching anyway. -
Crossed responses.
send()held the transport lock andread_response()did not, so two concurrent tool calls on one pooled connection could each receive the other's answer — silently, with plausible values. -
Desynchronised connections. A read timeout does not end the exchange; the instrument may answer later, permanently offsetting the stream. A timeout now poisons the connection until
resync()proves it is both aligned and empty. -
Silent command rejection. A SCPI write returns nothing, so a malformed command produced
{"status": "ok"}. The error queue is now drained per transaction (one query per group, not per command). -
Single-plane safety clamps. With external attenuation declared, the CMW interprets levels at the DUT plane — so a request for −10 dBm through 20 dB of declared loss meant +10 dBm at the connector, and an over-declared attenuation could expose the analyser front end. Both planes are now checked.
-
lte_meas_fetch_allsilently omitted frequency error. -
Measurement fetches no longer swallow transport errors into a zero-valued result, which was indistinguishable from a genuine reading of zero.
-
mypy runs without
|| truein CI; the 45 errors it had been hiding are fixed. -
mcpdependency pinned below 2.0. -
Version drift between
__init__.py(0.2.0) andpyproject.toml.
Two items still need hardware or a datasheet, tracked as scpi-unverified:
- Instrument-plane power limits for the specific front-end variant (#29). Defaults are deliberately conservative placeholders.
- Option-code to family mapping (#35) is inferred from R&S naming
convention, which is why the capability gate defaults to
warn. Also confirm whetherSYSTem:BASE:OPTion:LIST?exists on this firmware — it appears in no vendor driver, and*OPT?is used in preference. Also worth knowing: frequency-dependent correction tables are a CMWS feature, not a CMW500 one. On this instrument de-embedding is a single scalar per direction per application, so a frequency-varying cable cannot be corrected instrument-side across a sweep.
- License: Apache-2.0 → AGPL-3.0-or-later. Aligns with the eng-mcp-suite toolkit-wide AGPL move. R&S hardware and proprietary client software are independent of this wrapper.
- WLAN non-signaling support -- 11 new MCP tools for 802.11a/b/g/n/ac/ax testing
- TX power, EVM, spectrum flatness, and frequency error measurements
- Configurable standard, bandwidth (20/40/80/160 MHz), frequency, and expected power
- Multi-instance measurement support (MEAS1-MEASn)
- Bluetooth/BLE non-signaling support -- 11 new MCP tools
- Classic Bluetooth: DH1/DH3/DH5/DM1/DM3/DM5 packet types
- BLE: 1M, 2M, Coded S2, Coded S8 PHY modes
- TX power, modulation (DEVM), and frequency offset/drift measurements
- Advanced GPRF tools -- 6 new tools for trigger configuration, power filters, baseband mode, RF port selection, user margin, and system-wide off
- Measurement templates -- 4 new pre-built configs
ble_tx-- BLE TX measurement (1M/2M/Coded S2 presets)ble_rx-- BLE RX sensitivity via GPRF generatorbt_classic_tx-- Bluetooth Classic TX (DH1/DH5 presets)wlan_rx-- WLAN RX sensitivity via GPRF generator (Wi-Fi 6 presets)
- Tool registry architecture -- modular tool system replacing monolithic dispatch
- Each technology in its own module with auto-registration
- Centralized error handling with proper
isErrorpropagation
- CI/CD pipeline -- GitHub Actions with Python 3.10/3.11/3.12 matrix
- Lint (ruff check), format (ruff format), test (pytest + coverage), typecheck (mypy)
- Data models -- typed enums, config dataclasses, and result dataclasses for WLAN and Bluetooth
- SCPI input sanitization -- protection against injection attacks
- Raw SCPI guard --
CMW_ALLOW_RAW_SCPIsetting (default: disabled) - Asyncio locks -- connection pool, template, and measurement locks for concurrent safety
- Integration test marker --
@pytest.mark.integrationfor hardware-dependent tests
- Tool count increased from 51 to 79 tools
- Test count increased from 248 to 373 tests
- Source code grew from ~3,500 to 7,087 lines
lte_meas_configure()-- de-stubbed with real SCPI (stat count, repetition)lte_configure_bearer()-- de-stubbed with real SCPI (APN, IP version)meas_configure_spectrum()-- de-stubbed with real SCPI (center freq, span, RBW, detector)wlan_txtemplate -- now uses native WLAN SCPI subsystem instead of GPRF workaround- Version bumped to 0.2.0
- Decomposed
tools.py(1,621 lines) intotools/package (10 modules) - Added
tools/registry.pywithToolRegistrysingleton pattern - Added
tools/shared.pywith connection pooling, locks, and shared helpers - Each technology module (connection, gprf, lte, wlan, bluetooth, scpi) self-registers tools at import time
- Initial release
- GPRF generator control (frequency, level, ARB waveforms)
- GPRF analyzer measurements (power, spectrum)
- LTE signaling mode (cell configuration, connection management)
- LTE TX measurements (power, EVM, ACLR, SEM)
- Safety limits system
- State save/restore
- Pass/fail limit checking
- 3 measurement templates (LTE TX, GPRF power, non-signaling RX)