diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..838aca5 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,98 @@ +name: MAVSDK-Java +on: + push: + branches: + - 'main' + pull_request: + branches: + - '**' + release: + types: [created] +jobs: + build: + if: github.event_name != 'release' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build and prepare mavsdk + working-directory: ./sdk + run: | + set -o pipefail + python3 -m venv venv + source ./venv/bin/activate + pip install protoc-gen-mavsdk + ./gradlew build + - name: Build and prepare mavsdk-server + working-directory: ./mavsdk_server + run: ./gradlew build + + release: + if: github.event_name == 'release' && github.event.action == 'created' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Validate version + id: version + run: | + set -o pipefail + tag_name="${{ github.ref_name }}" + python ./tools/version_validator.py "$tag_name" + echo "tag_name=$tag_name" >> $GITHUB_OUTPUT + - name: Prepare tokens keystore + run: | + echo "${{ secrets.TOKENS_KEYSTORE }}" > /tmp/keystore.properties.b64 + base64 -d -i /tmp/keystore.properties.b64 > /tmp/keystore.properties + cp /tmp/keystore.properties sdk + cp /tmp/keystore.properties mavsdk_server + - name: Prepare GPG key + run: echo "${{ secrets.SIGNING_PGP_KEY }}" | gpg --batch --import + - name: Extract version and sync proto submodule + run: | + set -o pipefail + tag_name="${{ steps.version.outputs.tag_name }}" + + # Extract version number + extracted_version=$(python ./tools/version_validator.py -e "$tag_name") + + # Clone MAVSDK repo with minimal depth to get proto submodule hash + git clone --depth 1 --branch "${extracted_version}" https://github.com/mavlink/mavsdk.git /tmp/mavsdk + + # Get the proto submodule commit hash from the MAVSDK repo + cd /tmp/mavsdk + proto_commit_hash=$(git ls-tree HEAD proto | awk '{print $3}') + + # Update our local proto submodule to match + cd $GITHUB_WORKSPACE/sdk/proto + git fetch origin + git checkout $proto_commit_hash + + # Cleanup + rm -rf /tmp/mavsdk + + echo "Updated proto submodule to commit: $proto_commit_hash for MAVSDK version: $extracted_version" + - name: Build and prepare mavsdk + working-directory: ./sdk + run: | + set -o pipefail + python3 -m venv venv + source ./venv/bin/activate + pip install protoc-gen-mavsdk + ./gradlew build -PVERSION=${{ steps.version.outputs.tag_name }} + ./gradlew publish -PVERSION=${{ steps.version.outputs.tag_name }} + - name: Build and prepare mavsdk-server + working-directory: ./mavsdk_server + run: | + set -o pipefail + ./gradlew build -PVERSION=${{ steps.version.outputs.tag_name }} + ./gradlew publish -PVERSION=${{ steps.version.outputs.tag_name }} + - name: Deploy mavsdk + working-directory: ./sdk + run: ./gradlew jreleaserDeploy -PVERSION=${{ steps.version.outputs.tag_name }} + - name: Deploy mavsdk-server + working-directory: ./mavsdk_server + run: ./gradlew jreleaserDeploy -PVERSION=${{ steps.version.outputs.tag_name }} + diff --git a/.gitignore b/.gitignore index 536d876..38197f9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build local.properties gradle.properties +keystore.properties .cxx venv diff --git a/README.md b/README.md index d188a30..2c5fe00 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ MAVSDK-Java is distributed through MavenCentral, meaning that it can be imported ``` dependencies { ... - implementation 'io.mavsdk:mavsdk:3.0.0' + implementation 'io.mavsdk:mavsdk:' ... } ``` @@ -29,12 +29,32 @@ For Android, `mavsdk_server` is distributed as an Android library (`aar`): ``` dependencies { ... - implementation 'io.mavsdk:mavsdk:3.0.0' - implementation 'io.mavsdk:mavsdk-server:3.0.0' + implementation 'io.mavsdk:mavsdk:' + implementation 'io.mavsdk:mavsdk-server:' ... } ``` +### SNAPSHOTs + +It is possible to fetch SNAPSHOTs by adding the repository to your Gradle config, as per the [sonatype documentation](https://central.sonatype.org/publish/publish-portal-snapshots/#consuming-via-gradle): + +``` +repositories { + maven { + name = 'Central Portal Snapshots' + url = 'https://central.sonatype.com/repository/maven-snapshots/' + + // Only search this repository for the specific dependency + content { + includeModule("io.mavsdk", "mavsdk") + includeModule("io.mavsdk", "mavsdk-server") + } + } + mavenCentral() +} +``` + ### ProGuard ProGuard users may need to add the following rule: @@ -50,7 +70,7 @@ ProGuard users may need to add the following rule: 2. For Android, run the `mavsdk_server` as follows: ```java -MavsdkServer server = new MavsdkServer(); +MavsdkServer server = new MavsdkServer(context); MavsdkEventQueue.executor().execute(() -> server.run(SYSTEM_ADDRESS, MAVSDK_SERVER_PORT)); ``` @@ -71,62 +91,19 @@ The plugins are constructed and initialized lazily upon their first call through 5. One-shot calls like `takeoff` and `land` are not added to the `mavsdk-event-queue` when the user subscribes to them. This is done to avoid their piling up while the `mavsdk_server` discovers a system. Instead, the `onError` callback will be triggered after a 100ms delay indicating that no system was available for the command. -## Contributing - -### Coding style - -Java/Android coding style is ensured using CheckStyle with the Google style. - -#### Command line - -A `checkstyle` task is defined in the root `build.gradle` of each project and can be run as follows: - - $ ./gradlew checkstyle - -The `build` task depends on `checkstyle`, meaning that `$ ./gradlew build` runs the checks as well. - -#### IntelliJ / Android-Studio - -There exist a plugin for CheckStyle in JetBrains' IDEs. - -##### Setup - -1. Install the plugin called "CheckStyle-IDEA" in IntelliJ / Android-Studio. -2. Import the checkstyle configuration as a code style scheme in _Settings > Editor > Code Style > Java > Manage... > - Import..._ by selecting "CheckStyle configuration" and then browsing to `config/checkstyle/checkstyle.xml`. -3. In _Settings > Other Settings > Checkstyle_, change the "Scan Scope" to "Only Java sources (including tests)". -4. Still in _Settings > Other Settings > Checkstyle_, add a new configuration file and browse to - `config/checkstyle/checkstyle.xml`. - -##### Usage - -In IntelliJ / Android-Studio's bottom task bar, you should see a "CheckStyle" tab. It will allow you to select your configuration -with the "Rules" dropdown-list, and to run the analysis on your code. - -Note that by default, the IDE will not run checkstyle when building the project (whereas `$ ./gradlew build` always does it). - -#### Troubleshooting - -In IntelliJ / Android-Studio, the IDE might force the order of the imports in a way that is not following the checkstyle rules. For some reason, this is not set when importing `checkstyle.xml` as a code style scheme. However, it can be manually updated in _Settings > Code Style > Java > Import Layout_. - ### Releasing -Both [sdk](./sdk) and [mavsdk_server](./mavsdk_server) are released with Maven. Publishing can be done through a gradle task: - -```gradle -./gradlew uploadArchives -``` - -This task requires a few secrets in `gradle.properties`: - -``` -signing.keyId= -signing.password= -signing.secretKeyRingFile= - -ossrhUsername= -ossrhPassword= -``` +When a GitHub release is created, the CI runs the release pipeline, which: + +1. Extracts the package version from the release tag. The release tag is expected + to be in the form "X.Y.Z-b[-SNAPSHOT]", where "X.Y.Z" correspond to the + matching version of MAVSDK-C++ and "b" is for build numbers of MAVSDK-Java. + For instance, 3.6.0, 3.6.0-2, 3.6.0-SNAPSHOT or 3.6.0-2-SNAPSHOT. +1. Updates the ./sdk/proto submodule to point to the same commit as the + corresponding proto submodule in MAVSDK-C++ (for version X.Y.Z of + MAVSDK-C++). +1. Build mavsdk and mavsdk_server and deploy them, either as SNAPSHOTs or as + definitive releases. ### Debugging without pushing to maven diff --git a/mavsdk_server/build.gradle b/mavsdk_server/build.gradle deleted file mode 100644 index 3c8437d..0000000 --- a/mavsdk_server/build.gradle +++ /dev/null @@ -1,155 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.10.0' - } -} - -allprojects { - apply plugin: 'com.android.library' - apply plugin: 'maven-publish' - apply plugin: 'signing' - - repositories { - google() - mavenCentral() - } - - def mavsdk_server_release = "v3.5.0" - - tasks.register('extractMavsdkServer', Copy) { - mkdir project.buildDir.getAbsolutePath() + "/tmp" - - def extractForArch = { arch -> - def archiveName = "mavsdk_server_android-" + arch + "_" + mavsdk_server_release + ".tar" - def archiveFile = new File(project.buildDir.getAbsolutePath() + "/tmp/" + archiveName) - def archiveUrl = "https://github.com/mavlink/MAVSDK/releases/download/" + mavsdk_server_release + "/mavsdk_server_android-" + arch + ".tar" - def destDir = project.projectDir.getAbsolutePath() + "/src/main/prebuiltLibs" - - if (!archiveFile.exists()) { - project.logger.lifecycle("Downloading " + archiveFile.getName() + " into " + archiveFile.getAbsolutePath() + "...") - new URL(archiveUrl).withInputStream { i -> archiveFile.withOutputStream { it << i } } - } else { - project.logger.lifecycle("Archive already exists! Skipping download!") - } - - project.logger.lifecycle("Extracting into " + destDir) - copy { - from tarTree(archiveFile) - eachFile({ file -> if (file.getName().endsWith(".so")) file.setMode(0755) }) - into(destDir) - } - } - - extractForArch("arm64") - extractForArch("arm") - extractForArch("x86") - extractForArch("x86_64") - } - build.dependsOn extractMavsdkServer - - if (project.hasProperty('ossrhUsername') - && project.hasProperty('ossrhPassword')) { - afterEvaluate { - publishing { - publications { - release(MavenPublication) { - from components.release - - pom { - name = 'MAVSDK-Server' - packaging = 'aar' - description = 'MAVSDK server for Android.' - url = 'https://github.com/mavlink/MAVSDK-Java' - - scm { - connection = 'scm:git:https://github.com/mavlink/MAVSDK-Java' - developerConnection = 'scm:git:https://github.com/mavlink/MAVSDK-Java' - url = 'https://github.com/mavlink/MAVSDK-Java' - } - - licenses { - license { - name = 'BSD 3-Clause "New"' - url = 'https://opensource.org/licenses/BSD-3-Clause' - } - } - - developers { - developer { - id = 'jonasvautherin' - name = 'Jonas Vautherin' - email = 'dev@jonas.vautherin.ch' - } - } - } - } - } - repositories { - maven { - url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - - credentials { - username = ossrhUsername - password = ossrhPassword - } - } - } - } - - signing { - useGpgCmd() - sign publishing.publications.release - } - } - } -} - -android { - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - defaultConfig { - minSdkVersion 21 - targetSdkVersion 35 - compileSdk 35 - - archivesBaseName = 'mavsdk-server' - group = 'io.mavsdk' - versionCode 3 - version '3.0.0' - - ndk { - abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64' - } - - externalNativeBuild { - cmake { - arguments "-DANDROID_STL=c++_shared" - } - } - } - - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - } - } - - publishing { - singleVariant('release') { - withSourcesJar() - withJavadocJar() - } - } - - ndkVersion "25.1.8937393" - namespace 'io.mavsdk.mavsdkserver' -} diff --git a/mavsdk_server/build.gradle.kts b/mavsdk_server/build.gradle.kts new file mode 100644 index 0000000..ca824b2 --- /dev/null +++ b/mavsdk_server/build.gradle.kts @@ -0,0 +1,239 @@ +import java.util.Properties +import java.io.FileInputStream +import java.io.IOException +import java.net.URI + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.jreleaser) + `maven-publish` +} + +// Load file "keystore.properties" where we keep our keys +val keystorePropertiesFile = rootProject.file("keystore.properties") +val keystoreProperties = Properties() + +try { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} catch (ignored: IOException) { + if (project.hasProperty("centralUsername")) keystoreProperties["centralUsername"] = property("centralUsername") + if (project.hasProperty("centralPassword")) keystoreProperties["centralPassword"] = property("centralPassword") + if (project.hasProperty("gpgPass")) keystoreProperties["gpgPass"] = property("gpgPass") +} + +allprojects { + // We fetch the mavsdk_server binary that corresponds to the mavsdk-java + // version. Say we set this package to be 3.6.0-2-SNAPSHOT, it means that it + // corresponds to mavsdk_server 3.6.0. + val mavsdk_server_release = if (!project.hasProperty("VERSION")) { + "v3.10.0" + } else { + val versionString = project.property("VERSION").toString() + val regex = Regex("v?(\\d+\\.\\d+\\.\\d+)") + val version = regex.find(versionString)?.groupValues?.get(1) + "v$version" + } + + tasks { + register("extractMavsdkServer") { + val tmpDir = File(layout.buildDirectory.get().asFile, "tmp") + tmpDir.mkdirs() + + fun extractForArch(arch: String) { + val archiveName = "mavsdk_server_android-${arch}_${mavsdk_server_release}.tar" + val archiveFile = File(tmpDir, archiveName) + val archiveUrl = "https://github.com/mavlink/MAVSDK/releases/download/${mavsdk_server_release}/mavsdk_server_android-$arch.tar" + val destDir = "${project.projectDir.getAbsolutePath()}/src/main/prebuiltLibs" + + inputs.file(archiveFile) + outputs.dir(destDir) + + if (!archiveFile.exists()) { + project.logger.warn("Downloading ${archiveFile.getName()} into ${archiveFile.getAbsolutePath()}...") + URI(archiveUrl).toURL().openStream().use { input -> + archiveFile.outputStream().use { output -> + input.copyTo(output) + } + } + } else { + project.logger.warn("Archive already exists! Skipping download.") + } + + project.logger.warn("Extracting $archiveFile into $destDir") + + from(tarTree(archiveFile)) { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + eachFile { + if (path.endsWith(".so")) { + filePermissions { + group.execute = true + user.execute = true + other.execute = true + } + } + } + into(destDir) + project.logger.warn("Should be extracted for arch $arch") + } + + extractForArch("arm64") + extractForArch("arm") + extractForArch("x86") + extractForArch("x86_64") + } + } + + tasks.named("preBuild") { + dependsOn("extractMavsdkServer") + } +} + +android { + namespace = "io.mavsdk.mavsdkserver" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + group = "io.mavsdk" + + // The version must be of the form "X.Y.Z-b[-SNAPSHOT]", where "X.Y.Z" + // is the MAVSDK-C++ version, "b" is the build number of this + // MAVSDK-Java package and "SNAPSHOT" optionally sets it as a SNAPSHOT. + version = + if (project.hasProperty("VERSION")) project.property("VERSION").toString() + else "3.10.0-SNAPSHOT" + + ndk { + abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + } + } + + externalNativeBuild { + cmake { + path = File("src/main/cpp/CMakeLists.txt") + } + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + ndkVersion = "28.1.13356709" +} + + +if (keystoreProperties.containsKey("centralUsername") && keystoreProperties.containsKey("centralPassword")) { + afterEvaluate { + publishing { + publications { + create("release") { + from(components["release"]) + + pom { + name = "MAVSDK-Server" + packaging = "aar" + description = "MAVSDK server for Android." + url = "https://github.com/mavlink/MAVSDK-Java" + + scm { + connection = "scm:git:https://github.com/mavlink/MAVSDK-Java" + developerConnection = "scm:git:https://github.com/mavlink/MAVSDK-Java" + url = "https://github.com/mavlink/MAVSDK-Java" + } + + licenses { + license { + name = "BSD 3-Clause" + url = "https://opensource.org/licenses/BSD-3-Clause" + } + } + + developers { + developer { + id = "jonasvautherin" + name = "Jonas Vautherin" + email = "dev@jonas.vautherin.ch" + } + developer { + id = "julianoes" + name = "Julian Oes" + email = "julian@oes.ch" + } + } + } + } + } + + repositories { + maven { + url = uri(layout.buildDirectory.dir("target/staging-deploy")) + } + } + } + } + + jreleaser { + signing { + setActive("ALWAYS") + armored.set(true) + setMode("COMMAND") + keystoreProperties["gpgPass"]?.let { + passphrase.set(it as String) + } + + command { + keyName.set("CF3FF35732A465F680A89BC25B01A8023597C84B") + } + } + deploy { + release { + github { + skipRelease = true + skipTag = true + } + } + maven { + mavenCentral { + create("sonatype") { + verifyPom = false + setActive("RELEASE") + username = keystoreProperties["centralUsername"] as String + password = keystoreProperties["centralPassword"] as String + url = "https://central.sonatype.com/api/v1/publisher" + stagingRepository("build/target/staging-deploy") + } + } + nexus2 { + create("snapshot-deploy") { + verifyPom = false + setActive("SNAPSHOT") + snapshotUrl.set("https://central.sonatype.com/repository/maven-snapshots") + url = "https://central.sonatype.com/repository/maven-snapshots" + applyMavenCentralRules = true + snapshotSupported = true + username = keystoreProperties["centralUsername"] as String + password = keystoreProperties["centralPassword"] as String + stagingRepository("build/target/staging-deploy") + } + } + } + } + } +} + diff --git a/mavsdk_server/gradle/libs.versions.toml b/mavsdk_server/gradle/libs.versions.toml new file mode 100644 index 0000000..e7e110c --- /dev/null +++ b/mavsdk_server/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +android-gradle-plugin = "8.6.1" +jreleaser-plugin = "1.19.0" + +[libraries] + +[plugins] +android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } +jreleaser = { id = "org.jreleaser", version.ref = "jreleaser-plugin" } diff --git a/mavsdk_server/settings.gradle b/mavsdk_server/settings.gradle deleted file mode 100644 index 004a467..0000000 --- a/mavsdk_server/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'mavsdk-server' diff --git a/mavsdk_server/settings.gradle.kts b/mavsdk_server/settings.gradle.kts new file mode 100644 index 0000000..a2d2957 --- /dev/null +++ b/mavsdk_server/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "mavsdk-server" + diff --git a/mavsdk_server/src/main/cpp/native-lib.cpp b/mavsdk_server/src/main/cpp/native-lib.cpp index f5e89d0..de81273 100644 --- a/mavsdk_server/src/main/cpp/native-lib.cpp +++ b/mavsdk_server/src/main/cpp/native-lib.cpp @@ -6,6 +6,8 @@ #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,"MAVSDK-Server",__VA_ARGS__) +extern "C" char* mavsdk_temp_path; + extern "C" { JNIEXPORT jlong JNICALL @@ -18,7 +20,7 @@ extern "C" JNIEXPORT jboolean JNICALL Java_io_mavsdk_mavsdkserver_MavsdkServer_runNative(JNIEnv* env, jobject thiz, jlong mavsdkServerHandle, jstring system_address, jint mavsdk_server_port) { - const char* native_connection_url = env->GetStringUTFChars(system_address, 0); + const char* native_connection_url = env->GetStringUTFChars(system_address, nullptr); auto mavsdk_server = reinterpret_cast(mavsdkServerHandle); LOGD("Running mavsdk_server with connection url: %s", native_connection_url); @@ -36,14 +38,15 @@ extern "C" JNIEXPORT jboolean JNICALL Java_io_mavsdk_mavsdkserver_MavsdkServer_runNativeWithMavIds(JNIEnv* env, jobject thiz, jlong mavsdkServerHandle, jstring system_address, jint mavsdk_server_port, jint system_id, jint component_id) { - const char* native_connection_url = env->GetStringUTFChars(system_address, 0); + const char* native_connection_url = env->GetStringUTFChars(system_address, nullptr); auto mavsdk_server = reinterpret_cast(mavsdkServerHandle); LOGD("Running mavsdk_server with connection url: %s", native_connection_url); - if (!mavsdk_server_run_with_mavlink_ids(mavsdk_server, native_connection_url, mavsdk_server_port, system_id, component_id)) { + auto result = mavsdk_server_run_with_mavlink_ids(mavsdk_server, native_connection_url, mavsdk_server_port, system_id, component_id); + if (result != 0) { + LOGD("Failed to start mavsdk_server: %d", result); return false; } - auto server_port = mavsdk_server_get_port(mavsdk_server); LOGD("mavsdk_server is now running, listening on port %d", server_port); return true; @@ -76,4 +79,16 @@ extern "C" auto mavsdk_server = reinterpret_cast(mavsdkServerHandle); mavsdk_server_destroy(mavsdk_server); } + + JNIEXPORT void JNICALL + Java_io_mavsdk_mavsdkserver_MavsdkServer_setTempDirectory(JNIEnv *env, jobject thiz, jstring temp_dir) + { + const char* temp_c_str = env->GetStringUTFChars(temp_dir, 0); + + static char our_copy[256]; + strncpy(our_copy, temp_c_str, sizeof(our_copy)); + mavsdk_temp_path = our_copy; + + env->ReleaseStringUTFChars(temp_dir, temp_c_str); + } }; diff --git a/mavsdk_server/src/main/java/io/mavsdk/mavsdkserver/MavsdkServer.java b/mavsdk_server/src/main/java/io/mavsdk/mavsdkserver/MavsdkServer.java index bc74f33..9a391f0 100644 --- a/mavsdk_server/src/main/java/io/mavsdk/mavsdkserver/MavsdkServer.java +++ b/mavsdk_server/src/main/java/io/mavsdk/mavsdkserver/MavsdkServer.java @@ -1,15 +1,26 @@ package io.mavsdk.mavsdkserver; +import android.content.Context; + public class MavsdkServer { static { java.lang.System.loadLibrary("native_lib"); } + public MavsdkServer(Context context) { + String cacheDir = context.getCacheDir().getAbsolutePath(); + setTempDirectory(cacheDir); + } + + public MavsdkServer(String tempDirectory) { + setTempDirectory(tempDirectory); + } + private long mavsdkServerHandle; /** - * Run MavsdkServer with MAVLink defaulting to udp://:14540. + * Run MavsdkServer with MAVLink defaulting to udpin://:14540. * *

MavsdkServer will start a gRPC server listening on an * arbitrary port, to which a `System` should connect.

@@ -18,7 +29,7 @@ public class MavsdkServer { * A return value of 0 means that the server failed to start. */ public int run() { - return run("udp://:14540"); + return run("udpin://:14540"); } /** @@ -29,9 +40,12 @@ public int run() { * * @param systemAddress The address on which the remote MAVLink system is expected. * Valid formats are: - * For TCP : tcp://[server_host][:server_port]. - * For UDP : udp://[bind_host][:bind_port]. - * For Serial : serial:///path/to/serial/dev[:baudrate]. + * UDP in (server): udpin://our_ip:port + * UDP out (client): udpout://remote_ip:port + * TCP in (server): tcpin://our_ip:port + * TCP out (client): tcpout://remote_ip:port + * Serial: serial://dev_node:baudrate + * Serial with flow control: serial_flowcontrol://dev_node:baudrate * @return The port on which MavsdkServer listens for a `System` to connect. * A return value of 0 means that the server failed to start. */ @@ -46,9 +60,12 @@ public int run(String systemAddress) { * * @param systemAddress The address on which the remote MAVLink system is expected. * Valid formats are: - * For TCP : tcp://[server_host][:server_port] - * For UDP : udp://[bind_host][:bind_port] - * For Serial : serial:///path/to/serial/dev[:baudrate] + * UDP in (server): udpin://our_ip:port + * UDP out (client): udpout://remote_ip:port + * TCP in (server): tcpin://our_ip:port + * TCP out (client): tcpout://remote_ip:port + * Serial: serial://dev_node:baudrate + * Serial with flow control: serial_flowcontrol://dev_node:baudrate * @param mavsdkServerPort The port on which the server should listen for a `System`. * @return The port on which MavsdkServer listens for a `System` to connect. * A return value of 0 means that the server failed to start. @@ -69,10 +86,12 @@ public int run(String systemAddress, int mavsdkServerPort) { *

MavsdkServer will listen for a `System` to connect on `mavsdkServerPort`.

* * @param systemAddress The address on which the remote MAVLink system is expected. - * Valid formats are: - * For TCP : tcp://[server_host][:server_port] - * For UDP : udp://[bind_host][:bind_port] - * For Serial : serial:///path/to/serial/dev[:baudrate] + * UDP in (server): udpin://our_ip:port + * UDP out (client): udpout://remote_ip:port + * TCP in (server): tcpin://our_ip:port + * TCP out (client): tcpout://remote_ip:port + * Serial: serial://dev_node:baudrate + * Serial with flow control: serial_flowcontrol://dev_node:baudrate * @param mavsdkServerPort The port on which the server should listen for a `System`. * @param systemId The MAVLink sysid that MAVSDK should use. * @param componentId The MAVLink compid that MAVSDK should use. @@ -117,4 +136,6 @@ public void destroy() { } private native void destroy(long mavsdkServerHandle); + + public native void setTempDirectory(String dir); } diff --git a/sdk/README.md b/sdk/README.md index 5d33b57..9523847 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -8,7 +8,7 @@ Because it is deployed on Maven Central, adding MAVSDK-Java to your project is a ```groovy dependencies { - implementation 'io.mavsdk:mavsdk:2.1.0' + implementation 'io.mavsdk:mavsdk:' } ``` diff --git a/sdk/build.gradle b/sdk/build.gradle deleted file mode 100644 index 5411a6c..0000000 --- a/sdk/build.gradle +++ /dev/null @@ -1,179 +0,0 @@ -buildscript { - repositories { - jcenter() - mavenCentral() - } - - dependencies { - classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.4" - } -} - -subprojects { - apply plugin: 'com.google.protobuf' - apply plugin: 'idea' - apply plugin: 'java-library' - apply plugin: 'maven-publish' - apply plugin: 'signing' - - group = 'io.mavsdk' - archivesBaseName = 'mavsdk' - version = '3.0.0' - - repositories { - jcenter() - mavenCentral() - } - - configurations { - checkstyleClasspath - } - - dependencies { - checkstyleClasspath 'com.puppycrawl.tools:checkstyle:8.17' - } - - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - - def grpcVersion = '1.61.1' - - protobuf { - protoc { - artifact = 'com.google.protobuf:protoc:3.25.2' - } - - plugins { - grpc { - artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" - } - } - - generateProtoTasks { - all()*.each { task -> - task.builtins { - java { - option "lite" - } - } - - task.plugins { - grpc { - option 'lite' - } - - mavsdk { - option 'file_ext=java' - option "template_path=$project.rootDir/templates/" - } - } - } - } - } - - sourceSets { - main { - proto { - srcDir "$project.rootDir/proto/protos/" - } - - java { - srcDir 'build/generated/source/proto/main/javalite' - srcDir 'build/generated/source/proto/main/grpc' - srcDir 'build/generated/source/proto/main/mavsdk' - } - } - } - - tasks.withType(Jar).all { - duplicatesStrategy = DuplicatesStrategy.INCLUDE - } - - java { - withJavadocJar() - withSourcesJar() - } - - if (project.hasProperty('ossrhUsername') - && project.hasProperty('ossrhPassword')) { - - afterEvaluate { - publishing { - publications { - mavenJava(MavenPublication) { - from components.java - - pom { - name = 'MAVSDK-Java' - packaging = 'jar' - description = 'MAVSDK client for Java.' - url = 'https://github.com/mavlink/MAVSDK-Java' - - scm { - connection = 'scm:git:https://github.com/mavlink/MAVSDK-Java' - developerConnection = 'scm:git:https://github.com/mavlink/MAVSDK-Java' - url = 'https://github.com/mavlink/MAVSDK-Java' - } - - licenses { - license { - name = 'BSD 3-Clause "New"' - url = 'https://opensource.org/licenses/BSD-3-Clause' - } - } - - developers { - developer { - id = 'jonasvautherin' - name = 'Jonas Vautherin' - email = 'dev@jonas.vautherin.ch' - } - } - } - } - } - repositories { - maven { - url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - - credentials { - username = ossrhUsername - password = ossrhPassword - } - } - } - } - - signing { - useGpgCmd() - sign publishing.publications.mavenJava - } - } - } - - idea { - module { - inheritOutputDirs = false - outputDir = compileJava.destinationDir - testOutputDir = compileTestJava.destinationDir - } - } - - dependencies { - // The protobuf configuration which was set earlier would generate well-known - // protos which would create conflicts with other libraries that use them as well - compileOnly "io.grpc:grpc-protobuf:${grpcVersion}" - - implementation "io.grpc:grpc-okhttp:${grpcVersion}" - implementation "io.grpc:grpc-protobuf-lite:${grpcVersion}" - implementation "io.grpc:grpc-stub:${grpcVersion}" - implementation 'org.slf4j:slf4j-api:2.0.12' - api 'io.reactivex.rxjava2:rxjava:2.2.21' - - compileOnly "javax.annotation:javax.annotation-api:1.3.2" - - testImplementation "io.grpc:grpc-testing:${grpcVersion}" - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.13.0' - } -} diff --git a/sdk/build.gradle.kts b/sdk/build.gradle.kts new file mode 100644 index 0000000..7216c89 --- /dev/null +++ b/sdk/build.gradle.kts @@ -0,0 +1,192 @@ +import java.util.Properties +import java.io.FileInputStream +import java.io.IOException + +plugins { + alias(libs.plugins.jreleaser) + alias(libs.plugins.protobuf) + `java-library` + `maven-publish` +} + +// Load file "keystore.properties" where we keep our keys +val keystorePropertiesFile = rootProject.file("keystore.properties") +val keystoreProperties = Properties() + +try { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} catch (ignored: IOException) { + if (project.hasProperty("centralUsername")) keystoreProperties["centralUsername"] = property("centralUsername") + if (project.hasProperty("centralPassword")) keystoreProperties["centralPassword"] = property("centralPassword") + if (project.hasProperty("gpgPass")) keystoreProperties["gpgPass"] = property("gpgPass") +} + +group = "io.mavsdk" + +// The version must be of the form "X.Y.Z-b[-SNAPSHOT]", where "X.Y.Z" +// is the MAVSDK-C++ version, "b" is the build number of this +// MAVSDK-Java package and "SNAPSHOT" optionally sets it as a SNAPSHOT. +// For instance, if the version is 3.6.0-2, it should be built with the same +// version of the proto files as MAVSDK-C++ v3.6.0. +version = + if (project.hasProperty("VERSION")) project.property("VERSION").toString() + else "3.10.0-SNAPSHOT" + +val grpcVersion = "1.61.1" + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.25.2" + } + + plugins { + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + } + } + + generateProtoTasks { + all().forEach { task -> + task.builtins { + getByName("java") { + option("lite") + } + } + task.plugins { + create("grpc") { + option("lite") + } + + create("mavsdk") { + option("file_ext=java") + option("template_path=templates/") + } + } + } + } +} + +tasks.withType().configureEach { + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + + withJavadocJar() + withSourcesJar() +} + +if (keystoreProperties.containsKey("centralUsername") && keystoreProperties.containsKey("centralPassword")) { + afterEvaluate { + publishing { + publications { + create("java") { + from(components["java"]) + + pom { + name = "MAVSDK-Java" + packaging = "jar" + description = "MAVSDK client for Java." + url = "https://github.com/mavlink/MAVSDK-Java" + + scm { + connection = "scm:git:https://github.com/mavlink/MAVSDK-Java" + developerConnection = "scm:git:https://github.com/mavlink/MAVSDK-Java" + url = "https://github.com/mavlink/MAVSDK-Java" + } + + licenses { + license { + name = "BSD 3-Clause" + url = "https://opensource.org/licenses/BSD-3-Clause" + } + } + + developers { + developer { + id = "jonasvautherin" + name = "Jonas Vautherin" + email = "dev@jonas.vautherin.ch" + } + } + } + } + } + + repositories { + maven { + url = uri(layout.buildDirectory.dir("target/staging-deploy")) + } + } + } + } + + jreleaser { + signing { + setActive("ALWAYS") + armored.set(true) + setMode("COMMAND") + keystoreProperties["gpgPass"]?.let { + passphrase.set(it as String) + } + + command { + keyName.set("CF3FF35732A465F680A89BC25B01A8023597C84B") + } + } + deploy { + release { + github { + skipRelease = true + skipTag = true + } + } + maven { + mavenCentral { + create("sonatype") { + verifyPom = false + setActive("RELEASE") + username = keystoreProperties["centralUsername"] as String + password = keystoreProperties["centralPassword"] as String + url = "https://central.sonatype.com/api/v1/publisher" + stagingRepository("build/target/staging-deploy") + } + } + nexus2 { + create("snapshot-deploy") { + verifyPom = false + setActive("SNAPSHOT") + snapshotUrl.set("https://central.sonatype.com/repository/maven-snapshots") + url = "https://central.sonatype.com/repository/maven-snapshots" + applyMavenCentralRules = true + snapshotSupported = true + username = keystoreProperties["centralUsername"] as String + password = keystoreProperties["centralPassword"] as String + stagingRepository("build/target/staging-deploy") + } + } + } + } + } +} + +dependencies { + protobuf(files("proto/protos/")) + + compileOnly("io.grpc:grpc-protobuf:${grpcVersion}") + + implementation("io.grpc:grpc-okhttp:${grpcVersion}") + implementation("io.grpc:grpc-protobuf-lite:${grpcVersion}") + implementation("io.grpc:grpc-stub:${grpcVersion}") + implementation("org.slf4j:slf4j-api:2.0.12") + api("io.reactivex.rxjava2:rxjava:2.2.21") + + compileOnly("javax.annotation:javax.annotation-api:1.3.2") + + testImplementation("io.grpc:grpc-testing:${grpcVersion}") + testImplementation("junit:junit:4.12") + testImplementation("org.mockito:mockito-core:2.13.0") +} diff --git a/sdk/gradle/libs.versions.toml b/sdk/gradle/libs.versions.toml new file mode 100644 index 0000000..ab410b5 --- /dev/null +++ b/sdk/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +jreleaser-plugin = "1.19.0" +protobuf-gradle-plugin = "0.9.4" + +[libraries] + +[plugins] +jreleaser = { id = "org.jreleaser", version.ref = "jreleaser-plugin" } +protobuf = { id = "com.google.protobuf", version.ref = "protobuf-gradle-plugin" } diff --git a/sdk/mavsdk/settings.gradle b/sdk/mavsdk/settings.gradle deleted file mode 100644 index 5ef60bf..0000000 --- a/sdk/mavsdk/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'mavsdk' diff --git a/sdk/mavsdk/src/test/java/io/mavsdk/MavsdkTest.java b/sdk/mavsdk/src/test/java/io/mavsdk/MavsdkTest.java deleted file mode 100644 index 9084829..0000000 --- a/sdk/mavsdk/src/test/java/io/mavsdk/MavsdkTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.mavsdk; - -import java.util.concurrent.TimeUnit; -import org.junit.Test; - -public class MavsdkTest { - - @Test - public void testStream() throws InterruptedException { - System system = new System(); - system.getTelemetry().getPosition() - .doOnNext(next -> java.lang.System.out.println(next)) - .test() - .await(5, TimeUnit.SECONDS); - } - - @Test - public void testCall() throws InterruptedException { - System system = new System(); - system.getAction().arm() - .andThen(system.getAction().takeoff()) - .delay(5, TimeUnit.SECONDS) - .andThen(system.getAction().land()) - .test() - .await(); - } - - @Test - public void testRequest() throws InterruptedException { - System system = new System(); - system.getAction().getTakeoffAltitude() - .doOnSuccess(result -> java.lang.System.out.println(result)).test().await(); - } -} diff --git a/sdk/proto b/sdk/proto index e053eca..61b1b1a 160000 --- a/sdk/proto +++ b/sdk/proto @@ -1 +1 @@ -Subproject commit e053ecabc5b825bb9c831f3f31ab24ff727ce1e6 +Subproject commit 61b1b1a197966faed181f8c7c5f2fa57d832cf1f diff --git a/sdk/settings.gradle b/sdk/settings.gradle deleted file mode 100644 index f47d1fa..0000000 --- a/sdk/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include 'mavsdk' diff --git a/sdk/settings.gradle.kts b/sdk/settings.gradle.kts new file mode 100644 index 0000000..7ec28bd --- /dev/null +++ b/sdk/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + + +rootProject.name = "mavsdk" + diff --git a/sdk/mavsdk/src/main/java/io/mavsdk/MavsdkEventQueue.java b/sdk/src/main/java/io/mavsdk/MavsdkEventQueue.java similarity index 100% rename from sdk/mavsdk/src/main/java/io/mavsdk/MavsdkEventQueue.java rename to sdk/src/main/java/io/mavsdk/MavsdkEventQueue.java diff --git a/sdk/mavsdk/src/main/java/io/mavsdk/MavsdkException.java b/sdk/src/main/java/io/mavsdk/MavsdkException.java similarity index 100% rename from sdk/mavsdk/src/main/java/io/mavsdk/MavsdkException.java rename to sdk/src/main/java/io/mavsdk/MavsdkException.java diff --git a/sdk/mavsdk/src/main/java/io/mavsdk/Plugin.java b/sdk/src/main/java/io/mavsdk/Plugin.java similarity index 100% rename from sdk/mavsdk/src/main/java/io/mavsdk/Plugin.java rename to sdk/src/main/java/io/mavsdk/Plugin.java diff --git a/sdk/mavsdk/src/main/java/io/mavsdk/System.java b/sdk/src/main/java/io/mavsdk/System.java similarity index 96% rename from sdk/mavsdk/src/main/java/io/mavsdk/System.java rename to sdk/src/main/java/io/mavsdk/System.java index 3372f1d..116c0e4 100644 --- a/sdk/mavsdk/src/main/java/io/mavsdk/System.java +++ b/sdk/src/main/java/io/mavsdk/System.java @@ -14,6 +14,7 @@ import io.mavsdk.internal.LazyPlugin; import io.mavsdk.log_files.LogFiles; import io.mavsdk.manual_control.ManualControl; +import io.mavsdk.mavlink_direct.MavlinkDirect; import io.mavsdk.mission.Mission; import io.mavsdk.mission_raw.MissionRaw; import io.mavsdk.mission_raw_server.MissionRawServer; @@ -44,6 +45,7 @@ public class System { private final LazyPlugin info; private final LazyPlugin logFiles; private final LazyPlugin manualControl; + private final LazyPlugin mavlinkDirect; private final LazyPlugin mission; private final LazyPlugin missionRaw; private final LazyPlugin missionRawServer; @@ -89,6 +91,7 @@ public System(@NonNull String host, int port) { info = LazyPlugin.from(() -> new Info(host, port)); logFiles = LazyPlugin.from(() -> new LogFiles(host, port)); manualControl = LazyPlugin.from(() -> new ManualControl(host, port)); + mavlinkDirect = LazyPlugin.from(() -> new MavlinkDirect(host, port)); mission = LazyPlugin.from(() -> new Mission(host, port)); missionRaw = LazyPlugin.from(() -> new MissionRaw(host, port)); missionRawServer = LazyPlugin.from(() -> new MissionRawServer(host, port)); @@ -169,6 +172,11 @@ public ManualControl getManualControl() { return manualControl.get(); } + @NonNull + public MavlinkDirect getMavlinkDirect() { + return mavlinkDirect.get(); + } + @NonNull public Mission getMission() { return mission.get(); @@ -252,6 +260,7 @@ public void dispose() { info.dispose(); logFiles.dispose(); manualControl.dispose(); + mavlinkDirect.dispose(); mission.dispose(); missionRaw.dispose(); missionRawServer.dispose(); diff --git a/sdk/mavsdk/src/main/java/io/mavsdk/internal/LazyPlugin.java b/sdk/src/main/java/io/mavsdk/internal/LazyPlugin.java similarity index 100% rename from sdk/mavsdk/src/main/java/io/mavsdk/internal/LazyPlugin.java rename to sdk/src/main/java/io/mavsdk/internal/LazyPlugin.java diff --git a/sdk/mavsdk/src/main/java/io/mavsdk/internal/Provider.java b/sdk/src/main/java/io/mavsdk/internal/Provider.java similarity index 100% rename from sdk/mavsdk/src/main/java/io/mavsdk/internal/Provider.java rename to sdk/src/main/java/io/mavsdk/internal/Provider.java diff --git a/tools/version_validator.py b/tools/version_validator.py new file mode 100644 index 0000000..79ac27f --- /dev/null +++ b/tools/version_validator.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" +Version Number Validator + +This script validates version numbers and extracts major.minor.patch format. + +Version format rules: +- Can start with 'v' (optional): v2.3.1 or 2.3.1 +- Must have major.minor.patch: 1.2.3 (minimum required) +- May have build number: 1.2.3-1 or 1.2.3-2 +- May end with -SNAPSHOT: 3.4.5-1-SNAPSHOT or 3.4.5-SNAPSHOT + +Usage: + python version_validator.py + python version_validator.py --extract +""" + +import re +import sys +import argparse + +def extract_major_minor_patch(version_string): + """ + Extract major.minor.patch from a valid version string in vX.Y.Z format. + + Args: + version_string (str): The version string to extract from + + Returns: + str: The extracted version in vX.Y.Z format, or None if invalid + """ + # Define the regex pattern for valid version strings + pattern = r'^v?(\d+)\.(\d+)\.(\d+)(?:-(\d+))?(?:-SNAPSHOT)?$' + match = re.match(pattern, version_string) + + if match: + major, minor, patch = match.groups()[:3] + return f"v{major}.{minor}.{patch}" + + return None + +def validate_version(version_string): + """ + Validate a version string according to the specified rules. + + Args: + version_string (str): The version string to validate + + Returns: + bool: True if valid, False otherwise + """ + return extract_major_minor_patch(version_string) is not None + +def main(): + parser = argparse.ArgumentParser( + description="Validate version number and extract major.minor.patch format", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python version_validator.py v2.3.1 # Validate version + python version_validator.py 1.2.3-1 # Validate version with build + python version_validator.py --extract v2.3.1-SNAPSHOT # Extract v2.3.1 + python version_validator.py -e 3.4.5-2-SNAPSHOT # Extract v3.4.5 + +Valid version formats: + v1.2.3, 1.2.3, v1.2.3-1, 1.2.3-1, v1.2.3-SNAPSHOT, 1.2.3-1-SNAPSHOT + """ + ) + + parser.add_argument( + 'version', + help='Version string to validate/extract' + ) + parser.add_argument( + '-e', '--extract', + action='store_true', + help='Extract major.minor.patch in vX.Y.Z format' + ) + + args = parser.parse_args() + + if args.extract: + # Extract mode + result = extract_major_minor_patch(args.version) + if result: + print(result) + sys.exit(0) + else: + print(f"Error: '{args.version}' is not a valid version string", file=sys.stderr) + sys.exit(1) + else: + # Validation mode + if validate_version(args.version): + print(f"'{args.version}' is a valid version string") + sys.exit(0) + else: + print(f"Error: '{args.version}' is not a valid version string", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + # If no arguments provided, run some test cases + if len(sys.argv) == 1: + print("Running test cases...") + + test_cases = [ + # Valid cases + ("v2.3.1", True), + ("2.3.1", True), + ("1.2.3-1", True), + ("v1.2.3-1", True), + ("3.4.5-SNAPSHOT", True), + ("v3.4.5-SNAPSHOT", True), + ("1.2.3-1-SNAPSHOT", True), + ("v1.2.3-1-SNAPSHOT", True), + ("10.20.30", True), + ("v0.0.1", True), + + # Invalid cases + ("1.2", False), + ("v1.2", False), + ("1.2.3.4", False), + ("v1.2.3.4", False), + ("1.2.3-", False), + ("1.2.3-SNAPSHOT-1", False), + ("1.2.3-a", False), + ("v1.2.3-a", False), + ("av1.2.3", False), + ("a1.2.3", False), + (" v1.2.3", False), + ("V1.2.3-a", False), + ("1.2.x", False), + ("", False), + ("v", False), + ("1.2.3-1-SNAPSHOT-extra", False), + ] + + print("\nValidation Tests:") + for version, expected in test_cases: + result = validate_version(version) + status = "✓" if result == expected else "✗" + print(f"{status} {version:<25} -> {result} (expected {expected})") + + print("\nExtraction Tests:") + extraction_tests = [ + ("v2.3.1", "v2.3.1"), + ("2.3.1", "v2.3.1"), + ("1.2.3-1", "v1.2.3"), + ("v1.2.3-1-SNAPSHOT", "v1.2.3"), + ("3.4.5-SNAPSHOT", "v3.4.5"), + ("invalid", None), + ] + + for version, expected in extraction_tests: + result = extract_major_minor_patch(version) + status = "✓" if result == expected else "✗" + print(f"{status} {version:<25} -> {result} (expected {expected})") + else: + main()