# Zenoh > Documentation for Zenoh --- # Docs · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/ # Source: https://zenoh.io/docs/ ## What is Zenoh? ## Zenoh in action ## Your first Zenoh app ## Installation ## Deployment ## For a quick test using Docker ## Troubleshooting ## Abstractions ## Configuration ## Zenoh plugins ## REST plugin ## Storage manager plugin ## TLS authentication ## QUIC transport ## User-Password authentication ## Access Control ## Rust API ## C API ## Pico API ## C++ API ## Python API ## REST API ## Kotlin API ## Migrating from Zenoh v0.5.x to Zenoh v0.6.x ## Migrating from Zenoh v0.5.x Rust API to Zenoh v0.6.x Rust API ## Migrating from Zenoh-C v0.5.x zenoh-net API to Zenoh-C v0.6.x zenoh API ## Migrating from Zenoh v0.5.x Python API to Zenoh v0.6.x Python API ## Migrating from Zenoh-Pico v0.5.x to Zenoh-Pico v0.6.x ## Migrating from Zenoh-C to Zenoh-Pico (and vice-versa) ## Concepts ## Rust ## C++ ## C / Pico ## Python ## Java ## Kotlin --- # Migrating from Zenoh v0.5.x Rust API to Zenoh v0.6.x Rust API · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-rust-v0.5.x-v0.6.x # Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-rust-v0.5.x-v0.6.x # Migrating from Zenoh v0.5.x Rust API to Zenoh v0.6.x Rust API In zenoh version 0.6.0, zenoh and zenoh-net APIs have been merged into a single API. ## General considerations about the new Rust v0.6.x zenoh API ### Resolvables Most of the operations of the new API return builder structs that implement theResolvable,SyncResolveandAsyncResolvetraits. Aresfunction needs to be called on those builders to obtain the final result of the operation. When using Rust sync, theSyncResolvetrait needs to be used and theresfunction directly returns the final result. When using Rust async/await,AsyncResolvetrait needs to be used and theresfunction returns aFuture. zenoh v0.6.x sync ```rust use zenoh::prelude::sync::*; let session = zenoh::open(config).res().unwrap(); ``` zenoh v0.6.x async ```rust use zenoh::prelude::r#async::*; let session = zenoh::open(config).res().await.unwrap(); ``` ## Migrating from Rust v0.5.x zenoh API to Rust v0.6.x zenoh API ### Session establishment The zenoh session is now obtained through azenoh::open()function. zenoh v0.5.x ```rust let zenoh = Zenoh::new(config).await.unwrap(); ``` zenoh v0.6.x ```rust let session = zenoh::open(config).res().await.unwrap(); ``` ### Workspace removed The workspace has been removed. All operations are now directly accessible from the zenoh session itself. zenoh v0.5.x ```rust let zenoh = Zenoh::new(config.into()).await.unwrap(); let workspace = zenoh.workspace(None).await.unwrap(); workspace.put(&key_expr, value).await.unwrap(); ``` zenoh v0.6.x ```rust let session = zenoh::open(config).res().await.unwrap(); session.put(&key_expr, value).res().await.unwrap(); ``` ### Path and PathExpr to KeyExpr TypesPathandPathExprhave been replaced by a single typeKeyExpr. ### Subscribing Thesubscribeoperation has been replaced by adeclare_subscriberoperation. It now accepts any type that implementsTryIntoas parameter. Note:declare_subscriberby default returns aHandlerthat derefs to aflume::Receiver. Samples can be accessed through flumerecvandrecv_asyncoperations. It is possible to access samples through a callback by calling thecallbackfunction on the subscriber builder. zenoh v0.5.x ```rust let mut change_stream = workspace.subscribe(&"/key/expression".try_into().unwrap()).await.unwrap(); while let Some(change) = change_stream.next().await { println!("Received {:?} {} : {:?}", change.kind, change.path, change.value); } ``` zenoh v0.6.x ```rust let subscriber = session.declare_subscriber("key/expression").res().await.unwrap(); while let Ok(sample) = subscriber.recv_async().await { println!("Received {} {} : {}", sample.kind, sample.key_expr, sample.value); } ``` ### Publishing Theputoperation now accepts any type that implementsTryIntoas first parameter and any type that implementsIntoas second parameter. zenoh v0.5.x ```rust workspace.put( &"/key/expression".try_into().unwrap(), "value".into() ).await.unwrap(); ``` zenoh v0.6.x ```rust session.put("key/expression", "value").res().await.unwrap(); ``` If needed the encoding or other options can be refined through a builder pattern. zenoh v0.6.x ```rust session .put("key/expression", "value") .encoding(Encoding::TEXT_PLAIN) .res() .await .unwrap(); ``` ### Querying Thegetoperation now accepts any type that implementsTryIntoas first parameter. It now returns someReplyinstead of a someData. Note:getby default returns aflume::Receiver. Replies can be accessed through flumerecvandrecv_asyncoperations. It is possible to access replies through a callback by calling thecallbackfunction on the get builder. zenoh v0.5.x ```rust let mut data_stream = workspace.get(&"/key/expression/**".try_into().unwrap()).await.unwrap(); while let Some(data) = data_stream.next().await { println!("Received {} : {:?}", data.path, data.value) } ``` zenoh v0.6.x ```rust let replies = session.get("key/expression").res().await.unwrap(); while let Ok(reply) = replies.recv_async().await { match reply.sample { Ok(sample) => println!( ">> Received {}: {}", sample.key_expr, sample.value), Err(err) => println!(">> Received ERROR: {}", err), } } ``` ### Eval Theregister_evaloperation has been replaced by adeclare_queryableoperation. It now accepts any type that implementsTryIntoas parameter. Thereply_asyncoperation has been replaced by arelpyoperation that now accepts aResultas parameter. Note:declare_queryableby default returns aHandlerthat derefs to aflume::Receiver. Queries can be accessed through flumerecvandrecv_asyncoperations. It is possible to access queries through a callback by calling thecallbackfunction on the queryable builder. zenoh v0.5.x ```rust let mut get_stream = workspace.register_eval(&"/key/expression".try_into().unwrap()).await.unwrap(); while let Some(get_request) = get_stream.next().await { get_request.reply_async("/demo/example/eval".try_into().unwrap(), "value".into()).await; ``` zenoh v0.6.x ```rust let queryable = session.declare_queryable("key/expression").res().await.unwrap(); while let Ok(query) = queryable.recv_async().await { query.reply(Ok(Sample::new(query.key_expr().clone(), "value"))).res().await.unwrap(); } ``` ### Examples More examples are available there : zenoh v0.5.0-beta9 zenoh v0.6.0 ## Migrating from Rust v0.5.x zenoh-net API to Rust v0.6.x zenoh API ### zenoh::net module removal All types and operations from thezenoh::netmodule have been moved to thezenohmodule. zenoh-net v0.5.x ```rust let session = zenoh::net::open(config).await.unwrap(); ``` zenoh v0.6.x ```rust let session = zenoh::open(config).res().await.unwrap(); ``` ### Subscribing Thedeclare_subscribernow accepts any type that implementsTryIntoas parameter. It now only takes one parameter. Subscription configuration is performed with the help of a builder pattern. Note:declare_subscriberby default returns aHandlerthat derefs to aflume::Receiver. Samples can be accessed through flumerecvandrecv_asyncoperations. It is possible to access samples through a callback by calling thecallbackfunction on the subscriber builder. zenoh-net v0.5.x ```rust let sub_info = SubInfo { reliability: Reliability::Reliable, mode: SubMode::Push, period: None }; let mut subscriber = session.declare_subscriber(&"/key/expression".into(), &sub_info).await.unwrap(); while let Some(sample) = subscriber.receiver().next().await { println!("Received : {:?}", sample); } ``` zenoh v0.6.x ```rust let subscriber = session .declare_subscriber("key/expression") .reliable() .res() .await .unwrap(); while let Ok(sample) = subscriber.recv_async().await { println!("Received : {:?}", sample); } # }) ``` ### Subscribing with callback Thedeclare_callback_subscriberoperation has been removed. ACallbackSubscribercan now be declared by usingdeclare_subscriberand calling thecallbackfunction on the returned builder. zenoh-net v0.5.x ```rust let sub_info = SubInfo { reliability: Reliability::Reliable, mode: SubMode::Push, period: None }; let subscriber = session .declare_callback_subscriber(&"/key/expression".into(), &sub_info, |sample| { println!("Received : {:?}", sample); }) .await .unwrap(); ``` zenoh v0.6.x ```rust let subscriber = session .declare_subscriber("key/expression") .reliable() .callback(|sample| { println!("Received : {:?}", sample); }) .res() .await .unwrap(); # }) ``` ### Publishing Thewriteoperation has been replaced by aputoperation. It now accepts any type that implementsTryIntoas first parameter and any type that implementsIntoas second parameter. zenoh-net v0.5.x ```rust session.write(&"/key/expression".into(), "value".as_bytes().into()).await.unwrap(); ``` zenoh v0.6.x ```rust session.put("key/expression", "value").res().await.unwrap(); ``` Thewrite_extoperation has been removed. Configuration is now performed with the help of a builder pattern. zenoh-net v0.5.x ```rust session.write_ext( &"/key/expression".into(), "value".as_bytes().into(), encoding::TEXT_PLAIN, data_kind::PUT, CongestionControl::Drop, ).await.unwrap(); ``` zenoh v0.6.x ```rust session .put("key/expression", "value") .encoding(Encoding::TEXT_PLAIN) .kind(SampleKind::Put) .congestion_control(CongestionControl::Drop) .res() .await .unwrap(); ``` Thedeclare_publishernow accepts any type that implementsTryIntoas parameter. It now has aputoperation that only takes any type that implementsIntoas parameter, adeleteoperation that takes no parameter and awriteoperation that takes both aSampleKindas first paramerter and any type that implementsIntoas second parameter. zenoh-net v0.5.x ```rust let publisher = session.declare_publisher(&"/key/expression".into()).await.unwrap(); session.write(&"/key/expression".into(), "value".as_bytes().into()).await.unwrap(); ``` zenoh v0.6.x ```rust let publisher = session.declare_publisher("key/expression").res().await.unwrap(); publisher.put("value").res().await.unwrap(); ``` ### Querying Thequeryoperation has been replaced by agetoperation. It now accepts any type that implementsTryIntoas single parameter instead of a key expression and a predicate. Finer configuration is performed with the help of a builder pattern. Note:getby default returns aflume::Receiver. Replies can be accessed through flumerecvandrecv_asyncoperations. It is possible to access replies through a callback by calling thecallbackfunction on the get builder. zenoh-net v0.5.x ```rust let mut replies = session.query( &"/key/expression".into(), "predicate", QueryTarget::default(), QueryConsolidation::default() ).await.unwrap(); while let Some(reply) = replies.next().await { println!(">> Received {:?}", reply.data); } ``` zenoh v0.6.x ```rust let mut replies = session .get("key/expression?predicate") .target(QueryTarget::default()) .consolidation(QueryConsolidation::default()) .res() .await .unwrap(); while let Ok(reply) = replies.recv_async().await { match reply.sample { Ok(sample) => println!( ">> Received {}", sample.value), Err(err) => println!(">> Received ERROR: {}", err), } } ``` ### Queryable Thedeclare_queryableoperation now accepts any type that implementsTryIntoas first parameter. It takes a single parameter. Finer configuration is perfromed with the help of a builder pattern. Thereply_asyncoperation has been replaced by arelpyoperation that now accepts aResultas parameter. Note:declare_queryableby default returns aHandlerthat derefs to aflume::Receiver. Queries can be accessed through flumerecvandrecv_asyncoperations. It is possible to access queries through a callback by calling thecallbackfunction on the queryable builder. zenoh-net v0.5.x ```rust let mut queryable = session.declare_queryable(&"/key/expression".into(), EVAL).await.unwrap(); while let Some(query) = queryable.receiver().next().await { query.reply_async(Sample{ res_name: "/key/expression".to_string(), payload: "value".as_bytes().into(), data_info: None, }).await; } ``` zenoh v0.6.x ```rust let queryable = session.declare_queryable("key/expression").res().await.unwrap(); while let Ok(query) = queryable.recv_async().await { query.reply(Ok(Sample::new(query.key_expr().clone(), "value"))).res().await.unwrap(); } ``` ### Examples More examples are available there : zenoh-net v0.5.0-beta9 zenoh v0.6.0 --- # Storage manager plugin · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/manual/plugin-storage-manager # Source: https://zenoh.io/docs/manual/plugin-storage-manager # Storage manager plugin Thestorage_managerplugin provideszenohdwith the ability to store values associated with a set of keys, allowing other nodes to query the most recent values associated with these keys. Library name:zplugin_storage_manager ## Backends and Volumes Since there exist many ways for a Zenoh node to store values it may need to serve later, the storage manager plugin relies on dynamically loaded “backends” to provide this functionality. Typically, a backend will leverage some third-party technology, such as databases, to handle storage. A possibly convenient side effect of using databases as backends is that they may also be used as an interface between your Zenoh infrastructure and an external infrastructure that may interact independently with the database. You may want to load the same backend multiple times with different configurations: we refer to these instances as “volumes”. These volumes can in turn be relied on by any number of storages, just like you can store many files on a filesystem volume. When defining volumes, there are multiple ways to inform it of which backend it should use: - With thebackendoption, you may specify the name of a backend that will be used for lookup. - With the__path__option, you may specify a list of absolute paths. The storage manager will then load the first file to be found at one of these paths. Specifying this option disables name-based lookup completely. - Using neither of these options will result in the same name-based lookup as with thebackendoption, using the volume’s name. This name-based lookup consists in searching the configuredbackend_search_dirsfor azbackend_dynamic library file; the exact searched filenames are platform-specific: - on Unix/Linux:libzbackend_.so - on macOS:libzbackend_.dylib - on Windows:zbackend_.dll ### Integrated volumes By design, the storage manager may make some volumes available regardless of configuration. Currently, the only such volume is thememoryvolume. This volume stores key-value pairs in RAM. While convenient, keep in mind that this storage is not persistent through restarts ofzenohd. ## Configuration Storages use structuredconfigurationsto require the creation and deletion of backends and storages.The main schema is as follows: ```json { // Search directories when backends are requested by name backend_search_dirs?: string | string[], // The list of volumes that must be created volumes: { // All volumes on a Zenoh node must have unique names "": { // The name of the backend this volume should run on. If unspecified, it defaults to the name of the volume. backend?: string, // Much like for plugins, if a list of paths is configured, the backend will be run using the first path pointing to a loadable library. // If unspecified, name-based lookup will be used instead. __path__?: string | string[], // backends may have options specific to them, such as `url` for the influxdb backends ...backend_specifics: any, } }, storages: { // All storages must also have unique names. "": { // The key expression that the storage should be set up for, such as "demo/storage/**" key_expr: string, // A prefix of `key_expr` that should be stripped from it when storing keys in the storage strip_prefix?: string, // Storages depend on a volume to do the actual storing of data. // `volume: "memory"` is equivalent to `volume: {id: "memory"}`, but some volumes may require additional configuration. For example, a volume running on the `filesystem` backend needs each storage to specify a `base_dir`. volume: string | { id: string, ...configuration: any} } } } ``` The example configuration providedherehas concrete examples of backend and storage configuration. ## Backends list Here is a list of the available backends: ## Volumes and Storage management during runtime The Volumes and Storages of a Zenoh router can be managed at runtime via its admin space, if it’s configured to be writeable: - either via the configuration file in theadminspace.permissionssection - either via thezenohdcommand line option:--adminspace-permissions <[r|w|rw|none]> The most convenient way to edit configuration at runtime is through theadmin space, via theREST API. This is the method we will teach here throughcurlcommands. If you’re unfamiliar withcurl, it’s a command line tool to make HTTP requests, here’s a quick catch-up, with the equivalent in JS’s standard library’sfetch: ```json curl -X PUT 'http://hostname:8000/key/expression' -H 'content-type:application/json' -d '{"some": ["json", "data"]}' # ^HTTP METHOD ^Target URL for the request ^Header saying the data is in JSON ^The body of the request ``` ```json fetch('http://hostname:8000/key/expression', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({some: ["json", "data"]}) }) ``` ### Adding a volume To create a volume, add an entry describing it to the configuration. Let’s say you’d like to create “my-volume” on a node that had the influxdb daemon listen on port 8086: ```json curl -X PUT 'http://hostname:8000/@/local/router/config/plugins/storage_manager/volumes/my-volume' -H 'content-type:application/json' -d '{"backend": "influxdb", "url": "http://localhost:8086"}' ``` Note that if you only planned on having a single volume relying on influxdb, you might as well name that volume “influxdb”, saving you the need to specify that the “influxdb” backend is the one you want to use: ```json curl -X PUT 'http://hostname:8000/@/local/router/config/plugins/storage_manager/volumes/influxdb' -H 'content-type:application/json' -d '{"url": "http://localhost:8086"}' ``` ### Removing a volume To remove a volume, simply delete its entry from the configuration with: ```text curl -X DELETE 'http://hostname:8000/@/local/router/config/plugins/storage_manager/volumes/my-volume' ``` The storage manager will delete the associated storages as well. ### Adding a storage You can add storages at any time by adding them to the configuration through the admin space. The simplest volume to use is the integrated “memory” volume, since it requires no extra configuration. Let’s have “my-storage” work ondemo/my-storage/**: ```json curl -X PUT 'http://hostname:8000/@/local/router/config/plugins/storage_manager/storages/my-storage' -H 'content-type:application/json' -d '{"key_expr": "demo/my-storage/**", "volume": "memory"}' ``` Some volumes, like that “my-volume” one we createdearlier, need a bit more configuration. Any volume supported by theinfluxdbbackend, for example, needs to know on what database to store the data associated with each storage through adbargument: ```json curl -X PUT 'http://hostname:8000/@/local/router/config/plugins/storage_manager/storages/my-other-storage' -H 'content-type:application/json' -d '{"key_expr": "demo/my-other-storage/**", "volume": {"id": "my-volume", "db": "MyOtherStorage"}}' ``` ### Removing a storage Just like volumes, removing a storage is as simple as deleting its entry in the configuration. Note that removing a volume’s last storage will not remove that volume: volumes with 0 storages depending on them are perfectly legal. ```text curl -X DELETE 'http://hostname:8000/@/local/router/config/plugins/storage_manager/storages/my-storage' ``` ### Checking a volume’s or a storage’s status TODO: this part hasn’t been redocumented yet. Feel free to contact us onDiscordto get more information. --- # C++ · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/migration_1.0/c++ # Source: https://zenoh.io/docs/migration_1.0/c++ # C++ Zenoh 1.0.0 brings a number of changes to the API, with a concentrated effort to bring the C++ API to more closely resemble the Rust API in usage. The improvements we bring in this update include: - A simpler organization of the Zenoh classes, abstracting away the notion of View and Closure. - Improved and more flexible Error Handling through error codes and exceptions. - Support for serialization of common types like strings, numbers, vectors through Codecs. - Ability for users to define their own Codecs (for their own types or to overwrite the default one)! - Improved stream handlers and callback support. - Simpler attachment API. Now that theamuse boucheis served, let’s get into the main course! ## Error Handling In version 0.11.0 all Zenoh call failures were handled by either returning aboolvalue indicating success or failure (and probably returning an error code) or returning anstd::variant. For instance: ```cpp std::variant config_client(const z::StrArrayView& peers); bool put(const z::BytesView& payload, const z::PublisherPutOptions& options, ErrNo& error); ``` In 1.0.0, all functions that can fail on the Zenoh side now offer 2 options for error handling: A. Exceptions B. Error Codes Any function that can fail now accepts an optional parameterZError* errpointer to the error code. If it is not provided (or set tonullptr), an instance ofZExceptionwill be thrown, otherwise the error code will be written into theerrpointer. ```cpp static Config client(const std::vector& peers, ZError* err = nullptr); ``` This also applies to constructors: if a failure occurs, either an exception is thrown or the error code is written to the provided pointer. In the latter case, the returned object will be in an “empty” state (i.e. converting it to a boolean returnsfalse). ```cpp Config config = Config::create_default(); // Receiving an error code Zerror err = Z_OK; auto session = Session::open(std::move(config), &err); if (err != Z_OK) { // or alternatively if (!session) // handle failure } // Catching exception Zenoh::session s(nullptr); // Empty session object try { s = Session::open(std::move(config), &err); } catch (const ZException& e) { // handle failure } ``` All returned andstd::move’d-in objects are guaranteed to be left in an “empty” state in case of function call failure. ## Payload In version 0.11.0 it was only possible to sendstd::string/const char*orstd::vector/uint8_t*using theBytesViewclass: ```text publisher.put("my_payload"); ``` In 1.0.0, theBytesViewclass is gone and we introduced theBytesobject which represents a (serialized) payload. Similarly to 0.11.0 it can be used to store raw bytes or strings: ```c void publish_string(const Publisher& publisher, const std::string& data) { publisher.put(Bytes(data)); } void publish_string_without_copy(const Publisher& publisher, std::string&& data) { publisher.put(Bytes(data)); } void receive_string(const Sample &sample) { std::cout <<"Received: " << sample.get_payload().as_string() << "\n"; }; void publish_bytes(const Publisher& publisher, const std::vector& data) { publisher.put(Bytes(data)); } void publish_bytes_without_copy(const Publisher& publisher, std::vector&& data) { publisher.put(Bytes(data)); } void receive_bytes(const Sample &sample) { std::vector = sample.get_payload().as_vector(); }; ``` Additionallyzenoh::extnamespace provides support for serialization/deserialziation of typed data to/intoBytes: ```json // arithmetic types double pi = 3.1415926; Bytes b = ext::serialize(pi); assert(ext::deserialize(b) == pi); // Composite types std::vector v = {0.1f, 0.2f, 0.3f}; b = ext::serialize(v); assert(ext::deserialize>(b) == v); std::unordered_map> m = { {"a", {0.5, 0.2}}, {"b", {-123.45, 0.4}}, {"abc", {3.1415926, -1.0} } }; b = ext::serialize(m); assert(ext::deserialize>>(b) == m); ``` Users can easily define serialization/deserialization for their own custom types by usingext::Serializerandext::Deserializerclasses: ```rust struct CustomStruct { std::vector vd; int32_t i; std::string s; }; // One needs to implement __zenoh_serialize_with_serializer and __zenoh_deserialize_with_deserializer // in the same namespace, where CustomStruct is defined. // To simplify implementation users are allowed to use // serialize_with_serializer and deserialize_with_deserializer functions defined in zenoh::ext::detail namespace. bool __zenoh_serialize_with_serializer(zenoh::ext::Serializer& serializer, const CustomStruct& s, ZResult* err) { return zenoh::ext::detail::serialize_with_serializer(serializer, s.vd, err) && zenoh::ext::detail::serialize_with_serializer(serializer, s.i, err) && zenoh::ext::detail::serialize_with_serializer(serializer, s.s, err); } bool __zenoh_deserialize_with_deserializer(zenoh::ext::Deserializer& deserializer, CustomStruct& s, ZResult* err) { return zenoh::ext::detail::deserialize_with_deserializer(deserializer, s.vd, err) && zenoh::ext::detail::deserialize_with_deserializer(deserializer, s.i, err) && zenoh::ext::detail::deserialize_with_deserializer(deserializer, s.s, err); } void serialize_custom() { CustomStruct s = {{0.1, 0.2, -1000.55}, 32, "test"}; Bytes b = ext::serialize(s); CustomStruct s_out = ext::deserialize(b); assert(s.vd == s_out.vd); assert(s.i == s_out.i); assert(s.s == s_out.s); } ``` For lower-level access to theBytescontentBytes::Reader,Bytes::WriterandBytes::SliceIteratorclasses can be used. ## Stream Handlers and Callbacks In version 0.11.0 stream handlers were only supported forget: ```cpp // callback session.get(keyexpr, "", {on_reply, on_done}, opts); // stream handlers interface auto [send, recv] = reply_fifo_new(16); session.get(keyexpr, "", std::move(send), opts); Reply reply(nullptr); // blocking for (recv(reply); reply.check(); recv(reply)) { auto sample = expect(reply.get()); std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string() << "')\n"; } // non-blocking for (bool call_success = recv(reply); !call_success || reply.check(); call_success = recv(reply)) { if (!call_success) { std::cout << "."; Sleep(1); continue; } auto sample = expect(reply.get()); std::cout << "\nReceived ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string() << "')"; } ``` In 1.0.0,Subscriber,Queryableandgetcan now use either a callable object or a stream handler. Currently, Zenoh provides 2 types of handlers: - FifoHandler- serving messages in Fifo order,blockingonce full. - RingHandler- serving messages in Fifo order,dropping older messagesonce full to make room for new ones. ```cpp // callback session.get( keyexpr, "", on_reply, on_done, {.target = Z_QUERY_TARGET_ALL} ); // stream handlers interface auto replies = session.get( keyexpr, "", channels::FifoChannel(16), {.target = QueryTarget::Z_QUERY_TARGET_ALL} ); // blocking for (auto res = replies.recv(); std::has_alternative(res); res = replies.recv()) { const auto& sample = std::get(res).get_ok(); std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string() << "')\n"; } // non-blocking while (true) { auto res = replies.try_recv(); if (std::has_alternative(res)) { const auto& sample = std::get(res).get_ok(); std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string() << "')\n"; } else if (std::get(res) == channels::RecvError::Z_NODATA) { // try_recv is non-blocking call, so may fail to return a reply if the Fifo buffer is empty std::cout << "."; std::this_thread::sleep_for(1s); continue; } else { // std::get(res) == channels::RecvError::Z_DISCONNECTED break; // no more replies will arrive } } std::cout << std::endl; ``` The same works forSubscriberandQueryable: ```cpp // callback auto data_callback = [](const Sample &sample) { std::cout << ">> [Subscriber] Received ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string() << "')\n"; }; auto subscriber = session.declare_subscriber( keyexpr, data_callback, closures::none // or dedicated function to call when subscriber is undeclared ); std::cout << "Press CTRL-C to quit...\n"; while (true) { std::this_thread::sleep_for(1s); } // stream handlers interface auto subscriber = session.declare_subscriber(keyexpr, channels::FifoChannel(16)); const auto& messages = subscriber.handler(); //blocking for (auto res = messages.recv(); std::has_alternative(res); res = messages.recv()) { // recv will block until there is at least one sample in the Fifo buffer // it will return an empty sample and alive=false once subscriber gets disconnected const Sample& sample = std::get(res); std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string() << "')\n"; } // non-blocking while (true) { auto res = messages.try_recv(); if (std::has_alternative(res)) { const auto& sample = std::get(res); std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string() << "')\n"; } else if (std::get(res) == channels::RecvError::Z_NODATA) { // try_recv is non-blocking call, so may fail to return a sample if the Fifo buffer is empty std::cout << "."; std::this_thread::sleep_for(1s); } else { // std::get(res) == channels::RecvError::Z_DISCONNECTED break; // no more samples will arrive } } std::cout << std::endl; ``` ## Attachment In version 0.11.0 an attachment could only represent a set of key-value pairs and had a somewhat complicated interface: ```json // publish message with attachment options.set_encoding(Z_ENCODING_PREFIX_TEXT_PLAIN); std::unordered_map attachment_map = { {"source", "C++"}, {"index", "0"} }; options.set_attachment(attachment_map); pub.put(s, options); // subscriber callback function receiving message with attachment void data_handler(const Sample &sample) { std::cout << ">> [Subscriber] Received \" ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string_view() << "')\n"; if (sample.get_attachment().check()) { // reads full attachment sample.get_attachment().iterate([](const BytesView &key, const BytesView &value) -> bool { std::cout << " attachment: " << key.as_string_view() << ": '" << value.as_string_view() << "'\n"; return true; }); // or read particular attachment item auto index = sample.get_attachment().get("index"); if (index != "") { std::cout << " message number: " << index.as_string_view() << std::endl; } } }; ``` In 1.0.0, attachment handling was greatly simplified. It is now represented asBytes(i.e. the same class we use to represent serialized data) and can thus contain data in any format. ```cpp // publish a message with attachment auto session = Session::open(std::move(config)); auto pub = session.declare_publisher(KeyExpr(keyexpr)); // send some key-value pairs as attachment // allocate attachment map std::unordered_map attachment_map = { {"source", "C++"}, {"index", "0"} }; pub.put( Bytes("my_payload"), {.encoding = Encoding("text/plain"), .attachment = ext::serialize(attachment_map)} ); // subscriber callback function receiving a message with attachment void data_handler(const Sample &sample) { std::cout << ">> [Subscriber] Received ('" << sample.get_keyexpr().as_string_view() << "' : '" << sample.get_payload().as_string() << "')\n"; auto attachment = sample.get_attachment(); if (!attachment.has_value()) return; // we expect attachment in the form of key-value pairs auto attachment_deserialized = ext::deserialize>(attachment->get()); for (auto&& [key, value]: attachment_deserialized) { std::cout << " attachment: " << key << ": '" << value << "'\n"; } }; ``` # Optional Parameters Handling for optional parameters for Zenoh functions was simplified. There are no more getters/setters and all fields of option structures are public. Also option arguments are automatically set to their default values, and if your compiler has support for designated initializers, it is sufficient to only set the fields that are needed to be different from default ones. In version 0.11.0: ```text GetOptions opts; opts.set_target(Z_QUERY_TARGET_ALL); opts.set_value(value); ... session.get(keyexpr, "", {on_reply, on_done}, opts); ``` In 1.0.0: ```json session.get(keyexpr, "", on_reply, on_done, {.target = Z_QUERY_TARGET_ALL, .payload = ext::serialize(value)}); ``` --- # Migrating from Zenoh v0.5.x Python API to Zenoh v0.6.x Python API · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-python-v0.5.x-v0.6.x # Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-python-v0.5.x-v0.6.x # Migrating from Zenoh v0.5.x Python API to Zenoh v0.6.x Python API ## Explorability In previous releases, the Python bindings were entirely defined in Rust, making it very hard for Pythoners to explore it. With 0.6, the bindings have evolved: a “private” layer is exposed by Rust, and wrapped in Python, with 2 main advantages: - IDEs can now find available symbols, signatures and documentation more easily. - Any dynamic type handling is done in Python, letting you investigate what happens depending on the types of values you pass more easily. ## Handling RAII The bindings are generated throughpyo3, which among other things ensures that the destructors of the wrapped Rust types get called when a value becomes inaccessible. This may lead to somewhat surprising behaviours comming from Python, where RAII is uncommon, as this implies that, for instance, subscribers will be undeclared as soon as no variable points to them anymore. A typical case for that issubscriber = session.declare_subscriber("key/expr", callback): unless thatsubscriber =is there, the subscriber will indeed be declared, but it will be undeclared the moment the method returns. This has always been the case, but we never addressed this much in documentation. ## API Changes ### The KeyExpr class You’ll notice thatKeyExpris a recurring type in the new API: it represents a valid Key Expressions. While you can keep using strings for any functionIntoKeyExpr, constructing aKeyExprallows you to make sure that string is a valid key expression sooner, and allows Zenoh to bypass checking that when you use it. ### Subscribing Thedeclare_subscriberoperation now accepts more than just callbacks. zenoh v0.5.x ```text subscriber = session.declare_subscriber("/key/expression", callback) ``` zenoh v0.6.x ```python # you can pass a callback subscriber = session.declare_subscriber("key/expression", callback) # you can also pass a `zenoh.Queue`, to consume data in a for-loop (note that until subscriber is undeclared, that loop will never end) subscriber = session.declare_subscriber("key/expression", zenoh.Queue()) for sample in sub.receiver: print(f">> [Subscriber] Received {sample.kind} ('{sample.key_expr}': '{sample.payload.decode('utf-8')}')") ``` ### Publishing Theputoperation’s API is mostly the same, but you can now alsodeclare_publishers when you expect to publish data on the same key a lot. ### Querying getno longer returns a list of samples, but instead accepts a handler, the same waydeclare_subscriberdoes. While this appears less convenient, it does solve a couple issues: - You don’t need to wait for every reply to arrive before processing the earliest ones. - You don’t have to store all replies at once, relieving pressure on your memory. - You don’t have to block while waiting for all replies to arrive. zenoh v0.5.x ```python replies = session.get("/key/expression/**") for reply in replies: print(f"Received {reply.sample.key_expr} : {reply.sample.payload.decode('utf-8')}") ``` zenoh v0.6.x ```text query = session.get("key/expression", callback) ``` If you liked the old behaviour better, you can usezenoh.ListCollectorto get a very similar one: ```python replies = session.get("key/expression", zenoh.ListCollector()) for reply in replies(): # Note that with ListCollector, `get` returns a closure that will return the data once it's all collected try: print(f">> Received ('{reply.ok.key_expr}': '{reply.ok.payload.decode('utf-8')}')") except: print(">> Received an error) ``` Queries are now guaranteed to only receive replies whose key expressions intersect with the queried one. An API to disable this restriction will be introduced in the near future. Please let us know if that is of interest to you. ### Eval Theregister_evaloperation has been replaced bydeclare_queryable. Like thedeclare_subscriber, you can now pass handlers that aren’t just callbacks. zenoh v0.5.x ```text eval = session.register_eval("/key/expression", callback) ``` zenoh v0.6.x ```text queryable = session.declare_queryable("key/expression", callback) ``` Note that queryables (the new name for evals) must now reply on key expressions that intersect with the queried one (and will raise an exception otherwise), unless the query specifically allowed replies on any key expression. At the moment, there is no stable API to check whether a query allows such replies. An API to do so will be introduced in the near future. ### Examples More examples are available there : zenoh v0.5.0-beta9 zenoh v0.6.0 ## What about Zenoh Net? Zenoh Net was removed, as Zenoh’s API has shifted to a more declarative style which allows us to optimize Zenoh for you without causing breaking changes repeatedly. With API stability as one of our main goals, maintaining a low level API such as Zenoh Net will either be too constraining, or too human-ressource intensive. We suspect most Python users were not using Zenoh Net much. If you are a Zenoh Net Python user in need of help migrating to the new API, feel free to ping us onDiscord. --- # C / Pico · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/migration_1.0/c_pico # Source: https://zenoh.io/docs/migration_1.0/c_pico # C / Pico ## General API changes We have reworked the type naming to clarify how types should be interacted with. ### Owned types Owned types are allocated by the user and it is their responsibility to drop them usingz_drop(orz_closefor sessions). Previously, we were returning Zenoh structures by value. In Zenoh 1.0.0, a reference to memory must be provided. This allows initializing user allocated structures and frees return value for error codes. Here is a quick example of this change: - Zenoh 0.11.x ```text z_owned_session_t session = z_open(z_move(config)); if (!z_check(session)) { return -1; } z_close(z_move(session)); ``` - Zenoh 1.0.0 ```text z_owned_session_t session; if (z_open(&session, z_move(config), &opts) < 0) { return -1; } z_close(z_move(session)) ``` The owned objects have a “null” state. - If the constructor function (e.g.z_open) fails, the owned object will be set to its null state. - Thez_dropreleases the resources of the owned object and sets it to the null state to avoid double drop. Callingz_dropon an object in a null state is safe and does nothing. - Calling z_drop on an uninitialized object is invalid. Owned types support move semantics, which will consume the owned object and turn it into a moved object, see next section. ### Moved types Moved types are obtained when usingz_moveon an owned type object. They are consumed on use when passed to relevant functions. Any non-constructor function accepting a moved object (i.e. an object passed by moved pointer) becomes responsible for calling drop on it. The object is guaranteed to be in the null state upon such function return, even if it fails. ### Loaned types Each owned type now has a correspondingz_loaned_xxx_ttype, which is obtained by calling z_loanorz_loan_muton it, or eventually received from Zenoh functions / callbacks. It is no longer possible to directly access the fields of an owned object that has been loaned, the accessor functions on the loaned objects should instead be used. Here is a quick example: - Zenoh 0.11.x ```c void reply_handler(z_owned_reply_t *reply, void *ctx) { if (z_reply_is_ok(reply)) { z_sample_t sample = z_reply_ok(reply); printf(">> Received ('%.*s')\n", (int)sample.payload.len, sample.payload.start); } } ``` - Zenoh 1.0.0 ```c void reply_handler(const z_loaned_reply_t *reply, void *ctx) { if (z_reply_is_ok(reply)) { const z_loaned_sample_t *sample = z_reply_ok(reply); z_owned_string_t replystr; z_bytes_deserialize_into_string(z_sample_payload(sample), &replystr); printf(">> Received ('%s')\n", z_string_data(z_loan(replystr))); } } ``` Certain loaned types can be copied into owned, using thez_clonefunction. All objects that can be cloned will have a function with the signaturez_xxx_clone. Please consult the docs of each type to determine if it can be copied into owned ```c void reply_handler(const z_loaned_reply_t *reply, void *ctx) { some_struct_t *some_ctx = (some_struct_t *)(ctx); z_clone(some_ctx->query, query); some_ctx->has_query = true; } ``` In the case of our callback, this allows the data from the loaned type to be used in another thread context. Note that this other thread should not forget to callz_dropto avoid a memory leak! ### View types View types are only wrappers to user allocated data, likez_view_keyexpr_t.These types can be loaned in the same way as owned types but they don’t need to be dropped explicitly (user is fully responsible for deallocation of wrapped data). - Zenoh 0.11.x ```cpp const char *keyexpr = "example/demo/*"; z_owned_subscriber_t sub = z_declare_subscriber(z_loan(session), z_keyexpr(keyexpr), z_move(callback), NULL); if (!z_check(sub)) { printf("Unable to declare subscriber.\n"); return -1; } ``` - Zenoh 1.0.0 ```cpp const char *keyexpr = "example/demo/*"; z_owned_subscriber_t sub; z_view_keyexpr_t ke; z_view_keyexpr_from_str(&ke, keyexpr); if (z_declare_subscriber(z_loan(session), &sub, z_loan(ke), z_move(callback), NULL) < 0) { return -1; } ``` ## Payload and Serialization Zenoh 1.0.0 handles payload differently. Before one would pass the pointer to the buffer and its length, now everything must be converted/serialized intoz_owned_bytes_t. Raw data in the form of null-terminated strings or buffers (i.e.uint8_t*+ length) can be converted directly intoz_owned_bytes_tas follows: - Zenoh 0.11.x ```cpp int8_t send_string() { char *value = "Some data to publish on Zenoh"; if (z_put(z_loan(session), z_keyexpr(ke), (const uint8_t *)value, strlen(value), NULL) < 0) { return -1; } return 0; } int8_t send_buf() { uint8_t *buf = (uint8_t*)z_malloc(16); // fill the buf if (z_put(z_loan(session), z_keyexpr(ke), buf, 16, NULL) < 0) { return -1; } return 0; } ``` - Zenoh 1.0.0 ```c int8_t send_string() { char *value = "Some data to publish on Zenoh"; z_owned_bytes_t payload; z_bytes_copy_from_str(&payload, value); // this copeies value to the payload if (z_put(z_loan(session), z_loan(ke), z_move(payload), NULL) < 0) { return -1; } // z_bytes_to_string can be used to convert received z_loaned_bytes_t into z_owned_string_t return 0; } void receive_string(const z_loaned_bytes_t* payload) { z_owned_string_t s; z_bytes_to_string(payload, &s); // do something with the string // raw ptr can be accessed via z_string_data(z_loan(s)) // data length can be accessed via z_string_len(z_loan(s)) // in the end string should be dropped since it contains a copy of the payload data z_drop(z_move(s)); } int8_t void send_buf() { uint8_t *buf = (uint8_t*)z_malloc(16); // fill the buf z_bytes_from_buf(&payload, buf, 16, my_custom_delete_function, NULL); // this moves buf into the payload // my_custom_delete_function will be called to free the buf, once the corresponding data is send // alternatively z_bytes_copy_from_buf(&payload, buf, 16) can be used, if coying the buf is required. if (z_put(z_loan(session), z_loan(ke), z_move(payload), NULL) < 0) { return -1; } // z_bytes_to_slice can be used to convert received z_loaned_bytes_t into z_owned_slice_t return 0; } /// possible my_custom_delete_function implementation void my_custom_delete_function(void *data, void* context) { // perform delete of data by optionally using extra information in the context free(data); } void receive_buf(const z_loaned_bytes_t* payload) { z_owned_slice_t s; z_bytes_to_slice(payload, &s); // do something with the string // raw ptr can be accessed via z_slice_data(z_loan(s)) // data length can be accessed via z_slice_len(z_loan(s)) // in the end string should be dropped since it contains a copy of the payload data z_drop(z_move(s)); } ``` The structured data can be serialized intoz_owned_bytes_tby using provided serialization functionality. Zenoh provides support for serializing arithmetic types, strings, sequences and tuples. More comprehensive serialization/deserialization examples are provided inhttps://github.com/eclipse-zenoh/zenoh-c/blob/main/examples/z_bytes.candhttps://github.com/eclipse-zenoh/zenoh-pico/blob/main/examples/unix/c11/z_bytes.c. To simplify serialization/deserialization we provide support for some primitive types likeuint8_t*+ length, null-terminated strings and arithmetic types. Primitive types can be serialized directly intoz_owned_bytes_t: ```text // Serialization uint32_t input_u32 = 1234; ze_serialize_uint32(&payload, input_u32); // Deserialization uint32_t output_u32 = 0; ze_deserialize_uint32(z_loan(payload), &output_u32); // now output_u32 = 1234 z_drop(z_move(payload)); ``` while tuples and/or arrays require usage ofze_owned_serializer_tandze_deserializer_t: ```python typedef struct my_struct_t { float f; int32_t n; } my_struct_t; ... // Serialization my_struct_t input_vec[] = {{1.5f, 1}, {2.4f, 2}, {-3.1f, 3}, {4.2f, 4}}; ze_owned_serializer_t serializer; ze_serializer_empty(&serializer); ze_serializer_serialize_sequence_length(z_loan_mut(serializer), 4); for (size_t i = 0; i < 4; ++i) { ze_serializer_serialize_float(z_loan_mut(serializer), input_vec[i].f); ze_serializer_serialize_int32(z_loan_mut(serializer), input_vec[i].n); } ze_serializer_finish(z_move(serializer), &payload); // Deserialization my_struct_t output_vec[4] = {0}; ze_deserializer_t deserializer = ze_deserializer_from_bytes(z_loan(payload)); size_t num_elements = 0; ze_deserializer_deserialize_sequence_length(&deserializer, &num_elements); assert(num_elements == 4); for (size_t i = 0; i < num_elements; ++i) { ze_deserializer_deserialize_float(&deserializer, &output_vec[i].f); ze_deserializer_deserialize_int32(&deserializer, &output_vec[i].n); } // now output_vec = {{1.5f, 1}, {2.4f, 2}, {-3.1f, 3}, {4.2f, 4}} z_drop(z_move(payload)); ``` To implement custom (de-)serialization, Zenoh 1.0.0 providesze_owned_bytes_serializer,ze_bytes_deserializer_tor lower-levelz_owned_bytes_wrtiter_tandz_bytes_reader_ttypes and corresponding functions. Note that it is no longer possible to access the underlying payload data pointer directly, since Zenoh cannot guarantee that the data is delivered as a single fragment. So in order to get access to raw payload data one must use eitherz_bytes_reader_tor alternativelyz_bytes_slice_iterator_tand their related functions: ```text z_bytes_reader_t reader = z_bytes_get_reader(z_loan(payload)); uint8_t data1[10] = {0}; uint8_t data2[20] = {0}; z_bytes_reader_read(&reader, data1, 10); // copy first 10 payload bytes to data1 z_bytes_reader_read(&reader, data2, 20); // copy next 20 payload bytes to data2 // or z_bytes_slice_iterator_t slice_iter = z_bytes_get_slice_iterator(z_bytes_loan(&payload)); z_view_slice_t curr_slice; while (z_bytes_slice_iterator_next(&slice_iter, &curr_slice)) { // curr_slice provides a view on the corresponding fragment bytes. // Note that there is no guarantee regarding each individual slice size } ``` ## Channel Handlers and Callbacks In version 0.11.0 Channel handlers were only supported forz_getandz_owned_queryable_t: ```cpp // callback z_owned_closure_reply_t callback = z_closure(reply_handler); z_get(z_loan(session), z_keyexpr(keyexpr), "", z_move(callback), &opts); // Channel handlers interface // blocking z_owned_reply_channel_t channel = zc_reply_fifo_new(16); z_get(z_loan(session), z_keyexpr(keyexpr), "", z_move(channel.send), &opts); z_owned_reply_t reply = z_reply_null(); for (z_call(channel.recv, &reply); z_check(reply); z_call(channel.recv, &reply)) { if (z_reply_is_ok(&reply)) { z_sample_t sample = z_reply_ok(&reply); // do something with sample and keystr } else { printf("Received an error\n"); } z_drop(z_move(reply)); } z_drop(z_move(channel)); // non-blocking z_owned_reply_channel_t channel = zc_reply_non_blocking_fifo_new(16); z_get(z_loan(s), keyexpr, "", z_move(channel.send), &opts); z_owned_reply_t reply = z_reply_null(); for (bool call_success = z_call(channel.recv, &reply); !call_success || z_check(reply); call_success = z_call(channel.recv, &reply)) { if (!call_success) { continue; } if (z_reply_is_ok(z_loan(reply))) { const z_loaned_sample_t *sample = z_reply_ok(&reply); // do something with sample } else { printf("Received an error\n"); } z_drop(z_move(reply)); } z_drop(z_move(channel)); ``` In 1.0.0,z_owned_subscriber_t,z_owned_queryable_tandz_getcan use either a callable object or a stream handler. In addition, the same handler type now provides both a blocking and non-blocking interface. For the time being Zenoh provides 2 types of handlers: - FifoHandler- serving messages in Fifo order,dropping new messagesonce full. It is worth noting that it will drop new messages only if the queue is full and the default multi-thread feature is disabled. - RingHandler- serving messages in Fifo order,dropping older messagesonce full to make room for new ones. ```cpp // callback z_owned_closure_reply_t callback = z_closure(reply_handler); z_get(z_loan(session), z_keyexpr(keyexpr), "", z_move(callback), &opts); // stream handlers interface z_owned_fifo_handler_reply_t handler; z_owned_closure_reply_t closure; z_fifo_channel_reply_new(&closure, &handler, 16); z_get(z_loan(s), z_loan(keyexpr), "", z_move(closure), z_move(opts)); z_owned_reply_t reply; // blocking while (z_recv(z_loan(handler), &reply) == Z_OK) { // z_recv will block until there is at least one sample in the Fifo buffer if (z_reply_is_ok(z_loan(reply))) { const z_loaned_sample_t *sample = z_reply_ok(z_loan(reply)); // do something with sample } else { printf("Received an error\n"); } z_drop(z_move(reply)); } // non-blocking while (true) { z_result_t res = z_try_recv(z_loan(handler), &reply); if (res == Z_CHANNEL_NODATA) { // z_try_recv is non-blocking call, so will fail to return a reply if the Fifo buffer is empty // do some other work or just sleep } else if (res == Z_OK) { if (z_reply_is_ok(z_loan(reply))) { const z_loaned_sample_t *sample = z_reply_ok(z_loan(reply)); // do something with sample } else { printf("Received an error\n"); } z_drop(z_move(reply)); } else { // res == Z_CHANNEL_DISCONNECTED break; // channel is closed - no more replies will arrive } } ``` The same now also works forSubscriberandQueryable: ```c // callback void data_handler(const z_loaned_sample_t *sample, void *context) { // do something with sample } z_owned_closure_sample_t callback; z_closure(&callback, data_handler, NULL, NULL); z_owned_subscriber_t sub; if (z_declare_subscriber(z_loan(session), &sub, z_loan(keyexpr), z_move(callback), NULL) < 0) { printf("Unable to declare subscriber.\n"); exit(-1); } // stream handlers interface z_owned_fifo_handler_sample_t handler; z_owned_closure_sample_t closure; z_fifo_channel_reply_new(&closure, &handler, 16); z_owned_subscriber_t sub; if (z_declare_subscriber(z_loan(session), &sub, z_loan(keyexpr), z_move(closure), NULL) < 0) { printf("Unable to declare subscriber.\n"); exit(-1); } z_owned_sample_t sample; // blocking while (z_recv(z_loan(handler), &sample) == Z_OK) { // z_recv will block until there is at least one sample in the Fifo buffer // it will return an empty sample and is_alive=false once subscriber gets disconnected // do something with sample z_drop(z_move(sample)); } // non-blocking while (true) { z_result_t res = z_try_recv(z_loan(handler), &sample); if (res == Z_CHANNEL_NODATA) { // z_try_recv is non-blocking call, so will fail to return a sample if the Fifo buffer is empty // do some other work or just sleep } else if (res == Z_OK) { // do something with sample z_drop(z_move(sample)); } else { // res == Z_CHANNEL_DISCONNECTED break; // channel is closed - no more samples will be received } } ``` Thez_owned_pull_subscriber_twas removed, given thatRingHandlercan provide similar functionality with ordinaryz_owned_subscriber_t.Since the callback in 1.0.0. carries a loaned sample whenever it is triggered, we can save an explicit drop on the sample now. ## Attachment In 0.11.0, attachments were a separate type and could only be a set of key-value pairs: ```cpp // publish message with attachment char *value = "Some data to publish on Zenoh"; z_put_options_t options = z_put_options_default(); z_owned_bytes_map_t map = z_bytes_map_new(); z_bytes_map_insert_by_alias(&map, _z_bytes_wrap((uint8_t *)"test", 2), _z_bytes_wrap((uint8_t *)"attachement", 5)); options.attachment = z_bytes_map_as_attachment(&map); if (z_put(z_loan(s), z_keyexpr(keyexpr), (const uint8_t *)value, strlen(value), &options) < 0) { return -1; } z_bytes_map_drop(&map); // receive sample with attachment int8_t attachment_reader(z_bytes_t key, z_bytes_t val, void* ctx) { printf(" attachment: %.*s: '%.*s'\n", (int)key.len, key.start, (int)val.len, val.start); return 0; } void data_handler(const z_sample_t* sample, void* arg) { // checks if attachment exists if (z_check(sample->attachment)) { // reads full attachment z_attachment_iterate(sample->attachment, attachment_reader, NULL); // reads particular attachment item z_bytes_t index = z_attachment_get(sample->attachment, z_bytes_from_str("index")); if (z_check(index)) { printf(" message number: %.*s\n", (int)index.len, index.start); } } z_drop(z_move(keystr)); } ``` In 1.0.0, attachments were greatly simplified. They are now represented asz_..._bytes_t(i.e. the same type we use to represent payload data) and can thus contain data in any format. ```python // publish attachment typedef struct { char* key; char* value; } kv_pair_t; ... kv_pair_t attachment_kvs[2] = {; (kv_pair_t){.key = "index", .value = "1"}, (kv_pair_t){.key = "source", .value = "C"} } z_owned_bytes_t payload, attachment; // serialzie key value pairs as attachment ze_owned_serializer_t serializer; ze_serializer_empty(&serializer); ze_serializer_serialize_sequence_length(z_loan_mut(serializer), 2); for (size_t i = 0; i < 2; ++i) { ze_serializer_serialize_str(z_loan_mut(serializer), attachment_kvs[i].key); ze_serializer_serialize_str(z_loan_mut(serializer), attachment_kvs[i].value); } ze_serializer_finish(z_move(serializer), &payload); options.attachment = &attachment; z_bytes_copy_from_str(&payload, "payload"); z_publisher_put(z_loan(pub), z_move(payload), &options); // receive sample with attachment void data_handler(const z_loaned_sample_t *sample, void *arg) { z_view_string_t key_string; z_keyexpr_as_view_string(z_sample_keyexpr(sample), &key_string); z_owned_string_t payload_string; z_bytes_deserialize_into_string(z_sample_payload(sample), &payload_string); printf(">> [Subscriber] Received %s ('%.*s': '%.*s')\n", kind_to_str(z_sample_kind(sample)), (int)z_string_len(z_loan(key_string)), z_string_data(z_loan(key_string)), (int)z_string_len(z_loan(payload_string)), z_string_data(z_loan(payload_string))); z_drop(z_move(payload_string)); const z_loaned_bytes_t *attachment = z_sample_attachment(sample); // checks if attachment exists if (attachment == NULL) { return; } // read attachment key-value pairs using bytes_iterator ze_deserializer_t deserializer = ze_deserializer_from_bytes(z_loan(payload)); size_t num_elements = 0; ze_deserializer_deserialize_sequence_length(&deserializer, &num_elements); z_owned_string_t key, value; for (size_t i = 0; i < num_elements; ++i) { ze_deserializer_deserialize_string(&deserializer, &key); ze_deserializer_deserialize_string(&deserializer, &value); printf(" attachment: %.*s: '%.*s'\n", (int)z_string_len(z_loan(key)), z_string_data(z_loan(key)), (int)z_string_len(z_loan(value)), z_string_data(z_loan(value))); z_drop(z_move(key)); z_drop(z_move(value)); } } ``` ## Encoding Encoding handling has been reworked: before one would use an enum id and a string suffix value, now only the encoding metadata needs to be registered usingz_encoding_from_str. There is a set of predefined constant encodings subject to some wire-level optimization. To benefit from this, the provided encoding should follow the format:";" All predefined constants provided can be found in hereEncoding Variants - Zenoh 0.11.x ```cpp char *value = "Some data to publish on Zenoh"; z_put_options_t options = z_put_options_default(); options.encoding = z_encoding(Z_ENCODING_PREFIX_TEXT_PLAIN, "utf8"); if (z_put(z_loan(session), z_keyexpr(ke), (const uint8_t *)value, strlen(value), &options) < 0) { return -1; } ``` - Zenoh 1.0.0 ```text char *value = "Some data to publish on Zenoh"; z_owned_bytes_t payload; z_bytes_serialize_from_str(&payload, value); z_publisher_put_options_t options; z_publisher_put_options_default(&options); z_encoding_from_str(&encoding, "text/plain;utf8"); options.encoding = z_move(encoding); if (z_put(z_loan(session), z_loan(ke), z_move(payload), &options) < 0) { return -1; } ``` ## Timestamps The generation of timestamps is now tied to a Zenoh session, with the timestamp inheriting theZenohIDof the session. - Zenoh 0.11.x ```text // Didn't exist ``` - Zenoh 1.0.0 ```text z_timestamp_t ts; z_timestamp_new(&ts, z_loan(s)); options.timestamp = &ts; z_publisher_put(z_loan(pub), z_move(payload), &options); ``` ## Error Handling In 1.0.0, we unified the return type of Zenoh functions asz_result_t. For example, - Zenoh 0.11.x ```rust int8_t z_put(struct z_session_t session, struct z_keyexpr_t keyexpr, const uint8_t *payload, size_t len, const struct z_put_options_t *opts); ``` - Zenoh 1.0.0 ```rust z_result_t z_put(const struct z_loaned_session_t *session, const struct z_loaned_keyexpr_t *key_expr, struct z_owned_bytes_t *payload, struct z_put_options_t *options); ``` ## KeyExpr / String Conversion In 1.0.0, we have reworked the conversion between key expressions and strings, illustrated below. - Zenoh 0.11.x ```cpp // keyexpr => string z_owned_str_t keystr = z_keyexpr_to_string(z_loan(z_owned_keyexpr_t)); // keyexpr => const char * z_loan(keystr) // const char* => keystr z_owned_keyexpr_t keyexpr = z_keyexpr_new(const char*) ``` - Zenoh 1.0.0 ```cpp // keyexpr => string z_view_string_t keystr; z_keyexpr_as_view_string(z_loan(z_owned_keyexpr_t), &keystr); // z_view_string_t => const char* z_string_data(z_loan(keystr)) // const char* => keystr z_owned_keyexpr_t keyexpr; z_error_t res = z_keyexpr_from_string(&keyexpr, const char *); ``` NOTE: Based on efficiency considerations, the char pointer ofz_string_datamight not be null-terminated in zenoh-c. To read the string data, we need to pair it with the lengthz_string_len. ```text z_view_str_t keystr; z_keyexpr_as_view_string(z_loan(keyexpr), &keystr); printf("%.*s", (int)z_string_len(z_loan(keystr)), z_string_data(z_loan(keystr))); ``` And to compare the string withstrncmpinstead ofstrcmp. ```cpp z_view_str_t keystr; z_keyexpr_as_view_string(z_loan(keyexpr), &keystr); const char* target = "string"; strncmp(target, z_string_data(z_loan(keystr)), z_string_len(z_loan(keystr))); ``` ## Accessor Pattern In 1.0.0, we have made our API more convenient and consistent to use across languages. We use opaque types to wrap the raw Zenoh data from the Rust library in zenoh-c. With this change, we introduce the accessor pattern to read the field of a struct. For instance, to get the attachment of a sample in zenoh-c: - 0.11.0 ```python typedef struct z_sample_t { struct z_keyexpr_t keyexpr; struct z_bytes_t payload; struct z_encoding_t encoding; const void *_zc_buf; enum z_sample_kind_t kind; struct z_timestamp_t timestamp; struct z_qos_t qos; struct z_attachment_t attachment; } z_sample_t; ``` - 1.0.0 Opaque type ofz_sample ```rust /// An owned Zenoh sample. /// /// This is a read only type that can only be constructed by cloning a `z_loaned_sample_t`. /// Like all owned types, it should be freed using z_drop or z_sample_drop. #[derive(Copy, Clone)] #[repr(C, align(8))] pub struct z_owned_sample_t { _0: [u8; 224], } /// A loaned Zenoh sample. #[derive(Copy, Clone)] #[repr(C, align(8))] pub struct z_loaned_sample_t { _0: [u8; 224], } ``` Get attachment ```rust const struct z_loaned_bytes_t *z_sample_attachment(const struct z_loaned_sample_t *this_); ``` In zenoh-pico, we recommend users follow the accessor pattern even though the structz_sample_tis explicitly defined in the library. ## Usage ofz_bytes_clone In short,z_owned_bytes_tis made of reference-counted data slices. In 1.0.0, we aligned the implementation ofz_bytes_cloneand made it perform a shallow copy for improved efficiency. - Zenoh 0.11.x ```rust ZENOHC_API struct zc_owned_payload_t zc_sample_payload_rcinc(const struct z_sample_t *sample); ``` - Zenoh 1.0.0 ```c ZENOHC_API void z_bytes_clone(struct z_owned_bytes_t *dst, const struct z_loaned_bytes_t *this_); ``` NOTE: We don’t offer a deep copy API. However, users can create a deep copy by deserializing thez_bytes_tinto a zenoh object. For example, usez_bytes_deserialize_into_sliceto deserialize it into az_owned_slice_tand then callz_slice_datato obtain a pointer touint8_tdata. ## Zenoh-C Specific ### Shared Memory Shared Memory subsystem has been heavily reworked and improved. The key functionality changes are: - Buffer reference counting is now robust across abnormal process termination - Support plugging of user-defined SHM implementations - Dynamic SHM transport negotiation: Sessions are interoperable with any combination of SHM configuration and physical location - Support aligned allocations - Manual buffer invalidation - Buffer write access - Rich buffer allocation interface ⚠️ Please note that SHM API is still unstable and will be improved in the future. ### SharedMemoryManager → ShmProvider + ShmProviderBackend - Zenoh 0.11.x ```cpp // size to dedicate to SHM manager const size_t size = 1024 * 1024; // construct session id string const z_id_t id = z_info_zid(z_loan(s)); char idstr[33]; for (int i = 0; i < 16; i++) { sprintf(idstr + 2 * i, "%02x", id.id[i]); } idstr[32] = 0; // create SHM manager zc_owned_shm_manager_t manager = zc_shm_manager_new(z_loan(s), idstr, size); ``` - Zenoh 1.0.0 ```cpp // size to dedicate to SHM provider const size_t total_size = 1024 * 1024; // Difference: SHM provider now respects alignment z_alloc_alignment_t alignment = {0}; z_owned_memory_layout_t layout; z_memory_layout_new(&layout, total_size, alignment); // create SHM provider z_owned_shm_provider_t provider; z_posix_shm_provider_new(&provider, z_loan(layout)); ``` ### Buffer allocation - Zenoh 0.11.x ```cpp // buffer size const size_t alloc_size = 1024; // allocate SHM buffer zc_owned_shmbuf_t shmbuf = zc_shm_alloc(&manager, alloc_size); if (!z_check(shmbuf)) { zc_shm_gc(&manager); shmbuf = zc_shm_alloc(&manager, alloc_size); if (!z_check(shmbuf)) { printf("Failed to allocate an SHM buffer, even after GCing\n"); exit(-1); } } ``` - Zenoh 1.0.0 ```cpp // buffer size and alignment const size_t alloc_size = 1024; // Diffrence: allocation now respects alignment z_alloc_alignment_t alignment = {0}; // allocate SHM buffer z_buf_layout_alloc_result_t alloc; // Difference: there is a rich set of policies available to control // allocation behavior and handle allocation failures automatically z_shm_provider_alloc_gc(&alloc, z_loan(provider), alloc_size, alignment); if (!z_check(alloc.buf)) { printf("Failed to allocate an SHM buffer, even after GCing\n"); exit(-1); } ``` --- # Zenoh in action · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/overview/zenoh-in-action # Source: https://zenoh.io/docs/overview/zenoh-in-action # Zenoh in action Let us now look into a sample scenario of Zenoh working. Zenoh supports two paradigms of communication - publish-subscribe and queries. - Pub/Sub in Zenoh - Query in Zenoh ## Pub/Sub in Zenoh This animation shows a basic pub/sub in action. The subscribers connected to the system receive the values sent by the publishers routed efficicently through the Zenoh network. You can also observe the presence of a sleeping subscriber connected to the network. Once the subscriber awakes, the nearest Zenoh node will send the pending publications. ## Queries in Zenoh This animation shows a simple query in action through Zenoh. You can see the presence of storages and queryables. A queryable is any process that can reply to queries. A storage is a combination of a subscriber and a queryable. In the following sections, we introduce the primitives and abstractions in Zenoh that enables this sample scenario. --- # Zenoh plugins · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/manual/plugins # Source: https://zenoh.io/docs/manual/plugins # Zenoh plugins The Zenoh router (zenohdexecutable) supports the loading of plugins at start-up, or at runtime if write permission is configured on its admin space. A Zenoh plugin is a library that can be loaded by the Zenoh router at start-up. It shares a runtime with it, allowing the plugin to use the regular Zenoh rust APIs with the same peer ID. Zenoh already provides the following plugins in its default repository: - theREST plugin: providing the Zenoh REST API - theStorage Manager plugin: providing management ofstorages Zenoh relies on the configuration files provided to decide the plugins to be loaded at startup. If a plugin is added to the configuration during runtime (for example through theadmin space), it will be loaded then. The configuration has 2 fields that is related to plugins: - plugins, where you may specify which plugins you require, as well as provide configuration for them. - plugins_search_dirs, where you may specify a list of directories wherezenohdshould look for the specified plugins. ### Thepluginsconfiguration field This field may contain a dictionary, where each key is the configured plugin’s name, and the associated value is a dictionary holding its configuration. In this dictionary, 2 properties are reserved byzenohd: - __path__may hold a string or list of strings, which are the paths where the plugin is expected to be located. If this option is defined, lookup-by-name is disabled for the requested plugin, and the first path to resolve in a successful load will be used as the plugin's path. - __required__may hold a boolean value. If set totrue,zenohdwill panic if unable to load the requested plugin. Otherwise, it will simply log an error. Plugins are encouraged to look to this field to ascertain whether they should be allowed to panic or not. The rest of the dictionary may be used by each plugin as they see fit. If elements of their configuration are modified at runtime, plugins will be given a chance to observe the modification and interfere with it. --- # Migrating from Zenoh v0.5.x to Zenoh v0.6.x · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-v0.5.x-v0.6.x # Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-v0.5.x-v0.6.x # Migrating from Zenoh v0.5.x to Zenoh v0.6.x ## Key expressions Some key expressions are now considered invalid: - Heading slashes are forbidden. Example:"/key/expression". - Trailing slashes are forbidden. Example:"key/expression/". - Empty chunks are forbidden. Example:"key//expression". An error will be returned when trying to use such invalid key expressions. ## APIs In zenoh version 0.6.0, zenoh and zenoh-net APIs have been merged into a single API. ## Configuration In v0.5.x the Zenoh configuration was a list of key/value pairs. In v0.6.x the has a structured format which can be expressed in JSON, JSON5 or YAML. zenoh configuration has moved from a list of key value pairs to a more structured configuration that can be expressed as JSON5. Here was the configuration used by default by the zenoh router v0.5.x: ```bash mode=peer peer= listener= user= password= multicast_scouting=true multicast_interface=auto multicast_ipv4_address=224.0.0.224:7447 scouting_timeout=3.0 scouting_delay=0.2 add_timestamp=false link_state=true user_password_dictionary= peers_autoconnect=true tls_server_certificate= tls_root_ca_certificate= shm=true routers_autoconnect_multicast=false routers_autoconnect_gossip=false local_routing=true join_subscriptions= join_publications= link_lease=10000 link_keep_alive=2500 seq_num_resolution=268435456 open_timeout=10000 open_pending=1024 peer_id= batch_size=65535 max_sessions=1024 max_links=4 version= qos=true join_interval=2500 defrag_buff_size=1073741824 link_rx_buff_size=16777216 multicast_ipv6_address=[ff24::224]:7447 ``` Here is the new configuration (JSON5 format) used by default by the zenoh router v0.6.x: ```json5 { id: null, mode: "router", connect: { endpoints: [] }, listen: { endpoints: [ "tcp/0.0.0.0:7447" ] }, scouting: { timeout: null, delay: null, multicast: { enabled: true, address: null, interface: null, autoconnect: null, listen: null }, gossip: { enabled: null, autoconnect: null } }, timestamping: { enabled: null, drop_future_timestamp: null }, queries_default_timeout: null, routing: { peer: { mode: null } }, aggregation: { subscribers: [], publishers: [] }, transport: { unicast: { accept_timeout: 10000, accept_pending: 100, max_sessions: 1000, max_links: 1 }, multicast: { join_interval: 2500, max_sessions: 1000 }, qos: { enabled: true }, link: { tx: { sequence_number_resolution: 268435456, lease: 10000, keep_alive: 4, batch_size: 65535, queue: { size: { control: 1, real_time: 1, interactive_high: 1, interactive_low: 1, data_high: 2, data: 4, data_low: 4, background: 4 }, backoff: 100 }, threads: 2 }, rx: { buffer_size: 65535, max_message_size: 1073741824 }, tls: { root_ca_certificate: null, server_private_key: null, server_certificate: null, client_auth: null, client_private_key: null, client_certificate: null } }, shared_memory: { enabled: true }, auth: { usrpwd: { user: null, password: null, dictionary_file: null }, pubkey: { public_key_pem: null, private_key_pem: null, public_key_file: null, private_key_file: null, key_size: null, known_keys_file: null } } }, adminspace: { permissions: { read: true, write: false, }, }, plugins_search_dirs: [], plugins: { rest: { http_port: "8000" } } } ``` For more details on the configuration file, seehttps://github.com/eclipse-zenoh/zenoh/blob/0.6.0-beta.1/DEFAULT_CONFIG.json5 ## Admin space In zenoh version 0.5.0 the admin space was returning router information on@/router/, but was mainly used for management of Backends and Storages with put/delete/get on@/router//plugin/storages/backend/for Backends and@/router//plugin/storages/backend//storage/for Storages. In zenoh version 0.6.0 the admin space is splitted in 3 parts: - @/router/:read-onlykey returning the status of the router itself - @/router//config/**:write-onlysubset of keys to change the configuration. The keys under this prefix exactly map the configuration file structure. Not all configuration keys can be changed, but the storages plugin configuration can in order to add/remove Backends and Storages (see below). - @/router//status/plugins/**:read-onlysubset of keys to retrieve the status of plugins (and Backends and Storages) In zenoh version 0.5.0, the admin space was writeable by default. In v0.6.0 it’s read-only by default. Read/Write permission on its admin space can be now configured for a Zenoh router in 2 ways: - via the configuration file in theadminspace.permissionssection - via thezenohdcommand line option:--adminspace-permissions <[r|w|rw|none]> ## Plugins configurations In zenoh version 0.5.0 the plugins were automatically loaded byzenohdat startup when it detected their libraries on disk (libzplugin_*.sofiles).In zenoh version 0.6.0 a plugin is loaded at startup only if there is a configuration defined for it (in thepluginsJSON5 struct).Note that in the default configuration the REST plugin is configured withport: 8000, meaning that this plugin is automatically loaded at startup. To not load is, set a configuration file without therestconfiguration. A plugin can also be dynamically configured and loaded at runtime, publishing its configurartion in the zenoh router's admin space. For instance, to make a router to load the webserver plugin, you can publish to the REST API:curl -X PUT -H 'content-type:application/json' -d '{http_port: "8080"}' http://localhost:8000/@/router/local/config/plugins/webserver ## Backends/Storages configurations Zenoh 0.5.0 was not making the distinction between the Backend library, and an instanciation of this library. Thus, it was not possible for instance to configure for the same router twice the InfluxDB backend but with different URL to disctinct InfluxDB servers. Zenoh 0.6.0 introduces the concept of Volume as an instance of a Backend library. To summarize: - aBackendis a dynamic library (.so file) implementing Zenoh storages with the help of a specific technology (e.g. InfluxDB, RocksDB or the local file system) - aVolumeis an instance of a Backend with a specific configuration (e.g. an instance of the InfluxDB backend with configured URL and credentials to for an InfluxDB server) - aStorageis attached to a Volume and configured with the specification of a sub-space in this Volume (e.g. a database name, a directory on filesystem…). It’s also configured with the Zenoh key expression it must subscribes to (and replies to queries). Starting from zenoh 0.6.0, the Backends and Storages are configurable for startup via the router configuration file, configuring thestorage_managerplugin. Here is an examples of configuration file with several Backends/Storages: ```json5 { plugins: { // configuration of "storage_manager" plugin: storage_manager: { backends: { volumes: { // configuration of a "influxdb" volume using the "zbackend_influxdb" influxdb: { // URL to the InfluxDB service url: "http://localhost:8086", }, // configuration of a 2nd volume using the "zbackend_influxdb" but using a different URL influxdb2: { backend: "influxdb", // URL to the InfluxDB service url: "http://192.168.1.2:8086", }, // configuration of a "fs" volume using the "zbackend_fs" library (fs = file system) fs: {}, }, storages: { // configuration of a "storage1" storage using the "influxdb" volume storage1: { // the key expression this storage will subscribes to key_expr: "demo/storage1/**", // this prefix will be stripped from the received key when converting to database key. // i.e.: "demo/storage1/a/b" will be stored as "a/b" // this option is optional strip_prefix: "demo/storage1", volume: { id: "influxdb", // the database name within InfluxDB db: "zenoh_example", // if the database doesn't exist, create it create_db: true, } }, // configuration of a "storage1" storage using the "influxdb2" volume storage2: { // the key expression this storage will subscribes to key_expr: "demo/storage2/**", strip_prefix: "demo/storage2", volume: { id: "influxdb2", // the database name within InfluxDB db: "zenoh_example", // if the database doesn't exist, create it create_db: true, } }, // configuration of a "demo" storage using the "fs" volume fs_storage: { // the key expression this storage will subscribes to key_expr: "demo/example/**", strip_prefix: "demo/example", volume: { id: "fs", // the key/values will be stored as files within this directory (relative to ${ZBACKEND_FS_ROOT}) dir: "example" } }, // configuration of a "demo" storage using the "memory" volume (which is always present by default) in_memory: { key_expr: "demo/example/**", volume: { id: "memory" } } } } } } } ``` Providing that the Zenoh router is configured with write permission on its admin space, the Volumes and Storages can be dynamically added/removed at runtime via PUT/GET on the admin space, updating the storage_manager plugin config under@/router//config/plugins/storage_manager. Examples: - to add a memory Storage: ```bash curl -X PUT -H 'content-type:application/json' -d '{key_expr:"demo/example/**",volume:{id:"memory"}}' http://localhost:8000/@/router/local/config/plugins/storage_manager/storages/demo ``` - to add an InfluxDB Volume: ```bash curl -X PUT -H 'content-type:application/json' -d '{url:"http://localhost:8086"}' http://localhost:8000/@/router/local/config/plugins/storage_manager/volumes/influxdb ``` - to add an InfluxDB Storage: ```bash curl -X PUT -H 'content-type:application/json' -d '{key_expr:"demo/example/**",db:"zenoh_example",create_db:true,volume:{id:"influxdb"}}' http://localhost:8000/@/router/local/config/plugins/storage_manager/storages/demo2 ``` --- # Migrating from Zenoh-C v0.5.x zenoh-net API to Zenoh-C v0.6.x zenoh API · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-c-v0.5.x-v0.6.x # Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-c-v0.5.x-v0.6.x # Migrating from Zenoh-C v0.5.x zenoh-net API to Zenoh-C v0.6.x zenoh API ### Opening a session All types and operations from thezn_*primitives have been updated and migrated to thez_*primitives. zenoh v0.5.x ``` zn_properties_t *config = zn_config_default(); zn_session_t *s = zn_open(config); if (s == NULL) { printf("Unable to open session!\n"); exit(-1); } ``` zenoh v0.6.x ``` z_owned_config_t config = z_config_default(); z_owned_session_t s = z_open(z_move(config)); if (!z_check(s)) { printf("Unable to open session!\n"); exit(-1); } ``` ### Subscribing For this release, Zenoh-C only supports subscribers with callbacks. It is possible to access samples through a callback by calling thecallbackfunction passed as argument ondeclare_subscriberfunction. When declaring a subscriber, keyexpr optimizations (i.e., keyexpr declaration) will be automatically performed if required. Finer configuration is performed with the help of an options struct. zenoh-net v0.5.x ``` void data_handler(const zn_sample_t *sample, const void *arg) { printf(">> [Subscription listener] Received (%.*s, %.*s)\n", sample->key.len, sample->key.val, sample->value.len, sample->value.val); } // (...) zn_subscriber_t *sub = zn_declare_subscriber(s, zn_rname("/key/expression"), zn_subinfo_default(), data_handler, NULL); if (sub == NULL) { printf("Unable to declare subscriber.\n"); exit(-1); } ``` zenoh v0.6.x ``` void data_handler(const z_sample_t *sample, void *arg) { char *keystr = z_keyexpr_to_string(sample->keyexpr); printf(">> [Subscriber] Received ('%s': '%.*s')\n", keystr, (int)sample->payload.len, sample->payload.start); free(keystr); } // (...) z_owned_closure_sample_t callback = z_closure(data_handler); z_owned_subscriber_t sub = z_declare_subscriber(z_loan(s), z_keyexpr("key/expression"), z_move(callback), NULL); if (!z_check(sub)) { printf("Unable to declare subscriber.\n"); exit(-1); } ``` ### Publishing Thewriteoperation has been replaced by aputoperation. When declaring a publisher, keyexpr optimizations (i.e., keyexpr declaration) will be automatically performed if required. Finer configuration is performed with the help of an options struct. zenoh-net v0.5.x ``` zn_write(s, reskey, "value", strlen("value")); ``` zenoh v0.6.x ``` if (z_put(z_loan(s), z_loan(keyexpr), "value", strlen("value"), NULL) < 0) { printf("Put has failed!\n"); } ``` Thewrite_extoperation has been removed. Configuration is now performed with the help of a option struct. zenoh-net v0.5.x ``` zn_write_ext(s, reskey, "value", strlen("value"), Z_ENCODING_TEXT_PLAIN, Z_DATA_KIND_DEFAULT, zn_congestion_control_t_BLOCK); ``` zenoh v0.6.x ``` z_put_options_t options = z_put_options_default(); options.congestion_control = Z_CONGESTION_CONTROL_DROP; options.encoding = Z_ENCODING_PREFIX_TEXT_PLAIN; options.priority = Z_PRIORITY_DATA; if (z_put(z_loan(s), z_loan(keyexpr), "value", strlen("value"), &options) < 0) { printf("Put has failed!\n"); } ``` Thedeclare_publishernow returns a publisher object upon whichputanddeleteoperations can be performed. zenoh-net v0.5.x ``` zn_publisher_t *pub = zn_declare_publisher(s, reskey); if (pub == NULL) { printf("Unable to declare publisher.\n"); exit(-1); } ``` zenoh v0.6.x ``` z_owned_publisher_t pub = z_declare_publisher(z_loan(s), z_keyexpr("key/expression"), NULL); if (!z_check(pub)) { printf("Unable to declare publisher!\n"); exit(-1); } z_publisher_put(z_loan(pub), "value", strlen("value"), NULL); ``` ### Querying Thequery_collectoperation has been replaced by agetoperation. Thegetoperation is no longer blocking and returning a list of replies, but instead it makes replies accessible through a callback by calling thecallbackfunction passed as argument ongetfunction. When declaring a publisher, keyexpr optimizations (i.e., keyexpr declaration) will be automatically performed if required. Finer configuration is performed with the help of an options struct. zenoh-net v0.5.x ``` void reply_handler(const zn_source_info_t *info, const zn_sample_t *sample, const void *arg) { printf(">> [Reply handler] received (%.*s, %.*s)\n", sample->key.len, sample->key.val, sample->value.len, sample->value.val); } // (...) zn_query(s, zn_rname("/key/expression"), "", zn_query_target_default(), zn_query_consolidation_default(), reply_handler, NULL); ``` zenoh v0.6.x ``` void reply_dropper(void *ctx) { printf(">> Received query final notification\n"); } void reply_handler(z_owned_reply_t *oreply, void *ctx) { if (z_reply_is_ok(oreply)) { z_sample_t sample = z_reply_ok(oreply); char *keystr = z_keyexpr_to_string(sample.keyexpr); printf(">> Received ('%s': '%.*s')\n", keystr, (int)sample.payload.len, sample.payload.start); free(keystr); } else { printf(">> Received an error\n"); } } // (...) z_get_options_t opts = z_get_options_default(); opts.target = Z_QUERY_TARGET_ALL; z_owned_closure_reply_t callback = z_closure(reply_handler, reply_dropper); if (z_get(z_loan(s), z_keyexpr("key/expression"), "", z_move(callback), &opts) < 0) { printf("Unable to send query.\n"); exit(-1); } ``` ### Queryable It is possible to access queries through a callback by calling thecallbackfunction passed as argument ondeclare_queryablefunction. When declaring a queryable, keyexpr optimizations (i.e., keyexpr declaration) will be automatically performed if required. Finer configuration is performed with the help of an options struct. Thesend_replyoperation has been also extended with an options struct for finer configuration. zenoh-net v0.5.x ``` void query_handler(zn_query_t *query, const void *ctx) { z_string_t res = zn_query_res_name(query); z_string_t pred = zn_query_predicate(query); printf(">> [Query handler] Handling '%.*s?%.*s'\n", (int)res.len, res.val, (int)pred.len, pred.val); zn_send_reply(query, "/key/expression", "value", strlen("value")); } // (...) zn_queryable_t *qable = zn_declare_queryable(s, zn_rname("/key/expression"), ZN_QUERYABLE_EVAL, query_handler, NULL); if (qable == NULL) { printf("Unable to declare queryable.\n"); exit(-1); } ``` zenoh v0.6.x ``` void query_handler(const z_query_t *query, void *ctx) { char *keystr = z_keyexpr_to_string(z_query_keyexpr(query)); z_bytes_t pred = z_query_value_selector(query); printf(">> [Queryable ] Received Query '%s%.*s'\n", keystr, (int)pred.len, pred.start); z_query_reply(query, z_keyexpr("key/expression"), "value", strlen("value"), NULL); free(keystr); } // (...) z_owned_closure_query_t callback = z_closure_query(query_handler); z_owned_queryable_t qable = z_declare_queryable(z_loan(s), z_keyexpr("key/expression"), z_closure(callback), NULL); if (!z_check(qable)) { printf("Unable to create queryable.\n"); exit(-1); } ``` ### Examples More examples are available here: zenoh v0.6.0 zenoh-net v0.5.0-beta9 --- # User-Password authentication · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/manual/user-password # Source: https://zenoh.io/docs/manual/user-password # User-Password authentication Zenoh supports basicuser-password authentication. Clients and peers can useuserandpasswordfor authentication against a router or a peer. Similarly, peers and routers can useuserandpasswordfor authentication among themselves. The configuration of credentials is done via aconfiguration file. ## Client configuration The required configuration fields for aclientwould hence be: ``` { /// The node's mode (router, peer or client) mode: "client", transport: { auth: { /// The configuration of authentication. /// A password implies a username is required. usrpwd: { user: "clientusername", password: "clientpassword", }, }, }, } ``` When using such configuration, the client will use the provideduserandpasswordto authenticate against any peer or router. Let’s assume the above configuration is then saved with the nameclient.json5. ## Router or peer configuration The required configuration fields for arouteror apeerwould hence be: ``` { /// The node's mode (router, peer or client) mode: "router", transport: { auth: { /// The configuration of authentication. usrpwd: { user: "routerusername", password: "routerpassword", /// The path to a file containing the user password dictionary dictionary_file: "credentials.txt", }, }, }, } ``` Thedictionary_fileindicates the path of a text file containing the full list of authorized credentials for connection. The authorized credentials text file would hence be: ``` clientusername:clientpassword ``` Each line of the file represents the one single authorized credential in the form ofuser:password. The authorized credentials text file can contain any number of lines. When using such configuration, the router or peer will use the provideduserandpasswordto authenticate against any other peer or router. At the same time, it will use the file containing the authorized credentials to authenticate incoming connections. Let’s assume that the above configurations are then saved with the namerouter.json5andcredentials.txt. ## Testing the user-password authentication Let’s assume a scenario with one Zenoh router and two clients connected to it: one publisher and one subscriber. The first thing to do is to run the router passing its configuration, i.e.router.json5: ``` $ zenohd -c router.json5 ``` Make sure that the path indicated in theuser_password_dictionaryproperty points to a valid credentials file. Then, let’s start the subscriber in client mode passing its configuration, i.e.client.json5: ``` $ z_sub -c client.json5 ``` Lastly, let’s start the publisher in client mode passing its configuration, i.e.client.json5: ``` $ z_pub -c client.json5 ``` As it can be noticed, the sameclient.json5is used forz_subandz_put. Both are using the same credentials and are authenticated accordingly by therouter. Nevertheless, different configuration files and credentials could be used. ## Final remark Consider to not store clear text password in the configuration files. Before creating the configuration files, a hashing function should be used to hash the password. For example, let’s compute the hash for theclientandrouterpasswords: ``` $ echo clientpassword | sha256sum 92df0d4f39c218607e3200bf93ccac87a80cb910e811d84b286bebe0a8860724 $ echo routerpassword | sha256sum 2ce01a11893f276a4064f586f24b8f1868008e2e623f6c4e74ef381247e49df1 ``` Therefore, theclient.json5would become: ``` { /// The node's mode (router, peer or client) mode: "client", transport: { auth: { /// The configuration of authentication. /// A password implies a username is required. usrpwd: { user: "clientusername", password: "92df0d4f39c218607e3200bf93ccac87a80cb910e811d84b286bebe0a8860724", }, }, }, } ``` And therouter.json5file would become: ``` { /// The node's mode (router, peer or client) mode: "router", transport: { auth: { /// The configuration of authentication. usrpwd: { user: "routerusername", password: "2ce01a11893f276a4064f586f24b8f1868008e2e623f6c4e74ef381247e49df1", /// The path to a file containing the user password dictionary dictionary_file: "credentials.txt", }, }, }, } ``` Finally, thecredentials.txtfile would become: ``` clientusername:92df0d4f39c218607e3200bf93ccac87a80cb910e811d84b286bebe0a8860724 ``` --- # Troubleshooting · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/getting-started/troubleshooting # Source: https://zenoh.io/docs/getting-started/troubleshooting # Troubleshooting ## Activate logging Activating the Zenoh logging can provide useful information for any troubleshooting. The Zenoh router (zenohd) and all the Zenoh APIs (except zenoh-pico) are developed with a Rust code base. Logging is controlled via theRUST_LOGenvironment variable that can typically be defined with the desired logging level amongst: - error- this is the default level ifRUST_LOGis not defined - warn - info - debug - trace - off- to disable all logging More advanced logging directives can be defined via theRUST_LOG. For more details see thispage.Note that the logs are written onstderr. ## Known troubles with the Zenoh router (zenohd) ### Segmentation fault at startup The router is likely trying to load an incompatible plugin. To check this look for such logs: ``` [2022-03-28T15:23:36Z INFO zenohd] Successfully started plugin rest from "/Users/test/.zenoh/lib/libzplugin_rest.dylib" [2022-03-28T15:23:36Z INFO zenohd] Successfully started plugin storage_manager from "/Users/test/.zenoh/lib/libzplugin_storage_manager.dylib" [2022-03-28T15:23:36Z INFO zenohd] Successfully started plugin webserver from "/Users/test/.zenoh/lib/libzplugin_webserver.dylib" ``` Here you can see all the plugins libraries that have been loaded byzenohdat startup. You must check if each of the plugins are using the same Zenoh version as dependency thanzenohdand Rust compiler version as thezenohdinstance.To assess which one is causing troubles, you can also move or rename all the libraries but one and test ifzenohdis correctly loading this one. Then repeat the process for each library. ## Known troubles with the APIs ### “Error treating timestamp for received Data” If you get such log: ``` [2021-08-23T08:44:47Z ERROR zenoh::net::routing::pubsub] Error treating timestamp for received Data (incoming timestamp from E74B6FF3D82D49BEA11B8F1BD0AC4C7A exceeding delta 100ms is rejected: 2021-08-23T08:44:47.299498897Z vs. now: 2021-08-23T08:44:47.069513846Z): drop it! ``` It means your Zenoh application on your local host received a publication from another remote host with a system time that is drifting in the future for more than 100ms with respect to the local system time. You should synchronize the time between your hosts using a tool such asPTPorNTP. --- # Configuration Source: https://zenoh.io/docs/manual/configuration From version 0.6 of Zenoh, configuration has changed in major ways. This page will take you through the new behaviour of configuration, whether you're using Zenoh as a library, or as an executable through zenohd. ## Configuring zenohd There are 3 ways to configure zenohd, which may be used in any combination: - using a configuration file, - through the command line arguments, - and by putting values on the configuration through the admin space. ## Configuration files zenohd has supported configuration files for a long time now, but with version 0.6, we hope to make this the primary interface for configuring your Zenoh infrastructure. As was the case before, you can specify which configuration file to load with the `--config=/path/to/config/file` CLI argument. If no path is specified, zenohd will use a default configuration instead. Currently, JSON5 and YAML are the primary configuration format (as opposed to v0.5's flat key-value files), but we may add support for other serialization formats in the future. An example configuration can be read here, apart from the `plugins` section, we make an effort to keep the values aligned with the defaults. The exact schema for the configuration is the Config structure, which can be found in this file. Don't be alarmed, all of these fields are optional. Only configure the parts that are of interest to you. We'd like to bring your attention to the `plugins` part of the configuration, as plugin management has also changed a lot with version 0.6. More on this in the page on plugins. ## Command line arguments If you want to run zenohd with small changes in its configuration, without going through the hassle of writing a new configuration file for it, you may use the `--cfg` CLI argument to edit the configuration. Specifically, you may use any amount of `--cfg='PATH:VALUE'` arguments to specify the VALUEs you'd like to insert at specific PATHs in the configuration. PATHs are `/`-separated paths to the part of the configuration you wish to change. Note for plugins that setting a value in a plugin-less configuration for `plugins/example-plugin/example/path` will result in the recursive creation of the intermediate objects if necessary. VALUEs must be JSON5-deserializable values: don't forget to surround strings with quotes. Due to this, surrounding the whole `PATH:VALUE` pair with single-quotes is a good practice to avoid parsing errors. If a value was already present for the specified PATH, it will be replaced with VALUE. For convenience, some arguments of zenohd are provided as shorthands for particularly useful `--cfg` patterns, such as `-P ` which desugars to `--cfg='plugins//__required__:true'`. In case of conflicts, `--cfg` options will override any other sources of configuration for their PATH. You can use `--rest-http-port=8000` to enable the REST plugin in zenohd. ## Reactive configuration It is possible to register callbacks that will be called when the configuration structure is modified. This lets zenohd (or your own application) react to changes in the configuration during runtime. In the case of zenohd, the only user-accessible way of editing the configuration during runtime is through the admin space, as explained a bit further in this page. Whether and how to react to modifications to the configuration file when it exists is still under debate by the core team. ## Admin space configuration The configuration of a Zenoh router can be changed at runtime via its admin space, if it's configured to be writeable: - either via the configuration file in the `adminspace.permissions` section ```json { adminspace: { permissions: { read: true, write: true } } } ``` - either via the zenohd command line option: `--adminspace-permissions <[r|w|rw|none]>` Then you can change elements of its configuration once it's started, by sending PUT messages to its admin space. If one of the zenohd instances uses the REST plugin to expose Zenoh to HTTP requests, this can be done simply by sending such requests with tools such as curl. Remember to enable the REST plugin in zenohd with the command line option `--rest-http-port=8000`. To do this, use commands such as ```bash curl -X PUT http://localhost:8000/@/local/router/config/plugins/storage_manager/storages/my-storage -d '{key_expr:"demo/mystore/**", volume:{id:"memory"}}' # ^- REST plugin addr ^ ^--- config space --^ ^---- the path to the configured value ---^ ^-------------- the value to insert ----------------^ ``` Path-value pairs work much like they do when using CLI arguments. Note that while you may attempt to change any part of the configuration through this mean, not all of its properties are actually watched. For example, while the storage plugin watches for any change in its configuration and will attempt to act on it, the REST plugin will only log a warning that it observed a change, but won't apply it. Changes to non-plugin parts of the configuration may be registered by the configuration, but not acted upon, such as the `mode` field of the configuration which is only ever read at startup. --- # Migrating from Zenoh-C to Zenoh-Pico (and vice-versa) Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-zenohc-zenohpico Both Zenoh-C and Zenoh-Pico APIs offer a C client API for the zenoh protocol, thus this release took an extra step to make Zenoh-C code to be compatible with Zenoh-Pico code (and vice-versa). Such approach aids users to easily migrate its Zenoh-based code to microcontrollers and embedded systems. Nevertheless, in order to keep your code optimal some minor changes might be required while moving from Zenoh-C to Zenoh-Pico: - `zc_*` refers to Zenoh-C API only, while `zp_*` refers to Zenoh-Pico API only. - Zenoh configurations are handled in a different way. - Read and Lease tasks must be spawn and destroyed explicitly by the user. - Keyexpr to_string vs resolve Everything else should be copy-paste friendly. Note that, although the API between Zenoh-C and Zenoh-Pico are the same, their ABI is different w.r.t. how parameters are passed to functions. Still, `z_loan` and `z_move` hide those differences from the user. ## zc_* and zp_* In order to help users to identify what functions / helpers are Zenoh-C or Zenoh-Pico exclusive, they are prefixed respectively with `zc_*` and `zp_*`. Everything else (i.e., `z_*` functions and helpers) are common to both APIs. For the moment, their use is limited to the Zenoh configurations (to support more idiomatic usages on their target platform), and tasks (to support single-thread or multi-thread in Zenoh-Pico). ## Zenoh configurations Due to system-wide differences between microcontrollers / embedded systems and computers, configurations must be handled in more idiomatic way. While computers can easily handle configuration files that are passed to running processes, microcontrollers / embedded systems tend to leverage more on compile-time configurations with limited runtime-configurations. In Zenoh-Pico, both types of configurations are available (check /path/to/zenoh-pico/include/zenoh-pico/config.h). On one hand, compile-time configurations can be done directly on the previous file, defined on the user code, or as build flags. On the other hand, runtime configurations are handled as a key-string mapping, as the following example: ```c z_owned_config_t config = z_config_default(); zp_config_insert(z_loan(config), Z_CONFIG_PEER_KEY, z_string_make("tcp/192.168.0.1:7447")); zp_config_insert(z_loan(config), Z_CONFIG_MODE_KEY, z_string_make("client")); zp_config_insert(z_loan(config), Z_CONFIG_SCOUTING_TIMEOUT_KEY, z_string_make("2000")); ``` In Zenoh-C, configuration may be loaded from JSON5 or YAML files, and is generally interacted in-code through JSON5. ```c z_owned_config_t config = z_config_default(); zc_config_insert_json(z_loan(config), Z_CONFIG_MODE_KEY, "\"client\""); char *mode = zc_config_get(z_loan(config), Z_CONFIG_MODE_KEY); assert(mode == "\"client\""); free(mode); ``` ## Read and Lease tasks (Zenoh-Pico only) As Zenoh-Pico is targeting microcontrollers and embedded systems, it is a requirement to support both single-thread and multi-thread implementations. To do so, the user has explicit control over the read and lease tasks. If multi-thread behavior is intended, the user must spawn both tasks manually by including the following lines after Zenoh Session is successfully open. ```c zp_start_read_task(z_loan(session), NULL); zp_start_lease_task(z_loan(session), NULL); ``` Likewise, the user must also destroy both tasks manually by including the following lines just before closing the session: ```c zp_stop_read_task(z_loan(session)); zp_stop_lease_task(z_loan(session)); ``` Note that, `z_close(z_move(s));` will stop and destroy the tasks if the user forgets to do it. However, for the sake of symmetric operations, the user is advised to stop them manually. If single-thread behavior is intended, the user must not spawn the any of the tasks. Instead, the user can a single execution of the read task and lease tasks at its own pace: ```c zp_read(z_loan(session), NULL); zp_send_keep_alive(z_loan(session), NULL); zp_send_join(z_loan(session), NULL); ``` ## Keyexpr to_string vs resolve Zenoh-C and Zenoh-Pico store keyexpr in a slightly different way. While Zenoh-C stores full expanded string for the keyexprs, Zenoh-Pico might store them in their declared form as a requirement for constrained devices. Thus, Zenoh-Pico requires to reconstruct the full expanded string for the keyexpr by using the `z_keyexpr_resolve`. In turn, Zenoh-C is able to return a pointer to the already full expanded string (by using `z_keyexpr_to_string`). Note that, in both cases extra memory allocations are performed that must be released by the user. ## zc_init_logger By default, `z_open` and `z_scout` will initialize the Zenoh logger. If you'd rather disable the logger (or have the option to), you may build zenoh-c with `DISABLE_LOGGER_AUTOINIT=true` in your cmake configuration options. If you do so, you may still initialize the logger manually using `zc_init_logger`. ## How to write code compatible with Zenoh-C and Zenoh-Pico simultaneously To enable the user to write code that works for both Zenoh-C and Zenoh-Pico simultaneously, both introduce a set of define directives. For Zenoh-C: ```c #define ZENOH_C "0.6.0" #define ZENOH_C_MAJOR 0 #define ZENOH_C_MINOR 6 #define ZENOH_C_PATCH 0 ``` For Zenoh-Pico: ```c #define ZENOH_PICO "0.6.0" #define ZENOH_PICO_MAJOR 0 #define ZENOH_PICO_MINOR 6 #define ZENOH_PICO_PATCH 0 ``` By using them, the user can have code that is only compiled against Zenoh-C or Zenoh-Pico libraries. For example: ```c // (...) z_owned_config_t config = z_config_default(); #ifdef ZENOH_C z_config_insert_json(z_loan(config), Z_CONFIG_CONNECT_KEY, "tcp/192.168.0.1:7447")) #endif #ifdef ZENOH_PICO zp_config_insert(z_loan(config), Z_CONFIG_PEER_KEY, z_string_make("tcp/192.168.0.1:7447")); #endif // (...) ``` --- # Java Source: https://zenoh.io/docs/migration_1.0/java ## Java API migration guide for 1.0.0 The API has been extensively modified with the following goals in mind: - Match the API rework done through the Rust Zenoh API. - Abstracting users from the underlying native mechanisms. - Making the API more idiomatic, more "imperative". ## Removal of the builder patterns and options parameters Throughout the 0.11.0 API, we were exposing builders, for instance: ```java session.put(keyExpr, value) .congestionControl(CongestionControl.BLOCK) .priority(Priority.REALTIME) .res() ```text This seemed odd, because "put" is an imperative statement. This could lead to confusions since it's not evident that instead of performing the put operation, that function returns a builder that must be built with a 'res()' (from resolve) function. After some deliberation, we opted for the following approach: - Making the put operation actually imperative, meaning that calling that function actually performs the put operation. No need for the `.res()` call anymore. - Provide configuration options with an optional parameter, in this case a `PutOptions` parameter: ```java var putOptions = new PutOptions(); putOptions.setCongestionControl(CongestionControl.BLOCK); putOptions.setPriority(Priority.REALTIME); //... session.put(keyExpr, payload, putOptions); // triggers the put ```text If not provided, the default configuration is used: `session.put(keyExpr, payload);` ## Session opening `Session.open(config: Config)` has now been replaced with `Zenoh.open(config: Config)`. ## Encoding rework The `Encoding` class has been modified. In 0.11.0 it had the signature ```java class Encoding(val knownEncoding: KnownEncoding, val suffix: String = "") ```text where `KnownEncoding` was an enum. In 0.11.0 an encoding instance would be created as follows: ```java var encoding = new Encoding(KnownEncoding.TEXT_JSON) ```text In 1.0.0 we have implemented the following changes: - `KnownEncoding` enum is removed, instead we provide static `Encoding` instances containing an ID and a description. - Custom encodings can be created - The list of pre-defined encodings has been expanded. In 1.0.0 the previous example would instead now become: ```java var encoding = Encoding.TEXT_JSON ```text ## Session-managed declarations Up until 0.11.0, it was up to the user to keep track of their variable declarations to keep them alive, because once the variable declarations were garbage collected, the declarations were closed. This was because each Kotlin variable declaration is associated with a native Rust instance, and in order to avoid leaking the memory of that Rust instance, it was necessary to free it upon dropping the declaration instance. However, this behavior could be counterintuitive, as users were expecting the declaration to keep running despite losing track of the reference to it. In this release we introduce a change in which any session declaration is internally associated to the session from which it was declared. Users may want to keep a reference to the declaration in case they want to undeclare it earlier before the session is closed, otherwise, the declaration is kept alive. For instance: ```java var keyExprA = KeyExpr::tryFrom("A/B/C"); var subscriber = session.declareSubscriber(keyExprA, { sample -> System.out.println("Receiving sample on 'A/B/C': " + sample.getPayload() + ")") }); var keyExprB = KeyExpr::tryFrom("A/C/D"); session.declareSubscriber(keyExprB, { sample -> System.out.println("Receiving sample on 'A/C/D': " + sample.getPayload() + ")") }); // No variable is associated to the declared session, on 0.11.0 it would have been instantly dropped ```text Therefore, when receiving a 'hello' message on `A/**` we would still see: ```text >> Receiving sample on 'A/B/C': hello >> Receiving sample on 'A/C/D': hello ```text since both declarations are still alive. Now the question is, for how long? What happens first, either when: - you call `undeclare()` or `close()` to the declaration - the session is closed, then all the associated declarations are automatically undeclared. ## Key Expression rework KeyExpr instances are not bound to a native key expression anymore, unless they are declared from a session. It is safe to drop the reference to the key expression instance, but the memory management associated to a key expression will differ: - If the KeyExpr was not declared from a session, then the garbage collector simply claims back the memory. - If it was declared from a session, then the session keeps track of it and frees the native memory upon closing the session. Declaring a KeyExpr on a session results in better performance, since the session is informed that we intend to use a key expression repeatedly. We also associate a native key expression to a Kotlin key expression instance, avoiding copies. ## Config loading When opening a session, it's now mandatory to provide a configuration to the session, even for a default config: ```java var config = Config.loadDefault(); var session = Zenoh.open(config); ```text The `Config` class has been modified: - `Config.loadDefault(): Config`: returns the default config - `Config.fromFile(file: File): Config`: allows to load a config file. - `Config.fromPath(path: Path): Config`: allows to load a config file from a path. - `Config.fromJson(json: String): Config`: loads the config from a string literal with json format - `Config.fromJson5(json5: String): Config`: loads the config from a string literal with json5 format - `Config.fromYaml(yaml: String): Config`: loads the config from a string literal with yaml format - `Config.fromJsonElement(jsonElement: JsonElement): Config`: loads the config from a kotlin JsonElement. In case of failure loading the config, an exception is thrown. ## Packages rework The package structure of the API is now aligned with Zenoh Rust package structure. Changes: - Removing the "prelude" package - QoS package now contains: `CongestionControl`, `Priority`, `Reliability`, `QoS` - Bytes package is created with: `ZBytes`, `IntoZBytes`, `Encoding` - Config package: `Config`, `ZenohId` - Session package: `SessionInfo` - Query package: contains `Query` and `Queryable` removing queryable package ## Reliability The `Reliability` config parameter used on when declaring a subscriber, has been moved. It now must be specified when declaring a `Publisher` or when performing a `Put` or a `Delete` operation. ## Logging There are two main changes regarding logging, the interface and the mechanism. Lets look at the following example, where we want to run the ZPub example with debug logging. On 1.0.0 we'll do: ```bash RUST_LOG=debug gradle ZPub ```text If we wanted to enable debug logging and tracing for some specific package, for instance `zenoh::net::routing`, we'd do: ```bash RUST_LOG=debug,zenoh::net::routing=trace gradle ZPub ```text However, this is not enabled by default. In order to enable logging, one of these newly provided functions must be called: ```java Zenoh.tryInitLogFromEnv(); ```text and ```java Zenoh.initLogFromEnvOr(fallbackFilter: String); ```text This last function allows to programmatically specify the logs configuration if not provided as an environment variable. ## ZBytes serialization / deserialization & replacement of Value We have created a new abstraction with the name of `ZBytes`. This class represents the bytes received through the Zenoh network. This new approach has the following implications: - `Attachment` class is replaced by `ZBytes`. - `Value` is replaced by the combination of `ZBytes` and `Encoding`. - Replacing `ByteArray` to represent payloads With `ZBytes` we have also introduced a Serialization and Deserialization for convenient conversion between `ZBytes` and Kotlin types. ### Serialization & Deserialization We can serialize primitive types into a `ZBytes` instance, that is, converting the data into bytes processed by the zenoh network. We'll see that for serialization and deserialization, we need to create instances of `ZSerializer` and `ZDeserializer` respectively. #### Primitive types (De)Serialization is supported by the following primitive types: - Numeric: `Byte`, `Short`, `Integer`, `Long`, `Float`, and `Double` - `String` - `ByteArray` For instance: ```java Integer input = 123456; ZSerializer serializer = new ZSerializer<>() {}; ZBytes zbytes = serializer.serialize(input); ZDeserializer deserializer = new ZDeserializer<>() {}; Integer output = deserializer.deserialize(zbytes); assert input.equals(output); ```text This approach works as well for the other aforementioned types. For serialization, `String` and `ByteArray` the functions `ZBytes::from(string: String)` and `ZBytes::from(bytes: ByteArray)` can be used respectively. Analogously, deserialization, `ZBytes::toString()` and `ZBytes::toByteArray()` can be used. For instance: ```java var exampleString = "example string"; var zbytes = ZBytes.from(exampleString); var output = zbytes.toString(); assert exampleString.equals(output); ```text #### Lists Lists are supported, but they must be either: - List of numeric types: (`Byte`, `Short`, `Int`, `Long`, `Float`, `Double`) - List of `String` - List of `ByteArray` - List of another supported type The serialize syntax must be used: ```java List input = List.of(1, 2, 3, 4, 5); ZSerializer> serializer = new ZSerializer<>() {}; ZBytes zbytes = serializer.serialize(input); ZDeserializer> deserializer = new ZDeserializer<>() {}; List output = deserializer.deserialize(zbytes); assert input.equals(output); ```text #### Maps Maps are supported as well, with the restriction that their inner types must supported primitives: - Numeric types - `String` - `ByteArray` - Map of another supported types ```java Map input = Map.of("one", 1, "two", 2, "three", 3); ZSerializer> serializer = new ZSerializer<>() {}; ZBytes zbytes = serializer.serialize(input); ZDeserializer> deserializer = new ZDeserializer<>() {}; Map output = deserializer.deserialize(zbytes); assert input.equals(output); ```text #### Parameterized types combinations Combinations of all the above types is supported. For instance: - List of lists ```java List> input = List.of(List.of(1, 2, 3)); ZSerializer>> serializer = new ZSerializer<>() {}; ZBytes zbytes = serializer.serialize(input); ZDeserializer>> deserializer = new ZDeserializer<>() {}; List> output = deserializer.deserialize(zbytes18); assert input.equals(output); ```text - List of maps ```java List> input = List.of(Map.of("a", 1, "b", 2)); ZSerializer>> serializer = new ZSerializer<>() {}; ZBytes zbytes = serializer.serialize(input); ZDeserializer>> deserializer = new ZDeserializer<>() {}; List> output = deserializer.deserialize(zbytes); assert input.equals(output); ```text --- # Kotlin Source: https://zenoh.io/docs/migration_1.0/kotlin ## Kotlin API migration guide for 1.0.0 The API has been extensively modified with the following goals in mind: - Enhancing the API to be more idiomatic to Kotlin. - Match the API rework done through the Rust Zenoh API. - Abstracting users from the underlying native mechanisms ## Default arguments Throughout the 0.11.0 API we were exposing builders, however we decided to replace all of them with the more Kotlin-idiomatic way of the default arguments. For instance in 0.11.0: ```kotlin session.put(keyExpr, value) .congestionControl(CongestionControl.BLOCK) .priority(Priority.REALTIME) .res() ```text Becomes the following in 1.0.0: ```kotlin session.put(keyExpr, value, congestionControl = CongestionControl.BLOCK, priority = Priority.REALTIME, ) ```text Notice that the `.res()` function has been removed. Now functions exposed by the API execute immediately, without the need of `.res()`. i.e. When doing `session.put(...)` the put is run immediately. This applies to all the builders being present on 0.11.0. Generally all the parameters present on each builder have now their corresponding argument available in the functions. ## Session opening `Session.open(config: Config)` has now been replaced with `Zenoh.open(config: Config)`. ## Encoding rework The `Encoding` class has been modified. In 0.11.0 it had the signature ```kotlin class Encoding(val knownEncoding: KnownEncoding, val suffix: String = "") ```text where `KnownEncoding` was an enum. In 0.11.0 an encoding instance would be created as follows: ```kotlin val encoding = Encoding(knownEncoding = KnownEncoding.TEXT_JSON) ```text In 1.0.0 we have implemented the following changes: - `KnownEncoding` enum is removed, instead we provide static `Encoding` instances containing an ID and a description. - Custom encodings can be created - The list of pre-defined encodings has been expanded. In 1.0.0 the previous example would instead now become: ```kotlin val encoding = Encoding.TEXT_JSON ```text ## Session-managed declarations Up until 0.11.0, it was up to the user to keep track of their variable declarations to keep them alive, because once the variable declarations were garbage collected, the declarations were closed. This was because each Kotlin variable declaration is associated with a native Rust instance, and in order to avoid leaking the memory of that Rust instance, it was necessary to free it upon dropping the declaration instance. However, this behavior could be counterintuitive, as users were expecting the declaration to keep running despite losing track of the reference to it. In this release we introduce a change in which any session declaration is internally associated to the session from which it was declared. Users may want to keep a reference to the declaration in case they want to undeclare it earlier before the session is closed, otherwise, the declaration is kept alive. For instance: ```kotlin val keyExprA = "A/B/C".intoKeyExpr().getOrThrow() val subscriber = session.declareSubscriber(keyExprA, callback = { sample -> println("Receiving sample on 'A/B/C': ${sample.payload}") }).getOrThrow() val keyExprB = "A/C/D".intoKeyExpr().getOrThrow() session.declareSubscriber(keyExprB, callback = { sample -> println("Receiving sample on 'A/C/D': ${sample.payload}") }) // No variable is associated to the declared session, on 0.11.0 it would have been instantly dropped ```text Therefore, when receiving a 'hello' message on `A/**` we would still see: ```text >> Receiving sample on 'A/B/C': hello >> Receiving sample on 'A/C/D': hello ```text since both declarations are still alive. Now the question is, for how long? What happens first, either when: - you call `undeclare()` or `close()` to the declaration - the session is closed, then all the associated declarations are automatically undeclared. ## Key Expression rework KeyExpr instances are not bound to a native key expression anymore, unless they are declared from a session. It is safe to drop the reference to the key expression instance, but the memory management associated to a key expression will differ: - If the KeyExpr was not declared from a session, then the garbage collector simply claims back the memory. - If it was declared from a session, then the session keeps track of it and frees the native memory upon closing the session. Declaring a KeyExpr on a session results in better performance, since the session is informed that we intend to use a key expression repeatedly. We also associate a native key expression to a Kotlin key expression instance, avoiding copies. ## Config loading When opening a session, it's now mandatory to provide a configuration to the session, even for a default config: ```kotlin val config = Config.default() Zenoh.open(config).onSuccess { // ... } ```text The `Config` class has been modified: - `Config.default()`: returns the default config - `Config.fromFile(file: File): Result`: allows to load a config file. - `Config.fromPath(path: Path): Result`: allows to load a config file from a path. - `Config.fromJson(json: String): Result`: loads the config from a string literal with json format - `Config.fromJson5(json5: String): Result`: loads the config from a string literal with json5 format - `Config.fromYaml(yaml: String): Result`: loads the config from a string literal with yaml format - `Config.fromJsonElement(jsonElement: JsonElement): Result`: loads the config from a kotlin JsonElement. In case of failure loading the config, a `Result.Error` is returned. ## Packages rework The package structure of the API is now aligned with Zenoh Rust package structure. Changes: - Removing the "prelude" package - QoS package now contains: `CongestionControl`, `Priority`, `Reliability`, `QoS` - Bytes package is created with: `ZBytes`, `IntoZBytes`, `Encoding` - Config package: `Config`, `ZenohId` - Session package: `SessionInfo` - Query package: contains `Query` and `Queryable` removing queryable package ## Reliability The `Reliability` config parameter used on when declaring a subscriber, has been moved. It now must be specified when declaring a `Publisher` or when performing a `Put` or a `Delete` operation. ## Logging There are two main changes regarding logging, the interface and the mechanism. Lets look at the following example, where we want to run the ZPub example with debug logging. On 1.0.0 we'll do: ```bash RUST_LOG=debug gradle ZPub ```text If we wanted to enable debug logging and tracing for some specific package, for instance `zenoh::net::routing`, we'd do: ```bash RUST_LOG=debug,zenoh::net::routing=trace gradle ZPub ```text However, this is not enabled by default. In order to enable logging, one of these newly provided functions must be called: ```kotlin Zenoh.tryInitLogFromEnv() ```text and ```kotlin Zenoh.initLogFromEnvOr(fallbackFilter: String): Result ```text This last function allows to programmatically specify the logs configuration if not provided as an environment variable. ## ZBytes serialization / deserialization & replacement of Value We have created a new abstraction with the name of `ZBytes`. This class represents the bytes received through the Zenoh network. This new approach has the following implications: - `Attachment` class is replaced by `ZBytes`. - `Value` is replaced by the combination of `ZBytes` and `Encoding`. - Replacing `ByteArray` to represent payloads With `ZBytes` we have also introduced a Serialization and Deserialization for convenient conversion between `ZBytes` and Kotlin types. ### Serialization & Deserialization We can serialize primitive types into a `ZBytes` instance, that is, converting the data into bytes processed by the zenoh network: #### Primitive types (De)Serialization is supported by the following primitive types: - Numeric: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `UByte`, `UShort`, `UInt` and `ULong`. - `String` - `ByteArray` For instance: ```kotlin val exampleInt: Int = 256 val zbytes: ZBytes = zSerialize(exampleInt).getOrThrow() val deserialization = zDeserialize(zbytes).getOrThrow() check(exampleInt == deserialization) ```text This approach works as well for the other aforementioned types. For serialization, `String` and `ByteArray` the functions `ZBytes::from(string: String)` and `ZBytes::from(bytes: ByteArray)` can be used respectively. Analogously, deserialization, `ZBytes::toString()` and `ZBytes::toByteArray()` can be used. #### Lists Lists are supported, but they must be either: - List of numeric types: (`Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `UByte`, `UShort`, `UInt` and `ULong`) - List of `String` - List of `ByteArray` - List of another supported type The serialize syntax must be used: ```kotlin val myList = listOf(1, 2, 5, 8, 13, 21) val zbytes = zSerialize>(myList).getOrThrow() val outputList = zDeserialize>(zbytes).getOrThrow() check(myList == outputList) ```text #### Maps Maps are supported as well, with the restriction that their inner types must supported primitives: - Numeric types - `String` - `ByteArray` - Map of another supported types ```kotlin val myMap: Map = mapOf("foo" to 1, "bar" to 2) val zbytes = zSerialize>(myMap).getOrThrow() val outputMap = zDeserialize>(zbytes).getOrThrow() output(myMap == outputMap) ```text #### Pair & Triple Similarly, - Pair: ```kotlin val input: Pair = Pair("one", 1) val zbytes = zSerialize(input).getOrThrow() val output: Pair = zDeserialize(zbytes).getOrThrow() check(input == output) ```text - Triple: ```kotlin val input: Triple = Triple("one", 1, true) val zbytes = zSerialize(input).getOrThrow() val output: Triple = zDeserialize(zbytes).getOrThrow() check(input == output) ```text #### Parameterized types combinations Combinations of all the above types is supported. For instance: - List of lists ```kotlin val input: List> = listOf(listOf(1, 2, 3)) val zbytes = zSerialize(input).getOrThrow() val output = zDeserialize>>(zbytes).getOrThrow() check(input == output) ```text - List of maps ```kotlin val input: List> = listOf(mapOf("a" to 1, "b" to 2)) val zbytes = zSerialize(input).getOrThrow() val output = zDeserialize>>(zbytes).getOrThrow() check(input == output) ```text ## Reply handling Previously on 0.11.0 we were exposing the following classes: - Reply.Success - Reply.Error In 0.11.0 executing a get on a session was done in the following way. ```kotlin session.get(selector).res() .onSuccess { receiver -> runBlocking { for (reply in receiver!!) { when (reply) { is Reply.Success -> {println("Received ('${reply.sample.keyExpr}': '${reply.sample.value}')")} is Reply.Error -> println("Received (ERROR: '${reply.error}')") } } } } ```text Now, in 1.0.0, the `Reply` instances contain a `result` attribute, which is of type `Result`. ```kotlin session.get(selector, channel = Channel()) .onSuccess { channelReceiver -> runBlocking { for (reply in channelReceiver) { reply.result.onSuccess { sample -> when (sample.kind) { SampleKind.PUT -> println("Received ('${sample.keyExpr}': '${sample.payload}')") SampleKind.DELETE -> println("Received (DELETE '${sample.keyExpr}')") } }.onFailure { error -> println("Received (ERROR: '${error.message}')") } } } } ```text Some parameters have been either moved (e.g. `Reliability`) or removed (e.g. `SampleKind`). --- # REST plugin Source: https://zenoh.io/docs/manual/plugin-http The REST plugin provides access to the Zenoh REST API by enabling an HTTP server on the Zenoh node where it is running. Library name: `zplugin_rest` There are two main ways to start this plugin: - Through startup arguments: zenohd's `--rest-http-port=[PORT | IP:PORT | none]` argument allows you to choose which port will be listened to by the HTTP server. Note that the default value for this argument is `8000`, meaning that unless you specify `none` explicitly, zenohd will use this plugin by default. - Through configuration: you may also configure the rest plugin in a zenohd config file, as illustrated in the Zenoh repo's `DEFAULT_CONFIG.json5` file --- # Migrating from Zenoh-Pico v0.5.x to Zenoh-Pico v0.6.x Source: https://zenoh.io/docs/migration_0.5_to_0.6/migrationguide-pico-v0.5.x-v0.6.x ## General considerations about the new Zenoh-Pico v0.6.x API ### Ownership model The new Zenoh-Pico API, similarly to the Zenoh-C API, introduced a more explicit ownership model to the user. Such model targets a better memory management where e.g. memory leaks can be easily identified and double free can be avoided. The user will have a clear understanding on what is owned by his side of the code, and what has been loaned or moved to the API. In a nutshell, - For each Zenoh type in the API, there are in fact two types: an owned (e.g., `z_owned_*_t`) and a loaned (e.g., `z_*_t`) type. For example, `z_session_t` has an equivalent defined as `z_owned_session_t`. While the former is a borrowed/loaned object (meaning, the user does not need to release it), the latter is owned by the user and thus the user is responsible to release it. - A set of ownership helpers, `z_move`, `z_loan`, `z_check` and `z_drop` are provided to ease the management of owned objects and defines how an object is passed to the API. - With `z_move`, the user give the ownership of an owned object to the API, meaning that the user no longer owns the object and therefore the user is no more responsible for releasing it. - With `z_loan`, the user retain the ownership of the owned object upon return of the API call. Note that, releasing an object while loan references still exist causes undefined behavior. - With `z_check`, the user can check if a owned object is valid, which also means that it retains memory to be released before going out of scope. - With `z_drop`, the user can release the memory associated to a given owned object. Although `z_drop` is double-free safe, loaned references to the object that might still live might cause undefined behavior. ### Private functions, types and constants In the new Zenoh-Pico API, any private function, type or constant is prefixed with `_z_*` or `_Z_*`. The user must not use them, since we will not guarantee their stability or even existence. Users might solely use any function, type or constant that starts with `z_*` or `Z_*`. Also, any struct members denotated with a `_` are also private and must not be used. All the remaining members can be used, and we plan to keep them stable. ## Migrating from Zenoh-Pico v0.5.x zenoh-net API to Zenoh-Pico v0.6.x zenoh API ### Opening a session All types and operations from the `zn_*` primitives have been updated and migrated to the `z_*` primitives. zenoh v0.5.x ```c zn_properties_t *config = zn_config_default(); zn_session_t *s = zn_open(config); if (s == NULL) { printf("Unable to open session!\n"); exit(-1); } ``` zenoh v0.6.x (C11) ```c z_owned_config_t config = z_config_default(); z_owned_session_t s = z_open(z_move(config)); if (!z_check(s)) { printf("Unable to open session!\n"); exit(-1); } ``` zenoh v0.6.x (C99) ```c z_owned_config_t config = z_config_default(); z_owned_session_t s = z_open(z_config_move(&config)); if (!z_session_check(&s)) { printf("Unable to open session!\n"); exit(-1); } ``` ### Subscribing For this release, Zenoh-Pico only supports subscribers with callbacks. It is possible to access samples through a callback by calling the callback function passed as argument on `declare_subscriber` function. When declaring a subscriber, keyexpr optimizations (i.e., keyexpr declaration) will be automatically performed if required. Finer configuration is performed with the help of an options struct. zenoh-net v0.5.x ```c void data_handler(const zn_sample_t *sample, const void *arg) { printf(">> [Subscription listener] Received (%.*s, %.*s)\n", (int)sample->key.len, sample->key.val, (int)sample->value.len, sample->value.val); } // (...) zn_subscriber_t *sub = zn_declare_subscriber(s, zn_rname("/key/expression"), zn_subinfo_default(), data_handler, NULL); ``` zenoh v0.6.x (C11) ```c void data_handler(const z_sample_t *sample, void *arg) { char *keystr = z_keyexpr_to_string(sample->keyexpr); printf(">> [Subscriber] Received ('%s': '%.*s')\n", keystr, (int)sample->payload.len, sample->payload.start); free(keystr); } // (...) z_owned_closure_sample_t callback = z_closure(data_handler); z_owned_subscriber_t sub = z_declare_subscriber(z_loan(s), z_keyexpr("key/expression"), z_move(callback), NULL); if (!z_check(sub)) { printf("Unable to declare subscriber.\n"); exit(-1); } ``` zenoh v0.6.x (C99) ```c void data_handler(const z_sample_t *sample, void *arg) { char *keystr = z_keyexpr_to_string(sample->keyexpr); printf(">> [Subscriber] Received ('%s': '%.*s')\n", keystr, (int)sample->payload.len, sample->payload.start); free(keystr); } // (...) z_owned_closure_sample_t callback = z_closure_sample(data_handler, NULL, NULL); z_owned_subscriber_t sub = z_declare_subscriber(z_session_loan(&s), z_keyexpr("key/expression"), z_closure_sample_move(&callback), NULL); if (!z_subscriber_check(&sub)) { printf("Unable to declare subscriber.\n"); exit(-1); } ``` ### Publishing The write operation has been replaced by a put operation. When declaring a publisher, keyexpr optimizations (i.e., keyexpr declaration) will be automatically performed if required. Finer configuration is performed with the help of an options struct. zenoh-net v0.5.x ```c zn_write(s, reskey, "value", strlen("value")); ``` zenoh v0.6.x (C11) ```c if (z_put(z_loan(s), z_keyexpr("key/expression"), (const uint8_t *)"value", strlen("value"), NULL) < 0) { printf("Put has failed!\n"); } ``` zenoh v0.6.x (C99) ```c if (z_put(z_session_loan(&s), z_keyexpr("key/expression"), (const uint8_t *)"value", strlen("value"), NULL) < 0) { printf("Put has failed!\n"); } ``` The write_ext operation has been removed. Configuration is now performed with the help of a option struct. zenoh-net v0.5.x ```c zn_write_ext(s, reskey, "value", strlen("value"), Z_ENCODING_TEXT_PLAIN, Z_DATA_KIND_DEFAULT, zn_congestion_control_t_BLOCK); ``` zenoh v0.6.x (C11) ```c z_put_options_t options = z_put_options_default(); options.congestion_control = Z_CONGESTION_CONTROL_DROP; options.encoding = z_encoding(Z_ENCODING_PREFIX_TEXT_PLAIN, NULL); options.priority = Z_PRIORITY_DATA; if (z_put(z_loan(s), z_keyexpr("key/expression"), (const uint8_t *)"value", strlen("value"), &options) < 0) { printf("Put has failed!\n"); } ``` zenoh v0.6.x (C99) ```c z_put_options_t options = z_put_options_default(); options.congestion_control = Z_CONGESTION_CONTROL_DROP; options.encoding = z_encoding(Z_ENCODING_PREFIX_TEXT_PLAIN, NULL); options.priority = Z_PRIORITY_DATA; if (z_put(z_session_loan(&s), z_keyexpr("key/expression"), (const uint8_t *)"value", strlen("value"), &options) < 0) { printf("Put has failed!\n"); } ``` The declare_publisher now returns a publisher object upon which put and delete operations can be performed. zenoh-net v0.5.x ```c zn_publisher_t *pub = zn_declare_publisher(s, reskey); if (pub == NULL) { printf("Unable to declare publisher.\n"); exit(-1); } ``` zenoh v0.6.x (C11) ```c z_owned_publisher_t pub = z_declare_publisher(z_loan(s), z_keyexpr("key/expression"), NULL); if (!z_check(pub)) { printf("Unable to declare publisher!\n"); exit(-1); } z_publisher_put(z_loan(pub), (const uint8_t *)"value", strlen("value"), NULL); ``` zenoh v0.6.x (C99) ```c z_owned_publisher_t pub = z_declare_publisher(z_session_loan(&s), z_keyexpr("key/expression"), NULL); if (!z_publisher_check(&pub)) { printf("Unable to declare publisher!\n"); exit(-1); } z_publisher_put(z_publisher_loan(&pub), (const uint8_t *)"value", strlen("value"), NULL); ``` ### Querying The query_collect operation has been replaced by a get operation. The get operation is no longer blocking and returning a list of replies, but instead it makes replies accessible through a callback by calling the callback function passed as argument on get function. When declaring a publisher, keyexpr optimizations (i.e., keyexpr declaration) will be automatically performed if required. Finer configuration is performed with the help of an options struct. zenoh-net v0.5.x ```c zn_reply_data_array_t replies = zn_query_collect(s, zn_rname("/key/expression"), "", zn_query_target_default(), zn_query_consolidation_default()); for (unsigned int i = 0; i < replies.len; ++i) { printf(">> [Reply handler] received (%.*s, %.*s)\n", (int)replies.val[i].data.key.len, replies.val[i].data.key.val, (int)replies.val[i].data.value.len, replies.val[i].data.value.val); } zn_reply_data_array_free(replies); ``` zenoh v0.6.x (C11) ```c void reply_dropper(void *ctx) { printf(">> Received query final notification\n"); } void reply_handler(z_owned_reply_t *oreply, void *ctx) { if (z_reply_is_ok(oreply)) { z_sample_t sample = z_reply_ok(oreply); char *keystr = z_keyexpr_to_string(sample.keyexpr); printf(">> Received ('%s': '%.*s')\n", keystr, (int)sample.payload.len, sample.payload.start); free(keystr); } else { printf(">> Received an error\n"); } } // (...) z_get_options_t opts = z_get_options_default(); opts.target = Z_QUERY_TARGET_ALL; z_owned_closure_reply_t callback = z_closure(reply_handler, reply_dropper); if (z_get(z_loan(s), z_keyexpr("key/expression"), NULL, z_move(callback), &opts) < 0) { printf("Unable to send query.\n"); exit(-1); } ``` zenoh v0.6.x (C99) ```c void reply_dropper(void *ctx) { printf(">> Received query final notification\n"); } void reply_handler(z_owned_reply_t *oreply, void *ctx) { if (z_reply_is_ok(oreply)) { z_sample_t sample = z_reply_ok(oreply); char *keystr = z_keyexpr_to_string(sample.keyexpr); printf(">> Received ('%s': '%.*s')\n", keystr, (int)sample.payload.len, sample.payload.start); free(keystr); } else { printf(">> Received an error\n"); } } // (...) z_get_options_t opts = z_get_options_default(); opts.target = Z_QUERY_TARGET_ALL; z_owned_closure_reply_t callback = z_closure_reply(reply_handler, reply_dropper, NULL); if (z_get(z_session_loan(&s), z_keyexpr("key/expression"), "", z_closure_reply_move(&callback), &opts) < 0) { printf("Unable to send query.\n"); exit(-1); } ``` ### Queryable It is possible to access queries through a callback by calling the callback function passed as argument on `declare_queryable` function. When declaring a queryable, keyexpr optimizations (i.e., keyexpr declaration) will be automatically performed if required. Finer configuration is performed with the help of an options struct. The send_reply operation has been also extended with an options struct for finer configuration. zenoh-net v0.5.x ```c void query_handler(zn_query_t *query, const void *ctx) { z_string_t res = zn_query_res_name(query); z_string_t pred = zn_query_predicate(query); printf(">> [Query handler] Handling '%.*s?%.*s'\n", (int)res.len, res.val, (int)pred.len, pred.val); zn_send_reply(query, "/key/expression", "value", strlen("value")); } // (...) zn_queryable_t *qable = zn_declare_queryable(s, zn_rname("/key/expression"), ZN_QUERYABLE_EVAL, query_handler, NULL); if (qable == NULL) { printf("Unable to declare queryable.\n"); exit(-1); } ``` zenoh v0.6.x (C11) ```c void query_handler(const z_query_t *query, void *ctx) { char *keystr = z_keyexpr_to_string(z_query_keyexpr(query)); z_bytes_t pred = z_query_parameters(query); printf(">> [Queryable ] Received Query '%s%.*s'\n", keystr, (int)pred.len, pred.start); z_query_reply(query, z_keyexpr("key/expression"), (const uint8_t *)"value", strlen("value"), NULL); free(keystr); } // (...) z_owned_closure_query_t callback = z_closure(query_handler); z_owned_queryable_t qable = z_declare_queryable(z_loan(s), z_keyexpr("key/expression"), z_move(callback), NULL); if (!z_check(qable)) { printf("Unable to create queryable.\n"); exit(-1); } ``` zenoh v0.6.x (C99) ```c void query_handler(const z_query_t *query, void *ctx) { char *keystr = z_keyexpr_to_string(z_query_keyexpr(query)); z_bytes_t pred = z_query_parameters(query); printf(">> [Queryable ] Received Query '%s%.*s'\n", keystr, (int)pred.len, pred.start); z_query_reply(query, z_keyexpr("key/expression"), (const uint8_t *)"value", strlen("value"), NULL); free(keystr); } // (...) z_owned_closure_query_t callback = z_closure_query(query_handler, NULL, NULL); z_owned_queryable_t qable = z_declare_queryable(z_session_loan(&s), z_keyexpr("key/expression"), z_closure_query_move(&callback), NULL); if (!z_queryable_check(&qable)) { printf("Unable to create queryable.\n"); exit(-1); } ``` ### Read and Lease tasks As Zenoh-Pico is targeting microcontrollers and embedded systems, it is a requirement to support both single-thread and multi-thread implementations. To do so, the user has explicit control over the read and lease tasks. If multi-thread behavior is intended, the user must spawn both tasks manually by including the following lines after Zenoh Session is successfully open. zenoh-net v0.5.x ```c zp_start_read_task(s); zp_start_lease_task(s); ``` zenoh v0.6.x (C11) ```c zp_start_read_task(z_loan(s), NULL); zp_start_lease_task(z_loan(s), NULL); ``` zenoh v0.6.x (C99) ```c zp_start_read_task(z_session_loan(&s), NULL); zp_start_lease_task(z_session_loan(&s), NULL); ``` Likewise, the user must also destroy both tasks manually by including the following lines just before closing the session: zenoh-net v0.5.x ```c zp_stop_read_task(s); zp_stop_lease_task(s); ``` zenoh v0.6.x (C11) ```c zp_stop_read_task(z_loan(s)); zp_stop_lease_task(z_loan(s)); ``` zenoh v0.6.x (C99) ```c zp_stop_read_task(z_session_loan(&s)); zp_stop_lease_task(z_session_loan(&s)); ``` Note that, `z_close(z_move(s));` will stop and destroy the tasks if the user forgets to do it. However, for the sake of symmetric operations, the user is advised to stop them manually. If single-thread behavior is intended, the user must not spawn the any of the tasks. Instead, the user can a single execution of the read task and lease tasks at its own pace: zenoh v0.6.x (C11) ```c zp_read(z_loan(s), NULL); zp_send_keep_alive(z_loan(s), NULL); zp_send_join(z_loan(s), NULL); ``` zenoh v0.6.x (C99) ```c zp_read(z_session_loan(&s), NULL); zp_send_keep_alive(z_session_loan(&s), NULL); zp_send_join(z_session_loan(&s), NULL); ``` ### Examples More examples are available here: - zenoh v0.6.0 (C11) - zenoh v0.6.0 (C99) - zenoh-net v0.5.0-beta9 --- # Pico API · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/apis/pico # Source: https://zenoh.io/docs/apis/pico # Pico API --- # Python API · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/apis/python # Source: https://zenoh.io/docs/apis/python # Python API --- # C++ API · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/apis/cpp # Source: https://zenoh.io/docs/apis/cpp # C++ API --- # Kotlin API · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/apis/kotlin # Source: https://zenoh.io/docs/apis/kotlin # Kotlin API --- # TLS authentication · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/manual/tls # Source: https://zenoh.io/docs/manual/tls # TLS authentication Zenoh supports TLS as a transport protocol. TLS can be configured in two ways: - server side authentication: clients validate the server TLS certificate but not the other way around, that is, the same way of operating on the web where the web browsers validate the identity of the server via means of the TLS certificate. - mutual authentication (mTLS): where both server-side and client-side authentication is required. The configuration of TLS certificates is done via aconfiguration file. ## Client configuration The fieldroot_ca_certificateis used to specify the path to the certificate used to authenticate theTLS server. It’s important to note that if the field is not specified then the default behaviour is to load the root certificates provided byMozilla’s CA for use with webpki. However, if we manage our own certificates, we need to specify the root certificate. Suppose we generated the certificate usingMiniCA as explained below, then the configuration file for aclientwould be: ```json { /// The node's mode (router, peer or client) "mode": "client", "connect": { "endpoints": ["tls/localhost:7447"] }, "transport": { "link": { "tls": { "root_ca_certificate": "/home/user/tls/minica.pem" } } } } ```json When using such configuration, the client will use the providedminica.pemcertificate to authenticate theTLS server certificate. Let's assume the above configuration is then saved with the nameclient.json5. ## Router configuration The requiredtlsfields for configuring aTLS certificatefor a router arelisten_private_keyandlisten_certificate. A configuration file for arouterwould be: ```json { /// The node's mode (router, peer or client) "mode": "router", "listen": { "endpoints": ["tls/localhost:7447"] }, "transport": { "link": { "tls": { "listen_private_key": "/home/user/tls/localhost/key.pem", "listen_certificate": "/home/user/tls/localhost/cert.pem" } } } } ```json When using such configuration, the router will use the providedlisten_private_keyandlisten_certificatefor establishing a TLS session with any client. Let's assume that the above configurations are then saved with the nameserver.json5. ## Peer configuration The requiredtlsfields for configuring aTLS certificatefor a peer areroot_ca_certificate,listen_private_keyandlisten_certificate. A configuration file for apeerwould be: ```json { /// The node's mode (router, peer or client) "mode": "peer", "transport": { "link": { "tls": { "root_ca_certificate": "/home/user/tls/minica.pem", "listen_private_key": "/home/user/tls/localhost/key.pem", "listen_certificate": "/home/user/tls/localhost/cert.pem" } } } } ```json When using such configuration, the peer will use the providedroot_ca_certificateto authenticate theTLS certificateof thepeerit is connecting to. At the same time, the peer will use the providedlisten_private_keyandlisten_certificatefor initiating incoming TLS sessions from other peers. Let's assume that the above configurations are then saved with the namepeer.json5. ## TLS with Scouting ⚠️ Zenoh provides ascouting mechanismthat allows peers to discover other neighboring peers automatically. By default, this feature is enabled and attempts to establish connections with other peersusing all Zenoh-supported protocols(not just TLS). To ensure that all connections are established using TLS, you can configure the protocols filter as shown below: ```json { "transport": { "link": { "protocols": ["tls"] } } } ```json Theprotocolsconfiguration field specifies which protocols Zenoh should whitelist for accepting and opening sessions. If this field is not configured, Zenoh will automatically whitelist all supported protocols. ## Mutual authentication (mTLS) In order to enable mutual authentication, we'll need two sets of keys and certificates, one for the "server" and one for the "client". These sets of keys and certificates can be generated as explainedin the appendix section below. Let's suppose we are storing them under$home/user/with the following files and folders structure: ```bash user ├── client │   ├── localhost │   │   ├── cert.pem │   │   └── key.pem │   ├── minica-key.pem │   └── minica.pem └── server ├── localhost │   ├── cert.pem │   └── key.pem ├── minica-key.pem └── minica.pem ```bash ### Router configuration The filedenable_mtlsneeds to be set totrueand we must provide the router (acting as server) the certificate authority to validate the client's keys and certificates under the fieldroot_ca_certificate. Thelisten_private_keyandlisten_certificatefields are also required in order to authenticate the router in front of the client. ```json { "mode": "router", "listen": { "endpoints": ["tls/localhost:7447"] }, "transport": { "link": { "tls": { "root_ca_certificate": "/home/user/client/minica.pem", "enable_mtls": true, "listen_private_key": "/home/user/server/localhost/key.pem", "listen_certificate": "/home/user/server/localhost/cert.pem" } } } } ```json ### Client configuration Again, the fieldenable_mtlsneeds to be set totrueand we must provide the certificate authority to validate the server keys and certificates. Similarly, we need to provide the client keys and certificates for the server to authenticate our connection. ```json { "mode": "client", "connect": { "endpoints": ["tls/localhost:7447"] }, "transport": { "link": { "tls": { "root_ca_certificate": "/home/user/server/minica.pem", "enable_mtls": true, "connect_private_key": "/home/user/client/localhost/key.pem", "connect_certificate": "/home/user/client/localhost/cert.pem" } } } } ```json ## Close on certificate expiration Starting with Zenoh v1.0.3, TLS and QUIC links can be closed when the remote certificate chain expires: the configured local instance will monitor the expiration time of the first expiring certificate in the remote instance's certificate chain, and will disconnect the link when said time is reached. This behavior can be enabled via the zenoh config file, by setting the fieldclose_link_on_expirationtotrue. This is valid for both TLS clients and servers. ### Client configuration Below is an example config for a TLS client with certificate expiration monitoring.mTLS-related config fields can also be added if required. ```json { "mode": "client", "connect": { "endpoints": ["tls/localhost:7447"] }, "transport": { "link": { "tls": { "root_ca_certificate": "/home/user/server/minica.pem", "close_link_on_expiration": true } } } } ```json ### Listener configuration Note that certificate expiration can only be monitored by a TLS listener whenmTLSis enabled, since withoutmTLSa client does not need certificates to connect. Below is an example config for a router acting as TLS server with certificate expiration monitoring. ```json { "mode": "router", "listen": { "endpoints": ["tls/localhost:7447"] }, "transport": { "link": { "tls": { "root_ca_certificate": "/home/user/client/minica.pem", "listen_private_key": "/home/user/server/localhost/key.pem", "listen_certificate": "/home/user/server/localhost/cert.pem", "enable_mtls": true, "close_link_on_expiration": true } } } } ```json ## Testing the TLS transport Let's assume a scenario with one Zenoh router and two clients connected to it: one publisher and one subscriber. The first thing to do is to run the router passing its configuration, i.e.router.json5: ```bash $ zenohd -c router.json5 ```json Then, let's start the subscriber in client mode passing its configuration, i.e.client.json5: ```bash $ z_sub -c client.json5 ```bash Lastly, let's start the publisher in client mode passing its configuration, i.e.client.json5: ```bash $ z_pub -c client.json5 ```bash As it can be noticed, the sameclient.json5is used forz_subandz_pub. ### Peer-to-peer scenario Let's assume a scenario with two peers. First, let's start the first peer in peer mode passing its configuration, i.e.peer.json5: ```bash $ z_sub -c peer.json5 -l tls/localhost:7447 ```bash Next, let's start the second peer in peer mode passing its configuration, i.e.peer.json5: ```bash $ z_pub -c peer.json5 -l tls/localhost:7448 -e tls/localhost:7447 ```bash As it can be noticed, the samepeer.json5is used forz_subandz_pub. ## Appendix: TLS certificates creation In order to use TLS as a transport protocol, we need first to create the TLS certificates. While multiple ways of creating TLS certificates exist, in this guide we are going to useminicafor simplicity: > Minica is a simple CA intended for use in situations where the CA operator also operates each host where a certificate will be used. It automatically generates both a key and a certificate when asked to produce a certificate. It does not offer OCSP or CRL services. Minica is appropriate, for instance, for generating certificates for RPC systems or microservices. Minica is a simple CA intended for use in situations where the CA operator also operates each host where a certificate will be used. It automatically generates both a key and a certificate when asked to produce a certificate. It does not offer OCSP or CRL services. Minica is appropriate, for instance, for generating certificates for RPC systems or microservices. First, you need to install minica by following theseinstructions. Once you have successfully installed on your machine, let’s create the certificates as follows assuming that we will test Zenoh over TLS onlocalhost. First let’s create a folder to store our certificates: ```bash $/home/user: mkdir tls $/home/user: cd tls $/home/user/tls: pwd /home/user/tls ```bash Then, let’s generate the TLS certificate for thelocalhostdomain: ```bash $/home/user/tls: minica --domains localhost ```bash This should create the following files: ```bash $/home/user/tls: ls localhost minica-key.pem minica.pem ```bash minica.pemis the root CA certificate that will be used by the client to validate the server certificate. The server certificatecert.pemand private keykey.pemcan be found inside thelocalhostfolder. ```bash $/home/user/tls: ls localhost cert.pem key.pem ```bash Once the above certificates have been correctly generated, we can proceed to configure Zenoh to use TLS as explained. Since version 0.7.1-rc from Zenoh, we can generate certificates associated not only to dns domains but also to ip addresses as well. For instance, we can generate them as follows with minica: ```bash $/home/user/server/tls: minica --ip-addresses 127.0.0.1 ```bash ```bash $/home/user/server/tls: ls 127.0.0.1 minica-key.pem minica.pem ```bash Then on the Zenoh configuration file we'll be able to set up the TLS configuration specifying the ip address, for instance for a server and a client with tls: ```json { "mode": "router", "listen": { "endpoints": ["tls/127.0.0.1:7447"] }, "transport": { "link": { "tls": { "listen_private_key": "/home/user/server/127.0.0.1/key.pem", "listen_certificate": "/home/user/server/127.0.0.1/cert.pem" } } } } ```json ```json { "mode": "client", "connect": { "endpoints": ["tls/127.0.0.1:7447"] }, "transport": { "link": { "tls": { "root_ca_certificate": "/home/user/server/minica.pem" } } } } ```json --- # Your first Zenoh app · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/getting-started/first-app # Your first Zenoh app Let us take a step-by-step approach in putting together your first Zenoh application in Python. As the first step, let us see how we get some data from a temperature sensor in our kitchen. Then we see how we can route this data to store and perform some analytics. Before cranking some code, let's define some terminology. Zenoh deals with keys/values where each key is a path and is associated to a value. A key looks like just a Unix file system path, such asmyhome/kitchen/temp. The value can be defined with different encodings (string, JSON, raw bytes buffer...). Let's get started! ## Pub/sub in Zenoh First thing first, we need to install thezenoh Python library. ```bash pip install eclipse-zenoh ``` The examples are updated to use the 1.0 version currently in rc, which is why version must be specified in the installation command. You can find more information about the 1.0 changes in themigration guides. Then, let's write an application,z_sensor.pythat will produce temperature measurements at each second: ```python import zenoh, random, time random.seed() def read_temp(): return random.randint(15, 30) if __name__ == "__main__": with zenoh.open(zenoh.Config()) as session: key = 'myhome/kitchen/temp' pub = session.declare_publisher(key) while True: t = read_temp() buf = f"{t}" print(f"Putting Data ('{key}': '{buf}')...") pub.put(buf) time.sleep(1) ``` Now we need a subscriber,z_subscriber.pythat can receive the measurements: ```python import zenoh, time def listener(sample): print(f"Received {sample.kind} ('{sample.key_expr}': '{sample.payload.to_string()}')") if __name__ == "__main__": with zenoh.open(zenoh.Config()) as session: sub = session.declare_subscriber('myhome/kitchen/temp', listener) time.sleep(60) ``` Start the subscriber: ```bash python3 z_subscriber.py ``` The subscriber waits for an update onmyhome/kitchen/temp. Now startz_sensor.pyas follows ```bash python3 z_sensor.py ``` You can see the values produced by the sensor being consumed by the subscriber. ## Store and Query in Zenoh As the next step, let's see how the value generated by a publisher can be stored in Zenoh. For this, we useZenoh router(zenohd). By default, a Zenoh router starts without any storage. In order to store the temperature, we need to configure one. Create azenoh-myhome.json5configuration file for Zenoh with this content: ```json5 { plugins: { rest: { // activate and configure the REST plugin http_port: 8000 // with HTTP server listening on port 8000 }, storage_manager: { // activate and configure the storage_manager plugin storages: { myhome: { // configure a "myhome" storage key_expr: "myhome/**", // which subscribes and replies to query on myhome/** volume: { // and using the "memory" volume (always present by default) id: "memory" } } } } } } ``` Installand start the Zenoh router with this configuration file: ```bash zenohd -c zenoh-myhome.json5 ``` Now the data generated by our temperature sensor is stored in memory. We can retrieve the latest temperature value stored in Zenoh: ```python import zenoh if __name__ == "__main__": with zenoh.open(zenoh.Config()) as session: replies = session.get('myhome/kitchen/temp') for reply in replies: try: print("Received ('{}': '{}')" .format(reply.ok.key_expr, reply.ok.payload.to_string())) except: print("Received (ERROR: '{}')" .format(reply.err.payload.to_string())) ``` ## Other examples You can also have a look at the examples provided with each client API: - Rust:https://github.com/eclipse-zenoh/zenoh/tree/main/examples - Python:https://github.com/eclipse-zenoh/zenoh-python/tree/main/examples - C:https://github.com/eclipse-zenoh/zenoh-c/tree/main/examples --- # Concepts · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/migration_1.0/concepts # Source: https://zenoh.io/docs/migration_1.0/concepts # Concepts The Zenoh team have been hard at work preparing an official version 1.0.0 of Zenoh!This major release comes with several API changes, quality of life improvements and developer conveniences. We now have a more stable API and intend to keep backward compatibility in future Zenoh revisions.This guide is here to ease the transition to Zenoh 1.0.0 for our users! ## Value is gone, long live ZBytes We have replacedValuewithZBytesandEncoding.ZBytesis the type core to data representation in Zenoh, all API’s have been reworked to acceptZBytesor something that can be converted into aZBytes.We have added a number of conversion implementations for language primitives as well as methods to seamlessly allow user defined structs to be serialized intoZBytes.Sample’s payloads are nowZBytes.Publisher,QueryableandSubscribernow expectZBytesfor all their interfaces. TheAttachmentAPI also now acceptsZBytes. Each Language bindings will have their own specifics of Serializing and Deserializing, but for the most part it will involve implementing a serialize / deserialize function for your datatype or make use of auto-generated conversions for composite types. ## Encoding Encodinghas been reworked, moving away from enumerables to now accepting strings. While Zenoh does not impose anyEncodingrequirement on the user, providing anEncodingcan offer automatic wire-level optimization for known types.For the user definedEncoding, it can be thought of as optional metadata, carried over by Zenoh in such a way that the end user’s application may perform different operations based onEncoding.We have expanded our list of predefined encoding types from Zenoh 0.11.0 to include variants for numerous IANA standard encodings, including but not limited tovideo/x,application/x,text/x,image/xandaudio/xencoding families, as well as an encoding family specific to Zenoh defined by the prefixzenoh/x.Users can also define their own encoding scheme that does not need to be based on the predefined IANA variants. Upon declaring a specific encoding, the users encoding scheme will be prefixed withzenoh/bytesfor distinction. ## Attachment We have made attachment more flexible across API’s, essentially accepting anything that can be converted to aZBytesas an optional extra toput,delete, onQuery’s and Queryreply’s.We also allow for composite types to be converted intoZBytes, meaning that using the Attachment API as a metadata transport is easier than ever. ## Query & Queryable Thereplymethod of aQueryablehas gained two variants:reply_delandreply_errto respectively indicate that a deletion should be performed and that an error occurred.Additionally, the 3 variants behave similarly toputanddel, hence providing improved ergonomics. We have added the ability to get the underlyingHandlerof a Queryable as well. ## Use accessors to get private members Across language bindings we encapsulate members of structs, and they can’t be accessed directly now.The only way to access struct values is to use the getter function associated with them. ## Pull Subscribers have been removed The concept of a pull subscriber no longer exists in Zenoh. However, when creating aSubscriber, it may be the case that developers only care about the latest data and want to discard the oldest data. TheRingChannelcan be used to get a similar behaviour.Rust ExampleThis contrasts with theFIFOChannel, the default channel type used internally in Subscribers, which drops new messages once its buffer is full. You can take a look at examples of usage in any language’s examples/z_pull.x ## Timestamps Previously we exposed a function to generate timestamps outside of a session. Due to our efforts to improve the storage replication logic, users will now have to generate timestamps from a session, with the timestamp inheriting theZenohIDof the session. This will affect user-created plugins and applications that need to generate timestamps in their Storage and sending of data.⚠️ Note: Timestamps are important for Storage Alignment (a.k.a. Replication). Data stored in Data bases must include a Timestamp to be properly aligned across Data Stores by Zenoh. Thetimestampingconfiguration option must also be enabled for this. ## Plugins ### Storages ⚠️ Note: The storage-manager will fail to launch if thetimestampingconfiguration option is disabled.From Zenoh 1.0.0 user-applications can load plugins.A, somehow, implicit assumption that dictated the behaviour of storages is that the Zenoh node loading themhas to add a timestamp to any received publication that did not have one. This functionality is controlled by thetimestampingconfiguration option.Until Zenoh 1.0.0 this assumption held true as only a router could load storage and the default configuration for a router enablestimestamping. However, in Zenoh 1.0.0 nodes configured inclient&peermode can load storage andtheir default configuration disablestimestamping. ### Plugin Loading We added the ability for user-applications to load compiled plugins written in Rust, regardless of which language bindings you are using! Usage of this feature isPlugin Loading ⚠️ Note : When loading a plugin, the Plugin must have been built with the same version of the Rust compiler as the bindings loading it, and theCargo.lockof the plugin must be synced with the same commit of Zenoh.This means that if the language bindings are usingrustcversion1.75, the plugin must: - Be built with the same toolchain version1.75 - Be built with the same Zenoh Commit - The pluginCargo.lockhave had its packages synced with the ZenohCargo.lock The reason behind this strict set of requirements is due to Rust making no guarantees regarding data layout in memory.This means between compiler versions, the representation may change based on optimizations.More on this topic at here :Rust:Type-Layout ## Config Changes ### Plugin Loading Loading plugins is achieved by enabling theplugins_loadingsection in config file, with the membersenabledset to true, and specifying thesearch_dirsfor the plugins. Directories are specified as an object with fieldskindandvalue. - Ifkindiscurrent_exe_parent, then the parent of the current executable’s directory is searched andvalueshould benull. In Bash notation,{ "kind": "current_exe_parent" }equals$(dirname $(which zenohd))while"."equals$PWD. - Ifkindis"path", thenvalueis interpreted as a filesystem path, i.e.{ "kind": "path" , "value": "path/to/plugin/dir"}.Simply supplying a string instead of an object is equivalent to this.Ifenabled: trueandsearch_dirsis not specified thensearch_dirsfalls back to the default value of:".:~/.zenoh/lib:/opt/homebrew/lib:/usr/local/lib:/usr/lib” ``` plugins_loading: { // Enable plugins loading. enabled: false, /// Directories where plugins configured by name should be looked for. Plugins configured by __path__ are not subject to lookup. /// If `enabled: true` and `search_dirs` is not specified then `search_dirs` falls back to the default value: ".:~/.zenoh/lib:/opt/homebrew/lib:/usr/local/lib:/usr/lib" search_dirs: [{ "kind": "current_exe_parent" }, ".", "~/.zenoh/lib", "/opt/homebrew/lib", "/usr/local/lib", "/usr/lib"], } // ... Rest of Config ``` ### Mode Dependent endpoints Configuration now supports a different list of endpoints depending on the mode zenohd is launched with. The old behaviour of a single List of endpoints is still supported, applying torouter,peerandclient, however users can now set endpoints per mode: ``` connect: { /// The list of endpoints to connect to. /// Accepts a single list (e.g. endpoints: ["tcp/10.10.10.10:7447", "tcp/11.11.11.11:7447"]) /// or different lists for router, peer and client endpoints: { router: ["tcp/10.10.10.10:7447"], peer: ["tcp/11.11.11.11:7447"], client: ["tcp/somewhere1::7447", "udp/somewhere2:7447"] } }, ``` ⚠️ Note: inclientmode,zenohdwill try connect to each endpoint in order until one is successful, then stop subsequent endpoint connection attempts.client’s only connect to a single endpoint. ### Scouting We have implemented a small change in the configuration syntax concerning thescoutingsection.Bothgossipandmulticast’sautoconnectsection’s have changed to accept lists of either"peer","client"or"router" ``` // Zenoh 0.11.0 scouting: { multicast: { autoconnect: { router: "", peer: "router|peer" }, }, gossip: { autoconnect: { router: "", peer: "router|peer" }, }, }, // Zenoh 1.0.0 scouting: { multicast: { autoconnect: { router: [], peer: ["router", "peer"] }, }, gossip: { autoconnect: { router: [], peer: ["router", "peer"] }, }, } ``` Next step is to dive into the migration examples for your favourite language! --- # Python · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/migration_1.0/python # Source: https://zenoh.io/docs/migration_1.0/python # Python ## Highlights The library has been fully rewritten to use only Rust. It should make no difference for users, except for a significant performance improvement. The API has also been reworked to feel more pythonic, using notably context managers. ## Context managers and background callbacks Youshouldclose the zenoh session after use and the recommended way is through context manager: ``` import zenoh with zenoh.open(zenoh.Config()) as session: # `session.close()` will be called at the end of the block ``` Session-managed objects like subscribers or queryables can also be managed using context managers: ``` with session.declare_subscriber("my/keyexpr") as subscriber: # `subscriber.undeclare()` will be called at the end of the block` ``` In previous version, it was necessary to keep a variable in the scope for a subscriber/queryable declared with a callback. This constraint has been lifted, and it’s now possible to declare a “background” entity; this entity will keep living in background, having its callback executed until the session is closed. ``` import zenoh with zenoh.open(zenoh.Config()) as session: # no need to declare a variable session.declare_subscriber("my/keyepxr", lambda s: print(s), background=True) sleep(10) # subscriber stays in background and its callback can be called # `session.close()` will be called at the end of the block, # and it will undeclare the subscriber ``` ## Drop-callback has to be wrapped inhandlers.Callback In the previous 0.11.0 version, it was possible to pass a drop-callback with the main callback in a tuple for operations likeSession.declare_subscriber. However, it was also possible to pass a tuple with a “receiver” (renamed “handler” in 1.0.0) as second member, and that could confuse users. The API has been changed and now requires the drop-callback to be wrapped inhandlers.Callback. - Zenoh 0.11.x ``` def on_sample(sample: zenoh.Sample): ... def on_done(): ... session.declare_subscriber((on_sample, on_done)) ``` - Zenoh 1.0.0 ``` def on_sample(sample: zenoh.Sample): ... def on_done(): ... session.declare_subscriber(zenoh.handlers.Callback(on_sample, on_done)) ``` NOTE: ⚠️ Passing drop-callback in a tuple will no longer work as expected, as the drop callback will never be executed. To ease migration and avoid surprises, a warning will be displayed in this case. ## Value is gone, long live ZBytes Valuehas been split intoZBytesandEncoding.putand other operations now require aZBytespayload, and builders accept an optionalEncodingparameter. ZBytesis a raw bytes container. It can be created directly from raw bytes/strings usingZBytesconstructor. Then bytes can be retrieved usingZBytes.to_bytesorZBytes.to_string. Sample payload is now aZBytesinstead ofbytes. - Zenoh 0.11.x ``` sample = subscriber.recv() my_string = sample.payload.decode("utf-8") ``` - Zenoh 1.0.0 ``` sample = subscriber.recv() my_string = sample.payload.to_string() ``` You can look at a full set of examples inexamples/z_bytes.py. ## Serialization Zenoh does provide serialization for convenience as an extension inzenoh.extmodule. Serialization is implemented for a bunch of standard types likeint,float,list,dict,tuple, etc. and is used through functionsz_serialize/z_deserialize. ``` input = b"raw bytes" payload = ZBytes(input) output = payload.to_bytes() ``` zenoh.extserialization doesn’t pretend to cover all use cases, as it is just one available choice among other serialization formats like JSON, Protobuf, CBOR, etc. In the end, Zenoh will just send and receive payload raw bytes independently of the serialization used. NOTE: ⚠️ Serialization ofbytesis not the same as passingbytestoZBytesconstructor. ## Encoding Encodinghas been reworked. Zenoh does not impose any encoding requirement on the user, nor does it operate on it. It can be thought of as optional metadata, carried over by Zenoh in such a way that the end user’s application may perform different operations based on encoding. NOTE: ⚠️ The encoding is no longer automatically inferred from the payload type. ``` session.put(json.dumps({"key", "value"}), encoding=Encoding.APPLICATION_JSON) ``` Users can also define their own encoding scheme that does not need to be based on the pre-defined variants. ``` encoding = Encoding("pointcloud/LAS") ``` Because encoding is now optional forput,Publishercan be declared with a default encoding, which will be used in everyPublisher.put. ``` publisher = session.declare_publisher("my/keyepxr", encoding=Encoding.APPLICATION_JSON) // default encoding from publisher `application/json` publisher.put(json.dumps({"key", "value"})) ``` ## Handlers The library now directly exposes Rust-backed handlers inzenoh.handlers. When no handler is provided,zenoh.handlers.DefaultHandleris used. ``` import zenoh.handlers subscriber = session.declare_subscriber("my/keyexpr", zenoh.handlers.DefaultHandler()) # equivalent to `session.declare_subscriber("my/keyexpr")`# builtin handlers provides `try_recv`/`recv` methods and can be iterated sample_or_none = subscriber.handler.try_recv() sample = subscriber.handler.recv() for sample in subscriber.handler: ... # builtin handlers methods can be accessed directly through subscriber/queryable object sample_or_none = subscriber.try_recv() sample = subscriber.recv() for sample in subscriber: ... ``` Callbacks can also be used as handler: ``` def handle_sample(sample: zenoh.Sample): ... session.declare_subscriber("my/keyexpr", handle_sample) # A drop callback can be passed using `zenoh.handlers.Callback`def stop(): ... session.declare_subscriber("my/keyexpr", zenoh.handlers.Callback(handle_sample, stop)) ``` Note that for each callback handler, zenoh will in fact use a builtin handler and spawn a thread iterating the handler and calling the callback. This is needed to avoid GIL-related issues in low-level parts of zenoh, and as a result, leads to a significant performance improvement. --- # QUIC transport · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/manual/quic # Source: https://zenoh.io/docs/manual/quic # QUIC transport Zenoh supports QUIC as a transport protocol. As you may already know, QUIC is a UDP-based, stream-multiplexing, encrypted transport protocol. It natively embeds TLS for encryption, authentication and confidentiality. As of today, the only supported TLS authentication mode in Zenoh is server-authentication1: clients validate the server TLS certificate but not the other way around. That is, the same way of operating on the web where the web browsers validate the identity of the server via means of the TLS certificate. ## TLS configuration In order to use QUIC as a transport protocol, we need first to create the TLS certificates. The instructions to properly generate TLS certificates can be foundhere. As you can see, they are the same instructions required to run Zenoh on TLS over TCP. Here instead, the only difference is that we have TLS in QUIC! Nevertheless, the procedures to generate the certificates are exactly the same. ## Testing the QUIC transport You can test out Zenoh over QUIC in both client-router and peer-to-peer scenarios. ### Client-Router scenario Let’s assume a scenario with one Zenoh router and two clients connected to it: one publisher and one subscriber. The first thing to do is to generate therouter.json5andclient.json5configuration files as explainedhere, but replace theendpointsfields toquic/localhost:7447, in which the transport protocol is now specified asquic. Next, it’s time to run the router passing its configuration, i.e.router.json5: ``` $ zenohd -c router.json5 ``` Then, let’s start the subscriber in client mode passing its configuration, i.e.client.json5: ``` $ z_sub -c client.json5 ``` Lastly, let’s start the publisher in client mode passing its configuration, i.e.client.json5: ``` $ z_pub -c client.json5 ``` As it can be noticed, the sameclient.json5is used forz_subandz_pub. ### Peer-to-peer scenario Let’s assume a scenario with two peers. The first thing to do is to generate thepeer.json5configuration files as explainedhere. Then, let’s start the first peer in peer mode passing its configuration, i.e.peer.json5: ``` $ z_sub -c peer.json5 -l quic/localhost:7447 ``` Lastly, let’s start the second peer in peer mode passing its configuration, i.e.peer.json5: ``` $ z_pub -c peer.json5 -l quic/localhost:7448 -e quic/localhost:7447 ``` As it can be noticed, the samepeer.json5is used forz_subandz_pub. - Starting fromZenoh 0.7.0-rc, Zenohsupports both TLS and mTLS (mutual TLS) as communication transports.↩︎ --- # Abstractions · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/manual/abstractions # Source: https://zenoh.io/docs/manual/abstractions # Abstractions Zenoh is adistributed serviceto define, manage and operate onkey/valuespaces. The main abstractions at the core of Zenoh are the following: ## Key Zenoh operates onkey/valuepairs. The most important thing to know about Zenoh keys is that/is the hierarchical separator, just like in unix filesystems. While you could set up your own hierarchy using other separators, your Zenoh exchanges would benefit from better performance using/, as it will let Zenoh do clever optimisations (users have informed us in the past that switching from.to/as their hierarchy-separator almost divided their CPU usage by 2). However, you will much more often interact withkey expressions, which provide a small regular language to match sets of keys. There are a few restrictions on what may be a key: - It is a/-joined list of non-empty UTF-8 chunks. This implies that leading and trailing/are forbidden, as well as the//pattern. - An individual key may not contain the characters*,$,?,#. A typical Zenoh key would look something like:organizationA/building8/room275/sensor3/temperature ## Key Expression A key expression denotes a set of keys. It is declared usingKey Expression Language, a small regular language, where: - *matches any set of characters in a key, except'/'. It can only be surrounded by/. For example, subscribing toorganizationA/building8/room275/*/temperaturewill ensure that any temperature message from any device in room 275 of building 8 will be routed to your subscriber.Note however that this expression wouldn’t matchorganizationA/building8/room275/temperature. - $*is like*except it may be surrounded by any other characters. For example, subscribing toorganizationA/building8/room275/thermometer$*/temperaturewill get the temperature readings from all thermometers in the room. - **is equivalent to.*in regular expression syntax: it will match absolutely anything, including nothing. They may appear at the beginning of a key expression or after a/, and/is the only allowed character after a**For example, subscribing toorganizationA/**/temperaturewill ensure that any temperature message from ALL devices in organization A. This language is designed to ensure that two key expressions addressing the same set of keysmustbe the same string. To ensure that, only acanonform is allowed for key expressions: - **/**must always be replaced by** - **/*must always be replaced by*/** - $*$*must always be replaced by$* - $*must be replaced by*if alone in a chunk. ### Notes on key-space design Here are some rules of thumb to make Zenoh more comfortable to work with, and more resource-efficient: - $*is slower than*, design your key-space to avoid needing it. The need for$*usually stems from mixing different discriminants within a chunk. Preferrobot/12andpc/18torobot12andpc18. - A strict hierarchy, where you ensure thata/keyexpr/that/ends/with/*always yields data from a single type, will save you the hassle of filtering out data that’s not of the right type, while saving the network bandwidth. ## Selector A selector (specification) is an extension of thekey expressionsyntax, and is made of two parts: - The key expression, which is the part of the selector that routers will consider when routing a Zenoh message. - Optionally, separated from the key expression by a?, the parameters. Here’s what a selector concretely looks like: ``` path/**/something?arg1=val1;arg2=value%202 ^ ^ ^ ^ |Key Expression-| |----- parameters -----| ``` Which deserializes to: ``` { key_expr: "path/**/something", parameters: {arg1: "val1", arg2: "value 2"} } ``` The selector’sparameterssection functions just like query parameters: - It’s separated from the path (Key Expr) by a?. - It’s a?list of key-value pairs. - The first=in a key-value pair separates the key from the value. - If no=is found, the value is an empty string:hello=there;kenobiis interpreted as{"hello": "there", "kenobi": ""}. - The selector is assumed to be url-encoded: any character can be escaped using%. There are however some additional conventions: - Duplicate keys are considered Undefined Behaviour; but the recommended behaviour (implemented by the tools we provide for selector interpretation) is to check for duplicates of the interpreted keys, returning errors when they arise. - The Zenoh Team considers any key that does not start with an ASCII alphabetic character reserved, intending to standardize some parameters to facilitate working with diverse queryables. - Since Zenoh operations may be distributed over diverse networks, we encourage queryable developers to use some prefix in their custom keys to avoid collisions. - When interpreting a key-value pair as a boolean, the absence of the key-value pair, or the value being"false"are the only “falsey” values: in the previous examples, the bothhelloandkenobiwould be considered truthy if interpreted as boolean. The list of standardized parameters, as well as their usage, is documented in theselector specification. ## Value A user provided data item along with itsencoding. ## Encoding A description of thevalueformat, allowing Zenoh (or your application) to know how to encode/decode the value to/from a bytes buffer. By default, Zenoh is able to transport and store any format of data as long as it’s serializable as a bytes buffer. But for advanced features such as content filtering (usingselector) or to automatically deserialize the data into a concrete type in the client APIs, Zenoh requires a description of the data encoding. Some noteworthy supported encodings are: - TextPlain: the value is a UTF-8 string - AppJsonorTextJson: the value is a JSON string - AppProperties: the value is a string representing a list of keys/values separated by';'(e.g."k1=v1;k2=v2..."), where both key and value are string-typed. - AppInteger: the value is an integer - AppFloat: the value is a float You may refer toZenoh’s Rust API documentationto get more information on the supported encodings. You may also write your own encodings by either suffixing an existing one, or by suffixing theEMPTYencoding, if you wish to use encodings that are unknown to Zenoh. While Zenoh will not be able to deserialize these encodings, it will expose them to your application so that it may be informed on how it should deserialize any received value. ## Timestamp When avalueis put into Zenoh, the first Zenoh router receiving this value automatically associates it with a timestamp.This timestamp is made of 2 items: - Atimegenerated by aHybrid Logical Clock (HLC). This time is a 64-bit time with a similar structure than a NTP timestamp (but with a different epoch):The higher 32-bit part is the number of seconds since midnight, January 1, 1970 UTC (implying a rollover in 2106).The lower 32-bit part is a fraction of second, but with the 4 last bits replaced by a counter.This time gives a theoretical resolution of (0xF * 10^9 / 2^32) = 3.5 nanoseconds.The counter guarantees that the same time cannot be generated twice and that thehappened-beforerelationship is preserved. - The higher 32-bit part is the number of seconds since midnight, January 1, 1970 UTC (implying a rollover in 2106). - The lower 32-bit part is a fraction of second, but with the 4 last bits replaced by a counter. - TheUUIDof the Zenoh router that generated the time. Such a timestamp allows Zenoh to guarantee that each value introduced into the system has a unique timestamp, and that those timestamps (and therefore the values) can be ordered in the same way at any point of the system, without the need of any consensus algorithm. ## Subscriber An entity registering interest for any change (put or delete) to a value associated with a key matching the specifiedkey expression. ## Publisher An entity declaring that it will be updating the key/value with keys matching a givenkey expression. ## Queryable A computation registered at a specifickey expression. This computation can be triggered by agetoperation on aselectormatching this key expression. The computation function will receive the selector as parameters. ## Storage Storagesare both a queryable and subscriber. They - subscribe tokey expression; - upon receiving publications matching their subscription, they store the associated values; - when queried with a selector matching their subscription, they return the latest values for each matching key. zenohd, the reference implementation of a Zenoh node, supports storages through thestoragesplugin. Since there exist many ways to implement the storing part of the process, thestoragesplugin relies on dynamically loadedvolumesto do the actual value-storing. Each volume has its own tradeoffs, as well as potential uses besides acting as a database forzenohd. ## Admin space The key space of Zenoh dedicated to administering a Zenoh router and its plugins. It is accessible via regular GET/PUT on Zenoh, under the@/router/prefix, whereis the UUID of a Zenoh router. When using the REST API, you can replace thewith thelocalkeyword, meaning the operation addresses the Zenoh router the HTTP client is connected to. For instance, the following keys can be used: - @//router(read-only):Returns a JSON with the status information about the router. - @//router/**(write-only):Allows you to edit the configuration of the router at runtime. Some plugins may extend the admin space, such asStorages, which will add the following keys: - @//router/status/plugins/storage_manager/volumes/(read-only):Returning information about the selected backend in JSON format - @//router/status/plugins/storage_manager/storages/(read-only):Returning information about the selected storage in JSON format --- # Installation · Zenoh - pub/sub, geo distributed storage, query Source: https://zenoh.io/docs/getting-started/installation # Source: https://zenoh.io/docs/getting-started/installation # Installation To start playing with Zenoh we need the Zenoh router and/or the Zenoh client library. ## Installing client library To develop your application Zenoh, you need to install a Zenoh client library. Depending on your programming language, pick one of the following API and refer to the installation and usage instructions in here: - Rust API - Python API - C API - Pico API: A port of Zenoh in C, targeted at low-power devices. Note that if you wish to always have access to all of Zenoh’s latest features, Rust is Zenoh’s original language, and will therefore always be the most feature-complete version. ## Installing the Zenoh router The Zenoh router (a.k.a.zenohd) and its plugins are currently available as pre-built binaries for various platforms. All release packages can be downloaded from: - https://download.eclipse.org/zenoh/zenoh/latest/ Each subdirectory has the name of the Rust target. See the platforms each target corresponds to onhttps://doc.rust-lang.org/stable/rustc/platform-support.html You can also install it via a package manager on macOS (homebrew) or Linux Debian (apt). See instructions below. For other platforms, you can use theDocker imageorbuild itdirectly on your platform. ### MacOS Tap our brew package repository: ``` $ brew tap eclipse-zenoh/homebrew-zenoh ``` Install Zenoh: ``` $ brew install zenoh ``` Then you can start the Zenoh router with this command: ``` $ zenohd ``` ### Ubuntu or any Debian Add Eclipse Zenoh public key to apt keyring ``` $ curl -L https://download.eclipse.org/zenoh/debian-repo/zenoh-public-key | sudo gpg --dearmor --yes --output /etc/apt/keyrings/zenoh-public-key.gpg ``` Add Eclipse Zenoh private repository to the sources list: ``` $ echo "deb [signed-by=/etc/apt/keyrings/zenoh-public-key.gpg] https://download.eclipse.org/zenoh/debian-repo/ /" | sudo tee -a /etc/apt/sources.list > /dev/null $ sudo apt update ``` Install Zenoh: ``` $ sudo apt install zenoh ``` Then you can start the Zenoh router with this command: ``` $ zenohd ``` ### Windows Download the Zenoh archive fromhttps://download.eclipse.org/zenoh/zenoh/latest/: - For Windows 64 bits: get thex86_64-pc-windows-msvc/zenoh--x86_64-pc-windows-msvc.zipfile Unzip the archive. Go to Zenoh directory and start Zenoh router: ``` > cd C:\path\to\zenoh\dir > zenohd.exe ``` ## Testing Your Installation To test the installation, try to see the Zenoh man page by executing the following command: ``` $ zenohd --help ``` You should see the following output on your console: ``` 2024-08-12T13:27:29.724708Z INFO main ThreadId(01) zenohd: zenohd v0.11.0-dev-965-g764be602d built with rustc 1.75.0 (82e1608df 2023-12-21) The zenoh router Usage: zenohd [OPTIONS] Options: -c, --config The configuration file. Currently, this file must be a valid JSON5 or YAML file -l, --listen Locators on which this router will listen for incoming sessions. Repeat this option to open several listeners -e, --connect A peer locator this router will try to connect to. Repeat this option to connect to several peers -i, --id The identifier (as an hexadecimal string, with odd number of chars - e.g.: A0B23...) that zenohd must use. If not set, a random unsigned 128bit integer will be used. WARNING: this identifier must be unique in the system and must be 16 bytes maximum (32 chars)! -P, --plugin A plugin that MUST be loaded. You can give just the name of the plugin, zenohd will search for a library named 'libzenoh_plugin_\.so' (exact name depending the OS). Or you can give such a string: "\:\" Repeat this option to load several plugins. If loading failed, zenohd will exit --plugin-search-dir Directory where to search for plugins libraries to load. Repeat this option to specify several search directories --no-timestamp By default zenohd adds a HLC-generated Timestamp to each routed Data if there isn't already one. This option disables this feature --no-multicast-scouting By default zenohd replies to multicast scouting messages for being discovered by peers and clients. This option disables this feature --rest-http-port Configures HTTP interface for the REST API (enabled by default on port 8000). Accepted values: - a port number - a string with format `:` (to bind the HTTP server to a specific interface) - `none` to disable the REST API --cfg Allows arbitrary configuration changes as column-separated KEY:VALUE pairs, where: - KEY must be a valid config path. - VALUE must be a valid JSON5 string that can be deserialized to the expected type for the KEY field. Examples: - `--cfg='startup/subscribe:["demo/**"]'` - `--cfg='plugins/storage_manager/storages/demo:{key_expr:"demo/example/**",volume:"memory"}'` --adminspace-permissions <[r|w|rw|none]> Configure the read and/or write permissions on the admin space. Default is read only -h, --help Print help (see a summary with '-h') -V, --version Print version ```