{"id":26965581,"url":"https://github.com/zubax/kocherga","last_synced_at":"2025-06-16T04:11:15.864Z","repository":{"id":40424579,"uuid":"131180427","full_name":"Zubax/kocherga","owner":"Zubax","description":"Robust platform-agnostic Cyphal/DroneCAN bootloader for deeply embedded systems","archived":false,"fork":false,"pushed_at":"2024-04-16T20:32:17.000Z","size":905,"stargazers_count":47,"open_issues_count":4,"forks_count":14,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-04-03T07:36:56.485Z","etag":null,"topics":["avionics","bootloader","can-bootloader","cpp","cpp-17","cyphal","cyphal-bootloader","dronecan","dronecan-bootloader","embedded","embedded-cpp","embedded-systems","firmware-update","firmware-updates","hacktoberfest","high-integrity","mcu-bootloader","ota","uavcan","uavcan-bootloader"],"latest_commit_sha":null,"homepage":"https://zubax.com","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Zubax.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-04-26T16:07:06.000Z","updated_at":"2025-01-12T07:30:30.000Z","dependencies_parsed_at":"2024-04-16T21:50:31.195Z","dependency_job_id":"1f3e2256-5533-49c8-bc89-c37cd2d6359e","html_url":"https://github.com/Zubax/kocherga","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/Zubax/kocherga","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zubax%2Fkocherga","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zubax%2Fkocherga/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zubax%2Fkocherga/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zubax%2Fkocherga/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Zubax","download_url":"https://codeload.github.com/Zubax/kocherga/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zubax%2Fkocherga/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260093723,"owners_count":22957726,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["avionics","bootloader","can-bootloader","cpp","cpp-17","cyphal","cyphal-bootloader","dronecan","dronecan-bootloader","embedded","embedded-cpp","embedded-systems","firmware-update","firmware-updates","hacktoberfest","high-integrity","mcu-bootloader","ota","uavcan","uavcan-bootloader"],"created_at":"2025-04-03T07:30:08.393Z","updated_at":"2025-06-16T04:11:15.843Z","avatar_url":"https://github.com/Zubax.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kochergá\n\n[![CI](https://github.com/Zubax/kocherga/actions/workflows/main.yml/badge.svg)](https://github.com/Zubax/kocherga/actions/workflows/main.yml)\n[![Forum](https://img.shields.io/discourse/https/forum.zubax.com/users.svg?color=e00000)](https://forum.zubax.com)\n[![Forum](https://img.shields.io/discourse/https/forum.opencyphal.org/users.svg?color=1700b3)](https://forum.opencyphal.org)\n\n**Kochergá is a robust platform-agnostic [Cyphal](https://opencyphal.org) bootloader for deeply embedded systems.**\n\nTechnical support is provided on the [OpenCyphal Forum](https://forum.opencyphal.org/).\n\nA standard-compliant implementation of the software update server is provided in\n[Yakut](https://github.com/OpenCyphal/yakut#updating-node-software).\n\n## Features\n\n- **Portability** -- Kochergá is written in standard C++17 and is distributed as a header-only library with no external\n  dependencies.\n\n- **Robustness** -- Kochergá is brick-proof. The application (i.e., firmware) update process can be interrupted at any\n  point (e.g., by turning off the power supply or by disconnecting the interface), and it is guaranteed that the device\n  will always end up in a known valid state. If a dysfunctional application image is uploaded, Kochergá can regain\n  control after a watchdog reset.\n\n- **Safety** -- Kochergá verifies the correctness of the application image with a 64-bit hash before every boot.\n  Kochergá's own codebase features extensive test coverage.\n\n- **Multiple supported transports:**\n    - **Cyphal/CAN** + **DroneCAN** -- the protocol version is auto-detected at runtime.\n    - **Cyphal/serial**\n    - More may appear in the future -- new transports are easy to add.\n\n## Usage\n\n### Integration\n\nThe entire library is contained in the header file `kocherga.hpp`; protocol implementations are provided each in a\nseparate header file named `kocherga_*.hpp`. Kochergá does not have any compilation units of its own.\n\nTo integrate Kochergá into your application, just include this repository as a git subtree/submodule, or simply\ncopy-paste the required header files into your source tree.\n\nFor reference, a typical implementation on an ARM Cortex M4 MCU supporting\nCyphal/serial (USB+UART), Cyphal/CAN, and DroneCAN (autodetection) would set you back by about ~32K of flash.\n\n### Application signature\n\nThe bootloader looks for an instance of the `AppInfo` structure located in the ROM image of the application at every\nboot. Only if a valid `AppInfo` structure is found the application will be launched. It is recommended to allocate the\nstructure closer to the beginning of the image in order to speed up its verification. The structure is defined as\nfollows:\n\n| Offset | Type       | Description                                                                                      |\n|--------|------------|--------------------------------------------------------------------------------------------------|\n| -16    | `uint64`   | Constant value 0x5E4415146FC0C4C7 used for locating the descriptor and detecting the byte order. |\n| -8     | `uint8[8]` | Set to `APDesc00`; used for compatibility with legacy deployments.                               |\n| 0      | `uint64`   | CRC-64-WE of the entire application image when this field itself is set to zero.                 |\n| 8      | `uint32`   | Size of the application image, in bytes. Note that the image must be padded to eight bytes.      |\n| 12     | `void32`   | Reserved. Used to contain the 32-bit version control system revision ID; see replacement below.  |\n| 16     | `uint8[2]` | Major and minor semantic version numbers.                                                        |\n| 18     | `uint8`    | Flags: 1 - this is a release build; 2 - this is a dirty build (uncommitted changes present).     |\n| 19     | `void8`    | Reserved; set to 0.                                                                              |\n| 20     | `uint32`   | UNIX UTC build timestamp; i.e., the number of seconds since 1970-01-01T00:00:00Z.                |\n| 24     | `uint64`   | Version control system (VCS) revision ID (e.g., the git commit hash).                            |\n| 32     | `void64`   | Reserved.                                                                                        |\n| 40     | `void64`   | Reserved.                                                                                        |\n\nWhen computing the application image CRC, the process will eventually encounter the location where the CRC itself is\nstored. In order to avoid recursive dependency, the CRC storage location must be replaced with zero bytes when\ncomputing/verifying the CRC. The parameters of the CRC-64 algorithm are the following:\n\n* Initial value: 0xFFFF'FFFF'FFFF'FFFF\n* Polynomial: 0x42F0'E1EB'A9EA'3693\n* Reverse: no\n* Output xor: 0xFFFF'FFFF'FFFF'FFFF\n* Check: 0x62EC'59E3'F1A4'F00A\n\nThe CRC and size fields cannot be populated until after the application binary is compiled and linked. One possible way\nto populate these fields is to initialize them with zeroes in the source code, and then use the\nscript `tools/kocherga_image.py` after the binary is generated to update the fields with their actual values. The script\ncan be invoked from the build system (e.g., from a Makefile rule) trivially as follows:\n\n```sh\nkocherga_image.py application-name-goes-here.bin\n```\n\nThe output will be stored in a file whose name follows the pattern expected by the firmware update server implemented in\nthe [Yakut CLI tool](https://github.com/OpenCyphal/yakut#updating-node-software).\n\n### State machine\n\nThe following diagram documents the state machine of the bootloader:\n\n![Kochergá State Machine Diagram](docs/state_machine.svg \"Kochergá State Machine Diagram\")\n\nThe bootloader states are mapped onto Cyphal node states as follows:\n\n| Bootloader state    | Node mode         | Node health | Vendor-specific status code        |\n|---------------------|-------------------|-------------|------------------------------------|\n| NoAppToBoot         | `SOFTWARE_UPDATE` | `WARNING`   | 0                                  |\n| BootDelay           | `SOFTWARE_UPDATE` | `NOMINAL`   | 0                                  |\n| BootCancelled       | `SOFTWARE_UPDATE` | `ADVISORY`  | 0                                  |\n| AppUpdateInProgress | `SOFTWARE_UPDATE` | `NOMINAL`   | number of read requests, always \u003e0 |\n\n### API usage\n\nThe following snippet demonstrates how to integrate Kochergá into your bootloader executable.\nUser-provided functions are shown in `SCREAMING_SNAKE_CASE()`.\nThis is a stripped-down example; the full API documentation is available in the header files.\n\nThe integration test application available under `/tests/integration/bootloader/` may also be a good reference.\n\n#### Configuring Kochergá\n\n##### Random number generation\n\nKochergá needs a source of random numbers regardless of the transport used.\nYou need to provide a definition of `kocherga::getRandomByte() -\u003e std::uint8_t` for the library to build successfully.\nYou can use this implementation based on `std::rand()`:\n\n```c++\n#include \u003ccstdlib\u003e\n\nauto kocherga::getRandomByte() -\u003e std::uint8_t\n{\n    const auto product =\n        static_cast\u003cstd::uint64_t\u003e(std::rand()) * static_cast\u003cstd::uint64_t\u003e(std::numeric_limits\u003cstd::uint8_t\u003e::max());\n    return static_cast\u003cstd::uint8_t\u003e(product / RAND_MAX);\n}\n\nint main()\n{\n    std::srand(GET_ENTROPY());\n    // bootloader implementation below\n    return 0;\n}\n```\n\nAn alternative is to use a generator from C++ standard library:\n\n```c++\n#include \u003crandom\u003e\n\nauto kocherga::getRandomByte() -\u003e std::uint8_t\n{\n    static std::mt19937 rd{GET_ENTROPY()};\n    return static_cast\u003cstd::uint8_t\u003e(rd() * std::numeric_limits\u003cstd::uint8_t\u003e::max() / std::mt19937::max());\n}\n```\n\nIn both cases beware that you need to initialize the psudorandom sequence with `GET_ENTROPY()`.\nThis function should retrieve a sufficiently random or unique value (such as the number of seconds since epoch).\nLook for more information in the respective documentation of both `std::srand` and `std::mt19937`.\n\n##### Providing custom assert macros\n\nKochergá uses the `assert` macro from the stadard C library to check its invariants.\nIf this is undesireable in your project, you can redefine the following macros.\nYou can do this before including Kochergá or globally in your build system.\n\n```c++\n#define KOCHERGA_ASSERT(x) some_other_assert(x, ...);\n#include \u003ckocherga.hpp\u003e\n```\n\nYou can disable all internal assertions like this:\n\n```c++\n#define KOCHERGA_ASSERT(x) (void)(x);\n#include \u003ckocherga.hpp\u003e\n```\n\n##### Compatibility with environments with missing operator delete\n\nKocherga does not require heap but some toolchains may refuse to link the code if operator delete is not available.\nIf your environment does not define `operator delete`, you can provide a custom definition in your code like this:\n\n```c++\nvoid operator delete(void*) noexcept { std::abort(); }\n```\n\nThis is needed as Kochergá uses virtual destructors, code generation for which includes\nan `operator delete` even if deleting an object through pointer to its base class is\nnot used in your entire application.\n\n#### ROM interface\n\nThe ROM backend abstracts the specifics of reading and writing your ROM (usually this is the on-chip flash memory).\nBe sure to avoid overwriting the bootloader while modifying the ROM.\n\n```c++\nclass MyROMBackend final : public kocherga::IROMBackend\n{\n    auto write(const std::size_t offset, const std::byte* const data, const std::size_t size) override\n        -\u003e std::optional\u003cstd::size_t\u003e\n    {\n        if (WRITE_ROM(offset, data, size))\n        {\n            return size;\n        }\n        return {};  // Failure case\n    }\n\n    auto read(const std::size_t offset, std::byte* const out_data, const std::size_t size) const override\n        -\u003e std::size_t\n    {\n        return READ_ROM(offset, out_data, size);  // Return the number of bytes read (may be less than size).\n    }\n};\n```\n\n#### Media layer interfaces\n\nTransport implementations --- Cyphal/CAN, Cyphal/serial, etc., depending on which transports you need ---\nare interfaced with your hardware as follows.\n\n```c++\nclass MySerialPort final : public kocherga::serial::ISerialPort\n{\n    auto receive() -\u003e std::optional\u003cstd::uint8_t\u003e override\n    {\n        if (SERIAL_RX_PENDING())\n        {\n            return SERIAL_READ_BYTE();\n        }\n        return {};\n    }\n\n    auto send(const std::uint8_t b) -\u003e bool override { return SERIAL_WRITE_BYTE(b); }\n};\n```\n\n```c++\nclass MyCANDriver final : public kocherga::can::ICANDriver\n{\n    auto configure(const Bitrate\u0026                                  bitrate,\n                   const bool                                      silent,\n                   const kocherga::can::CANAcceptanceFilterConfig\u0026 filter) -\u003e std::optional\u003cMode\u003e override\n    {\n        tx_queue_.clear();\n        CAN_CONFIGURE(bitrate, silent, filter);\n        return Mode::FD;  // Or Mode::Classic if CAN FD is not supported by the CAN controller.\n    }\n\n    auto push(const bool          force_classic_can,\n              const std::uint32_t extended_can_id,\n              const std::uint8_t  payload_size,\n              const void* const   payload) -\u003e bool override\n    {\n        const std::chrono::microseconds now = GET_TIME_SINCE_BOOT();\n        // You can use tx_queue_.size() to limit maximum depth of the queue.\n        const bool ok = tx_queue_.push(now, force_classic_can, extended_can_id, payload_size, payload);\n        pollTxQueue(now);\n        return ok;\n    }\n\n    auto pop(PayloadBuffer\u0026 payload_buffer) -\u003e std::optional\u003cstd::pair\u003cstd::uint32_t, std::uint8_t\u003e\u003e override\n    {\n        pollTxQueue();  // Check if the HW TX mailboxes are ready to accept the next frame from the SW queue.\n        return CAN_POP(payload_buffer.data());  // The return value is optional(can_id, payload_size).\n    }\n\n    void pollTxQueue(const std::chrono::microseconds now)\n    {\n        if (const auto* const item = tx_queue_.peek())         // Take the top frame from the prioritized queue.\n        {\n            const bool expired = now \u003e (item-\u003etimestamp + kocherga::can::SendTimeout);  // Drop expired frames.\n            if (expired || CAN_PUSH(item-\u003eforce_classic_can,   // force_classic_can means no DLE, no BRS.\n                                    item-\u003eextended_can_id,\n                                    item-\u003epayload_size,\n                                    item-\u003epayload))\n            {\n                tx_queue_.pop();    // Enqueued into the HW TX mailbox or expired -- remove from the SW queue.\n            }\n        }\n    }\n\n    // Some CAN drivers come with built-in queue (e.g., SocketCAN), in which case this will not be needed.\n    // The recommended heap is https://github.com/pavel-kirienko/o1heap.\n    kocherga::can::TxQueue\u003cvoid*(*)(std::size_t), void(*)(void*)\u003e tx_queue_(\u0026MY_MALLOC, \u0026MY_FREE);\n};\n```\n\n#### Passing arguments from the application\n\nWhen the application is commanded to upgrade itself, it needs to store relevant context into a struct,\nwrite this struct into a pre-defined memory location, and then reboot.\nThe bootloader would check that location to see if there is a valid argument struct in it.\nKochergá provides a convenient class for that --- `kocherga::VolatileStorage\u003c\u003e` ---\nwhich checks the presence and validity of the arguments with a strong 64-bit CRC.\n\n```c++\n/// The application may pass this structure when rebooting into the bootloader.\n/// Feel free to modify the contents to suit your system.\n/// It is a good idea to include an explicit version field here for future-proofing.\nstruct ArgumentsFromApplication\n{\n    std::uint16_t cyphal_serial_node_id;                    ///\u003c Invalid if unknown.\n\n    std::pair\u003cstd::uint32_t, std::uint32_t\u003e cyphal_can_bitrate;         ///\u003c Zeros if unknown.\n    std::uint8_t                            cyphal_can_not_dronecan;    ///\u003c 0xFF-unknown; 0-DroneCAN; 1-Cyphal/CAN.\n    std::uint8_t                            cyphal_can_node_id;         ///\u003c Invalid if unknown.\n\n    std::uint8_t                  trigger_node_index;       ///\u003c 0 - serial, 1 - CAN, \u003e1 - none.\n    std::uint16_t                 file_server_node_id;      ///\u003c Invalid if unknown.\n    std::array\u003cstd::uint8_t, 256\u003e remote_file_path;         ///\u003c Null-terminated string.\n};\nstatic_assert(std::is_trivial_v\u003cArgumentsFromApplication\u003e);\n```\n\n#### Running the bootloader\n\n```c++\n#include \u003ckocherga_serial.hpp\u003e  // Pick the transports you need.\n#include \u003ckocherga_can.hpp\u003e     // In this example we are using Cyphal/serial + Cyphal/CAN.\n\nint main()\n{\n    // Check if the application has passed any arguments to the bootloader via shared RAM.\n    // The address where the arguments are stored obviously has to be shared with the application.\n    // If the application uses heap, then it might be a good idea to alias this area with the heap.\n    std::optional\u003cArgumentsFromApplication\u003e args =\n        kocherga::VolatileStorage\u003cArgumentsFromApplication\u003e(reinterpret_cast\u003cstd::uint8_t*\u003e(0x2000'4000U)).take();\n\n    // Initialize the bootloader core.\n    MyROMBackend rom_backend;\n    kocherga::SystemInfo system_info = GET_SYSTEM_INFO();\n    kocherga::Bootloader::Params params{.linger = args.has_value()};  // Read the docs on the available params.\n    kocherga::Bootloader boot(rom_backend, system_info, params);\n    // It's a good idea to check if the app is valid and safe to boot before adding the nodes.\n    // This way you can skip the potentially slow or disturbing interface initialization on the happy path.\n    // You can do it by calling poll() here once.\n\n    // Add a Cyphal/serial node to the bootloader instance.\n    MySerialPort serial_port;\n    kocherga::serial::SerialNode serial_node(serial_port, system_info.unique_id);\n    if (args \u0026\u0026 (args-\u003ecyphal_serial_node_id \u003c= kocherga::serial::MaxNodeID))\n    {\n        serial_node.setLocalNodeID(args-\u003ecyphal_serial_node_id);\n    }\n    boot.addNode(\u0026serial_node);\n\n    // Add a Cyphal/CAN node to the bootloader instance.\n    std::optional\u003ckocherga::can::ICANDriver::Bitrate\u003e can_bitrate;\n    std::optional\u003cstd::uint8_t\u003e                       cyphal_can_not_dronecan;\n    std::optional\u003ckocherga::NodeID\u003e                   cyphal_can_node_id;\n    if (args)\n    {\n        if (args-\u003ecyphal_can_bitrate.first \u003e 0)\n        {\n            can_bitrate = ICANDriver::Bitrate{args.cyphal_can_bitrate.first, args.cyphal_can_bitrate.second};\n        }\n        cyphal_can_not_dronecan = args-\u003ecyphal_can_not_dronecan;// Will be ignored if invalid.\n        cyphal_can_node_id = args-\u003ecyphal_can_node_id;          // Will be ignored if invalid.\n    }\n    MyCANDriver can_driver;\n    kocherga::can::CANNode can_node(can_driver,\n                                    system_info.unique_id,\n                                    can_bitrate,\n                                    cyphal_can_not_dronecan,\n                                    cyphal_can_node_id);\n    boot.addNode(\u0026can_node);\n\n    while (true)\n    {\n        const auto uptime = GET_TIME_SINCE_BOOT();\n        if (const auto fin = boot.poll(std::chrono::duration_cast\u003cstd::chrono::microseconds\u003e(uptime)))\n        {\n            if (*fin == kocherga::Final::BootApp)\n            {\n                BOOT_THE_APPLICATION();\n            }\n            if (*fin == kocherga::Final::Restart)\n            {\n                RESTART_THE_BOOTLOADER();\n            }\n            assert(false);\n        }\n        // Trigger the update process internally if the required arguments are provided by the application.\n        // The trigger method cannot be called before the first poll().\n        if (args \u0026\u0026 (args-\u003etrigger_node_index \u003c 2))\n        {\n            (void) boot.trigger(args-\u003etrigger_node_index,                   // Use serial or CAN?\n                                args-\u003efile_server_node_id,                  // Which node to download the file from?\n                                std::strlen(args-\u003eremote_file_path.data()), // Remote file path length.\n                                args-\u003eremote_file_path.data());\n            args.reset();\n        }\n        // Sleep until the next hardware event (like reception of CAN frame or UART byte) but no longer than\n        // 1 second. A fixed sleep is also acceptable but the resulting polling interval should be adequate\n        // to avoid data loss (about 100 microseconds is usually ok).\n        WAIT_FOR_EVENT();\n    }\n}\n```\n\n#### Building a compliant application image\n\nDefine the following application signature structure somewhere in your application:\n\n```c++\nstruct AppDescriptor\n{\n    std::uint64_t               magic = 0x5E44'1514'6FC0'C4C7ULL;\n    std::array\u003cstd::uint8_t, 8\u003e signature{{'A', 'P', 'D', 'e', 's', 'c', '0', '0'}};\n\n    std::uint64_t                               image_crc  = 0;     // Populated after build\n    std::uint32_t                               image_size = 0;     // Populated after build\n    [[maybe_unused]] std::array\u003cstd::byte, 4\u003e   _reserved_a{};\n    std::uint8_t                                version_major = SOFTWARE_VERSION_MAJOR;\n    std::uint8_t                                version_minor = SOFTWARE_VERSION_MINOR;\n    std::uint8_t                                flags =\n#if RELEASE_BUILD\n        Flags::ReleaseBuild\n#else\n        0\n#endif\n    ;\n    [[maybe_unused]] std::array\u003cstd::byte, 1\u003e   _reserved_b{};\n    std::uint32_t                               build_timestamp_utc = TIMESTAMP_UTC;\n    std::uint64_t                               vcs_revision_id     = GIT_HASH;\n    [[maybe_unused]] std::array\u003cstd::byte, 16\u003e  _reserved_c{};\n\n    struct Flags\n    {\n        static constexpr std::uint8_t ReleaseBuild = 1U;\n        static constexpr std::uint8_t DirtyBuild   = 2U;\n    };\n};\n\nstatic const volatile AppDescriptor g_app_descriptor;\n// Optionally, use explicit placement near the beginning of the binary:\n//      __attribute__((used, section(\".app_descriptor\")));\n// and define the .app_descriptor section in the linker file.\n```\n\nThen modify your build script to invoke `kocherga_image.py` as explained earlier.\n\n#### Rebooting into the bootloader from the application\n\nIf the application needs to pass arguments to the bootloader, it can be done with the help of\n`kocherga::VolatileStorage\u003c\u003e`.\nEnsure that the definition of `ArgumentsFromApplication` used by the application and by the bootloader use\nthe same binary layout.\n\nNote that the application does not need to depend on the Kochergá library.\nIt is recommended to copy-paste relevant pieces from Kochergá instead; specifically:\n\n- `kocherga::VolatileStorage\u003c\u003e`\n- `kocherga::CRC64`\n\n## Revisions\n\n### v2.0\n\n- Provide dedicated parameter struct to minimize usage errors.\n- Retry timed out requests up to a configurable number of times (https://github.com/Zubax/kocherga/issues/17).\n\n### v1.0\n\nThe first stable revision is virtually identical to v0.2.\n\n### v0.2\n\n- Add helper `kocherga::can::TxQueue`.\n- Promote `kocherga::CRC64` to public API.\n- Add optional support for legacy app descriptors to simplify integration into existing projects.\n- Minor doc and API enhancements.\n\n### v0.1\n\nThe first revision to go public.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzubax%2Fkocherga","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzubax%2Fkocherga","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzubax%2Fkocherga/lists"}