├── app ├── app │ ├── .cxx │ │ ├── ndk_locator_record_1e2102h1.json │ │ └── ndk_locator_record_1e2102h1_key.json │ ├── .gitignore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── logo.png │ │ │ │ │ ├── rounded_button.xml │ │ │ │ │ ├── rounded_dialog.xml │ │ │ │ │ ├── ic_stop_24px.xml │ │ │ │ │ ├── red_gradient.xml │ │ │ │ │ ├── green_gradient.xml │ │ │ │ │ ├── ic_play_arrow_24px.xml │ │ │ │ │ ├── cellular.xml │ │ │ │ │ ├── ic_done_black_18dp.xml │ │ │ │ │ ├── ic_code_24px.xml │ │ │ │ │ ├── ic_info_24px.xml │ │ │ │ │ ├── ic_videocam_24px.xml │ │ │ │ │ ├── ic_chat_24px.xml │ │ │ │ │ ├── vpn.xml │ │ │ │ │ ├── ic_close_24.xml │ │ │ │ │ ├── ethernet.xml │ │ │ │ │ ├── ic_print_24px.xml │ │ │ │ │ ├── ic_favorite_24px.xml │ │ │ │ │ ├── circle_drawable.xml │ │ │ │ │ ├── ic_baseline_article_24.xml │ │ │ │ │ ├── ic_baseline_heart_broken_24.xml │ │ │ │ │ ├── wifi.xml │ │ │ │ │ ├── ic_usb_24px.xml │ │ │ │ │ ├── ic_help.xml │ │ │ │ │ ├── ic_baseline_extension_24.xml │ │ │ │ │ ├── ic_share_24.xml │ │ │ │ │ └── ic_settings_24px.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── values │ │ │ │ │ ├── ids.xml │ │ │ │ │ ├── attrs.xml │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── arrays.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── menu │ │ │ │ │ ├── main_appbar.xml │ │ │ │ │ └── nav_menu.xml │ │ │ │ ├── layout │ │ │ │ │ ├── bootstrap_spinner_view.xml │ │ │ │ │ ├── activity_webinterface.xml │ │ │ │ │ ├── view_ip_address.xml │ │ │ │ │ ├── dialog_camera_preview.xml │ │ │ │ │ ├── activity_main.xml │ │ │ │ │ ├── extension_view.xml │ │ │ │ │ ├── view_installation_item.xml │ │ │ │ │ ├── fragment_extensions.xml │ │ │ │ │ ├── view_usb_devices_item.xml │ │ │ │ │ ├── fragment_about.xml │ │ │ │ │ ├── fragment_terminal_sheet.xml │ │ │ │ │ ├── fragment_server.xml │ │ │ │ │ ├── activity_landing.xml │ │ │ │ │ ├── view_status_card.xml │ │ │ │ │ └── select_driver_bottom_sheet.xml │ │ │ │ ├── navigation │ │ │ │ │ └── navigation_graph.xml │ │ │ │ ├── xml │ │ │ │ │ ├── device_filter.xml │ │ │ │ │ └── main_preferences.xml │ │ │ │ └── drawable-v24 │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── ic_launcher-playstore.png │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── octo4a │ │ │ │ │ ├── serial │ │ │ │ │ ├── VSPListener.java │ │ │ │ │ ├── VSPPty.kt │ │ │ │ │ ├── SerialData.kt │ │ │ │ │ └── PrinterProber.kt │ │ │ │ │ ├── utils │ │ │ │ │ ├── AnimationExtensions.kt │ │ │ │ │ ├── ManualLifecycleOwner.kt │ │ │ │ │ ├── CancelableTimer.kt │ │ │ │ │ ├── WaitableEvent.kt │ │ │ │ │ ├── ProgressTrackingInputStream.kt │ │ │ │ │ ├── preferences │ │ │ │ │ │ ├── MainPreferences.kt │ │ │ │ │ │ └── Preferences.kt │ │ │ │ │ ├── Octo4aWakeLock.kt │ │ │ │ │ ├── ProcessUtils.kt │ │ │ │ │ └── Extensions.kt │ │ │ │ │ ├── ui │ │ │ │ │ ├── BugReportingDialogExtension.kt │ │ │ │ │ ├── views │ │ │ │ │ │ ├── ExtensionView.kt │ │ │ │ │ │ ├── IPAddressView.kt │ │ │ │ │ │ ├── InstallationProgressItem.kt │ │ │ │ │ │ ├── UsbDeviceView.kt │ │ │ │ │ │ └── StatusView.kt │ │ │ │ │ ├── fragments │ │ │ │ │ │ ├── AboutFragment.kt │ │ │ │ │ │ ├── ExtensionsFragment.kt │ │ │ │ │ │ └── TerminalSheetDialog.kt │ │ │ │ │ └── WebinterfaceActivity.kt │ │ │ │ │ ├── Octo4aApplication.kt │ │ │ │ │ ├── repository │ │ │ │ │ ├── GithubRepository.kt │ │ │ │ │ ├── SystemStatusRepository.kt │ │ │ │ │ ├── FIFOEventRepository.kt │ │ │ │ │ └── LoggerRepository.kt │ │ │ │ │ ├── BootReceiver.kt │ │ │ │ │ ├── AppModule.kt │ │ │ │ │ ├── camera │ │ │ │ │ ├── MJpegServer.kt │ │ │ │ │ ├── MjpegResponse.kt │ │ │ │ │ ├── NativeCameraUtils.kt │ │ │ │ │ └── RotateUtils.kt │ │ │ │ │ └── viewmodel │ │ │ │ │ ├── StatusViewModel.kt │ │ │ │ │ └── InstallationViewModel.kt │ │ │ ├── cpp │ │ │ │ ├── util.h │ │ │ │ ├── CMakeLists.txt │ │ │ │ ├── real_pty.h │ │ │ │ ├── openpty.c │ │ │ │ ├── ioctl-hook.c │ │ │ │ └── yuv2rgb.cpp │ │ │ └── AndroidManifest.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── octo4a │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── octo4a │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── .gitignore ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── build.gradle.kts ├── gradle.properties └── gradlew.bat ├── fastlane └── metadata │ └── android │ └── en-US │ ├── title.txt │ ├── changelogs │ ├── 1000003.txt │ └── 1000002.txt │ ├── short_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ └── full_description.txt ├── .github ├── FUNDING.yml ├── OTG-connection.png ├── readme-banner.png └── workflows │ └── build-app.yml ├── scripts ├── setup-plugin-extras.sh ├── setup-octo4a.sh ├── setup-klipper.sh └── comm-fix.py ├── bootstrap-builder ├── go.mod ├── fake_dpkg.sh ├── properties.sh ├── go.sum ├── replace_paths.go └── dpkg_replacer.go ├── local.properties ├── .gitignore └── README.md /app/app/.cxx/ndk_locator_record_1e2102h1.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .gradle/* 3 | local.properties 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | OctoPrint for Android 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/1000003.txt: -------------------------------------------------------------------------------- 1 | Fix FDroid release 2 | -------------------------------------------------------------------------------- /app/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.cxx 3 | /src/main/obj 4 | /debug 5 | /release 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/1000002.txt: -------------------------------------------------------------------------------- 1 | First FDroid release 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [feelfreelinux] 2 | custom: ["https://www.paypal.me/feelfreelinux"] 3 | -------------------------------------------------------------------------------- /scripts/setup-plugin-extras.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | apk add python3-dev build-base gcc g++ 5 | -------------------------------------------------------------------------------- /.github/OTG-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/.github/OTG-connection.png -------------------------------------------------------------------------------- /.github/readme-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/.github/readme-banner.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Use your old Android device as an OctoPrint server. 2 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /app/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/app/.cxx/ndk_locator_record_1e2102h1_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkFolder": "/Users/cspot/Library/Android/sdk", 3 | "sideBySideNdkFolderNames": [] 4 | } -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/serial/VSPListener.java: -------------------------------------------------------------------------------- 1 | package com.octo4a.serial; 2 | 3 | public interface VSPListener { 4 | void onDataReceived(SerialData data); 5 | } -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /bootstrap-builder/go.mod: -------------------------------------------------------------------------------- 1 | module dpkg_replacer 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb 7 | github.com/mitchellh/gox v1.0.1 // indirect 8 | github.com/ulikunitz/xz v0.5.10 9 | ) 10 | -------------------------------------------------------------------------------- /app/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Mar 07 22:49:50 CET 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/app/src/main/cpp/util.h: -------------------------------------------------------------------------------- 1 | #ifndef _UTIL_H_ 2 | #define _UTIL_H_ 3 | 4 | #ifdef DEBUG_TRACE 5 | void _trace(const char* format, ...); 6 | #endif 7 | 8 | #ifdef DEBUG_TRACE 9 | #define TRACE(X) _trace X; 10 | #else /*DEBUG_TRACE*/ 11 | #define TRACE(X) 12 | #endif /*DEBUG_TRACE*/ 13 | 14 | #endif //_UTIL_H_ -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/rounded_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /bootstrap-builder/fake_dpkg.sh: -------------------------------------------------------------------------------- 1 | #!/data/data/com.octo4a/files/usr/bin/bash 2 | for var in "$@" 3 | do 4 | if [[ "$var" == *.deb ]] 5 | then 6 | >&2 echo "Replacing deb $var" 7 | dpkg_replacer $var 8 | rm $var 9 | mv $var.replaced $var 10 | 11 | fi 12 | done 13 | /data/data/com.octo4a/files/usr/bin/realDpkg $@ 14 | -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Sun Feb 05 13:08:54 UTC 2023 8 | -------------------------------------------------------------------------------- /app/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":app") 2 | rootProject.name = "OctoPrint for Android" 3 | pluginManagement { 4 | repositories { 5 | google() 6 | jcenter() 7 | gradlePluginPortal() 8 | mavenCentral() 9 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } 10 | } 11 | } -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/rounded_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_stop_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/red_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/green_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_play_arrow_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/cellular.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_done_black_18dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/menu/main_appbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_code_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/AnimationExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils 2 | 3 | import android.view.View 4 | import android.view.animation.LinearInterpolator 5 | 6 | var View.animatedAlpha: Float 7 | get() = alpha 8 | set(value) { 9 | animate().apply { 10 | interpolator = LinearInterpolator() 11 | duration = 300 12 | alpha(value) 13 | start() 14 | } 15 | } -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_info_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/serial/VSPPty.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.serial 2 | 3 | class VSPPty { 4 | init { 5 | System.loadLibrary("vsp-pty") 6 | } 7 | 8 | external fun setVSPListener(listener: VSPListener) 9 | external fun writeData(data: ByteArray) 10 | external fun getBaudrate(data: Int): Int 11 | external fun runPtyThread() 12 | external fun cancelPtyThread() 13 | external fun createEventPipe() 14 | 15 | } -------------------------------------------------------------------------------- /app/app/src/test/java/com/octo4a/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_videocam_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_chat_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/serial/SerialData.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.serial 2 | 3 | import com.octo4a.utils.isBitSet 4 | 5 | class SerialData(val data: ByteArray, val baudrate: Int, val c_iflag: Int, val c_oflag: Int, val c_cflag: Int, val c_lflag: Int) { 6 | companion object { 7 | const val TIOCPKT_DATA = 0 8 | } 9 | 10 | val isStartPacket = data[0].toInt().isBitSet(TIOCPKT_DATA) 11 | val serialData: ByteArray 12 | get() = data.copyOfRange(1, data.size) 13 | } -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/vpn.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_close_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ethernet.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_print_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_favorite_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(Octo4a) 2 | cmake_minimum_required(VERSION 3.4.1) 3 | 4 | add_library(vsp-pty SHARED 5 | vsp-pty.cpp openpty.c) 6 | 7 | target_link_libraries(vsp-pty 8 | android 9 | log) 10 | 11 | add_library(yuv2rgb SHARED 12 | yuv2rgb.cpp) 13 | 14 | target_link_libraries(yuv2rgb 15 | android) 16 | 17 | add_library(ioctl-hook SHARED 18 | ioctl-hook.c) 19 | 20 | target_link_libraries(ioctl-hook 21 | android) -------------------------------------------------------------------------------- /app/app/src/main/cpp/real_pty.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | #ifndef _PTY_H 5 | #define _PTY_H 1 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | /* Create pseudo tty master slave pair with NAME and set terminal 13 | attributes according to TERMP and WINP and return handles for both 14 | ends in AMASTER and ASLAVE. */ 15 | int openpty (int *__amaster, int *__aslave, char *__name, 16 | struct termios *__termp, struct winsize *__winp); 17 | 18 | #ifdef __cplusplus 19 | } 20 | #endif 21 | #endif /* pty.h */ -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/circle_drawable.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 9 | 12 | 17 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_baseline_article_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_baseline_heart_broken_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/wifi.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_usb_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_help.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_baseline_extension_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | gradlePluginPortal() 4 | jcenter() 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("com.android.tools.build:gradle:7.1.3") 10 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30") 11 | classpath("com.bugsnag:bugsnag-android-gradle-plugin:7.+") 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | jcenter() 20 | maven { url = uri("https://jitpack.io") } 21 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } 22 | } 23 | } 24 | 25 | tasks.register("clean", Delete::class){ 26 | delete(rootProject.buildDir) 27 | } 28 | -------------------------------------------------------------------------------- /app/app/src/androidTest/java/com/octo4a/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.octo4a", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/app/src/main/res/menu/nav_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/ManualLifecycleOwner.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils 2 | 3 | import androidx.lifecycle.Lifecycle 4 | import androidx.lifecycle.LifecycleOwner 5 | import androidx.lifecycle.LifecycleRegistry 6 | 7 | class ManualLifecycleOwner : LifecycleOwner { 8 | private val lifecycleRegistry: LifecycleRegistry 9 | 10 | init { 11 | lifecycleRegistry = LifecycleRegistry(this) 12 | lifecycleRegistry.currentState = Lifecycle.State.CREATED 13 | } 14 | 15 | fun start() { 16 | lifecycleRegistry.currentState = Lifecycle.State.RESUMED 17 | } 18 | 19 | fun stop() { 20 | lifecycleRegistry.currentState = Lifecycle.State.CREATED 21 | } 22 | 23 | override fun getLifecycle(): Lifecycle { 24 | return lifecycleRegistry 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_share_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/CancelableTimer.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import java.util.Timer 6 | import java.util.TimerTask 7 | 8 | class CancelableTimer(private val handler: Handler = Handler(Looper.getMainLooper())) { 9 | private var timer: Timer? = null 10 | private var task: TimerTask? = null 11 | 12 | fun start(delay: Long, callback: () -> Unit) { 13 | timer?.cancel() // Cancel any existing timer 14 | timer = Timer() 15 | task = 16 | object : TimerTask() { 17 | override fun run() { 18 | handler.post { callback() } 19 | } 20 | } 21 | timer?.schedule(task, delay) 22 | } 23 | 24 | fun cancel() { 25 | task?.cancel() 26 | timer?.cancel() 27 | timer = null 28 | task = null 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/WaitableEvent.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils 2 | 3 | import java.util.concurrent.locks.ReentrantLock 4 | import kotlin.concurrent.withLock 5 | 6 | class WaitableEvent { 7 | private val lock = ReentrantLock() 8 | private val condition = lock.newCondition() 9 | private var isSet = false 10 | 11 | fun wait(autoreset: Boolean = false) { 12 | lock.withLock { 13 | while (!isSet) { 14 | condition.await() 15 | } 16 | if( autoreset ) { 17 | reset() 18 | } 19 | } 20 | } 21 | 22 | fun set() { 23 | lock.withLock { 24 | isSet = true 25 | condition.signalAll() 26 | } 27 | } 28 | 29 | fun reset() { 30 | lock.withLock { 31 | isSet = false 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/app/src/main/res/layout/bootstrap_spinner_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 21 | -------------------------------------------------------------------------------- /bootstrap-builder/properties.sh: -------------------------------------------------------------------------------- 1 | TERMUX_ANDROID_BUILD_TOOLS_VERSION=30.0.3 2 | TERMUX_NDK_VERSION_NUM=21 3 | TERMUX_NDK_REVISION="d" 4 | TERMUX_NDK_VERSION=$TERMUX_NDK_VERSION_NUM$TERMUX_NDK_REVISION 5 | 6 | if [ "${TERMUX_PACKAGES_OFFLINE-false}" = "true" ]; then 7 | export ANDROID_HOME=${TERMUX_SCRIPTDIR}/build-tools/android-sdk 8 | export NDK=${TERMUX_SCRIPTDIR}/build-tools/android-ndk 9 | else 10 | : "${ANDROID_HOME:="${HOME}/lib/android-sdk"}" 11 | : "${NDK:="${HOME}/lib/android-ndk"}" 12 | fi 13 | 14 | # Termux packages configuration. 15 | TERMUX_APP_PACKAGE="com.termux" 16 | TERMUX_BASE_DIR="/data/data/${TERMUX_APP_PACKAGE}/files" 17 | TERMUX_CACHE_DIR="/data/data/${TERMUX_APP_PACKAGE}/cache" 18 | TERMUX_ANDROID_HOME="${TERMUX_BASE_DIR}/home" 19 | TERMUX_PREFIX="${TERMUX_BASE_DIR}/usr" 20 | 21 | # Allow to override setup. 22 | if [ -f "$HOME/.termuxrc" ]; then 23 | . "$HOME/.termuxrc" 24 | fi 25 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/ProgressTrackingInputStream.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils 2 | 3 | import java.io.FilterInputStream 4 | import java.io.InputStream 5 | class ProgressTrackingInputStream( 6 | inputStream: InputStream, 7 | private val progressListener: (Long) -> Unit 8 | ) : FilterInputStream(inputStream) { 9 | 10 | private var totalBytesRead: Long = 0 11 | 12 | override fun read(): Int { 13 | val byte = super.read() 14 | if (byte >= 0) { 15 | totalBytesRead++ 16 | progressListener(totalBytesRead) 17 | } 18 | return byte 19 | } 20 | 21 | override fun read(b: ByteArray, off: Int, len: Int): Int { 22 | val bytesRead = super.read(b, off, len) 23 | if (bytesRead > 0) { 24 | totalBytesRead += bytesRead 25 | progressListener(totalBytesRead) 26 | } 27 | return bytesRead 28 | } 29 | } -------------------------------------------------------------------------------- /app/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #43a047 4 | #76d275 5 | #00701a 6 | #1976d2 7 | #63a4ff 8 | #004ba0 9 | #000000 10 | #ffffff 11 | 12 | #FFE082 13 | #d35400 14 | #1FFFFFFF 15 | #000000 16 | #ff4CAF50 17 | #1D1F21 18 | #C5C8C6 19 | 20 | -------------------------------------------------------------------------------- /bootstrap-builder/go.sum: -------------------------------------------------------------------------------- 1 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= 2 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= 3 | github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8= 4 | github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 5 | github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= 6 | github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= 7 | github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= 8 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 9 | github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= 10 | github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 11 | -------------------------------------------------------------------------------- /app/app/src/main/cpp/openpty.c: -------------------------------------------------------------------------------- 1 | #include "pty.h" 2 | 3 | #include 4 | #include 5 | #include "util.h" 6 | 7 | #define HAVE_openpty 8 | int openpty(int *amaster, int *aslave, char *name, struct termios *termp, struct winsize *winp) 9 | { 10 | char buf[512]; 11 | int master, slave; 12 | 13 | master = open("/dev/ptmx", O_RDWR); 14 | if (master == -1) return -1; 15 | if (grantpt(master) || unlockpt(master) || ptsname_r(master, buf, sizeof buf)) goto fail; 16 | 17 | slave = open(buf, O_RDWR | O_NOCTTY); 18 | if (slave == -1) goto fail; 19 | 20 | /* XXX Should we ignore errors here? */ 21 | if (termp) tcsetattr(slave, TCSAFLUSH, termp); 22 | if (winp) ioctl(slave, TIOCSWINSZ, winp); 23 | 24 | *amaster = master; 25 | *aslave = slave; 26 | if (name != NULL) strcpy(name, buf); 27 | return 0; 28 | 29 | fail: 30 | close(master); 31 | return -1; 32 | } -------------------------------------------------------------------------------- /bootstrap-builder/replace_paths.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | var REPLACE_FROM = []byte("com.termux") 12 | var REPLACE_TO = []byte("com.octo4a") 13 | 14 | func main() { 15 | if len(os.Args) < 2 { 16 | log.Fatalf("usage: replace_path ") 17 | } 18 | err := filepath.Walk(os.Args[1], 19 | func(path string, info os.FileInfo, err error) error { 20 | if err != nil { 21 | return err 22 | } 23 | if info.IsDir() { 24 | return nil 25 | } 26 | data, err := os.ReadFile(path) 27 | if err != nil { 28 | return fmt.Errorf("failed to read file %v: %w", path, err) 29 | } 30 | out := bytes.Replace(data, REPLACE_FROM, REPLACE_TO, -1) 31 | err = os.WriteFile(path, out, info.Mode()) 32 | if err != nil { 33 | return fmt.Errorf("failed to read write %v: %w", path, err) 34 | } 35 | return nil 36 | }) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 90 degree 5 | 180 degree 6 | 270 degree 7 | Do not rotate 8 | 9 | 10 | 90 11 | 180 12 | 270 13 | 0 14 | 15 | 16 | 17 | 5 FPS 18 | 10 FPS 19 | 15 FPS 20 | 20 FPS 21 | 30 FPS 22 | Do not limit 23 | 24 | 25 | 5 26 | 10 27 | 15 28 | 20 29 | 30 30 | -1 31 | 32 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/activity_webinterface.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /scripts/setup-octo4a.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | # install required dependencies 5 | apk add py3-pip py3-yaml py3-regex py3-zeroconf py3-netifaces py3-cffi py3-psutil unzip py3-pillow ttyd ffmpeg 6 | 7 | mkdir -p /root/extensions/ttyd 8 | cat << EOF > /root/extensions/ttyd/manifest.json 9 | { 10 | "title": "Remote web terminal (ttyd)", 11 | "description": "Uses port 5002; User root / ssh password" 12 | } 13 | EOF 14 | 15 | echo "octoprint" > /root/.octoCredentials 16 | cat << EOF > /root/extensions/ttyd/start.sh 17 | #!/bin/sh 18 | ttyd -p 5002 --credential root:\$(cat /root/.octoCredentials) bash 19 | EOF 20 | 21 | cat << EOF > /root/extensions/ttyd/kill.sh 22 | #!/bin/sh 23 | pkill ttyd 24 | EOF 25 | chmod +x /root/extensions/ttyd/start.sh 26 | chmod +x /root/extensions/ttyd/kill.sh 27 | chmod 777 /root/extensions/ttyd/start.sh 28 | chmod 777 /root/extensions/ttyd/kill.sh 29 | 30 | pip3 install -U packaging --ignore-installed 31 | pip3 install https://github.com/feelfreelinux/octo4a-argon2-mock/archive/main.zip 32 | touch /home/octoprint/.argon-fix 33 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/BugReportingDialogExtension.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui 2 | 3 | import android.app.Activity 4 | import android.app.AlertDialog 5 | import com.octo4a.Octo4aApplication 6 | import com.octo4a.R 7 | import com.octo4a.utils.preferences.MainPreferences 8 | 9 | fun Activity.showBugReportingDialog(prefs: MainPreferences) { 10 | if (!prefs.hasAskedAboutReporting) { 11 | val builder = AlertDialog.Builder(this) 12 | builder.apply { 13 | setTitle(getString(R.string.bugreport_dialog_title)) 14 | setMessage(getString(R.string.bugreport_dialog_msg)) 15 | setNegativeButton(getString(R.string.bugreport_dialog_dismiss)) { dialog, id -> 16 | prefs.hasAskedAboutReporting = true 17 | prefs.enableBugReporting = false 18 | } 19 | setPositiveButton(getString(R.string.bugreport_dialog_enable)) { dialog, id -> 20 | prefs.hasAskedAboutReporting = true 21 | prefs.enableBugReporting = true 22 | (application as Octo4aApplication).startBugsnag() 23 | } 24 | } 25 | val dialog = builder.create() 26 | dialog.show() 27 | } 28 | } -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_settings_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/navigation/navigation_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | 14 | 18 | 19 | 24 | 25 | 30 | 31 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/view_ip_address.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 23 | 24 | 31 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/Octo4aApplication.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a 2 | 3 | import androidx.multidex.MultiDexApplication 4 | import com.bugsnag.android.Bugsnag 5 | import com.octo4a.utils.TLSSocketFactory 6 | import com.octo4a.utils.preferences.MainPreferences 7 | import org.koin.android.ext.koin.androidLogger 8 | import org.koin.android.ext.koin.androidContext 9 | import org.koin.core.context.startKoin 10 | import javax.net.ssl.HttpsURLConnection 11 | 12 | class Octo4aApplication : MultiDexApplication() { 13 | val preferences by lazy { MainPreferences(this) } 14 | var bugsnagStarted = false 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | initializeSSLContext() 19 | 20 | // Start Koin 21 | startKoin { 22 | androidLogger() 23 | androidContext(this@Octo4aApplication) 24 | modules(appModule) 25 | } 26 | 27 | if (preferences.enableBugReporting) { 28 | startBugsnag() 29 | } 30 | } 31 | 32 | fun startBugsnag() { 33 | if (bugsnagStarted) return 34 | 35 | Bugsnag.start(this) 36 | bugsnagStarted = true 37 | } 38 | 39 | fun initializeSSLContext() { 40 | val noSSLv3Factory: TLSSocketFactory = TLSSocketFactory(applicationContext) 41 | 42 | HttpsURLConnection.setDefaultSSLSocketFactory(noSSLv3Factory) 43 | } 44 | } -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | #org.gradle.jvmargs=-Xmx2048m -Dkotlin.daemon.jvm.options=--illegal-access=permit 10 | org.gradle.jvmargs=--illegal-access=permit -Dkotlin.daemon.jvm.options=--illegal-access=permit 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | # AndroidX package structure to make it clearer which packages are bundled with the 16 | # Android operating system, and which are packaged with your app"s APK 17 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 18 | android.useAndroidX=true 19 | # Automatically convert third-party libraries to use AndroidX 20 | android.enableJetifier=true 21 | # Kotlin code style for this project: "official" or "obsolete": 22 | kotlin.code.style=official 23 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/repository/GithubRepository.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.repository 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.request.* 5 | import io.ktor.http.* 6 | import java.util.Date 7 | 8 | data class GithubAsset(val name: String, val url: String, val browserDownloadUrl: String) 9 | 10 | data class GithubRelease(val tagName: String, val zipballUrl: String, val body: String, val assets: List, val htmlUrl: String, val name: String, val prerelease: Boolean, val publishedAt: Date) 11 | 12 | interface GithubRepository { 13 | suspend fun getNewestRelease(repository: String): GithubRelease 14 | suspend fun getNewestReleases(repository: String): List 15 | } 16 | 17 | class GithubRepositoryImpl(val httpClient: HttpClient): GithubRepository { 18 | private val baseUrl = "https://api.github.com/" 19 | 20 | override suspend fun getNewestRelease(repository: String): GithubRelease { 21 | return httpClient.get { 22 | url("${baseUrl}repos/${repository}/releases/latest") 23 | contentType(ContentType.Application.Json) 24 | } 25 | } 26 | 27 | override suspend fun getNewestReleases(repository: String): List { 28 | return httpClient.get { 29 | url("${baseUrl}repos/${repository}/releases") 30 | contentType(ContentType.Application.Json) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 14 | 15 | 24 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/preferences/MainPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils.preferences 2 | 3 | import android.content.Context 4 | 5 | class MainPreferences(context: Context) : Preferences(context, true) { 6 | var enableCameraServer by booleanPref() 7 | var selectedCamera by stringPref(defaultValue = null) 8 | var selectedResolution by stringPref() 9 | var selectedVideoResolution by stringPref() 10 | var enableSSH by booleanPref(defaultValue = false) 11 | var manualAF by booleanPref(defaultValue = false) 12 | var manualAFValue by floatPref(defaultValue = 0f) 13 | var changeSSHPassword by stringPref() 14 | var sshPort by stringPref(defaultValue = "8022") 15 | var flashWhenObserved by booleanPref() 16 | var currentRelease by stringPref(defaultValue = "") 17 | var defaultPrinterPid by intPref() 18 | var defaultPrinterVid by intPref() 19 | var defaultPrinterCustomDriver by stringPref() 20 | var imageRotation by stringPref(defaultValue = "0") 21 | var fpsLimit by stringPref(defaultValue = "-1") 22 | var sshPasword by stringPref() 23 | var enableBugReporting by booleanPref(defaultValue = false) 24 | var hasAskedAboutReporting by booleanPref(defaultValue = false) 25 | var extensionSettings by stringPref(defaultValue = "[]") 26 | var updateDismissed by stringPref() 27 | var startOnBoot by booleanPref(defaultValue = false) 28 | var warnDisableBatteryOptimization by booleanPref(defaultValue = true) 29 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/repository/SystemStatusRepository.kt: -------------------------------------------------------------------------------- 1 | //package com.octo4a.repository 2 | // 3 | //package com.octo4a.repository 4 | // 5 | //import android.content.Context 6 | //import com.octo4a.octoprint.BootstrapUtils 7 | //import com.octo4a.utils.* 8 | //import com.octo4a.utils.preferences.MainPreferences 9 | //import kotlinx.coroutines.flow.MutableStateFlow 10 | //import kotlinx.coroutines.flow.StateFlow 11 | //import kotlin.math.roundToInt 12 | // 13 | //enum class ServerStatus(val value: Int) { 14 | // InstallingBootstrap(0), 15 | // DownloadingOctoPrint(1), 16 | // InstallingDependencies(2), 17 | // BootingUp(3), 18 | // Running(4), 19 | // Stopped(5) 20 | //} 21 | // 22 | //data class UsbDeviceStatus(val isAttached: Boolean, val port: String = "") 23 | // 24 | //fun ServerStatus.getInstallationProgress(): Int { 25 | // return ((value.toDouble() / 4) * 100).roundToInt() 26 | //} 27 | // 28 | //fun ServerStatus.isInstallationFinished(): Boolean { 29 | // return value == ServerStatus.Running.value 30 | //} 31 | // 32 | //interface SystemStatusRepository { 33 | // val serverState: StateFlow 34 | // val usbDeviceStatus: StateFlow 35 | // 36 | // fun setServerStatus(status: ServerStatus) 37 | // fun setSSHEnabled(isSSHEnabled: Boolean) 38 | // fun usbDevicePluggedIn(port: String) 39 | // fun usbDeviceDetached() 40 | // fun setCameraServerRunning(isRunning: Boolean) 41 | //} 42 | // 43 | //class SystemStatusRepositoryImpl() : SystemStatusRepository { 44 | // private var _serverState = MutableStateFlow(ServerStatus.InstallingBootstrap) 45 | // private var _usbDeviceStatus = MutableStateFlow(UsbDeviceStatus(false)) 46 | // 47 | //} -------------------------------------------------------------------------------- /app/app/src/main/res/layout/dialog_camera_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 22 | 27 | 31 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/repository/FIFOEventRepository.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.google.gson.GsonBuilder 6 | import org.koin.java.KoinJavaComponent.inject 7 | import java.io.File 8 | import java.lang.Exception 9 | 10 | data class FIFOEvent(val eventType: String) 11 | 12 | interface FIFOEventRepository { 13 | val eventState: LiveData 14 | fun handleFifoEvents() 15 | } 16 | 17 | class FIFOEventRepositoryImpl(val logger: LoggerRepository) : FIFOEventRepository { 18 | private var _eventState = MutableLiveData() 19 | override val eventState = _eventState 20 | 21 | private val eventFifoPath = "/data/data/com.octo4a/files/bootstrap/bootstrap/eventPipe" 22 | private val gson by lazy { 23 | GsonBuilder() 24 | .create() 25 | } 26 | 27 | override fun handleFifoEvents() { 28 | try { 29 | val fifoFile = File(eventFifoPath) 30 | while (true) { 31 | fifoFile.inputStream().bufferedReader().forEachLine { 32 | logger.log(this) { "Got event $it" } 33 | try { 34 | val event = gson.fromJson(it.replace("\n", ""), FIFOEvent::class.java) 35 | _eventState.postValue(event) 36 | } catch (e: Exception) { 37 | logger.log(this) { "Error occured when parsing fifo event " + e.message } 38 | } 39 | } 40 | } 41 | } catch (e: Exception) { 42 | logger.log(this) { "FIFO Handler error " + e.message } 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/views/ExtensionView.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | 7 | import androidx.constraintlayout.widget.ConstraintLayout 8 | import com.octo4a.R 9 | import com.octo4a.repository.ExtensionStatus 10 | import com.octo4a.repository.ExtensionsRepository 11 | import com.octo4a.repository.RegisteredExtension 12 | import kotlinx.android.synthetic.main.extension_view.view.* 13 | 14 | class ExtensionView @JvmOverloads 15 | constructor(private val ctx: Context, private val extensionsRepository: ExtensionsRepository, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0) 16 | : ConstraintLayout(ctx, attributeSet, defStyleAttr) { 17 | 18 | var onActionClicked: () -> Unit = {} 19 | 20 | init { 21 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 22 | inflater.inflate(R.layout.extension_view, this) 23 | } 24 | 25 | fun setExtensionDescription(extensionInfo: RegisteredExtension) { 26 | titleText.text = extensionInfo.title 27 | 28 | val status = when (extensionInfo.status) { 29 | ExtensionStatus.Stopped -> " (stopped)" 30 | ExtensionStatus.Running -> " (running)" 31 | else -> " (crashed)" 32 | } 33 | 34 | subtitleText.text = extensionInfo.description + status 35 | 36 | 37 | extensionEnabled.isChecked = extensionsRepository.getExtensionSettings(extensionInfo.name)?.enabled ?: false 38 | extensionEnabled.setOnCheckedChangeListener { _, checked -> 39 | extensionsRepository.modifyExtensionSetting(extensionInfo.name, checked) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /scripts/setup-klipper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | COL='\033[1;32m' 4 | NC='\033[0m' # No Color 5 | echo -e "${COL}Setting up klipper" 6 | 7 | read -p "Do you have \"Plugin extras\" installed? (y/n): " -n 1 -r 8 | if [[ ! $REPLY =~ ^[Yy]$ ]] 9 | then 10 | echo -e "${COL}\nPlease go to settings and install plugin extras${NC}" 11 | [[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1 12 | fi 13 | 14 | echo -e "${COL}\nInstalling dependencies...\n${NC}" 15 | # install required dependencies 16 | apk add py3-cffi py3-greenlet linux-headers can-utils 17 | pip3 install python-can 18 | 19 | echo -e "${COL}Downloading klipper\n${NC}" 20 | curl -o klipper.zip -L https://github.com/Klipper3d/klipper/archive/refs/heads/master.zip 21 | 22 | echo -e "${COL}Extracting klipper\n${NC}" 23 | unzip klipper.zip 24 | rm -rf klipper.zip 25 | mv klipper-master /klipper 26 | echo "# replace with your config" >> /root/printer.cfg 27 | 28 | mkdir -p /root/extensions/klipper 29 | cat << EOF > /root/extensions/klipper/manifest.json 30 | { 31 | "title": "Klipper plugin", 32 | "description": "Requires OctoKlipper plugin" 33 | } 34 | EOF 35 | 36 | cat << EOF > /root/extensions/klipper/start.sh 37 | #!/bin/sh 38 | python3 /klipper/klippy/klippy.py /root/printer.cfg -l /tmp/klippy.log -a /tmp/klippy_uds 39 | EOF 40 | 41 | cat << EOF > /root/extensions/klipper/kill.sh 42 | #!/bin/sh 43 | pkill -f 'klippy\.py' 44 | EOF 45 | chmod +x /root/extensions/klipper/start.sh 46 | chmod +x /root/extensions/klipper/kill.sh 47 | chmod 777 /root/extensions/klipper/start.sh 48 | chmod 777 /root/extensions/klipper/kill.sh 49 | 50 | cat << EOF 51 | ${COL} 52 | Klipper installed! 53 | Please place your own klipper config file at /root/printer.cfg 54 | Please kill the app and restart it again to see it in extension settings${NC} 55 | EOF 56 | -------------------------------------------------------------------------------- /app/app/src/main/res/xml/device_filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/repository/LoggerRepository.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.repository 2 | 3 | import android.graphics.Color 4 | import android.os.Looper 5 | import android.util.Log 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import kotlinx.coroutines.flow.SharedFlow 8 | import kotlinx.coroutines.runBlocking 9 | 10 | enum class LogType { 11 | SYSTEM, 12 | BOOTSTRAP, 13 | OCTOPRINT, 14 | EXTENSION, 15 | OTHER 16 | } 17 | 18 | data class LogEntry(val entry: String, val type: LogType = LogType.SYSTEM) 19 | 20 | fun LogType.getTypeEmoji(): String { 21 | return when (this) { 22 | LogType.BOOTSTRAP -> { 23 | "\uD83D\uDC38" 24 | } 25 | LogType.OCTOPRINT -> { 26 | "\uD83D\uDC19" 27 | } 28 | LogType.EXTENSION -> { 29 | "\uD83D\uDD0C" 30 | } 31 | LogType.SYSTEM -> { 32 | "\uD83D\uDCBB" 33 | } 34 | else ->"❓" 35 | } 36 | } 37 | 38 | interface LoggerRepository { 39 | val logHistoryFlow: SharedFlow 40 | fun log(obj: Any? = null, type: LogType = LogType.SYSTEM, getMessage: () -> String) 41 | } 42 | 43 | class LoggerRepositoryImpl: LoggerRepository { 44 | private var _logHistoryFlow = MutableSharedFlow(1000) 45 | 46 | override val logHistoryFlow: SharedFlow 47 | get() = _logHistoryFlow 48 | 49 | override fun log(obj: Any?, type: LogType, getMessage: () -> String) { 50 | var tag = "octo4a: " 51 | obj?.let { 52 | tag = obj::class.java.simpleName + ": " 53 | } 54 | 55 | Log.d(type.getTypeEmoji(), getMessage()) 56 | 57 | runBlocking { 58 | Looper.getMainLooper().run { 59 | _logHistoryFlow.emit(LogEntry(getMessage(), type)) 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/BootReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a 2 | 3 | import android.app.AlarmManager 4 | import android.app.PendingIntent 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.os.Build 9 | import android.util.Log 10 | import com.octo4a.ui.InitialActivity 11 | import com.octo4a.utils.preferences.MainPreferences 12 | 13 | 14 | class BootReceiver: BroadcastReceiver() { 15 | override fun onReceive(context: Context, intent: Intent) { 16 | val preferences = MainPreferences(context) 17 | 18 | if (preferences.startOnBoot) { 19 | restartApp(context) 20 | } 21 | } 22 | 23 | // I'm guessing android made it impossible to straight up startActivity at boot in newer sdk versions 24 | // BUT HEY OFC THERE'S A WEIRD WORKAROUND THAT BYPASSES IT ON STACKOVERFLOW 25 | private fun restartApp(context: Context) { 26 | try { 27 | val restartTime = (1000 * 5).toLong() 28 | val intents = context.packageManager.getLaunchIntentForPackage(context.packageName) 29 | val restartIntent = PendingIntent.getActivity(context, 0, intents, PendingIntent.FLAG_ONE_SHOT) 30 | val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 32 | mgr.setExactAndAllowWhileIdle( 33 | AlarmManager.RTC_WAKEUP, 34 | System.currentTimeMillis() + restartTime, 35 | restartIntent 36 | ) 37 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 38 | mgr.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + restartTime, restartIntent) 39 | } 40 | } catch (e: Exception) { 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/fragments/AboutFragment.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui.fragments 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.Fragment 10 | import com.octo4a.R 11 | import com.octo4a.repository.BootstrapRepository 12 | import kotlinx.android.synthetic.main.fragment_about.* 13 | import org.koin.android.ext.android.inject 14 | 15 | class AboutFragment : Fragment() { 16 | private val bootstrapRepository: BootstrapRepository by inject() 17 | 18 | override fun onCreateView( 19 | inflater: LayoutInflater, 20 | container: ViewGroup?, 21 | savedInstanceState: Bundle?): View? { 22 | return inflater.inflate(R.layout.fragment_about, container, false) 23 | } 24 | 25 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 26 | super.onViewCreated(view, savedInstanceState) 27 | val pInfo = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0) 28 | val version = pInfo.versionName 29 | val bootstrapVersion = bootstrapRepository.bootstrapVersion 30 | 31 | appVersionText.text = "octo4a build $version • $bootstrapVersion" 32 | 33 | joinTelegramButton.setOnClickListener { 34 | openWebsite("https://t.me/octo4achat") 35 | } 36 | 37 | donateButton.setOnClickListener { 38 | openWebsite("https://paypal.me/feelfreelinux") 39 | } 40 | 41 | appProjectButton.setOnClickListener { 42 | openWebsite("https://github.com/feelfreelinux/octo4a") 43 | } 44 | } 45 | 46 | fun openWebsite(url: String) { 47 | val i = Intent(Intent.ACTION_VIEW) 48 | i.data = Uri.parse(url) 49 | startActivity(i) 50 | } 51 | } -------------------------------------------------------------------------------- /app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/fragments/ExtensionsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui.fragments 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.fragment.app.Fragment 11 | import androidx.lifecycle.asLiveData 12 | import com.octo4a.R 13 | import com.octo4a.repository.ExtensionsRepository 14 | import com.octo4a.repository.OctoPrintHandlerRepository 15 | import com.octo4a.ui.views.ExtensionView 16 | import kotlinx.android.synthetic.main.fragment_extensions.* 17 | import org.koin.android.ext.android.inject 18 | 19 | class ExtensionsFragment : Fragment() { 20 | val octoPrintHandlerRepository: OctoPrintHandlerRepository by inject() 21 | val extensionsRepository: ExtensionsRepository by inject() 22 | 23 | override fun onCreateView( 24 | inflater: LayoutInflater, 25 | container: ViewGroup?, 26 | savedInstanceState: Bundle?): View? { 27 | return inflater.inflate(R.layout.fragment_extensions, container, false) 28 | } 29 | 30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 31 | super.onViewCreated(view, savedInstanceState) 32 | extensionsRepository.getValidExtensions() 33 | extensionsRepository.extensionsState.asLiveData().observe(viewLifecycleOwner) { 34 | extensionsList.removeAllViews() 35 | it.forEach { 36 | val extensionView = ExtensionView(requireContext(), extensionsRepository) 37 | extensionsList.addView(extensionView) 38 | extensionView.setExtensionDescription(it) 39 | } 40 | } 41 | 42 | extensionsCard.setOnClickListener { 43 | val i = Intent(Intent.ACTION_VIEW) 44 | i.data = Uri.parse("https://github.com/feelfreelinux/octo4a/wiki/Extensions-system") 45 | startActivity(i) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/app/src/main/cpp/ioctl-hook.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | int g_obvio=0; 15 | #define DPRINTF(format, args...) if (!g_obvio) { g_obvio=1; fprintf(stderr, format, ## args); g_obvio=0; } 16 | 17 | #ifndef RTLD_NEXT 18 | #define RTLD_NEXT ((void *)-1l) 19 | #endif 20 | 21 | #define REAL_LIBC RTLD_NEXT 22 | #define request_t int 23 | #define TIOCMBIS 0x5416 24 | 25 | static char fifoPath[] = "/home/octoprint/eventPipe"; 26 | static char eventJsonStart[] = "{\"eventType\": \""; 27 | static char eventJsonEnd[] = "\"}\n"; 28 | 29 | 30 | int writeEventToPipe(char* event, int len) { 31 | int eventFifoFd = open(fifoPath, O_WRONLY); 32 | write(eventFifoFd, eventJsonStart, sizeof(eventJsonStart)-1); 33 | write(eventFifoFd, event, len); 34 | write(eventFifoFd, eventJsonEnd, sizeof(eventJsonEnd)-1); 35 | close(eventFifoFd); 36 | } 37 | 38 | int ioctl(int fd, int request, ...) 39 | { 40 | static int (*funcIoctl)(int, request_t, void *) = NULL; 41 | va_list args; 42 | void *argp; 43 | 44 | if (!funcIoctl) 45 | funcIoctl = (int (*)(int, request_t, void *))dlsym(REAL_LIBC, "ioctl"); 46 | va_start(args, request); 47 | argp = va_arg(args, void *); 48 | va_end(args); 49 | 50 | if (request == TIOCMBIS) 51 | { 52 | writeEventToPipe("rtsDts", 6); 53 | return 0; 54 | } 55 | 56 | if (request == 0x802c542a || request == 0x402C542B) 57 | { 58 | writeEventToPipe("customBaud", 10); 59 | return 0; 60 | } 61 | //DPRINTF ("HOOK: ioctl (fd=%d, request=%p, argp=%p [%02X])\n", fd, request, argp); 62 | return funcIoctl(fd, request, argp); 63 | } 64 | 65 | int flock(int fd, int operation) { 66 | // DPRINTF ("HOOK: flock (fd=%d, operation=%p)\n", fd, operation); 67 | return 0; 68 | } -------------------------------------------------------------------------------- /app/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 19 | 27 | 28 | 36 | 37 | 38 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | You don't have a Raspberry Pi, but you want to control your 3D printer remotely? Use your phone as an octoprint host! With the Octo4a app you can install Octoprint on your android phone in minutes, without any special Linux knowledge. 2 | 3 | Usage 4 | 5 | - Enable installing 3rd-party .apk in your phone's settings. 6 | - Install the apk file downloaded from the releases page. 7 | - Open the app. 8 | - Click "Install OctoPrint" to download and install OctoPrint 9 | - Allow the app to access the storage, if asked for permission. 10 | - Wait for the installation to complete. This may take a long time, depending on your internet speed. 11 | - Click "Continue" when the installation finishes. 12 | - Optionally start the camera server to enable watching yur printer from octoprint. 13 | - Navigate to the IP address shown at the top in your browser to access and set-up OctoPrint. 14 | - Happy printing! 15 | 16 | Features 17 | - Quick and easy octoprint installation. 18 | - Printer connection via USB OTG. Thanks to our custom USB driver you can use octoprint even on phones without root access. 19 | - Built-in camera support. You can use the built-in camera in your phone to see the progress of your 3D prints, instead of buying a separate module. The app also supports octolapse. 20 | - SSH support. You can easily log-in via ssh and customize your octoprint installation. 21 | 22 | Wiki 23 | FAQ and many different topics are described in the project's wiki 24 | 25 | Contributing 26 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 27 | 28 | Disclaimer 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/build-app.yml: -------------------------------------------------------------------------------- 1 | name: Build app 2 | on: [pull_request, push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout the code 8 | uses: actions/checkout@v2 9 | - name: Setup ndk 10 | uses: nttld/setup-ndk@v1 11 | with: 12 | ndk-version: r21d 13 | - name: Build the app 14 | run: | 15 | ./gradlew assembleRelease 16 | working-directory: ./app 17 | - name: Setup build tool version variable 18 | shell: bash 19 | run: | 20 | BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) 21 | echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV 22 | echo Last build tool version is: $BUILD_TOOL_VERSION 23 | - uses: r0adkll/sign-android-release@v1 24 | name: Sign app APK 25 | id: sign_app 26 | with: 27 | releaseDirectory: app/app/build/outputs/apk/release 28 | signingKeyBase64: ${{ secrets.SIGNING_KEY }} 29 | alias: ${{ secrets.ALIAS }} 30 | keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} 31 | keyPassword: ${{ secrets.KEY_PASSWORD }} 32 | env: 33 | BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} 34 | - name: Upload artifacts 35 | uses: actions/upload-artifact@v2 36 | with: 37 | name: Signed app 38 | path: ${{steps.sign_app.outputs.signedReleaseFile}} 39 | - name: Get the version 40 | if: startsWith(github.ref, 'refs/tags/') 41 | id: get_version 42 | run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} 43 | - name: Move and name release 44 | if: startsWith(github.ref, 'refs/tags/') 45 | run: mv ${{steps.sign_app.outputs.signedReleaseFile}} octo4a-${{steps.get_version.outputs.VERSION}}.apk 46 | - name: Create github release 47 | uses: softprops/action-gh-release@v1 48 | if: startsWith(github.ref, 'refs/tags/') 49 | with: 50 | files: octo4a-${{steps.get_version.outputs.VERSION}}.apk 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/extension_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 22 | 23 | 35 | 36 | 40 | 48 | 49 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/view_installation_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 26 | 35 | 44 | 45 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/Octo4aWakeLock.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.net.wifi.WifiManager 8 | import android.os.Build 9 | import android.os.PowerManager 10 | import android.provider.Settings 11 | import android.widget.Toast 12 | import androidx.lifecycle.LifecycleService 13 | import com.octo4a.R 14 | import com.octo4a.repository.LoggerRepository 15 | import java.time.Duration 16 | 17 | class Octo4aWakeLock(val context: Context, val logger: LoggerRepository) { 18 | var wakeLock: PowerManager.WakeLock? = null 19 | var wifiLock: WifiManager.WifiLock? = null 20 | 21 | fun acquire() { 22 | if (wakeLock != null && wakeLock!!.isHeld && wifiLock != null && wifiLock!!.isHeld) { 23 | return 24 | } 25 | 26 | val pm = context.getSystemService(LifecycleService.POWER_SERVICE) as PowerManager 27 | wakeLock = 28 | pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "octo4a:service-wakelock") 29 | wakeLock?.acquire(10*60*1000L /*10 minutes*/) 30 | 31 | 32 | // http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak 33 | val wm = context.applicationContext.getSystemService(LifecycleService.WIFI_SERVICE) as WifiManager 34 | wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "octo4a:wifilock") 35 | wifiLock?.acquire() 36 | 37 | val packageName = context.packageName 38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !pm.isIgnoringBatteryOptimizations(packageName)) { 39 | val whitelist = Intent() 40 | whitelist.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 41 | whitelist.data = Uri.parse("package:$packageName") 42 | whitelist.flags = Intent.FLAG_ACTIVITY_NEW_TASK 43 | try { 44 | context.startActivity(whitelist) 45 | } catch (e: ActivityNotFoundException) { 46 | logger.log(this) { "failed to open battery optimization dialog" } 47 | } 48 | } 49 | } 50 | 51 | fun remove() { 52 | wakeLock?.release() 53 | wifiLock?.release() 54 | wakeLock = null 55 | wifiLock = null 56 | } 57 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a 2 | 3 | import com.google.gson.FieldNamingPolicy 4 | import com.octo4a.camera.CameraEnumerationRepository 5 | import com.octo4a.repository.* 6 | import com.octo4a.serial.VirtualSerialDriver 7 | import com.octo4a.utils.TLSSocketFactory 8 | import com.octo4a.utils.preferences.MainPreferences 9 | import com.octo4a.viewmodel.InstallationViewModel 10 | import com.octo4a.viewmodel.NetworkStatusViewModel 11 | import com.octo4a.viewmodel.StatusViewModel 12 | import io.ktor.client.* 13 | import io.ktor.client.engine.android.* 14 | import io.ktor.client.features.json.* 15 | import org.koin.android.ext.koin.androidApplication 16 | import org.koin.android.ext.koin.androidContext 17 | import org.koin.androidx.viewmodel.dsl.viewModel 18 | import org.koin.dsl.module 19 | 20 | val appModule = module { 21 | single { 22 | HttpClient(Android) { 23 | install(JsonFeature) { 24 | serializer = GsonSerializer { 25 | serializeNulls() 26 | setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 27 | } 28 | } 29 | engine { 30 | sslManager = { 31 | it.sslSocketFactory = TLSSocketFactory(androidContext()) 32 | } 33 | } 34 | } 35 | } 36 | 37 | factory { MainPreferences(androidContext()) } 38 | factory { GithubRepositoryImpl(get()) } 39 | 40 | single { BootstrapRepositoryImpl(get(), get(), androidContext()) } 41 | single { FIFOEventRepositoryImpl(get()) } 42 | single { VirtualSerialDriver(androidContext(), get(), get(), get()) } 43 | single { ExtensionsRepositoryImpl(androidContext(), get(), get(), get()) } 44 | single { LoggerRepositoryImpl() } 45 | single { OctoPrintHandlerRepositoryImpl(androidContext(), get(), get(), get(), get(), get(), get()) } 46 | single { CameraEnumerationRepository(androidApplication()) } 47 | 48 | viewModel { InstallationViewModel(androidApplication(), get(), get(), get()) } 49 | viewModel { StatusViewModel(androidApplication(), get(), get()) } 50 | viewModel { NetworkStatusViewModel(androidApplication(), get()) } 51 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/camera/MJpegServer.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.camera 2 | 3 | import android.util.Log 4 | import fi.iki.elonen.NanoHTTPD 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.runBlocking 9 | import java.io.BufferedOutputStream 10 | import java.io.ByteArrayInputStream 11 | import java.io.PipedInputStream 12 | import java.io.PipedOutputStream 13 | 14 | interface MJpegFrameProvider { 15 | data class FrameInfo(val image: ByteArray? = null, val id: Int) 16 | 17 | suspend fun takeSnapshot(): ByteArray 18 | fun getNewFrame(prevFrame: FrameInfo?): FrameInfo 19 | fun registerListener(): Boolean 20 | fun unregisterListener() 21 | } 22 | 23 | // Simple http server hosting mjpeg stream along with 24 | class MJpegServer(port: Int, private val frameProvider: MJpegFrameProvider): NanoHTTPD(port) { 25 | 26 | override fun serve(session: IHTTPSession?): Response { 27 | when (session?.uri) { 28 | "/snapshot" -> { 29 | var res: Response? = null 30 | kotlin.runCatching { 31 | runBlocking { 32 | val data = frameProvider?.takeSnapshot() 33 | val inputStream = ByteArrayInputStream(data) 34 | 35 | res = newFixedLengthResponse( 36 | Response.Status.OK, 37 | "image/jpeg", 38 | inputStream, 39 | data.size.toLong() 40 | ) 41 | } 42 | }.onFailure { 43 | } 44 | 45 | return res ?: newFixedLengthResponse( 46 | "

Failed to fetch image

" 47 | ) 48 | } 49 | "/mjpeg" -> { 50 | return MjpegResponse(frameProvider) 51 | } 52 | else -> return newFixedLengthResponse( 53 | "" 54 | + "

GET /snapshot

GET a current JPEG image.

" 55 | + "

GET /mjpeg

GET MJPEG frames.

" 56 | + "" 57 | ) 58 | } 59 | } 60 | 61 | fun startServer() { 62 | start() 63 | } 64 | 65 | fun stopServer() { 66 | stop() 67 | } 68 | } -------------------------------------------------------------------------------- /app/app/src/main/cpp/yuv2rgb.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace { 13 | 14 | void yuv420toNv21(int image_width, int image_height, const int8_t* y_buffer, 15 | const int8_t* u_buffer, const int8_t* v_buffer, int y_pixel_stride, 16 | int uv_pixel_stride, int y_row_stride, int uv_row_stride, 17 | int8_t *nv21) { 18 | for(int y = 0; y < image_height; ++y) { 19 | int destOffset = image_width * y; 20 | int yOffset = y * y_row_stride; 21 | memcpy(nv21 + destOffset, y_buffer + yOffset, image_width); 22 | } 23 | 24 | int idUV = image_width * image_height; 25 | int uv_width = image_width / 2; 26 | int uv_height = image_height / 2; 27 | for(int y = 0; y < uv_height; ++y) { 28 | int uvOffset = y * uv_row_stride; 29 | for (int x = 0; x < uv_width; ++x) { 30 | int bufferIndex = uvOffset + (x * uv_pixel_stride); 31 | // V channel. 32 | nv21[idUV++] = v_buffer[bufferIndex]; 33 | // U channel. 34 | nv21[idUV++] = u_buffer[bufferIndex]; 35 | } 36 | } 37 | } 38 | 39 | } // namespace 40 | 41 | extern "C" { 42 | 43 | jboolean Java_com_octo4a_camera_NativeCameraUtils_yuv420toNv21( 44 | JNIEnv *env, jclass clazz, 45 | jint image_width, jint image_height, jobject y_byte_buffer, 46 | jobject u_byte_buffer, jobject v_byte_buffer, jint y_pixel_stride, 47 | jint uv_pixel_stride, jint y_row_stride, jint uv_row_stride, 48 | jbyteArray nv21_array) { 49 | 50 | auto y_buffer = static_cast(env->GetDirectBufferAddress(y_byte_buffer)); 51 | auto u_buffer = static_cast(env->GetDirectBufferAddress(u_byte_buffer)); 52 | auto v_buffer = static_cast(env->GetDirectBufferAddress(v_byte_buffer)); 53 | 54 | jbyte* nv21 = env->GetByteArrayElements(nv21_array, nullptr); 55 | if (nv21 == nullptr || y_buffer == nullptr || u_buffer == nullptr 56 | || v_buffer == nullptr) { 57 | // Log this. 58 | return false; 59 | } 60 | 61 | yuv420toNv21(image_width, image_height, y_buffer, u_buffer, v_buffer, 62 | y_pixel_stride, uv_pixel_stride, y_row_stride, uv_row_stride, 63 | nv21); 64 | 65 | env->ReleaseByteArrayElements(nv21_array, nv21, 0); 66 | return true; 67 | } 68 | 69 | } // extern "C" -------------------------------------------------------------------------------- /app/app/src/main/res/layout/fragment_extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | 21 | 25 | 33 | 41 | 42 | 43 | 44 | 45 | 50 | 51 | -------------------------------------------------------------------------------- /app/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/camera/MjpegResponse.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.camera 2 | 3 | import fi.iki.elonen.NanoHTTPD 4 | import java.io.OutputStream 5 | import java.io.PrintWriter 6 | import java.text.SimpleDateFormat 7 | import java.util.* 8 | 9 | class MjpegResponse(private val frameProvider: MJpegFrameProvider) : 10 | NanoHTTPD.Response(Status.OK, "multipart/x-mixed-replace; boundary=--octo4a", null, 0) { 11 | override fun send(outputStream: OutputStream) { 12 | val registered = frameProvider.registerListener() 13 | kotlin 14 | .runCatching { 15 | if (registered) { 16 | sendStream(outputStream) 17 | } else { 18 | sendError(outputStream, 500, "Internal Server Error - Camera Not Initialized") 19 | } 20 | } 21 | .onFailure { outputStream.close() } 22 | .onSuccess { outputStream.close() } 23 | 24 | if (registered) { 25 | frameProvider.unregisterListener() 26 | } 27 | } 28 | 29 | private fun sendStream(outputStream: OutputStream) { 30 | val gmtFrmt = SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US) 31 | gmtFrmt.timeZone = TimeZone.getTimeZone("GMT") 32 | 33 | val pw = PrintWriter(outputStream) 34 | pw.apply { 35 | print("HTTP/1.1 ${status.description} \r\n") 36 | print("Date: ${gmtFrmt.format(Date())}\r\n") 37 | print("Connection: close\r\n") 38 | print( 39 | "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n") 40 | print("Expires: 0\r\n") 41 | print("Max-Age: 0\r\n") 42 | print("Pragma: no-cache\r\n") 43 | print("Content-Type: ${mimeType}\r\n") 44 | print("\r\n--octo4a\r\n") 45 | flush() 46 | var frameInfo: MJpegFrameProvider.FrameInfo? = null 47 | while (true) { 48 | frameInfo = frameProvider.getNewFrame(frameInfo) 49 | val image = frameInfo.image ?: ByteArray(0) 50 | if (image.isNotEmpty()) { 51 | write("Content-type: image/jpeg\r\n") 52 | write("Content-Length: ${image?.size}\r\n") 53 | write("\r\n") 54 | flush() 55 | outputStream.write(image, 0, image.size) 56 | outputStream.flush() 57 | print("\r\n--octo4a\r\n") 58 | flush() 59 | } 60 | } 61 | } 62 | } 63 | 64 | private fun sendError(outputStream: OutputStream, statusCode: Int, message: String) { 65 | val pw = PrintWriter(outputStream) 66 | pw.apply { 67 | print("HTTP/1.1 $statusCode $message\r\n") 68 | print("Content-Length: ${message.length}\r\n") 69 | print("\r\n$message") 70 | flush() 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/viewmodel/StatusViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.viewmodel 2 | 3 | import android.app.Application 4 | import android.content.pm.PackageInfo 5 | import android.content.pm.PackageManager 6 | import androidx.lifecycle.* 7 | import com.octo4a.Octo4aApplication 8 | import com.octo4a.repository.GithubRelease 9 | import com.octo4a.repository.GithubRepository 10 | import com.octo4a.repository.OctoPrintHandlerRepository 11 | import com.octo4a.utils.SemVer 12 | import com.octo4a.utils.ipAddress 13 | import com.octo4a.utils.withIO 14 | import kotlinx.coroutines.launch 15 | import java.lang.Exception 16 | 17 | 18 | class StatusViewModel(context: Application, 19 | private val octoPrintHandlerRepository: OctoPrintHandlerRepository, 20 | private val githubRepository: GithubRepository) : AndroidViewModel(context) { 21 | val serverStatus = octoPrintHandlerRepository.serverState.asLiveData() 22 | val usbStatus = octoPrintHandlerRepository.usbDeviceStatus.asLiveData() 23 | val cameraStatus = octoPrintHandlerRepository.cameraServerStatus.asLiveData() 24 | val updateAvailable = MutableLiveData() 25 | 26 | fun startServer() { 27 | octoPrintHandlerRepository.startOctoPrint() 28 | } 29 | 30 | fun checkUpdateAvailable() { 31 | viewModelScope.launch { 32 | withIO { 33 | try { 34 | val newestRelease = githubRepository.getNewestRelease("feelfreelinux/octo4a") 35 | 36 | val app = getApplication() 37 | val pInfo: PackageInfo = app.packageManager.getPackageInfo(app.packageName, 0) 38 | val version = pInfo.versionName 39 | if (SemVer.parse(version) < SemVer.parse(newestRelease.tagName)) { 40 | // New version available, check if is built already 41 | if (newestRelease.assets.any { it.name.contains("bootstrap") } 42 | && newestRelease.assets.any { it.name.contains(".apk") }) { 43 | updateAvailable.postValue(newestRelease) 44 | } 45 | } 46 | } catch (e: Exception) { 47 | e.printStackTrace() 48 | } 49 | } 50 | } 51 | } 52 | 53 | private fun getServerPort(): String { 54 | return octoPrintHandlerRepository.getConfigValue("server.port") 55 | } 56 | 57 | fun getServerAddress(): String { 58 | return "${getApplication().applicationContext.ipAddress}:5000" 59 | } 60 | 61 | fun stopServer() { 62 | octoPrintHandlerRepository.stopOctoPrint() 63 | } 64 | } -------------------------------------------------------------------------------- /app/app/src/main/res/layout/view_usb_devices_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 26 | 27 | 37 | 38 | 49 | 50 | 58 | 59 | 63 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/views/IPAddressView.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui.views 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.graphics.ColorFilter 7 | import android.os.Build 8 | import android.util.AttributeSet 9 | import android.view.LayoutInflater 10 | import android.widget.Toast 11 | import androidx.constraintlayout.widget.ConstraintLayout 12 | import com.octo4a.R 13 | import com.octo4a.viewmodel.IPAddress 14 | import com.octo4a.viewmodel.IPAddressType 15 | import kotlinx.android.synthetic.main.view_ip_address.view.* 16 | 17 | class IPAddressView@JvmOverloads 18 | constructor(private val ctx: Context, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0) 19 | : ConstraintLayout(ctx, attributeSet, defStyleAttr) { 20 | init { 21 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 22 | inflater.inflate(R.layout.view_ip_address, this) 23 | ipAddressView.setOnClickListener { 24 | // copy ipAddressTextView.text to clipboard 25 | val clipboard = 26 | ctx.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 27 | clipboard.primaryClip = ClipData.newPlainText("octo4a", ipAddressTextView.text) 28 | 29 | // Only show a toast for Android 12 and lower. 30 | if (Build.VERSION.SDK_INT <= 32) { // API 32 is Android 12 31 | Toast.makeText( 32 | context, 33 | ctx.getString(R.string.ip_address_copied_to_clipboard), 34 | Toast.LENGTH_SHORT 35 | ).show() 36 | 37 | } 38 | } 39 | } 40 | 41 | var ipAddress: IPAddress 42 | get() = IPAddress(IPAddressType.Wifi, "") 43 | set(value) { 44 | connectionTypeIcon.setImageResource( 45 | when (value.type) { 46 | IPAddressType.Wifi -> R.drawable.wifi 47 | IPAddressType.Cellular -> R.drawable.cellular 48 | IPAddressType.Ethernet -> R.drawable.ethernet 49 | IPAddressType.VPN -> R.drawable.vpn 50 | 51 | } 52 | ) 53 | connectionTypeIcon.imageAlpha = when (value.type) { 54 | IPAddressType.Cellular -> 100 55 | else -> 255 56 | } 57 | ipAddressTextView.setTypeface(null, when (value.type) { 58 | IPAddressType.Cellular -> android.graphics.Typeface.NORMAL 59 | else -> android.graphics.Typeface.BOLD 60 | }) 61 | ipAddressTextView.text = value.address + if (value.port != "") ":${value.port}" else "" 62 | } 63 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/serial/PrinterProber.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.serial 2 | 3 | import com.hoho.android.usbserial.driver.* 4 | 5 | 6 | // I should probably come up with a better way of handling those. 7 | // Maybe generation of VID / PID pairs from linux vid / pid db is an option? 8 | data class UsbDeviceId(val vendorId: Int, val productId: Int) 9 | 10 | val prolificDevices = listOf( 11 | UsbDeviceId(1659, 8963), 12 | ) 13 | 14 | val cdcDevices = listOf( 15 | // Prusa devices 16 | UsbDeviceId(11417, 1), 17 | UsbDeviceId(11417, 2), 18 | 19 | // Bunch of Arduinos 20 | UsbDeviceId(9025, 16), 21 | UsbDeviceId(9025, 54), 22 | UsbDeviceId(9025, 61), 23 | UsbDeviceId(9025, 62), 24 | UsbDeviceId(9025, 63), 25 | UsbDeviceId(9025, 66), 26 | UsbDeviceId(9025, 67), 27 | UsbDeviceId(9025, 68), 28 | UsbDeviceId(9025, 69), 29 | UsbDeviceId(9025, 73), 30 | UsbDeviceId(9025, 32822), 31 | UsbDeviceId(9025, 32824), 32 | UsbDeviceId(9025, 32825), 33 | 34 | // Teensy (who tf builds printers with those anyways?) 35 | UsbDeviceId(1155, 5824), 36 | 37 | // Atmel LUFA 38 | UsbDeviceId(1003, 8260), 39 | 40 | // Maple printer controllers 41 | UsbDeviceId(7855, 4), 42 | UsbDeviceId(7855, 41), 43 | 44 | // NXP Armbed 45 | UsbDeviceId(3368, 516), 46 | 47 | // Marlin CDC driver 48 | UsbDeviceId(7504, 24617), 49 | 50 | // Duet 2 WiFi 51 | UsbDeviceId(7504, 24812) 52 | ) 53 | 54 | val ftdiDevices = listOf( 55 | // Bunch of ftdi devices 56 | UsbDeviceId(1027, 24577), 57 | UsbDeviceId(1027, 24592), 58 | UsbDeviceId(1027, 24593), 59 | UsbDeviceId(1027, 24596), 60 | UsbDeviceId(1027, 24597) 61 | ) 62 | 63 | val ch341Devices = listOf( 64 | UsbDeviceId(6790, 29987),// CH341 65 | ) 66 | 67 | val cp21xxDevices = listOf( 68 | UsbDeviceId(4292, 60000), 69 | UsbDeviceId(4292, 60016), 70 | UsbDeviceId(4292, 60017), 71 | UsbDeviceId(4292, 60032), 72 | ) 73 | 74 | fun getCustomPrinterProber(): ProbeTable { 75 | val probeTable = ProbeTable() 76 | prolificDevices.forEach { 77 | probeTable.addProduct(it.vendorId, it.productId, ProlificSerialDriver::class.java) 78 | } 79 | 80 | cdcDevices.forEach { 81 | probeTable.addProduct(it.vendorId, it.productId, CdcAcmSerialDriver::class.java) 82 | } 83 | 84 | ftdiDevices.forEach { 85 | probeTable.addProduct(it.vendorId, it.productId, FtdiSerialDriver::class.java) 86 | } 87 | 88 | ch341Devices.forEach { 89 | probeTable.addProduct(it.vendorId, it.productId, Ch34xSerialDriver::class.java) 90 | } 91 | 92 | cp21xxDevices.forEach { 93 | probeTable.addProduct(it.vendorId, it.productId, Cp21xxSerialDriver::class.java) 94 | } 95 | 96 | return probeTable 97 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/views/InstallationProgressItem.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui.views 2 | 3 | import android.animation.LayoutTransition 4 | import android.content.Context 5 | import android.graphics.Typeface 6 | import android.util.AttributeSet 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import androidx.constraintlayout.widget.ConstraintLayout 10 | import androidx.core.view.isGone 11 | import com.octo4a.R 12 | import com.octo4a.repository.ServerStatus 13 | import com.octo4a.utils.animatedAlpha 14 | import com.octo4a.utils.getArchString 15 | import kotlinx.android.synthetic.main.view_installation_item.view.* 16 | 17 | class InstallationProgressItem @JvmOverloads 18 | constructor(private val ctx: Context, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0) 19 | : ConstraintLayout(ctx, attributeSet, defStyleAttr) { 20 | 21 | init { 22 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 23 | inflater.inflate(R.layout.view_installation_item, this) 24 | layoutTransition = LayoutTransition() 25 | } 26 | 27 | var status: ServerStatus 28 | get() = ServerStatus.Stopped 29 | set(value) { 30 | contentTextView.text = when (value) { 31 | ServerStatus.DownloadingBootstrap -> resources.getString(R.string.installation_step_downloading, getArchString()) 32 | ServerStatus.ExtractingBootstrap -> resources.getString(R.string.installation_step_extracting, getArchString()) 33 | ServerStatus.BootingUp -> resources.getString(R.string.installation_step_bootup) 34 | ServerStatus.Running -> resources.getString(R.string.installation_step_done) 35 | ServerStatus.InstallationError -> resources.getString(R.string.installation_error) 36 | else -> "Unknown status" 37 | } 38 | } 39 | 40 | var statusText: String 41 | get() = contentTextView.text.toString() 42 | set(value) { 43 | contentTextView.text = value 44 | } 45 | var isLoading: Boolean 46 | get() = false 47 | set(value) { 48 | spinnerView.isGone = !value 49 | doneIconView.isGone = value 50 | if (!value) { 51 | contentTextView.animatedAlpha = 0.4F 52 | contentTextView.typeface = Typeface.DEFAULT 53 | } else { 54 | contentTextView.animatedAlpha = 1F 55 | contentTextView.typeface = Typeface.DEFAULT_BOLD 56 | } 57 | } 58 | 59 | fun setUpcoming() { 60 | spinnerView.isGone = true 61 | doneIconView.isGone = true 62 | contentTextView.animatedAlpha = 0.4F 63 | contentTextView.typeface = Typeface.DEFAULT 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildozer/* 2 | bin/* 3 | app/build/* 4 | app/build 5 | app/.gradle 6 | app/.gradle/* 7 | app/app/src/main/jniLibs/* 8 | .gradle 9 | .DS_STORE 10 | *.class 11 | *.lock 12 | *.log 13 | *.pyc 14 | *.swp 15 | *.o 16 | .DS_Store 17 | .atom/ 18 | .buildlog/ 19 | .history 20 | .svn/ 21 | 22 | # IntelliJ related 23 | *.iml 24 | *.ipr 25 | *.iws 26 | .idea/ 27 | 28 | # Visual Studio Code related 29 | .classpath 30 | .project 31 | .settings/ 32 | .vscode/ 33 | 34 | # Flutter repo-specific 35 | /bin/cache/ 36 | /bin/mingit/ 37 | /dev/benchmarks/mega_gallery/ 38 | /dev/bots/.recipe_deps 39 | /dev/bots/android_tools/ 40 | /dev/docs/doc/ 41 | /dev/docs/flutter.docs.zip 42 | /dev/docs/lib/ 43 | /dev/docs/pubspec.yaml 44 | /dev/integration_tests/**/xcuserdata 45 | /dev/integration_tests/**/Pods 46 | /packages/flutter/coverage/ 47 | version 48 | 49 | # packages file containing multi-root paths 50 | .packages.generated 51 | 52 | # Flutter/Dart/Pub related 53 | **/doc/api/ 54 | .dart_tool/ 55 | .flutter-plugins 56 | .flutter-plugins-dependencies 57 | .packages 58 | .pub-cache/ 59 | .pub/ 60 | build/ 61 | flutter_*.png 62 | linked_*.ds 63 | unlinked.ds 64 | unlinked_spec.ds 65 | 66 | # Android related 67 | **/android/**/gradle-wrapper.jar 68 | **/android/.gradle 69 | **/android/captures/ 70 | **/android/gradlew 71 | **/android/gradlew.bat 72 | **/android/local.properties 73 | **/android/**/GeneratedPluginRegistrant.java 74 | **/android/key.properties 75 | *.jks 76 | 77 | # iOS/XCode related 78 | **/ios/**/*.mode1v3 79 | **/ios/**/*.mode2v3 80 | **/ios/**/*.moved-aside 81 | **/ios/**/*.pbxuser 82 | **/ios/**/*.perspectivev3 83 | **/ios/**/*sync/ 84 | **/ios/**/.sconsign.dblite 85 | **/ios/**/.tags* 86 | **/ios/**/.vagrant/ 87 | **/ios/**/DerivedData/ 88 | **/ios/**/Icon? 89 | **/ios/**/Pods/ 90 | **/ios/**/.symlinks/ 91 | **/ios/**/profile 92 | **/ios/**/xcuserdata 93 | **/ios/.generated/ 94 | **/ios/Flutter/App.framework 95 | **/ios/Flutter/Flutter.framework 96 | **/ios/Flutter/Flutter.podspec 97 | **/ios/Flutter/Generated.xcconfig 98 | **/ios/Flutter/app.flx 99 | **/ios/Flutter/app.zip 100 | **/ios/Flutter/flutter_assets/ 101 | **/ios/Flutter/flutter_export_environment.sh 102 | **/ios/ServiceDefinitions.json 103 | **/ios/Runner/GeneratedPluginRegistrant.* 104 | 105 | # macOS 106 | **/macos/Flutter/GeneratedPluginRegistrant.swift 107 | **/macos/Flutter/Flutter-Debug.xcconfig 108 | **/macos/Flutter/Flutter-Release.xcconfig 109 | **/macos/Flutter/Flutter-Profile.xcconfig 110 | 111 | # Coverage 112 | coverage/ 113 | 114 | # Exceptions to above rules. 115 | !**/ios/**/default.mode1v3 116 | !**/ios/**/default.mode2v3 117 | !**/ios/**/default.pbxuser 118 | !**/ios/**/default.perspectivev3 119 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 120 | !/dev/ci/**/Gemfile.lock 121 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/fragment_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 22 | 31 | 33 | 42 | 44 | 53 | 54 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/fragment_terminal_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 14 | 21 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 46 | 58 | 59 | 65 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/WebinterfaceActivity.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.ActivityManager 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.view.View 9 | import android.view.WindowManager 10 | import android.webkit.WebResourceRequest 11 | import android.webkit.WebView 12 | import android.webkit.WebViewClient 13 | import androidx.annotation.RequiresApi 14 | import androidx.appcompat.app.AppCompatActivity 15 | import com.octo4a.R 16 | import kotlinx.android.synthetic.main.activity_webinterface.* 17 | 18 | const val WEBINTERFACE_ADDRESS = "127.0.0.1:5000" 19 | 20 | class WebinterfaceActivity : AppCompatActivity() { 21 | 22 | @SuppressLint("SetJavaScriptEnabled") 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | setContentView(R.layout.activity_webinterface) 26 | webview.webViewClient = WebinterfaceClient(this) 27 | webview.settings.loadsImagesAutomatically = true 28 | webview.settings.javaScriptEnabled = true 29 | webview.settings.domStorageEnabled = true 30 | webview.settings.userAgentString = "TouchUI" 31 | webview.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY 32 | webview.loadUrl("http://$WEBINTERFACE_ADDRESS") 33 | } 34 | 35 | override fun onResume() { 36 | window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 37 | WindowManager.LayoutParams.FLAG_FULLSCREEN) 38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !isPinned()) { 39 | startLockTask() 40 | } 41 | super.onResume() 42 | } 43 | 44 | @Suppress("DEPRECATION") 45 | fun isPinned(): Boolean { 46 | val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager 47 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 48 | (activityManager.lockTaskModeState 49 | != ActivityManager.LOCK_TASK_MODE_NONE) 50 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ 51 | activityManager.isInLockTaskMode 52 | } else { 53 | false 54 | } 55 | } 56 | 57 | override fun onBackPressed() { 58 | if (!isPinned()){ 59 | super.onBackPressed() 60 | } else if (webview.canGoBack()) { 61 | webview.goBack() 62 | } 63 | } 64 | 65 | private fun webViewLoadingFinished() { 66 | webview.visibility = View.VISIBLE 67 | loadingIndicator.visibility = View.GONE 68 | } 69 | 70 | private class WebinterfaceClient(val activity: WebinterfaceActivity): WebViewClient() { 71 | override fun onPageFinished(view: WebView?, url: String?) { 72 | activity.webViewLoadingFinished() 73 | super.onPageFinished(view, url) 74 | } 75 | 76 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 77 | override fun shouldOverrideUrlLoading( 78 | view: WebView?, 79 | request: WebResourceRequest? 80 | ): Boolean { 81 | return if (request?.url?.authority == WEBINTERFACE_ADDRESS) { 82 | false 83 | } else { 84 | activity.stopLockTask() 85 | val intent = Intent(Intent.ACTION_VIEW, request?.url) 86 | activity.startActivity(intent) 87 | true 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/viewmodel/InstallationViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.viewmodel 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.asLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import com.octo4a.repository.BootstrapRepository 8 | import com.octo4a.repository.GithubRelease 9 | import com.octo4a.repository.GithubRepository 10 | import com.octo4a.repository.OctoPrintHandlerRepository 11 | import com.octo4a.utils.getArchString 12 | import com.octo4a.utils.withIO 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.launch 15 | 16 | data class BootstrapItem(val title: String, val bootstrapVersion: String, val assetUrl: String, val prerelease: Boolean, var recommended: Boolean = false) 17 | class InstallationViewModel(context: Application, octoPrintHandlerRepository: OctoPrintHandlerRepository, private val githubRepository: GithubRepository, private val bootstrapRepository: BootstrapRepository) : AndroidViewModel(context) { 18 | private var _bootstrapReleases = MutableStateFlow(listOf()) 19 | private var _selectedRelease = MutableStateFlow(null) 20 | 21 | val serverStatus = octoPrintHandlerRepository.serverState.asLiveData() 22 | val installErrorDescription = octoPrintHandlerRepository.installErrorDescription.asLiveData() 23 | val bootstrapReleases = _bootstrapReleases.asLiveData() 24 | val bootstrapDownloadProgress = bootstrapRepository.downloadProgressData 25 | 26 | fun fetchBootstrapReleases() { 27 | viewModelScope.launch { 28 | withIO { 29 | try { 30 | // Fetch releases from octo4a-bootstrap-builder 31 | val newestReleases = githubRepository.getNewestReleases("feelfreelinux/octo4a-bootstrap-builder").toMutableList() 32 | 33 | // Sort for newest releases 34 | newestReleases.sortByDescending { it.publishedAt } 35 | 36 | val arch = getArchString() 37 | 38 | val bootstrapItems = newestReleases.mapNotNull { 39 | // Get matching asset 40 | val asset = it.assets.firstOrNull { asset -> asset.name.contains(arch) } 41 | val nameSplit = it.name.split("-") 42 | 43 | // Filter out invalid releases 44 | if (nameSplit.size != 2 || (asset == null)) null 45 | else BootstrapItem("OctoPrint ${nameSplit[1]}", nameSplit.first().removePrefix("v"), asset.browserDownloadUrl, it.prerelease) 46 | } 47 | 48 | 49 | // Latest non-prerelease should be the recommended bootstrap 50 | val recommendedItem = bootstrapItems.firstOrNull { !it.prerelease } 51 | 52 | recommendedItem?.recommended = true 53 | _bootstrapReleases.value = bootstrapItems 54 | _selectedRelease.value = recommendedItem 55 | 56 | recommendedItem?.apply { 57 | bootstrapRepository.selectReleaseForInstallation(this) 58 | } 59 | } catch (e: Exception) { 60 | e.printStackTrace() 61 | } 62 | } 63 | } 64 | } 65 | 66 | fun selectBootstrapRelease(bootstrapItem: BootstrapItem) { 67 | _selectedRelease.value = bootstrapItem 68 | bootstrapRepository.selectReleaseForInstallation(bootstrapItem) 69 | } 70 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/ProcessUtils.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils 2 | 3 | import com.bugsnag.android.Bugsnag 4 | import com.octo4a.repository.LogType 5 | import com.octo4a.repository.LoggerRepository 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import java.time.Duration 10 | import java.util.Date 11 | 12 | suspend fun withIO(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block) 13 | 14 | fun Process.waitAndPrintOutput( 15 | logger: LoggerRepository, 16 | type: LogType = LogType.BOOTSTRAP 17 | ): String { 18 | var outputStr = "" 19 | inputStream.reader().forEachLine { 20 | logger.log(this, type) { it } 21 | outputStr += it 22 | } 23 | 24 | 25 | val exitCode = waitFor() 26 | if (exitCode != 0) { 27 | val uselessWarnings = arrayListOf( 28 | "proot warning: can't sanitize binding \"/data/data/com.octo4a/files/serialpipe\": No such file or directory", 29 | "WARNING: linker: ./root/bin/proot: unused DT entry:" 30 | ) 31 | val logLines = outputStr 32 | // replace useless proot warning 33 | .let { 34 | uselessWarnings.runningFold(it) { curr, valueToReplace -> 35 | curr.replace(valueToReplace, "") 36 | } 37 | } 38 | .toString() 39 | .lines() 40 | .filter { it.trim() != "" } 41 | .takeLast(2) 42 | .joinToString("\n") { it.take(50) } 43 | throw RuntimeException("Process exited with error code ${exitCode}. $logLines") 44 | } 45 | 46 | return outputStr 47 | } 48 | 49 | fun Process.getOutputAsString(): String { 50 | val log = StringBuilder() 51 | var line: String? 52 | while (inputStream.bufferedReader().readLine().also { line = it } != null) { 53 | log.append(line + "\n") 54 | } 55 | 56 | return log.toString() 57 | } 58 | 59 | fun Process.setPassword(password: String) { 60 | outputStream.bufferedWriter().apply { 61 | write("$password\n") 62 | flush() 63 | write("$password\n") 64 | flush() 65 | } 66 | } 67 | 68 | fun Process.isRunning(): Boolean { 69 | try { 70 | exitValue() 71 | } catch (_: Throwable) { 72 | return true 73 | } 74 | return false 75 | } 76 | 77 | /** 78 | * retryOperation will retry the operation until it succeeds or the maxRetries is reached 79 | * It does not retry if the operation takes less than minSeconds, to avoid retrying operations that fail instantly. 80 | * @param logger the logger to use 81 | * @param maxRetries the maximum number of retries 82 | * @param minSeconds the minimum time the operation needs to run to be retried 83 | */ 84 | fun retryOperation( 85 | logger: LoggerRepository, 86 | maxRetries: Int = 2, 87 | minSeconds: Int = 6, 88 | op: () -> Unit 89 | ) { 90 | var timesLeft = maxRetries 91 | 92 | while (true) { 93 | var started = Date() 94 | try { 95 | 96 | op() 97 | return 98 | } catch (e: java.lang.Exception) { 99 | timesLeft-- 100 | var now = Date() 101 | // Don't wanna use Duration.between because it's not available on API 24 102 | if (now.time - started.time < minSeconds * 1000) { 103 | throw e 104 | } 105 | if (timesLeft <= 0) { 106 | throw e 107 | } 108 | logger.log { "An error has occurred:$e" } 109 | logger.log { "Retries left: $timesLeft/$maxRetries" } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /app/app/src/main/res/layout/fragment_server.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | 20 | 24 | 32 | 40 | 41 | 42 | 43 | 44 | 53 | 55 | 64 | 66 | 68 | 69 | 78 | 79 | -------------------------------------------------------------------------------- /bootstrap-builder/dpkg_replacer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "strings" 13 | 14 | "github.com/blakesmith/ar" 15 | "github.com/ulikunitz/xz" 16 | ) 17 | 18 | var REPLACE_FROM = []byte("com.termux") 19 | var REPLACE_TO = []byte("com.octo4a") 20 | 21 | func ReplaceInReader(r io.Reader, w io.Writer) error { 22 | data, err := io.ReadAll(r) 23 | if err != nil { 24 | return fmt.Errorf("failed to read from reader: %w", err) 25 | } 26 | out := bytes.Replace(data, REPLACE_FROM, REPLACE_TO, -1) 27 | r2 := bytes.NewReader(out) 28 | n, err := io.Copy(w, r2) 29 | if err != nil { 30 | return fmt.Errorf("failed to write to writer: %w", err) 31 | } 32 | log.Printf("n == %v, len(out) == %v", n, len(out)) 33 | return nil 34 | } 35 | 36 | func ReplaceTarFile(inFile io.Reader, outFile io.Writer) error { 37 | tarFile := tar.NewReader(inFile) 38 | 39 | outTar := tar.NewWriter(outFile) 40 | for { 41 | header, err := tarFile.Next() 42 | if errors.Is(err, io.EOF) { 43 | break 44 | } 45 | if err != nil { 46 | return fmt.Errorf("failed to read from tar file: %w", err) 47 | } 48 | header.Name = strings.ReplaceAll(header.Name, string(REPLACE_FROM), string(REPLACE_TO)) 49 | if err := outTar.WriteHeader(header); err != nil { 50 | return fmt.Errorf("failed to write header to tar file: %w", err) 51 | } 52 | log.Printf("name == %v, headerSize = %v", header.Name, header.Size) 53 | if header.Size != 0 { 54 | if err := ReplaceInReader(tarFile, outTar); err != nil { 55 | return fmt.Errorf("failed to replace in reader in tar: %w", err) 56 | } 57 | } 58 | 59 | } 60 | 61 | outTar.Close() 62 | return nil 63 | } 64 | 65 | func ReplaceInArFile(inFile io.Reader, outFile io.Writer) error { 66 | arFile := ar.NewReader(inFile) 67 | 68 | outAr := ar.NewWriter(outFile) 69 | outAr.WriteGlobalHeader() 70 | for { 71 | header, err := arFile.Next() 72 | if errors.Is(err, io.EOF) { 73 | break 74 | } 75 | if err != nil { 76 | return fmt.Errorf("failed to read heade from ar file: %w", err) 77 | } 78 | 79 | buf := &bytes.Buffer{} 80 | if header.Name == "data.tar.xz/" { 81 | xzReader, err := xz.NewReader(arFile) 82 | if err != nil { 83 | return fmt.Errorf("failed to create xzReader: %w", err) 84 | } 85 | header.Name = "data.tar/" 86 | err = ReplaceTarFile(xzReader, buf) 87 | if err != nil { 88 | return fmt.Errorf("failed to replace xz compressed tar file: %w", err) 89 | } 90 | 91 | } else { 92 | log.Printf("Unknown file type %v, leaving as is", header.Name) 93 | _, err := io.Copy(buf, arFile) 94 | if err != nil { 95 | return fmt.Errorf("failed to copy file %v inside of ar archive: %w", header.Name, err) 96 | } 97 | } 98 | header.Size = int64(buf.Len()) 99 | err = outAr.WriteHeader(header) 100 | if err != nil { 101 | return fmt.Errorf("failed to write ar header: %w", err) 102 | } 103 | _, err = outAr.Write(buf.Bytes()) 104 | if err != nil { 105 | return fmt.Errorf("failed to write ar data: %w", err) 106 | } 107 | } 108 | return nil 109 | } 110 | 111 | func main() { 112 | fmt.Println("BRUH\n") 113 | flag.Parse() 114 | if len(flag.Args()) < 1 { 115 | log.Fatal("missing file") 116 | } 117 | fmt.Println("BRUH\n") 118 | f, err := os.Open(flag.Args()[0]) 119 | if err != nil { 120 | log.Fatal("failed to open input .deb file: %v", err) 121 | } 122 | defer f.Close() 123 | outFile, err := os.Create(flag.Args()[0] + ".replaced") 124 | if err != nil { 125 | log.Fatal("failed to open output .deb file: %v", err) 126 | } 127 | defer outFile.Close() 128 | if err := ReplaceInArFile(f, outFile); err != nil { 129 | log.Fatalf("failed to replace in ar file: %v", err) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /scripts/comm-fix.py: -------------------------------------------------------------------------------- 1 | import octoprint.plugin 2 | 3 | from octoprint.util.pip import create_pip_caller 4 | from octoprint.util.version import ( get_comparable_version, is_octoprint_compatible ) 5 | 6 | # Imports from comm.py 7 | import contextlib 8 | import copy 9 | import fnmatch 10 | import glob 11 | import logging 12 | import os 13 | import queue 14 | import re 15 | import threading 16 | import time 17 | from collections import deque 18 | 19 | import serial 20 | import wrapt 21 | import octoprint.util.version 22 | import octoprint.plugin 23 | from octoprint.events import Events, eventManager 24 | from octoprint.filemanager import valid_file_type 25 | from octoprint.filemanager.destinations import FileDestinations 26 | from octoprint.settings import settings 27 | from octoprint.systemcommands import system_command_manager 28 | from octoprint.util import ( 29 | CountedEvent, 30 | PrependableQueue, 31 | RepeatedTimer, 32 | ResettableTimer, 33 | TypeAlreadyInQueue, 34 | TypedQueue, 35 | chunks, 36 | filter_non_ascii, 37 | filter_non_utf8, 38 | get_bom, 39 | get_exception_string, 40 | sanitize_ascii, 41 | to_unicode, 42 | comm 43 | ) 44 | 45 | from octoprint.util.platform import get_os, set_close_exec 46 | 47 | _logger = logging.getLogger(__name__) 48 | 49 | class Octo4a18Fix(octoprint.plugin.StartupPlugin): 50 | def on_startup(self, host, port): 51 | self._logger.info("Monkey patching comm.py") 52 | def patchedSerialList(): 53 | if os.name == "nt": 54 | candidates = [] 55 | try: 56 | key = winreg.OpenKey( 57 | winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM" 58 | ) 59 | i = 0 60 | while True: 61 | candidates += [winreg.EnumValue(key, i)[1]] 62 | i += 1 63 | except Exception: 64 | pass 65 | 66 | else: 67 | candidates = [] 68 | try: 69 | with os.scandir("/dev") as it: 70 | for entry in it: 71 | if regex_serial_devices.match(entry.name): 72 | candidates.append(entry.path) 73 | except Exception: 74 | pass 75 | 76 | # additional ports 77 | additionalPorts = settings().get(["serial", "additionalPorts"]) 78 | if additionalPorts: 79 | for additional in additionalPorts: 80 | candidates += glob.glob(additional) 81 | 82 | hooks = octoprint.plugin.plugin_manager().get_hooks( 83 | "octoprint.comm.transport.serial.additional_port_names" 84 | ) 85 | for name, hook in hooks.items(): 86 | try: 87 | candidates += hook(candidates) 88 | except Exception: 89 | logging.getLogger(__name__).exception( 90 | "Error while retrieving additional " 91 | "serial port names from hook {}".format(name) 92 | ) 93 | 94 | # blacklisted ports 95 | blacklistedPorts = settings().get(["serial", "blacklistedPorts"]) 96 | if blacklistedPorts: 97 | for pattern in settings().get(["serial", "blacklistedPorts"]): 98 | candidates = list( 99 | filter(lambda x: not fnmatch.fnmatch(x, pattern), candidates) 100 | ) 101 | 102 | # last used port = first to try, move to start 103 | prev = settings().get(["serial", "port"]) 104 | if prev in candidates: 105 | candidates.remove(prev) 106 | candidates.insert(0, prev) 107 | 108 | return candidates 109 | 110 | comm.serialList = patchedSerialList 111 | 112 | __plugin_pythoncompat__ = ">=3.0,<4" 113 | __plugin_implementation__ = Octo4a18Fix() 114 | def __plugin_check__(): 115 | return is_octoprint_compatible(">=1.8.0") -------------------------------------------------------------------------------- /app/app/src/main/res/layout/activity_landing.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 19 | 30 | 34 | 42 | 50 | 56 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 87 | 88 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/camera/NativeCameraUtils.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.camera 2 | 3 | import android.media.Image 4 | import android.util.Log 5 | import androidx.camera.core.ImageProxy 6 | import java.nio.ByteBuffer 7 | import java.nio.ReadOnlyBufferException 8 | import kotlin.experimental.inv 9 | 10 | class NativeCameraUtils { 11 | init { 12 | System.loadLibrary("yuv2rgb") 13 | } 14 | 15 | external fun yuv420toNv21( 16 | imageWidth: Int, 17 | imageHeight: Int, 18 | yByteBuffer: ByteBuffer?, 19 | uByteBuffer: ByteBuffer?, 20 | vByteBuffer: ByteBuffer?, 21 | yPixelStride: Int, 22 | uvPixelStride: Int, 23 | yRowStride: Int, 24 | uvRowStride: Int, 25 | nv21Output: ByteArray? 26 | ): Boolean 27 | 28 | fun toNv21(image: ImageProxy): ByteArray? { 29 | val nv21 = ByteArray((image.width * image.height * 1.5f).toInt()) 30 | return if (!yuv420toNv21( 31 | image.width, 32 | image.height, 33 | image.planes[0].buffer, // Y buffer 34 | image.planes[1].buffer, // U buffer 35 | image.planes[2].buffer, // V buffer 36 | image.planes[0].pixelStride, // Y pixel stride 37 | image.planes[1].pixelStride, // U/V pixel stride 38 | image.planes[0].rowStride, // Y row stride 39 | image.planes[1].rowStride, // U/V row stride 40 | nv21 41 | ) 42 | ) { 43 | null 44 | } else nv21 45 | } 46 | 47 | 48 | fun yuvToNv21Slow(image: ImageProxy): ByteArray { 49 | val width = image.width 50 | val height = image.height 51 | val ySize = width * height 52 | val uvSize = width * height / 4 53 | val nv21 = ByteArray(ySize + uvSize * 2) 54 | val yBuffer = image.planes[0].buffer // Y 55 | val uBuffer = image.planes[1].buffer // U 56 | val vBuffer = image.planes[2].buffer // V 57 | var rowStride = image.planes[0].rowStride 58 | assert(image.planes[0].pixelStride == 1) 59 | var pos = 0 60 | if (rowStride == width) { // likely 61 | yBuffer[nv21, 0, ySize] 62 | pos += ySize 63 | } else { 64 | var yBufferPos = -rowStride.toLong() // not an actual position 65 | while (pos < ySize) { 66 | yBufferPos += rowStride.toLong() 67 | yBuffer.position(yBufferPos.toInt()) 68 | yBuffer[nv21, pos, width] 69 | pos += width 70 | } 71 | } 72 | rowStride = image.planes[2].rowStride 73 | val pixelStride = image.planes[2].pixelStride 74 | assert(rowStride == image.planes[1].rowStride) 75 | assert(pixelStride == image.planes[1].pixelStride) 76 | if (pixelStride == 2 && rowStride == width && uBuffer[0] == vBuffer[1]) { 77 | // maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0] 78 | val savePixel = vBuffer[1] 79 | try { 80 | vBuffer.put(1, savePixel.inv() as Byte) 81 | if (uBuffer[0] == savePixel.inv() as Byte) { 82 | vBuffer.put(1, savePixel) 83 | vBuffer.position(0) 84 | uBuffer.position(0) 85 | vBuffer[nv21, ySize, 1] 86 | uBuffer[nv21, ySize + 1, uBuffer.remaining()] 87 | return nv21 // shortcut 88 | } 89 | } catch (ex: ReadOnlyBufferException) { 90 | // unfortunately, we cannot check if vBuffer and uBuffer overlap 91 | } 92 | 93 | // unfortunately, the check failed. We must save U and V pixel by pixel 94 | vBuffer.put(1, savePixel) 95 | } 96 | 97 | // other optimizations could check if (pixelStride == 1) or (pixelStride == 2), 98 | // but performance gain would be less significant 99 | for (row in 0 until height / 2) { 100 | for (col in 0 until width / 2) { 101 | val vuPos = col * pixelStride + row * rowStride 102 | nv21[pos++] = vBuffer[vuPos] 103 | nv21[pos++] = uBuffer[vuPos] 104 | } 105 | } 106 | return nv21 107 | } 108 | 109 | 110 | } 111 | 112 | 113 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/preferences/Preferences.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils.preferences 2 | import android.content.Context 3 | import android.content.SharedPreferences 4 | import android.preference.PreferenceManager 5 | import kotlin.reflect.KProperty 6 | 7 | abstract class Preferences(var context: Context? = null, useDefaultFile: Boolean = false) { 8 | 9 | private val prefs: SharedPreferences by lazy { 10 | if (context != null && useDefaultFile) 11 | PreferenceManager.getDefaultSharedPreferences(context) 12 | else if (context != null) 13 | context!!.getSharedPreferences(javaClass.simpleName, Context.MODE_PRIVATE) 14 | else 15 | throw IllegalStateException("Context was not initialized. Call Preferences.init(context) before using it") 16 | } 17 | 18 | private val listeners = mutableListOf() 19 | 20 | abstract class PrefDelegate(val prefKey: String?) { 21 | abstract operator fun getValue(thisRef: Any?, property: KProperty<*>): T 22 | abstract operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) 23 | } 24 | 25 | interface SharedPrefsListener { 26 | fun onSharedPrefChanged(property: KProperty<*>) 27 | } 28 | 29 | fun stringPref(prefKey: String? = null, defaultValue: String? = null) = StringPrefDelegate(prefKey, defaultValue) 30 | 31 | inner class StringPrefDelegate(prefKey: String? = null, val defaultValue: String?) : PrefDelegate(prefKey) { 32 | override fun getValue(thisRef: Any?, property: KProperty<*>): String? = prefs.getString(prefKey ?: property.name, defaultValue) 33 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { 34 | prefs.edit().putString(prefKey ?: property.name, value).apply() 35 | onPrefChanged(property) 36 | } 37 | } 38 | 39 | fun intPref(prefKey: String? = null, defaultValue: Int = 0) = IntPrefDelegate(prefKey, defaultValue) 40 | 41 | inner class IntPrefDelegate(prefKey: String? = null, val defaultValue: Int) : PrefDelegate(prefKey) { 42 | override fun getValue(thisRef: Any?, property: KProperty<*>) = prefs.getInt(prefKey ?: property.name, defaultValue) 43 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { 44 | prefs.edit().putInt(prefKey ?: property.name, value).apply() 45 | onPrefChanged(property) 46 | } 47 | } 48 | 49 | fun floatPref(prefKey: String? = null, defaultValue: Float = 0.0f) = FloatPrefDelegate(prefKey, defaultValue) 50 | 51 | inner class FloatPrefDelegate(prefKey: String? = null, val defaultValue: Float) : PrefDelegate(prefKey) { 52 | override fun getValue(thisRef: Any?, property: KProperty<*>) = prefs.getFloat(prefKey ?: property.name, defaultValue) 53 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Float) { 54 | prefs.edit().putFloat(prefKey ?: property.name, value).apply() 55 | onPrefChanged(property) 56 | } 57 | } 58 | 59 | fun booleanPref(prefKey: String? = null, defaultValue: Boolean = false) = BooleanPrefDelegate(prefKey, defaultValue) 60 | 61 | inner class BooleanPrefDelegate(prefKey: String? = null, val defaultValue: Boolean) : PrefDelegate(prefKey) { 62 | override fun getValue(thisRef: Any?, property: KProperty<*>) = prefs.getBoolean(prefKey ?: property.name, defaultValue) 63 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { 64 | prefs.edit().putBoolean(prefKey ?: property.name, value).apply() 65 | onPrefChanged(property) 66 | } 67 | } 68 | 69 | fun stringSetPref(prefKey: String? = null, defaultValue: Set = HashSet()) = StringSetPrefDelegate(prefKey, defaultValue) 70 | 71 | inner class StringSetPrefDelegate(prefKey: String? = null, val defaultValue: Set) : PrefDelegate>(prefKey) { 72 | override fun getValue(thisRef: Any?, property: KProperty<*>): Set = prefs.getStringSet(prefKey ?: property.name, defaultValue)!! 73 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) { 74 | prefs.edit().putStringSet(prefKey ?: property.name, value).apply() 75 | onPrefChanged(property) 76 | } 77 | } 78 | 79 | private fun onPrefChanged(property: KProperty<*>) { 80 | listeners.forEach { it.onSharedPrefChanged(property) } 81 | } 82 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/camera/RotateUtils.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.camera 2 | 3 | 4 | class RotateUtils { 5 | companion object { 6 | fun rotate270(nv21_data: ByteArray, width: Int, height: Int): ByteArray? { 7 | try { 8 | val y_size = width * height 9 | val buffser_size = y_size * 3 / 2 10 | val nv21_rotated = ByteArray(buffser_size) 11 | var i = 0 12 | 13 | // Rotate the Y luma 14 | for (x in width - 1 downTo 0) { 15 | var offset = 0 16 | for (y in 0 until height) { 17 | nv21_rotated[i] = nv21_data[offset + x] 18 | i++ 19 | offset += width 20 | } 21 | } 22 | 23 | // Rotate the U and V color components 24 | i = y_size 25 | var x = width - 1 26 | while (x > 0) { 27 | var offset = y_size 28 | for (y in 0 until height / 2) { 29 | nv21_rotated[i] = nv21_data[offset + (x - 1)] 30 | i++ 31 | nv21_rotated[i] = nv21_data[offset + x] 32 | i++ 33 | offset += width 34 | } 35 | x = x - 2 36 | } 37 | return nv21_rotated 38 | } catch (e: Exception) { 39 | } 40 | return null 41 | } 42 | 43 | /** 44 | * 旋转180度 45 | */ 46 | fun rotate180(nv21_data: ByteArray, width: Int, height: Int): ByteArray? { 47 | try { 48 | val y_size = width * height 49 | val buffser_size = y_size * 3 / 2 50 | val nv21_rotated = ByteArray(buffser_size) 51 | var i = 0 52 | var count = 0 53 | i = y_size - 1 54 | while (i >= 0) { 55 | nv21_rotated[count] = nv21_data[i] 56 | count++ 57 | i-- 58 | } 59 | i = buffser_size - 1 60 | while (i >= y_size) { 61 | nv21_rotated[count++] = nv21_data[i - 1] 62 | nv21_rotated[count++] = nv21_data[i] 63 | i -= 2 64 | } 65 | return nv21_rotated 66 | } catch (e: Exception) { 67 | } 68 | return null 69 | } 70 | 71 | /** 72 | * 旋转90度 73 | */ 74 | fun rotate90(nv21_data: ByteArray, width: Int, height: Int): ByteArray? { 75 | try { 76 | val y_size = width * height 77 | val buffser_size = y_size * 3 / 2 78 | val nv21_rotated = ByteArray(buffser_size) 79 | 80 | // Rotate the Y luma 81 | var i = 0 82 | val startPos = (height - 1) * width 83 | for (x in 0 until width) { 84 | var offset = startPos 85 | for (y in height - 1 downTo 0) { 86 | nv21_rotated[i] = nv21_data[offset + x] 87 | i++ 88 | offset -= width 89 | } 90 | } 91 | // Rotate the U and V color components 92 | i = buffser_size - 1 93 | var x = width - 1 94 | while (x > 0) { 95 | var offset = y_size 96 | for (y in 0 until height / 2) { 97 | nv21_rotated[i] = nv21_data[offset + x] 98 | i-- 99 | nv21_rotated[i] = nv21_data[offset + (x - 1)] 100 | i-- 101 | offset += width 102 | } 103 | x = x - 2 104 | } 105 | return nv21_rotated 106 | } catch (e: Exception) { 107 | } 108 | return null 109 | } 110 | 111 | 112 | fun rotate(bytes: ByteArray, width: Int, height: Int, rotateDegree: Int): ByteArray? { 113 | return if (rotateDegree == 270) { 114 | rotate270(bytes, width, height) 115 | } else if (rotateDegree == 180) { 116 | rotate180(bytes, width, height) 117 | 118 | } else { 119 | rotate90(bytes, width, height) 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /app/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.utils 2 | 3 | import android.app.Activity 4 | import android.app.ActivityManager 5 | import android.content.Context 6 | import android.graphics.ImageFormat 7 | import android.graphics.Rect 8 | import android.graphics.YuvImage 9 | import android.net.wifi.WifiManager 10 | import android.os.Build 11 | import android.text.format.Formatter 12 | import android.util.TypedValue 13 | import android.widget.Toast 14 | import androidx.annotation.AttrRes 15 | import androidx.annotation.ColorInt 16 | import androidx.annotation.RequiresApi 17 | import androidx.camera.core.ImageProxy 18 | import java.io.ByteArrayOutputStream 19 | import java.nio.ByteBuffer 20 | 21 | 22 | fun Int.isBitSet(bit: Int): Boolean { 23 | return this and (1 shl bit) != 0 24 | } 25 | 26 | @ColorInt 27 | fun Context.getColorFromAttr( 28 | @AttrRes attrColor: Int, 29 | typedValue: TypedValue = TypedValue(), 30 | resolveRefs: Boolean = true 31 | ): Int { 32 | theme.resolveAttribute(attrColor, typedValue, resolveRefs) 33 | return typedValue.data 34 | } 35 | 36 | fun Context.isServiceRunning(service: Class<*>): Boolean { 37 | val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 38 | manager.getRunningServices(Integer.MAX_VALUE).forEach { 39 | if (service.name.equals(it.service.className)) { 40 | return true 41 | } 42 | } 43 | 44 | return false 45 | } 46 | 47 | 48 | fun ByteArray.NV21toJPEG( width: Int, height: Int, quality: Int): ByteArray? { 49 | val out = ByteArrayOutputStream() 50 | val yuv = YuvImage(this, ImageFormat.NV21, width, height, null) 51 | yuv.compressToJpeg(Rect(0, 0, width, height), quality, out) 52 | return out.toByteArray() 53 | } 54 | 55 | 56 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 57 | fun ImageProxy.YUV420toNV21(): ByteArray { 58 | val width: Int = cropRect.width() 59 | val height: Int = cropRect.height() 60 | val data = ByteArray(width * height * ImageFormat.getBitsPerPixel(format) / 8) 61 | val rowData = ByteArray(planes[0].rowStride) 62 | var channelOffset = 0 63 | var outputStride = 1 64 | for (i in planes.indices) { 65 | when (i) { 66 | 0 -> { 67 | channelOffset = 0 68 | outputStride = 1 69 | } 70 | 1 -> { 71 | channelOffset = width * height + 1 72 | outputStride = 2 73 | } 74 | 2 -> { 75 | channelOffset = width * height 76 | outputStride = 2 77 | } 78 | } 79 | val buffer: ByteBuffer = planes[i].buffer 80 | val rowStride: Int = planes[i].rowStride 81 | val pixelStride: Int = planes[i].pixelStride 82 | val shift = if (i == 0) 0 else 1 83 | val w = width shr shift 84 | val h = height shr shift 85 | buffer.position(rowStride * (cropRect.top shr shift) + pixelStride * (cropRect.left shr shift)) 86 | for (row in 0 until h) { 87 | var length: Int 88 | if (pixelStride == 1 && outputStride == 1) { 89 | length = w 90 | buffer.get(data, channelOffset, length) 91 | channelOffset += length 92 | } else { 93 | length = (w - 1) * pixelStride + 1 94 | buffer.get(rowData, 0, length) 95 | for (col in 0 until w) { 96 | data[channelOffset] = rowData[col * pixelStride] 97 | channelOffset += outputStride 98 | } 99 | } 100 | if (row < h - 1) { 101 | buffer.position(buffer.position() + rowStride - length) 102 | } 103 | } 104 | } 105 | return data 106 | } 107 | 108 | private fun is64Bit(): Boolean { 109 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 110 | (Build.SUPPORTED_64_BIT_ABIS != null && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) 111 | } else { 112 | !(Build.CPU_ABI != "x86" && Build.CPU_ABI2 != "x86") 113 | } 114 | } 115 | 116 | fun getArchString(): String { 117 | var arch = System.getProperty("os.arch")!!.toString() 118 | 119 | if (arch != "x86_64" && arch != "i686") { 120 | arch = if (is64Bit()) { 121 | "aarch64" 122 | } else { 123 | "arm" 124 | } 125 | } 126 | return arch 127 | } 128 | 129 | val Context.ipAddress: String 130 | get() { 131 | val wm = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager 132 | return Formatter.formatIpAddress(wm.connectionInfo.ipAddress) 133 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # octo4a - Run OctoPrint on Android 2 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/feelfreelinux) 3 | ![GitHub Sponsors](https://img.shields.io/github/sponsors/feelfreelinux) 4 | ![IzzySoft](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/com.octo4a) 5 | 6 | 7 | ![A banner visually expaining how the app works](.github/readme-banner.png) 8 | 9 | You don't have a Raspberry Pi, but you want to control your 3D printer remotely? Use your phone as an octoprint host! With the Octo4a app you can install Octoprint on your android phone in minutes, without any special Linux knowledge. 10 | 11 | ## Important note about charging and using OTG 12 | 13 | Some phones do not support simultaneous charging and USB on the go (OTG). Please consult this community contributed list of phones our users use, If you wish to buy a phone for the purpose of running octo4a: 14 | 15 | https://docs.google.com/spreadsheets/d/1pbKAELzvTrMVm6NpDpK8TfQR33w3QHQEgqkcGdbPFI0/edit?usp=sharing 16 | 17 | If you have a phone that is not on the list, please add it and let us know if it works or not: 18 | 19 | https://forms.gle/cnTtdDqG3UcwTgnW9 20 | 21 | ## Download 22 | 23 | Newest `.apk` files are available in the [releases page](https://github.com/feelfreelinux/octo4a/releases). 24 | 25 | ## Usage 26 | 27 | 1. Enable installing 3rd-party .apk in your phone's settings. 28 | 2. Install the apk file downloaded from the releases page. 29 | 3. Open the app. 30 | 4. Click "Install OctoPrint" to download and install OctoPrint (needs working connection to internet & querries Google DNS servers 8.8.8.8 and 8.8.4.4) 31 | 5. Allow the app to access the storage, if asked for permission. 32 | 6. Wait for the installation to complete. This may take a long time, depending on your internet speed. 33 | 7. Click "Continue" when the installation finishes. 34 | 8. Optionally start the camera server to enable watching your printer from octoprint. 35 | 9. Navigate to the IP address shown at the top in your browser to access and set-up OctoPrint. 36 | 10. Happy printing! 37 | 38 | ## Features 39 | 40 | - Quick and easy octoprint installation. 41 | - Printer connection via USB OTG. Thanks to our custom USB driver you can use octoprint even on phones without root access. 42 | - Built-in camera support. You can use the built-in camera in your phone to see the progress of your 3D prints, instead of buying a separate module. The app also supports octolapse. 43 | - SSH support. You can easily log-in via ssh and customize your octoprint installation. 44 | 45 | ## Wiki 46 | 47 | FAQ and many different topics are described in the [project's wiki](https://github.com/feelfreelinux/octo4a/wiki) 48 | 49 | ## Video tutorials 50 | 51 | Here are some video tutorials made by the octo4a community that will help you with setting everything up: 52 | 53 | - ["Octo4A - Octoprint On Your Android Phone - 2022 - Chris's Basement" by Chris Riley](https://www.youtube.com/watch?v=2Psbwc-NFTU) 54 | 55 | [![Video tutorial by Chris Riley - thumbnail](https://img.youtube.com/vi/2Psbwc-NFTU/0.jpg)](https://www.youtube.com/watch?v=2Psbwc-NFTU) 56 | - ["How to run OctoPrint on your phone!" by Thomas Sanladerer](https://www.youtube.com/watch?v=74xdib_-X38) 57 | 58 | [![Video tutorial by Thomas Sanladerer - thumbnail](https://img.youtube.com/vi/74xdib_-X38/0.jpg)](https://www.youtube.com/watch?v=74xdib_-X38) 59 | - ["Running OctoPrint on Android with Octo4a!" by Sumit Basra](https://www.youtube.com/watch?v=X56itQuqXY4) 60 | 61 | [![Video tutorial by Sumit Basra - thumbnail](https://img.youtube.com/vi/X56itQuqXY4/0.jpg)](https://www.youtube.com/watch?v=X56itQuqXY4) 62 | 63 | 64 | ## Contributing 65 | 66 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 67 | 68 | ## Donating 69 | 70 | If you like this project, feel free to [donate on PayPal](https://paypal.me/feelfreelinux) or [sponsor me on GitHub](https://github.com/sponsors/feelfreelinux). This project heavily relies on great work done on [OctoPrint](https://www.patreon.com/foosel) so please consider donating to them if you like this app. Thank you for your support :) 71 | 72 | ## Disclaimer 73 | 74 | ``` 75 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 76 | ``` 77 | 78 | TL;DR: If your house burns down because this app malfunctioned, you cannot sue me. 79 | 80 | 81 | ## License 82 | 83 | This project is licensed under the AGPL license. 84 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/view_status_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 17 | 18 | 25 | 26 | 27 | 38 | 39 | 53 | 54 | 66 | 67 | 76 | 77 | 86 | 87 | 100 | 101 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/fragments/TerminalSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui.fragments 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.DialogInterface 6 | import android.content.Intent 7 | import android.content.res.Resources 8 | import android.graphics.Color 9 | import android.os.Bundle 10 | import android.os.Handler 11 | import android.os.Looper 12 | import android.text.Spannable 13 | import android.text.SpannableString 14 | import android.text.TextUtils 15 | import android.text.style.ForegroundColorSpan 16 | import android.util.Log 17 | import android.view.LayoutInflater 18 | import android.view.View 19 | import android.view.ViewGroup 20 | import android.widget.FrameLayout 21 | import android.widget.TextView 22 | import androidx.lifecycle.asLiveData 23 | import com.google.android.material.bottomsheet.BottomSheetBehavior 24 | import com.google.android.material.bottomsheet.BottomSheetDialog 25 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 26 | import com.octo4a.R 27 | import com.octo4a.repository.LogEntry 28 | import com.octo4a.repository.LogType 29 | import com.octo4a.repository.LoggerRepository 30 | import com.octo4a.repository.getTypeEmoji 31 | import kotlinx.android.synthetic.main.fragment_terminal_sheet.* 32 | import kotlinx.coroutines.* 33 | import org.koin.android.ext.android.inject 34 | 35 | 36 | class TerminalSheetDialog: BottomSheetDialogFragment() { 37 | val logger: LoggerRepository by inject() 38 | var logCache = mutableListOf() 39 | var shouldUpdateTermUi = true 40 | private val refreshRate = 500L 41 | 42 | @SuppressLint("SetTextI18n") 43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 44 | super.onViewCreated(view, savedInstanceState) 45 | logger.logHistoryFlow.asLiveData().observe(viewLifecycleOwner) { 46 | logCache.add(it) 47 | } 48 | 49 | // Update logs every 1/4 second, takes care of rapid logs 50 | CoroutineScope(Dispatchers.IO).launch { 51 | while(shouldUpdateTermUi) { 52 | if (logCache.isNotEmpty()) { 53 | withContext(Dispatchers.Main) { 54 | if (terminalView == null) return@withContext 55 | var textToDisplay: CharSequence? = terminalView.text 56 | 57 | logCache.forEach { 58 | // Display logs 59 | val prefix = it.type.getTypeEmoji() 60 | 61 | textToDisplay = textToDisplay.toString() + "$prefix ${it.entry}\n" 62 | } 63 | 64 | terminalView?.setText(textToDisplay, TextView.BufferType.SPANNABLE) 65 | 66 | // Auto scroll if enabled 67 | if (enableAutoScroll.isChecked) { 68 | scrollView.post { 69 | scrollView.fullScroll(View.FOCUS_DOWN) 70 | } 71 | } 72 | } 73 | 74 | logCache.clear() 75 | } 76 | delay(refreshRate) 77 | } 78 | } 79 | 80 | 81 | closeDialog.setOnClickListener { 82 | dismiss() 83 | } 84 | 85 | shareLogs.setOnClickListener { 86 | val shareIntent: Intent = Intent() 87 | shareIntent.action = Intent.ACTION_SEND 88 | shareIntent.type = "text/plain" 89 | shareIntent.putExtra(Intent.EXTRA_TEXT, terminalView.text.toString()) 90 | startActivity(Intent.createChooser(shareIntent, "Share logs")) 91 | 92 | } 93 | } 94 | 95 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 96 | return inflater.inflate(R.layout.fragment_terminal_sheet, container, false) 97 | } 98 | 99 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 100 | val bottomSheetDialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog 101 | bottomSheetDialog.setOnShowListener { dialog: DialogInterface -> 102 | val dialogc = dialog as BottomSheetDialog 103 | val bottomSheet = 104 | dialogc.findViewById(R.id.design_bottom_sheet) as FrameLayout 105 | val bottomSheetBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(bottomSheet) 106 | bottomSheetBehavior.peekHeight = Resources.getSystem().displayMetrics.heightPixels 107 | bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED) 108 | } 109 | return bottomSheetDialog 110 | } 111 | 112 | override fun onDestroy() { 113 | super.onDestroy() 114 | shouldUpdateTermUi = false 115 | } 116 | } -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/views/UsbDeviceView.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui.views 2 | 3 | import android.animation.LayoutTransition 4 | import android.content.Context 5 | import android.os.Build 6 | import android.util.AttributeSet 7 | import android.view.LayoutInflater 8 | import android.widget.Toast 9 | import androidx.constraintlayout.widget.ConstraintLayout 10 | import com.google.android.material.bottomsheet.BottomSheetDialog 11 | import com.octo4a.R 12 | import com.octo4a.serial.ConnectedUsbDevice 13 | import com.octo4a.serial.SerialDriverClass 14 | import com.octo4a.serial.VirtualSerialDriver 15 | import com.octo4a.serial.getCustomPrinterProber 16 | import kotlinx.android.synthetic.main.select_driver_bottom_sheet.* 17 | import kotlinx.android.synthetic.main.view_usb_devices_item.view.* 18 | 19 | class UsbDeviceView @JvmOverloads 20 | constructor(private val ctx: Context, private val vsp: VirtualSerialDriver, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0) 21 | : ConstraintLayout(ctx, attributeSet, defStyleAttr) { 22 | 23 | private val prober by lazy { getCustomPrinterProber() } 24 | 25 | init { 26 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 27 | inflater.inflate(R.layout.view_usb_devices_item, this) 28 | layoutTransition = LayoutTransition() 29 | } 30 | 31 | fun drawDriverInfo(usbDevice: ConnectedUsbDevice) { 32 | val driverText = when (usbDevice.driverClass) { 33 | SerialDriverClass.CDC -> "CDC" 34 | SerialDriverClass.CH341 -> "CH341" 35 | SerialDriverClass.PROLIFIC -> "Prolific" 36 | SerialDriverClass.FTDI -> "FTDI" 37 | SerialDriverClass.CP21XX -> "CP21xx" 38 | SerialDriverClass.UNKNOWN -> "Unknown" 39 | } 40 | 41 | serialDriverText.text = driverText + " " + context.getString(R.string.serial_driver) 42 | if (!usbDevice.autoDetect) { 43 | serialDriverText.text = serialDriverText.text.toString() + context.getString(R.string.tap_to_select) 44 | } 45 | } 46 | 47 | fun setUsbDevice(usbDevice: ConnectedUsbDevice) { 48 | vidPidText.text = "VID " + usbDevice.vendorId.toString(16) + " / PID " + usbDevice.productId.toString(16) 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 50 | titleText.text = usbDevice.device.productName 51 | } 52 | 53 | drawDriverInfo(usbDevice) 54 | selectCheckbox.isChecked = usbDevice.isSelected 55 | selectCheckbox.setOnCheckedChangeListener { _, checked -> 56 | if (checked) { 57 | if (usbDevice.driverClass != SerialDriverClass.UNKNOWN) { 58 | vsp.tryToSelectDevice(usbDevice) 59 | } else { 60 | selectCheckbox.isChecked = false 61 | Toast.makeText(context, context.getString(R.string.requesting_usb_permission), Toast.LENGTH_LONG).show() 62 | } 63 | } else { 64 | selectCheckbox.isChecked = true 65 | } 66 | } 67 | 68 | 69 | setOnClickListener { 70 | openBottomSheet(usbDevice) 71 | } 72 | } 73 | 74 | fun openBottomSheet(usbDevice: ConnectedUsbDevice) { 75 | if (usbDevice.autoDetect) { 76 | return 77 | } 78 | val bottomSheetDialog = BottomSheetDialog(context) 79 | bottomSheetDialog.setContentView(R.layout.select_driver_bottom_sheet) 80 | 81 | 82 | bottomSheetDialog.apply { 83 | ftdi.setOnClickListener { 84 | usbDevice.driverClass = SerialDriverClass.FTDI 85 | drawDriverInfo(usbDevice) 86 | vsp.tryToSelectDevice(usbDevice) 87 | dismiss() 88 | } 89 | 90 | cp21xx.setOnClickListener { 91 | usbDevice.driverClass = SerialDriverClass.CP21XX 92 | drawDriverInfo(usbDevice) 93 | vsp.tryToSelectDevice(usbDevice) 94 | dismiss() 95 | } 96 | 97 | prolific.setOnClickListener { 98 | usbDevice.driverClass = SerialDriverClass.PROLIFIC 99 | drawDriverInfo(usbDevice) 100 | vsp.tryToSelectDevice(usbDevice) 101 | dismiss() 102 | } 103 | 104 | cdcAcm.setOnClickListener { 105 | usbDevice.driverClass = SerialDriverClass.CDC 106 | drawDriverInfo(usbDevice) 107 | vsp.tryToSelectDevice(usbDevice) 108 | dismiss() 109 | } 110 | 111 | ch341.setOnClickListener { 112 | usbDevice.driverClass = SerialDriverClass.CH341 113 | drawDriverInfo(usbDevice) 114 | vsp.tryToSelectDevice(usbDevice) 115 | dismiss() 116 | } 117 | } 118 | bottomSheetDialog.show() 119 | } 120 | } -------------------------------------------------------------------------------- /app/app/src/main/res/xml/main_preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 15 | 18 | 24 | 30 | 37 | 44 | 45 | 48 | 54 | 60 | 66 | 72 | 73 | 82 | 83 | 84 | 93 | 100 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/select_driver_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 20 | 22 | 28 | 34 | 35 | 36 | 37 | 44 | 45 | 51 | 52 | 53 | 60 | 61 | 67 | 68 | 69 | 70 | 77 | 78 | 79 | 85 | 86 | 87 | 88 | 95 | 96 | 102 | 103 | 104 | 111 | 112 | 118 | 119 | -------------------------------------------------------------------------------- /app/app/src/main/java/com/octo4a/ui/views/StatusView.kt: -------------------------------------------------------------------------------- 1 | package com.octo4a.ui.views 2 | 3 | import android.content.Context 4 | import android.graphics.PorterDuff 5 | import android.util.AttributeSet 6 | import android.view.LayoutInflater 7 | import android.view.ViewGroup 8 | import androidx.annotation.ColorInt 9 | import androidx.annotation.ColorRes 10 | import androidx.annotation.DrawableRes 11 | import androidx.constraintlayout.widget.ConstraintLayout 12 | import androidx.core.content.ContextCompat 13 | import androidx.core.view.setPadding 14 | import androidx.recyclerview.widget.LinearLayoutManager 15 | import androidx.recyclerview.widget.RecyclerView 16 | import com.octo4a.R 17 | import com.octo4a.repository.ServerStatus 18 | import com.octo4a.viewmodel.IPAddress 19 | import kotlinx.android.synthetic.main.view_installation_item.view.* 20 | import kotlinx.android.synthetic.main.view_status_card.view.* 21 | 22 | class IPAddressListAdapter(private val dataSet: Array) : 23 | RecyclerView.Adapter() { 24 | class ViewHolder(val ipAddressView: IPAddressView) : RecyclerView.ViewHolder(ipAddressView) {} 25 | 26 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { 27 | val view = IPAddressView(viewGroup.context) 28 | return ViewHolder(view) 29 | } 30 | 31 | override fun getItemCount(): Int { 32 | return dataSet.size 33 | } 34 | 35 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 36 | holder.ipAddressView.ipAddress = dataSet[position] 37 | } 38 | } 39 | 40 | class StatusView @JvmOverloads 41 | constructor( 42 | private val ctx: Context, 43 | private val attributeSet: AttributeSet? = null, 44 | private val defStyleAttr: Int = 0 45 | ) : ConstraintLayout(ctx, attributeSet, defStyleAttr) { 46 | 47 | var onActionClicked: () -> Unit = {} 48 | 49 | init { 50 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 51 | inflater.inflate(R.layout.view_status_card, this) 52 | setPadding(12) 53 | attributeSet?.apply { 54 | val styledAttributes = 55 | context.obtainStyledAttributes(this, R.styleable.StatusView, 0, 0) 56 | val titleValue = styledAttributes.getString(R.styleable.StatusView_title) 57 | val subtitleValue = styledAttributes.getString(R.styleable.StatusView_subtitle) 58 | val icon = styledAttributes.getDrawable(R.styleable.StatusView_icon) 59 | val iconColor = styledAttributes.getColor(R.styleable.StatusView_iconColor, 0) 60 | 61 | val backgroundDrawable = ContextCompat.getDrawable(context, R.drawable.circle_drawable) 62 | backgroundDrawable!!.setColorFilter(iconColor, PorterDuff.Mode.SRC_ATOP) 63 | iconCircle.background = backgroundDrawable 64 | 65 | iconCircle.iconView.setImageDrawable(icon) 66 | titleText.text = titleValue 67 | subtitleText.text = subtitleValue 68 | styledAttributes.recycle() 69 | ipAddressList.adapter = IPAddressListAdapter(arrayOf()) 70 | ipAddressList.layoutManager = LinearLayoutManager(context) 71 | } 72 | 73 | actionButton.setOnClickListener { 74 | onActionClicked() 75 | } 76 | } 77 | 78 | fun setDrawableAndColor(@DrawableRes resource: Int, @ColorRes colorRes: Int) { 79 | val backgroundDrawable = ContextCompat.getDrawable(context, resource) 80 | val color = ContextCompat.getColor(context, colorRes); 81 | backgroundDrawable!!.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) 82 | actionButton.background = backgroundDrawable 83 | } 84 | 85 | var title: String 86 | get() = "" 87 | set(value) { 88 | titleText.text = value 89 | } 90 | 91 | var subtitle: String 92 | get() = "" 93 | set(value) { 94 | subtitleText.text = value 95 | } 96 | var showIpAddresses: Boolean 97 | get() = false 98 | set(value) { 99 | if (value) { 100 | ipAddressList.visibility = VISIBLE 101 | subtitleText.visibility = GONE 102 | warningText.visibility = if (warningText.text.isEmpty()) GONE else VISIBLE 103 | } else { 104 | ipAddressList.visibility = GONE 105 | subtitleText.visibility = VISIBLE 106 | warningText.visibility = GONE 107 | } 108 | } 109 | 110 | var ipAddresses: Array 111 | get() = arrayOf() 112 | set(value) { 113 | ipAddressList.adapter = IPAddressListAdapter(value) 114 | } 115 | var warning: String 116 | get() = "" 117 | set(value) { 118 | warningText.text = value 119 | warningText.visibility = if (value.isEmpty() || ipAddressList.visibility != VISIBLE) GONE else VISIBLE 120 | } 121 | } 122 | 123 | 124 | --------------------------------------------------------------------------------