First off, thank you for considering contributing to rmOneNoteSync! Whether you are fixing a bug, adding a feature, or improving documentation, your help is greatly appreciated. This guide is written to help you get oriented quickly, even if this is your very first open-source contribution.
- Getting Started
- System Architecture Overview
- Part 1: The Desktop Application
- Part 2: The Device Daemon
- How Everything Connects: The Data Flow
- Repository Structure
- Development Setup
- CI/CD Pipeline
- Submitting a Contribution
If you're new to open source, here's a quick overview of the workflow:
We follow a standard, clean open-source workflow:
- Find or Open an Issue: Before writing code, ensure there is an open Issue for your bug or feature. This prevents duplicated effort!
- Fork & Clone: Fork the repository to your account and clone it locally.
- Branch: Create a branch using a semantic naming convention (e.g.,
git checkout -b fix/issue-42-button-colororfeat/new-sync-ui). - Code & Test: Make your changes. Ensure
dotnet buildandmake allrun cleanly. - Push: Push the branch to your fork.
- Open a PR: Open a Pull Request against our
mainbranch.- Crucial: In your PR description, write
Fixes #ISSUE_NUMBERso GitHub automatically links and closes the issue when merged.
- Crucial: In your PR description, write
Before you start coding, please read the architecture sections below. Understanding how the two halves of the project communicate is the key to working on this codebase effectively.
This project uses a decoupled, two-part architecture. This design is intentional: it minimizes the CPU and battery impact on the reMarkable tablet by offloading all heavy processing (authentication, format conversion, cloud API calls) to the user's desktop computer.
flowchart LR
subgraph Desktop[Desktop Computer]
direction TB
subgraph Avalonia[Avalonia Desktop App]
Receiver[HTTP Sync Server<br/>receives .rm files]
Uploader[Sync Service<br/>converts & uploads]
Receiver --> Uploader
end
end
subgraph Tablet[reMarkable Tablet]
direction TB
subgraph Daemon[C Daemon - systemd]
Watcher[watcher service<br/>monitors xochitl]
Cache[(.sync_cache)]
Http[httpclient service]
Watcher -->|Updates| Cache
Cache -->|Reads| Http
end
end
OneNote((Microsoft OneNote<br/>Graph API))
%% Network Connections
Avalonia <-->|SSH: Deploy; HTTP: Config| Daemon
Http -->|HTTP POST| Receiver
Uploader -->|HTTPS Upload| OneNote
%% Styling
classDef cloud fill:#0078d4,stroke:#fff,stroke-width:2px,color:#fff;
class OneNote cloud;
Language & Framework: C# 13, .NET 9, Avalonia UI (cross-platform UI framework). Pattern: MVVM (Model-View-ViewModel) using CommunityToolkit.Mvvm.
The desktop app is the "brain" of the operation. It handles everything the tablet cannot or should not do: user authentication, database management, file format conversion, and communicating with the Microsoft Graph API.
All services and ViewModels are registered as singletons in Program.cs using Microsoft.Extensions.Hosting. This means you can access any service from anywhere by injecting it through a constructor. Platform-specific services (like device detection) are registered conditionally based on the OS.
These are simple C# data classes (POCOs) that define the shape of the data used throughout the application.
| File | Purpose |
|---|---|
SyncConfiguration.cs |
Stores all user-configurable settings: device IP, password, sync interval, OneNote tokens, cache retention, etc. This is the single source of truth for app configuration, persisted in SQLite. |
DeviceInfo.cs |
Represents a connected reMarkable device. Contains the IP, MAC address, connection type (USB/WiFi), and a model codename mapper (e.g., "ferrari" → "reMarkable Paper Pro"). Also defines the ConnectionState enum used across the UI (Disconnected, Detected, Authenticating, Connected, Configured, Error). |
PageMetadata.cs |
Tracks the state of each individual notebook page through its sync lifecycle (Pending → InProgress → Uploaded/Failed/Skipped). Contains links to local .rm files, converted output paths, OneNote page URLs, and retry counts. Also defines DocumentMetadata, which groups pages and maps them to OneNote notebook names via virtual path parsing. |
This is the heaviest part of the codebase. Each service is an implementation of an interface defined in Services/Interfaces/, which makes them mockable and testable.
| Service | Interface | Responsibility |
|---|---|---|
SshService |
ISshService |
All communication with the tablet goes through here. Uses SSH.NET for connecting, executing shell commands, and transferring files via SFTP. Exposes an OnConnectionChanged event consumed by the UI. |
SqliteDatabaseService |
IDatabaseService |
Manages the local SQLite database (sync.db). Stores SyncConfiguration, PageMetadata, DocumentMetadata, sync history, and generic telemetry key-value pairs. Uses Dapper as its micro-ORM. |
SyncService |
ISyncService |
The main orchestrator. Triggered via a bounded System.Threading.Channels queue instead of overlapping timers, using an event-driven fire-and-forget architecture. It converts .rm files via RmConverterService (limited by MaxThreads), enforces a 5-thread SemaphoreSlim limit and exponential backoff jitter for Microsoft Graph API HTTP 429 timeouts, and uploads the page. Fires granular progress events (Queued, Transcoding, etc.) consumed by SyncStatusViewModel. |
SyncServerService |
ISyncServerService |
Runs a local HTTP server (using HttpListener) on port 8080. This is the endpoint the tablet's httpclient daemon POSTs .rm files to. It validates API keys, checks the document whitelist, saves files to disk, records metadata in the database, and raises a FileReceived event that triggers SyncService. |
SyncServerHostedService |
(IHostedService) | A thin wrapper that starts and stops SyncServerService as a .NET background hosted service, so the HTTP server runs for the lifetime of the application. |
DeploymentService |
IDeploymentService |
Handles the full lifecycle of installing, updating, and uninstalling the C daemon on the tablet. It downloads the correct pre-compiled binaries from GitHub Releases (matching the device model), uploads them via SSH, writes config files, installs systemd service units, and starts the services. |
OneNoteAuthService |
IOneNoteAuthService |
Manages Microsoft Account authentication using MSAL. Handles interactive sign-in, silent token refresh, and sign-out. Persists the token cache to disk (msalcache.bin). |
OneNoteClient |
IOneNoteClient |
Wraps the Microsoft Graph SDK for all OneNote operations: listing/creating notebooks and sections, uploading InkML+HTML pages as multipart payloads, and managing page lifecycle. |
ConfigurationProviderService |
IConfigurationProviderService |
Manages device configuration via granular HTTP API. Instead of overwriting a single file, it interacts with endpoints like /endpoints and /whitelist to ensure non-destructive updates and persistence across multiple hosts. |
RmConverterService |
IRmConverterService |
Shells out to the rmc external tool to convert reMarkable's proprietary .rm ink format into InkML (.xml) and a presentation HTML file (.html). |
StartupService |
IStartupService |
Manages OS-level auto-start registration (e.g., adding a .desktop file to ~/.config/autostart on Linux). |
SoftwareUpdateService |
ISoftwareUpdateService |
Checks the GitHub Releases API for newer versions of the application. |
Device detection (finding the reMarkable on the network) is OS-specific:
| File | Platform | Strategy |
|---|---|---|
DeviceDetectionServiceBase.cs |
Shared | Contains common detection logic: first check USB (10.11.99.1), then cached IP, then ARP scan with limited retries. Implements a 30-second grace period for disconnections and reactive IP tracking (triggering automatic endpoint registration when the device changes IPs on the network). |
WindowsDeviceDetectionService.cs |
Windows | Uses WMI/System.Management for ARP table queries. |
LinuxDeviceDetectionService.cs |
Linux | Parses /proc/net/arp and uses ip neigh. |
MacOSDeviceDetectionService.cs |
macOS | Shells out to arp -a. |
GenericDeviceDetectionService.cs |
Fallback | Uses SharpPcap for generic packet capture based detection. |
ViewModels are the glue between Services and Views. They use [ObservableProperty] and [RelayCommand] source generators from CommunityToolkit.Mvvm to expose reactive properties and commands to the XAML views.
| File | Responsibility |
|---|---|
MainViewModel |
The root ViewModel. Manages navigation between all views, coordinates the full device connection flow (detect → authenticate → deploy → configure), and controls whether the Setup screen or the main Dashboard is shown. |
DashboardViewModel |
Populates the Dashboard: recently synced notebooks, recent activity log, last-connected timestamp, and clickable notebook links to OneNote. |
FolderPickerViewModel |
Lets the user browse the tablet's filesystem over SSH and select which folders/notebooks to sync. Selected document IDs are saved to SyncConfiguration.SyncFiles and also pushed to the daemon's whitelist. |
SyncStatusViewModel |
Displays real-time sync progress by subscribing to SyncService events. Shows per-page status, step counts, and overall progress bars. |
SettingsViewModel |
Exposes all user-configurable settings, device management actions (disconnect, clear cache, restart services), and OneNote sign-in/sign-out. |
LogsViewModel |
Fetches and displays logs from both the local app (Serilog files) and the remote device (journalctl over SSH), sorted by source and timestamp. |
ConfirmDialogViewModel |
A simple confirmation dialog (OK/Cancel) used before destructive actions. |
ViewModelBase |
Base class for all ViewModels. Extends ObservableObject. |
Avalonia XAML files for rendering the UI. Each View is bound to its corresponding ViewModel via DataContext. The root window is MainWindow.axaml, which hosts a navigation sidebar and swaps the content area between views.
Language: C (C99). Build System: GNU Make with cross-compilation via the reMarkable Codex toolchain.
The daemon is designed to be as lightweight as possible. It runs silently on the tablet and has two main duties: detecting document changes and managing its own configuration state via a built-in HTTP server.
This module provides a REST-like API on port 8000 for the desktop app to manage the device without needing full SSH access for every setting change.
GET /config/POST /config: Manages core daemon settings (daemon.conf).GET /endpoints/POST /endpoints/add: Manages the additive list of host URLs (endpoints.conf).GET /whitelist/PUT /whitelist: Manages the sync document whitelist (whitelist.dat).GET /filetree: Serves a JSON representation of the tablet's file structure (filters out items in the "trash").POST /sync: Instantly wakes up the watcher queue to perform a manual sync on demand rather than waiting for the sleep cycle.GET /version: Returns current daemon version.- Uses
flock()to ensure atomic, thread-safe access to configuration files.
This is the first of two systemd services (onenote-sync-watcher). Its job is to watch for notebook changes and queue modified pages for upload.
How it works:
- Reads paths and whitelist configuration from
daemon.confandwhitelist.dat. - Opens the xochitl documents directory using Linux's inotify API (
inotify_init,inotify_add_watch). - Enters an infinite event loop, watching for
IN_CREATE,IN_MODIFY,IN_DELETE, andIN_MOVED_TOevents. - When a
.metadatafile changes, it parses the JSON to extractlastModifiedandlastOpenedtimestamps. If the document was modified after it was last opened (meaning the user made edits), it scans all.rmpages in that document's subdirectory. - For each modified page, it checks the shared binary cache (
.sync_cache). If the page is new or has a newermtime, it's added to the cache queue. - Filters all events against a whitelist of document UUIDs stored in
whitelist.dat. Only explicitly selected notebooks are processed.
This is the second systemd service (onenote-sync-httpclient). It reads the cache queue and uploads files to the desktop app.
How it works:
- Reads configuration from
daemon.confand server URLs fromendpoints.conf. - Enters a polling loop (default: every 30 seconds).
- Each cycle, it reloads the shared cache, grabs up to 10 pending pages, and attempts to upload each one.
- For each page, it reconstructs the full virtual path (e.g.,
Academy/Physics/Calculus/Page 3) usingmetadata_parser, then sends the.rmfile via HTTP POST to one of the servers listed inendpoints.conf. - The HTTP request includes custom headers (
X-API-Key,X-Document-Path,X-Document-Id,X-Filename). - Priority Promotion: If an upload succeeds, that server URL is promoted to the top of
endpoints.conf, ensuring the most reliable connection is used first in future cycles. - Successfully uploaded pages are removed from the cache queue.
This is the IPC mechanism between the watcher and httpclient. It's a custom binary file format (.sync_cache) backed by a hash table of documents, each containing a linked list of pages.
Key structures:
PageEntry: UUID, page number, modification time, linked list pointer.DocumentEntry: Document UUID, linked list of pages, hash table chaining pointer.CacheHandle: The hash table itself, a dirty flag, and the file path.
The cache operates as a FIFO queue: the watcher pushes entries in, and the httpclient pops them out after successful upload. Both processes call cache_reload() before reading to stay in sync with the on-disk state.
Reconstructs human-readable folder paths from reMarkable's flat UUID-based filesystem. Each document on the tablet has a .metadata JSON file containing a parent UUID pointing to its containing folder. This module walks the parent chain to build paths like Shared Vault/Physics/Calculus Notes.
Also parses .content files to extract page numbers (the "idx" field), mapping page UUIDs to their user-visible page numbers.
A minimal, dependency-free HTTP client built on raw POSIX sockets. Only supports what the project needs:
http_get()— For fetching configuration from the desktop server.http_post_file()— For uploading.rmfiles asapplication/octet-streamwith custom headers.- No external dependencies (no libcurl) to keep the binary small and easy to cross-compile.
The daemon uses a split configuration system to ensure multi-host persistence without data loss.
| File | Purpose |
|---|---|
daemon.conf |
Core daemon settings (API key, upload interval, retry limits). |
endpoints.conf |
An additive, priority-ordered list of server URLs. Successful connections are moved to the top. |
whitelist.dat |
A flat list of document/folder UUIDs explicitly selected for sync. |
onenote-sync-watcher.service |
systemd unit file for the watcher daemon. Starts after home.mount, restarts on failure. |
onenote-sync-httpclient.service |
systemd unit file for the httpclient daemon. Starts after network is available. |
| File | Purpose |
|---|---|
cache_debug.c |
A standalone CLI tool that reads and dumps the .sync_cache file contents for debugging. Shows document IDs, page UUIDs, virtual paths, and filenames. |
test_server.py |
A Python mock of the desktop HTTP server for local testing without the full .NET app running. |
watcher_profiler.sh |
A shell script for profiling the watcher's CPU and memory usage on the device. |
Here's the complete journey of a notebook page from pen stroke to OneNote:
User edits inotify event Cache write HTTP POST
a notebook ───► watcher.c detects ───► .sync_cache ───► httpclient.c
on tablet .metadata change (binary FIFO) sends .rm file
│
▼
Desktop HTTP Server
(SyncServerService)
│
▼
Save .rm to disk
Record in SQLite
│
▼
SyncService triggers
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
RmConverterService OneNoteClient Database
(.rm → InkML + HTML) (Graph API upload) (status update)
│
▼
Page appears
in OneNote 🎉
rmOneNoteSync/
├── .github/
│ ├── assets/ # App icon, preview screenshots
│ ├── ISSUE_TEMPLATE/ # GitHub issue forms
│ └── workflows/
│ └── build.yml # CI/CD pipeline (see below)
├── app/
│ └── rmOneNoteSyncApp/ # C# Avalonia Desktop Application
│ ├── Assets/ # Icons, .desktop files
│ ├── Converters/ # XAML value converters (bool, color, equality)
│ ├── Models/ # Data classes (SyncConfiguration, DeviceInfo, PageMetadata)
│ ├── Services/
│ │ ├── Interfaces/ # 12 service contracts (ISshService, ISyncService, etc.)
│ │ └── Platform/ # OS-specific device detection (Windows, Linux, macOS)
│ ├── ViewModels/ # UI logic (MainViewModel, DashboardViewModel, etc.)
│ ├── Views/ # Avalonia XAML UI definitions
│ ├── Program.cs # Entry point, DI container setup, Serilog config
│ ├── App.axaml / App.axaml.cs # Avalonia application root
│ └── rmOneNoteSyncApp.csproj # Project file with all NuGet dependencies
├── rm-daemon/ # Native C codebase for the tablet
│ ├── src/
│ │ ├── watcher.c # inotify-based filesystem monitor
│ │ ├── httpclient.c # Polling HTTP upload client
│ │ ├── cache_io.c / .h # Shared binary cache (FIFO queue)
│ │ ├── metadata_parser.c / .h # Virtual path reconstruction from UUIDs
│ │ ├── http_simple.c / .h # Dependency-free raw socket HTTP client
│ │ └── version.h # Version stamp (injected by CI)
│ ├── config/ # systemd units and .conf templates
│ ├── testing_tools/ # cache_debug, test_server.py, profiler
│ └── Makefile # Cross-compilation build rules
├── installer.iss # Inno Setup script for Windows installer
├── install.sh # Portable installation script for Linux
└── uninstall.sh # Portable removal script for Linux
Prerequisites:
- .NET 9.0 SDK
- An IDE with C# support (e.g., JetBrains Rider, Visual Studio, or VS Code with the C# Dev Kit)
- Avalonia UI Extensions: If you are using Visual Studio or VS Code, ensure you install the official Avalonia UI extension for XAML previewing and code completion.
# Navigate to the app directory and restore dependencies
cd app/rmOneNoteSyncApp
dotnet restoreNote on Authentication: To enable OneNote synchronization during local development, you need an Azure App Registration Client ID. The repository contains a placeholder (
__REPLACE_ME__) inappsettings.json.Do not edit
appsettings.jsondirectly, as you might accidentally commit your personal Client ID to the repository! Instead, use the .NET User Secrets manager from within theapp/rmOneNoteSyncAppdirectory to safely store it locally on your machine:dotnet user-secrets set "AzureAd:ClientId" "YOUR_COPIED_CLIENT_ID_HERE"For local development without OneNote, you can skip this step. The app will still compile and run, but the authentication features will simply fail gracefully.
# Run the application in development mode
dotnet runPrerequisites:
- The reMarkable Codex toolchain for your target device
- GNU Make
# Source the cross-compilation environment for your device (Example for Paper Pro)
source /opt/codex/ferrari/4.3.98/environment-setup-cortexa53-crypto-remarkable-linux
# Build all binaries
cd rm-daemon
make allThis produces three binaries in the build/ directory: watcher, httpclient, and cache_debug.
Tip: If you are only working on the Desktop Application, you do not need the Codex toolchain at all. The daemon binaries are pre-compiled and distributed via GitHub Releases. The desktop app will download them automatically during the deployment phase.
The project uses GitHub Actions (build.yml) with four coordinated jobs:
| Job | Runner | What It Does |
|---|---|---|
build-core |
ubuntu-latest |
Publishes the .NET app for Windows, Linux, and macOS (self-contained, single-file). Also builds .deb and .rpm packages for Linux. |
build-windows-installer |
windows-latest |
Downloads the Windows portable artifact from build-core and packages it into an Inno Setup installer (.exe). |
build-rm-daemon |
ubuntu-latest (matrix) |
Cross-compiles the C daemon for all four reMarkable device targets (ferrari, chiappa, rm2, rm1) using the official Codex toolchains. |
publish-release |
ubuntu-latest |
Runs on tag pushes (v*). Collects all artifacts, packages them as .zip/.tar.gz, and publishes a GitHub Release. Tags containing alpha or beta are marked as pre-releases. |
Version numbers are injected from the Git tag into rmOneNoteSyncApp.csproj, installer.iss, and version.h during CI. The placeholder value 0.0.0-PLACEHOLDER is replaced with the clean version string (e.g., 1.0.0).
- Ensure your changes build without errors:
dotnet buildfor the C# app,make allfor the daemon. - If you've changed C code in the
rm-daemon, test it locally with thecache_debugtool and monitor the logs. - Write clear, descriptive commit messages.
- If your change is user-facing, update the relevant section of the
README.md. - Open a PR with a description of what you changed and why.
- 🐛 Bug fixes — Check the Issues tab for open bugs.
- 🧪 Testing on new platforms — If you own a reMarkable device and can test the app on an "Untested" combination from the Compatibility Matrix, please open a compatibility report!
- 📖 Documentation — Improved guides, screenshots, or translated documentation are always welcome.
- ✨ Feature contributions — Check for feature requests in the Issues tab, or propose your own.
Thank you for reading this far. Happy contributing! 🎉