Skip to main content

weiguo-adaptive-driver

Current release: weiguo-adaptive-driver==1.0.0. This is the new package and Python namespace for the maintained framework, based on the previous 1.3.1.dev1 source. Use from weiguo_adaptive_driver import ... throughout. The old muse-driver-framework distribution and muse_driver_framework import are retired for current source; no import alias is shipped.

python -m pip install weiguo-adaptive-driver==1.0.0

Select [http], [websocket], [mqtt], [telnet] or [ssh] only when needed. The base package remains pure Python with no mandatory third-party dependency. Four maintained driver sources and all seven templates pin the new release; existing MUSE programs change only after a separate full upload. No new control queue, discovery or deployment mechanism is introduced by this rename.

High-load MUSE capacity limits remain as recorded in POST_STRESS_ACCEPTANCE.md; this release does not claim that the 35-device/four-panel 10 Hz target passed. The older numbered feature/acceptance sections below describe the previous package's development history; installation examples use the new package.

See UDP shared-port design and acceptance for the configuration, architecture boundaries, protocol references and simulation scope. Pure UDP and TCP+UDP clients automatically share identical fixed local binds; there is no shared/exclusive configuration switch. Port 0 creates an independent OS-assigned socket per channel; different fixed binds also use separate sockets. Their existing create_udp_channel() / receive() / send() interface remains. A socket routes by resolved source IP+port, or by fixed IP when udp_strict_peer_port is false. Ambiguous overlapping source routes are rejected. Distinct managers and programs do not share listeners. Direct AsyncUdpChannel construction owns a private broker, unless explicitly given broker=...; driver overrides use super().create_udp_channel() or manager.udp_channel(...) to join their manager's broker. Per-device FIFO/latest buffers and lifecycle stay separate.

PyPI distributes only the framework library and its build/documentation metadata. The five stable and two candidate driver templates, protocol recipes and tests stay in the development checkout; they are not included in either the wheel or source distribution. Copy a template locally, then let its requirements.txt declare which framework version MUSE should install. Templates are not synchronized from PyPI.

Raw TCP TLS (1.3.0)

Use the existing AsyncTcpDeviceClient / AsyncTcpDeviceManager with tls: true. Default false preserves plaintext TCP. This option supports outbound tcp_mode: "client" only; server mode with TLS is rejected explicitly. TLS uses Python's standard library, without a new extra or template.

tls_ca_file / tls_ca_data configure private trust; tls_cert_file / tls_key_file optionally present a client certificate. Relative file paths resolve from config.json. tls_server_hostname defaults to host; set the certificate's DNS name when connecting to an IP endpoint whose certificate identifies that name. Certificate and hostname verification remain enabled. The connection deadline includes the TLS handshake; failures do not fall back to plaintext.

Framing, application login, FIFO/latest, ACK, watchdog and reconnect still use the existing TCP protocol/lifecycle. Runtime statistics report tcpTls and the active tlsVersion; TLS itself does not prove application state or device execution. The TCP part of AsyncTcpUdpDeviceClient can use the same option; its UDP channel remains unchanged. Telnet/SSH, STARTTLS and a TLS server are separate protocols or modes and are outside this addition.

See TCP TLS design and acceptance and the existing examples/scoped-device/config.tls.example.json. Stable template requirements remain 1.2.0; adopting TLS requires the exact 1.3.0 dependency after this release is published. No current business driver is upgraded.

P2 message transports (1.3.0)

AsyncWebSocketDeviceClient / AsyncWebSocketDeviceManager preserve complete text/binary messages with optional [websocket] dependencies. Protocol handshake owns receive until authenticated; then the listener starts before initial RPC queries. Paths, headers, subprotocols and verified WSS CA/client certificates are configurable; Ping/Pong alone never marks a physical device online.

AsyncMqttDeviceClient / AsyncMqttDeviceManager separate logical devices from one MqttBrokerSession per manager. A MqttTopicRouter shares subscriptions by reference count and routes to bounded per-device FIFOs. [mqtt] is optional. MQTT 3.1.1, QoS 0/1/2 and retained metadata are supported; MQTT 5 and cross-process sharing aren't included. Transport PUBACK/PUBCOMP never substitutes for a device action ACK. A new clean session and protocol-owned reads rebuild real state; pending writes aren't replayed after session loss.

The seven local templates include the new candidate websocket-device (Shelly WS plus optional OBS/HA recipes) and mqtt-device (Tasmota plus Shelly RPC). These two require the 1.3.0 message transports and their named extra; the original five and installed drivers keep public 1.2.0 pins. Read each candidate template's Chinese integration guide before editing protocol hooks. The actual WS/TLS sockets and Aedes MQTT broker tests model documented product subsets, not vendor firmware certification. See P2 acceptance.

Script-as-Device workflow

Maintain one complete local program.json and upload the whole driver folder with the native VSCode MUSE Plugin. No descriptor-generation step or second device-descriptor.json is needed. Each driver entry point exports its registered descriptor to memory through context.export.exporter.exportDescriptor(); it does not read the controller-managed program.json at runtime. This SDK glue stays outside the transport library. The workflow was verified on MUSE 1.15.45.

This entry-point change does not require a new framework or panel release. Existing drivers must receive updated entry-point source through a full driver upload; changing the installed framework alone cannot update their Python files. See the development checkout's Script-as-Device开发与上传流程.md for the current upload, testing and deployment checklist. Keep site configuration private.

The package contains the transport-independent pieces shared by the Precis Driver and Meter Validation programs:

  • asynchronous TCP connection lifecycle and reconnect handling;
  • pure UDP clients/managers with protocol-validated online state;
  • optional asynchronous HTTP/HTTPS request/response clients and managers;
  • explicit optional Telnet/SSH byte streams reusing the TCP lifecycle and queues;
  • independent TCP plus UDP lifecycle supervision for dual-transport devices;
  • a fixed-peer AsyncUdpChannel for datagram devices, without discovery or retries;
  • fixed static slot -> host:port configuration with no discovery or dynamic rebinding;
  • descriptor control registry and control type inference;
  • bounded, latest-only MeterStream frame delivery;
  • bounded, latest-only XYSeriesStream delivery for complete [x, y] frames.

XYSeriesStream accepts a variable-length array of finite pairs, including an empty array. Each frame is atomic and only the newest complete frame is kept, so a slow chart cannot create a backlog or mix points from different frames.

TCP/UDP have no third-party runtime dependencies and support Python 3.9+. HTTP devices opt into weiguo-adaptive-driver[http] (HTTPX 0.28.x); importing the framework or using TCP/UDP does not import or require HTTPX. Telnet and SSH similarly opt into [telnet] / [ssh]; raw TCP/UDP do not load telnetlib3, AsyncSSH or cryptography.

Telnet/SSH and gateway templates (1.2.0)

AsyncTelnetDeviceClient uses telnetlib3 for ongoing negotiation and preserves application bytes/whitespace after removing the Telnet line ending. It uses the normal TCP manager. Application login prompts belong in the driver's handshake.

AsyncSshDeviceClient uses AsyncSSH for an explicit shell/command byte channel. AsyncSshDeviceManager.load_config supports password or key authentication and resolves ssh_known_hosts / ssh_client_keys relative to the config file. Host-key checking remains enabled. Optional ssh_term_type / ssh_command select the device's terminal or command; no session protocol is auto-detected. The owned SSH connection closes before session_ended().

TCP-based public online follows handshake and the explicit initial command group. A required initialization ACK which is rejected therefore cannot briefly publish online. Protocols without initial ACKs retain session-ready semantics; there is no new requirement to wait for every point's feedback.

Python 3.9 uses AsyncSSH 2.21.x; Python 3.10+ uses 2.24.x. SSH also requires the tested cryptography 48.x series; Telnet uses telnetlib3 5.0.x. These are optional dependencies, with loopback validation separate from MUSE platform validation. See examples/scoped-device/session_examples.py and its session guide.

examples/gateway-device is a fifth complete template: one static endpoint, multiple configured object IDs, exact descriptor subscopes and a shared queue. See examples/协议选择与配方指南.md for framing, UDP topology, HTTP method semantics and action/absolute-target choices. No panel contract changes are required.

HTTP/HTTPS devices (1.2.0)

AsyncHttpDeviceClient and AsyncHttpDeviceManager share the existing command queues and descriptor feedback contract. Configure a fixed base_url, optional http_auth (none/basic/digest), credentials, http_headers, and an optional http_ca_file. HTTPS validates certificates normally; relative CA paths in a loaded config are relative to that file. Override create_http_client() for a device-specific SSLContext or authentication scheme.

HttpRequest(method, path, query=..., headers=..., body=...) preserves the protocol's raw body; HttpRequest.json(...) encodes JSON. Paths are relative to the configured base URL, including its path prefix. HttpResponse exposes the request, status, lowercase headers and raw body, plus json() and text(). The driver validates HTTP status and business content, then calls mark_feedback_received(). An HTTP 200 or local client creation never marks online. Initial read-only queries and polling can establish online without a pre-existing online state. request_timeout_seconds is a total deadline, including a Digest exchange. Failed writes are not replayed; local sessions discard queued writes on failure. Session cleanup uses session_ended().

See examples/http-device/ for a complete copyable driver and simulated-device tests. These APIs are included in 1.2.0, not the previously published 1.1.9 package. Test source with pip install -e '.[http]', or install the published weiguo-adaptive-driver[http]==1.0.0.

Runtime boundaries

For byte-preserving TCP framing, override read_message() with read_delimited(self.reader, b'\0') (also LF/CR/CRLF) or read_fixed(self.reader, frame_size). They return bytes, do not decode/strip, and return None only at a frame boundary EOF. Incomplete frames raise asyncio.IncompleteReadError. The legacy default line decoder is unchanged. examples/scoped-device/framing_examples.py also demonstrates a length header without adding vendor-specific length fields to the framework.

Override control_send_policy(control) to select fifo or latest by operation semantics. Defaults preserve the old mapping; relative actions or confirmed numeric values can use FIFO, while absolute targets use latest. ACK still requires FIFO. No new panel metadata is required. See send_policy_examples.py. FIFO starts at driver admission: the panel also coalesces continuous inputs. Use a discrete action/enum or per-action Command/API input for relative moves, not a continuous slider whose intermediate values may already be merged. completion_ack_example.py in each TCP/UDP template demonstrates a single in-flight command without request IDs, distinguishing ACK from completion while unsolicited feedback continues. Choose the confirmation point for the protocol; neither ACK nor completion fabricates actual point values.

examples/tcp-udp-device/subscription_example.py uses the existing initialization query and UDP-open hooks to register a UDP destination after TCP initialization and UDP bind, whichever completes first. It re-registers on TCP reconnect or UDP port change without imposing that relationship on independent UDP notifications. The udpOnline runtime statistic means the UDP channel opened, not evidence that remote feedback is current; use udpSocketOpen for socket diagnostics. The device's public online state remains protocol-specific. This is not an Art-Net output scheduler or an SSE client. WebSocket uses the separate optional message transport described above, not this TCP/UDP adapter.

Each framework import belongs to one model-specific Script-as-Device runtime. The framework does not discover devices, verify model identity, or merge devices from other driver programs. A driver connects directly to the static address configured for each enabled slot. Multiple slots in one manager must represent the same device model and protocol. The manager also rejects a factory that returns different client classes for different slots; this is a local structural guard, not a cross-brand registry or device identity check.

AsyncUdpChannel is a composable transport usable by pure UDP devices or beside a TCP control connection. The driver creates one channel inside its own manager/event loop with a fixed peer and local bind address. It accepts and returns complete datagrams and bounds the receive queue. The default queue_policy="latest" drops the oldest queued datagram when a consumer is slow; only independently disposable packets may use this policy. The optional queue_policy="fifo" retains accepted datagrams in local arrival order and rejects new ones when full, reporting BufferError on the next receive(). Accepted datagrams remain queued until consumed or the channel is closed. It does not provide discovery, device validation, retransmission, fragmentation, ordering, or acknowledgement; those behaviors remain protocol-specific. A reliable UDP protocol must implement its framing and retry rules in the driver instead of treating a datagram as a TCP reply. receive() waits for one datagram, so best-effort telemetry readers should wrap each receive in a short asyncio.wait_for() timeout and continue sending the next sample after a timeout. Do not let a missing UDP sample block the driver's TCP command/feedback loop. The timeout is a driver liveness policy, not a framework acknowledgement or retry mechanism. After a channel-level UDP error, a driver may call restart() to clear stale datagrams and reopen the same fixed peer; this does not affect the manager's TCP connection unless the driver explicitly chooses to coordinate them.

AsyncTcpUdpDeviceClient supplies the common coordination for this pattern: TCP keeps its normal command/reconnect lifecycle while a driver-owned udp_receive_loop() is supervised independently. A UDP parser failure is reported and the UDP channel is reopened without tearing down TCP. The driver still owns HiQnet or vendor-specific subscriptions, framing, ordering, and complete-frame validation.

For UDP command replies or fragments, configure udp_queue_policy="fifo" and a suitable bounded udp_queue_size. FIFO preserves local arrival order, not network delivery or network ordering. The driver still applies sequence rules, reassembles fragments and validates complete frames before offering them to MeterStream or XYSeriesStream. An overflow invalidates assumptions about completeness; recover according to the protocol rather than silently using a partial frame. An uncaught overflow in udp_receive_loop() is reported and causes only the UDP supervisor to restart its channel.

Pure UDP devices (introduced in 1.1.9)

AsyncUdpDeviceClient and AsyncUdpDeviceManager provide a complete pure UDP runtime without opening TCP connections. They share the existing bounded command queue, FIFO/latest control routing, ACK wait hooks, stream ownership, thread and cross-thread scheduler with TCP; existing TCP classes keep their public names and connection behavior. The current source version is 1.2.0; publishing and target installation are separate tasks. All five complete templates pin 1.2.0.

Copy examples/udp-device for a complete MUSE entry point, descriptor, config and independently testable protocol. Implement handle_feedback(bytes) and call mark_feedback_received() only after validating the reply. An open socket or successful send never marks UDP online. Ordinary controls are rejected while offline; initialization queries, polling and health probes can discover restored feedback. There is no complete-snapshot gate.

The pure UDP manager defaults to FIFO reception (100 datagrams), a 15-second valid-feedback timeout, no periodic probe/poll, and status queries on open. Configure protocol-appropriate probes and timeouts; a device with no query or unsolicited reply cannot prove online by opening UDP. Timeout/socket error/FIFO overflow closes the session, publishes offline, clears stale queued commands and reopens the fixed endpoint. Invalid input reported by the parser as ValueError or UnicodeError is logged and ignored without refreshing the feedback deadline. Programming errors reach the supervisor. No automatic retransmission is added.

host/port is the fixed command destination. udp_local_host/udp_local_port is the local bind (port 0 selects an ephemeral port); configure the actual port for stations sending unsolicited feedback to a fixed destination. Conflicting enabled local binds are rejected. No discovery or runtime rebinding is added.

Strict source IP+port matching remains the default. For documented protocols replying from another source port, pure UDP config may explicitly set udp_strict_peer_port: false (channel API: strict_peer_port=False). It still filters by the configured peer IP and sends to the configured destination port. This does not authenticate packets. Source resolution is part of the bounded open operation. The low-level channel still defaults to latest reception for backward compatibility; the pure UDP manager chooses FIFO explicitly.

Protocol code owns subscriptions, framing, ACK correlation, idempotence, retries and state resynchronization. Delayed bare ACKs cannot be disambiguated generically. before_disconnect() runs after session tasks stop and must not wait on their receiver. Registered complete-frame publishers live across UDP session restarts and stop with the client, as they do for TCP.

Shared runtime contracts (introduced in 1.1.9, included in 1.2.0)

These additions are transport-neutral. TCP, pure UDP and the TCP control side of TCP+UDP use the same contracts; protocol matching/retries/identity and host SDK calls remain outside the framework. The templates pin 1.2.0; existing device drivers retain their individual exact pins until explicitly upgraded.

  • async prepare_command_ack(command): for (delay, command, True) steps, called after the delay and before send. Allocate/reset the protocol waiter here; wait_for_command_ack(command) still awaits it after send. Default prepare is empty for compatibility. A prepare error prevents sending. A send/wait error or cancellation ends the session; do final cleanup below. Ordinary no-ACK steps do not call prepare. Keep encode_command() a codec.
  • async session_ended(): called once after each entered control-transport attempt, including failed open/handshake, even without a socket or an online transition. Receiver/sender tasks are stopped, sockets closed, stale queues cleared first. No I/O is available. Bounded by disconnect_timeout_seconds; exceptions/timeouts are logged, default is empty. Must tolerate partial initialization and cooperate with cancellation. before_disconnect() remains a separate, resource-available hook for sending an unsubscribe/close message. Neither hook can wait on the stopped receiver. A first stagger delay cancelled before any open attempt does not invoke the per-session hook. Independent mixed-mode UDP telemetry still uses udp_closed(); its restarts never end the TCP control session or trigger session_ended().
  • manager.submit(client, steps) -> concurrent.futures.Future[bool]: True means actual FIFO admission, False means offline/full/stopped/not running; enqueue errors appear as Future exceptions. This is not send/ACK/device execution. Use a thread-safe, nonblocking done callback or await asyncio.wrap_future(); never block the owner loop on .result(). A callback may execute immediately on the submitting thread if the Future is already complete. Cancellation does not retract commands already admitted; do not mutate submitted commands. Existing schedule() keeps its scheduling-only bool. Continuous coalescing remains schedule_latest(); submit does not silently turn text into latest.
  • String/dstring descriptor controls infer text and now use discrete FIFO, including optional ACK. Numeric/boolean/stream behavior is unchanged.

TCP handshake retains exclusive access to its reader for legacy login code. The ordinary listener starts after handshake, before startup queries, so ACK-requiring status initialization can receive replies. A handshake needing a reply must read it itself; there is no background listener during TCP handshake. For mixed clients, udp_strict_peer_port, udp_max_send_buffer_bytes and udp_open_timeout_seconds are forwarded to the independent UDP channel. Defaults remain strict IP+port, 65536 bytes and 10 seconds; telemetry defaults to latest, pure UDP defaults to FIFO. Use FIFO explicitly for fragments/events/replies. TCP online semantics are preserved; no new first-feedback gate is imposed on existing drivers. A UDP telemetry packet never proves TCP control liveness.

Optional lifecycle extensions

The original lifecycle extensions were published in 1.1.6. The 1.1.8 source candidate introduced the additional transport-resilience and host-adapter APIs described below; all are included in 1.2.0. Publication and target-runtime installation are separate steps. Older artifacts are unchanged. See examples for TCP, pure UDP, TCP+UDP and HTTP templates.

The original five copyable directories include the MUSE entry point, descriptor, configuration example, protocol module and minimal loopback tests. The TCP/UDP directories have optional ACK recipes; HTTP uses its request/response flow. The mixed template uses ordinary TCP controls plus a UDP readonly indicator; no complete template requires sibling example modules.

TCP configuration slots and ports accept integers or integer strings. Decimal JSON numbers and booleans are rejected instead of silently truncated. Zero retains the existing local-port/default-remote-port semantics.

Physical Device Online State

The following opt-in policies are available in public 1.1.6. BLU and Precis power-off/power-on availability was tested with real hardware and one local Automator panel on 2026-10-12; this does not validate every future device protocol.

  • feedback_timeout_seconds (default 0, disabled): a connection-owned watchdog closes and reconnects TCP without valid protocol replies. An open socket, junk bytes, successful writes and UDP traffic do not prove TCP is usable. The protocol parser calls mark_feedback_received() only after decoding a valid TCP reply.
  • health_probe_seconds (default 0, disabled): independent read-only probes via health_commands(), serialized through the same bounded sender. This is separate from bulk status_poll_seconds; a protocol can query one small known parameter even with bulk polling disabled. Feedback timeout must exceed probe interval. No discovery or identity verification occurs.
  • handshake_timeout_seconds (default 0, disabled) optionally bounds the whole handshake(), separate from protocol login/read-step timeouts. Precis opts into 30 seconds and BLU into 10 seconds. Legacy drivers keep their existing login behavior.
  • online is the sole physical-device availability state. Command admission no longer inspects feedback_snapshot or waits for all parameter replies. FIFO/latest-wins, offline queue rejection and automatic reconnection remain.
  • The public FeedbackSnapshot utility remains importable for compatibility with already published drivers; it is not used by Precis/BLU or by command admission, and does not impose a readiness requirement on controls.

Precis and BLU use a configurable 5-second read-only probe and 15-second valid feedback deadline. EOF/errors may detect failure earlier; silent power loss is not instantly detectable. A device without readable probes or periodic valid status must not be given a timeout that mistakes legitimate silence for failure. UDP socket-open is only a local transport condition; realtime frame freshness is checked separately. TCP recovery re-runs the driver's initialization; subscription commands remain protocol-owned, not generic vendor rules.

Expose readonly boolean health parameters in program.json, with semantic metadata .metadata.availabilityRole equal to online. Put it beside the physical device's controls (inside each same-model slot, not beside aggregate counts). ControlPanel uses the nearest annotated ancestor, shares subscriptions and applies disabling automatically. false disables only that physical scope; true restores it without a snapshot or per-point gate. No annotated online parameter means no automatic disabling. Names are arbitrary; frontend authors write no health code. The driver publishes these through its existing MUSE adapter; the framework remains independent of mojo.

Old drivers without these policies/metadata keep their previous behavior. Publishing the framework alone does not update existing descriptors or panel bridges. Use the appropriate published exact requirement, then re-Apply bindings to generate health subscriptions. Do not overwrite published versions.

Register optional realtime publishers in the client constructor, before start:

self.register_stream("meters", self.meter_stream, publish_meter_frame)
self.register_stream("positions", self.xy_stream, publish_xy_frame)

Each registered stream has one publisher. The client owns its task throughout TCP reconnections and cancels/awaits it when the client stops. Streams are not closed on stop and can be reused after a restart. Names and stream objects must be unique within a client; do not share a stream with another consumer or client. Without registration, the existing manual run() / take_frame() APIs remain unchanged. offer_frame() still only replaces one latest-frame cache, never creates a task per sample. Registration does not implement protocol assembly, subscriptions, source invalidation or epoch resets; those remain in the driver.

Publishers may be synchronous or async, but must not block the owning event loop. A publisher exception is logged and exposed in runtime_stats()["streams"][name]["lastError"]; TCP and other publishers keep running. Failed publishers are not automatically retried; restarting the client lifecycle starts them again. Explicit stream close() is terminal, unlike canceling its publisher task. Callbacks must cooperate with task cancellation.

Override public async before_disconnect(self) for optional protocol cleanup while self.writer is still available, including after a failed handshake. It is bounded by disconnect_timeout_seconds (default 1 second); socket closure has a separate timeout of the same length. Exceptions are logged and the socket is still closed. The command sender has already stopped: write/drain directly, not enqueue a command and wait for its reply. The hook must tolerate partial initialization and cooperate with cancellation. No existing driver needs to override this hook; existing private cleanup overrides remain compatible.

UDP health distinguishes udpSocketOpen from udpLastDatagramAt (monotonic milliseconds), also available as channel lastDatagramAt. Socket-open is not remote-online; even an oversized received datagram updates this raw activity timestamp. Only the driver can determine whether complete, valid device data is fresh. Timestamp and counters on a reusable channel persist through restart; the dual client retains the last timestamp when replacing a channel. The old udpOnline field retains its socket-ready meaning for compatibility. Channel stats additionally report queuePolicy and overflows; no background health poller, discovery service or cross-driver state is added.

Transport resilience (included in 1.1.8)

TCP and UDP keep separate supervisors. A UDP receiver exception or unexpected supervisor return/cancellation is recorded and retried independently; TCP sender/listener tasks are not canceled. Transport independence does not remove a protocol's legitimate dependency on TCP subscription or authentication. BLU retains its TCP session gate; other drivers can accept UDP independently of is_online.

  • udp_hook_timeout_seconds defaults to 5 seconds and must be finite/positive. It bounds each udp_opened() / udp_closed() hook. Hooks must be async, nonblocking and cooperate with cancellation; a timeout cannot preempt blocking Python code or a coroutine that suppresses cancellation indefinitely.
  • udp_state_changed() is synchronous and nonblocking. Callback exceptions are recorded without killing UDP supervision or changing physical online. Callback delivery failure remains an observable error, not a device-offline decision. udpOnline keeps its historical socket-ready meaning.
  • UDP retry delay stays fixed and configurable. Nonfinite delays are rejected; a configured zero has a 5 ms minimum scheduling delay to avoid a failure spin. This is transport recovery, not UDP packet retransmission or protocol retry.
  • runtime_stats() additionally exposes udpSupervisorRunning and recoveryPending; stats belong only to this device instance.

Use await self.enqueue_recovery("subscriptions", commands, timeout_seconds=1) from an async protocol hook to request TCP recovery commands. The key is scoped to this device and only one request with that key may be waiting, queued or sending. Duplicate callers share the admission result. Queue admission is bounded by the caller's deadline, preserves ordinary sender FIFO, and returns false when full/offline. True means local queue admission, never a hardware ACK or successful re-subscription. Protocols decide how to respond to rejection; BLU lets the UDP supervisor retry its recovery hook. Commands are still generated by the protocol, not by the framework.

Cancellation or a TCP session change discards a waiting recovery request. Disconnect clears queued recovery markers, with no replay into a new session. Duplicate-caller cancellation does not cancel the owning request. Keys and queued requests are capacity-bounded; there is no unbounded background retry queue or cross-driver connection pool. Existing enqueue() and latest-wins control behavior are unchanged. Re-run initialization after a TCP reconnect according to the protocol; the framework does not invent subscriptions.

These additions were tested using local loopback sockets, fault injection and 50 independent asyncio-loop/thread driver runtimes. The current release preparation does not install into Automator, upload to MUSE or retest hardware. Manager shutdown cancels device-owner tasks first and allows 250 ms for orderly cleanup before canceling residual tasks. The fallback also handles legacy receivers that swallow an initial cancellation during a receive timeout race. New BLU source requires enqueue_recovery and refuses to start with old framework code. Its exact requirement has now been explicitly upgraded to the published 1.2.0. Previously published versions remain unchanged; this documentation update does not rebuild or replace the published 1.2.0 artifacts.

The manager constructor accepts the normalized device list, not the config root: pass config[entries_key] and config["connection_stagger_seconds"]. Root objects, malformed entries and duplicate slots now fail with a useful error before any client is created. This does not alter existing JSON formats.

Runtime use in MUSE

Each Script-as-Device program declares the exact framework version in its own requirements.txt:

weiguo-adaptive-driver==1.0.0

The program imports the package, never a copied local driver_framework.py:

from weiguo_adaptive_driver import AsyncTcpUdpDeviceClient, AsyncTcpDeviceManager

MUSE must install that exact dependency, through the public index when reachable or a matching prepared offline wheel bundle. Keep the version exact so a framework release cannot silently change an existing driver.

Local verification

Build and install the wheel into an isolated environment before testing a driver. The driver directories should not contain a local framework copy.

To test unpublished source changes, run python3 -m unittest discover -s tests -v here, or sh ../local-validation/test-source.sh. First install requirements-dev.txt in the testing environment to exercise HTTP as well; without HTTPX, the optional HTTP I/O tests are explicitly skipped. The framework suite includes line and length-prefixed TCP framing, dual transport failure isolation, real-socket FIFO overflow and fragment retention, cleanup timeouts, publisher failure isolation, complete Meter/XY-series frame recovery, and 50 mixed-protocol runtimes repeated through start/stop. Separate tests host realtime streams in 50 independent managers and in 50 clients on one loop. The parent regression runner uses PYTHONPATH for source testing only; MUSE still installs the pinned public version, not these local edits. Tests and builds do not publish or deploy code.

Numeric commands and configuration timers reject nonfinite values before clamping or scheduling. Realtime Meter/XY-series descriptors are read-only and cannot declare both delivery types on one parameter. Driver entry points should call manager.stop() from their normal stop/exit cleanup.

TCP devices use a bounded StreamReader limit (tcp_read_limit_bytes, default 65536). This protects line-oriented drivers from an unbounded malformed frame; binary or length-prefixed protocols must override read_message() and perform their own complete-frame validation. A read-limit error is a connection error, so the normal driver reconnect lifecycle handles it.

Connection stability options are per device and optional. Existing configs keep the historical fixed reconnect delay by default. A driver may add connect_timeout_seconds, reconnect_backoff_factor, reconnect_max_delay_seconds, and reconnect_jitter_seconds to reduce a reconnect storm without changing its static slot mapping. The manager exposes runtime_stats() for low-rate health reporting; it is not a discovery API.

The TCP manager owns one event loop per model-specific driver runtime. A UDP channel used by that driver must be created, opened, consumed, and closed on the same loop. Independent driver programs do not share channels or lifecycle state, even when their static addresses belong to the same LAN.

Performance and ownership

The control registry compiles path, protocol-channel and feedback-route indexes once at construction. Treat the descriptor and registered controls as a startup snapshot; create a new registry after changing their mapping. Lookup results do not expose mutable internal sets or path lists.

Continuous commands are also coalesced by device/key before crossing into the TCP event loop, so a burst does not create one scheduled task per intermediate value. Pending keys per device are bounded by command_queue_size; stopping discards them. FIFO commands are not coalesced, and new continuous keys retain their scheduling positions relative to FIFO requests.

Meter and XY-series caches own their accepted frames. snapshot() and take_frame() capture a complete frame under the producer lock, then copy it outside the lock; callers still receive independent mutable arrays. This reduces producer contention without removing validation or changing the wire format.

run(publish) includes copying and synchronous/asynchronous publishing time in the rate-limit period, rather than sleeping an extra full period after publishing. Slow callbacks still reduce achieved throughput. Missed periods are not replayed, and idle streams wait instead of spinning. These are limits, not a promise that every configuration or MUSE host sustains the declared rate.

The parent project's local-validation/benchmark-driver.py measures source CPU costs; local-validation/stress-driver.py tests 50 loopback TCP processors and multiple complete telemetry streams with a mock SDK. Neither tests actual MUSE SDK transport or physical devices. Unpublished source changes require a new framework release and updated pinned driver requirements before deployment.

1.1.8: Explicit Feedback Scopes

Version 1.1.8 introduces an intentional API migration beyond published 1.1.7. ProgramControlRegistry.paths_for(slot, type, channel) is removed. No Precis- specific compatibility entry is retained. The current templates pin 1.2.0. Publish its prepared artifacts before uploading these templates; editing source does not publish a package. Previously published versions remain unchanged.

registry = ProgramControlRegistry(program_descriptor)
device = registry.for_scope("/rack/units/0")
paths = device.paths_for("slider", 7)
channels = device.protocol_channels({"slider", "toggle"})

Scopes are explicit descriptor container paths, not MUSE network scopes. / means the entire descriptor (appropriate for a flat single-device driver). Arrays use zero-based indexes; arbitrary nested objects and device names work. Unknown containers fail with ValueError rather than silently selecting another device. Queries and feedback aliases are indexed once at initialization; all aliases with the same type/channel inside a scope are returned. The root view intentionally spans all devices; choose a single-device subtree for isolation. Shared protocol numbers across device scopes are allowed. If a protocol reuses numbers inside a device (for example across banks), select that bank's subtree or implement its compound address mapping in the driver. No discovery or automatic device-boundary inference is performed.

DescriptorFeedbackPublisher(registry, update) injects a synchronous host callback without importing mojo. publish(path, value) resolves a declared ordinary parameter and converts its descriptor path to the host export path; publish_route(scope, type, channel, value) fans out to matching aliases and returns their count (zero for unknown protocol points). The driver owns value decoding and range conversion. No optimistic command echo, deduplication, new queue, or readiness gate is added. Callback exceptions propagate to the caller; callbacks run on the caller's thread and must not block. This does not prove SDK thread marshalling. Meter/XY-series scalar publishing is rejected: use their complete latest-frame APIs instead. Their epochs, sequence rules and rate limits are unchanged.

Precis selects /processors/<slot> explicitly; the independent BLU-806 uses / for its flat matrix controls and keeps protocol-specific object/SVID mapping. Meter Validation remains transport-free synthetic telemetry and needs no fake TCP client. Its ordinary status/manifest parameters now use the host adapter; its complete real-time frames still use stream events. None of these use a native HiQnet Provider.

Raw TCP input/output with traffic_debug now uses the shared DriverLogger at DEBUG instead of INFO; connection changes remain INFO, errors remain WARN/ERROR. Explicit raw feedback events still follow the existing traffic_debug opt-in behavior. UDP parsers can use the same logger; no per-datagram log is mandatory. Never enable raw logging around credentials without reviewing protocol payloads.

The copyable examples/scoped-device/ separates a thin MUSE entry point from an SDK-independent invented protocol and includes descriptor/config examples. Its tests use two loopback simulators, not physical hardware. Existing TCP/UDP examples remain useful as protocol illustrations; they are not universal drivers.

1.1.8: Host Adapter, Logging and Development Types

weiguo_adaptive_driver.muse.MuseFeedbackAdapter is a separate MUSE adaptation layer. Inject context.export; the module itself never imports mojo. The protocol layer can continue to use an ordinary DescriptorFeedbackPublisher with any synchronous callback. Precis, BLU-806 and Meter Validation now route ordinary parameters through the adapter instead of calling export.update throughout their protocol code. Native event dispatch and complete telemetry frames remain separate; neither goes through scalar publishing.

feedback = MuseFeedbackAdapter(registry, context.export)
feedback.publish("/units/0/volume", decoded_volume)
feedback.publish_many({"/units/0/name": device_name})
feedback.publish_online(("/units/0/online",), client.is_online)

The driver supplies exact paths; the adapter does not guess device boundaries, choose another slot, infer power/network failures or wait for complete feedback. publish_online requires boolean values and declared boolean target parameters. publish_many preflights all paths before any host call, but is not an atomic host transaction: callback failure can leave preceding fields published. Repeated confirmed values are always forwarded. No framework value cache or deduplication is introduced; protocol-specific decoded state stays in the driver. Declared async callbacks are rejected. All ordinary callbacks are synchronous, run on the caller's thread, and must be nonblocking. SDK thread behavior still requires target-runtime validation.

DriverLogger.wrap(host_log) accepts MUSE-style warn or standard warning. It is idempotent. TRACE falls back only to DEBUG, and DEBUG only to TRACE; if neither exists, raw logs are omitted, never promoted to INFO. raw_enabled reports method availability, not the host's active level filter. ERROR falls back to WARN only when ERROR is absent. INFO plus WARN/WARNING are required. The adapter does not create another log queue or retain payloads. Explicit raw feedback events remain opt-in and are not automatically redacted.

Development contracts include Logger, HostExport, FeedbackUpdate, FramePublisher, and CommandStep. Public descriptor/scope, core TCP entry points, UDP I/O and stream APIs have annotations, and py.typed is package data. Command sequences retain both two-item steps and three-item ACK steps. Raw JSON descriptors/configuration and frames retain dynamic fields; protocol validation is still necessary. This is not a claim that all legacy transport internals are strict-typed.

From this framework directory, use a development virtual environment:

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/python ../local-validation/typecheck-driver.py

The checker strictly checks contracts, logger, feedback publisher, MUSE adapter, the minimal example protocol and a positive API fixture. It also requires four intentional API mistakes to fail. Existing transport modules expose their new annotations to callers but their internal type errors are excluded explicitly in the configuration. No MUSE SDK stubs are copied into runtime drivers. To include type checking in the full source runner, set MUSE_TYPECHECK=1 and PYTHON to that development environment's Python.

These APIs originated in 1.1.8 and are included in 1.2.0. The current release is prepared locally; publication is a separate operation. All five templates pin 1.2.0. Publish that version before deploying a template that requires it.

可选的设备间隔与正常拒绝

config.example.json 的 min_command_interval_seconds 默认 0;按设备手册填写。 它约束该客户端受管命令之间的最小空隙:TCP/UDP 从本地发送完成计时,HTTP 从收到响应后计时;ACK 等待和步骤 delay 会抵扣剩余时间。 latest_command_interval_seconds 仍只是同一连续控件的更新间隔。初始化查询经过发送组;在 handshake 里直接写 writer 的报文由协议自行安排。

CommandRejected 表示设备合法拒绝当前命令。发送队列会停止本组后续步骤,继续下一组,保留会话,不重发或伪造目标值。 HTTP 解析可以直接抛出它;TCP/UDP 接收器应完成对应 ACK Future 的异常,由 sender 的等待函数抛出。 首次初始化失败仍沿初始化失败处理;它不是入队结果或执行成功回调。参见 ACK/Completion 配方。

1.3.1.dev1 post-stress prerelease

The source candidate adds control_send_key(control) as the queue-identity hook. Its default preserves protocol-channel aliases. The gateway template prefixes the logical object address, so two objects can both use slider/channel 1 on one socket without overwriting each other's target.

MuseFeedbackAdapter(..., deduplicate=True) opts into changed ordinary-state exports. Explicit feedbackIntervalSeconds parameter metadata additionally coalesces trailing state on the owning asyncio loop. A forced actual feedback publication can use publish(path, value, force=True). Protocol ACK/events and health still process every valid reply. DescriptorFeedbackPublisher and legacy adapter callers retain repeated delivery. bind_scope(scope, online_path) resets only that device's export cache on offline; close() discards delayed old state. No polling, command echo or per-device publishing thread is introduced.

The seven source templates now select this public prerelease and include lifecycle wiring. Existing business driver pins remain unchanged. See template feedback instructions and post-stress evidence. Do not treat source tests or local replay as successful MUSE installation of the candidate.

MQTT on Python 3.10: the [mqtt] extra explicitly supplies typing-extensions for Python <3.11. aiomqtt 2.4.0 imports Self from that package on 3.10, while its upstream wheel only declares the dependency for Python <3.10. A clean MUSE 3.10 installation exposed this packaging gap; the fix does not add a base dependency.

Metadata

Release files for weiguo-adaptive-driver 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for weiguo-adaptive-driver 1.0.0
File Size Uploaded Provenance Actions
weiguo_adaptive_driver-1.0.0.tar.gz 131.7 kB Without provenance Details for weiguo_adaptive_driver-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for weiguo-adaptive-driver 1.0.0
File Interpreter ABI Platform Actions
weiguo_adaptive_driver-1.0.0-py3-none-any.whl Python 3 none any Details for weiguo_adaptive_driver-1.0.0-py3-none-any.whl

Total release size: 217.2 kB

Release files / weiguo_adaptive_driver-1.0.0.tar.gz

Download URL weiguo_adaptive_driver-1.0.0.tar.gz
Size 131.7 kB
Tags Source
SHA-256 checksum
How to use checksums
a481b0da7c741a64b683e42cf0ef9cd90a6667ad13267dbf5bdaf86e5eb91dc6
BLAKE2b-256 checksum
How to use checksums
9fbaa682b85b98ec23da779c42c1da3eac0344e9b42514780845d41cd3dd0b98
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.7

Release files / weiguo_adaptive_driver-1.0.0-py3-none-any.whl

Download URL weiguo_adaptive_driver-1.0.0-py3-none-any.whl
Size 85.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
44b71a9e6f848a7827a1d33f102feab77a1fbdc326402985b00fe5fec91f7057
BLAKE2b-256 checksum
How to use checksums
3537de7a0768bd2e08a15895121a249b912eb977ec3abd49a50d1cdd8d8d66c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.7

Security for weiguo-adaptive-driver 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page