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
- Android App – Kotlin
- MainActivity.kt
- MainViewModel.kt
- SensorService.kt
- MadgwickFusion.kt
- CalibrationHelper.kt / CalibrationUseCase.kt
- EnhancedGestureDetector.kt / GestureDetector.kt
- DataSender.kt
- AutoReconnect.kt
- PreferencesDataStore.kt / PreferencesManager.kt
- SensorRepository.kt & NetworkRepository.kt
- DebugOverlayService.kt & DebugOverlay.kt
- CalibrationActivity.kt & SettingsScreen.kt
- UI Layouts (XML)
- AndroidManifest.xml
- Gradle Build Files
- PC Server – Python
- Build Scripts (APK without Android Studio)
- 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(orViewBindingin the enhanced version). - Initialises
PreferencesManager,BatterySaver,DebugOverlay. - Requests permissions for
INTERNET,VIBRATE, andSYSTEM_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, andDebugOverlay.setSensorService. - Sets click listeners for all buttons.
setupSensitivitySlider()
- Reads saved sensitivity from
PreferencesManager. - Calculates progress:
(sensitivity - 0.2) / 1.8 * 100. - Sets
SeekBarlistener: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 = trueand updates status.
- Calls
startAirMouse()
- Verifies calibration and IP address.
- Checks WiFi.
- Saves IP to
PreferencesManager. - Creates
DataSenderandAutoReconnect, starts them. - Sets callbacks on
SensorService:setOnOrientationChange: computes delta (dx, dy) using sensitivity, sends viaDataSender.sendMove, updates green square rotation.setOnGyroUpdateandsetOnAccelUpdate: stores latest values for debug overlay.setOnGestureDetected: sends corresponding network messages (click, double‑click, right‑click, scroll) and flashes green square.
- Starts
SensorServiceandBatterySaver.
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
SettingsDialogwith the currentPreferencesManagerand a lambda that callsgestureDetector.reloadThresholds()when dismissed.
isNetworkAvailable()
- Uses
ConnectivityManagerto check if WiFi or Ethernet is connected.
Lifecycle methods
onPause(): stopsSensorServiceandBatterySaver.onResume(): restarts them ifisActiveis true.onDestroy(): stops all services, sender, auto‑reconnect, and hides debug overlay.
Important Design Choices
- Uses
lifecycleScope.launchfor calibration to avoid blocking the UI. - Sensitivity save is debounced to avoid excessive
SharedPreferenceswrites. - The
lastOrientationis 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.sensitivityFlowand updates_sensitivity. - Collects
sensorRepo.sensorEvents(a flow ofSensorData). For each event:- Computes delta:
dx = (yaw - lastYaw) * sensitivity * 0.8f,dy = (roll - lastRoll) * sensitivity * 0.8f. - Sends
movevianetworkRepo. - Calls
gestureDetector.detect(gyroY, accelY, roll). - Based on the resulting gesture, sends corresponding network messages and updates LiveData (toast, click flash).
- Computes delta:
Public methods
setSensitivity(value: Float): saves toprefs.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 byMainActivityfor permission reasons).onCleared(): disconnects network repo and stops active flag.
Threading
- All
prefsandsensorRepocalls are suspend functions, launched inviewModelScope. - 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 withMainActivity/ ViewModel.timestamp– used to computedt(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_NORMALinstead.
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
dtusingSystem.currentTimeMillis(). - For each sensor type:
- Accelerometer: corrects raw values using
calibrationHelper, callsmadgwick.updateAccel(). Also passesaytogestureDetector.detectScroll()and invokes callback. - Gyroscope: corrects raw values, calls
madgwick.updateGyro(gx, gy, gz, dt). PassesgytogestureDetector.detectClick(). - Magnetometer: corrects raw values, calls
madgwick.updateMag().
- Accelerometer: corrects raw values using
- After processing, calls
madgwick.getRoll()andgetYaw(), then invokesorientationCallback. - 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
onSensorChangedruns on the backgroundHandlerThread. All callbacks are invoked on that same thread; therefore, they should not perform heavy UI operations directly. The ViewModel usespostValueto 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
fand JacobianJ(closed‑form expressions from Madgwick’s paper). - Computes step
s = (J^T * f) / (J^T * J). - Adjusts
qDotby-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
updateGyrocall.
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
updateGyrocall 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
CalibrationUseCaseand callscalibrateGyro(), 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
SensorEventListenerforTYPE_GYROSCOPE. - Collects 500 samples, unregisters, averages them, and returns the bias.
- Uses
suspendCancellableCoroutineto 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(viaHandler.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| > clickSpeedThresholdand time since last click > doubleClickInterval. - If
potentialDoubleClickis true → returnstrue(caller will interpret as double‑click). - Else sets
potentialDoubleClick = true, schedules a timer to clear it, and returnstrue(caller interprets as single click after timer? This is tricky – inSensorServicewe calldetectClickand 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, returnstrue.
detectScroll(accelY, dt): Int
- Returns
1(down),-1(up), or0. - Uses
scrollThresholdandscrollDebounce.
Simpler GestureDetector.kt (Domain layer)
- Similar logic but may not include double‑click or right‑click. The domain version is used by
MainViewModelin 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, callsconnect()andprocessLoop(). - Catches exceptions and retries after
RECONNECT_DELAY_MS(5 seconds).
connect()
- Opens
Socket(host, port), createsDataOutputStreamandBufferedReader.
processLoop()
- Polls the queue (
take()). - For each message, calls
sendMessage().
sendMessage(msg: String)
- Writes
msgfollowed 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).
- Extracts the
ACK receiver thread
- Reads lines from the socket.
- If a line contains
"ack", parses theidand removes frompendingAcks.
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
DataSenderitself is aThread. 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)– usesrunBlockingfor 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
callbackFlowto create a flow that emitsSensorData(roll, yaw, gyroY, accelY). - Internally registers a
SensorEventListenerthat uses aMadgwickFusioninstance 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 innerCalibrationHelper.
vibrate(duration)
- Uses
Vibratorservice.
NetworkRepository.kt
- Simplified wrapper around
DataSender. Providesconnect(ip),sendMove,sendClick, etc. - In the full implementation,
MainViewModeluses this instead of creatingDataSenderdirectly.
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.LayoutParamswithTYPE_APPLICATION_OVERLAY. - Inflates
debug_overlay.xml(aTextView) and adds it to the window manager. - Singleton companion object:
instanceso that other parts can callupdateData(). updateData(roll, yaw, gyroY, accelY)updates theTextViewtext.
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
AppCompatActivitythat hosts the calibration wizard. - Uses
ViewPager2andCalibrationPagerAdapterto 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 byCalibrationActivity.
fragment_settings.xml
- Contains: three groups of (
TextView,SeekBar,TextView) for the three thresholds.
debug_overlay.xml
- A single
TextViewwith 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
MainActivitywith intent filter for LAUNCHER. - Service
DebugOverlayService.
Gradle Build Files
Project‑level build.gradle
- Defines
buildscriptwith 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
pluginManagementwith Google, MavenCentral, GradlePluginPortal.dependencyResolutionManagementwithFAIL_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
asynciofor networking andtkinterfor GUI. AirMouseGUIclass creates window with start/stop buttons, log area, sensitivity slider.AirMouseServerclass hashandle_clientthat reads JSON lines, processes messages, sends ACK.- Supports
move,click,doubleclick,rightclick,scroll. - Sensitivity slider updates
CONFIG["sensitivity"]and themouse.sensitivityattribute.
server.py – Console Server
- Reads
config.jsonfor host, port, sensitivity, log level, log file. - Logs to both console and file.
- Similar
AirMouseServerclass, but no GUI.
run.py / run.sh / run.bat
run.pychecks forpyautogui, installs dependencies, then launchesgui.py.- Shell/batch scripts for one‑click start.
perfetto_analyzer.py
- Uses
perfetto.trace_processor.TraceProcessorandpandas. - 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 assembleDebugin theandroidfolder. - Outputs APK location.
Common Code Flow
- User opens app –
MainActivity.onCreatesets up UI, initialises components, requests permissions. - User calibrates –
startCalibration()runsCalibrationHelpersuspend functions, updating UI. - User enters IP and starts –
startAirMouse()createsDataSenderandAutoReconnect, starts them, then startsSensorService. - SensorService registers listeners and waits for sensor events.
- On sensor event –
onSensorChangedcorrects raw values, runs Madgwick fusion, computes deltas, sendsmovepackets, detects gestures. - DataSender queues messages, sends over TCP, handles ACKs.
- PC server receives messages, moves cursor, clicks, scrolls, sends ACKs.
- User closes app –
onDestroystops 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.