Multi-version Central compatibility#190
Conversation
Implemented both factory functions that construct a CentralAdapter. Implemented a workaround for fetching Central's application and protocol versions.
The translation units containing the factory functions now link with the correct Windows libraries.
Remove the example for the CentralAdapter now that it's an implementation detail of ShmemSession. Add new compatibility version detection logic to ShmemSession::Impl. Split detectCompatProtocol into two separate methods: one for inspecting Central's binary for version information and another for inspecting the shared memory as the original function did. Compatability version detection for a STANDALONE + CENTRAL_COMPAT CereLink instance is (theoretically) supported, but this logic is dead because this configuration is never created by SdkSession::create.
…+ CENTRAL_COMPAT CereLink instances This configuration (STANDALONE + CENTRAL_COMPAT) appears to only be theoretical and is never instantiated by SdkSession.
Add central types for 4.2, 4.1, and 4.0. Move CentralConfigBuffer into config_buffer.h. Place version-specific constants and structs in namespaces and remove CENTRAL_ prefixes.
Macros in cbproto collide with names in central_types/ matching those in Central's source, so switch to variables to respect namespaces. Add annotations of version differences with 'VER:'
…yCFGBUFF. These structs ensure that each version of CentralLegacyCFGBUFF maps exactly to the corresponding version of Central's configuration buffer. Structure differences between versions are annotated with VER:
Each translator file can be diffed with another to see translation differences between versions. Translations are two-way between legacy versions and the current protocol version.
Diffable adapter files for quickly seeing differences between versions. Implement primitive get/set methods for all planned legacy versions (v3.11, v4.0, v4.1, v4.2). Partially implement integration with ShmemSession. Functional v4.2 and v4.1 compatiblity. Theoretically functional v4.0 and v3.11 compatibility, but untested.
Each Central adapter has access to all raw pointer buffers used by ShmemSession. These pointers are provided to the Adapter constructor. Buffer size methods are within the BootstrapAdapter so the buffer pointers are initialized before getting passed to the Adapter constructor. All translator methods must have the translation context provided so the signatures remains the same. The translator for cbCFGBUFF has a fromLegacy translator but not a toLegacy translator. This is due to cbCFGBUFF containing information for multiple instruments which is lost upon translation, thus making reversing the operation impossible without access to the original buffer. The translators for cbPcStatus also result in loss of multi-instrument information. This issue would be resolved by combining the translators with the central adapter. Translators for the spike buffer and cache are necessary for full compatibility.
Removed the CENTRAL_COMPAT layout and replaced it with CENTRAL. Spike buffer and cache operations with the CENTRAL layout are only performed for the most recent version of Central. In the future, most of the ShmemSession implementation should be moved into the Central adapter.
Each ShmemSession operates on a specific instrument. With the CENTRAL layout, the instrument filter is always active. Multi-instrument control is possible by creating multiple SdkSession instances (one per instrument).
Add translators for cbPKT_SPK, cbSPKBUFF, and cbSPKCACHE. Implement getSpikeCache and getRecentSpike for ShmemSession with layout == CENTRAL.
Rework how Blackrock .cmp channel-map files are applied to a device's
in-memory channel config.
Matching: rows are now matched to live channels by the device's own
(bank, term) read from chaninfo, instead of by an ordinal channel ID
(the old sort-and-assign start_chan+N scheme). start_chan shifts the
CMP's bank letters by start_chan/32 banks to target a headstage's banks
(129 -> +4 -> bank A maps to device bank E), which makes (bank, term) a
globally-unique join key. This is robust to incomplete CMPs.
position[]: the four cbPKT_CHANINFO.position slots now carry real
geometry {x, y, size, headstage_id} instead of {col, row, bank, elec}.
Columns: the //-comment header line immediately before the data (when
present) is the ground truth for column order, matched by name
(col/c, row/r, bank/b, elec/e, size/s, label/l); no header falls back
to the legacy "col row bank electrode [label]" order, so existing files
parse unchanged.
Units: with no size column and a unit-spaced index grid (every non-zero
col/row delta is exactly 1), values are electrode indices -> size 1 and
x/y/size scaled by the 400 um Utah-array pitch. With a size column, or
non-uniform spacing, values are taken at face value.
Labels: stored verbatim; the hs{N}- prefix is dropped since the stored
headstage id disambiguates reused labels.
pycbsdk/cmp.py is kept in sync with the C++ parser; session.py
docstrings corrected. C++ and Python unit tests updated; the
hardware-gated nplay integration tests join device readback to parser
output via ChanInfoField.BANK/TERM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng, because some arrays will have gaps in their grid and therefore have >1 spacing in some dimensions.
Common methods for all Adapter methods are tested. All copyArr and copyArr2D methods from base.h are tested. detectCentralVersion does not have unit tests and must be tested manually.
|
pycbsdk-windows-x64.zip |
|
I couldn't get this to work on my lab setup running a LEGACY_NSP with firmware 7.6.0 and Central 7.6.1. |
Label strings were previously returned as const char* types. With multi-version-central-compat, the API for fetching certain configuration structs was changed to return copies instead of pointers. This resulted in the getter methods for labels (const char*) pointing to objects allocated on the stack instead of shared memory, so the pointers were left dangling after the getter returned. This fix requires callers of cbsdk to provide an allocated char buffer in order to fetch a label string.
Fields m_nNspStatus, m_nNumNTrodesPerInstrument, and m_nGeminiSystem are now calculated according to Central's 7.0.6 source. Versions 7.0, 7.5, 7.6, 7.7, and 7.8 have been tested with Central+NPLAY. Versions 7.6, 7.7, and 7.8 have been tested with Central+LEGACY_NSP, Central+NSP or Central+Hub1.
…ng garbage The rec ring publishes head_index and head_wrap as two separate words, and on a wrap the producer bumps head_wrap before republishing head_index. No ordering of two independent stores keeps the combined position (wrap*buflen + index) monotonic across a wrap, so the CLIENT reader could observe a torn pair (old index + new wrap). That corrupted the overrun check, dropped tail off a packet boundary, and the bad-size path then byte-scanned through garbage, delivering misinterpreted bytes to user callbacks — the intermittent "malformed packets after a buffer wrap" failure seen on Windows under load. Make the reader fail safe: - Read (head_index, head_wrap) as a seqlock-style snapshot to shrink (but not fully close) the torn-pair window. - On overrun, an implausible packet size, or a header that cannot belong to a real packet (type high byte set, or chid outside a channel / config range), resync tail to the current head and report data loss, instead of advancing by a garbage size and scanning more garbage. Because the two-word update is fundamentally non-atomic (and the Central- compatible layout forbids merging the fields), validation is the real safety net: torn snapshots and genuine overruns now drop data cleanly and never reach user callbacks. Verified against nPlayServer 7.8.0 (CLIENT data-reception integration tests pass); no change on the normal path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n copyArr2D CentralBootstrapAdapterBase is held and deleted through a std::unique_ptr<CentralBootstrapAdapterBase> but had no virtual destructor, so deleting a derived BootstrapAdapter through the base pointer was undefined behavior (-Wdelete-abstract-non-virtual-dtor). Give it a virtual default destructor, matching CentralAdapterBase. Also drop the meaningless `const` on the `void` return type of the templated copyArr2D overload (-Wignored-qualifiers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Following the fix for marker packets being expected in 1. Struct layout mismatches against CentralShared-memory structs must match Central's byte layout exactly; an incorrect field size shifts the offset of every subsequent field in the containing struct.
2. Adapter translation defects
3. Open-time validation
4. Resource management in the new version-detection code
5. Public C API
6. Test coverage
7. Nits
There are also other issues found during this code review, but things that this PR didn't touch on but still affect correctness. If you have time to tackle those within this PR, I can list them as well. Otherwise I'll open an issue. |
The 'reserved' field in v7_5 should be an array of two uint8_t instead of just one. cbPKT_SPKCACHELINECNT should be cbMAXCHANS to match the comment, but it was left as cbNUM_ANALOG_CHANS by accident.
Add the missing cbFILTDESC label. Zero out the clock sync fields in NativeConfigBuffer. Remove optiontable and colortable from NativeConfigBuffer. Legacy cbPKT_AOUT_WAVEFORM.trig maps to current trig with trigInst set to 0.
2919497 to
e3b3ff5
Compare
Fix cbPKT_CHANINFO.monsource translation for Central 7.0 and 7.5 to match CCFUtilsBinary. This fix affects the relevant Central adapters and PacketTranslator methods.
76a1bfd to
9bc6fe3
Compare
Move getMaxProcs from CentralAdapterBase to CentralBootstrapAdapterBase. Add factory makeAdapter on the bootstrap base. Add CentralAdapterArgs which bundles the 7 construction parameters.
…of Central Reject unsupported or unrecognized Centrla versions with errors instead of returning UNKNOWN. Match the name of Central's executable in a case-insentitive way.
Central constants prefixed with 'CENTRAL_'. This workaround is necessary because types.h is inherited from cbproto and contains macros which collide with constants with the same name.
Allow Cerelink to connect to older versions of Central.
Summary
Previously, CereLink could only interoperate with the single latest version of Central's shared
memory. This branch introduces a versioned compatibility layer that lets CereLink attach to
Central's shared memory across multiple protocol versions (7.0, 7.5, 7.6, 7.7, and 7.8/current),
automatically detecting the running Central version and translating its structs to and from
CereLink's native types.
The change also reworks ShmemSession into a per-instrument, layout-aware session and modernizes its
API across cbsdk and the examples/tests.
Motivation
Central exposes its configuration, status, and spike buffers through shared memory, but the binary
layout of those structs changes between protocol versions. Central's shared memory carries no usable
version field (the version field is a magic constant, and procinfo[].version's byte offset shifts
between versions), so CereLink previously assumed the newest layout and broke silently against any
other version. This work makes cross-version compatibility explicit and maintainable.
Approach
Adapter layer. Each supported protocol version has a BootstrapAdapter and Adapter. The
BootstrapAdapter reads the sizes of Central's shared-memory structs and instantiates an Adapter
holding raw pointers into each buffer. Each Adapter translates version-specific Central structs
to/from the native CereLink equivalents (cbproto + native_types.h), which are the common language
ShmemSession operates on.
across versions so they diff cleanly)
Splitting the previous monolithic central_types.h/adapters.h into per-version files makes
version-to-version differences reviewable via simple diffs, and adding a version is a documented
copy-diff-rectify procedure (see docs/multi_version_central_compat/README.md).
Version detection. New central_version.{h,cpp} detect Central's version on Windows by inspecting
Central.exe's ProductVersion (via the running-process list) and mapping the application version to a
protocol version. Detection was deliberately moved out of ShmemSession so the version is resolved
once and the correct adapter is selected at open. getCompatProtocolVersion() exposes the resolved
version to callers (returns CBPROTO_PROTOCOL_CURRENT for the NATIVE layout).
Per-instrument, layout-aware ShmemSession. The session now takes an explicit ShmemLayout (CENTRAL vs
NATIVE) and is created per instrument:
ShmemSession::create(Mode mode, ShmemLayout layout,
const std::string& name_qualifier, cbproto::InstrumentId id);
Segment names are synthesized internally from the layout and qualifier (Central's fixed well-known
names + instance suffix for CENTRAL; cbshm__ for NATIVE). Indexing always uses
packet.instrument so standalone-written packets land in the slot a later CLIENT would read.
Scope / Limitations
The compatibility layer covers Central's configuration, status, and spike buffers. The
receive/transmit buffers are still handled by version-specific logic spread across cbdev, cbproto,
and cbshm; folding these into an adapter is noted as future work in the docs.
Key Changes
central_types/v7_* headers, central_version.{h,cpp}, central_current.h
multi-instrument-safe cbPcStatus), large cbproto/types.h cleanup (macros → global constants; type
names matched to Central)
stack overflow when translating large structs; id/idx discrepancies; MSVC/Windows build fixes for
the adapters
runbook); updated shared-memory architecture doc and cbshm/cbsdk READMEs
new API and expanded
the new API
Compatibility Notes
name_qualifier, and instrument id). All in-repo callers, examples, and tests are updated.
Testing