diff --git a/.gitignore b/.gitignore index 918bb0c..be4da0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Editor and IDE specific files .idea/ .vs/ +.vscode/ *.sln.DotSettings.user # C# build directories @@ -20,4 +21,4 @@ __pycache__/ # Artifacts artifacts/ -packages/ \ No newline at end of file +packages/ diff --git a/README.md b/README.md index 28ff0fb..0574872 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # LibStored.Net +[![.NET](https://github.com/DEMCON/libstored.net/actions/workflows/dotnet.yml/badge.svg)](https://github.com/DEMCON/libstored.net/actions/workflows/dotnet.yml) +[![NuGet](https://img.shields.io/nuget/dt/LibStored.Net.svg)](https://www.nuget.org/packages/LibStored.Net) +[![NuGet](https://img.shields.io/nuget/vpre/LibStored.Net.svg)](https://www.nuget.org/packages/LibStored.Net) A 100% native C# implementation of the [libstored](https://github.com/DEMCON/libstored) library, which is a library for storing and retrieving data in a structured way, using a store definition file. The library provides a set of layers to build applications that can communicate with a Debugger or Synchroniser. @@ -71,26 +74,16 @@ flowchart TD - SyncZeroMQLayer: to connect to the Synchronizer (connection) ### Store generation -C# stores can be generated from the `Meta.py`. Use the [libstored](https://demcon.github.io/libstored) generator to parse the `.st` and output the `Meta.py`. -The `generator.py` script in this repository under `/python` -Make sure a virtual environment is created, activated and the requirements.txt is installed in it. -```bash -cd python -python -m venv .venv -./.venv/Scripts/activate -pip install -r requirements.txt -``` - -Call the generator script to create a C# store that implements the `Store` abstract class, which can then be mapped to the `Synchronizer` and `Debugger` in LibStored.Net. +This project uses a C# Source Generator to create strongly-typed store classes from store metadata YAML (.yml) files at compile time. +This YAML file is automatically created when generating code for a store (.st) using [libstored](https://demcon.github.io/libstored) (from version v2.1.0). -```bash -python generator.py -m Meta.py -o output_dir -``` +How to use the Source Generator: +- Add the store metadata files (*.yml) to your project as AdditionalFiles so the source generator can read them during compilation. +- Reference the LibStored.Net either by adding the NuGet package. This package include the Source Generator. +- The generator emits C# store classes implementing the `Store` base class. Use them directly from your code after a build. -#### Source Generator - -The C# code can be automatically generated from a metadata`*.yml` file generated by the `generator.py` script (or in the future - see [libstored issue#76](https://github.com/DEMCON/libstored/issues/76)). Reference the store(s) metadata as AddinionalFiles: +Example csproj snippet: ```xml @@ -116,15 +109,6 @@ Another option would be to create a store per thread, and synchronize them using - StdioLayer (use `libstored.Stdio2Zmq` as alternative for local debugging, not production ready) - SerialLayer (use `libstored.Serial2Zmq` as alternative for local debugging, not production ready) -#### Store source generation -Currently, C# stores are created manually by running a Python script using the Meta.py output of [libstored](demcon.github.io/libstored). -A C# Source Generator could automate this process of converting the store data structure to a C# class, using the hash, names, types, offset and -sizes of all objects in the store. - -A C# Source Generator could generate the stores automatically during compile-time of the C# application using this library. The input of the source generated is the `.rtf` / `.csv` / `Meta.py`, or parse the actual store definition in `.st` files. Maybe [libstored](demcon.github.io/libstored) can be extended to generate a language agnostic `.json` or `.yml` file with the store definition, which can be used by the C# Source Generator to generate the store class. The `.json` file would be the easiest to implement since its easy to parse and can contain all relevant store data. The Source Generator that transform a `*.yml` has been implemented, as described in the [README.md](src/LibStored.Net.Generator/README.md). - -Another option is to create a template file for the C# version of the store in [libstored](demcon.github.io/libstored). - #### Unsupported features Types: `Pointer`, `Pointer32` and `Pointer64` are not supported, as they are not needed in C# applications. The `ptr32` and `ptr64` types are used to store pointers to other objects in the store, which is not needed in C# applications as the objects are stored in managed memory. These types will be mapped to unsigned integers (`uint` and `ulong`) in the C# implementation. @@ -139,7 +123,7 @@ using LibStored.Net.ZeroMQ; using NetMQ; using NetMQ.Sockets; -// Make sure this ExampleStore is created by the source generator or python script. +// Make sure this ExampleStore is created by the source generator from ExampleStore.yml. ExampleStore store = new(); // Attach the store to the debugger @@ -173,6 +157,7 @@ python -m libstored.gui -p 5555 ## Compatibility Tested with libstored versions: +- [v2.1.0](https://github.com/DEMCON/libstored/releases/tag/v2.1.0) - [v2.0.0](https://github.com/DEMCON/libstored/releases/tag/v2.0.0) - [v1.8.0](https://github.com/DEMCON/libstored/releases/tag/v1.8.0) - [v1.7.1](https://github.com/DEMCON/libstored/releases/tag/v1.7.1) (see note below) @@ -186,7 +171,7 @@ The ZeroMQ socket type changed from `PAIR` to `DEALER` for the SyncZeroMQLayer. ## Build -Install the [.NET SDK](https://dotnet.microsoft.com/download), version 9.0 or higher, and run the following command in the root directory of the project: +Install the [.NET SDK](https://dotnet.microsoft.com/download), version 10.0 or higher, and run the following command in the root directory of the project: Build the library and examples: ```bash diff --git a/examples/Arq/LibStored.Arq/Dockerfile b/examples/Arq/LibStored.Arq/Dockerfile index efac3e1..def988e 100644 --- a/examples/Arq/LibStored.Arq/Dockerfile +++ b/examples/Arq/LibStored.Arq/Dockerfile @@ -1,11 +1,6 @@ -# Add ARG for libstored version -ARG LIBSTORED_VERSION=2.0.0 - FROM ubuntu:22.04 as builder -# Use the ARG after FROM -ARG LIBSTORED_VERSION - +# Set noninteractive frontend for apt RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections RUN apt-get update && \ @@ -36,6 +31,9 @@ RUN apt-get update && \ WORKDIR /src/extern +# Add ARG for libstored version +ARG LIBSTORED_VERSION=2.0.0 + RUN wget https://github.com/DEMCON/libstored/archive/refs/tags/v${LIBSTORED_VERSION}.tar.gz && \ tar -xzf v${LIBSTORED_VERSION}.tar.gz --transform="s/libstored-${LIBSTORED_VERSION}/libstored/" && \ rm v${LIBSTORED_VERSION}.tar.gz diff --git a/examples/Arq/LibStored.Net.Arq.AppHost/AppHost.cs b/examples/Arq/LibStored.Net.Arq.AppHost/AppHost.cs index 9ee6a23..93e5971 100644 --- a/examples/Arq/LibStored.Net.Arq.AppHost/AppHost.cs +++ b/examples/Arq/LibStored.Net.Arq.AppHost/AppHost.cs @@ -2,7 +2,7 @@ var builder = DistributedApplication.CreateBuilder(args); -const string libstoredVersion = "2.0.0"; +const string libstoredVersion = "2.1.0"; var arq = builder.AddDockerfile("libstored-arq", "../LibStored.Arq/", "Dockerfile") .WithImage("demcon/libstored-arq-example") .WithImageTag(libstoredVersion) diff --git a/examples/Arq/LibStored.Net.Arq.AppHost/LibStored.Net.Arq.AppHost.csproj b/examples/Arq/LibStored.Net.Arq.AppHost/LibStored.Net.Arq.AppHost.csproj index b649ef0..86d0286 100644 --- a/examples/Arq/LibStored.Net.Arq.AppHost/LibStored.Net.Arq.AppHost.csproj +++ b/examples/Arq/LibStored.Net.Arq.AppHost/LibStored.Net.Arq.AppHost.csproj @@ -1,6 +1,4 @@ - - - + Exe @@ -10,7 +8,7 @@ - + diff --git a/examples/Arq/LibStored.Net.Arq.Console/Program.cs b/examples/Arq/LibStored.Net.Arq.Console/Program.cs index 40bdcc6..aba0af3 100644 --- a/examples/Arq/LibStored.Net.Arq.Console/Program.cs +++ b/examples/Arq/LibStored.Net.Arq.Console/Program.cs @@ -109,7 +109,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) bool connected = false; arqLayer.EventOccurred += (_, e) => { - _logger.LogInformation("Arc Event: {Event}", e.Event); + _logger.LogInformation("Arq Event: {Event}", e.Event); if (e.Event is ArqEvent.Connected or ArqEvent.Reconnect) { connected = true; diff --git a/examples/Arq/README.md b/examples/Arq/README.md new file mode 100644 index 0000000..68f5ee4 --- /dev/null +++ b/examples/Arq/README.md @@ -0,0 +1,23 @@ +# Arq Example + +This example demonstrates cross-language synchronization over a lossy channel. The Arq layer will retransmist lost messages between C++ and C# components. The C++ component runs inside a container while the C# implementation runs natively. The setup is adapted from the libstored project's synchronization examples and uses the Arq transport/adapter to route changes between the stores. + +In this example, the C++ code runs inside a container, while the C# implementation runs natively. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download) +- [Aspire CLI](https://aspire.dev/docs/cli/) +- [Podman](https://podman.io/) or [Docker](https://www.docker.com/products/docker-desktop) + +## Running the Example + +Start the example environment with Aspire: + +```sh +aspire run +``` + +## Learn More + +- [Aspire Documentation](https://aspire.dev/docs/) \ No newline at end of file diff --git a/examples/Sync/LibStored.Net.Example.AppHost/Dockerfile b/examples/Sync/LibStored.Net.Example.AppHost/Dockerfile index e3bfed3..c692af3 100644 --- a/examples/Sync/LibStored.Net.Example.AppHost/Dockerfile +++ b/examples/Sync/LibStored.Net.Example.AppHost/Dockerfile @@ -1,11 +1,6 @@ -# Add ARG for libstored version -ARG LIBSTORED_VERSION=2.0.0 - FROM ubuntu:22.04 as builder -# Use the ARG after FROM -ARG LIBSTORED_VERSION - +# Set noninteractive frontend for apt RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections RUN apt-get update && \ @@ -36,6 +31,9 @@ RUN apt-get update && \ WORKDIR /tmp +# Add ARG for libstored version +ARG LIBSTORED_VERSION=2.0.0 + RUN wget https://github.com/DEMCON/libstored/archive/refs/tags/v${LIBSTORED_VERSION}.tar.gz && \ tar -xzf v${LIBSTORED_VERSION}.tar.gz --transform="s/libstored-${LIBSTORED_VERSION}/libstored/" && \ rm v${LIBSTORED_VERSION}.tar.gz && \ diff --git a/examples/Sync/LibStored.Net.Example.AppHost/LibStored.Net.Example.AppHost.csproj b/examples/Sync/LibStored.Net.Example.AppHost/LibStored.Net.Example.AppHost.csproj index 9a7b996..6ba5d8d 100644 --- a/examples/Sync/LibStored.Net.Example.AppHost/LibStored.Net.Example.AppHost.csproj +++ b/examples/Sync/LibStored.Net.Example.AppHost/LibStored.Net.Example.AppHost.csproj @@ -1,11 +1,8 @@ - - - + Exe CS1591 - true d11b339a-7d36-4112-8969-2ebfad7419e0 diff --git a/examples/Sync/LibStored.Net.Example.AppHost/Program.cs b/examples/Sync/LibStored.Net.Example.AppHost/Program.cs index 2a304d3..32ea345 100644 --- a/examples/Sync/LibStored.Net.Example.AppHost/Program.cs +++ b/examples/Sync/LibStored.Net.Example.AppHost/Program.cs @@ -12,7 +12,7 @@ builder.AddDockerComposeEnvironment("docker-compose"); -const string libstoredVersion = "2.0.0"; +const string libstoredVersion = "2.1.0"; var sync = builder.AddDockerfile("libstored", ".", "Dockerfile") .WithImage("demcon/libstored") .WithImageTag(libstoredVersion) diff --git a/python/ExampleMeta.g.cs.json b/python/ExampleMeta.g.cs.json deleted file mode 100644 index d9a8c60..0000000 --- a/python/ExampleMeta.g.cs.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "ExampleMeta", - "hash": "96cc75d260a03f4931816b2bc0824eed28faa9ae", - "littleEndian": true, - "variables": [ - { - "name": "a double", - "cname": "a_double", - "type": "double", - "size": 8, - "offset": 0, - "init": 1.618 - }, - { - "name": "world", - "cname": "world", - "type": "string", - "size": 7, - "offset": 8, - "init": "hello" - }, - { - "name": "some int", - "cname": "some_int", - "type": "int32", - "size": 4, - "offset": 16, - "init": 42 - } - ] -} \ No newline at end of file diff --git a/python/ExampleMeta.g.cs.yml b/python/ExampleMeta.g.cs.yml deleted file mode 100644 index 938e4fb..0000000 --- a/python/ExampleMeta.g.cs.yml +++ /dev/null @@ -1,22 +0,0 @@ -hash: 96cc75d260a03f4931816b2bc0824eed28faa9ae -littleEndian: true -name: ExampleMeta -variables: -- cname: a_double - init: 1.618 - name: a double - offset: 0 - size: 8 - type: double -- cname: world - init: hello - name: world - offset: 8 - size: 7 - type: string -- cname: some_int - init: 42 - name: some int - offset: 16 - size: 4 - type: int32 diff --git a/python/ExampleMeta.st b/python/ExampleMeta.st deleted file mode 100644 index 89a3cbf..0000000 --- a/python/ExampleMeta.st +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-FileCopyrightText: 2020-2023 Jochem Rutgers -// -// SPDX-License-Identifier: CC0-1.0 - -int32=42 some int -double=1.618 a double -string:7="hello" world diff --git a/python/ExampleMetaMeta.py b/python/ExampleMetaMeta.py deleted file mode 100644 index 6f01abb..0000000 --- a/python/ExampleMetaMeta.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: 2020-2023 Jochem Rutgers -# -# SPDX-License-Identifier: MPL-2.0 - -from collections import namedtuple - -ExampleMetaObjectMeta = namedtuple('ExampleMetaObjectMeta', ['name', 'cname', 'type', 'ctype', 'size', 'isfunction', 'f', 'offset', 'init', 'axi']) - -class ExampleMetaMeta(object): - def __init__(self): - self._objects = [ - ExampleMetaObjectMeta('some int', 'some_int', 'int32', 'int32_t', 4, False, None, 16, int(42), 0), - ExampleMetaObjectMeta('a double', 'a_double', 'double', 'double', 8, False, None, 0, float('1.618'), None), - ExampleMetaObjectMeta('world', 'world', 'string', 'char', 7, False, None, 8, 'hello', None)] - - @property - def name(self): - return 'ExampleMeta' - - @property - def hash(self): - return '96cc75d260a03f4931816b2bc0824eed28faa9ae' - - @property - def objects(self): - return self._objects - - @property - def functions(self): - return filter(lambda o: o.isfunction, self._objects) - - @property - def variables(self): - return filter(lambda o: not o.isfunction, self._objects) - - def __len__(self): - return len(self._objects) - - def __getitem__(self, key): - return next(filter(lambda o: o.name == key, self._objects)) - - def __getattr__(self, name): - return next(filter(lambda o: o.cname == name, self._objects)) - - def __iter__(self): - return iter(self._objects) \ No newline at end of file diff --git a/python/TestStore.g.cs.json b/python/TestStore.g.cs.json deleted file mode 100644 index 550474e..0000000 --- a/python/TestStore.g.cs.json +++ /dev/null @@ -1,495 +0,0 @@ -{ - "name": "TestStore", - "hash": "b0bba06dabc9dff2a4c18c4ebdcc39e1418575b4", - "littleEndian": true, - "variables": [ - { - "name": "init string", - "cname": "init_string", - "type": "string", - "size": 8, - "offset": 0, - "init": ".nan" - }, - { - "name": "double amp/gain", - "cname": "double_amp__gain", - "type": "double", - "size": 8, - "offset": 16, - "init": -3 - }, - { - "name": "init decimal", - "cname": "init_decimal", - "type": "int32", - "size": 4, - "offset": 24, - "init": 42 - }, - { - "name": "init negative", - "cname": "init_negative", - "type": "int32", - "size": 4, - "offset": 28, - "init": -42 - }, - { - "name": "init hex", - "cname": "init_hex", - "type": "int32", - "size": 4, - "offset": 32, - "init": 84 - }, - { - "name": "init bin", - "cname": "init_bin", - "type": "int32", - "size": 4, - "offset": 36, - "init": 5 - }, - { - "name": "init float 1", - "cname": "init_float_1", - "type": "float", - "size": 4, - "offset": 40, - "init": 1 - }, - { - "name": "init float 3.14", - "cname": "init_float_3_14", - "type": "float", - "size": 4, - "offset": 44, - "init": 3.14 - }, - { - "name": "init float -4000", - "cname": "init_float_4000", - "type": "float", - "size": 4, - "offset": 48, - "init": -4000.0 - }, - { - "name": "init float nan", - "cname": "init_float_nan", - "type": "float", - "size": 4, - "offset": 52, - "init": NaN - }, - { - "name": "init float inf", - "cname": "init_float_inf", - "type": "float", - "size": 4, - "offset": 56, - "init": Infinity - }, - { - "name": "init float neg inf", - "cname": "init_float_neg_inf", - "type": "float", - "size": 4, - "offset": 60, - "init": -Infinity - }, - { - "name": "array single", - "cname": "array_single", - "type": "float", - "size": 4, - "offset": 64, - "init": 3 - }, - { - "name": "amp/gain", - "cname": "amp__gain", - "type": "float", - "size": 4, - "offset": 68, - "init": 2 - }, - { - "name": "amp/offset", - "cname": "amp__offset", - "type": "float", - "size": 4, - "offset": 72, - "init": 0.5 - }, - { - "name": "amp/low", - "cname": "amp__low", - "type": "float", - "size": 4, - "offset": 76, - "init": -1 - }, - { - "name": "amp/high", - "cname": "amp__high", - "type": "float", - "size": 4, - "offset": 80, - "init": 10 - }, - { - "name": "amp/override", - "cname": "amp__override", - "type": "float", - "size": 4, - "offset": 84, - "init": NaN - }, - { - "name": "small amp/gain", - "cname": "small_amp__gain", - "type": "float", - "size": 4, - "offset": 88, - "init": 3.5 - }, - { - "name": "small amp/override", - "cname": "small_amp__override", - "type": "float", - "size": 4, - "offset": 92, - "init": NaN - }, - { - "name": "ambiguous amp/gain", - "cname": "ambiguous_amp__gain", - "type": "float", - "size": 4, - "offset": 96, - "init": -1 - }, - { - "name": "init true", - "cname": "init_true", - "type": "bool", - "size": 1, - "offset": 100, - "init": true - }, - { - "name": "init bool 10", - "cname": "init_bool_10", - "type": "bool", - "size": 1, - "offset": 101, - "init": true - }, - { - "name": "array bool[0]", - "cname": "array_bool_0", - "type": "bool", - "size": 1, - "offset": 102, - "init": true - }, - { - "name": "array bool[1]", - "cname": "array_bool_1", - "type": "bool", - "size": 1, - "offset": 103, - "init": true - }, - { - "name": "amp/enable", - "cname": "amp__enable", - "type": "bool", - "size": 1, - "offset": 104, - "init": true - }, - { - "name": "default string", - "cname": "default_string", - "type": "string", - "size": 10, - "offset": 112, - "init": null - }, - { - "name": "default int64", - "cname": "default_int64", - "type": "int64", - "size": 8, - "offset": 128, - "init": null - }, - { - "name": "default uint64", - "cname": "default_uint64", - "type": "uint64", - "size": 8, - "offset": 136, - "init": null - }, - { - "name": "default double", - "cname": "default_double", - "type": "double", - "size": 8, - "offset": 144, - "init": null - }, - { - "name": "default ptr64", - "cname": "default_ptr64", - "type": "ptr64", - "size": 8, - "offset": 152, - "init": null - }, - { - "name": "init string empty", - "cname": "init_string_empty", - "type": "string", - "size": 8, - "offset": 160, - "init": null - }, - { - "name": "default blob", - "cname": "default_blob", - "type": "blob", - "size": 5, - "offset": 176, - "init": null - }, - { - "name": "default int32", - "cname": "default_int32", - "type": "int32", - "size": 4, - "offset": 184, - "init": null - }, - { - "name": "default uint32", - "cname": "default_uint32", - "type": "uint32", - "size": 4, - "offset": 188, - "init": null - }, - { - "name": "default float", - "cname": "default_float", - "type": "float", - "size": 4, - "offset": 192, - "init": null - }, - { - "name": "default ptr32", - "cname": "default_ptr32", - "type": "ptr32", - "size": 4, - "offset": 196, - "init": null - }, - { - "name": "init float 0", - "cname": "init_float_0", - "type": "float", - "size": 4, - "offset": 200, - "init": null - }, - { - "name": "array string[0]", - "cname": "array_string_0", - "type": "string", - "size": 4, - "offset": 204, - "init": null - }, - { - "name": "array string[1]", - "cname": "array_string_1", - "type": "string", - "size": 4, - "offset": 212, - "init": null - }, - { - "name": "array string[2]", - "cname": "array_string_2", - "type": "string", - "size": 4, - "offset": 220, - "init": null - }, - { - "name": "scope/inner int", - "cname": "scope__inner_int", - "type": "int32", - "size": 4, - "offset": 228, - "init": null - }, - { - "name": "value with unit (km/s)", - "cname": "value_with_unit_km__s", - "type": "float", - "size": 4, - "offset": 232, - "init": null - }, - { - "name": "value with complex unit (J/s/m^2)", - "cname": "value_with_complex_unit_J__s__m_2", - "type": "float", - "size": 4, - "offset": 236, - "init": null - }, - { - "name": "value with abiguous unit (m/s)", - "cname": "value_with_abiguous_unit_m__s", - "type": "float", - "size": 4, - "offset": 240, - "init": null - }, - { - "name": "value with abiguous unit (m/h)", - "cname": "value_with_abiguous_unit_m__h", - "type": "float", - "size": 4, - "offset": 244, - "init": null - }, - { - "name": "amp/input", - "cname": "amp__input", - "type": "float", - "size": 4, - "offset": 248, - "init": null - }, - { - "name": "amp/output", - "cname": "amp__output", - "type": "float", - "size": 4, - "offset": 252, - "init": null - }, - { - "name": "small amp/output", - "cname": "small_amp__output", - "type": "float", - "size": 4, - "offset": 256, - "init": null - }, - { - "name": "ambiguous amp/output", - "cname": "ambiguous_amp__output", - "type": "float", - "size": 4, - "offset": 260, - "init": null - }, - { - "name": "default int16", - "cname": "default_int16", - "type": "int16", - "size": 2, - "offset": 264, - "init": null - }, - { - "name": "default uint16", - "cname": "default_uint16", - "type": "uint16", - "size": 2, - "offset": 266, - "init": null - }, - { - "name": "default int8", - "cname": "default_int8", - "type": "int8", - "size": 1, - "offset": 268, - "init": null - }, - { - "name": "default uint8", - "cname": "default_uint8", - "type": "uint8", - "size": 1, - "offset": 269, - "init": null - }, - { - "name": "default bool", - "cname": "default_bool", - "type": "bool", - "size": 1, - "offset": 270, - "init": null - }, - { - "name": "init false", - "cname": "init_false", - "type": "bool", - "size": 1, - "offset": 271, - "init": null - }, - { - "name": "init bool 0", - "cname": "init_bool_0", - "type": "bool", - "size": 1, - "offset": 272, - "init": null - }, - { - "name": "array bool[2]", - "cname": "array_bool_2", - "type": "bool", - "size": 1, - "offset": 273, - "init": null - }, - { - "name": "scope/inner bool", - "cname": "scope__inner_bool", - "type": "bool", - "size": 1, - "offset": 274, - "init": null - }, - { - "name": "some other scope/some other inner bool", - "cname": "some_other_scope__some_other_inner_bool", - "type": "bool", - "size": 1, - "offset": 275, - "init": null - }, - { - "name": "ambiguous amp/enable", - "cname": "ambiguous_amp__enable", - "type": "bool", - "size": 1, - "offset": 276, - "init": null - } - ] -} \ No newline at end of file diff --git a/python/TestStore.g.cs.yml b/python/TestStore.g.cs.yml deleted file mode 100644 index 1486b2a..0000000 --- a/python/TestStore.g.cs.yml +++ /dev/null @@ -1,370 +0,0 @@ -name: TestStore -hash: b0bba06dabc9dff2a4c18c4ebdcc39e1418575b4 -littleEndian: true -variables: -- name: init string - cname: init_string - type: string - size: 8 - offset: 0 - init: '.nan' -- name: double amp/gain - cname: double_amp__gain - type: double - size: 8 - offset: 16 - init: -3 -- name: init decimal - cname: init_decimal - type: int32 - size: 4 - offset: 24 - init: 42 -- name: init negative - cname: init_negative - type: int32 - size: 4 - offset: 28 - init: -42 -- name: init hex - cname: init_hex - type: int32 - size: 4 - offset: 32 - init: 84 -- name: init bin - cname: init_bin - type: int32 - size: 4 - offset: 36 - init: 5 -- name: init float 1 - cname: init_float_1 - type: float - size: 4 - offset: 40 - init: 1 -- name: init float 3.14 - cname: init_float_3_14 - type: float - size: 4 - offset: 44 - init: 3.14 -- name: init float -4000 - cname: init_float_4000 - type: float - size: 4 - offset: 48 - init: -4000.0 -- name: init float nan - cname: init_float_nan - type: float - size: 4 - offset: 52 - init: .nan -- name: init float inf - cname: init_float_inf - type: float - size: 4 - offset: 56 - init: .inf -- name: init float neg inf - cname: init_float_neg_inf - type: float - size: 4 - offset: 60 - init: -.inf -- name: array single - cname: array_single - type: float - size: 4 - offset: 64 - init: 3 -- name: amp/gain - cname: amp__gain - type: float - size: 4 - offset: 68 - init: 2 -- name: amp/offset - cname: amp__offset - type: float - size: 4 - offset: 72 - init: 0.5 -- name: amp/low - cname: amp__low - type: float - size: 4 - offset: 76 - init: -1 -- name: amp/high - cname: amp__high - type: float - size: 4 - offset: 80 - init: 10 -- name: amp/override - cname: amp__override - type: float - size: 4 - offset: 84 - init: .nan -- name: small amp/gain - cname: small_amp__gain - type: float - size: 4 - offset: 88 - init: 3.5 -- name: small amp/override - cname: small_amp__override - type: float - size: 4 - offset: 92 - init: .nan -- name: ambiguous amp/gain - cname: ambiguous_amp__gain - type: float - size: 4 - offset: 96 - init: -1 -- name: init true - cname: init_true - type: bool - size: 1 - offset: 100 - init: true -- name: init bool 10 - cname: init_bool_10 - type: bool - size: 1 - offset: 101 - init: true -- name: array bool[0] - cname: array_bool_0 - type: bool - size: 1 - offset: 102 - init: true -- name: array bool[1] - cname: array_bool_1 - type: bool - size: 1 - offset: 103 - init: true -- name: amp/enable - cname: amp__enable - type: bool - size: 1 - offset: 104 - init: true -- name: default string - cname: default_string - type: string - size: 10 - offset: 112 - init: null -- name: default int64 - cname: default_int64 - type: int64 - size: 8 - offset: 128 - init: null -- name: default uint64 - cname: default_uint64 - type: uint64 - size: 8 - offset: 136 - init: null -- name: default double - cname: default_double - type: double - size: 8 - offset: 144 - init: null -- name: default ptr64 - cname: default_ptr64 - type: ptr64 - size: 8 - offset: 152 - init: null -- name: init string empty - cname: init_string_empty - type: string - size: 8 - offset: 160 - init: null -- name: default blob - cname: default_blob - type: blob - size: 5 - offset: 176 - init: null -- name: default int32 - cname: default_int32 - type: int32 - size: 4 - offset: 184 - init: null -- name: default uint32 - cname: default_uint32 - type: uint32 - size: 4 - offset: 188 - init: null -- name: default float - cname: default_float - type: float - size: 4 - offset: 192 - init: null -- name: default ptr32 - cname: default_ptr32 - type: ptr32 - size: 4 - offset: 196 - init: null -- name: init float 0 - cname: init_float_0 - type: float - size: 4 - offset: 200 - init: null -- name: array string[0] - cname: array_string_0 - type: string - size: 4 - offset: 204 - init: null -- name: array string[1] - cname: array_string_1 - type: string - size: 4 - offset: 212 - init: null -- name: array string[2] - cname: array_string_2 - type: string - size: 4 - offset: 220 - init: null -- name: scope/inner int - cname: scope__inner_int - type: int32 - size: 4 - offset: 228 - init: null -- name: value with unit (km/s) - cname: value_with_unit_km__s - type: float - size: 4 - offset: 232 - init: null -- name: value with complex unit (J/s/m^2) - cname: value_with_complex_unit_J__s__m_2 - type: float - size: 4 - offset: 236 - init: null -- name: value with abiguous unit (m/s) - cname: value_with_abiguous_unit_m__s - type: float - size: 4 - offset: 240 - init: null -- name: value with abiguous unit (m/h) - cname: value_with_abiguous_unit_m__h - type: float - size: 4 - offset: 244 - init: null -- name: amp/input - cname: amp__input - type: float - size: 4 - offset: 248 - init: null -- name: amp/output - cname: amp__output - type: float - size: 4 - offset: 252 - init: null -- name: small amp/output - cname: small_amp__output - type: float - size: 4 - offset: 256 - init: null -- name: ambiguous amp/output - cname: ambiguous_amp__output - type: float - size: 4 - offset: 260 - init: null -- name: default int16 - cname: default_int16 - type: int16 - size: 2 - offset: 264 - init: null -- name: default uint16 - cname: default_uint16 - type: uint16 - size: 2 - offset: 266 - init: null -- name: default int8 - cname: default_int8 - type: int8 - size: 1 - offset: 268 - init: null -- name: default uint8 - cname: default_uint8 - type: uint8 - size: 1 - offset: 269 - init: null -- name: default bool - cname: default_bool - type: bool - size: 1 - offset: 270 - init: null -- name: init false - cname: init_false - type: bool - size: 1 - offset: 271 - init: null -- name: init bool 0 - cname: init_bool_0 - type: bool - size: 1 - offset: 272 - init: null -- name: array bool[2] - cname: array_bool_2 - type: bool - size: 1 - offset: 273 - init: null -- name: scope/inner bool - cname: scope__inner_bool - type: bool - size: 1 - offset: 274 - init: null -- name: some other scope/some other inner bool - cname: some_other_scope__some_other_inner_bool - type: bool - size: 1 - offset: 275 - init: null -- name: ambiguous amp/enable - cname: ambiguous_amp__enable - type: bool - size: 1 - offset: 276 - init: null diff --git a/python/TestStoreMeta.py b/python/TestStoreMeta.py deleted file mode 100644 index c5cb72c..0000000 --- a/python/TestStoreMeta.py +++ /dev/null @@ -1,113 +0,0 @@ -# SPDX-FileCopyrightText: 2020-2023 Jochem Rutgers -# -# SPDX-License-Identifier: MPL-2.0 - -from collections import namedtuple - -TestStoreObjectMeta = namedtuple('TestStoreObjectMeta', ['name', 'cname', 'type', 'ctype', 'size', 'isfunction', 'f', 'offset', 'init', 'axi']) - -class TestStoreMeta(object): - def __init__(self): - self._objects = [ - TestStoreObjectMeta('default int8', 'default_int8', 'int8', 'int8_t', 1, False, None, 268, None, 0), - TestStoreObjectMeta('default int16', 'default_int16', 'int16', 'int16_t', 2, False, None, 264, None, 4), - TestStoreObjectMeta('default int32', 'default_int32', 'int32', 'int32_t', 4, False, None, 184, None, 8), - TestStoreObjectMeta('default int64', 'default_int64', 'int64', 'int64_t', 8, False, None, 128, None, None), - TestStoreObjectMeta('default uint8', 'default_uint8', 'uint8', 'uint8_t', 1, False, None, 269, None, 12), - TestStoreObjectMeta('default uint16', 'default_uint16', 'uint16', 'uint16_t', 2, False, None, 266, None, 16), - TestStoreObjectMeta('default uint32', 'default_uint32', 'uint32', 'uint32_t', 4, False, None, 188, None, 20), - TestStoreObjectMeta('default uint64', 'default_uint64', 'uint64', 'uint64_t', 8, False, None, 136, None, None), - TestStoreObjectMeta('default float', 'default_float', 'float', 'float', 4, False, None, 192, None, 24), - TestStoreObjectMeta('default double', 'default_double', 'double', 'double', 8, False, None, 144, None, None), - TestStoreObjectMeta('default bool', 'default_bool', 'bool', 'bool', 1, False, None, 270, None, 28), - TestStoreObjectMeta('default ptr32', 'default_ptr32', 'ptr32', 'void*', 4, False, None, 196, None, 32), - TestStoreObjectMeta('default ptr64', 'default_ptr64', 'ptr64', 'void*', 8, False, None, 152, None, None), - TestStoreObjectMeta('default blob', 'default_blob', 'blob', 'void', 5, False, None, 176, None, None), - TestStoreObjectMeta('default string', 'default_string', 'string', 'char', 10, False, None, 112, None, None), - TestStoreObjectMeta('init decimal', 'init_decimal', 'int32', 'int32_t', 4, False, None, 24, int(42), 36), - TestStoreObjectMeta('init negative', 'init_negative', 'int32', 'int32_t', 4, False, None, 28, int(-42), 40), - TestStoreObjectMeta('init hex', 'init_hex', 'int32', 'int32_t', 4, False, None, 32, int(84), 44), - TestStoreObjectMeta('init bin', 'init_bin', 'int32', 'int32_t', 4, False, None, 36, int(5), 48), - TestStoreObjectMeta('init true', 'init_true', 'bool', 'bool', 1, False, None, 100, True, 52), - TestStoreObjectMeta('init false', 'init_false', 'bool', 'bool', 1, False, None, 271, None, 56), - TestStoreObjectMeta('init bool 0', 'init_bool_0', 'bool', 'bool', 1, False, None, 272, None, 60), - TestStoreObjectMeta('init bool 10', 'init_bool_10', 'bool', 'bool', 1, False, None, 101, int(10), 64), - TestStoreObjectMeta('init float 0', 'init_float_0', 'float', 'float', 4, False, None, 200, None, 68), - TestStoreObjectMeta('init float 1', 'init_float_1', 'float', 'float', 4, False, None, 40, int(1), 72), - TestStoreObjectMeta('init float 3.14', 'init_float_3_14', 'float', 'float', 4, False, None, 44, float('3.14'), 76), - TestStoreObjectMeta('init float -4000', 'init_float_4000', 'float', 'float', 4, False, None, 48, float('-4000.0'), 80), - TestStoreObjectMeta('init float nan', 'init_float_nan', 'float', 'float', 4, False, None, 52, float('nan'), 84), - TestStoreObjectMeta('init float inf', 'init_float_inf', 'float', 'float', 4, False, None, 56, float('inf'), 88), - TestStoreObjectMeta('init float neg inf', 'init_float_neg_inf', 'float', 'float', 4, False, None, 60, float('-inf'), 92), - TestStoreObjectMeta('init string', 'init_string', 'string', 'char', 8, False, None, 0, '.nan', None), - TestStoreObjectMeta('init string empty', 'init_string_empty', 'string', 'char', 8, False, None, 160, None, None), - TestStoreObjectMeta('f read/write', 'f_read__write', 'double', 'double', 8, True, 1, None, None, None), - TestStoreObjectMeta('f read-only', 'f_read_only', 'uint16', 'uint16_t', 2, True, 2, None, None, None), - TestStoreObjectMeta('f write-only', 'f_write_only', 'string', 'char', 4, True, 3, None, None, None), - TestStoreObjectMeta('array bool[0]', 'array_bool_0', 'bool', 'bool', 1, False, None, 102, True, 96), - TestStoreObjectMeta('array bool[1]', 'array_bool_1', 'bool', 'bool', 1, False, None, 103, True, 100), - TestStoreObjectMeta('array bool[2]', 'array_bool_2', 'bool', 'bool', 1, False, None, 273, None, 104), - TestStoreObjectMeta('array string[0]', 'array_string_0', 'string', 'char', 4, False, None, 204, None, 108), - TestStoreObjectMeta('array string[1]', 'array_string_1', 'string', 'char', 4, False, None, 212, None, 112), - TestStoreObjectMeta('array string[2]', 'array_string_2', 'string', 'char', 4, False, None, 220, None, 116), - TestStoreObjectMeta('array f int[0]', 'array_f_int_0', 'int32', 'int32_t', 4, True, 4, None, None, None), - TestStoreObjectMeta('array f int[1]', 'array_f_int_1', 'int32', 'int32_t', 4, True, 5, None, None, None), - TestStoreObjectMeta('array f int[2]', 'array_f_int_2', 'int32', 'int32_t', 4, True, 6, None, None, None), - TestStoreObjectMeta('array f int[3]', 'array_f_int_3', 'int32', 'int32_t', 4, True, 7, None, None, None), - TestStoreObjectMeta('array f blob[0]', 'array_f_blob_0', 'blob', 'void', 2, True, 8, None, None, None), - TestStoreObjectMeta('array f blob[1]', 'array_f_blob_1', 'blob', 'void', 2, True, 9, None, None, None), - TestStoreObjectMeta('array single', 'array_single', 'float', 'float', 4, False, None, 64, int(3), 120), - TestStoreObjectMeta('scope/inner bool', 'scope__inner_bool', 'bool', 'bool', 1, False, None, 274, None, 124), - TestStoreObjectMeta('scope/inner int', 'scope__inner_int', 'int32', 'int32_t', 4, False, None, 228, None, 128), - TestStoreObjectMeta('some other scope/some other inner bool', 'some_other_scope__some_other_inner_bool', 'bool', 'bool', 1, False, None, 275, None, 132), - TestStoreObjectMeta('value with unit (km/s)', 'value_with_unit_km__s', 'float', 'float', 4, False, None, 232, None, 136), - TestStoreObjectMeta('value with complex unit (J/s/m^2)', 'value_with_complex_unit_J__s__m_2', 'float', 'float', 4, False, None, 236, None, 140), - TestStoreObjectMeta('value with abiguous unit (m/s)', 'value_with_abiguous_unit_m__s', 'float', 'float', 4, False, None, 240, None, 144), - TestStoreObjectMeta('value with abiguous unit (m/h)', 'value_with_abiguous_unit_m__h', 'float', 'float', 4, False, None, 244, None, 148), - TestStoreObjectMeta('amp/input', 'amp__input', 'float', 'float', 4, False, None, 248, None, 152), - TestStoreObjectMeta('amp/enable', 'amp__enable', 'bool', 'bool', 1, False, None, 104, True, 156), - TestStoreObjectMeta('amp/gain', 'amp__gain', 'float', 'float', 4, False, None, 68, int(2), 160), - TestStoreObjectMeta('amp/offset', 'amp__offset', 'float', 'float', 4, False, None, 72, float('0.5'), 164), - TestStoreObjectMeta('amp/low', 'amp__low', 'float', 'float', 4, False, None, 76, int(-1), 168), - TestStoreObjectMeta('amp/high', 'amp__high', 'float', 'float', 4, False, None, 80, int(10), 172), - TestStoreObjectMeta('amp/override', 'amp__override', 'float', 'float', 4, False, None, 84, float('nan'), 176), - TestStoreObjectMeta('amp/output', 'amp__output', 'float', 'float', 4, False, None, 252, None, 180), - TestStoreObjectMeta('small amp/gain', 'small_amp__gain', 'float', 'float', 4, False, None, 88, float('3.5'), 184), - TestStoreObjectMeta('small amp/override', 'small_amp__override', 'float', 'float', 4, False, None, 92, float('nan'), 188), - TestStoreObjectMeta('small amp/output', 'small_amp__output', 'float', 'float', 4, False, None, 256, None, 192), - TestStoreObjectMeta('ambiguous amp/gain', 'ambiguous_amp__gain', 'float', 'float', 4, False, None, 96, int(-1), 196), - TestStoreObjectMeta('ambiguous amp/enable', 'ambiguous_amp__enable', 'bool', 'bool', 1, False, None, 276, None, 200), - TestStoreObjectMeta('ambiguous amp/output', 'ambiguous_amp__output', 'float', 'float', 4, False, None, 260, None, 204), - TestStoreObjectMeta('double amp/gain', 'double_amp__gain', 'double', 'double', 8, False, None, 16, int(-3), None)] - - @property - def name(self): - return 'TestStore' - - @property - def hash(self): - return 'b0bba06dabc9dff2a4c18c4ebdcc39e1418575b4' - - @property - def objects(self): - return self._objects - - @property - def functions(self): - return filter(lambda o: o.isfunction, self._objects) - - @property - def variables(self): - return filter(lambda o: not o.isfunction, self._objects) - - def __len__(self): - return len(self._objects) - - def __getitem__(self, key): - return next(filter(lambda o: o.name == key, self._objects)) - - def __getattr__(self, name): - return next(filter(lambda o: o.cname == name, self._objects)) - - def __iter__(self): - return iter(self._objects) \ No newline at end of file diff --git a/python/generator.py b/python/generator.py deleted file mode 100644 index 733bcbe..0000000 --- a/python/generator.py +++ /dev/null @@ -1,331 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2025 Guus Kuiper -# -# SPDX-License-Identifier: MIT - -import argparse -import importlib -import jinja2 -import os -import sys -import struct -import types -import json -import base64 -import yaml -from dataclasses import dataclass, asdict -from typing import Iterable, Protocol, Union, runtime_checkable - -@runtime_checkable -class MetaObjectMeta(Protocol): - # Defines the expected interface for a meta object class - - def __init__(self, name, cname, type, ctype, size, isfunction, f, offset, init, axi): - self.name = name - self.cname = cname - self.type = type - self.ctype = ctype - self.size = size - self.offset = offset - self.isfunction = isfunction - - - def _asdict(self) -> dict: - ... - - def __repr__(self): - return f'MetaObjectMeta(name={self.name}, cname={self.cname}, type={self.type})' - - -@runtime_checkable -class MetaProtocol(Protocol): - # Defines the expected interface for a meta protocol class - - @property - def name(self) -> str: - ... - - @property - def hash(self) -> str: - ... - - @property - def objects(self) -> Iterable[MetaObjectMeta]: - ... - - @property - def functions(self) -> Iterable[MetaObjectMeta]: - ... - - @property - def variables(self) -> Iterable[MetaObjectMeta]: - ... - -@dataclass -class StoreVariable: - name: str - cname: str - type: str - size: int - offset: int - init: Union[str, float, int, None] - -@dataclass -class StoreModel: - name: str - hash: str - littleEndian: bool - variables: list[StoreVariable] - -def cstr(s): - bs = str(s).encode() - cs = '"' - for b in bs: - if b < 32 or b >= 127: - cs += f'\\x{b:02x}' - else: - cs += chr(b) - return cs + '"' - -def cstypes(o): - return { - 'bool': 'bool', - 'int8': 'sbyte', - 'uint8': 'byte', - 'int16': 'short', - 'uint16': 'ushort', - 'int32': 'int', - 'uint32': 'uint', - 'int64': 'long', - 'uint64': 'ulong', - 'float': 'float', - 'double': 'double', - 'ptr32': 'uint', - 'ptr64': 'ulong', - 'blob': 'byte[]', - 'string': 'string' - }[o] - -def csetypes(o : str): - t = { - 'bool': 'Types.Bool', - 'int8': 'Types.Int8', - 'uint8': 'Types.Uint8', - 'int16': 'Types.Int16', - 'uint16': 'Types.Uint16', - 'int32': 'Types.Int32', - 'uint32': 'Types.Uint32', - 'int64': 'Types.Int64', - 'uint64': 'Types.Uint64', - 'float': 'Types.Float', - 'double': 'Types.Double', - 'ptr32': 'Types.Pointer32', - 'ptr64': 'Types.Pointer64', - 'blob': 'Types.Blob', - 'string': 'Types.String' - }[o] - - return t - -def csprop(cname: str) -> str: - # Split by underscores, capitalize each part, and join - parts = cname.split('_') - prefix = '_' if not parts[0] else '' - return prefix + ''.join(capitalize_first(part) for part in cname.split('_')) - -def csfield(cname: str) -> str: - # Split by underscores, capitalize each part except the first, then join and prefix with '_' - parts = cname.split('_') - prefix = '_' if not parts[0] else '' - field_name = parts[0] + ''.join(capitalize_first(part) for part in parts[1:]) - return f'{prefix}_{field_name}' - -def capitalize_first(s: str) -> str: - """ - Capitalizes only the first character of the string, leaving the rest unchanged. - """ - return s[:1].upper() + s[1:] if s else s - -def is_variable(o): - return not o.isfunction and o.type != 'blob' and o.type != 'string' - -def encode_string(x): - s = x.encode() - assert len(s) <= x.size - return s + bytes([0] * (x.size - len(s))) - -def bytes_to_hex_list(data) -> str: - return ', '.join(f'0x{b:02x}' for b in data) - -def encode_initial(xs : Iterable[MetaObjectMeta], littleEndian=True) -> str: - endian = '<' if littleEndian else '>' - res = bytearray() - for x in xs: - if x.init is None: - break - padding = x.offset - len(res) - if padding > 0: - # Fill with zeros until the offset - res += bytearray([0] * padding) # Fill with zeros until the offset - - res += encode(x, littleEndian) - return bytes_to_hex_list(res) - -def encode(x, littleEndian=True): - endian = '<' if littleEndian else '>' - res = { - 'bool': lambda x: struct.pack(endian + '?', not x in [False, 'false', 0]), - 'int8': lambda x: struct.pack(endian + 'b', int(x)), - 'uint8': lambda x: struct.pack(endian + 'B', int(x)), - 'int16': lambda x: struct.pack(endian + 'h', int(x)), - 'uint16': lambda x: struct.pack(endian + 'H', int(x)), - 'int32': lambda x: struct.pack(endian + 'i', int(x)), - 'uint32': lambda x: struct.pack(endian + 'I', int(x)), - 'int64': lambda x: struct.pack(endian + 'q', int(x)), - 'uint64': lambda x: struct.pack(endian + 'Q', int(x)), - 'float': lambda x: struct.pack(endian + 'f', float(x)), - 'double': lambda x: struct.pack(endian + 'd', float(x)), - 'ptr32': lambda x: struct.pack(endian + 'L', int(x)), - 'ptr64': lambda x: struct.pack(endian + 'Q', int(x)), - 'blob': lambda x: bytearray(x), - 'string': lambda s: s.encode() + bytes([0] * (x.size - len(s.encode()))) - }[x.type](x.init) - return res - - -def load_class_from_file(filepath: str, classname: str) -> MetaProtocol: - spec = importlib.util.spec_from_file_location(classname, filepath) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - cls = getattr(module, classname) - return cls - -def load_class_from_source(source_code: str) -> MetaProtocol: - module = types.ModuleType("dynamic_module") - exec(source_code, module.__dict__) - for name, obj in module.__dict__.items(): - if isinstance(obj, object) and name.endswith('Meta') and not name.endswith('ObjectMeta'): - return obj - return None - -def generate_model(meta : MetaProtocol) -> StoreModel: - - variables = [] - for v in sorted(meta.variables, key=lambda x: x.offset): - init = v.init - - # Fix for bool initializations libstored also accepts int > 1, so convert to bool true / false - if v.type == 'bool' and v.init is not None: - init = bool(v.init) - - sv = StoreVariable( - name=v.name, - cname=v.cname, - type=v.type, - size=v.size, - offset=v.offset, - init=init - ) - variables.append(sv) - - store_model = StoreModel( - name=meta.name, - hash=meta.hash, - littleEndian=True, - variables=variables - ) - - return store_model - -def generate(meta : MetaProtocol, tmpl_filename : str) -> tuple[str, str, str]: - - # Validate the meta object - if not isinstance(meta, MetaProtocol): - raise TypeError("Expected a MetaProtocol instance") - - if not isinstance(next(meta.variables), MetaObjectMeta): - raise TypeError("Expected a MetaObjectMeta instances") - - model = generate_model(meta) - - jenv = jinja2.Environment( - loader=jinja2.FileSystemLoader(os.path.dirname(tmpl_filename)), - trim_blocks=True) - - jenv.filters['cstr'] = cstr - jenv.filters['cstypes'] = cstypes - jenv.filters['csetypes'] = csetypes - jenv.filters['csprop'] = csprop - jenv.filters['csfield'] = csfield - jenv.filters['encode_initial'] = encode_initial - jenv.tests['variable'] = is_variable - - tmpl = jenv.get_template(os.path.basename(tmpl_filename)) - - source = tmpl.render(store=meta) - - js = json.dumps(asdict(model), indent=2) - - yml = yaml.safe_dump(asdict(model), sort_keys=False) - - return source, js, yml - -def generate_cs_meta_py(meta_py_code: str) -> str: - """ - Generates a C# store file from the provided python meta source code. - """ - - # Load the class from the source code - cls = load_class_from_source(meta_py_code) - if cls is None: - raise ValueError("No class found in the provided source code.") - - # Create an instance of the class - meta_instance = cls() - - template = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'store.cs.tmpl') - - cs, _ = generate(meta_instance, template) - - return cs - - -def main(): - parser = argparse.ArgumentParser(description='Generator using store meta data') - script_dir = os.path.dirname(os.path.abspath(__file__)) - - parser.add_argument('-m', '--meta', type=str, default=os.path.join(script_dir, 'TestStoreMeta.py'), help='path to Meta.py as input', dest="meta") - parser.add_argument('-t', '--template', type=str, default=os.path.join(script_dir, 'store.cs.tmpl'), help='path to jinja2 template META is to be applied to', dest="template") - parser.add_argument('-o', '--output', type=str, help='output file for jinja2 generated content', dest="output") - - args = parser.parse_args() - - file = os.path.abspath(args.meta) - template = os.path.abspath(args.template) - - module_name = os.path.splitext(os.path.basename(file))[0] - loaded_meta_class = load_class_from_file(file, module_name) - - meta = loaded_meta_class() # Create instance - - output_name = args.output if args.output else os.path.join(script_dir, f'{meta.name}.g.cs') - output_name = os.path.abspath(output_name) - - cs, json, yml = generate(meta, template) - - output_dir = os.path.dirname(output_name) - if not os.path.exists(output_dir): - os.mkdir(output_dir) - - with open(output_name, 'w') as f: - f.write(cs) - - with open(output_name + '.json', 'w') as jf: - jf.write(json) - - with open(output_name + '.yml', 'w') as yf: - yf.write(yml) - - -if __name__ == "__main__": - main() diff --git a/python/requirements.txt b/python/requirements.txt deleted file mode 100644 index 23a5dae..0000000 --- a/python/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Jinja2==3.1.6 -PyYAML>=6.0 \ No newline at end of file diff --git a/python/store.cs.tmpl b/python/store.cs.tmpl deleted file mode 100644 index 8ce43cb..0000000 --- a/python/store.cs.tmpl +++ /dev/null @@ -1,90 +0,0 @@ -#nullable enable - -namespace LibStored.Net.Example.Console; - -/// -/// {{store.name}} generated from {{store.name}}Meta.py. -/// -public class {{store.name}} : global::LibStored.Net.Store, global::System.ComponentModel.INotifyPropertyChanged -{ - private static readonly byte[] InitialBuffer = [ - {{ store.variables|sort(attribute='offset')|encode_initial }} - ]; - - private readonly byte[] _data = new byte[{{ (store.variables|max(attribute='offset')).offset + (store.variables|max(attribute='offset')).size }}]; - - private readonly global::System.Collections.Generic.Dictionary _debugDirectory = []; - -{% for o in store.variables %} -{% if o is variable %} - private readonly global::LibStored.Net.StoreVariable<{{o.type|cstypes}}> {{o.cname|csfield}}; -{% else %} - private readonly global::LibStored.Net.StoreVariant<{{o.type|cstypes}}> {{o.cname|csfield}}; -{% endif %} -{% endfor %} - - public {{store.name}}() - { - {{store.name}}.InitialBuffer.AsSpan().CopyTo(_data.AsSpan()); - -{% for o in store.variables %} -{% if o is variable %} - {{o.cname|csfield}} = new global::LibStored.Net.StoreVariable<{{o.type|cstypes}}>({{o.offset}}, {{o.size}}, this); -{% else %} - {{o.cname|csfield}} = new global::LibStored.Net.StoreVariant<{{o.type|cstypes}}>({{o.offset}}, {{o.size}}, this); -{% endif %} -{% endfor %} - -{% for o in store.variables %} - _debugDirectory.Add("/{{o.name}}", new global::LibStored.Net.DebugVariantInfo({{o.type|csetypes}}, {{o.offset}}, {{o.size}})); -{% endfor %} - } - - public event global::System.ComponentModel.PropertyChangedEventHandler? PropertyChanged; - - public override global::System.Span GetBuffer() => _data; - public override global::System.Collections.Generic.IReadOnlyDictionary GetDebugVariants() => _debugDirectory; - - public override string Name => "/{{store.name}}"; - public override string Hash => "{{store.hash}}"; - public override int VariableCount => {{store.variables|list|count}}; - -{% for o in store.variables %} - /// - /// {{o.name}}. - /// - public {{o.type|cstypes}} {{o.cname|csprop}} - { -{% if o is variable %} - get => {{o.cname|csfield}}.Get(); - set => {{o.cname|csfield}}.Set(value); -{% else %} - get => StoreVariantExtensions.Get({{o.cname|csfield}}); - set => StoreVariantExtensions.Set({{o.cname|csfield}}, value); -{% endif %} - } - -{% endfor %} - /// - /// Notify property changed for the specific variable that changed based on the offset. - /// - /// - /// - public override void Changed(int offset) - { - if (PropertyChanged is null) - { - return; - } - - global::System.ComponentModel.PropertyChangedEventArgs args = new(offset switch - { -{% for o in store.variables|sort(attribute='offset') %} - {{o.offset}} => nameof({{store.name}}.{{o.cname|csprop}}), -{% endfor %} - _ => throw new ArgumentOutOfRangeException(nameof(offset), offset, "Unknown offset") - }); - - PropertyChanged?.Invoke(this, args); - } -} diff --git a/src/LibStored.Net.Generator/README.md b/src/LibStored.Net.Generator/README.md index d4fe194..3eb2065 100644 --- a/src/LibStored.Net.Generator/README.md +++ b/src/LibStored.Net.Generator/README.md @@ -21,9 +21,6 @@ > The metadata file is a YAML file; its filename should end with `.yml` or `.yaml` - Use the script `python/generator.py` to generate a metadata file. - In the future this store metadata file will be generated by libstored, see https://github.com/DEMCON/libstored/issues/76. - For example to use all files in the `Stores` folder ending with `.yml`: ```xml diff --git a/src/LibStored.Net/Protocol/ArqLayer.cs b/src/LibStored.Net/Protocol/ArqLayer.cs index 781d40e..8588d9f 100644 --- a/src/LibStored.Net/Protocol/ArqLayer.cs +++ b/src/LibStored.Net/Protocol/ArqLayer.cs @@ -29,7 +29,7 @@ public enum ArqEvent /// Retransmit, /// - /// A connection has been establised. + /// A connection has been established. /// Connected, } @@ -59,7 +59,7 @@ public class ArqLayer : ProtocolLayer /// /// Number of successive retransmits before the event is emitted. /// - public const int RetransmitCallbackThreshold = 10; + public const int RetransmitCallbackThreshold = 16; private const byte NopFlag = 0x40; private const byte AckFlag = 0x80; @@ -71,6 +71,7 @@ public class ArqLayer : ProtocolLayer private int _encodeQueueBytes; private bool _encoding; + private bool _connected; private bool _pauseTransmit; private bool _didTransmit; private byte _sendSeq; @@ -94,6 +95,11 @@ public ArqLayer(int maxEncodeBufferSize = 0) /// public event EventHandler? EventOccurred; + /// + /// Gets whether this ARQ layer has an established connection. + /// + public bool IsConnected => _connected; + /// public override void Reset() { @@ -103,6 +109,7 @@ public override void Reset() } _encoding = false; + _connected = false; _pauseTransmit = false; _didTransmit = false; _retransmits = 0; @@ -110,8 +117,8 @@ public override void Reset() _recvSeq = 0; _buffer.Clear(); + base.Disconnected(); base.Reset(); - KeepAlive(); } @@ -125,6 +132,8 @@ public override bool Flush() /// public override void Encode(ReadOnlySpan buffer, bool last) { + bool isIdle = !WaitingForAck(); + if (_maxEncodeBufferSize > 0 && _maxEncodeBufferSize < _encodeQueueBytes + buffer.Length + 1 ) { Event(ArqEvent.EncodeBufferOverflow); @@ -154,80 +163,94 @@ public override void Encode(ReadOnlySpan buffer, bool last) } } - Transmit(); + if (isIdle) + { + Transmit(); + } } /// public override void Decode(Span buffer) { - bool reconnect = false; + bool resetHandshake = false; Span response = [0, 0]; byte responseLen = 0; bool doTransmit = false; bool doDecode = false; + Debug.Assert(!_pauseTransmit); + _pauseTransmit = true; + while (buffer.Length > 0) { byte header = buffer[0]; + byte headerSeq = (byte)(header & SeqMask); + if ((header & AckFlag) != 0) { - if (WaitingForAck() && (header & SeqMask) == (_encodeQueue.First()[0] & SeqMask)) + if (headerSeq == 0) + { + // Maybe be an ack to our reset message. + resetHandshake = true; + } + + if (WaitingForAck() && headerSeq == (_encodeQueue.First()[0] & SeqMask)) { PopEncodeQueue(); _retransmits = 0; doTransmit = true; - if ((header & SeqMask) == 0) + if (resetHandshake) { - reconnect = true; - // Libstored uses a method, here an event is used. + // This is an ack to our reset message. + _connected = true; + _recvSeq = NextSeq(0); + base.Connected(); Event(ArqEvent.Connected); } } buffer = buffer.Slice(1); } - else if((header & SeqMask) == _recvSeq) + else if (headerSeq == 0) { - // The next message - response[responseLen++] = (byte) (_recvSeq | AckFlag); - _recvSeq = NextSeq(_recvSeq); - - doDecode = (header & NopFlag) == 0; - doTransmit = true; - - buffer = buffer.Slice(1); - } - else if ((header & SeqMask) == 0) - { - // Unexpected reset - Event(ArqEvent.Reconnect); + // Reset handshake // Send ack - _recvSeq = NextSeq(0); response[responseLen++] = AckFlag; + // Drop the rest + buffer = Span.Empty; // Also reset our send seq. - if (!reconnect && (_encodeQueueBytes == 0 || _encodeQueue.First()[0] != NopFlag)) + if (!resetHandshake) { - // Insert at front of queue. - PushEncodeQueueRaw([NopFlag], false); - _sendSeq = NextSeq(0); + PushReset(); + + doTransmit = true; - // Re-encode existing outbound messages. - foreach (byte[] bytes in _encodeQueue.Skip(1)) + if (_connected) { - bytes[0] = (byte)((bytes[0] & ~SeqMask) | _sendSeq); - _sendSeq = NextSeq(_sendSeq); + _connected = false; + Event(ArqEvent.Reconnect); } + + base.Disconnected(); } + } + else if(headerSeq == _recvSeq) + { + // The next message + response[responseLen++] = (byte) (_recvSeq | AckFlag); + _recvSeq = NextSeq(_recvSeq); + doDecode = (header & NopFlag) == 0; doTransmit = true; + buffer = buffer.Slice(1); } - else if(NextSeq((byte)(header & SeqMask)) == _recvSeq) + else if(NextSeq(headerSeq) == _recvSeq) { // Retransmit, send an Ack again response[responseLen++] = (byte) (header & SeqMask | AckFlag); @@ -263,9 +286,6 @@ public override void Decode(Span buffer) if (doDecode) { - Debug.Assert(!_pauseTransmit); - _pauseTransmit = true; - _didTransmit = false; // Decode and queue only base.Decode(buffer); @@ -273,11 +293,12 @@ public override void Decode(Span buffer) { doTransmit = true; } - - Debug.Assert(_pauseTransmit); - _pauseTransmit = false; } + // Dont expect recursion + Debug.Assert(_pauseTransmit); + _pauseTransmit = false; + if (responseLen > 0) { base.Encode(response.Slice(0,responseLen), !doTransmit); @@ -308,6 +329,25 @@ public void KeepAlive() Transmit(); } + /// + /// Call this function at a regular interval to retransmit messages, when necessary. + /// When no messages are queued, this function does nothing. + /// + /// True when a message was sent. + public bool Process() => Transmit(); + + /// + /// Don't propagate the connected event. + /// A reconnection is handled by this layer itself, via re-transmits or resets. + /// + public override void Connected() {} + + /// + /// Don't propagate the disconnected event. + /// A reconnection is handled by this layer itself, via re-transmits or resets. + /// + public override void Disconnected() {} + /// /// Transmit the first message in the encode queue. /// @@ -351,6 +391,18 @@ private void PushEncodeQueue(ReadOnlySpan buffer) PushEncodeQueueRaw(bytes); } + private void PushReset() + { + while (_encodeQueue.Count > 0) + { + PopEncodeQueue(); + } + + _sendSeq = 0; + PushEncodeQueueRaw([NopFlag], false); + _recvSeq = 0; + } + private void PushEncodeQueueRaw(byte[] bytes, bool back = true) { if (back) diff --git a/src/LibStored.Net/Protocol/Crc16Layer.cs b/src/LibStored.Net/Protocol/Crc16Layer.cs index ccad310..4fee223 100644 --- a/src/LibStored.Net/Protocol/Crc16Layer.cs +++ b/src/LibStored.Net/Protocol/Crc16Layer.cs @@ -112,6 +112,13 @@ public override void Reset() base.Reset(); } + /// + public override void Disconnected() + { + _crc = _init; + base.Disconnected(); + } + /// public override int Mtu() => base.Mtu() switch { diff --git a/src/LibStored.Net/Protocol/Crc32Layer.cs b/src/LibStored.Net/Protocol/Crc32Layer.cs index 4c332d7..67bca13 100644 --- a/src/LibStored.Net/Protocol/Crc32Layer.cs +++ b/src/LibStored.Net/Protocol/Crc32Layer.cs @@ -128,6 +128,13 @@ public override void Reset() base.Reset(); } + /// + public override void Disconnected() + { + _crc = _init; + base.Disconnected(); + } + /// public override int Mtu() => base.Mtu() switch { diff --git a/src/LibStored.Net/Protocol/Crc8Layer.cs b/src/LibStored.Net/Protocol/Crc8Layer.cs index fc49199..ec7b5c2 100644 --- a/src/LibStored.Net/Protocol/Crc8Layer.cs +++ b/src/LibStored.Net/Protocol/Crc8Layer.cs @@ -103,6 +103,13 @@ public override void Reset() base.Reset(); } + /// + public override void Disconnected() + { + _crc = _init; + base.Disconnected(); + } + /// public override int Mtu() => base.Mtu() switch { diff --git a/src/LibStored.Net/Protocol/Protocol.cs b/src/LibStored.Net/Protocol/Protocol.cs index 33f1da1..129d9b3 100644 --- a/src/LibStored.Net/Protocol/Protocol.cs +++ b/src/LibStored.Net/Protocol/Protocol.cs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2025 Guus Kuiper -// +// // SPDX-License-Identifier: MIT namespace LibStored.Net.Protocol; @@ -69,15 +69,52 @@ public ProtocolLayer() { } /// The upper protocol layer to wrap. public void Wrap(ProtocolLayer up) { - if (up._down is not null) + // Disconnect our old upper layer. + ProtocolLayer? oldUp = _up; + if (oldUp is not null) { - throw new InvalidOperationException("Cannot wrap a layer that already has a down layer."); + oldUp._down = null; + _up = null; } + // Inject ourselves below the given layer. + ProtocolLayer? currentBottom = Bottom(); + ProtocolLayer? injectAbove = up._down; + + if (injectAbove is not null) + { + currentBottom._down = injectAbove; + injectAbove._up = currentBottom; + currentBottom = injectAbove.Bottom(); + } + + // Set out new upper layer up._down = this; _up = up; + + // Invoke all notifications. + oldUp?.Disconnected(); + + if (injectAbove is not null) + { + currentBottom.Connected(); + } + else + { + up.Connected(); + } } + /// + /// (Re)connected notification (bottom-up). + /// + public virtual void Connected() => _up?.Connected(); + + /// + /// Disconnected notification (bottom-up). + /// + public virtual void Disconnected() => _up?.Disconnected(); + private ProtocolLayer Bottom() { ProtocolLayer p = this; diff --git a/src/LibStored.Net/Protocol/SegmentationLayer.cs b/src/LibStored.Net/Protocol/SegmentationLayer.cs index f4f66c9..5e436e1 100644 --- a/src/LibStored.Net/Protocol/SegmentationLayer.cs +++ b/src/LibStored.Net/Protocol/SegmentationLayer.cs @@ -113,6 +113,20 @@ public override void Reset() /// public override int Mtu() => 0; // No MTU limit for segmentation layer + /// + public override void Connected() + { + _encoded = 0; + base.Connected(); + } + + /// + public override void Disconnected() + { + _decodeBuffer.Clear(); + base.Disconnected(); + } + private int LowerMtu() { int lowerMtu = base.Mtu(); diff --git a/src/LibStored.Net/Protocol/TerminalLayer.cs b/src/LibStored.Net/Protocol/TerminalLayer.cs index e42441c..ecdea08 100644 --- a/src/LibStored.Net/Protocol/TerminalLayer.cs +++ b/src/LibStored.Net/Protocol/TerminalLayer.cs @@ -133,6 +133,14 @@ public override void Reset() base.Reset(); } + /// + public override void Disconnected() + { + _data.Clear(); + _decodingMessage = false; + base.Disconnected(); + } + /// public override int Mtu() => base.Mtu() switch { diff --git a/src/LibStored.Net/Synchronization/SyncConnection.cs b/src/LibStored.Net/Synchronization/SyncConnection.cs index e5941b5..b592178 100644 --- a/src/LibStored.Net/Synchronization/SyncConnection.cs +++ b/src/LibStored.Net/Synchronization/SyncConnection.cs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2025 Guus Kuiper -// +// // SPDX-License-Identifier: MIT using System.Buffers; @@ -18,14 +18,22 @@ public class StoreInfo /// Gets or sets the last known sequence number for the store. /// public Seq Seq { get; set; } + /// /// Gets or sets the outgoing ID for the store in the connection. + /// The value 0 indicates not connected. /// public Id IdOut { get; set; } + /// /// Gets or sets a value indicating whether this store is a source in the connection. /// public bool Source { get; set; } + + /// + /// + /// + public bool IsConnected => IdOut != 0; } /// @@ -133,7 +141,7 @@ public Seq Process(StoreJournal store) return 0; } - if (!store.HasChanged(info.Seq)) + if (!info.IsConnected || !store.HasChanged(info.Seq)) { // No recent changes return 0; @@ -209,6 +217,7 @@ public override void Decode(Span buffer) }; _stores[journal] = info; + Debug.Assert(info.IsConnected); id = NextId(); _idIn[id] = journal; EncodeId(id); @@ -246,6 +255,7 @@ public override void Decode(Span buffer) info.Seq = seq; info.IdOut = welcomeId; Debug.Assert(info.Source); + Debug.Assert(info.IsConnected); break; } @@ -300,7 +310,7 @@ public override void Decode(Span buffer) } StoreInfo info = _stores[value]; - if (info.Source && info.IdOut == 0) + if (info.Source && info.IsConnected) { HelloAgain(value); } @@ -323,7 +333,7 @@ public override void Decode(Span buffer) break; } - if (info.Source && info.IdOut == 0) + if (info.Source && info.IsConnected) { HelloAgain(journal); } @@ -349,6 +359,13 @@ public override void Decode(Span buffer) /// True if the store is being synchronized; otherwise, false. public bool IsSynchronizing(StoreJournal journal) => _stores.ContainsKey(journal); + /// + /// Returns if the given store is currently connected over this connection. + /// + /// + /// + public bool IsConnected(StoreJournal journal) => _stores.TryGetValue(journal, out StoreInfo? info) && info.IsConnected; + /// /// Resets the connection, drops all non-source stores, and sends Hello messages for sources. /// @@ -361,6 +378,20 @@ public override void Reset() HelloAgain(); } + /// + public override void Connected() + { + HelloAgain(); + base.Connected(); + } + + /// + public override void Disconnected() + { + DropNonSources(); + base.Disconnected(); + } + /// /// Encode a Bye and drop all stores. /// @@ -372,7 +403,7 @@ protected void SendBye() } /// - /// + /// /// /// protected void SendBye(string hash) @@ -572,6 +603,7 @@ private void HelloAgain(StoreJournal store) Id id = _idIn.FirstOrDefault(x => x.Value == store).Key; Debug.Assert(id > 0); + Debug.Assert(!info.IsConnected); EncodeCmd(SyncConnection.Hello); store.EncodeHash(this, false); diff --git a/src/LibStored.Net/Synchronization/Synchronizer.cs b/src/LibStored.Net/Synchronization/Synchronizer.cs index e9be371..fc90a07 100644 --- a/src/LibStored.Net/Synchronization/Synchronizer.cs +++ b/src/LibStored.Net/Synchronization/Synchronizer.cs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2025 Guus Kuiper -// +// // SPDX-License-Identifier: MIT namespace LibStored.Net.Synchronization; @@ -149,6 +149,18 @@ public bool IsSynchronizing(StoreJournal journal, SyncConnection notOverConnecti return false; } + /// + /// Returns if the given store is currently connected over the given connection. + /// + /// + /// + /// + public bool IsConnected(StoreJournal journal, SyncConnection connection) + { + SyncConnection? c = _connections.Contains(connection) ? connection : null; + return c is not null && c.IsConnected(journal); + } + internal StoreJournal? ToJournal(string hash) { if (string.IsNullOrEmpty(hash)) diff --git a/src/LibStored.Net/Types.cs b/src/LibStored.Net/Types.cs index 00a1efa..f28bc59 100644 --- a/src/LibStored.Net/Types.cs +++ b/src/LibStored.Net/Types.cs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2025 Guus Kuiper -// +// // SPDX-License-Identifier: MIT namespace LibStored.Net; @@ -83,12 +83,12 @@ public enum Types : byte /// /// Native signed integer type (platform size). /// - Int = Types.FlagFixed | Types.FlagInt | (sizeof(int) - 1), + Int = Types.FlagFixed | Types.FlagInt | Types.FlagSigned | (sizeof(int) - 1), /// /// Native unsigned integer type (platform size). /// - Uint = Types.FlagFixed | (sizeof(uint) - 1), + Uint = Types.FlagFixed | Types.FlagInt | (sizeof(uint) - 1), /// /// 32-bit floating point type. diff --git a/src/LibStored.Net/TypesExtensions.cs b/src/LibStored.Net/TypesExtensions.cs index 661e581..7b6cc0d 100644 --- a/src/LibStored.Net/TypesExtensions.cs +++ b/src/LibStored.Net/TypesExtensions.cs @@ -39,6 +39,13 @@ public static class TypesExtensions /// True if the type is signed; otherwise, false. public static bool IsSigned(this Types type) => (type & Types.FlagSigned) != 0; + /// + /// Determines whether the type is a floating point number. + /// + /// The type to check. + /// True if the type is floating point number; otherwise, false. + public static bool IsFloat(this Types type) => type.IsFixed() && type.IsSigned() && !type.IsInt(); + /// /// Determines whether the type is a special type (undefined length). /// diff --git a/test/LibStored.Net.Tests/ArqLayerTests.cs b/test/LibStored.Net.Tests/ArqLayerTests.cs index ce0a5ca..ef8fa20 100644 --- a/test/LibStored.Net.Tests/ArqLayerTests.cs +++ b/test/LibStored.Net.Tests/ArqLayerTests.cs @@ -76,6 +76,7 @@ public void RetransmitTest() top.Encode(" 2"u8, true); // Triggers retransmit of 1 + arq.KeepAlive(); Assert.Equal(ProtocolTests.String([0x01, .." 1"u8]), bottom.Encoded[1]); top.Flush(); @@ -191,6 +192,7 @@ public void ReconnectTest() Assert.Equal(ProtocolTests.String([0x80, 0x40]), bottom.Encoded[2]); top.Encode(" 3"u8, true); + arq.KeepAlive(); Assert.Equal(ProtocolTests.String([0x40]), bottom.Encoded[3]); // Retransmit bottom.Decode([0x40]); @@ -198,25 +200,27 @@ public void ReconnectTest() // Separate ack/reset does not fully reconnect; expect reset. bottom.Decode([0xC0]); - Assert.Equal(ProtocolTests.String([0x01, .." 3"u8]), bottom.Encoded[5]); + arq.KeepAlive(); // nop, no retransmit after reset + Assert.Equal(ProtocolTests.String([0x41]), bottom.Encoded[5]); bottom.Decode([0x40]); Assert.Equal(ProtocolTests.String([0x80, 0x40]), bottom.Encoded[6]); // Full reset again // In same message, reconnection completes. bottom.Decode([0xC0, 0x40]); - Assert.Equal(ProtocolTests.String([0x80, 0x01, .." 3"u8]), bottom.Encoded[7]); + Assert.Equal(ProtocolTests.String([0x80]), bottom.Encoded[7]); bottom.Decode([0x81]); top.Encode(" 4"u8, true); - Assert.Equal(ProtocolTests.String([0x02, .." 4"u8]), bottom.Encoded[8]); - bottom.Decode([0x82]); + Assert.Equal(ProtocolTests.String([0x01, .." 4"u8]), bottom.Encoded[8]); + bottom.Decode([0x81]); top.Encode(" 5"u8, true); - Assert.Equal(ProtocolTests.String([0x03, .." 5"u8]), bottom.Encoded[9]); + Assert.Equal(ProtocolTests.String([0x02, .." 5"u8]), bottom.Encoded[9]); bottom.Decode([0x40]); Assert.Equal(ProtocolTests.String([0x80, 0x40]), bottom.Encoded[10]); // Full reset again bottom.Decode([0x80]); - Assert.Equal(ProtocolTests.String([0x01, .." 5"u8]), bottom.Encoded[11]); + arq.KeepAlive(); + Assert.Equal(ProtocolTests.String([0x41]), bottom.Encoded[11]); } [Fact] @@ -232,6 +236,9 @@ public void Reconnect2Test() LoopbackLayer l = new(a, b); + // Complete handshake + a.KeepAlive(); + la.Encode(" 1"u8, true); Assert.Equal(" 1", lb.Decoded[0]);