diff --git a/.github/workflows/ci-dotnet.yaml b/.github/workflows/ci-dotnet.yaml
index d80dcf5a..3e56599c 100644
--- a/.github/workflows/ci-dotnet.yaml
+++ b/.github/workflows/ci-dotnet.yaml
@@ -1,35 +1,74 @@
-name: dotnet CI build
+# Keep this filename: its existing run counter supplies all package versions.
+name: Protocol packages
-on: push
+on:
+ push:
+ branches: ["**"]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
jobs:
- donet-ci-build:
+ version:
runs-on: ubuntu-latest
+ outputs:
+ package-version: ${{ steps.version.outputs.version }}
steps:
- - name: Checkout source
- uses: actions/checkout@v4
+ - uses: actions/checkout@v6
+ - name: Test shared version rules
+ run: python3 -B -m unittest discover -s scripts -p 'test_*.py'
+ - name: Calculate the shared package version
+ id: version
+ run: |
+ VERSION=$(python3 scripts/package-version.py)
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- - name: Setup .NET Core @ Latest
- uses: actions/setup-dotnet@v4
+ nuget:
+ needs: version
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-dotnet@v4
with:
- dotnet-version: |
- 9.0.x
+ dotnet-version: 9.0.x
source-url: https://nuget.pkg.github.com/BluEye-Robotics/index.json
env:
- NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
-
- - name: Sanitize branch name
- id: branches
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Build the NuGet package
+ env:
+ PACKAGE_VERSION: ${{ needs.version.outputs.package-version }}
+ run: dotnet pack Blueye.Protocol.Protobuf.csproj -p:Version="$PACKAGE_VERSION" -c Release -o out
+ - name: Publish to GitHub Packages
+ if: github.event_name != 'pull_request' && github.repository == 'BluEye-Robotics/ProtocolDefinitions'
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- BRANCH="${GITHUB_REF_NAME}"
- SANITIZED=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | tr '[:upper:]' '[:lower:]' | cut -c1-63)
- echo "sanitized-branch-name=$SANITIZED" >> "$GITHUB_OUTPUT"
+ dotnet nuget push ./out/*.nupkg --skip-duplicate --no-symbols \
+ --source https://nuget.pkg.github.com/BluEye-Robotics/index.json --api-key "$NUGET_AUTH_TOKEN"
- - name: Build project and generate NuGet package
- run: |
- dotnet pack Blueye.Protocol.Protobuf.csproj --version-suffix "${{ steps.branches.outputs.sanitized-branch-name }}.$GITHUB_RUN_NUMBER" -c Release -o out
+ kotlin:
+ needs: version
+ permissions:
+ contents: read
+ packages: write
+ uses: ./.github/workflows/ci-kotlin.yaml
+ with:
+ package-version: ${{ needs.version.outputs.package-version }}
- - name: Push generated package to GitHub registry
- env:
- NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
- run: dotnet nuget push ./out/*.nupkg --skip-duplicate --no-symbols --api-key $NUGET_AUTH_TOKEN
\ No newline at end of file
+ swift:
+ needs: [version, nuget, kotlin]
+ if: >-
+ github.event_name != 'pull_request' &&
+ github.repository == 'BluEye-Robotics/ProtocolDefinitions' &&
+ github.ref == 'refs/heads/master' &&
+ vars.SWIFT_PROTOCOL_REPOSITORY != ''
+ uses: ./.github/workflows/ci-swift.yaml
+ with:
+ package-version: ${{ needs.version.outputs.package-version }}
+ secrets:
+ CI_PAT: ${{ secrets.CI_PAT }}
diff --git a/.github/workflows/ci-kotlin.yaml b/.github/workflows/ci-kotlin.yaml
new file mode 100644
index 00000000..acdb8fbc
--- /dev/null
+++ b/.github/workflows/ci-kotlin.yaml
@@ -0,0 +1,55 @@
+name: Kotlin package
+
+on:
+ workflow_call:
+ inputs:
+ package-version:
+ required: true
+ type: string
+
+permissions:
+ contents: read
+
+defaults:
+ run:
+ working-directory: kotlin
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: "17"
+ - uses: gradle/actions/setup-gradle@v4
+ - name: Build, test, and publish to a local test repository
+ env:
+ PACKAGE_VERSION: ${{ inputs.package-version }}
+ run: ./gradlew --no-daemon build publishProtocolPublicationToSmokeTestRepository -PpackageVersion="$PACKAGE_VERSION"
+ - name: Verify package from a separate consuming project
+ env:
+ PACKAGE_VERSION: ${{ inputs.package-version }}
+ run: ./gradlew --no-daemon -p smoke-test run -PpackageVersion="$PACKAGE_VERSION"
+
+ publish:
+ if: github.event_name != 'pull_request' && github.repository == 'BluEye-Robotics/ProtocolDefinitions'
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: "17"
+ - uses: gradle/actions/setup-gradle@v4
+ - name: Publish to GitHub Packages
+ env:
+ GITHUB_ACTOR: ${{ github.actor }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PACKAGE_VERSION: ${{ inputs.package-version }}
+ run: ./gradlew --no-daemon publishProtocolPublicationToGitHubPackagesRepository -PpackageVersion="$PACKAGE_VERSION"
diff --git a/.github/workflows/ci-swift.yaml b/.github/workflows/ci-swift.yaml
new file mode 100644
index 00000000..0ca53c1a
--- /dev/null
+++ b/.github/workflows/ci-swift.yaml
@@ -0,0 +1,29 @@
+name: Release blueye.protocol.swift
+
+on:
+ workflow_call:
+ inputs:
+ package-version:
+ required: true
+ type: string
+ secrets:
+ CI_PAT:
+ required: true
+
+permissions:
+ contents: read
+
+jobs:
+ dispatch:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Dispatch the protocol revision and shared version
+ env:
+ GH_TOKEN: ${{ secrets.CI_PAT }}
+ SWIFT_PROTOCOL_REPOSITORY: ${{ vars.SWIFT_PROTOCOL_REPOSITORY }}
+ PACKAGE_VERSION: ${{ inputs.package-version }}
+ run: |
+ gh api "repos/$SWIFT_PROTOCOL_REPOSITORY/dispatches" \
+ -f event_type=protocol-update \
+ -f "client_payload[sha]=$GITHUB_SHA" \
+ -f "client_payload[version]=$PACKAGE_VERSION"
diff --git a/.gitignore b/.gitignore
index afc52765..19d07f1c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,7 @@ node_modules
# Rider directories
.idea
+
+# Gradle build state
+.gradle
+.kotlin
diff --git a/Blueye.Protocol.Protobuf.csproj b/Blueye.Protocol.Protobuf.csproj
index 64cb0c37..a386a5bc 100644
--- a/Blueye.Protocol.Protobuf.csproj
+++ b/Blueye.Protocol.Protobuf.csproj
@@ -5,7 +5,6 @@
true
13.0
Blueye.Protocol.Protobuf
- 6.3.0
Blueye Robotics AS
True
Library with dotnet representation of the ProtocolDefinition protobuf files.
@@ -25,7 +24,7 @@
-
+
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..17e961a3
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+ $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)version.txt').Trim()).0
+
+
diff --git a/README.md b/README.md
index 3a3da4f6..662f90ea 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,49 @@ Drones running a Blunux version \< 3.0 will need to use the legacy protocol. [bl
Version 3 of the Blueye communication protocol is based on [Protocol Buffers](https://developers.google.com/protocol-buffers).
-The protobuf definitions are compiled to language specific libraries, and are available as a [NuGet package](https://github.com/BluEye-Robotics/ProtocolDefinitions/packages/1239508), [Python package](https://pypi.org/project/blueye.protocol/) and a [npm package](https://www.npmjs.com/package/@blueyerobotics/protocol-definitions); a Rust crate is available as a git dependency (see below).
+The protobuf definitions are compiled to language specific libraries, and are available as a [NuGet package](https://github.com/BluEye-Robotics/ProtocolDefinitions/packages/1239508), [Python package](https://pypi.org/project/blueye.protocol/) and a [npm package](https://www.npmjs.com/package/@blueyerobotics/protocol-definitions). Swift sources are maintained in [blueye.protocol.swift](https://github.com/BluEye-Robotics/blueye.protocol.swift). This repository also builds a [Kotlin/JVM Maven package](kotlin/README.md) and a Rust crate (see below).
Automatically generated documentation for the protocol format can be found [here](https://blueyebuildserver.blob.core.windows.net/protocoldefinitions/docs/protocol.html).
+### Swift and Kotlin
+
+The Swift package exposes `BlueyeProtocol` through Swift Package Manager from
+the separate [blueye.protocol.swift repository](https://github.com/BluEye-Robotics/blueye.protocol.swift).
+Like Python, it pins this repository as a submodule and commits generated
+sources in its own repository. Swift tags use the same version as NuGet and Maven.
+
+The Kotlin/JVM library publishes to GitHub Packages as
+`com.blueyerobotics:protocol-definitions`, with the full Protobuf runtime and
+generated Java classes plus Kotlin DSLs. See [Kotlin setup and publishing](kotlin/README.md).
+
+All three packages use `protobuf_definitions/` as their source of truth. No
+generated Java/Kotlin sources are committed here. Package versions are separate
+from the wire protocol version described above.
+
+`version.txt` holds the Blueye App major/minor version, currently `6.5`. The
+**Protocol packages** workflow (`ci-dotnet.yaml`) computes one version for every
+package using its existing run counter:
+
+- Master builds: stable `6.5.` releases.
+- Other branches and pull requests: `6.5.-branch-`.
+- Local builds: `6.5.0-local` for Maven and `6.5.0` for .NET unless overridden.
+
+Merging to master automatically creates the next stable package version. Update
+`version.txt` only when the Blueye App major/minor version changes. NuGet and Maven
+publish on upstream branch pushes; Swift receives master releases after both
+publish successfully. Its receiver generates and validates the pinned schema,
+then commits and tags it with the same version. Delayed older releases are
+generated and tagged separately without moving the Swift default branch backward.
+Python keeps its separate repository and release versioning. Do not rename or
+recreate the coordinating workflow without preserving a monotonically increasing
+build counter.
+
+Once the Swift receiver is installed, set the Actions variable
+`SWIFT_PROTOCOL_REPOSITORY` to `BluEye-Robotics/blueye.protocol.swift`. The existing
+`CI_PAT` secret must have permission to dispatch events to that repository.
+`ci-swift.yaml` receives the shared version and sends it with the exact protocol
+SHA. Dispatch is skipped until the variable is configured.
+
### Rust
The `rust/` directory contains the `blueye-protocol` crate. Code is generated at build time from `protobuf_definitions/*.proto` using [prost](https://crates.io/crates/prost) and [protox](https://crates.io/crates/protox) — no `protoc` installation required.
diff --git a/kotlin/README.md b/kotlin/README.md
new file mode 100644
index 00000000..8b869aa6
--- /dev/null
+++ b/kotlin/README.md
@@ -0,0 +1,99 @@
+# Kotlin/JVM package
+
+Maven coordinate: `com.blueyerobotics:protocol-definitions`.
+
+The library generates Java message classes and Kotlin builder DSLs from the
+shared schemas. It uses the **full** `protobuf-java` and `protobuf-kotlin` runtimes,
+including descriptors, reflection, and `Any`. It targets Java 8 bytecode, uses
+Kotlin 2.2.21, and builds with JDK 17 and the included Gradle 8.14.3 wrapper.
+It is a JVM/Android library, not a Kotlin Multiplatform library, and contains no
+network transport implementation.
+
+## Consume
+
+Add the repository to `dependencyResolutionManagement.repositories` in your
+`settings.gradle.kts` (or your project's `repositories` block):
+
+```kotlin
+maven {
+ url = uri("https://maven.pkg.github.com/blueye-robotics/ProtocolDefinitions")
+ credentials {
+ username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
+ password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
+ }
+ content { includeGroup("com.blueyerobotics") }
+}
+mavenCentral()
+```
+
+Store `gpr.user` and `gpr.key` in your user-level `~/.gradle/gradle.properties`,
+not in source control. Use a GitHub personal access token (classic) with
+`read:packages` and access to the repository. In Actions, a workflow's
+`GITHUB_TOKEN` can be used where it has the required package access.
+
+After the first release is published, use its full version from the package workflow
+(the run number below is an example):
+
+```kotlin
+dependencies {
+ implementation("com.blueyerobotics:protocol-definitions:6.5.815")
+}
+```
+
+Both Protobuf runtimes are transitive API dependencies. Consumers do not need a
+Protobuf Gradle plugin or `protoc` installation.
+
+```kotlin
+import blueye.protocol.Attitude
+import blueye.protocol.attitude
+import com.google.protobuf.Any as ProtoAny
+
+val original = attitude { yaw = 90f }
+val decoded = Attitude.parseFrom(original.toByteArray())
+val packed = ProtoAny.pack(original)
+val unpacked = packed.unpack(Attitude::class.java)
+val descriptor = Attitude.getDescriptor()
+```
+
+Java classes live directly in `blueye.protocol` because the schemas enable
+`java_multiple_files`. This does not change protobuf wire names or field numbers.
+For Protobuf JSON, add `com.google.protobuf:protobuf-java-util` at the same runtime
+version (currently `4.36.2`); tests exercise JSON with an `Any` type registry.
+
+## Develop and test
+
+From `kotlin/`:
+
+```sh
+./gradlew build
+./gradlew publishProtocolPublicationToSmokeTestRepository -PpackageVersion=6.5.0-local
+./gradlew -p smoke-test run -PpackageVersion=6.5.0-local
+```
+
+The Gradle build downloads the matching `protoc` artifact and runtimes using the
+version in `../protobuf.version`. Java and Kotlin sources are generated only in
+the build directory. The separate smoke-test project resolves the actual local
+Maven publication to verify the JAR, Kotlin DSL, and transitive dependencies.
+The publication includes a sources JAR, POM, and Gradle module metadata.
+
+## Publishing
+
+`ci-kotlin.yaml` is called by the **Protocol packages** workflow in
+`ci-dotnet.yaml`. The caller supplies the same version to NuGet, Maven, and Swift;
+this reusable workflow does not calculate its own build number. Builds and tests
+run on pushes and pull requests. Upstream branch pushes publish to GitHub Packages
+using `GITHUB_TOKEN` with `packages: write`; pull requests and forks do not publish.
+
+The shared `version.txt` contains the Blueye App major/minor version, currently
+`6.5`. Master publishes stable `6.5.` versions automatically. Other
+branches publish `6.5.-branch-`. Local builds default
+to `6.5.0-local`. The run counter belongs to `ci-dotnet.yaml`, so merges to master
+do not require a version-file edit. NuGet and Maven publish first, then the same
+version and source SHA are dispatched to Swift for generation and tagging.
+
+Published versions are immutable. A workflow rerun keeps its original version;
+prefer rerunning failed jobs so successful publications are not repeated.
+
+To publish manually after testing, set credentials as above and run
+`./gradlew publishProtocolPublicationToGitHubPackagesRepository -PpackageVersion=`.
+The generic `publish` task also writes to the local smoke-test repository.
diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts
new file mode 100644
index 00000000..a2a4e1fa
--- /dev/null
+++ b/kotlin/build.gradle.kts
@@ -0,0 +1,110 @@
+import com.google.protobuf.gradle.*
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ `java-library`
+ `maven-publish`
+ kotlin("jvm") version "2.2.21"
+ id("com.google.protobuf") version "0.9.5"
+}
+
+group = "com.blueyerobotics"
+val baseVersion = file("../version.txt").readText().trim()
+version = providers.gradleProperty("packageVersion").getOrElse("$baseVersion.0-local")
+
+val protocVersion = file("../protobuf.version").readText().trim()
+val protobufVersion = "4.$protocVersion"
+
+repositories {
+ mavenCentral()
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(17))
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ withSourcesJar()
+}
+
+kotlin {
+ compilerOptions.jvmTarget.set(JvmTarget.JVM_1_8)
+}
+
+tasks.withType().configureEach {
+ options.release.set(8)
+}
+
+dependencies {
+ // Public APIs expose the full Java message classes and Kotlin DSL types.
+ api("com.google.protobuf:protobuf-java:$protobufVersion")
+ api("com.google.protobuf:protobuf-kotlin:$protobufVersion")
+ testImplementation(kotlin("test-junit5"))
+ testImplementation("com.google.protobuf:protobuf-java-util:$protobufVersion")
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.4")
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.11.4")
+}
+
+sourceSets {
+ main {
+ proto {
+ setSrcDirs(listOf("../protobuf_definitions"))
+ }
+ }
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:$protobufVersion"
+ }
+ generateProtoTasks {
+ all().configureEach {
+ builtins {
+ // The Java builtin is enabled by default. Kotlin builds on that output.
+ id("kotlin")
+ }
+ }
+ }
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
+
+publishing {
+ publications {
+ register("protocol") {
+ from(components["java"])
+ artifactId = "protocol-definitions"
+ pom {
+ name.set("Blueye Protocol Definitions")
+ description.set("Protocol Buffer messages and Kotlin DSLs for Blueye underwater drones.")
+ url.set("https://github.com/BluEye-Robotics/ProtocolDefinitions")
+ licenses {
+ license {
+ name.set("GNU Lesser General Public License v3.0 only")
+ url.set("https://www.gnu.org/licenses/lgpl-3.0.html")
+ }
+ }
+ scm {
+ url.set("https://github.com/BluEye-Robotics/ProtocolDefinitions")
+ connection.set("scm:git:https://github.com/BluEye-Robotics/ProtocolDefinitions.git")
+ }
+ }
+ }
+ }
+ repositories {
+ maven {
+ name = "GitHubPackages"
+ url = uri("https://maven.pkg.github.com/blueye-robotics/ProtocolDefinitions")
+ credentials {
+ username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
+ password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
+ }
+ }
+ // Exercise the published artifact without credentials or remote writes.
+ maven {
+ name = "SmokeTest"
+ url = uri(layout.buildDirectory.dir("smoke-repository"))
+ }
+ }
+}
diff --git a/kotlin/gradle.properties b/kotlin/gradle.properties
new file mode 100644
index 00000000..265ded2f
--- /dev/null
+++ b/kotlin/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
+kotlin.code.style=official
diff --git a/kotlin/gradle/wrapper/gradle-wrapper.jar b/kotlin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..1b33c55b
Binary files /dev/null and b/kotlin/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/kotlin/gradle/wrapper/gradle-wrapper.properties b/kotlin/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..78cb6e16
--- /dev/null
+++ b/kotlin/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/kotlin/gradlew b/kotlin/gradlew
new file mode 100755
index 00000000..23d15a93
--- /dev/null
+++ b/kotlin/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/kotlin/gradlew.bat b/kotlin/gradlew.bat
new file mode 100644
index 00000000..db3a6ac2
--- /dev/null
+++ b/kotlin/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/kotlin/settings.gradle.kts b/kotlin/settings.gradle.kts
new file mode 100644
index 00000000..d2500958
--- /dev/null
+++ b/kotlin/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "protocol-definitions"
diff --git a/kotlin/smoke-test/build.gradle.kts b/kotlin/smoke-test/build.gradle.kts
new file mode 100644
index 00000000..2d2bd675
--- /dev/null
+++ b/kotlin/smoke-test/build.gradle.kts
@@ -0,0 +1,25 @@
+plugins {
+ application
+ kotlin("jvm") version "2.2.21"
+}
+
+repositories {
+ maven {
+ url = uri("../build/smoke-repository")
+ content { includeGroup("com.blueyerobotics") }
+ }
+ mavenCentral()
+}
+
+dependencies {
+ // Resolve the publication, including its transitive runtime dependencies.
+ implementation("com.blueyerobotics:protocol-definitions:${providers.gradleProperty("packageVersion").get()}")
+}
+
+kotlin {
+ jvmToolchain(17)
+}
+
+application {
+ mainClass.set("ConsumerKt")
+}
diff --git a/kotlin/smoke-test/settings.gradle.kts b/kotlin/smoke-test/settings.gradle.kts
new file mode 100644
index 00000000..07ada910
--- /dev/null
+++ b/kotlin/smoke-test/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "protocol-consumer-smoke-test"
diff --git a/kotlin/smoke-test/src/main/kotlin/Consumer.kt b/kotlin/smoke-test/src/main/kotlin/Consumer.kt
new file mode 100644
index 00000000..7585b4f9
--- /dev/null
+++ b/kotlin/smoke-test/src/main/kotlin/Consumer.kt
@@ -0,0 +1,11 @@
+import blueye.protocol.Attitude
+import blueye.protocol.attitude
+import com.google.protobuf.Any as ProtoAny
+
+fun main() {
+ val original = attitude { yaw = 90f }
+ check(Attitude.parseFrom(original.toByteArray()) == original)
+ check(ProtoAny.pack(original).unpack(Attitude::class.java) == original)
+ check(Attitude.getDescriptor().fullName == "blueye.protocol.Attitude")
+ println("Published Kotlin package: DSL, full runtime, and transitive dependencies OK")
+}
diff --git a/kotlin/src/test/kotlin/blueye/protocol/ProtocolTest.kt b/kotlin/src/test/kotlin/blueye/protocol/ProtocolTest.kt
new file mode 100644
index 00000000..ae798889
--- /dev/null
+++ b/kotlin/src/test/kotlin/blueye/protocol/ProtocolTest.kt
@@ -0,0 +1,90 @@
+package blueye.protocol
+
+import com.google.protobuf.Any as ProtoAny
+import com.google.protobuf.DynamicMessage
+import com.google.protobuf.InvalidProtocolBufferException
+import com.google.protobuf.TypeRegistry
+import com.google.protobuf.timestamp
+import com.google.protobuf.util.JsonFormat
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+class ProtocolTest {
+ @Test
+ fun attitudeMatchesWireFixture() {
+ val original = attitude {
+ roll = 1f
+ pitch = 2f
+ yaw = 3f
+ }
+ // The same fixed32 wire fixture is checked by the Swift client tests.
+ val expected = byteArrayOf(
+ 0x0d, 0x00, 0x00, 0x80.toByte(), 0x3f,
+ 0x15, 0x00, 0x00, 0x00, 0x40,
+ 0x1d, 0x00, 0x00, 0x40, 0x40,
+ )
+ assertContentEquals(expected, original.toByteArray())
+ assertEquals(original, Attitude.parseFrom(expected))
+ }
+
+ @Test
+ fun anyAndTimestampRoundTrip() {
+ val original = attitude { yaw = 90f }
+ val record = binlogRecord {
+ payload = ProtoAny.pack(original)
+ unixTimestamp = timestamp {
+ seconds = 1_700_000_000
+ nanos = 123_456_789
+ }
+ }
+ val decoded = BinlogRecord.parseFrom(record.toByteArray())
+ assertEquals(record, decoded)
+ assertEquals("type.googleapis.com/blueye.protocol.Attitude", decoded.payload.typeUrl)
+ assertEquals(original, decoded.payload.unpack(Attitude::class.java))
+ }
+
+ @Test
+ fun fullRuntimeSupportsReflectionAndAnyJson() {
+ val original = attitude { roll = 1.5f }
+ val descriptor = Attitude.getDescriptor()
+ assertEquals("blueye.protocol.Attitude", descriptor.fullName)
+ val dynamic = DynamicMessage.parseFrom(descriptor, original.toByteArray())
+ assertEquals(1.5f, dynamic.getField(descriptor.findFieldByName("roll")))
+
+ val registry = TypeRegistry.newBuilder().add(descriptor).build()
+ val packed = ProtoAny.pack(original)
+ val json = JsonFormat.printer().usingTypeRegistry(registry).print(packed)
+ val decoded = ProtoAny.newBuilder()
+ JsonFormat.parser().usingTypeRegistry(registry).merge(json, decoded)
+ assertEquals(packed, decoded.build())
+ }
+
+ @Test
+ fun missionOneofRoundTrip() {
+ val original = mission {
+ id = 42
+ name = "Inspection"
+ instructions += instruction {
+ id = 1
+ depthSetPointCommand = depthSetPointCommand {
+ depthSetPoint = depthSetPoint { depth = 10f }
+ }
+ }
+ }
+ val decoded = Mission.parseFrom(original.toByteArray())
+ assertEquals(original, decoded)
+ assertTrue(decoded.getInstructions(0).hasDepthSetPointCommand())
+ }
+
+ @Test
+ fun unknownFieldsSurviveRoundTrip() {
+ val bytes = byteArrayOf(0xa0.toByte(), 0x06, 0x07) // Field 100, varint 7.
+ assertContentEquals(bytes, Attitude.parseFrom(bytes).toByteArray())
+ assertFailsWith {
+ Attitude.parseFrom(byteArrayOf(0x0d))
+ }
+ }
+}
diff --git a/protobuf.version b/protobuf.version
new file mode 100644
index 00000000..b82d77ec
--- /dev/null
+++ b/protobuf.version
@@ -0,0 +1 @@
+36.2
diff --git a/protobuf_definitions/aquatroll.proto b/protobuf_definitions/aquatroll.proto
index a01e11f9..12bf7d3c 100644
--- a/protobuf_definitions/aquatroll.proto
+++ b/protobuf_definitions/aquatroll.proto
@@ -6,6 +6,7 @@ syntax = "proto3";
package blueye.protocol;
import "google/protobuf/timestamp.proto";
option csharp_namespace = "Blueye.Protocol.Protobuf";
+option java_multiple_files = true;
// Type IDs
enum Type {
diff --git a/protobuf_definitions/control.proto b/protobuf_definitions/control.proto
index 09528486..065ff262 100644
--- a/protobuf_definitions/control.proto
+++ b/protobuf_definitions/control.proto
@@ -7,6 +7,7 @@ package blueye.protocol;
import "aquatroll.proto";
import "message_formats.proto";
option csharp_namespace = "Blueye.Protocol.Protobuf";
+option java_multiple_files = true;
// Issue a command to move the drone in the surge, sway, heave, or yaw direction.
message MotionInputCtrl {
diff --git a/protobuf_definitions/message_formats.proto b/protobuf_definitions/message_formats.proto
index 05c06df6..f0a5cc82 100644
--- a/protobuf_definitions/message_formats.proto
+++ b/protobuf_definitions/message_formats.proto
@@ -8,6 +8,7 @@ import "google/protobuf/any.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "Blueye.Protocol.Protobuf";
+option java_multiple_files = true;
// Wrapper message for each entry in the drone telemetry logfile.
//
diff --git a/protobuf_definitions/mission_planning.proto b/protobuf_definitions/mission_planning.proto
index ba5a940e..2a6dc8a1 100644
--- a/protobuf_definitions/mission_planning.proto
+++ b/protobuf_definitions/mission_planning.proto
@@ -6,6 +6,7 @@ syntax = "proto3";
package blueye.protocol;
import "message_formats.proto";
option csharp_namespace = "Blueye.Protocol.Protobuf";
+option java_multiple_files = true;
// A list of waypoints describes a mission that the auto pilot can execute.
message Mission {
diff --git a/protobuf_definitions/req_rep.proto b/protobuf_definitions/req_rep.proto
index 796735eb..8e563151 100644
--- a/protobuf_definitions/req_rep.proto
+++ b/protobuf_definitions/req_rep.proto
@@ -9,6 +9,7 @@ import "message_formats.proto";
import "mission_planning.proto";
option csharp_namespace = "Blueye.Protocol.Protobuf";
+option java_multiple_files = true;
// Request to set video overlay parameters.
message SetOverlayParametersReq {
diff --git a/protobuf_definitions/telemetry.proto b/protobuf_definitions/telemetry.proto
index f693a6db..73fee511 100644
--- a/protobuf_definitions/telemetry.proto
+++ b/protobuf_definitions/telemetry.proto
@@ -8,6 +8,7 @@ import "aquatroll.proto";
import "message_formats.proto";
import "mission_planning.proto";
option csharp_namespace = "Blueye.Protocol.Protobuf";
+option java_multiple_files = true;
// Receive the current attitude of the drone.
message AttitudeTel {
diff --git a/scripts/package-version.py b/scripts/package-version.py
new file mode 100644
index 00000000..dc8b597b
--- /dev/null
+++ b/scripts/package-version.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+"""Calculate one version for NuGet, Maven, and the downstream Swift release."""
+
+import os
+from pathlib import Path
+import re
+
+
+def package_version(base: str, env: dict[str, str]) -> str:
+ if not re.fullmatch(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)", base):
+ raise ValueError("version.txt must contain a major.minor version, for example 6.5")
+
+ run = env.get("GITHUB_RUN_NUMBER")
+ if not run:
+ return f"{base}.0-local"
+ if not re.fullmatch(r"[1-9][0-9]*", run):
+ raise ValueError("GITHUB_RUN_NUMBER must be a positive integer")
+
+ version = f"{base}.{run}"
+ if env.get("GITHUB_REF") == "refs/heads/master" and not env.get("GITHUB_HEAD_REF"):
+ return version
+
+ ref = env.get("GITHUB_HEAD_REF") or env.get("GITHUB_REF_NAME", "dev")
+ branch = re.sub(r"[^a-z0-9]+", "-", ref.lower()).strip("-")[:63].rstrip("-") or "dev"
+ # A fixed prefix also makes numeric branch names valid SemVer prereleases.
+ return f"{version}-branch-{branch}"
+
+
+if __name__ == "__main__":
+ version_file = Path(__file__).resolve().parents[1] / "version.txt"
+ try:
+ print(package_version(version_file.read_text().strip(), dict(os.environ)))
+ except ValueError as error:
+ raise SystemExit(str(error))
diff --git a/scripts/test_package_version.py b/scripts/test_package_version.py
new file mode 100644
index 00000000..4b8bded4
--- /dev/null
+++ b/scripts/test_package_version.py
@@ -0,0 +1,49 @@
+import importlib.util
+from pathlib import Path
+import unittest
+
+
+spec = importlib.util.spec_from_file_location("package_version", Path(__file__).with_name("package-version.py"))
+module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(module)
+package_version = module.package_version
+
+
+class PackageVersionTests(unittest.TestCase):
+ def test_master_uses_the_existing_workflow_run_counter(self):
+ env = {"GITHUB_REF": "refs/heads/master", "GITHUB_RUN_NUMBER": "815"}
+ self.assertEqual(package_version("6.5", env), "6.5.815")
+ self.assertEqual(package_version("6.6", env), "6.6.815")
+
+ def test_each_master_run_gets_a_new_stable_version(self):
+ for run in (815, 816):
+ env = {"GITHUB_REF": "refs/heads/master", "GITHUB_RUN_NUMBER": str(run)}
+ self.assertEqual(package_version("6.5", env), f"6.5.{run}")
+
+ def test_pull_request_from_a_fork_master_is_a_prerelease(self):
+ env = {"GITHUB_REF": "refs/pull/12/merge", "GITHUB_HEAD_REF": "master", "GITHUB_RUN_NUMBER": "817"}
+ self.assertEqual(package_version("6.5", env), "6.5.817-branch-master")
+
+ def test_numeric_and_punctuation_branch_names_are_valid_prereleases(self):
+ for branch, expected in [("001", "001"), ("Feature/New Sensor", "feature-new-sensor"), ("///", "dev")]:
+ env = {"GITHUB_REF_NAME": branch, "GITHUB_RUN_NUMBER": "818"}
+ self.assertEqual(package_version("6.5", env), f"6.5.818-branch-{expected}")
+
+ def test_reruns_keep_the_same_version(self):
+ env = {"GITHUB_REF": "refs/heads/master", "GITHUB_RUN_NUMBER": "815", "GITHUB_RUN_ATTEMPT": "2"}
+ self.assertEqual(package_version("6.5", env), "6.5.815")
+
+ def test_local_builds_cannot_be_mistaken_for_releases(self):
+ self.assertEqual(package_version("6.5", {}), "6.5.0-local")
+
+ def test_invalid_base_and_counter_are_rejected(self):
+ for base in ("6.5.0", "v6.5", "06.5", "6.5-rc", "6"):
+ with self.subTest(base=base), self.assertRaises(ValueError):
+ package_version(base, {})
+ for run in ("0", "0815", "-1", "815.2"):
+ with self.subTest(run=run), self.assertRaises(ValueError):
+ package_version("6.5", {"GITHUB_RUN_NUMBER": run})
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/version.txt b/version.txt
new file mode 100644
index 00000000..7822c1ab
--- /dev/null
+++ b/version.txt
@@ -0,0 +1 @@
+6.5