tahamajs/CPS / Files /ComputerAssignments /CA2 /code /docs /CODE_WALKTHROUGH.md
tahamajs's picture
|
download
raw
26.3 kB

Code Walkthrough – Complete Line‑by‑Line Guide

This document provides an exhaustive, file‑by‑file explanation of the Air Mouse source code. It covers the Android app (Kotlin), the PC server (Python), and the configuration/build files. Each section describes the purpose of the file, its key data structures, important methods, and how it fits into the overall system. After reading this, you will understand every line of the project.


📖 Table of Contents

  1. Android App – Kotlin
  2. UI Layouts (XML)
  3. AndroidManifest.xml
  4. Gradle Build Files
  5. PC Server – Python
  6. Build Scripts (APK without Android Studio)
  7. Common Code Flow

Android App – Kotlin

1. MainActivity.kt

Location: com/airmouse/MainActivity.kt

Purpose: This is the entry point of the Android app. It inflates the main UI, handles user interactions (calibrate, start, settings, debug), requests permissions, and coordinates the lifecycle of the Air Mouse service.

Key Sections

onCreate(savedInstanceState: Bundle?)

  • Calls setContentView(R.layout.activity_main).
  • Binds UI elements using findViewById (or ViewBinding in the enhanced version).
  • Initialises PreferencesManager, BatterySaver, DebugOverlay.
  • Requests permissions for INTERNET, VIBRATE, and SYSTEM_ALERT_WINDOW.
  • Sets up the sensitivity slider:
    • Maps saved sensitivity (0.2–2.0) to SeekBar progress (0–100).
    • Listener updates the TextView and saves when user stops touching.
  • Instantiates CalibrationHelper, EnhancedGestureDetector, SensorService, and DebugOverlay.setSensorService.
  • Sets click listeners for all buttons.

setupSensitivitySlider()

  • Reads saved sensitivity from PreferencesManager.
  • Calculates progress: (sensitivity - 0.2) / 1.8 * 100.
  • Sets SeekBar listener:
    • onProgressChanged: updates the displayed value.
    • onStopTrackingTouch: saves the new sensitivity (debounced).

startCalibration()

  • Checks network availability (WiFi).
  • Launches a coroutine in lifecycleScope:
    • Calls calibrationHelper.calibrateMagnetometer(30000)
    • Calls calibrationHelper.calibrateGyro()
    • Calls calibrationHelper.calibrateAccelerometer()
    • Sets isCalibrated = true and updates status.

startAirMouse()

  • Verifies calibration and IP address.
  • Checks WiFi.
  • Saves IP to PreferencesManager.
  • Creates DataSender and AutoReconnect, starts them.
  • Sets callbacks on SensorService:
    • setOnOrientationChange: computes delta (dx, dy) using sensitivity, sends via DataSender.sendMove, updates green square rotation.
    • setOnGyroUpdate and setOnAccelUpdate: stores latest values for debug overlay.
    • setOnGestureDetected: sends corresponding network messages (click, double‑click, right‑click, scroll) and flashes green square.
  • Starts SensorService and BatterySaver.

updateUIIndicator(roll, yaw)

  • Sets the rotation of the green square to yaw (in degrees, converted from radians). Yaw controls horizontal orientation.

flashClick()

  • Changes the background color of the green square to red, then back to green after 100 ms.

showSettingsDialog()

  • Creates a SettingsDialog with the current PreferencesManager and a lambda that calls gestureDetector.reloadThresholds() when dismissed.

isNetworkAvailable()

  • Uses ConnectivityManager to check if WiFi or Ethernet is connected.

Lifecycle methods

  • onPause(): stops SensorService and BatterySaver.
  • onResume(): restarts them if isActive is true.
  • onDestroy(): stops all services, sender, auto‑reconnect, and hides debug overlay.

Important Design Choices

  • Uses lifecycleScope.launch for calibration to avoid blocking the UI.
  • Sensitivity save is debounced to avoid excessive SharedPreferences writes.
  • The lastOrientation is stored locally to compute relative deltas, not absolute positions, so the cursor never jumps when you start moving.

2. MainViewModel.kt

Location: com/airmouse/ui/MainViewModel.kt

Purpose: This ViewModel separates UI logic from business logic. It observes the sensor repository and updates LiveData objects that MainActivity observes. It also handles network communication and gesture detection.

Key Components

Properties

  • prefs: PreferencesDataStore – persistent settings.
  • sensorRepo: SensorRepository – provides a flow of sensor data.
  • networkRepo: NetworkRepository – sends move/click/scroll messages.
  • gestureDetector: GestureDetector – detects gestures from sensor values.

LiveData exposed to UI

  • statusText: LiveData<String> – connection/calibration status.
  • orientationYaw: LiveData<Float> – yaw angle for green square rotation.
  • clickFlashEvent: LiveData<Boolean> – triggers a visual flash on click.
  • toastMessage: LiveData<String?> – one‑time messages.
  • sensitivity: LiveData<Float> – current cursor sensitivity.
  • lastIp: LiveData<String> – last used IP.

init block

  • Collects prefs.sensitivityFlow and updates _sensitivity.
  • Collects sensorRepo.sensorEvents (a flow of SensorData). For each event:
    • Computes delta: dx = (yaw - lastYaw) * sensitivity * 0.8f, dy = (roll - lastRoll) * sensitivity * 0.8f.
    • Sends move via networkRepo.
    • Calls gestureDetector.detect(gyroY, accelY, roll).
    • Based on the resulting gesture, sends corresponding network messages and updates LiveData (toast, click flash).

Public methods

  • setSensitivity(value: Float): saves to prefs.
  • calibrate(): runs gyro, magnetometer, and accelerometer calibration in sequence, updating status LiveData.
  • start(): retrieves last IP, connects to server, starts sensor streaming.
  • toggleDebugOverlay(): shows a toast (actual toggling done by MainActivity for permission reasons).
  • onCleared(): disconnects network repo and stops active flag.

Threading

  • All prefs and sensorRepo calls are suspend functions, launched in viewModelScope.
  • Sensor events are collected on a background thread; LiveData updates are automatically posted to the main thread.

3. SensorService.kt

Location: com/airmouse/sensors/SensorService.kt

Purpose: This service is the core sensor manager. It registers listeners for accelerometer, gyroscope, and magnetometer, applies calibration corrections, runs the Madgwick fusion, and invokes orientation and gesture callbacks.

Key Properties

  • sensorManager, accelerometer, gyroscope, magnetometer – Android sensor objects.
  • madgwick: MadgwickFusion – fusion algorithm instance.
  • orientationCallback, gestureCallback, gyroUpdateCallback, accelUpdateCallback – lambdas for communicating with MainActivity / ViewModel.
  • timestamp – used to compute dt (delta time) between sensor events.

Important Methods

start()

  • Gets the SensorManager.
  • Finds default sensors.
  • Registers listeners with SENSOR_DELAY_GAME (20 ms period, 50 Hz).
  • If battery saver is active, it may register with SENSOR_DELAY_NORMAL instead.

stop()

  • Unregisters all listeners.

setSamplingRate(delay: Int)

  • Unregisters and re‑registers with a new delay (used by BatterySaver).

onSensorChanged(event: SensorEvent)

  • This is called by Android on the thread specified during registration (a dedicated HandlerThread).
  • Computes dt using System.currentTimeMillis().
  • For each sensor type:
    • Accelerometer: corrects raw values using calibrationHelper, calls madgwick.updateAccel(). Also passes ay to gestureDetector.detectScroll() and invokes callback.
    • Gyroscope: corrects raw values, calls madgwick.updateGyro(gx, gy, gz, dt). Passes gy to gestureDetector.detectClick().
    • Magnetometer: corrects raw values, calls madgwick.updateMag().
  • After processing, calls madgwick.getRoll() and getYaw(), then invokes orientationCallback.
  • Also calls gestureDetector.detectRightClick() (using roll) and invokes gesture callback accordingly.

Callback Setters

  • setOnOrientationChange(callback)
  • setOnGestureDetected(callback)
  • setOnGyroUpdate(callback)
  • setOnAccelUpdate(callback)

Thread Safety

  • The service is designed to be started/stopped from the main thread, but onSensorChanged runs on the background HandlerThread. All callbacks are invoked on that same thread; therefore, they should not perform heavy UI operations directly. The ViewModel uses postValue to safely update LiveData.

4. MadgwickFusion.kt

Location: com/airmouse/domain/MadgwickFusion.kt

Purpose: Implements the Madgwick AHRS algorithm. It maintains a quaternion representing orientation, updates it using gyroscope data, and corrects it using accelerometer and magnetometer data.

Key Variables

  • q0, q1, q2, q3 – quaternion components (unit quaternion).
  • ax, ay, az – last accelerometer reading.
  • mx, my, mz – last magnetometer reading.
  • beta – filter gain (default 0.1).

Key Methods

updateGyro(gx: Float, gy: Float, gz: Float, dt: Float)

  • Computes the quaternion derivative from gyroscope:
    qDot1 = 0.5f * (-q1*gx - q2*gy - q3*gz)
    qDot2 = 0.5f * ( q0*gx + q2*gz - q3*gy)
    qDot3 = 0.5f * ( q0*gy - q1*gz + q3*gx)
    qDot4 = 0.5f * ( q0*gz + q1*gy - q2*gx)
    
  • If accelerometer data is available (ax, ay, az ≠ 0), it performs gradient descent correction:
    • Normalises accelerometer reading.
    • Computes error function f and Jacobian J (closed‑form expressions from Madgwick’s paper).
    • Computes step s = (J^T * f) / (J^T * J).
    • Adjusts qDot by -beta * s.
  • Integrates: q += qDot * dt.
  • Normalises the quaternion to unit length.

updateAccel(x, y, z), updateMag(x, y, z)

  • Store the latest values for use in the next updateGyro call.

getRoll(), getPitch(), getYaw()

  • Convert quaternion to Euler angles (in radians):
    roll  = atan2(2*(q0*q1 + q2*q3), 1 - 2*(q1*q1 + q2*q2))
    pitch = asin(2*(q0*q2 - q3*q1))
    yaw   = atan2(2*(q0*q3 + q1*q2), 1 - 2*(q2*q2 + q3*q3))
    

Performance Notes

  • Each updateGyro call does ~100 floating‑point operations. On a modern phone, it takes less than 0.5 ms.
  • The fusion runs at the same rate as gyroscope updates (50 Hz).

5. CalibrationHelper.kt / CalibrationUseCase.kt

Location: com/airmouse/sensors/CalibrationHelper.kt and com/airmouse/domain/CalibrationUseCase.kt

Purpose: These classes handle sensor calibration. CalibrationHelper is the wrapper used by SensorService and MainActivity. CalibrationUseCase contains the low‑level suspend functions that collect samples.

CalibrationHelper.kt (simplified wrapper)

Properties

  • gyroBias, accelOffset, accelScale, magOffset, magScale – correction parameters.

Methods

suspend fun calibrateGyro()

  • Creates a CalibrationUseCase and calls calibrateGyro(), storing the returned bias.

suspend fun calibrateMagnetometer(durationMs: Long)

  • Calls CalibrationUseCase.calibrateMagnetometer(durationMs), stores offset and scale.

suspend fun calibrateAccelerometer()

  • Calls CalibrationUseCase.calibrateAccelerometerSimple() for simplified offset.

Correction functions

  • correctGyro(value, axis) = value - gyroBias[axis]
  • correctAccelerometer(x, y, z) – applies offset and scale.
  • correctMagnetometer(x, y, z) – applies offset and scale.

CalibrationUseCase.kt (low‑level collection)

suspend fun calibrateGyro(): FloatArray

  • Registers a SensorEventListener for TYPE_GYROSCOPE.
  • Collects 500 samples, unregisters, averages them, and returns the bias.
  • Uses suspendCancellableCoroutine to bridge callback to coroutine.

suspend fun calibrateMagnetometer(durationMs: Long): Pair<FloatArray, FloatArray>

  • Registers listener for TYPE_MAGNETIC_FIELD.
  • Updates min/max for each axis.
  • After durationMs (via Handler.postDelayed), unregisters, computes offset and scale, and returns.

suspend fun calibrateAccelerometerSimple(): Pair<FloatArray, FloatArray>

  • Collects 200 stationary samples, averages them.
  • Computes offset: offset[0] = avgX, offset[1] = avgY, offset[2] = avgZ - 9.81f. Scale is [1,1,1].

suspend fun calibrateAccelerometer(measuredOrientations: List<FloatArray>)

  • Full 6‑point calibration using pre‑collected data. Solves for offset and scale per axis.

6. EnhancedGestureDetector.kt / GestureDetector.kt

Location: com/airmouse/sensors/EnhancedGestureDetector.kt (or com/airmouse/domain/GestureDetector.kt)

Purpose: These classes detect gestures from gyroscope Y, accelerometer Y, and roll angle. EnhancedGestureDetector includes double‑click and right‑click; the simpler GestureDetector may only support single click and scroll.

EnhancedGestureDetector.kt – Complete Version

Enum

enum class Gesture { NONE, CLICK, DOUBLE_CLICK, RIGHT_CLICK, SCROLL_UP, SCROLL_DOWN }

State variables

  • lastClickTime, potentialDoubleClick, rightClickStartTime, rightClickTriggered, scrollInProgress.

reloadThresholds()

  • Reads all thresholds from PreferencesManager (click speed, double‑click interval, right‑click tilt, right‑click duration, scroll speed, scroll debounce).

detectClick(gyroY, dt): Boolean

  • Checks |gyroY| > clickSpeedThreshold and time since last click > doubleClickInterval.
  • If potentialDoubleClick is true → returns true (caller will interpret as double‑click).
  • Else sets potentialDoubleClick = true, schedules a timer to clear it, and returns true (caller interprets as single click after timer? This is tricky – in SensorService we call detectClick and separately handle double‑click detection via a callback. The actual implementation may differ.)

detectRightClick(roll, dt): Boolean

  • Checks |roll| > rightClickTiltAngle.
  • Starts timer; if held for rightClickDuration, returns true.

detectScroll(accelY, dt): Int

  • Returns 1 (down), -1 (up), or 0.
  • Uses scrollThreshold and scrollDebounce.

Simpler GestureDetector.kt (Domain layer)

  • Similar logic but may not include double‑click or right‑click. The domain version is used by MainViewModel in some implementations.

7. DataSender.kt

Location: com/airmouse/network/DataSender.kt

Purpose: A background thread that maintains a TCP connection to the PC server, sends JSON messages from a queue, and handles ACKs and retransmission for critical packets.

Key Components

Properties

  • socket, out, input – TCP connection.
  • running – controls the main loop.
  • queue: LinkedBlockingQueue<String> – thread‑safe queue of outgoing messages.
  • pendingAcks: ConcurrentHashMap<Long, String> – messages waiting for ACK.
  • ACK_TIMEOUT_MS = 500L

Main Thread (run())

  • While running, calls connect() and processLoop().
  • Catches exceptions and retries after RECONNECT_DELAY_MS (5 seconds).

connect()

  • Opens Socket(host, port), creates DataOutputStream and BufferedReader.

processLoop()

  • Polls the queue (take()).
  • For each message, calls sendMessage().

sendMessage(msg: String)

  • Writes msg followed by newline to the socket.
  • If the message is critical (contains "click", "doubleclick", "rightclick", or "scroll"):
    • Extracts the id.
    • Puts it into pendingAcks.
    • Starts a timer (simplified: sleeps for 500 ms, then retransmits if still pending).

ACK receiver thread

  • Reads lines from the socket.
  • If a line contains "ack", parses the id and removes from pendingAcks.

Public send methods

  • sendMove(dx, dy), sendClick(), sendDoubleClick(), sendRightClick(), sendScroll(delta)
  • Each builds a JSON object and offers it to the queue.

stopSending()

  • Sets running = false, closes socket and streams.

Threading Model

  • The DataSender itself is a Thread. It creates two additional threads: one for sending (the main loop) and one for receiving ACKs. In practice, the main loop is the thread itself; the ACK receiver is a separate thread.

8. AutoReconnect.kt

Location: com/airmouse/network/AutoReconnect.kt

Purpose: Monitors the DataSender connection and attempts to reconnect if it becomes disconnected. Uses a Handler to periodically check dataSender.isConnected (a flag we added).

Key Methods

start()

  • Starts a periodic runnable that checks every CHECK_INTERVAL_MS (5 seconds).
  • If !dataSender.isConnected, stops the old sender and starts a new one using the saved IP.

stop()

  • Cancels the runnable.

9. PreferencesDataStore.kt / PreferencesManager.kt

Location: com/airmouse/data/PreferencesDataStore.kt (modern) and com/airmouse/utils/PreferencesManager.kt (legacy)

Purpose: Persist user settings (sensitivity, gesture thresholds, haptic enabled, last IP). The DataStore version uses AndroidX DataStore (coroutine‑based). The PreferencesManager uses SharedPreferences (blocking).

PreferencesDataStore.kt

Keys

  • SENSITIVITY, CLICK_THRESHOLD, DOUBLE_CLICK_INTERVAL, SCROLL_THRESHOLD, SCROLL_DEBOUNCE, RIGHT_CLICK_TILT, RIGHT_CLICK_DURATION, HAPTIC_ENABLED, LAST_IP.

Flows

  • lastIpFlow: Flow<String> – observable IP.

Suspend getters

  • suspend fun getSensitivity(): Float – reads once.

Suspend setters

  • suspend fun setSensitivity(value: Float) = dataStore.edit { it[SENSITIVITY] = value }

Blocking wrappers

  • setSensitivityBlocking(value) – uses runBlocking for compatibility with legacy code.

PreferencesManager.kt (legacy)

  • Same functionality using SharedPreferences. Used by older parts of the code.

10. SensorRepository.kt & NetworkRepository.kt

Location: com/airmouse/data/

Purpose: These repositories abstract sensor and network operations.

SensorRepository.kt

sensorEvents: Flow<SensorData>

  • Uses callbackFlow to create a flow that emits SensorData(roll, yaw, gyroY, accelY).
  • Internally registers a SensorEventListener that uses a MadgwickFusion instance and calibration helper.
  • Each time gyroscope data arrives, it sends the latest roll, yaw, gyroY, and accelY to the flow.

Calibration methods

  • calibrateGyro(), calibrateMagnetometer(durationMs), calibrateAccelerometer() – delegate to inner CalibrationHelper.

vibrate(duration)

  • Uses Vibrator service.

NetworkRepository.kt

  • Simplified wrapper around DataSender. Provides connect(ip), sendMove, sendClick, etc.
  • In the full implementation, MainViewModel uses this instead of creating DataSender directly.

11. DebugOverlayService.kt & DebugOverlay.kt

Location: com/airmouse/ui/

Purpose: Show a floating overlay with live sensor values.

DebugOverlayService.kt

  • Extends Service.
  • Creates a WindowManager.LayoutParams with TYPE_APPLICATION_OVERLAY.
  • Inflates debug_overlay.xml (a TextView) and adds it to the window manager.
  • Singleton companion object: instance so that other parts can call updateData().
  • updateData(roll, yaw, gyroY, accelY) updates the TextView text.

DebugOverlay.kt

  • Object that starts/stops the service and provides update() forwarding.

12. CalibrationActivity.kt & SettingsScreen.kt

Location: com/airmouse/ui/

CalibrationActivity.kt

  • A dedicated AppCompatActivity that hosts the calibration wizard.
  • Uses ViewPager2 and CalibrationPagerAdapter to switch between the three guided sensor steps.
  • Shows a header, timer, progress bar, and Back/Next/Stop controls.
  • Prevents the user from advancing until the current step reports a valid calibration result.

SettingsScreen.kt

  • A modern Compose settings screen with sliders and switches for sensitivity, click threshold, double-click interval, scroll threshold, right-click tilt, haptic feedback, theme, AI smoothing, and predictive movement.
  • Reads and writes through SettingsViewModel, which in turn talks to the settings repository.

UI Layouts (XML)

activity_main.xml

  • Contains: IP input (TextInputLayout), buttons, status text, sensitivity seek bar, settings/debug buttons, and a View (green square) with a drawable background.

activity_calibration.xml

  • Contains: ViewPager2, step header, timer, progress bar, and the Back/Next/Stop control bar used by CalibrationActivity.

fragment_settings.xml

  • Contains: three groups of (TextView, SeekBar, TextView) for the three thresholds.

debug_overlay.xml

  • A single TextView with black background, white text, monospaced font.

green_square.xml (drawable)

  • A <shape> rectangle with solid green colour and 100dp size.

AndroidManifest.xml

  • Permissions: INTERNET, VIBRATE, SYSTEM_ALERT_WINDOW, ACCESS_NETWORK_STATE.
  • Uses‑feature tags for accelerometer, gyroscope, magnetometer (all required).
  • Application tag with theme Theme.AirMouse (MaterialComponents.DayNight.NoActionBar), usesCleartextTraffic="true".
  • Activity MainActivity with intent filter for LAUNCHER.
  • Service DebugOverlayService.

Gradle Build Files

Project‑level build.gradle

  • Defines buildscript with Google and MavenCentral repositories.
  • Classpath: Android Gradle Plugin 7.4.2, Kotlin Gradle Plugin 1.8.0.

Module‑level app/build.gradle

  • minSdk 29, targetSdk 34.
  • Build features: viewBinding true.
  • Dependencies: androidx.core-ktx, appcompat, material, constraintlayout, lifecycle-viewmodel-ktx, lifecycle-livedata-ktx, datastore-preferences, kotlinx-coroutines-android, org.json.

settings.gradle

  • pluginManagement with Google, MavenCentral, GradlePluginPortal.
  • dependencyResolutionManagement with FAIL_ON_PROJECT_REPOS.
  • include ':app'.

gradle-wrapper.properties

  • distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip.

PC Server – Python

gui.py – Dark Mode GUI

  • Uses asyncio for networking and tkinter for GUI.
  • AirMouseGUI class creates window with start/stop buttons, log area, sensitivity slider.
  • AirMouseServer class has handle_client that reads JSON lines, processes messages, sends ACK.
  • Supports move, click, doubleclick, rightclick, scroll.
  • Sensitivity slider updates CONFIG["sensitivity"] and the mouse.sensitivity attribute.

server.py – Console Server

  • Reads config.json for host, port, sensitivity, log level, log file.
  • Logs to both console and file.
  • Similar AirMouseServer class, but no GUI.

run.py / run.sh / run.bat

  • run.py checks for pyautogui, installs dependencies, then launches gui.py.
  • Shell/batch scripts for one‑click start.

perfetto_analyzer.py

  • Uses perfetto.trace_processor.TraceProcessor and pandas.
  • Queries the trace for sensor slices, sampling periods, filter CPU time, etc.
  • Prints sample data and textual answers to the 11 questions.

Build Scripts (APK without Android Studio)

build_apk.sh / build_apk.bat

  • Checks for Java 11 and Android SDK.
  • Sets ANDROID_HOME.
  • Accepts licenses via yes | sdkmanager --licenses.
  • Installs build-tools;29.0.3, platforms;android-29, platform-tools.
  • Runs ./gradlew clean assembleDebug in the android folder.
  • Outputs APK location.

Common Code Flow

  1. User opens app – MainActivity.onCreate sets up UI, initialises components, requests permissions.
  2. User calibrates – startCalibration() runs CalibrationHelper suspend functions, updating UI.
  3. User enters IP and starts – startAirMouse() creates DataSender and AutoReconnect, starts them, then starts SensorService.
  4. SensorService registers listeners and waits for sensor events.
  5. On sensor event – onSensorChanged corrects raw values, runs Madgwick fusion, computes deltas, sends move packets, detects gestures.
  6. DataSender queues messages, sends over TCP, handles ACKs.
  7. PC server receives messages, moves cursor, clicks, scrolls, sends ACKs.
  8. User closes app – onDestroy stops all services, closes sockets.

This code walkthrough covers every significant file in the Air Mouse project. Use it to understand the implementation and to modify or extend the system as needed for your coursework.

Xet Storage Details

Size:
26.3 kB
·
Xet hash:
dd15b745dfb92c45e3bea0848d5307d5bb8e4299540a9a8e719a59ec7c037476

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.