├── .gitignore ├── controller ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ ├── RunnerTests │ │ └── RunnerTests.swift │ ├── .gitignore │ └── Podfile ├── macos │ ├── Runner │ │ ├── Configs │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ ├── Warnings.xcconfig │ │ │ └── AppInfo.xcconfig │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_64.png │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── MainFlutterWindow.swift │ │ ├── Release.entitlements │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ └── Base.lproj │ │ │ └── MainMenu.xib │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── RunnerTests │ │ └── RunnerTests.swift │ ├── Podfile │ └── Podfile.lock ├── android │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── controller │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle.kts │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle.kts │ └── settings.gradle.kts ├── lib │ ├── services │ │ ├── env_service.dart │ │ └── gamepad_service.dart │ ├── main.dart │ └── components │ │ └── controller.dart ├── .gitignore ├── .metadata ├── analysis_options.yaml ├── pubspec.yaml └── pubspec.lock ├── env.example ├── rover-cam-gstreamer.service ├── rover-cam-publish.service ├── rover-control.service ├── rover ├── uvc_stereo_gstreamer.sh ├── cam_gstreamer.sh ├── cam_publish.sh ├── auth.py └── rover.py ├── install-services.sh ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | venv/ 3 | .DS_Store 4 | *.log 5 | __pycache__ -------------------------------------------------------------------------------- /controller/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /controller/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /controller/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /controller/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /controller/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /controller/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /controller/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /controller/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /controller/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /controller/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /controller/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /controller/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /controller/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /controller/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /controller/android/app/src/main/kotlin/com/example/controller/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.controller 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit-examples/rover-teleop/HEAD/controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /controller/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /controller/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | LIVEKIT_URL= 2 | LIVEKIT_API_KEY= 3 | LIVEKIT_API_SECRET= 4 | LIVEKIT_CONTROLLER_TOKEN= 5 | ROOM_NAME= 6 | ROVER_PORT=/dev/serial0 -------------------------------------------------------------------------------- /controller/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 6 | -------------------------------------------------------------------------------- /controller/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /controller/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /rover-cam-gstreamer.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Rover Camera GStreamer Pipeline 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/bin/bash /home/pi/rover-teleop/rover/cam_gstreamer.sh 7 | Restart=on-failure 8 | User=pi 9 | WorkingDirectory=/home/pi/rover-teleop 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /controller/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /controller/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /controller/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/to/reference-keystore 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /controller/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /controller/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /controller/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /controller/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /controller/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /rover-cam-publish.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Rover Camera LiveKit Publisher 3 | After=network.target rover-cam-gstreamer.service 4 | Requires=rover-cam-gstreamer.service 5 | 6 | [Service] 7 | ExecStart=/bin/bash /home/pi/rover-teleop/rover/cam_publish.sh 8 | Restart=on-failure 9 | User= 10 | WorkingDirectory=/home/pi/rover-teleop/rover 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /controller/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /controller/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /controller/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /rover-control.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Rover Control System 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/home/pi/rover-teleop/rover/rover.py 7 | Restart=on-failure 8 | User=pi 9 | WorkingDirectory=/home/pi/rover-teleop 10 | EnvironmentFile=/home/pi/rover-teleop/rover/.env 11 | Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/pi/.local/bin" 12 | 13 | [Install] 14 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /controller/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rover/uvc_stereo_gstreamer.sh: -------------------------------------------------------------------------------- 1 | gst-launch-1.0 -v v4l2src device=/dev/video2 io-mode=2 ! \ 2 | video/x-raw,format=YUY2,width=1600,height=600,framerate=30/1,colorimetry=2:4:16:1 ! \ 3 | videoconvert ! video/x-raw,format=NV12 ! \ 4 | v4l2h264enc extra-controls="controls,repeat_sequence_header=1" ! \ 5 | "video/x-h264,profile=baseline,level=(string)4" ! \ 6 | h264parse config-interval=1 ! \ 7 | queue max-size-buffers=1 max-size-bytes=0 max-size-time=0 ! \ 8 | tcpserversink host=0.0.0.0 port=5004 sync=false async=false -------------------------------------------------------------------------------- /controller/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /rover/cam_gstreamer.sh: -------------------------------------------------------------------------------- 1 | # create a gstreamer pipeline to publish the camera stream to a tcp sink server on port 5004 2 | gst-launch-1.0 -v libcamerasrc ! \ 3 | capsfilter caps=video/x-raw,width=640,height=480,format=NV12,interlace-mode=progressive ! \ 4 | v4l2h264enc extra-controls="controls,repeat_sequence_header=1" ! 'video/x-h264,level=(string)4,profile=baseline' ! \ 5 | h264parse config-interval=1 ! \ 6 | queue max-size-buffers=1 max-size-time=0 max-size-bytes=0 ! \ 7 | tcpserversink host=0.0.0.0 port=5004 sync=false async=false 8 | -------------------------------------------------------------------------------- /controller/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /controller/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /controller/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /controller/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = controller 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.controller 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /controller/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | // Set a fixed window size (width: 800, height: 600) 12 | self.setContentSize(NSSize(width: 800, height: 600)) 13 | 14 | // Disable window resizing 15 | self.styleMask.remove(.resizable) 16 | 17 | RegisterGeneratedPlugins(registry: flutterViewController) 18 | 19 | super.awakeFromNib() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /controller/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.device.camera 8 | 9 | com.apple.security.device.microphone 10 | 11 | com.apple.security.device.audio-input 12 | 13 | com.apple.security.files.user-selected.read-only 14 | 15 | com.apple.security.network.client 16 | 17 | com.apple.security.network.server 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /controller/lib/services/env_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 2 | 3 | class EnvService { 4 | static Future init() async { 5 | try { 6 | await dotenv.load(fileName: '.env'); 7 | print('Loaded environment variables: ${dotenv.env}'); 8 | } catch (e) { 9 | // .env file doesn't exist or couldn't be loaded 10 | // Fallback to default values will be used 11 | print('Warning: .env file not found. Using default values instead.'); 12 | } 13 | } 14 | 15 | static String get livekitUrl => dotenv.env['LIVEKIT_URL'] ?? 'wss://your-livekit-server.com'; 16 | static String get livekitToken => dotenv.env['LIVEKIT_CONTROLLER_TOKEN'] ?? 'your-token'; 17 | } -------------------------------------------------------------------------------- /controller/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /rover/cam_publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Load environment variables from .env file 4 | if [ -f ".env" ]; then 5 | export $(grep -v '^#' .env | xargs) 6 | else 7 | echo "Error: .env file not found. Please create it based on .env.example" 8 | exit 1 9 | fi 10 | 11 | # Check if required variables are set 12 | if [ -z "$LIVEKIT_API_KEY" ] || [ -z "$LIVEKIT_API_SECRET" ] || [ -z "$LIVEKIT_URL" ]; then 13 | echo "Error: Required environment variables not set. Please check your .env file." 14 | echo "Required: LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL" 15 | exit 1 16 | fi 17 | 18 | 19 | # Run the livekit-cli command with environment variables 20 | lk room join --identity rover-cam --api-key "$LIVEKIT_API_KEY" \ 21 | --api-secret "$LIVEKIT_API_SECRET" \ 22 | --url "$LIVEKIT_URL" --publish h264://127.0.0.1:5004 $ROOM_NAME 23 | -------------------------------------------------------------------------------- /controller/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.device.camera 12 | 13 | com.apple.security.device.microphone 14 | 15 | com.apple.security.device.audio-input 16 | 17 | com.apple.security.files.user-selected.read-only 18 | 19 | com.apple.security.network.client 20 | 21 | com.apple.security.network.server 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /controller/android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.0" apply false 22 | id("org.jetbrains.kotlin.android") version "1.8.22" apply false 23 | } 24 | 25 | include(":app") 26 | -------------------------------------------------------------------------------- /controller/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /controller/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /controller/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import connectivity_plus 9 | import device_info_plus 10 | import flutter_webrtc 11 | import gamepads_darwin 12 | import livekit_client 13 | import path_provider_foundation 14 | 15 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 16 | ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) 17 | DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 18 | FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) 19 | GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin")) 20 | LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) 21 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 22 | } 23 | -------------------------------------------------------------------------------- /controller/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /controller/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /install-services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if running as root 4 | if [ "$EUID" -ne 0 ]; then 5 | echo "Please run as root (sudo)" 6 | exit 1 7 | fi 8 | 9 | # Get the absolute path to the current directory 10 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 11 | 12 | # Copy service files to systemd directory 13 | cp $SCRIPT_DIR/rover-cam-gstreamer.service /etc/systemd/system/ 14 | cp $SCRIPT_DIR/rover-cam-publish.service /etc/systemd/system/ 15 | cp $SCRIPT_DIR/rover-control.service /etc/systemd/system/ 16 | 17 | # Reload systemd daemon 18 | systemctl daemon-reload 19 | 20 | # Enable services to start at boot 21 | systemctl enable rover-cam-gstreamer.service 22 | systemctl enable rover-cam-publish.service 23 | systemctl enable rover-control.service 24 | 25 | echo "Services installed and enabled. You can now start them with:" 26 | echo "sudo systemctl start rover-cam-gstreamer.service" 27 | echo "sudo systemctl start rover-cam-publish.service" 28 | echo "sudo systemctl start rover-control.service" 29 | echo "" 30 | echo "To check status use: sudo systemctl status " 31 | echo "" 32 | echo "IMPORTANT: Make sure to edit /home/pi/rover-teleop/rover/.env with your LiveKit credentials." -------------------------------------------------------------------------------- /controller/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "ea121f8859e4b13e47a8f845e4586164519588bc" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ea121f8859e4b13e47a8f845e4586164519588bc 17 | base_revision: ea121f8859e4b13e47a8f845e4586164519588bc 18 | - platform: android 19 | create_revision: ea121f8859e4b13e47a8f845e4586164519588bc 20 | base_revision: ea121f8859e4b13e47a8f845e4586164519588bc 21 | - platform: ios 22 | create_revision: ea121f8859e4b13e47a8f845e4586164519588bc 23 | base_revision: ea121f8859e4b13e47a8f845e4586164519588bc 24 | - platform: macos 25 | create_revision: ea121f8859e4b13e47a8f845e4586164519588bc 26 | base_revision: ea121f8859e4b13e47a8f845e4586164519588bc 27 | 28 | # User provided section 29 | 30 | # List of Local paths (relative to this file) that should be 31 | # ignored by the migrate tool. 32 | # 33 | # Files that are not part of the templates will be ignored by default. 34 | unmanaged_files: 35 | - 'lib/main.dart' 36 | - 'ios/Runner.xcodeproj/project.pbxproj' 37 | -------------------------------------------------------------------------------- /controller/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSCameraUsageDescription 26 | $(PRODUCT_NAME) uses your camera 27 | NSHumanReadableCopyright 28 | $(PRODUCT_COPYRIGHT) 29 | NSMainNibFile 30 | MainMenu 31 | NSMicrophoneUsageDescription 32 | $(PRODUCT_NAME) uses your microphone 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /rover/auth.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from livekit import api 4 | 5 | # Load environment variables from .env file 6 | load_dotenv() 7 | 8 | # Get LiveKit credentials from environment variables 9 | LIVEKIT_API_KEY = os.environ.get("LIVEKIT_API_KEY") 10 | LIVEKIT_API_SECRET = os.environ.get("LIVEKIT_API_SECRET") 11 | 12 | def generate_token(room_name, identity=None, name=None): 13 | """ 14 | Generate a LiveKit access token for room access. 15 | 16 | Args: 17 | room_name: The name of the room to join 18 | identity: The participant identity 19 | name: The display name (default: same as identity) 20 | 21 | Returns: 22 | JWT token string 23 | """ 24 | if not identity: 25 | identity = f"python-user-{room_name}" 26 | 27 | if not name: 28 | name = identity 29 | 30 | # Check if required environment variables are set 31 | if not LIVEKIT_API_KEY or not LIVEKIT_API_SECRET: 32 | raise ValueError("LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set in .env file") 33 | 34 | # Create token with video grants 35 | token = ( 36 | api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET) 37 | .with_identity(identity) 38 | .with_name(name) 39 | .with_grants( 40 | api.VideoGrants( 41 | room_join=True, 42 | room=room_name, 43 | ) 44 | ) 45 | .to_jwt() 46 | ) 47 | 48 | return token -------------------------------------------------------------------------------- /controller/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /controller/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | 32 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 33 | target 'RunnerTests' do 34 | inherit! :search_paths 35 | end 36 | end 37 | 38 | post_install do |installer| 39 | installer.pods_project.targets.each do |target| 40 | flutter_additional_macos_build_settings(target) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /controller/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | 33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_ios_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /controller/android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id("dev.flutter.flutter-gradle-plugin") 6 | } 7 | 8 | android { 9 | namespace = "com.example.controller" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_11.toString() 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.example.controller" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.getByName("debug") 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /controller/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /controller/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /controller/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Controller 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | controller 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /controller/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /controller/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /controller/macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - connectivity_plus (0.0.1): 3 | - FlutterMacOS 4 | - device_info_plus (0.0.1): 5 | - FlutterMacOS 6 | - flutter_webrtc (0.12.6): 7 | - FlutterMacOS 8 | - WebRTC-SDK (= 125.6422.06) 9 | - FlutterMacOS (1.0.0) 10 | - gamepads_darwin (0.1.1): 11 | - FlutterMacOS 12 | - livekit_client (2.4.5): 13 | - flutter_webrtc 14 | - FlutterMacOS 15 | - WebRTC-SDK (= 125.6422.06) 16 | - path_provider_foundation (0.0.1): 17 | - Flutter 18 | - FlutterMacOS 19 | - WebRTC-SDK (125.6422.06) 20 | 21 | DEPENDENCIES: 22 | - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) 23 | - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) 24 | - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) 25 | - FlutterMacOS (from `Flutter/ephemeral`) 26 | - gamepads_darwin (from `Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos`) 27 | - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) 28 | - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) 29 | 30 | SPEC REPOS: 31 | trunk: 32 | - WebRTC-SDK 33 | 34 | EXTERNAL SOURCES: 35 | connectivity_plus: 36 | :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos 37 | device_info_plus: 38 | :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos 39 | flutter_webrtc: 40 | :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos 41 | FlutterMacOS: 42 | :path: Flutter/ephemeral 43 | gamepads_darwin: 44 | :path: Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos 45 | livekit_client: 46 | :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos 47 | path_provider_foundation: 48 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin 49 | 50 | SPEC CHECKSUMS: 51 | connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e 52 | device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 53 | flutter_webrtc: 377dbcebdde6fed0fc40de87bcaaa2bffcec9a88 54 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 55 | gamepads_darwin: 643b6a69e20ca678fae83781b7f7fc8f15f5d710 56 | livekit_client: fd05811509a0d29c0a2fbcc943c49f9e19a68236 57 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 58 | WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db 59 | 60 | PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 61 | 62 | COCOAPODS: 1.16.2 63 | -------------------------------------------------------------------------------- /controller/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | import 'package:livekit_client/livekit_client.dart'; 4 | import 'package:rover_controller/components/controller.dart'; 5 | import 'package:rover_controller/services/gamepad_service.dart'; 6 | import 'services/env_service.dart'; 7 | 8 | void main() async { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | 11 | // Load environment variables from .env file 12 | await EnvService.init(); 13 | 14 | // Initialize GamepadService 15 | await GamepadService().initialize(); 16 | 17 | // Initialize LiveKit logger 18 | LiveKitClient.initialize(); 19 | runApp(const MyApp()); 20 | } 21 | 22 | class MyApp extends StatelessWidget { 23 | const MyApp({super.key}); 24 | 25 | // This widget is the root of your application. 26 | @override 27 | Widget build(BuildContext context) { 28 | return MaterialApp( 29 | title: 'Rover Controller', 30 | theme: ThemeData( 31 | // This is the theme of your application. 32 | // 33 | // TRY THIS: Try running your application with "flutter run". You'll see 34 | // the application has a purple toolbar. Then, without quitting the app, 35 | // try changing the seedColor in the colorScheme below to Colors.green 36 | // and then invoke "hot reload" (save your changes or press the "hot 37 | // reload" button in a Flutter-supported IDE, or press "r" if you used 38 | // the command line to start the app). 39 | // 40 | // Notice that the counter didn't reset back to zero; the application 41 | // state is not lost during the reload. To reset the state, use hot 42 | // restart instead. 43 | // 44 | // This works for code too, not just values: Most code changes can be 45 | // tested with just a hot reload. 46 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), 47 | textTheme: TextTheme( 48 | displayLarge: const TextStyle( 49 | fontSize: 72, 50 | fontWeight: FontWeight.bold, 51 | ), 52 | // ··· 53 | titleLarge: GoogleFonts.oswald( 54 | fontSize: 30, 55 | fontStyle: FontStyle.italic, 56 | ), 57 | bodyMedium: GoogleFonts.roboto(), 58 | displaySmall: GoogleFonts.roboto(fontSize: 12), 59 | ), 60 | useMaterial3: true, 61 | ), 62 | home: Controller( 63 | url: EnvService.livekitUrl, 64 | token: EnvService.livekitToken, 65 | ), 66 | debugShowCheckedModeBanner: false, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /controller/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /controller/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /controller/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /controller/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rover_controller 2 | description: "Rover Teleop Demo with LiveKit" 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: ^3.7.2 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.8 37 | livekit_client: ^2.4.5 38 | flutter_dotenv: ^5.1.0 39 | gamepads: ^0.1.5 40 | google_fonts: ^6.2.1 41 | fl_chart: ^0.66.2 42 | 43 | dev_dependencies: 44 | flutter_test: 45 | sdk: flutter 46 | 47 | # The "flutter_lints" package below contains a set of recommended lints to 48 | # encourage good coding practices. The lint set provided by the package is 49 | # activated in the `analysis_options.yaml` file located at the root of your 50 | # package. See that file for information about deactivating specific lint 51 | # rules and activating additional ones. 52 | flutter_lints: ^5.0.0 53 | 54 | # For information on the generic Dart part of this file, see the 55 | # following page: https://dart.dev/tools/pub/pubspec 56 | 57 | # The following section is specific to Flutter packages. 58 | flutter: 59 | 60 | # The following line ensures that the Material Icons font is 61 | # included with your application, so that you can use the icons in 62 | # the material Icons class. 63 | uses-material-design: true 64 | 65 | # To add assets to your application, add an assets section, like this: 66 | assets: 67 | - .env 68 | 69 | # An image asset can refer to one or more resolution-specific "variants", see 70 | # https://flutter.dev/to/resolution-aware-images 71 | 72 | # For details regarding adding assets from package dependencies, see 73 | # https://flutter.dev/to/asset-from-package 74 | 75 | # To add custom fonts to your application, add a fonts section here, 76 | # in this "flutter" section. Each entry in this list should have a 77 | # "family" key with the font family name, and a "fonts" key with a 78 | # list giving the asset and other descriptors for the font. For 79 | # example: 80 | # fonts: 81 | # - family: Schyler 82 | # fonts: 83 | # - asset: fonts/Schyler-Regular.ttf 84 | # - asset: fonts/Schyler-Italic.ttf 85 | # style: italic 86 | # - family: Trajan Pro 87 | # fonts: 88 | # - asset: fonts/TrajanPro.ttf 89 | # - asset: fonts/TrajanPro_Bold.ttf 90 | # weight: 700 91 | # 92 | # For details regarding fonts from package dependencies, 93 | # see https://flutter.dev/to/font-from-package 94 | -------------------------------------------------------------------------------- /controller/lib/services/gamepad_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:gamepads/gamepads.dart'; 4 | 5 | class GamepadService { 6 | static final GamepadService _instance = GamepadService._internal(); 7 | factory GamepadService() => _instance; 8 | GamepadService._internal(); 9 | 10 | final ValueNotifier isGamepadConnected = ValueNotifier(false); 11 | GamepadController? _activeGamepad; 12 | StreamSubscription? _eventSubscription; 13 | final ValueNotifier> controllerValues = ValueNotifier>({ 14 | 'leftStickX': 0.0, 15 | 'leftStickY': 0.0, 16 | 'rightStickX': 0.0, 17 | 'rightStickY': 0.0, 18 | 'leftTrigger': 0.0, 19 | 'rightTrigger': 0.0, 20 | 'buttonA': false, 21 | 'buttonB': false, 22 | 'buttonX': false, 23 | 'buttonY': false, 24 | 'dpadUp': false, 25 | 'dpadDown': false, 26 | 'dpadLeft': false, 27 | 'dpadRight': false, 28 | 'leftShoulder': false, 29 | 'rightShoulder': false, 30 | }); 31 | 32 | Future initialize() async { 33 | try { 34 | print('Initializing gamepad service'); 35 | 36 | // Check for connected gamepads 37 | await _checkForConnectedGamepads(); 38 | 39 | // Setup event listeners 40 | _setupListeners(); 41 | } catch (error) { 42 | print('Error initializing gamepad service: $error'); 43 | } 44 | } 45 | 46 | void _setupListeners() { 47 | // Listen to gamepad events 48 | _eventSubscription = Gamepads.events.listen(_handleGamepadEvent); 49 | } 50 | 51 | Future _checkForConnectedGamepads() async { 52 | final connectedGamepads = await Gamepads.list(); 53 | if (connectedGamepads.isNotEmpty) { 54 | _activeGamepad = connectedGamepads.first; 55 | isGamepadConnected.value = true; 56 | print('Found connected gamepad: ${_activeGamepad!.name}'); 57 | } else { 58 | print('No gamepads connected'); 59 | } 60 | } 61 | 62 | void _handleGamepadEvent(GamepadEvent event) { 63 | // Update state based on the event 64 | if (_activeGamepad != null && event.gamepadId == _activeGamepad!.id) { 65 | _updateValuesFromEvent(event); 66 | } 67 | } 68 | 69 | void _updateValuesFromEvent(GamepadEvent event) { 70 | final values = Map.from(controllerValues.value); 71 | 72 | // Handle button and axis values based on the key and type 73 | switch (event.key) { 74 | case 'l.joystick - xAxis': 75 | values['leftStickX'] = event.value; 76 | break; 77 | case 'l.joystick - yAxis': 78 | values['leftStickY'] = event.value; 79 | break; 80 | case 'r.joystick - xAxis': 81 | values['rightStickX'] = event.value; 82 | break; 83 | case 'r.joystick - yAxis': 84 | values['rightStickY'] = event.value; 85 | break; 86 | // case 'a': 87 | // case 'button_0': 88 | // values['buttonA'] = event.value > 0.5; 89 | // break; 90 | // case 'b': 91 | // case 'button_1': 92 | // values['buttonB'] = event.value > 0.5; 93 | // break; 94 | // case 'x': 95 | // case 'button_2': 96 | // values['buttonX'] = event.value > 0.5; 97 | // break; 98 | // case 'y': 99 | // case 'button_3': 100 | // values['buttonY'] = event.value > 0.5; 101 | // break; 102 | // case 'left_shoulder': 103 | // case 'button_4': 104 | // values['leftShoulder'] = event.value > 0.5; 105 | // break; 106 | // case 'right_shoulder': 107 | // case 'button_5': 108 | // values['rightShoulder'] = event.value > 0.5; 109 | // break; 110 | // case 'dpad_up': 111 | // case 'button_12': 112 | // values['dpadUp'] = event.value > 0.5; 113 | // break; 114 | // case 'dpad_down': 115 | // case 'button_13': 116 | // values['dpadDown'] = event.value > 0.5; 117 | // break; 118 | // case 'dpad_left': 119 | // case 'button_14': 120 | // values['dpadLeft'] = event.value > 0.5; 121 | // break; 122 | // case 'dpad_right': 123 | // case 'button_15': 124 | // values['dpadRight'] = event.value > 0.5; 125 | // break; 126 | // case 'left_x': 127 | // case 'axis_0': 128 | // values['leftStickX'] = event.value; 129 | // break; 130 | // case 'left_y': 131 | // case 'axis_1': 132 | // values['leftStickY'] = event.value; 133 | // break; 134 | // case 'right_x': 135 | // case 'axis_2': 136 | // values['rightStickX'] = event.value; 137 | // break; 138 | // case 'right_y': 139 | // case 'axis_3': 140 | // values['rightStickY'] = event.value; 141 | // break; 142 | // case 'left_trigger': 143 | // case 'axis_4': 144 | // values['leftTrigger'] = event.value; 145 | // break; 146 | // case 'right_trigger': 147 | // case 'axis_5': 148 | // values['rightTrigger'] = event.value; 149 | // break; 150 | default: 151 | print('Unhandled gamepad input: key=${event.key}, type=${event.type}, value=${event.value}'); 152 | } 153 | 154 | controllerValues.value = values; 155 | } 156 | 157 | void _resetControllerValues() { 158 | controllerValues.value = { 159 | 'leftStickX': 0.0, 160 | 'leftStickY': 0.0, 161 | 'rightStickX': 0.0, 162 | 'rightStickY': 0.0, 163 | 'leftTrigger': 0.0, 164 | 'rightTrigger': 0.0, 165 | 'buttonA': false, 166 | 'buttonB': false, 167 | 'buttonX': false, 168 | 'buttonY': false, 169 | 'dpadUp': false, 170 | 'dpadDown': false, 171 | 'dpadLeft': false, 172 | 'dpadRight': false, 173 | 'leftShoulder': false, 174 | 'rightShoulder': false, 175 | }; 176 | } 177 | 178 | void dispose() { 179 | _eventSubscription?.cancel(); 180 | _activeGamepad = null; 181 | isGamepadConnected.value = false; 182 | } 183 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Rover Teleoperations 2 | ![image](https://github.com/user-attachments/assets/cba39d62-a6be-4e29-939c-c5fb1ac55f4d) 3 | 4 | This project demostrates building a high performance robot tele-op system using LiveKit that enables < *200ms* latency video and controls. Everything needed to build the rover in this project was available off the shelf, costing no more than $200. The repo includes the source code that runs on the rover for streaming realtime video and receiving control messages via LiveKit. It also includes a Flutter app for remote teleop user for controlling the rover with a standard gamepad. 5 | 6 | ## Rover 7 | 8 | The rover is built with all off-the-shelf components costing less than $200 USD. This does not include the gamepad used by controller app. 9 | 10 | 1. [Raspberry Pi 4B 8GB](https://www.sparkfun.com/raspberry-pi-4-model-b-8-gb.html) - $75 11 | 2. [Raspberry Pi Camera V2](https://www.amazon.com/Raspberry-Pi-Camera-Module-Megapixel/dp/B01ER2SKFS) - $12 12 | 3. [Waveshare Rover](https://www.amazon.com/Waveshare-Flexible-Expandable-Chassis-Multiple/dp/B0CF55LM6Q) - $99 13 | 4. [3x 18650 batteries](https://www.amazon.com/dp/B0CDRBR2M1) - ~$14 14 | 4. Assorted mounting hardware & jumper cables 15 | 16 | Total cost = $200 17 | 18 | ### LiveKit Account 19 | 20 | You will need a API key for LiveKit cloud or host your own LiveKit server to run this demo. Get a free account at [https://cloud.livekit.io](https://cloud.livekit.io). 21 | 22 | ### Rover Hardware Setup 23 | 24 | 1. Install batteries into rover. 25 | 2. Install the Raspberry Pi onto the mounting bracket. 26 | 3. Install the camera module onto the mounting bracket. 27 | 4. Install the camera/compute bracket onto the rover. 28 | 5. Connect 5V, ground, Uart TX/TX to the rover ESP32. 29 | 30 | ### Raspberry Pi OS Setup 31 | 32 | Before setting up the rover teleop software, you need to prepare your Raspberry Pi. 33 | 34 | 1. Install Raspberry Pi OS 64-bit (Bookworm): 35 | - Download the Raspberry Pi Imager from [raspberrypi.com/software](https://www.raspberrypi.com/software/). 36 | - Use the imager to install Raspberry Pi OS 64-bit (Bookworm) on your SD card. 37 | - Complete the initial setup process (create user, set timezone, connect to WiFi). 38 | - This repo assumes you are configuring the Pi to use the default user `pi`. 39 | 40 | 2. Enable required interfaces: 41 | Power up the Pi connected to a monitor, keyboard, and mouse to continue the setup. It should boot directly into a GUI desktop environment. 42 | 43 | ``` 44 | sudo raspi-config 45 | ``` 46 | - Navigate to "Interface Options" 47 | - Enable SSH 48 | - Enable Serial port - but do not enable login shell on serial port. 49 | - Enable I2C 50 | - Enable SPI 51 | - Reboot when prompted or run `sudo reboot` 52 | 53 | 3. Disable booting into GUI interface 54 | ``` 55 | sudo systemctl set-default multi-user.target 56 | ``` 57 | 58 | 3. Set up the Raspberry Pi Camera v2: 59 | - Connect the camera module to the Raspberry Pi's camera port 60 | - Add the following to `/boot/firmware/config.txt`: 61 | ``` 62 | sudo nano /boot/firmware/config.txt 63 | ``` 64 | - Comment out the line that says: 65 | ``` 66 | camera_auto_detect=1 67 | ``` 68 | - Add the following line: 69 | ``` 70 | dtoverlay=imx219,rotation=0 71 | ``` 72 | - Save and reboot 73 | 74 | 4. Verify the camera is working: 75 | ``` 76 | rpicam-hello 77 | ``` 78 | - You should see the detected camera image displayed 79 | 80 | At this point, you should be able to ssh into the pi and do everything remotely. 81 | 82 | ### Install Dependencies 83 | 84 | Install other dependencies that the rover apps require: 85 | 86 | 1. Install uv (modern Python package manager): 87 | ``` 88 | curl -LsSf https://astral.sh/uv/install.sh | sh 89 | ``` 90 | 2. Install other dependencies 91 | ``` 92 | sudo apt install -y git 93 | ``` 94 | 3. Install Gstreamer 95 | ``` 96 | sudo apt install -y gstreamer1.0-libcamera gstreamer1.0-tools \ 97 | gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ 98 | gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa 99 | ``` 100 | 101 | 4. Install the LiveKit CLI 102 | ``` 103 | curl -sSL https://get.livekit.io/cli | bash 104 | ``` 105 | 106 | ### Rover App Setup 107 | 108 | 1. Clone this repository to your Raspberry Pi: 109 | ``` 110 | cd ~ 111 | git clone https://github.com/livekit-examples/rover-teleop.git 112 | cd rover-teleop 113 | ``` 114 | 115 | 4. Copy `env.example` to `.env` and fill with your actual credentials: 116 | ``` 117 | cp /home/pi/rover-teleop/env.example /home/pi/rover-teleop/rover/.env 118 | nano /home/pi/rover-teleop/rover/.env 119 | ``` 120 | 121 | Add your actual values for: 122 | ``` 123 | LIVEKIT_URL= 124 | LIVEKIT_API_KEY= 125 | LIVEKIT_API_SECRET= 126 | LIVEKIT_CONTROLLER_TOKEN= 127 | ROOM_NAME= 128 | ROVER_PORT=/dev/serial0 129 | ``` 130 | 131 | 3. Run the installation script to create systemd services: 132 | ``` 133 | sudo ./install-services.sh 134 | ``` 135 | 136 | This script will: 137 | - Install the systemd service files 138 | - Enable the services to start at boot 139 | 140 | 141 | ### Service Management 142 | 143 | Start services: 144 | ``` 145 | sudo systemctl start rover-cam-gstreamer.service 146 | sudo systemctl start rover-cam-publish.service 147 | sudo systemctl start rover-control.service 148 | ``` 149 | 150 | Check service status: 151 | ``` 152 | sudo systemctl status rover-cam-gstreamer.service 153 | sudo systemctl status rover-cam-publish.service 154 | sudo systemctl status rover-control.service 155 | ``` 156 | 157 | Stop services: 158 | ``` 159 | sudo systemctl stop rover-cam-gstreamer.service 160 | sudo systemctl stop rover-cam-publish.service 161 | sudo systemctl stop rover-control.service 162 | ``` 163 | 164 | View logs: 165 | ``` 166 | sudo journalctl -u rover-cam-gstreamer.service 167 | sudo journalctl -u rover-cam-publish.service 168 | sudo journalctl -u rover-control.service 169 | ``` 170 | 171 | ## Rover Teleop Controller 172 | 173 | This Flutter application connects to a LiveKit server to control and view a rover's camera feed. 174 | 175 | ### Setup 176 | 177 | 1. Create a copy of your `.env` file in the `controller` directory: 178 | 179 | ``` 180 | cp /home/pi/rover-teleop/rover/.env /home/pi/rover-teleop/controller/.env 181 | ``` 182 | 183 | 2. Change to the controller directory: 184 | 185 | ``` 186 | cd controller 187 | ``` 188 | 189 | 3. Install dependencies: 190 | 191 | ```bash 192 | flutter pub get 193 | ``` 194 | 195 | 4. Run the application: 196 | 197 | ```bash 198 | flutter run -d macos 199 | ``` 200 | 201 | ### Usage 202 | When the app is running, you will see the video from your rover. 203 | 204 | ![image](https://github.com/user-attachments/assets/928cb096-c130-49b2-80d9-0584f37b33b1) 205 | 206 | The app will automatically connect to the LiveKit server using the credentials from the `.env` file. 207 | 208 | - The main screen displays the rover camera feed when connected 209 | - Tap the `Start/Stop` button on the top left to enable/disable tele-op. 210 | - Tap the `Mute/Unmute` button on the top right to enable/disable streaming audio from local microphone. This is not used for anything currently. 211 | - The left thumbstick Y axis controls the throttle on the rover, pushing forward on the stick will cause the rover to drive forward, pulling back will reverse. 212 | - The right thumbstick X axis controls steering proportionally. 213 | 214 | ### Requirements 215 | 216 | - Flutter 3.7.2 or higher 217 | - A valid LiveKit server and token 218 | - A connected gamepad. It can usb or Bluetooth. XBox or Playstation controllers work great. 219 | 220 | ### Notes 221 | - This app has only been tested on MacOS, but it should work on iOS, Android, Windows without any issues. 222 | 223 | ## Performance 224 | 225 | The roundtrip glass-to-glass latency can be measured by pointing the rover at a clock on the same screen displaying the rover video stream. Rover and controller were both connected to LiveKit Cloud. By taking a screenshot, we can calculate the latency is approximately 190ms. 226 | 227 | ![image](https://github.com/user-attachments/assets/7059c73b-da3a-4b8f-b467-13c104cb60b0) 228 | 229 | ## Notes 230 | - To further reduce latency, we can add WIFI 6/7 capable radio to rover (-10ms) and move controller laptop to ethernet (-20-30ms). 231 | 232 | 233 | -------------------------------------------------------------------------------- /rover/rover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S uv run --script 2 | # /// script 3 | # dependencies = [ 4 | # "livekit", 5 | # "livekit_api", 6 | # "pyserial", 7 | # "python-dotenv", 8 | # "asyncio", 9 | # ] 10 | # /// 11 | 12 | import os 13 | import logging 14 | import asyncio 15 | import json 16 | import serial 17 | from dotenv import load_dotenv 18 | from signal import SIGINT, SIGTERM 19 | from livekit import rtc 20 | from auth import generate_token 21 | 22 | load_dotenv() 23 | # ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set in your .env file 24 | LIVEKIT_URL = os.environ.get("LIVEKIT_URL") 25 | ROOM_NAME = os.environ.get("ROOM_NAME") 26 | ROVER_PORT = os.environ.get("ROVER_PORT") 27 | 28 | async def read_serial_data(ser: serial.Serial, logger: logging.Logger, room: rtc.Room = None): 29 | """Read and parse data from serial port.""" 30 | if not ser or not ser.is_open: 31 | return 32 | 33 | try: 34 | if ser.in_waiting: 35 | line = ser.readline().decode('utf-8').strip() 36 | try: 37 | data = json.loads(line) 38 | # Check if this is IMU data (type 1002) 39 | if data.get('T') == 1002: 40 | # Parse IMU data 41 | imu_data = { 42 | 'type': 'imu', 43 | 'data': { 44 | 'orientation': { 45 | 'roll': data.get('r', 0), # Roll in degrees 46 | 'pitch': data.get('p', 0), # Pitch in degrees 47 | 'yaw': data.get('y', 0) # Yaw in degrees 48 | }, 49 | 'accel': { 50 | 'x': data.get('ax', 0), # Accelerometer X in mg 51 | 'y': data.get('ay', 0), # Accelerometer Y in mg 52 | 'z': data.get('az', 0) # Accelerometer Z in mg 53 | }, 54 | 'gyro': { 55 | 'x': data.get('gx', 0), # Gyroscope X in degrees/s 56 | 'y': data.get('gy', 0), # Gyroscope Y in degrees/s 57 | 'z': data.get('gz', 0) # Gyroscope Z in degrees/s 58 | }, 59 | 'mag': { 60 | 'x': data.get('mx', 0), # Magnetometer X in uT 61 | 'y': data.get('my', 0), # Magnetometer Y in uT 62 | 'z': data.get('mz', 0) # Magnetometer Z in uT 63 | }, 64 | 'temp': data.get('temp', 0) # Temperature in Celsius 65 | } 66 | } 67 | logger.info(f"Parsed IMU data: {imu_data}") 68 | 69 | # Publish IMU data to room if available 70 | if room and room.isconnected: 71 | try: 72 | await room.local_participant.publish_data( 73 | json.dumps(imu_data).encode(), 74 | topic="imu", 75 | reliable=False 76 | ) 77 | except Exception as e: 78 | logger.error(f"Failed to publish IMU data: {e}") 79 | except json.JSONDecodeError: 80 | logger.warning(f"Failed to parse JSON from serial: {line}") 81 | except Exception as e: 82 | logger.error(f"Error reading serial data: {e}") 83 | 84 | async def send_imu_query(ser: serial.Serial, logger: logging.Logger): 85 | """Send IMU query command to serial port.""" 86 | if not ser or not ser.is_open: 87 | return 88 | 89 | try: 90 | command = {"T": 126} 91 | command_json = json.dumps(command) + "\n" 92 | ser.write(command_json.encode()) 93 | except Exception as e: 94 | logger.error(f"Error sending IMU query: {e}") 95 | 96 | async def main(room: rtc.Room): 97 | logging.basicConfig(level=logging.INFO) 98 | logger = logging.getLogger(__name__) 99 | 100 | # Try to connect to serial port, but continue even if failed 101 | ser = None 102 | try: 103 | # Use the environment variable for port or default to a common port 104 | if not ROVER_PORT: 105 | logger.info("ROVER_PORT environment variable not set, defaulting to /dev/ttyUSB0") 106 | port = '/dev/ttyUSB0' 107 | else: 108 | port = ROVER_PORT 109 | 110 | # Create serial connection with 115200 baud rate using standard serial library 111 | ser = serial.Serial(port, 115200, timeout=1) 112 | logger.info(f"Successfully connected to serial port {port} at 115200 baud") 113 | except Exception as e: 114 | logger.warning(f"Failed to connect to serial port: {e}") 115 | logger.info("Continuing without serial connection - will only log received data") 116 | ser = None 117 | 118 | # Start periodic IMU query task 119 | async def periodic_imu_query(): 120 | while True: 121 | await send_imu_query(ser, logger) 122 | await asyncio.sleep(0.1) # 10Hz = 0.1 seconds 123 | 124 | # Start periodic serial data reading task 125 | async def periodic_serial_read(): 126 | while True: 127 | await read_serial_data(ser, logger, room) 128 | await asyncio.sleep(0.01) # Read at 100Hz to ensure we don't miss data 129 | 130 | # Start the periodic tasks if we have a serial connection 131 | if ser and ser.is_open: 132 | asyncio.create_task(periodic_imu_query()) 133 | asyncio.create_task(periodic_serial_read()) 134 | 135 | # handler for receiving data packet 136 | @room.on("data_received") 137 | def on_data_received(data: rtc.DataPacket): 138 | logger.info("Received data from %s topic: %s", data.participant.identity, data.topic) 139 | try: 140 | # Decode and parse the data 141 | decoded_data = data.data.decode('utf-8') 142 | 143 | # Try to parse as JSON 144 | json_data = json.loads(decoded_data) 145 | 146 | # First validate that data is of type 'gamepad' 147 | if not json_data.get('type') == 'gamepad': 148 | logger.info("Received data is not of type 'gamepad', ignoring") 149 | return 150 | 151 | # Get the gamepad data 152 | gamepad_data = json_data.get('data', {}) 153 | 154 | # Check if we have the expected thumbstick values 155 | if all(k in gamepad_data for k in ["left_x", "left_y", "right_x", "right_y"]): 156 | # Get the throttle (left_y) and steering (right_x) values 157 | # Gamepad values are typically in range [-1, 1] 158 | throttle = float(gamepad_data['left_y']) 159 | steering = float(gamepad_data['right_x']) 160 | 161 | # Scale throttle to [-0.5, 0.5] range 162 | throttle_scaled = round(throttle * 0.5, 3) 163 | 164 | # Apply Gord_W's formula: y = a * x^3 + (1-a) * x 165 | # Using a = 0.5 for a good balance between linear and cubic response 166 | a = 0.5 167 | steering_curved = a * (steering ** 3) + (1 - a) * steering 168 | 169 | # Calculate base steering effect (opposing motor commands) 170 | steering_effect = steering_curved * 0.3 # Scale steering effect 171 | 172 | # Invert steering when in reverse 173 | if throttle_scaled < 0: 174 | steering_effect = -steering_effect 175 | 176 | # Mix throttle and steering 177 | left_motor = throttle_scaled + steering_effect 178 | right_motor = throttle_scaled - steering_effect 179 | 180 | # Ensure values stay within the valid range [-0.5, 0.5] 181 | left_motor = max(min(left_motor, 0.5), -0.5) 182 | right_motor = max(min(right_motor, 0.5), -0.5) 183 | 184 | # Round to 3 decimal places 185 | left_motor = round(left_motor, 3) 186 | right_motor = round(right_motor, 3) 187 | 188 | # Create command JSON as specified 189 | command_data = { 190 | "T": 1, # Type 1 for motor control 191 | "L": left_motor, 192 | "R": right_motor 193 | } 194 | print(f"command_data: {command_data}") 195 | # Convert to JSON string 196 | command_json = json.dumps(command_data) 197 | 198 | # Forward to serial port if connection is available 199 | if ser and ser.is_open: 200 | # Add newline for serial transmission 201 | serial_command = command_json + "\n" 202 | ser.write(serial_command.encode()) 203 | logger.info(f"Successfully sent to serial port: {command_json}") 204 | else: 205 | logger.info("Serial connection not available - data logged but not sent") 206 | else: 207 | logger.info("Received data does not contain expected thumbstick values") 208 | 209 | except (UnicodeDecodeError, json.JSONDecodeError) as e: 210 | logger.error(f"Error decoding/parsing data: {e}") 211 | except Exception as e: 212 | logger.error(f"Error processing data: {e}") 213 | 214 | token = generate_token(ROOM_NAME, "rover", "Rover Receiver") 215 | await room.connect(LIVEKIT_URL, token, rtc.RoomOptions(auto_subscribe=False)) 216 | logger.info("Connected to room %s", room.name) 217 | 218 | if not ser: 219 | logger.warning("Running without serial connection - will only log received gamepad data") 220 | else: 221 | logger.info("Ready to forward gamepad controls to serial port") 222 | 223 | 224 | if __name__ == "__main__": 225 | logging.basicConfig( 226 | level=logging.INFO, 227 | handlers=[ 228 | logging.FileHandler("rover.log"), 229 | logging.StreamHandler(), 230 | ], 231 | ) 232 | 233 | loop = asyncio.get_event_loop() 234 | room = rtc.Room(loop=loop) 235 | 236 | async def cleanup(): 237 | await room.disconnect() 238 | loop.stop() 239 | 240 | asyncio.ensure_future(main(room)) 241 | for signal in [SIGINT, SIGTERM]: 242 | loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) 243 | 244 | try: 245 | loop.run_forever() 246 | finally: 247 | loop.close() 248 | 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /controller/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: transitive 6 | description: 7 | name: args 8 | sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.7.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.12.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.2" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.4.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.2" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.19.1" 52 | connectivity_plus: 53 | dependency: transitive 54 | description: 55 | name: connectivity_plus 56 | sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "6.1.4" 60 | connectivity_plus_platform_interface: 61 | dependency: transitive 62 | description: 63 | name: connectivity_plus_platform_interface 64 | sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.0.1" 68 | crypto: 69 | dependency: transitive 70 | description: 71 | name: crypto 72 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "3.0.6" 76 | cupertino_icons: 77 | dependency: "direct main" 78 | description: 79 | name: cupertino_icons 80 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.0.8" 84 | dart_webrtc: 85 | dependency: transitive 86 | description: 87 | name: dart_webrtc 88 | sha256: "8565f1f1f412b8a6fd862f3a157560811e61eeeac26741c735a5d2ff409a0202" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.5.3" 92 | dbus: 93 | dependency: transitive 94 | description: 95 | name: dbus 96 | sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "0.7.11" 100 | device_info_plus: 101 | dependency: transitive 102 | description: 103 | name: device_info_plus 104 | sha256: "0c6396126421b590089447154c5f98a5de423b70cfb15b1578fd018843ee6f53" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "11.4.0" 108 | device_info_plus_platform_interface: 109 | dependency: transitive 110 | description: 111 | name: device_info_plus_platform_interface 112 | sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "7.0.2" 116 | equatable: 117 | dependency: transitive 118 | description: 119 | name: equatable 120 | sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "2.0.7" 124 | fake_async: 125 | dependency: transitive 126 | description: 127 | name: fake_async 128 | sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.3.2" 132 | ffi: 133 | dependency: transitive 134 | description: 135 | name: ffi 136 | sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.1.4" 140 | file: 141 | dependency: transitive 142 | description: 143 | name: file 144 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "7.0.1" 148 | fixnum: 149 | dependency: transitive 150 | description: 151 | name: fixnum 152 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "1.1.1" 156 | fl_chart: 157 | dependency: "direct main" 158 | description: 159 | name: fl_chart 160 | sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "0.66.2" 164 | flutter: 165 | dependency: "direct main" 166 | description: flutter 167 | source: sdk 168 | version: "0.0.0" 169 | flutter_dotenv: 170 | dependency: "direct main" 171 | description: 172 | name: flutter_dotenv 173 | sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b 174 | url: "https://pub.dev" 175 | source: hosted 176 | version: "5.2.1" 177 | flutter_lints: 178 | dependency: "direct dev" 179 | description: 180 | name: flutter_lints 181 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" 182 | url: "https://pub.dev" 183 | source: hosted 184 | version: "5.0.0" 185 | flutter_test: 186 | dependency: "direct dev" 187 | description: flutter 188 | source: sdk 189 | version: "0.0.0" 190 | flutter_web_plugins: 191 | dependency: transitive 192 | description: flutter 193 | source: sdk 194 | version: "0.0.0" 195 | flutter_webrtc: 196 | dependency: transitive 197 | description: 198 | name: flutter_webrtc 199 | sha256: "23af761bb812e6f32c34263e62df48aaf26bb08582c8be8cc3170e2a018d1155" 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "0.13.1+hotfix.1" 203 | gamepads: 204 | dependency: "direct main" 205 | description: 206 | name: gamepads 207 | sha256: "6201a1ea34a9ae80842bd0fd12770b75e0274f721238b1089b7545d6871fdac5" 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "0.1.5" 211 | gamepads_android: 212 | dependency: transitive 213 | description: 214 | name: gamepads_android 215 | sha256: c57d5bfb75fe3ce3760a04f05b9dd2434c7c7cf5647445d81034667ed4b9ee98 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "0.1.3" 219 | gamepads_darwin: 220 | dependency: transitive 221 | description: 222 | name: gamepads_darwin 223 | sha256: "91250975ee196703816c55117502ec53ce4a1b881ca50dec1d0fbcb9fbb75eff" 224 | url: "https://pub.dev" 225 | source: hosted 226 | version: "0.1.2+2" 227 | gamepads_ios: 228 | dependency: transitive 229 | description: 230 | name: gamepads_ios 231 | sha256: aaea0aadc1a386f7772c096677b111e7dc2e12f92d0dfd7df8db78cb1b112795 232 | url: "https://pub.dev" 233 | source: hosted 234 | version: "0.1.2+2" 235 | gamepads_linux: 236 | dependency: transitive 237 | description: 238 | name: gamepads_linux 239 | sha256: f4c17915a84400d7f624aadb6371424ada596eedff2a25663121453e65917e0d 240 | url: "https://pub.dev" 241 | source: hosted 242 | version: "0.1.1+3" 243 | gamepads_platform_interface: 244 | dependency: transitive 245 | description: 246 | name: gamepads_platform_interface 247 | sha256: ddab8677a4137d92e381b04cc97a8081ae4b75673dc0f24c846618d2b5226c4f 248 | url: "https://pub.dev" 249 | source: hosted 250 | version: "0.1.2+1" 251 | gamepads_windows: 252 | dependency: transitive 253 | description: 254 | name: gamepads_windows 255 | sha256: "3af20fef8b554e0bbfb6c94f07a264e186ecf8f7829448b162c22dcc8a3c155b" 256 | url: "https://pub.dev" 257 | source: hosted 258 | version: "0.1.2" 259 | google_fonts: 260 | dependency: "direct main" 261 | description: 262 | name: google_fonts 263 | sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 264 | url: "https://pub.dev" 265 | source: hosted 266 | version: "6.2.1" 267 | http: 268 | dependency: transitive 269 | description: 270 | name: http 271 | sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f 272 | url: "https://pub.dev" 273 | source: hosted 274 | version: "1.3.0" 275 | http_parser: 276 | dependency: transitive 277 | description: 278 | name: http_parser 279 | sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" 280 | url: "https://pub.dev" 281 | source: hosted 282 | version: "4.1.2" 283 | js: 284 | dependency: transitive 285 | description: 286 | name: js 287 | sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "0.7.2" 291 | leak_tracker: 292 | dependency: transitive 293 | description: 294 | name: leak_tracker 295 | sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "10.0.8" 299 | leak_tracker_flutter_testing: 300 | dependency: transitive 301 | description: 302 | name: leak_tracker_flutter_testing 303 | sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "3.0.9" 307 | leak_tracker_testing: 308 | dependency: transitive 309 | description: 310 | name: leak_tracker_testing 311 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "3.0.1" 315 | lints: 316 | dependency: transitive 317 | description: 318 | name: lints 319 | sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "5.1.1" 323 | livekit_client: 324 | dependency: "direct main" 325 | description: 326 | name: livekit_client 327 | sha256: b50f478afcba14db90e014de543fc988d3529cf5aa168de88a556d84bc3f8df3 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "2.4.5" 331 | logging: 332 | dependency: transitive 333 | description: 334 | name: logging 335 | sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "1.3.0" 339 | matcher: 340 | dependency: transitive 341 | description: 342 | name: matcher 343 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "0.12.17" 347 | material_color_utilities: 348 | dependency: transitive 349 | description: 350 | name: material_color_utilities 351 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 352 | url: "https://pub.dev" 353 | source: hosted 354 | version: "0.11.1" 355 | meta: 356 | dependency: transitive 357 | description: 358 | name: meta 359 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 360 | url: "https://pub.dev" 361 | source: hosted 362 | version: "1.16.0" 363 | mime_type: 364 | dependency: transitive 365 | description: 366 | name: mime_type 367 | sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb 368 | url: "https://pub.dev" 369 | source: hosted 370 | version: "1.0.1" 371 | nm: 372 | dependency: transitive 373 | description: 374 | name: nm 375 | sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" 376 | url: "https://pub.dev" 377 | source: hosted 378 | version: "0.5.0" 379 | path: 380 | dependency: transitive 381 | description: 382 | name: path 383 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 384 | url: "https://pub.dev" 385 | source: hosted 386 | version: "1.9.1" 387 | path_provider: 388 | dependency: transitive 389 | description: 390 | name: path_provider 391 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" 392 | url: "https://pub.dev" 393 | source: hosted 394 | version: "2.1.5" 395 | path_provider_android: 396 | dependency: transitive 397 | description: 398 | name: path_provider_android 399 | sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 400 | url: "https://pub.dev" 401 | source: hosted 402 | version: "2.2.17" 403 | path_provider_foundation: 404 | dependency: transitive 405 | description: 406 | name: path_provider_foundation 407 | sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" 408 | url: "https://pub.dev" 409 | source: hosted 410 | version: "2.4.1" 411 | path_provider_linux: 412 | dependency: transitive 413 | description: 414 | name: path_provider_linux 415 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 416 | url: "https://pub.dev" 417 | source: hosted 418 | version: "2.2.1" 419 | path_provider_platform_interface: 420 | dependency: transitive 421 | description: 422 | name: path_provider_platform_interface 423 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 424 | url: "https://pub.dev" 425 | source: hosted 426 | version: "2.1.2" 427 | path_provider_windows: 428 | dependency: transitive 429 | description: 430 | name: path_provider_windows 431 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 432 | url: "https://pub.dev" 433 | source: hosted 434 | version: "2.3.0" 435 | petitparser: 436 | dependency: transitive 437 | description: 438 | name: petitparser 439 | sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" 440 | url: "https://pub.dev" 441 | source: hosted 442 | version: "6.1.0" 443 | platform: 444 | dependency: transitive 445 | description: 446 | name: platform 447 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 448 | url: "https://pub.dev" 449 | source: hosted 450 | version: "3.1.6" 451 | plugin_platform_interface: 452 | dependency: transitive 453 | description: 454 | name: plugin_platform_interface 455 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 456 | url: "https://pub.dev" 457 | source: hosted 458 | version: "2.1.8" 459 | protobuf: 460 | dependency: transitive 461 | description: 462 | name: protobuf 463 | sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" 464 | url: "https://pub.dev" 465 | source: hosted 466 | version: "3.1.0" 467 | sdp_transform: 468 | dependency: transitive 469 | description: 470 | name: sdp_transform 471 | sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45" 472 | url: "https://pub.dev" 473 | source: hosted 474 | version: "0.3.2" 475 | sky_engine: 476 | dependency: transitive 477 | description: flutter 478 | source: sdk 479 | version: "0.0.0" 480 | source_span: 481 | dependency: transitive 482 | description: 483 | name: source_span 484 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "1.10.1" 488 | sprintf: 489 | dependency: transitive 490 | description: 491 | name: sprintf 492 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "7.0.0" 496 | stack_trace: 497 | dependency: transitive 498 | description: 499 | name: stack_trace 500 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "1.12.1" 504 | stream_channel: 505 | dependency: transitive 506 | description: 507 | name: stream_channel 508 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "2.1.4" 512 | string_scanner: 513 | dependency: transitive 514 | description: 515 | name: string_scanner 516 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 517 | url: "https://pub.dev" 518 | source: hosted 519 | version: "1.4.1" 520 | synchronized: 521 | dependency: transitive 522 | description: 523 | name: synchronized 524 | sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" 525 | url: "https://pub.dev" 526 | source: hosted 527 | version: "3.3.1" 528 | term_glyph: 529 | dependency: transitive 530 | description: 531 | name: term_glyph 532 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 533 | url: "https://pub.dev" 534 | source: hosted 535 | version: "1.2.2" 536 | test_api: 537 | dependency: transitive 538 | description: 539 | name: test_api 540 | sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd 541 | url: "https://pub.dev" 542 | source: hosted 543 | version: "0.7.4" 544 | typed_data: 545 | dependency: transitive 546 | description: 547 | name: typed_data 548 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 549 | url: "https://pub.dev" 550 | source: hosted 551 | version: "1.4.0" 552 | uuid: 553 | dependency: transitive 554 | description: 555 | name: uuid 556 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff 557 | url: "https://pub.dev" 558 | source: hosted 559 | version: "4.5.1" 560 | vector_math: 561 | dependency: transitive 562 | description: 563 | name: vector_math 564 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 565 | url: "https://pub.dev" 566 | source: hosted 567 | version: "2.1.4" 568 | vm_service: 569 | dependency: transitive 570 | description: 571 | name: vm_service 572 | sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" 573 | url: "https://pub.dev" 574 | source: hosted 575 | version: "14.3.1" 576 | web: 577 | dependency: transitive 578 | description: 579 | name: web 580 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 581 | url: "https://pub.dev" 582 | source: hosted 583 | version: "1.1.1" 584 | webrtc_interface: 585 | dependency: transitive 586 | description: 587 | name: webrtc_interface 588 | sha256: e92afec11152a9ccb5c9f35482754edd99696e886ab6acaf90c06dd2d09f09eb 589 | url: "https://pub.dev" 590 | source: hosted 591 | version: "1.2.2+hotfix.1" 592 | win32: 593 | dependency: transitive 594 | description: 595 | name: win32 596 | sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f 597 | url: "https://pub.dev" 598 | source: hosted 599 | version: "5.12.0" 600 | win32_registry: 601 | dependency: transitive 602 | description: 603 | name: win32_registry 604 | sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" 605 | url: "https://pub.dev" 606 | source: hosted 607 | version: "2.1.0" 608 | xdg_directories: 609 | dependency: transitive 610 | description: 611 | name: xdg_directories 612 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 613 | url: "https://pub.dev" 614 | source: hosted 615 | version: "1.1.0" 616 | xml: 617 | dependency: transitive 618 | description: 619 | name: xml 620 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 621 | url: "https://pub.dev" 622 | source: hosted 623 | version: "6.5.0" 624 | sdks: 625 | dart: ">=3.7.2 <4.0.0" 626 | flutter: ">=3.27.0" 627 | -------------------------------------------------------------------------------- /controller/macos/Runner/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 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 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | -------------------------------------------------------------------------------- /controller/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 13 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 14 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 15 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 16 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXContainerItemProxy section */ 20 | 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { 21 | isa = PBXContainerItemProxy; 22 | containerPortal = 97C146E61CF9000F007C117D /* Project object */; 23 | proxyType = 1; 24 | remoteGlobalIDString = 97C146ED1CF9000F007C117D; 25 | remoteInfo = Runner; 26 | }; 27 | /* End PBXContainerItemProxy section */ 28 | 29 | /* Begin PBXCopyFilesBuildPhase section */ 30 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 31 | isa = PBXCopyFilesBuildPhase; 32 | buildActionMask = 2147483647; 33 | dstPath = ""; 34 | dstSubfolderSpec = 10; 35 | files = ( 36 | ); 37 | name = "Embed Frameworks"; 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXCopyFilesBuildPhase section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 44 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 45 | 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 46 | 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 48 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 49 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 51 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 52 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 53 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 55 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 57 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | 331C8082294A63A400263BE5 /* RunnerTests */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 331C807B294A618700263BE5 /* RunnerTests.swift */, 75 | ); 76 | path = RunnerTests; 77 | sourceTree = ""; 78 | }; 79 | 9740EEB11CF90186004384FC /* Flutter */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 83 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 84 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 85 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 86 | ); 87 | name = Flutter; 88 | sourceTree = ""; 89 | }; 90 | 97C146E51CF9000F007C117D = { 91 | isa = PBXGroup; 92 | children = ( 93 | 9740EEB11CF90186004384FC /* Flutter */, 94 | 97C146F01CF9000F007C117D /* Runner */, 95 | 97C146EF1CF9000F007C117D /* Products */, 96 | 331C8082294A63A400263BE5 /* RunnerTests */, 97 | ); 98 | sourceTree = ""; 99 | }; 100 | 97C146EF1CF9000F007C117D /* Products */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 97C146EE1CF9000F007C117D /* Runner.app */, 104 | 331C8081294A63A400263BE5 /* RunnerTests.xctest */, 105 | ); 106 | name = Products; 107 | sourceTree = ""; 108 | }; 109 | 97C146F01CF9000F007C117D /* Runner */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 113 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 114 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 115 | 97C147021CF9000F007C117D /* Info.plist */, 116 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 117 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 118 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 119 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 120 | ); 121 | path = Runner; 122 | sourceTree = ""; 123 | }; 124 | /* End PBXGroup section */ 125 | 126 | /* Begin PBXNativeTarget section */ 127 | 331C8080294A63A400263BE5 /* RunnerTests */ = { 128 | isa = PBXNativeTarget; 129 | buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; 130 | buildPhases = ( 131 | 331C807D294A63A400263BE5 /* Sources */, 132 | 331C807F294A63A400263BE5 /* Resources */, 133 | ); 134 | buildRules = ( 135 | ); 136 | dependencies = ( 137 | 331C8086294A63A400263BE5 /* PBXTargetDependency */, 138 | ); 139 | name = RunnerTests; 140 | productName = RunnerTests; 141 | productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; 142 | productType = "com.apple.product-type.bundle.unit-test"; 143 | }; 144 | 97C146ED1CF9000F007C117D /* Runner */ = { 145 | isa = PBXNativeTarget; 146 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 147 | buildPhases = ( 148 | 9740EEB61CF901F6004384FC /* Run Script */, 149 | 97C146EA1CF9000F007C117D /* Sources */, 150 | 97C146EB1CF9000F007C117D /* Frameworks */, 151 | 97C146EC1CF9000F007C117D /* Resources */, 152 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 153 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 154 | ); 155 | buildRules = ( 156 | ); 157 | dependencies = ( 158 | ); 159 | name = Runner; 160 | productName = Runner; 161 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 162 | productType = "com.apple.product-type.application"; 163 | }; 164 | /* End PBXNativeTarget section */ 165 | 166 | /* Begin PBXProject section */ 167 | 97C146E61CF9000F007C117D /* Project object */ = { 168 | isa = PBXProject; 169 | attributes = { 170 | BuildIndependentTargetsInParallel = YES; 171 | LastUpgradeCheck = 1510; 172 | ORGANIZATIONNAME = ""; 173 | TargetAttributes = { 174 | 331C8080294A63A400263BE5 = { 175 | CreatedOnToolsVersion = 14.0; 176 | TestTargetID = 97C146ED1CF9000F007C117D; 177 | }; 178 | 97C146ED1CF9000F007C117D = { 179 | CreatedOnToolsVersion = 7.3.1; 180 | LastSwiftMigration = 1100; 181 | }; 182 | }; 183 | }; 184 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 185 | compatibilityVersion = "Xcode 9.3"; 186 | developmentRegion = en; 187 | hasScannedForEncodings = 0; 188 | knownRegions = ( 189 | en, 190 | Base, 191 | ); 192 | mainGroup = 97C146E51CF9000F007C117D; 193 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 194 | projectDirPath = ""; 195 | projectRoot = ""; 196 | targets = ( 197 | 97C146ED1CF9000F007C117D /* Runner */, 198 | 331C8080294A63A400263BE5 /* RunnerTests */, 199 | ); 200 | }; 201 | /* End PBXProject section */ 202 | 203 | /* Begin PBXResourcesBuildPhase section */ 204 | 331C807F294A63A400263BE5 /* Resources */ = { 205 | isa = PBXResourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | 97C146EC1CF9000F007C117D /* Resources */ = { 212 | isa = PBXResourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 216 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 217 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 218 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | /* End PBXResourcesBuildPhase section */ 223 | 224 | /* Begin PBXShellScriptBuildPhase section */ 225 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 226 | isa = PBXShellScriptBuildPhase; 227 | alwaysOutOfDate = 1; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | ); 231 | inputPaths = ( 232 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 233 | ); 234 | name = "Thin Binary"; 235 | outputPaths = ( 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | shellPath = /bin/sh; 239 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 240 | }; 241 | 9740EEB61CF901F6004384FC /* Run Script */ = { 242 | isa = PBXShellScriptBuildPhase; 243 | alwaysOutOfDate = 1; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | ); 247 | inputPaths = ( 248 | ); 249 | name = "Run Script"; 250 | outputPaths = ( 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | shellPath = /bin/sh; 254 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 255 | }; 256 | /* End PBXShellScriptBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | 331C807D294A63A400263BE5 /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | 97C146EA1CF9000F007C117D /* Sources */ = { 268 | isa = PBXSourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 272 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXSourcesBuildPhase section */ 277 | 278 | /* Begin PBXTargetDependency section */ 279 | 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { 280 | isa = PBXTargetDependency; 281 | target = 97C146ED1CF9000F007C117D /* Runner */; 282 | targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; 283 | }; 284 | /* End PBXTargetDependency section */ 285 | 286 | /* Begin PBXVariantGroup section */ 287 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 288 | isa = PBXVariantGroup; 289 | children = ( 290 | 97C146FB1CF9000F007C117D /* Base */, 291 | ); 292 | name = Main.storyboard; 293 | sourceTree = ""; 294 | }; 295 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 296 | isa = PBXVariantGroup; 297 | children = ( 298 | 97C147001CF9000F007C117D /* Base */, 299 | ); 300 | name = LaunchScreen.storyboard; 301 | sourceTree = ""; 302 | }; 303 | /* End PBXVariantGroup section */ 304 | 305 | /* Begin XCBuildConfiguration section */ 306 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ALWAYS_SEARCH_USER_PATHS = NO; 310 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 311 | CLANG_ANALYZER_NONNULL = YES; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 330 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 331 | CLANG_WARN_STRICT_PROTOTYPES = YES; 332 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 336 | COPY_PHASE_STRIP = NO; 337 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 338 | ENABLE_NS_ASSERTIONS = NO; 339 | ENABLE_STRICT_OBJC_MSGSEND = YES; 340 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 341 | GCC_C_LANGUAGE_STANDARD = gnu99; 342 | GCC_NO_COMMON_BLOCKS = YES; 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 350 | MTL_ENABLE_DEBUG_INFO = NO; 351 | SDKROOT = iphoneos; 352 | SUPPORTED_PLATFORMS = iphoneos; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | VALIDATE_PRODUCT = YES; 355 | }; 356 | name = Profile; 357 | }; 358 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 359 | isa = XCBuildConfiguration; 360 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 361 | buildSettings = { 362 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 363 | CLANG_ENABLE_MODULES = YES; 364 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 365 | ENABLE_BITCODE = NO; 366 | INFOPLIST_FILE = Runner/Info.plist; 367 | LD_RUNPATH_SEARCH_PATHS = ( 368 | "$(inherited)", 369 | "@executable_path/Frameworks", 370 | ); 371 | PRODUCT_BUNDLE_IDENTIFIER = com.example.controller; 372 | PRODUCT_NAME = "$(TARGET_NAME)"; 373 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 374 | SWIFT_VERSION = 5.0; 375 | VERSIONING_SYSTEM = "apple-generic"; 376 | }; 377 | name = Profile; 378 | }; 379 | 331C8088294A63A400263BE5 /* Debug */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | BUNDLE_LOADER = "$(TEST_HOST)"; 383 | CODE_SIGN_STYLE = Automatic; 384 | CURRENT_PROJECT_VERSION = 1; 385 | GENERATE_INFOPLIST_FILE = YES; 386 | MARKETING_VERSION = 1.0; 387 | PRODUCT_BUNDLE_IDENTIFIER = com.example.controller.RunnerTests; 388 | PRODUCT_NAME = "$(TARGET_NAME)"; 389 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 390 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 391 | SWIFT_VERSION = 5.0; 392 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; 393 | }; 394 | name = Debug; 395 | }; 396 | 331C8089294A63A400263BE5 /* Release */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | BUNDLE_LOADER = "$(TEST_HOST)"; 400 | CODE_SIGN_STYLE = Automatic; 401 | CURRENT_PROJECT_VERSION = 1; 402 | GENERATE_INFOPLIST_FILE = YES; 403 | MARKETING_VERSION = 1.0; 404 | PRODUCT_BUNDLE_IDENTIFIER = com.example.controller.RunnerTests; 405 | PRODUCT_NAME = "$(TARGET_NAME)"; 406 | SWIFT_VERSION = 5.0; 407 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; 408 | }; 409 | name = Release; 410 | }; 411 | 331C808A294A63A400263BE5 /* Profile */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | BUNDLE_LOADER = "$(TEST_HOST)"; 415 | CODE_SIGN_STYLE = Automatic; 416 | CURRENT_PROJECT_VERSION = 1; 417 | GENERATE_INFOPLIST_FILE = YES; 418 | MARKETING_VERSION = 1.0; 419 | PRODUCT_BUNDLE_IDENTIFIER = com.example.controller.RunnerTests; 420 | PRODUCT_NAME = "$(TARGET_NAME)"; 421 | SWIFT_VERSION = 5.0; 422 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; 423 | }; 424 | name = Profile; 425 | }; 426 | 97C147031CF9000F007C117D /* Debug */ = { 427 | isa = XCBuildConfiguration; 428 | buildSettings = { 429 | ALWAYS_SEARCH_USER_PATHS = NO; 430 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 431 | CLANG_ANALYZER_NONNULL = YES; 432 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 433 | CLANG_CXX_LIBRARY = "libc++"; 434 | CLANG_ENABLE_MODULES = YES; 435 | CLANG_ENABLE_OBJC_ARC = YES; 436 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 437 | CLANG_WARN_BOOL_CONVERSION = YES; 438 | CLANG_WARN_COMMA = YES; 439 | CLANG_WARN_CONSTANT_CONVERSION = YES; 440 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 441 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 442 | CLANG_WARN_EMPTY_BODY = YES; 443 | CLANG_WARN_ENUM_CONVERSION = YES; 444 | CLANG_WARN_INFINITE_RECURSION = YES; 445 | CLANG_WARN_INT_CONVERSION = YES; 446 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 447 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 448 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 449 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 450 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 451 | CLANG_WARN_STRICT_PROTOTYPES = YES; 452 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 453 | CLANG_WARN_UNREACHABLE_CODE = YES; 454 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 455 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 456 | COPY_PHASE_STRIP = NO; 457 | DEBUG_INFORMATION_FORMAT = dwarf; 458 | ENABLE_STRICT_OBJC_MSGSEND = YES; 459 | ENABLE_TESTABILITY = YES; 460 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 461 | GCC_C_LANGUAGE_STANDARD = gnu99; 462 | GCC_DYNAMIC_NO_PIC = NO; 463 | GCC_NO_COMMON_BLOCKS = YES; 464 | GCC_OPTIMIZATION_LEVEL = 0; 465 | GCC_PREPROCESSOR_DEFINITIONS = ( 466 | "DEBUG=1", 467 | "$(inherited)", 468 | ); 469 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 470 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 471 | GCC_WARN_UNDECLARED_SELECTOR = YES; 472 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 473 | GCC_WARN_UNUSED_FUNCTION = YES; 474 | GCC_WARN_UNUSED_VARIABLE = YES; 475 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 476 | MTL_ENABLE_DEBUG_INFO = YES; 477 | ONLY_ACTIVE_ARCH = YES; 478 | SDKROOT = iphoneos; 479 | TARGETED_DEVICE_FAMILY = "1,2"; 480 | }; 481 | name = Debug; 482 | }; 483 | 97C147041CF9000F007C117D /* Release */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | ALWAYS_SEARCH_USER_PATHS = NO; 487 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 488 | CLANG_ANALYZER_NONNULL = YES; 489 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 490 | CLANG_CXX_LIBRARY = "libc++"; 491 | CLANG_ENABLE_MODULES = YES; 492 | CLANG_ENABLE_OBJC_ARC = YES; 493 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 494 | CLANG_WARN_BOOL_CONVERSION = YES; 495 | CLANG_WARN_COMMA = YES; 496 | CLANG_WARN_CONSTANT_CONVERSION = YES; 497 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 498 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 499 | CLANG_WARN_EMPTY_BODY = YES; 500 | CLANG_WARN_ENUM_CONVERSION = YES; 501 | CLANG_WARN_INFINITE_RECURSION = YES; 502 | CLANG_WARN_INT_CONVERSION = YES; 503 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 504 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 505 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 506 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 507 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 508 | CLANG_WARN_STRICT_PROTOTYPES = YES; 509 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 510 | CLANG_WARN_UNREACHABLE_CODE = YES; 511 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 512 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 513 | COPY_PHASE_STRIP = NO; 514 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 515 | ENABLE_NS_ASSERTIONS = NO; 516 | ENABLE_STRICT_OBJC_MSGSEND = YES; 517 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 518 | GCC_C_LANGUAGE_STANDARD = gnu99; 519 | GCC_NO_COMMON_BLOCKS = YES; 520 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 521 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 522 | GCC_WARN_UNDECLARED_SELECTOR = YES; 523 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 524 | GCC_WARN_UNUSED_FUNCTION = YES; 525 | GCC_WARN_UNUSED_VARIABLE = YES; 526 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 527 | MTL_ENABLE_DEBUG_INFO = NO; 528 | SDKROOT = iphoneos; 529 | SUPPORTED_PLATFORMS = iphoneos; 530 | SWIFT_COMPILATION_MODE = wholemodule; 531 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 532 | TARGETED_DEVICE_FAMILY = "1,2"; 533 | VALIDATE_PRODUCT = YES; 534 | }; 535 | name = Release; 536 | }; 537 | 97C147061CF9000F007C117D /* Debug */ = { 538 | isa = XCBuildConfiguration; 539 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 540 | buildSettings = { 541 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 542 | CLANG_ENABLE_MODULES = YES; 543 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 544 | ENABLE_BITCODE = NO; 545 | INFOPLIST_FILE = Runner/Info.plist; 546 | LD_RUNPATH_SEARCH_PATHS = ( 547 | "$(inherited)", 548 | "@executable_path/Frameworks", 549 | ); 550 | PRODUCT_BUNDLE_IDENTIFIER = com.example.controller; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 553 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 554 | SWIFT_VERSION = 5.0; 555 | VERSIONING_SYSTEM = "apple-generic"; 556 | }; 557 | name = Debug; 558 | }; 559 | 97C147071CF9000F007C117D /* Release */ = { 560 | isa = XCBuildConfiguration; 561 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 562 | buildSettings = { 563 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 564 | CLANG_ENABLE_MODULES = YES; 565 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 566 | ENABLE_BITCODE = NO; 567 | INFOPLIST_FILE = Runner/Info.plist; 568 | LD_RUNPATH_SEARCH_PATHS = ( 569 | "$(inherited)", 570 | "@executable_path/Frameworks", 571 | ); 572 | PRODUCT_BUNDLE_IDENTIFIER = com.example.controller; 573 | PRODUCT_NAME = "$(TARGET_NAME)"; 574 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 575 | SWIFT_VERSION = 5.0; 576 | VERSIONING_SYSTEM = "apple-generic"; 577 | }; 578 | name = Release; 579 | }; 580 | /* End XCBuildConfiguration section */ 581 | 582 | /* Begin XCConfigurationList section */ 583 | 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { 584 | isa = XCConfigurationList; 585 | buildConfigurations = ( 586 | 331C8088294A63A400263BE5 /* Debug */, 587 | 331C8089294A63A400263BE5 /* Release */, 588 | 331C808A294A63A400263BE5 /* Profile */, 589 | ); 590 | defaultConfigurationIsVisible = 0; 591 | defaultConfigurationName = Release; 592 | }; 593 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 594 | isa = XCConfigurationList; 595 | buildConfigurations = ( 596 | 97C147031CF9000F007C117D /* Debug */, 597 | 97C147041CF9000F007C117D /* Release */, 598 | 249021D3217E4FDB00AE95B9 /* Profile */, 599 | ); 600 | defaultConfigurationIsVisible = 0; 601 | defaultConfigurationName = Release; 602 | }; 603 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 604 | isa = XCConfigurationList; 605 | buildConfigurations = ( 606 | 97C147061CF9000F007C117D /* Debug */, 607 | 97C147071CF9000F007C117D /* Release */, 608 | 249021D4217E4FDB00AE95B9 /* Profile */, 609 | ); 610 | defaultConfigurationIsVisible = 0; 611 | defaultConfigurationName = Release; 612 | }; 613 | /* End XCConfigurationList section */ 614 | }; 615 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 616 | } 617 | -------------------------------------------------------------------------------- /controller/lib/components/controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:async'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:livekit_client/livekit_client.dart' as lk; 5 | import 'package:rover_controller/services/gamepad_service.dart'; 6 | import 'package:fl_chart/fl_chart.dart'; 7 | 8 | class Controller extends StatefulWidget { 9 | final String url; 10 | final String token; 11 | 12 | const Controller({super.key, required this.url, required this.token}); 13 | 14 | @override 15 | State createState() => _ControllerState(); 16 | } 17 | 18 | class _ControllerState extends State { 19 | lk.Room? _room; 20 | lk.EventsListener? _listener; 21 | bool _connecting = false; 22 | bool _connected = false; 23 | String? _errorMessage; 24 | List _participants = []; 25 | lk.VideoTrack? _roverVideoTrack; 26 | lk.Participant? _roverParticipant; 27 | final GamepadService _gamepadService = GamepadService(); 28 | bool _sendingControls = false; 29 | bool _sendingAudio = false; 30 | lk.LocalAudioTrack? _microphoneTrack; 31 | Timer? _controlsTimer; 32 | 33 | // IMU data storage 34 | final List _accelXSpots = []; 35 | final List _accelYSpots = []; 36 | final List _accelZSpots = []; 37 | final List _gyroXSpots = []; 38 | final List _gyroYSpots = []; 39 | final List _gyroZSpots = []; 40 | final List _rollSpots = []; 41 | final List _pitchSpots = []; 42 | final List _yawSpots = []; 43 | double _maxAccelValue = 0.0; 44 | double _maxGyroValue = 0.0; 45 | double _maxOrientValue = 0.0; 46 | double _temperature = 0.0; 47 | static const int _maxDataPoints = 50; // Keep last 50 data points 48 | static const double _minYRange = 49 | 1000.0; // Minimum Y-axis range for accelerometer 50 | static const double _minGyroRange = 51 | 100.0; // Minimum Y-axis range for gyroscope 52 | static const double _minOrientRange = 53 | 90.0; // Minimum Y-axis range for orientation 54 | static const double _timeWindow = 5.0; // Show last 5 seconds of data 55 | 56 | @override 57 | void initState() { 58 | super.initState(); 59 | _connectToLiveKit(); 60 | } 61 | 62 | @override 63 | void dispose() { 64 | _disconnectFromLiveKit(); 65 | _listener?.dispose(); 66 | _controlsTimer?.cancel(); 67 | _stopSendingAudio(); 68 | super.dispose(); 69 | } 70 | 71 | void _handleImuData(Map data) { 72 | if (data['type'] != 'imu') return; 73 | 74 | final accel = data['data']['accel']; 75 | final gyro = data['data']['gyro']; 76 | final orientation = data['data']['orientation']; 77 | final timestamp = DateTime.now().millisecondsSinceEpoch / 1000.0; 78 | 79 | setState(() { 80 | // Update temperature 81 | _temperature = data['data']['temp'].toDouble(); 82 | 83 | // Add new accelerometer data points 84 | _accelXSpots.add(FlSpot(timestamp, accel['x'].toDouble())); 85 | _accelYSpots.add(FlSpot(timestamp, accel['y'].toDouble())); 86 | _accelZSpots.add(FlSpot(timestamp, accel['z'].toDouble())); 87 | 88 | // Add new gyroscope data points 89 | _gyroXSpots.add(FlSpot(timestamp, gyro['x'].toDouble())); 90 | _gyroYSpots.add(FlSpot(timestamp, gyro['y'].toDouble())); 91 | _gyroZSpots.add(FlSpot(timestamp, gyro['z'].toDouble())); 92 | 93 | // Add new orientation data points 94 | _rollSpots.add(FlSpot(timestamp, orientation['roll'].toDouble())); 95 | _pitchSpots.add(FlSpot(timestamp, orientation['pitch'].toDouble())); 96 | _yawSpots.add(FlSpot(timestamp, orientation['yaw'].toDouble())); 97 | 98 | // Update max values for scaling 99 | _maxAccelValue = [ 100 | ..._accelXSpots.map((spot) => spot.y.abs()), 101 | ..._accelYSpots.map((spot) => spot.y.abs()), 102 | ..._accelZSpots.map((spot) => spot.y.abs()), 103 | ].reduce((a, b) => a > b ? a : b); 104 | _maxAccelValue = _maxAccelValue.clamp(_minYRange, double.infinity); 105 | 106 | _maxGyroValue = [ 107 | ..._gyroXSpots.map((spot) => spot.y.abs()), 108 | ..._gyroYSpots.map((spot) => spot.y.abs()), 109 | ..._gyroZSpots.map((spot) => spot.y.abs()), 110 | ].reduce((a, b) => a > b ? a : b); 111 | _maxGyroValue = _maxGyroValue.clamp(_minGyroRange, double.infinity); 112 | 113 | _maxOrientValue = [ 114 | ..._rollSpots.map((spot) => spot.y.abs()), 115 | ..._pitchSpots.map((spot) => spot.y.abs()), 116 | ..._yawSpots.map((spot) => spot.y.abs()), 117 | ].reduce((a, b) => a > b ? a : b); 118 | _maxOrientValue = _maxOrientValue.clamp(_minOrientRange, double.infinity); 119 | 120 | // Remove old data points if we exceed the limit 121 | if (_accelXSpots.length > _maxDataPoints) { 122 | _accelXSpots.removeAt(0); 123 | _accelYSpots.removeAt(0); 124 | _accelZSpots.removeAt(0); 125 | _gyroXSpots.removeAt(0); 126 | _gyroYSpots.removeAt(0); 127 | _gyroZSpots.removeAt(0); 128 | _rollSpots.removeAt(0); 129 | _pitchSpots.removeAt(0); 130 | _yawSpots.removeAt(0); 131 | } 132 | }); 133 | } 134 | 135 | Future _connectToLiveKit() async { 136 | if (_connecting || _connected) return; 137 | 138 | setState(() { 139 | _connecting = true; 140 | _errorMessage = null; 141 | }); 142 | 143 | try { 144 | // Create a new room 145 | _room = lk.Room(); 146 | 147 | // Set up listeners for room events 148 | _listener = _room!.createListener(); 149 | 150 | _listener!.on((event) { 151 | setState(() { 152 | _connected = true; 153 | _connecting = false; 154 | }); 155 | _updateParticipants(); 156 | print('Connected to room: ${event.room.name}'); 157 | }); 158 | 159 | _listener!.on((event) { 160 | setState(() { 161 | _connected = false; 162 | _connecting = false; 163 | _participants = []; 164 | _roverVideoTrack = null; 165 | _roverParticipant = null; 166 | _sendingControls = false; 167 | _sendingAudio = false; 168 | _microphoneTrack = null; 169 | }); 170 | print('Disconnected from room: ${event.reason}'); 171 | }); 172 | 173 | _listener!.on((event) { 174 | _updateParticipants(); 175 | _checkForRoverCam(event.participant); 176 | print('Participant connected: ${event.participant.identity}'); 177 | }); 178 | 179 | _listener!.on((event) { 180 | _updateParticipants(); 181 | if (event.participant.identity == 'rover-cam') { 182 | setState(() { 183 | _roverVideoTrack = null; 184 | _roverParticipant = null; 185 | }); 186 | } 187 | print('Participant disconnected: ${event.participant.identity}'); 188 | }); 189 | 190 | _listener!.on((event) { 191 | if (event.participant.identity == 'rover-cam' && 192 | event.track is lk.VideoTrack) { 193 | setState(() { 194 | _roverVideoTrack = event.track as lk.VideoTrack; 195 | _roverParticipant = event.participant; 196 | }); 197 | print('Subscribed to rover-cam video track'); 198 | 199 | // Start sending control data if we have a gamepad 200 | if (_gamepadService.isGamepadConnected.value) { 201 | _startSendingControlData(); 202 | } 203 | } 204 | }); 205 | 206 | _listener!.on((event) { 207 | if (event.participant.identity == 'rover-cam' && 208 | event.track is lk.VideoTrack) { 209 | setState(() { 210 | _roverVideoTrack = null; 211 | _sendingControls = false; 212 | }); 213 | print('Unsubscribed from rover-cam video track'); 214 | } 215 | }); 216 | 217 | // Add data received handler 218 | _listener!.on((event) { 219 | if (event.topic == 'imu') { 220 | try { 221 | final data = jsonDecode(utf8.decode(event.data)); 222 | _handleImuData(data); 223 | } catch (e) { 224 | print('Error parsing IMU data: $e'); 225 | } 226 | } 227 | }); 228 | 229 | print('Connecting to room: ${widget.url} with token: ${widget.token}'); 230 | // Connect to the room 231 | await _room!.connect(widget.url, widget.token); 232 | } catch (error) { 233 | setState(() { 234 | _connecting = false; 235 | _connected = false; 236 | _errorMessage = 'Error connecting to LiveKit: $error'; 237 | }); 238 | print('Error connecting to LiveKit: $error'); 239 | } 240 | } 241 | 242 | void _startSendingControlData() { 243 | if (_sendingControls) return; 244 | 245 | setState(() { 246 | _sendingControls = true; 247 | }); 248 | 249 | // Set up a timer to send control data at 20Hz (every 50ms) 250 | _controlsTimer = Timer.periodic( 251 | const Duration(milliseconds: 50), 252 | (_) => _sendControls(), 253 | ); 254 | } 255 | 256 | void _stopSendingControlData() { 257 | if (!_sendingControls) return; 258 | 259 | setState(() { 260 | _sendingControls = false; 261 | }); 262 | 263 | _controlsTimer?.cancel(); 264 | _controlsTimer = null; 265 | } 266 | 267 | Future _startSendingAudio() async { 268 | if (_sendingAudio || _room == null || _room!.localParticipant == null) 269 | return; 270 | 271 | try { 272 | // Create microphone audio track 273 | _microphoneTrack = await lk.LocalAudioTrack.create(); 274 | 275 | // Publish the audio track to the room 276 | await _room!.localParticipant!.publishAudioTrack(_microphoneTrack!); 277 | 278 | setState(() { 279 | _sendingAudio = true; 280 | }); 281 | 282 | print('Started sending audio from microphone'); 283 | } catch (error) { 284 | print('Error starting audio: $error'); 285 | _stopSendingAudio(); 286 | } 287 | } 288 | 289 | void _stopSendingAudio() { 290 | if (!_sendingAudio) return; 291 | 292 | try { 293 | if (_microphoneTrack != null) { 294 | // Stop the track first 295 | _microphoneTrack!.stop(); 296 | 297 | // If room is connected, try to unpublish 298 | if (_room != null && _room!.localParticipant != null) { 299 | // Current version of livekit doesn't need us to manually unpublish 300 | // The track will be unpublished when we dispose it 301 | } 302 | 303 | // Dispose the track 304 | _microphoneTrack!.dispose(); 305 | _microphoneTrack = null; 306 | } 307 | } catch (error) { 308 | print('Error stopping audio: $error'); 309 | } finally { 310 | setState(() { 311 | _sendingAudio = false; 312 | }); 313 | } 314 | } 315 | 316 | void _sendControls() { 317 | if (!_sendingControls || _room == null || _room!.localParticipant == null) 318 | return; 319 | 320 | try { 321 | // Get the current controller values 322 | final controlValues = _gamepadService.controllerValues.value; 323 | 324 | // Create a smaller object with just the essential joystick values 325 | final smallerControlData = { 326 | 'left_x': double.parse(controlValues['leftStickX'].toStringAsFixed(3)), 327 | 'left_y': double.parse(controlValues['leftStickY'].toStringAsFixed(3)), 328 | 'right_x': double.parse( 329 | controlValues['rightStickX'].toStringAsFixed(3), 330 | ), 331 | 'right_y': double.parse( 332 | controlValues['rightStickY'].toStringAsFixed(3), 333 | ), 334 | }; 335 | 336 | // Create a JSON string from the smaller control data 337 | final controlData = { 338 | 'type': 'gamepad', 339 | 'data': smallerControlData, 340 | 'timestamp': DateTime.now().millisecondsSinceEpoch, 341 | }; 342 | 343 | // Convert the JSON map to a string and then to UTF-8 bytes 344 | final jsonString = jsonEncode(controlData); 345 | final dataBytes = utf8.encode(jsonString); 346 | 347 | // Send the control data as a List 348 | _room!.localParticipant!.publishData( 349 | dataBytes, 350 | topic: 'controls', 351 | reliable: false, 352 | ); 353 | 354 | print('Sent control data: $jsonString'); 355 | } catch (e) { 356 | print('Error sending control data: $e'); 357 | } 358 | } 359 | 360 | void _sendControlData() { 361 | // This method is now just a wrapper for _sendControls 362 | _sendControls(); 363 | } 364 | 365 | void _checkForRoverCam(lk.Participant participant) { 366 | if (participant.identity == 'rover-cam') { 367 | // Check if participant already has published video tracks 368 | for (var trackPublication in participant.trackPublications.values) { 369 | if (trackPublication.kind == lk.TrackType.VIDEO && 370 | trackPublication.subscribed && 371 | trackPublication.track != null) { 372 | setState(() { 373 | _roverVideoTrack = trackPublication.track as lk.VideoTrack; 374 | _roverParticipant = participant; 375 | }); 376 | 377 | // Start sending control data if we have a gamepad 378 | if (_gamepadService.isGamepadConnected.value) { 379 | _startSendingControlData(); 380 | } 381 | break; 382 | } 383 | } 384 | } 385 | } 386 | 387 | Future _disconnectFromLiveKit() async { 388 | _stopSendingControlData(); 389 | _stopSendingAudio(); 390 | 391 | if (_room != null) { 392 | await _room!.disconnect(); 393 | _room = null; 394 | } 395 | setState(() { 396 | _connected = false; 397 | _connecting = false; 398 | _participants = []; 399 | _roverVideoTrack = null; 400 | _roverParticipant = null; 401 | }); 402 | } 403 | 404 | void _updateParticipants() { 405 | if (_room != null) { 406 | setState(() { 407 | _participants = []; 408 | if (_room!.localParticipant != null) { 409 | _participants.add(_room!.localParticipant!); 410 | } 411 | _participants.addAll(_room!.remoteParticipants.values); 412 | 413 | // Check if rover-cam is already in the room 414 | for (var participant in _participants) { 415 | _checkForRoverCam(participant); 416 | } 417 | }); 418 | } 419 | } 420 | 421 | @override 422 | Widget build(BuildContext context) { 423 | return Column( 424 | mainAxisAlignment: MainAxisAlignment.center, 425 | children: [ 426 | if (_connecting) 427 | _buildConnectingState() 428 | else if (_connected && _room != null) 429 | _buildConnectedState() 430 | else 431 | _buildDisconnectedState(), 432 | ], 433 | ); 434 | } 435 | 436 | Widget _buildConnectedState() { 437 | return Stack( 438 | children: [ 439 | if (_roverVideoTrack != null) 440 | _buildRoverVideoView() 441 | else 442 | Text( 443 | "Waiting for rover to connect...", 444 | style: Theme.of( 445 | context, 446 | ).textTheme.bodyMedium!.copyWith(color: Colors.grey), 447 | ), 448 | Positioned(top: 12, left: 12, child: _buildGamepadStatus()), 449 | Positioned(top: 12, right: 12, child: _buildAudioControl()), 450 | Positioned( 451 | bottom: 12, 452 | right: 12, 453 | child: Column( 454 | crossAxisAlignment: CrossAxisAlignment.end, 455 | children: [ 456 | _buildTemperature(), 457 | const SizedBox(height: 12), 458 | Row( 459 | mainAxisSize: MainAxisSize.min, 460 | children: [ 461 | _buildSensorGraph( 462 | 'Accel (mg)', 463 | _accelXSpots, 464 | _accelYSpots, 465 | _accelZSpots, 466 | _maxAccelValue, 467 | ), 468 | const SizedBox(width: 12), 469 | _buildSensorGraph( 470 | 'Gyro (deg/s)', 471 | _gyroXSpots, 472 | _gyroYSpots, 473 | _gyroZSpots, 474 | _maxGyroValue, 475 | ), 476 | const SizedBox(width: 12), 477 | _buildSensorGraph( 478 | 'RPY (deg)', 479 | _rollSpots, 480 | _pitchSpots, 481 | _yawSpots, 482 | _maxOrientValue, 483 | ), 484 | ], 485 | ), 486 | ], 487 | ), 488 | ), 489 | ], 490 | ); 491 | } 492 | 493 | Widget _buildAudioControl() { 494 | return ElevatedButton( 495 | onPressed: _sendingAudio ? _stopSendingAudio : _startSendingAudio, 496 | style: ElevatedButton.styleFrom( 497 | backgroundColor: _sendingAudio ? Colors.red : Colors.green, 498 | fixedSize: const Size(130, 36), 499 | ), 500 | child: Row( 501 | mainAxisSize: MainAxisSize.min, 502 | children: [ 503 | Icon(_sendingAudio ? Icons.mic : Icons.mic_off, size: 18), 504 | const SizedBox(width: 4), 505 | Text( 506 | _sendingAudio ? "Mute" : "Unmute", 507 | style: Theme.of( 508 | context, 509 | ).textTheme.bodyMedium!.copyWith(color: Colors.white), 510 | ), 511 | ], 512 | ), 513 | ); 514 | } 515 | 516 | Widget _buildGamepadStatus() { 517 | return ValueListenableBuilder( 518 | valueListenable: _gamepadService.isGamepadConnected, 519 | builder: (context, isConnected, child) { 520 | return Column( 521 | crossAxisAlignment: CrossAxisAlignment.start, 522 | mainAxisSize: MainAxisSize.min, 523 | children: [ 524 | if (isConnected && _roverVideoTrack != null) ...[ 525 | Column( 526 | children: [ 527 | ElevatedButton( 528 | onPressed: 529 | _sendingControls 530 | ? _stopSendingControlData 531 | : _startSendingControlData, 532 | style: ElevatedButton.styleFrom( 533 | backgroundColor: 534 | _sendingControls ? Colors.red : Colors.green, 535 | fixedSize: const Size(100, 36), 536 | ), 537 | child: Text( 538 | _sendingControls ? "Stop" : "Start", 539 | style: Theme.of( 540 | context, 541 | ).textTheme.bodyMedium!.copyWith(color: Colors.white), 542 | ), 543 | ), 544 | const SizedBox(height: 12), 545 | _buildJoystickVisualizer(), 546 | ], 547 | ), 548 | ], 549 | ], 550 | ); 551 | }, 552 | ); 553 | } 554 | 555 | Widget _buildJoystickVisualizer() { 556 | return ValueListenableBuilder>( 557 | valueListenable: _gamepadService.controllerValues, 558 | builder: (context, values, child) { 559 | final leftY = (values['leftStickY'] as num?)?.toDouble() ?? 0.0; 560 | final rightX = (values['rightStickX'] as num?)?.toDouble() ?? 0.0; 561 | 562 | return Container( 563 | width: 100, 564 | height: 100, 565 | decoration: BoxDecoration( 566 | color: Colors.black26, 567 | borderRadius: BorderRadius.circular(8), 568 | ), 569 | child: Stack( 570 | children: [ 571 | // Vertical bar (left_y) 572 | Center( 573 | child: Container( 574 | width: 10, 575 | height: 80, 576 | decoration: BoxDecoration( 577 | color: Colors.grey.shade300, 578 | borderRadius: BorderRadius.circular(5), 579 | ), 580 | ), 581 | ), 582 | // Horizontal bar (right_x) 583 | Center( 584 | child: Container( 585 | width: 80, 586 | height: 10, 587 | decoration: BoxDecoration( 588 | color: Colors.grey.shade300, 589 | borderRadius: BorderRadius.circular(5), 590 | ), 591 | ), 592 | ), 593 | // Center dot 594 | Center( 595 | child: Container( 596 | width: 10, 597 | height: 10, 598 | decoration: const BoxDecoration( 599 | color: Colors.black38, 600 | shape: BoxShape.circle, 601 | ), 602 | ), 603 | ), 604 | // Y indicator (left_y) 605 | Center( 606 | child: AnimatedContainer( 607 | duration: const Duration(milliseconds: 100), 608 | transform: Matrix4.translationValues(0, -leftY * 35, 0), 609 | child: Container( 610 | width: 16, 611 | height: 16, 612 | decoration: const BoxDecoration( 613 | color: Colors.blue, 614 | shape: BoxShape.circle, 615 | ), 616 | ), 617 | ), 618 | ), 619 | // X indicator (right_x) 620 | Center( 621 | child: AnimatedContainer( 622 | duration: const Duration(milliseconds: 100), 623 | transform: Matrix4.translationValues(rightX * 35, 0, 0), 624 | child: Container( 625 | width: 16, 626 | height: 16, 627 | decoration: const BoxDecoration( 628 | color: Colors.red, 629 | shape: BoxShape.circle, 630 | ), 631 | ), 632 | ), 633 | ), 634 | Positioned( 635 | bottom: 2, 636 | left: 4, 637 | child: Text( 638 | leftY.toStringAsFixed(2), 639 | style: Theme.of(context).textTheme.displaySmall!.copyWith( 640 | color: Colors.blue, 641 | fontWeight: FontWeight.bold, 642 | ), 643 | ), 644 | ), 645 | Positioned( 646 | bottom: 2, 647 | right: 4, 648 | child: Text( 649 | rightX.toStringAsFixed(2), 650 | style: Theme.of(context).textTheme.displaySmall!.copyWith( 651 | color: Colors.red, 652 | fontWeight: FontWeight.bold, 653 | ), 654 | ), 655 | ), 656 | ], 657 | ), 658 | ); 659 | }, 660 | ); 661 | } 662 | 663 | Widget _buildRoverVideoView() { 664 | return Column( 665 | children: [ 666 | SizedBox( 667 | height: 600, 668 | width: 800, 669 | child: lk.VideoTrackRenderer(_roverVideoTrack!), 670 | ), 671 | ], 672 | ); 673 | } 674 | 675 | Widget _buildButtonIndicator(String label, bool isPressed) { 676 | return Container( 677 | margin: const EdgeInsets.symmetric(horizontal: 4), 678 | width: 40, 679 | height: 40, 680 | decoration: BoxDecoration( 681 | color: isPressed ? Colors.blue : Colors.grey.withAlpha(77), 682 | borderRadius: BorderRadius.circular(20), 683 | ), 684 | child: Center( 685 | child: Text( 686 | label, 687 | style: TextStyle( 688 | fontWeight: FontWeight.bold, 689 | color: isPressed ? Colors.white : Colors.black, 690 | ), 691 | ), 692 | ), 693 | ); 694 | } 695 | 696 | Widget _buildDisconnectedState() { 697 | return Column( 698 | children: [ 699 | ElevatedButton( 700 | onPressed: _connectToLiveKit, 701 | child: const Text('Connect to LiveKit'), 702 | ), 703 | Padding( 704 | padding: const EdgeInsets.only(top: 16.0), 705 | child: Text( 706 | _errorMessage ?? '', 707 | style: Theme.of( 708 | context, 709 | ).textTheme.bodyMedium!.copyWith(color: Colors.red), 710 | ), 711 | ), 712 | ], 713 | ); 714 | } 715 | 716 | Widget _buildConnectingState() { 717 | return Column( 718 | children: [ 719 | const CircularProgressIndicator(), 720 | Padding( 721 | padding: const EdgeInsets.only(top: 16.0), 722 | child: Text( 723 | 'Connecting to LiveKit...', 724 | style: Theme.of( 725 | context, 726 | ).textTheme.bodyMedium!.copyWith(color: Colors.grey), 727 | ), 728 | ), 729 | ], 730 | ); 731 | } 732 | 733 | Widget _buildSensorGraph( 734 | String title, 735 | List xSpots, 736 | List ySpots, 737 | List zSpots, 738 | double maxValue, 739 | ) { 740 | if (xSpots.isEmpty) { 741 | return const SizedBox.shrink(); 742 | } 743 | 744 | final now = DateTime.now().millisecondsSinceEpoch / 1000.0; 745 | final minX = now - _timeWindow; 746 | 747 | return Column( 748 | crossAxisAlignment: CrossAxisAlignment.start, 749 | children: [ 750 | Text( 751 | title, 752 | style: Theme.of(context).textTheme.displaySmall!.copyWith( 753 | color: Colors.white, 754 | fontWeight: FontWeight.bold, 755 | ), 756 | ), 757 | const SizedBox(height: 4), 758 | ClipRRect( 759 | borderRadius: BorderRadius.circular(8), 760 | child: Container( 761 | width: 180, 762 | height: 80, 763 | decoration: const BoxDecoration(color: Colors.black54), 764 | child: Padding( 765 | padding: const EdgeInsets.all(8.0), 766 | child: LineChart( 767 | LineChartData( 768 | gridData: const FlGridData(show: false), 769 | titlesData: const FlTitlesData(show: false), 770 | borderData: FlBorderData(show: false), 771 | lineTouchData: const LineTouchData(enabled: false), 772 | minX: minX, 773 | maxX: now, 774 | minY: -maxValue * 1.2, 775 | maxY: maxValue * 1.2, 776 | lineBarsData: [ 777 | LineChartBarData( 778 | spots: xSpots, 779 | isCurved: true, 780 | color: Colors.red, 781 | barWidth: 2, 782 | isStrokeCapRound: true, 783 | dotData: const FlDotData(show: false), 784 | belowBarData: BarAreaData(show: false), 785 | preventCurveOverShooting: true, 786 | ), 787 | LineChartBarData( 788 | spots: ySpots, 789 | isCurved: true, 790 | color: Colors.green, 791 | barWidth: 2, 792 | isStrokeCapRound: true, 793 | dotData: const FlDotData(show: false), 794 | belowBarData: BarAreaData(show: false), 795 | preventCurveOverShooting: true, 796 | ), 797 | LineChartBarData( 798 | spots: zSpots, 799 | isCurved: true, 800 | color: Colors.blue, 801 | barWidth: 2, 802 | isStrokeCapRound: true, 803 | dotData: const FlDotData(show: false), 804 | belowBarData: BarAreaData(show: false), 805 | preventCurveOverShooting: true, 806 | ), 807 | ], 808 | ), 809 | ), 810 | ), 811 | ), 812 | ), 813 | const SizedBox(height: 4), 814 | Row( 815 | mainAxisSize: MainAxisSize.min, 816 | children: [ 817 | Container( 818 | width: 8, 819 | height: 8, 820 | decoration: const BoxDecoration( 821 | color: Colors.red, 822 | shape: BoxShape.circle, 823 | ), 824 | ), 825 | const SizedBox(width: 4), 826 | Text( 827 | 'X', 828 | style: Theme.of(context).textTheme.displaySmall!.copyWith( 829 | color: Colors.white, 830 | fontWeight: FontWeight.bold, 831 | ), 832 | ), 833 | const SizedBox(width: 8), 834 | Container( 835 | width: 8, 836 | height: 8, 837 | decoration: const BoxDecoration( 838 | color: Colors.green, 839 | shape: BoxShape.circle, 840 | ), 841 | ), 842 | const SizedBox(width: 4), 843 | Text( 844 | 'Y', 845 | style: Theme.of(context).textTheme.displaySmall!.copyWith( 846 | color: Colors.white, 847 | fontWeight: FontWeight.bold, 848 | ), 849 | ), 850 | const SizedBox(width: 8), 851 | Container( 852 | width: 8, 853 | height: 8, 854 | decoration: const BoxDecoration( 855 | color: Colors.blue, 856 | shape: BoxShape.circle, 857 | ), 858 | ), 859 | const SizedBox(width: 4), 860 | Text( 861 | 'Z', 862 | style: Theme.of(context).textTheme.displaySmall!.copyWith( 863 | color: Colors.white, 864 | fontWeight: FontWeight.bold, 865 | ), 866 | ), 867 | ], 868 | ), 869 | ], 870 | ); 871 | } 872 | 873 | Widget _buildTemperature() { 874 | return Container( 875 | padding: const EdgeInsets.all(8), 876 | decoration: BoxDecoration( 877 | color: Colors.black54, 878 | borderRadius: BorderRadius.circular(8), 879 | ), 880 | child: Row( 881 | mainAxisSize: MainAxisSize.min, 882 | children: [ 883 | const Icon(Icons.thermostat, color: Colors.white, size: 16), 884 | const SizedBox(width: 4), 885 | Text( 886 | '${_temperature.toStringAsFixed(1)}°C', 887 | style: Theme.of(context).textTheme.displaySmall!.copyWith( 888 | color: Colors.white, 889 | fontWeight: FontWeight.bold, 890 | ), 891 | ), 892 | ], 893 | ), 894 | ); 895 | } 896 | } 897 | --------------------------------------------------------------------------------