diff --git a/OS/.gitkeep b/OS/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/OS/.gitkeep
diff --git a/OS/BACKUP/Readme.md b/OS/BACKUP/Readme.md
new file mode 100644
index 0000000..4578fef
--- /dev/null
+++ b/OS/BACKUP/Readme.md
@@ -0,0 +1,243 @@
+# Restoring Windows on ASUS T101HA
+
+Complete guide to reverting from Xubuntu back to the original Windows installation.
+
+---
+
+## What you have
+
+| Item | Location | Purpose |
+|---|---|---|
+| `t101ha.img.zst` | PC: `E:\git\02 cSharp using mono\main\EARS-LINUX\OS\BACKUP\`
USB: BACKUP partition | Full byte-exact disk image, 17GB compressed |
+| `parts.txt` | Same locations | Partition table reference |
+| `window-key.txt` | Same locations | Windows product key |
+
+**Verified:** SHA256 `CBD4348ACB6777D5110D00F15FD67A6CBABE96152B628E503A8F2F6127E4EB63` (both copies identical)
+
+**Image expands to:** 62,537,072,640 bytes — exact match to device size
+
+---
+
+## Device reference
+
+- **Internal eMMC:** `/dev/mmcblk1` — ~58GiB (62,537,072,640 bytes)
+- **Firmware:** 64-bit UEFI (confirmed via `fw_platform_size`)
+- **Original layout:** ESP, Microsoft reserved, Windows (`p3`), recovery (`p4`)
+
+> **WARNING:** Verify the device with `lsblk` every time. Device names can shift between boots.
+
+---
+
+## Method 1 — Full image restore (recommended)
+
+Restores the exact original system: Windows activated, ASUS recovery partition, everything.
+
+### Preparation
+
+If the image lives on the PC, copy it back to the USB stick's BACKUP partition first. Restoring over a network is not recommended.
+
+### Steps
+
+**1. Boot the Ventoy live session**
+
+Power off fully. Hold F2, tap power, keep holding. Confirm Secure Boot is disabled, boot from USB, select the Xubuntu ISO, choose "Try Xubuntu".
+
+**2. Identify the devices**
+
+```bash
+lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT
+```
+
+Confirm:
+- eMMC = the **~58GiB** device (`mmcblk1`)
+- USB = the **~115GiB** device with Ventoy/VTOYEFI/BACKUP partitions
+
+**3. Mount the backup**
+
+```bash
+sudo mkdir -p /mnt/backup
+sudo mount /dev/sda3 /mnt/backup
+ls -lh /mnt/backup
+```
+
+Adjust `sda3` to match the BACKUP partition from step 2.
+
+**4. Verify the image before writing**
+
+```bash
+zstd -t /mnt/backup/t101ha.img.zst
+```
+
+Must complete without error. If it fails, use the PC copy instead — do not proceed with a corrupt image.
+
+**5. Restore**
+
+```bash
+sudo blockdev --getsize64 /dev/mmcblk1
+```
+
+Confirm this prints `62537072640`. Then:
+
+```bash
+zstd -dc /mnt/backup/t101ha.img.zst | sudo dd of=/dev/mmcblk1 bs=4M status=progress
+sync
+```
+
+> **WARNING:** `of=` is the destructive direction. Everything on that device is erased. Read the command twice before pressing Enter.
+
+Takes 1–2 hours. Do not interrupt it, and do not run anything else — 2GB RAM.
+
+**6. Finish**
+
+```bash
+sync
+sudo umount /mnt/backup
+sudo poweroff
+```
+
+Remove the USB stick. Power on. Windows should boot, activated, exactly as it was.
+
+### With a progress bar
+
+```bash
+sudo apt install -y pv
+sudo sh -c 'zstd -dc /mnt/backup/t101ha.img.zst | pv -s 62537072640 | dd of=/dev/mmcblk1 bs=4M'
+```
+
+---
+
+## Method 2 — Recover files only (non-destructive)
+
+To extract files from the Windows image without wiping Xubuntu. Requires ~58GB free space.
+
+```bash
+zstd -d /path/to/t101ha.img.zst -o /tmp/full.img
+sudo losetup -fP --show /tmp/full.img
+```
+
+Note the loop device it prints (e.g. `/dev/loop0`), then:
+
+```bash
+sudo mkdir -p /mnt/win
+sudo mount -o ro /dev/loop0p3 /mnt/win
+```
+
+Windows lives on partition 3. Browse `/mnt/win`, copy what you need.
+
+Cleanup:
+
+```bash
+sudo umount /mnt/win
+sudo losetup -d /dev/loop0
+rm /tmp/full.img
+```
+
+If mount fails with a BitLocker error, the partition is encrypted — you'll need the recovery key from https://account.microsoft.com/devices/recoverykey
+
+---
+
+## Method 3 — Clean Windows install (image lost)
+
+Last resort. The product key is stored in firmware (ACPI MSDM table), so a clean install self-activates without entering anything.
+
+1. Download the Windows 10 ISO from Microsoft on the PC
+2. Write it to USB with Rufus — **GPT**, **UEFI** target
+3. Boot the tablet from it, install to the internal eMMC
+4. Activation happens automatically from firmware
+
+**Caveats:**
+- The ASUS recovery partition is not restored
+- Cherry Trail drivers may need manual installation from ASUS support
+- **Windows 10 is past end of support (October 2025), and this hardware cannot run Windows 11** — a restored image is a supported-nothing situation either way, but a clean install is strictly worse
+
+---
+
+## Recovering the product key
+
+From Linux, on the running tablet:
+
+```bash
+sudo strings /sys/firmware/acpi/tables/MSDM | tail -1
+```
+
+From the saved file:
+
+```bash
+cat /mnt/backup/window-key.txt; echo
+```
+
+From Windows:
+
+```powershell
+(Get-CimInstance SoftwareLicensingService).OA3xOriginalProductKey
+```
+
+**Note:** viewing `window-key.txt` in Notepad may show CJK garbage — it's an encoding guess, not corruption. Read it on Linux with `cat`, or open it in VS Code and set encoding to UTF-8.
+
+---
+
+## Verifying image integrity
+
+Quick check (structural):
+
+```bash
+zstd -t t101ha.img.zst
+```
+
+Full check (byte count vs device):
+
+```bash
+DEV=$(sudo blockdev --getsize64 /dev/mmcblk1)
+IMG=$(zstd -dc /path/to/t101ha.img.zst | wc -c)
+echo "device: $DEV"
+echo "image: $IMG"
+test "$DEV" = "$IMG" && echo MATCH || echo MISMATCH
+```
+
+Compare copies (PowerShell):
+
+```powershell
+Get-FileHash 'path\to\t101ha.img.zst' -Algorithm SHA256
+```
+
+> **WARNING:** `[ "$DEV" = "$IMG" ]` requires spaces inside the brackets. Without them the shell reports "command not found" and prints MISMATCH regardless of the actual values. Use `test` to avoid this.
+
+---
+
+## Troubleshooting
+
+**Restore finishes but won't boot**
+
+Check Secure Boot in BIOS — re-enable it, since the original Windows install expects it. Confirm the boot order lists Windows Boot Manager.
+
+**`dd` reports "No space left on device"**
+
+Wrong target device. Verify with `lsblk` that you're writing to the ~58GiB eMMC.
+
+**Image fails `zstd -t`**
+
+Use the other copy. If both fail, fall back to Method 3.
+
+**Progress appears frozen**
+
+Check it's actually stalled before interrupting:
+
+```bash
+ps -eo pid,stat,cmd | grep -v grep | grep -E 'dd|zstd'
+sudo dmesg | tail -30
+```
+
+STAT `D` plus `mmc` errors in dmesg indicates bad sectors. STAT `R`/`S` with a clean dmesg means it's just slow — eMMC on this tablet throttles when warm.
+
+**"read kernel buffer failed"**
+
+`dmesg` needs root here: `sudo dmesg`
+
+---
+
+## Notes
+
+- Keep both copies of the image. The USB stick is a single point of failure.
+- Restoring reverts to the disk state as of **27 July 2026**. Nothing created in Xubuntu survives.
+- Do a full `zstd -t` on whichever copy you plan to use *before* wiping anything.
+- Original image created with: `sudo zstd -1 -T0 -f -o /mnt/backup/t101ha.img.zst /dev/mmcblk1`
\ No newline at end of file
diff --git a/OS/Readme.md b/OS/Readme.md
new file mode 100644
index 0000000..c9bb2f5
--- /dev/null
+++ b/OS/Readme.md
@@ -0,0 +1,746 @@
+# Xubuntu Setup — Asus T101HA (`ears-tablet`)
+
+Working notes for setting up an Asus T101HA (Atom x5-Z8350, 2 GB RAM / ~1.8 GB
+usable, eMMC) as a Mono + GTK#3 development and runtime machine.
+
+User: `ears` · Hostname: `ears-tablet`
+
+---
+
+## Contents
+
+- [0. Recommended baseline](#0-recommended-baseline)
+- [1. Installing Xubuntu](#1-installing-xubuntu)
+ - [1.1 Check firmware bitness first](#11-check-firmware-bitness-first--this-determines-everything)
+ - [1.2 Build the USB](#12-build-the-usb)
+ - [1.3 BIOS](#13-bios)
+ - [1.4 Install](#14-install)
+ - [1.5 Post-install GRUB fix (IA32 only)](#15-post-install-grub-fix-ia32-machines-only)
+ - [1.6 Expect to fix afterwards](#16-expect-to-fix-afterwards)
+- [2. First boot / recovery mode](#2-first-boot--recovery-mode)
+- [3. Low-RAM tuning](#3-low-ram-tuning)
+ - [Option A — `zram-tools`](#option-a--zram-tools-simple)
+ - [Option B — `systemd-zram-generator`](#option-b--systemd-zram-generator-current-preferred-on-2404)
+ - [Swappiness](#swappiness)
+- [4. apt: repair and sources](#4-apt-repair-and-sources)
+ - [4.1 The modern layout](#41-the-modern-layout)
+ - [4.2 Correct stock sources](#42-correct-stock-sources-2510-example--substitute-your-codename)
+ - [4.3 Which host? archive vs old-releases](#43-which-host-archive-vs-old-releases)
+ - [4.4 Check for genuinely broken packages](#44-check-for-genuinely-broken-packages)
+ - [4.5 Don't do these](#45-dont-do-these)
+ - [4.6 `full-upgrade` checklist](#46-full-upgrade-checklist)
+ - [4.7 Reboot needed?](#47-reboot-needed)
+- [5. Mono + GTK#3](#5-mono--gtk3)
+ - [Install](#install)
+ - [Optional](#optional)
+ - [Verify](#verify)
+ - [Smoke test](#smoke-test)
+ - [Notes vs. the WSL guide](#notes-vs-the-wsl-guide)
+- [6. Display rotation + touchscreen](#6-display-rotation--touchscreen)
+ - [Identify devices](#identify-devices)
+ - [Matrices](#matrices)
+ - [Persistence script](#persistence-script)
+ - [Login screen (LightDM)](#login-screen-lightdm--separate-from-your-session)
+ - [Better: rotate before the session starts](#better-rotate-before-the-session-starts)
+ - [Wallpaper breaks after rotating](#wallpaper-breaks-after-rotating)
+ - [Auto-rotation (optional)](#auto-rotation-optional)
+- [7. Audio](#7-audio)
+ - [Install and test](#install-and-test)
+ - [Choosing the right card](#choosing-the-right-card)
+ - [Renumbering at the kernel level](#renumbering-at-the-kernel-level)
+ - [Caveats](#caveats)
+- [8. Running a script at boot](#8-running-a-script-at-boot)
+- [9. File transfer to/from Windows](#9-file-transfer-tofrom-windows)
+- [10. Quick reference](#10-quick-reference)
+
+---
+
+## 0. Recommended baseline
+
+**Target: Xubuntu 24.04 LTS (Minimal ISO).**
+
+Reasons, from the apt/Mono investigation:
+- The machine was found running **Ubuntu 25.10 "questing"**, which hit end of
+ life on **9 July 2026**. No further updates will ever be published for it.
+- `gtk-sharp3` is **not packaged on 25.10**, and upgrading to 26.04 would not
+ bring it back.
+- `gtk-sharp3` **is** packaged on 24.04 (noble), supported to 2029.
+- Xfce on 24.04 idles around 500–600 MB, which matters at 1.8 GB RAM.
+
+Mono + `mcs` + GTK# is the correct toolchain for this hardware — the .NET SDK
+(~800 MB) and Avalonia are far heavier.
+
+Before reinstalling, back up: `*.cs`, `*.glade`, SSH keys, browser profile.
+
+```bash
+lsblk # is /home a separate partition?
+df -h /
+free -h
+mkdir -p ~/backup && cp ~/*.cs ~/*.glade ~/backup/
+```
+
+If `/home` is its own partition, it can be preserved during install (assign
+`/home` **without** format). Use the same username to keep permissions clean.
+
+---
+
+## 1. Installing Xubuntu
+
+### 1.1 Check firmware bitness first — this determines everything
+
+In Windows: **Settings → System → About → System type**
+
+- *32-bit operating system, x64-based processor* → **IA32 UEFI**, needs the
+ bootia32 workaround below.
+- *64-bit* → skip the GRUB steps.
+
+### 1.2 Build the USB
+
+Ventoy includes IA32 UEFI support, so try it first. If it won't boot, fall back
+to Rufus and copy a 32-bit `bootia32.efi` (GRUB) into `/EFI/BOOT` on the stick.
+
+From Linux, writing directly:
+
+```bash
+sudo dd if=xubuntu-24.04-minimal-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync
+```
+
+> Verify the target with `lsblk` first. `dd` to the wrong disk destroys it
+> silently.
+
+Verify the ISO: `sha256sum` against the checksum on the download page.
+
+### 1.3 BIOS
+
+- F2 at power-on
+- **Disable Secure Boot** (the ia32 GRUB is unsigned)
+- Boot menu is usually F12 / Esc
+
+### 1.4 Install
+
+Choose **Install**, not *Try* — the live session eats RAM you don't have.
+Don't open Firefox or anything else during install. Let it create swap.
+
+### 1.5 Post-install GRUB fix (IA32 machines only)
+
+The installer writes a 64-bit `grubx64.efi` the firmware can't read. Boot the
+live session, chroot in, and:
+
+```bash
+grub-install --target=i386-efi --efi-directory=/boot/efi
+```
+
+This is **not optional** on IA32 firmware.
+
+### 1.6 Expect to fix afterwards
+
+| Component | Notes |
+|---|---|
+| Wi-Fi | RTL8723BS SDIO — finicky |
+| Audio | ESS ES8316 codec, needs UCM configs |
+| Touchscreen | Works, but rotation needs manual matrix (§6) |
+| Auto-rotation | Needs `iio-sensor-proxy` + custom handler |
+
+A recent 6.x kernel fixes most of these.
+
+---
+
+## 2. First boot / recovery mode
+
+If the user account lacks admin rights:
+
+```bash
+usermod -aG sudo ears
+groups ears # confirm 'sudo' appears
+hostnamectl set-hostname ears-tablet
+localectl set-locale LANG=en_US.UTF-8 # optional, cosmetic
+exit # then 'resume'
+```
+
+`usermod -aG sudo` is the one that isn't optional — without it you can log in
+but can't run any admin command.
+
+---
+
+## 3. Low-RAM tuning
+
+### Option A — `zram-tools` (simple)
+
+```bash
+sudo apt install zram-tools
+```
+
+Edit `/etc/default/zramswap`:
+```
+PERCENT=60
+ALGO=zstd
+```
+
+### Option B — `systemd-zram-generator` (current, preferred on 24.04+)
+
+```bash
+sudo apt install systemd-zram-generator
+
+sudo tee /etc/systemd/zram-generator.conf > /dev/null <<'EOF'
+[zram0]
+zram-size = min(ram / 2, 4096)
+compression-algorithm = zstd
+swap-priority = 100
+EOF
+
+sudo systemctl daemon-reload
+sudo systemctl start systemd-zram-setup@zram0.service
+swapon --show # expect /dev/zram0
+zramctl
+```
+
+The package does nothing without a config file containing at least one section.
+
+### Swappiness
+
+Swapping to zram is cheap, so raise it so the kernel actually uses it:
+
+```bash
+echo 'vm.swappiness=100' | sudo tee /etc/sysctl.d/99-zram.conf
+```
+
+Reboot. Verdict: worth it at 2–4 GB, marginal at 8 GB, skip above 16 GB.
+zram compresses **RAM** — it does nothing for low disk space.
+
+---
+
+## 4. apt: repair and sources
+
+### 4.1 The modern layout
+
+Since 24.04, archive config lives in **`/etc/apt/sources.list.d/ubuntu.sources`**
+(deb822 format). `/etc/apt/sources.list` being empty is normal, not a symptom.
+Most apt advice online predates this and edits a file that does nothing.
+
+### 4.2 Correct stock sources (25.10 example — substitute your codename)
+
+```bash
+sudo tee /etc/apt/sources.list.d/ubuntu.sources > /dev/null <<'EOF'
+Types: deb
+URIs: http://archive.ubuntu.com/ubuntu/
+Suites: questing questing-updates questing-backports
+Components: main restricted universe multiverse
+Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
+
+Types: deb
+URIs: http://security.ubuntu.com/ubuntu/
+Suites: questing-security
+Components: main restricted universe multiverse
+Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
+EOF
+
+sudo apt update
+```
+
+Do not drop the `Signed-By:` line — without it apt falls back to the legacy
+trusted-keyring and you get confusing "not signed" errors later.
+
+`ubuntu.sources.curtin.orig` is the installer's pristine backup. apt ignores
+`.orig` files. Useful as a reference, don't copy blindly.
+
+### 4.3 Which host? archive vs old-releases
+
+An EOL release only moves to `old-releases.ubuntu.com` when the migration
+actually runs — which can lag the EOL date by weeks. Pointing there too early
+gives four clean 404s. Test before switching:
+
+```bash
+for p in questing questing-updates questing-backports questing-security; do
+ echo -n "$p: "
+ curl -sI "http://old-releases.ubuntu.com/ubuntu/dists/$p/Release" | head -1
+done
+
+curl -sI http://archive.ubuntu.com/ubuntu/dists/questing/Release | head -1
+```
+
+`200` = pocket exists. `404` = drop it or use the other host.
+
+### 4.4 Check for genuinely broken packages
+
+All read-only, cheapest first:
+
+```bash
+sudo dpkg --audit # clean = no output at all
+sudo apt-get check # verifies the dependency tree
+dpkg -l | grep -v '^ii' | grep -v '^rc'
+apt-mark showhold # held packages are silently skipped on upgrade
+```
+
+dpkg status codes (desired state, then actual):
+
+| Code | Meaning |
+|---|---|
+| `ii` | Healthy |
+| `rc` | Removed, config left — harmless |
+| `iU` | Unpacked, never configured |
+| `iF` | Half-configured |
+| `iH` | Half-installed |
+| `iW` / `it` | Waiting on / pending triggers |
+| `*R*` | Reinstall required → `apt install --reinstall ` |
+
+Then repair:
+
+```bash
+sudo dpkg --configure -a
+sudo apt --fix-broken install
+sudo apt full-upgrade
+```
+
+### 4.5 Don't do these
+
+```bash
+# WRONG — killall receives 'sudo', 'rm', paths etc. as process names
+sudo killall apt apt-get dpkg 2>/dev/null sudo rm /var/lib/apt/lists/lock ...
+
+# DANGEROUS — rm -rf also targets files named sudo, apt, clean, update in $PWD
+sudo rm -rf /var/lib/apt/lists/* sudo apt clean sudo apt update
+```
+
+These must be separate lines. Also: killing `dpkg` mid-transaction is what
+*creates* the broken state. Check first with `ps aux | grep -E 'apt|dpkg'`.
+
+Don't remove `universe` — it's an Ubuntu component, not a PPA, and
+`gtk-sharp3` lives there. And never run
+`add-apt-repository --remove ppa:repository-name/ppa` literally; that's a
+placeholder from a copied guide.
+
+### 4.6 `full-upgrade` checklist
+
+- Use `full-upgrade`, not `upgrade` — it permits removals where deps changed
+- Read the summary line; mass removals of desktop/kernel packages = stop
+- Plug the tablet in; power loss mid-`dpkg` recreates the broken state
+- On config prompts, keeping the local version (the default) is safe unless
+ you know you edited it
+- Then `sudo apt autoremove` — but answer `n` if anything with `efi` appears
+- Reboot before `do-release-upgrade` so the newest kernel is running
+
+### 4.7 Reboot needed?
+
+```bash
+[ -f /var/run/reboot-required ] && cat /var/run/reboot-required.pkgs
+sudo needrestart # interactive; shows what's running old libs
+```
+
+Kernel / initramfs / systemd / glibc → reboot. Everything else → restart the
+service.
+
+---
+
+## 5. Mono + GTK#3
+
+### Install
+
+```bash
+sudo add-apt-repository universe
+sudo apt update
+sudo apt install mono-complete gtk-sharp3
+```
+
+| Package | Contents | Size |
+|---|---|---|
+| `mono-runtime` | JIT + `mscorlib` only | ~15 MB |
+| `mono-devel` | Compiler + the BCL you need | ~200–400 MB |
+| `mono-complete` | Runtime + `mcs` + entire class library + F#/VB | ~300–500 MB |
+
+**Avoid `mono-runtime`.** The BCL is split across dozens of
+`libmono-system-*-cil` packages; hello-world runs, but anything touching LINQ,
+XML, HTTP, or `System.IO.Ports` (the COM-port combo box in `homepage.glade`)
+throws `Could not load file or assembly`. On a tight disk, `mono-devel` saves
+~100 MB over `mono-complete` and still covers everything this project needs.
+
+### Optional
+
+```bash
+sudo apt install glade # visual designer — skip on this machine
+sudo apt install libgdiplus # only if using System.Drawing
+sudo apt install libcanberra-gtk3-module # silences a harmless console warning
+```
+
+### Verify
+
+```bash
+mono --version
+mcs --version
+pkg-config --list-all | grep gtk-sharp # want gtk-sharp-3.0
+apt policy gtk-sharp3
+```
+
+### Smoke test
+
+```bash
+cat > gtkcheck.cs <<'EOF'
+using System;
+using Gtk;
+
+class GtkCheck {
+ static void Main() {
+ Application.Init();
+ var win = new Window("Hello GTK#3");
+ win.SetDefaultSize(300, 200);
+ win.DeleteEvent += (o, args) => Application.Quit();
+
+ var button = new Button("Click me");
+ button.Clicked += (o, args) => Console.WriteLine("Clicked!");
+ win.Add(button);
+
+ win.ShowAll();
+ Application.Run();
+ }
+}
+EOF
+
+mcs -pkg:gtk-sharp-3.0 gtkcheck.cs
+mono gtkcheck.exe
+```
+
+Do the throwaway test first — it isolates "is gtk-sharp3 working" from "does my
+app compile."
+
+Then the real thing:
+
+```bash
+mcs -pkg:gtk-sharp-3.0 program.cs MainWindow.cs ArduinoConnection.cs -out:test.exe
+mono test.exe
+```
+
+(`ArduinoConnection.cs` is needed because `MainWindow` calls
+`_arduino.Disconnect()`; omitting it gives `CS0246`.)
+
+### Notes vs. the WSL guide
+
+- No WSLg, no `$DISPLAY` fiddling — Xfce is itself GTK3, so the app picks up the
+ system theme and the window just appears.
+- Only exception: over SSH you need `ssh -X`.
+- Companion files (`-r:` DLLs, `homepage.glade`) must sit next to the `.exe`, or
+ you get `FileNotFoundException`.
+- `homepage.glade` targets GTK 3.24, which 22.04 and 24.04 both ship.
+- `gtk-sharp2` is gone from 24.04 onward — use `gtk-sharp3`.
+
+---
+
+## 6. Display rotation + touchscreen
+
+X11 does **not** rotate touch input when you rotate the display. The touch
+device needs its own coordinate transformation matrix.
+
+### Identify devices
+
+```bash
+xrandr # output name — DSI-1, or None-1 on this unit
+xinput list --name-only # look for Silead / GSL / SIS, NOT 'Touchpad'
+```
+
+> On this tablet the output reported as **`None-1`** (xrandr's fallback when the
+> connector type isn't identified — it can change between kernels) and the
+> touchscreen as **`SIS0457:00 0457:11ED`**.
+
+### Matrices
+
+| Rotation | Matrix |
+|---|---|
+| normal | `1 0 0 0 1 0 0 0 1` |
+| right (90° CW) | `0 1 0 -1 0 1 0 0 1` |
+| left (90° CCW) | `0 -1 1 1 0 0 0 0 1` |
+| inverted (180°) | `-1 0 1 0 -1 1 0 0 1` |
+
+```bash
+xrandr --output None-1 --rotate right
+xinput set-prop "SIS0457:00 0457:11ED" "Coordinate Transformation Matrix" 0 1 0 -1 0 1 0 0 1
+```
+
+### Persistence script
+
+```bash
+nano ~/rotate.sh
+```
+
+```bash
+#!/bin/bash
+sleep 3
+OUT=$(xrandr | awk '/ connected/{print $1; exit}')
+xrandr --output "$OUT" --rotate right
+TS=$(xinput list --name-only | grep -iE 'silead|gsl|touchscreen|SIS' | grep -vi 'touchpad' | head -1)
+[ -n "$TS" ] && xinput set-prop "$TS" "Coordinate Transformation Matrix" 0 1 0 -1 0 1 0 0 1
+xset s off
+xset -dpms
+```
+
+```bash
+chmod +x ~/rotate.sh
+~/rotate.sh # test before automating
+```
+
+Then **Settings → Session and Startup → Application Autostart → Add**, command
+`/home/ears/rotate.sh`.
+
+> **Gotcha:** grepping bare `touch` matches the **trackpad** and rotates the
+> pointer instead. Use `touchscreen` and exclude `touchpad`. To undo:
+> ```bash
+> TP=$(xinput list --name-only | grep -i -m1 touchpad)
+> xinput set-prop "$TP" "Coordinate Transformation Matrix" 1 0 0 0 1 0 0 0 1
+> ```
+> Or just log out — the matrix isn't saved anywhere.
+
+The `sleep 3` matters; the touchscreen isn't registered the instant the session
+starts. Raise to 5 if touch still comes out wrong.
+
+**Try Settings → Display first.** Its rotation dropdown often persists on its
+own, leaving the script to handle only the `xinput` part.
+
+### Login screen (LightDM — separate from your session)
+
+```bash
+sudo nano /etc/lightdm/lightdm.conf
+```
+```ini
+[Seat:*]
+display-setup-script=/usr/bin/xrandr --output None-1 --rotate right
+```
+
+### Better: rotate before the session starts
+
+```bash
+sudo nano /etc/X11/xorg.conf.d/10-monitor.conf
+```
+```
+Section "Monitor"
+ Identifier "DSI-1"
+ Option "Rotate" "right"
+EndSection
+```
+
+Whole machine including the text console — add to `/etc/default/grub`:
+`fbcon=rotate:1` plus `video=DSI-1:panel_orientation=right_side_up`, then
+`sudo update-grub`.
+
+### Wallpaper breaks after rotating
+
+The desktop paints the wallpaper onto a surface sized for the old geometry and
+doesn't repaint after `xrandr`. On Xfce:
+
+```bash
+xfdesktop --reload
+```
+
+Also set the image style to **scaled** — `spanned` and `zoom` misbehave badly
+when the aspect ratio flips. Rotating before the session starts avoids this
+entirely.
+
+### Auto-rotation (optional)
+
+Xfce has no built-in handler. Confirm the sensor works before writing anything:
+
+```bash
+sudo apt install -y iio-sensor-proxy
+monitor-sensor # tilt the tablet; orientation should print
+```
+
+If nothing prints, this unit needs a kernel quirk — not worth chasing.
+
+```bash
+monitor-sensor | while read -r line; do
+ case "$line" in
+ *normal*) R=normal; M="1 0 0 0 1 0 0 0 1" ;;
+ *right-up*) R=right; M="0 1 0 -1 0 1 0 0 1" ;;
+ *left-up*) R=left; M="0 -1 1 1 0 0 0 0 1" ;;
+ *bottom-up*) R=inverted; M="-1 0 1 0 -1 1 0 0 1" ;;
+ *) continue ;;
+ esac
+ xrandr --output DSI-1 --rotate "$R"
+ xinput set-prop "SIS0457:00 0457:11ED" "Coordinate Transformation Matrix" $M
+done
+```
+
+> Most community auto-rotate scripts only call `xrandr` and leave touch behind.
+> If a script doesn't contain `xinput set-prop`, it won't rotate touch.
+
+---
+
+## 7. Audio
+
+### Install and test
+
+```bash
+sudo apt install alsa-utils # this is what provides aplay
+
+aplay --version
+aplay -l # list playback devices
+aplay /usr/share/sounds/alsa/Front_Center.wav
+speaker-test -c 2 -t wav -l 1
+```
+
+PipeWire/PulseAudio layer:
+
+```bash
+pactl info # "Server Name" tells you which
+pactl list short sinks
+paplay /usr/share/sounds/alsa/Front_Center.wav
+```
+
+### Choosing the right card
+
+Neither config file exists by default — create whichever you prefer:
+
+```bash
+nano ~/.asoundrc # per-user; wins over the system file
+# or
+sudo nano /etc/asound.conf # system-wide, incl. root and systemd services
+```
+
+```
+defaults.pcm.card 1
+defaults.ctl.card 1
+```
+
+`ctl` is what `alsamixer` reads, `pcm` is what `aplay` reads — set both. No
+reload needed; it's read per-process.
+
+With `plughw` conversion behaviour as the default:
+
+```
+pcm.!default {
+ type plug
+ slave.pcm "hw:1,0"
+}
+ctl.!default {
+ type hw
+ card 1
+}
+```
+
+Verify — `-v` prints the resolved device:
+
+```bash
+aplay -D default -v /usr/share/sounds/alsa/Front_Center.wav 2>&1 | head -20
+```
+
+### Renumbering at the kernel level
+
+```bash
+cat /proc/asound/modules
+sudo nano /etc/modprobe.d/alsa-card-order.conf
+```
+```
+options snd_soc_sst_bytcr_rt5640 index=0
+options snd_hda_intel index=1
+```
+```bash
+sudo update-initramfs -u && sudo reboot
+```
+
+### Caveats
+
+- **PipeWire ignores all of the above.** `~/.asoundrc` only governs programs
+ talking to ALSA directly (`aplay`, `alsamixer`). For desktop apps use
+ `pactl set-default-sink` or `pavucontrol`.
+- Save your mixer levels or they come back muted: `sudo alsactl store 1`
+- Device busy → use `aplay -D default file.wav`, not `-D hw:0,0`
+- Permission errors → `sudo usermod -aG audio $USER`, then re-login
+- `aplay` handles WAV/AU/RAW only — use `mpg123` or `ffplay` for MP3/FLAC
+
+---
+
+## 8. Running a script at boot
+
+```bash
+sudo nano /usr/local/bin/myscript.sh
+sudo chmod +x /usr/local/bin/myscript.sh
+```
+
+Start with `#!/bin/bash` and use **absolute paths** — there's almost no
+environment at boot.
+
+```bash
+sudo nano /etc/systemd/system/myscript.service
+```
+
+```ini
+[Unit]
+Description=My boot script
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/bin/myscript.sh
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+```
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable --now myscript.service
+systemctl status myscript.service
+journalctl -u myscript.service
+```
+
+Variations:
+
+- Long-running daemon → `Type=simple`, drop `RemainAfterExit`
+- Before the login screen → `Before=display-manager.service` in `[Unit]`,
+ `WantedBy=display-manager.service` in `[Install]`
+- Quick and dirty → `sudo crontab -e`, then
+ `@reboot /usr/local/bin/myscript.sh` (no logging, no ordering)
+- Drop `After=`/`Wants=` if networking isn't needed — it'll run earlier
+
+> **A root systemd service cannot rotate the display** — no `DISPLAY`, no
+> `XAUTHORITY`. Use the config-level methods in §6 instead. Same for audio: a
+> root service has no audio session, so use `systemctl --user` or target the
+> hardware device directly.
+
+---
+
+## 9. File transfer to/from Windows
+
+**Tailscale is not required.** It creates a private network *across the
+internet* — only needed to reach the tablet from outside your home. For a
+tablet and PC on the same Wi-Fi it's just another background daemon on a
+2 GB machine.
+
+Lightest option — SSH:
+
+```bash
+sudo apt install -y openssh-server
+```
+
+Connect from Windows with WinSCP or `scp` in PowerShell. No configuration
+beyond the above.
+
+Samba is only worth it if you want the tablet's folders to appear in Windows
+Explorer as a normal network drive.
+
+---
+
+## 10. Quick reference
+
+```bash
+# state
+free -h; df -h /; lsblk; swapon --show
+cat /etc/os-release
+. /etc/os-release; echo $VERSION_CODENAME
+
+# apt health
+sudo dpkg --audit; sudo apt-get check; apt-mark showhold
+ls /etc/apt/sources.list.d/
+cat /etc/apt/sources.list.d/ubuntu.sources
+
+# display
+xrandr; xinput list --name-only
+~/rotate.sh; xfdesktop --reload
+
+# audio
+aplay -l; pactl list short sinks
+aplay /usr/share/sounds/alsa/Front_Center.wav
+sudo alsactl store 1
+
+# build
+mcs -pkg:gtk-sharp-3.0 program.cs MainWindow.cs ArduinoConnection.cs -out:test.exe
+mono test.exe
+```
\ No newline at end of file
diff --git a/Readme.md b/Readme.md
index 402d96e..51512ad 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,20 +1,28 @@
# Mono + GTK# + Glade — WSL Setup Guide
## Table of Contents
-- [1. Prerequisites](#1-prerequisites)
-- [2. Install Mono](#2-install-mono)
-- [3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)](#3-install-gtk-gtk-30-for-ubuntu-2404)
-- [4. Install Glade (visual UI designer)](#4-install-glade-visual-ui-designer)
-- [5. Optional: libgdiplus (only if using System.Drawing)](#5-optional-libgdiplus-only-if-using-systemdrawing)
-- [6. Compiling & Running](#6-compiling--running)
-- [7. Minimal Working Examples](#7-minimal-working-examples)
-- [8. Custom Output Names, Targets & Exporting DLLs](#8-custom-output-names-targets--exporting-dlls)
-- [9. Common Errors & Fixes](#9-common-errors--fixes)
-- [10. Notes on Alternatives](#10-notes-on-alternatives)
+- [Mono](#mono)
+ - [1. Prerequisites](#1-prerequisites)
+ - [2. Install Mono](#2-install-mono)
+ - [3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)](#3-install-gtk-gtk-30-for-ubuntu-2404)
+ - [4. Install Glade (visual UI designer)](#4-install-glade-visual-ui-designer)
+ - [5. Optional: libgdiplus (only if using System.Drawing)](#5-optional-libgdiplus-only-if-using-systemdrawing)
+ - [6. Compiling & Running](#6-compiling--running)
+ - [7. Minimal Working Examples](#7-minimal-working-examples)
+ - [8. Custom Output Names, Targets & Exporting DLLs](#8-custom-output-names-targets--exporting-dlls)
+ - [9. Common Errors & Fixes](#9-common-errors--fixes)
+ - [10. Notes on Alternatives](#10-notes-on-alternatives)
+- [OS](#os)
+ - [Workflow](#workflow)
+ - [OS images checked](#os-images-checked)
+ - [OS images to check](#os-images-to-check)
+
---
-## 1. Prerequisites
+## Mono
+
+### 1. Prerequisites
- Windows 11 (or updated Windows 10) with WSL2 and **WSLg** enabled for GUI support
- Ubuntu 24.04 (noble) or similar WSL distro
@@ -26,7 +34,7 @@
---
-## 2. Install Mono
+### 2. Install Mono
```bash
sudo apt update
@@ -41,7 +49,7 @@
---
-## 3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)
+### 3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)
> Note: `gtk-sharp2` is **not available** on Ubuntu 24.04 (noble). Use `gtk-sharp3` instead.
@@ -59,7 +67,7 @@
gtk-sharp-3.0 Gtk - Gtk
---
-## 4. Install Glade (visual UI designer)
+### 4. Install Glade (visual UI designer)
```bash
sudo apt install glade
@@ -73,7 +81,7 @@
---
-## 5. Optional: libgdiplus (only if using System.Drawing)
+### 5. Optional: libgdiplus (only if using System.Drawing)
Only needed if your code uses `System.Drawing.Bitmap`, `Graphics`, etc. (not required for plain GTK#/Glade apps):
@@ -83,21 +91,21 @@
---
-## 6. Compiling & Running
+### 6. Compiling & Running
-### Plain console C# program
+#### Plain console C# program
```bash
mcs myprogram.cs
mono myprogram.exe
```
-### GTK# 3.0 program (no Glade)
+#### GTK# 3.0 program (no Glade)
```bash
mcs -pkg:gtk-sharp-3.0 myapp.cs
mono myapp.exe
```
-### GTK# 3.0 program using a Glade file
+#### GTK# 3.0 program using a Glade file
```bash
mcs -pkg:gtk-sharp-3.0 -pkg:glade-sharp-3.0 myapp.cs
mono myapp.exe
@@ -106,9 +114,9 @@
---
-## 7. Minimal Working Examples
+### 7. Minimal Working Examples
-### Hello World (console)
+#### Hello World (console)
```csharp
using System;
@@ -123,7 +131,7 @@
mono hello.exe
```
-### Hello World (GTK# window, no Glade)
+#### Hello World (GTK# window, no Glade)
```csharp
using System;
using Gtk;
@@ -149,7 +157,7 @@
mono gtkcheck.exe
```
-### Loading a UI built in Glade
+#### Loading a UI built in Glade
```csharp
using System;
using Gtk;
@@ -175,11 +183,11 @@
---
-## 8. Custom Output Names, Targets & Exporting DLLs
+### 8. Custom Output Names, Targets & Exporting DLLs
By default, `mcs file.cs` names the output after the source file (`file.exe`). You can control this with `-out:` and change what kind of binary is produced with `-target:`.
-### 8.1 Custom output name
+#### 8.1 Custom output name
```bash
mcs -pkg:gtk-sharp-3.0 Program.cs -out:MyCustomApp.exe
```
@@ -188,7 +196,7 @@
mono MyCustomApp.exe
```
-### 8.2 `-target` options
+#### 8.2 `-target` options
| Target | Produces | Notes |
|---|---|---|
| `exe` (default) | Console executable | Shows a console window when run on Windows |
@@ -202,7 +210,7 @@
```
This produces `programwin.exe`, which on Windows will run without popping up a console window alongside your GTK window.
-### 8.3 Exporting a DLL
+#### 8.3 Exporting a DLL
If you want to package reusable code (helper classes, business logic, etc.) as a library instead of a standalone app:
@@ -213,7 +221,7 @@
- No `Main()` method is required in a `library` target (though it's fine if one class in your project has one — it just won't be used as an entry point for the DLL itself).
- This creates `MyLibrary.dll`, a Mono/.NET assembly that other C# programs can reference.
-### 8.4 Using a DLL in another program
+#### 8.4 Using a DLL in another program
Suppose `MyLibrary.dll` contains:
```csharp
@@ -253,7 +261,7 @@
mono ConsumerApp.exe
```
-### 8.5 Combining `-target:library` with GTK# packages
+#### 8.5 Combining `-target:library` with GTK# packages
If your DLL itself uses GTK# types (e.g., a shared custom widget), include the package flag when building the library too:
```bash
mcs -target:library -pkg:gtk-sharp-3.0 MyGtkWidgets.cs -out:MyGtkWidgets.dll
@@ -263,7 +271,7 @@
mcs -pkg:gtk-sharp-3.0 ConsumerApp.cs -r:MyGtkWidgets.dll -out:ConsumerApp.exe
```
-### 8.6 Quick reference
+#### 8.6 Quick reference
```bash
# Console exe, custom name
mcs Program.cs -out:myapp.exe
@@ -278,7 +286,7 @@
mcs Consumer.cs -r:MyLibrary.dll -out:Consumer.exe
```
-### 8.7 Running the compiled .exe on Windows (outside WSL)
+#### 8.7 Running the compiled .exe on Windows (outside WSL)
Compiling in WSL produces a `.exe` that targets the .NET/Mono runtime — it is **not** a native Windows binary, and it will not run on Windows by itself. Two things are needed:
@@ -308,7 +316,7 @@
---
-## 9. Common Errors & Fixes
+### 9. Common Errors & Fixes
| Error | Cause | Fix |
|---|---|---|
@@ -322,7 +330,7 @@
---
-## 10. Notes on Alternatives
+### 10. Notes on Alternatives
GTK# is a legacy, lightly-maintained binding. For new projects, consider:
- **Avalonia UI** — modern, XAML-based, cross-platform, actively maintained
@@ -353,4 +361,22 @@
to run note:
same for all cases
- mono NAME_OF_THE_FILE.exe
\ No newline at end of file
+ mono NAME_OF_THE_FILE.exe
+
+## OS
+
+To test the application/setup across different Linux distributions, I used **Ventoy** to create a multiboot USB drive. Ventoy lets you copy multiple ISO files onto a single USB stick and choose which one to boot at startup, without needing to reformat or re-flash the drive for each OS.
+
+### Workflow
+1. Install Ventoy on the target USB drive.
+2. Copy the desired `.iso` files directly onto the Ventoy partition (no extraction needed).
+3. Boot from the USB, select the ISO from the Ventoy boot menu, and test the OS live or install it.
+
+### OS images checked
+- Fedora-Workstation-Live-44-1.7.x86_64
+- lubuntu-26.04-desktop-amd64
+- xubuntu-25.10-desktop-amd64
+
+### OS images to check
+- antiX-26_x64-full
+- xubuntu-26.04-minimal-amd64
\ No newline at end of file
diff --git a/program/ArduinoConnection.cs b/program/ArduinoConnection.cs
new file mode 100644
index 0000000..6edc385
--- /dev/null
+++ b/program/ArduinoConnection.cs
@@ -0,0 +1,373 @@
+using System;
+using System.IO.Ports;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+public class ArduinoConnection : IDisposable
+{
+ public const int BaudRate = 115200;
+
+ // Must match what your sketch replies to "REQUEST_ID" (Form2.cs: EXPECTED_DEVICE_ID)
+ // public const string ExpectedDeviceId = "EARS_READER";
+ public const string ExpectedDeviceId = "N_EARS_ESP32C3";
+
+ private const int HeartbeatTimeoutMs = 10000;
+ private const int HeartbeatCheckMs = 2000;
+ private const int MaxReconnectAttempts = 5;
+ private const int ResetSettleMs = 1500; // board reboots when DTR asserts
+
+ private readonly object _gate = new object();
+ private SerialPort _port;
+ private Thread _reader;
+ private volatile bool _reading;
+
+ private DateTime _lastData = DateTime.MinValue;
+ private uint _heartbeatId;
+ private bool _autoReconnect;
+ private int _reconnectAttempts;
+ private DateTime _nextReconnect = DateTime.MinValue;
+
+ /// Raised on the GTK main thread for every complete line from the device.
+ public event Action LineReceived;
+ /// Raised on the GTK main thread with human-readable status text.
+ public event Action Log;
+ /// Raised on the GTK main thread when the link goes up or down.
+ public event Action ConnectionChanged;
+
+ public string PortName { get; private set; }
+
+ public bool IsOpen
+ {
+ get { lock (_gate) return _port != null && _port.IsOpen; }
+ }
+
+ public static string[] ListPorts()
+ {
+ return SerialPort.GetPortNames()
+ .Distinct()
+ .OrderBy(p => p, StringComparer.Ordinal)
+ .ToArray();
+ }
+
+ // ---------- public API ----------
+
+ public bool Connect(string portName)
+ {
+ if (string.IsNullOrEmpty(portName))
+ {
+ Emit(Log, "No port selected");
+ return false;
+ }
+
+ ClosePort(silent: true);
+
+ if (!OpenPort(portName))
+ return false;
+
+ _autoReconnect = false;
+ _reconnectAttempts = 0;
+ StartHeartbeat();
+
+ Emit(Log, $"Connected to {portName} @ {BaudRate}");
+ Emit(ConnectionChanged, true);
+
+ // Give the board time to finish booting before the first command.
+ Task.Delay(ResetSettleMs).ContinueWith(_ => Send("MODE3"));
+ return true;
+ }
+
+ public void Disconnect()
+ {
+ _autoReconnect = false;
+ StopHeartbeat();
+ ClosePort(silent: false);
+ }
+
+ public void Send(string command)
+ {
+ try
+ {
+ lock (_gate)
+ {
+ if (_port == null || !_port.IsOpen)
+ {
+ Emit(Log, $"Cannot send '{command}' — not connected");
+ return;
+ }
+ _port.Write(command + "\n");
+ }
+ Emit(Log, $"Sent: {command}");
+ }
+ catch (Exception ex)
+ {
+ Emit(Log, $"Send failed: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Probes every port for a device answering REQUEST_ID with the expected ID.
+ /// Returns the port name, or null. Runs off the UI thread; the caller
+ /// connects on the main thread so no GLib call happens from a worker.
+ ///
+ public Task DetectPortAsync()
+ {
+ return Task.Run(() =>
+ {
+ foreach (string p in ListPorts())
+ {
+ if (IsOpen && p == PortName) continue;
+
+ Emit(Log, $"Probing {p}...");
+ string id;
+ if (Probe(p, out id))
+ {
+ Emit(Log, $"Found {ExpectedDeviceId} on {p}");
+ return p;
+ }
+ if (id != null)
+ Emit(Log, $" {p}: reported '{id}' (not a match)");
+ }
+ return (string)null;
+ });
+ }
+
+ // ---------- port plumbing ----------
+
+ private bool OpenPort(string portName)
+ {
+ try
+ {
+ lock (_gate)
+ {
+ _port = new SerialPort(portName, BaudRate)
+ {
+ ReadTimeout = 500,
+ WriteTimeout = 1000,
+ NewLine = "\n",
+ DtrEnable = true,
+ RtsEnable = true,
+ };
+ _port.Open();
+ }
+
+ PortName = portName;
+ _lastData = DateTime.Now;
+ StartReader();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Emit(Log, $"Open failed on {portName}: {ex.Message}");
+ lock (_gate)
+ {
+ _port?.Dispose();
+ _port = null;
+ }
+ return false;
+ }
+ }
+
+ private void ClosePort(bool silent)
+ {
+ _reading = false;
+
+ Thread t = _reader;
+ _reader = null;
+ if (t != null && t.IsAlive && t != Thread.CurrentThread)
+ t.Join(1000);
+
+ lock (_gate)
+ {
+ try { if (_port != null && _port.IsOpen) _port.Close(); }
+ catch (Exception ex) { Emit(Log, $"Close error: {ex.Message}"); }
+ _port?.Dispose();
+ _port = null;
+ }
+
+ if (!silent)
+ {
+ Emit(Log, "Disconnected");
+ Emit(ConnectionChanged, false);
+ }
+ }
+
+ private void StartReader()
+ {
+ _reading = true;
+ _reader = new Thread(ReaderLoop)
+ {
+ IsBackground = true,
+ Name = "arduino-reader"
+ };
+ _reader.Start();
+ }
+
+ private void ReaderLoop()
+ {
+ var pending = new StringBuilder();
+ var chunk = new byte[4096];
+
+ while (_reading)
+ {
+ SerialPort p;
+ lock (_gate) p = _port;
+
+ if (p == null || !p.IsOpen) { Thread.Sleep(100); continue; }
+
+ try
+ {
+ int n = p.Read(chunk, 0, chunk.Length);
+ if (n <= 0) continue;
+
+ _lastData = DateTime.Now;
+ pending.Append(Encoding.ASCII.GetString(chunk, 0, n));
+
+ string buffered = pending.ToString();
+ int nl;
+ while ((nl = buffered.IndexOf('\n')) >= 0)
+ {
+ string line = buffered.Substring(0, nl).Trim();
+ buffered = buffered.Substring(nl + 1);
+ if (line.Length > 0)
+ Emit(LineReceived, line);
+ }
+ pending.Clear();
+ pending.Append(buffered);
+ }
+ catch (TimeoutException) { /* normal when idle */ }
+ catch (Exception ex)
+ {
+ if (_reading) Emit(Log, $"Read error: {ex.Message}");
+ Thread.Sleep(200);
+ }
+ }
+ }
+
+ private bool Probe(string portName, out string deviceId)
+ {
+ deviceId = null;
+ SerialPort test = null;
+ try
+ {
+ test = new SerialPort(portName, BaudRate)
+ {
+ ReadTimeout = 500,
+ WriteTimeout = 1000,
+ NewLine = "\n",
+ DtrEnable = true,
+ RtsEnable = true,
+ };
+ test.Open();
+ Thread.Sleep(ResetSettleMs);
+ test.DiscardInBuffer();
+ test.Write("REQUEST_ID\n");
+
+ DateTime deadline = DateTime.Now.AddSeconds(3);
+ while (DateTime.Now < deadline)
+ {
+ try
+ {
+ string line = test.ReadLine().Trim();
+ if (line.StartsWith("DEVICE_ID:"))
+ {
+ deviceId = line.Substring("DEVICE_ID:".Length).Trim();
+ return deviceId == ExpectedDeviceId;
+ }
+ }
+ catch (TimeoutException) { }
+ }
+ }
+ catch (Exception ex)
+ {
+ Emit(Log, $" {portName}: {ex.Message}");
+ }
+ finally
+ {
+ try { test?.Close(); } catch { }
+ test?.Dispose();
+ }
+ return false;
+ }
+
+ // ---------- heartbeat / auto-reconnect (runs on the GTK main loop) ----------
+
+ private void StartHeartbeat()
+ {
+ StopHeartbeat();
+ _heartbeatId = GLib.Timeout.Add(HeartbeatCheckMs, OnHeartbeatTick);
+ }
+
+ private void StopHeartbeat()
+ {
+ if (_heartbeatId != 0)
+ {
+ GLib.Source.Remove(_heartbeatId);
+ _heartbeatId = 0;
+ }
+ }
+
+ private bool OnHeartbeatTick()
+ {
+ if (_autoReconnect && !IsOpen)
+ {
+ if (_reconnectAttempts >= MaxReconnectAttempts)
+ {
+ _autoReconnect = false;
+ Log?.Invoke($"Reconnection failed after {MaxReconnectAttempts} attempts. Check USB cable, port and power, then reconnect manually.");
+ StopHeartbeat();
+ return false;
+ }
+
+ if (DateTime.Now >= _nextReconnect)
+ {
+ _reconnectAttempts++;
+ Log?.Invoke($"Reconnect attempt {_reconnectAttempts}/{MaxReconnectAttempts} on {PortName}...");
+
+ if (OpenPort(PortName))
+ {
+ _autoReconnect = false;
+ _reconnectAttempts = 0;
+ Log?.Invoke("Reconnected");
+ ConnectionChanged?.Invoke(true);
+ Task.Delay(ResetSettleMs).ContinueWith(_ => Send("MODE3"));
+ }
+ else
+ {
+ _nextReconnect = DateTime.Now.AddSeconds(2);
+ }
+ }
+ return true;
+ }
+
+ if (!IsOpen) return true;
+
+ if ((DateTime.Now - _lastData).TotalMilliseconds >= HeartbeatTimeoutMs)
+ {
+ Log?.Invoke("Heartbeat timeout — no device activity");
+ ClosePort(silent: true);
+ ConnectionChanged?.Invoke(false);
+ _autoReconnect = true;
+ _reconnectAttempts = 0;
+ _nextReconnect = DateTime.Now.AddSeconds(2);
+ }
+ return true;
+ }
+
+ // ---------- helpers ----------
+
+ private static void Emit(Action handler, T arg)
+ {
+ Action h = handler;
+ if (h == null) return;
+ Gtk.Application.Invoke((s, e) => h(arg));
+ }
+
+ public void Dispose()
+ {
+ _autoReconnect = false;
+ StopHeartbeat();
+ ClosePort(silent: true);
+ }
+}
\ No newline at end of file
diff --git a/program/Language.cs b/program/Language.cs
new file mode 100644
index 0000000..70695ae
--- /dev/null
+++ b/program/Language.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Gtk;
+
+public class Language
+{
+ public static bool LanguageSwitchValue = false;
+
+ private const string EngCssPath = "lang_eng.css";
+ private const string JpnCssPath = "lang_jpn.css";
+ private const string EngCsvPath = "info_eng.csv";
+ private const string JpnCsvPath = "info_jpn.csv";
+
+ private readonly Dictionary _engText;
+ private readonly Dictionary _jpnText;
+ private readonly Dictionary _buttonsById;
+ public readonly CssProvider _languageCssProvider = new CssProvider();
+ private readonly Label _languageLabel;
+
+ public Language(Dictionary buttonsById, Label languageLabel)
+ {
+ _buttonsById = buttonsById;
+ _languageLabel = languageLabel;
+ StyleContext.AddProviderForScreen(Gdk.Screen.Default, _languageCssProvider, 600);
+
+
+ _engText = LoadButtonText(EngCsvPath);
+ _jpnText = LoadButtonText(JpnCsvPath);
+ }
+
+ private Dictionary LoadButtonText(string path)
+ {
+ var result = new Dictionary();
+ if (!File.Exists(path))
+ {
+ Console.WriteLine($"Warning: text file not found: {path}");
+ return result;
+ }
+
+ var lines = File.ReadAllLines(path);
+ for (int i = 1; i < lines.Length; i++)
+ {
+ var line = lines[i].Trim();
+ if (string.IsNullOrEmpty(line)) continue;
+ var parts = line.Split(new[] { ',' }, 2);
+ if (parts.Length < 2) continue;
+ result[parts[0]] = parts[1];
+ }
+ return result;
+ }
+
+ public void ApplyLanguage(bool isEnglish)
+ {
+ var text = isEnglish ? _engText : _jpnText;
+ string cssPath = isEnglish ? EngCssPath : JpnCssPath;
+
+ foreach (var kvp in _buttonsById)
+ if (text.TryGetValue(kvp.Key, out var buttonText))
+ kvp.Value.Label = buttonText;
+
+ if (File.Exists(cssPath))
+ _languageCssProvider.LoadFromPath(cssPath);
+ else
+ Console.WriteLine($"Warning: CSS file not found: {cssPath}");
+
+ _languageLabel.Text = isEnglish ? "あ" : "A";
+ }
+
+ //for others
+ public Language(Dictionary buttonsById)
+ {
+ _buttonsById = buttonsById;
+ StyleContext.AddProviderForScreen(Gdk.Screen.Default, _languageCssProvider, 600);
+
+
+ _engText = LoadButtonText(EngCsvPath);
+ _jpnText = LoadButtonText(JpnCsvPath);
+ }
+
+
+ public void ApplyLanguages(bool isEnglish)
+ {
+ var text = isEnglish ? _engText : _jpnText;
+ string cssPath = isEnglish ? EngCssPath : JpnCssPath;
+
+ foreach (var kvp in _buttonsById)
+ if (text.TryGetValue(kvp.Key, out var buttonText))
+ kvp.Value.Label = buttonText;
+
+ if (File.Exists(cssPath))
+ _languageCssProvider.LoadFromPath(cssPath);
+ else
+ Console.WriteLine($"Warning: CSS file not found: {cssPath}");
+
+ }
+}
\ No newline at end of file
diff --git a/program/MainWindow.cs b/program/MainWindow.cs
new file mode 100644
index 0000000..e87516b
--- /dev/null
+++ b/program/MainWindow.cs
@@ -0,0 +1,296 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using Gtk;
+using UI = Gtk.Builder.ObjectAttribute;
+using System.Runtime.InteropServices;
+
+public class MainWindow : Window
+{
+
+ // ---- stack + pages (new all-in-one glade) ----
+ [UI] private Notebook RootNoteBook = null;
+
+ private const int PageSplash = 0;
+ private const int PageSettings = 1;
+
+ // ---- splash page widgets ----
+ [UI] private Button Option1Button = null;
+ [UI] private Button Option2Button = null;
+ [UI] private Button Option3Button = null;
+ // [UI] private Switch LanguageSwitch = null;
+ [UI] private ToggleButton LanguageToggle = null;
+ [UI] private Label LanguageLabel = null;
+ [UI] private Button CloseButton = null;
+ [UI] private Button SettingButton = null;
+ [UI] private Image ConnectionIcon = null; // was GtkIconView in the old glade
+
+ private CssProvider _scaleCssProvider;
+ // private CssProvider _languageCssProvider;
+ private int _lastAppliedWidth = -1;
+
+ private const int BaseWidth = 1920;
+ private const int BaseHeight = 1080;
+
+ // private const string EngCssPath = "lang_eng.css";
+ // private const string JpnCssPath = "lang_jpn.css";
+
+ // private Dictionary _engText;
+ // private Dictionary _jpnText;
+ private Dictionary _buttonsById;
+
+ private readonly ArduinoConnection _arduino = new ArduinoConnection();
+ internal SettingsWindow _settingsWindow;
+ internal Language _Language;
+
+ public MainWindow() : this(CreateBuilder()) { }
+
+ private static Builder CreateBuilder()
+ {
+ var builder = new Builder();
+ builder.AddFromFile("homepageall.glade");
+ return builder;
+ }
+
+ private MainWindow(Builder builder) : base(builder.GetObject("Root").Handle)
+ {
+ builder.Autoconnect(this);
+
+ _scaleCssProvider = new CssProvider();
+ // _languageCssProvider = new CssProvider();
+
+ StyleContext.AddProviderForScreen(Gdk.Screen.Default, _scaleCssProvider, 600);
+ // StyleContext.AddProviderForScreen(Gdk.Screen.Default, _Language._languageCssProvider, 600);
+
+ // make it full screen
+ this.Fullscreen();
+
+
+ // RootStack.TransitionType = StackTransitionType.None;
+ // RootStack.TransitionType = StackTransitionType.SlideLeftRight;
+ // RootStack.TransitionDuration = 150;
+
+ Option1Button.Clicked += OnNormalSessionClicked;
+ Option2Button.Clicked += OnPositionRecordingClicked;
+ Option3Button.Clicked += OnExamModeClicked;
+ CloseButton.Clicked += OnCloseClicked;
+ SettingButton.Clicked += OnSettingClicked;
+ // LanguageSwitch.AddNotification("active", OnLanguageSwitchNotify);
+ LanguageToggle.Toggled += OnLanguageToggled;
+
+ DeleteEvent += OnDeleteEvent;
+ SizeAllocated += OnWindowSizeAllocated;
+
+ _arduino.ConnectionChanged += OnArduinoConnectionChanged;
+
+ _buttonsById = new Dictionary
+ {
+ { "Option1Button", Option1Button },
+ { "Option2Button", Option2Button },
+ { "Option3Button", Option3Button },
+ { "CloseButton", CloseButton },
+ { "SettingButton", SettingButton },
+ };
+
+ // CSS ID selectors (#Option1Button etc.) need the widget Name set explicitly
+ foreach (var kvp in _buttonsById)
+ kvp.Value.Name = kvp.Key;
+
+ // _engText = LoadButtonText("info_eng.csv");
+ // _jpnText = LoadButtonText("info_jpn.csv");
+
+ _Language = new Language(_buttonsById,LanguageLabel);
+
+ // _Language.ApplyLanguage(LanguageSwitch.Active);
+
+ _settingsWindow = new SettingsWindow(builder, _arduino);
+ _settingsWindow.BackRequested += OnSettingsClosed;
+
+ // single source of truth: the toggle drives both pages
+ ApplyLanguageEverywhere(LanguageToggle.Active);
+
+ RootNoteBook.ShowTabs = false;
+ RootNoteBook.ShowBorder = false;
+ RootNoteBook.CurrentPage = PageSplash;
+
+ OnArduinoConnectionChanged(false);
+ }
+
+ // ---------- settings panel ----------
+
+ private void OnSettingClicked(object sender, EventArgs e)
+ {
+ // if (_settingsWindow == null)
+ // {
+ // // _settingsWindow = new SettingsWindow(_arduino);
+ // // _settingsWindow.BackRequested += OnSettingsClosed;
+ // // _settingsWindow.ShowAll(); // build it fullscreen once, up front
+ // // _settingsWindow.Fullscreen();
+ // RootNoteBook.CurrentPage = PageSettings;
+ // }
+
+
+ // _settingsWindow.ShowAll();
+ // _settingsWindow.Present();
+ // this.Hide();
+ RootNoteBook.CurrentPage = PageSettings;
+ }
+
+ private void OnSettingsClosed(object sender, EventArgs e)
+ {
+ // this.ShowAll();
+ // this.Present();
+ // _settingsWindow.Hide();
+ RootNoteBook.CurrentPage = PageSplash;
+ }
+
+ private void OnArduinoConnectionChanged(bool connected)
+ {
+ if (ConnectionIcon == null) return;
+
+ ConnectionIcon.SetFromIconName(
+ connected ? "network-transmit-receive" : "network-offline",
+ IconSize.Dnd);
+
+ ConnectionIcon.TooltipText = connected
+ ? $"Connected to {_arduino.PortName}"
+ : "Device not connected";
+ }
+
+ // ---------- localisation ----------
+
+ private Dictionary LoadButtonText(string path)
+ {
+ var result = new Dictionary();
+
+ if (!File.Exists(path))
+ {
+ Console.WriteLine($"Warning: text file not found: {path}");
+ return result;
+ }
+
+ var lines = File.ReadAllLines(path);
+ for (int i = 1; i < lines.Length; i++) // skip header row
+ {
+ var line = lines[i].Trim();
+ if (string.IsNullOrEmpty(line)) continue;
+
+ var parts = line.Split(new[] { ',' }, 2);
+ if (parts.Length < 2) continue;
+
+ result[parts[0]] = parts[1];
+ }
+
+ return result;
+ }
+
+ // private void ApplyLanguage(bool isEnglish)
+ // {
+ // var text = isEnglish ? _engText : _jpnText;
+ // string cssPath = isEnglish ? EngCssPath : JpnCssPath;
+
+ // foreach (var kvp in _buttonsById)
+ // {
+ // if (text.TryGetValue(kvp.Key, out var buttonText))
+ // kvp.Value.Label = buttonText;
+ // }
+
+ // if (File.Exists(cssPath))
+ // _languageCssProvider.LoadFromPath(cssPath);
+ // else
+ // Console.WriteLine($"Warning: CSS file not found: {cssPath}");
+
+ // LanguageLabel.Text = isEnglish ? "あ" : "A";
+ // }
+
+ // ---------- scaling ----------
+
+ private void OnWindowSizeAllocated(object o, SizeAllocatedArgs args)
+ {
+ int width = args.Allocation.Width;
+ int height = args.Allocation.Height;
+
+ if (width == _lastAppliedWidth) return;
+ _lastAppliedWidth = width;
+
+ double scaleX = (double)width / BaseWidth;
+ double scaleY = (double)height / BaseHeight;
+
+ ApplyScale(Math.Min(scaleX, scaleY));
+ }
+
+ private void ApplyScale(double scale)
+ {
+ int fontSize = Math.Max(10, (int)(20 * scale));
+ int buttonPadV = Math.Max(4, (int)(12 * scale));
+ int buttonPadH = Math.Max(8, (int)(24 * scale));
+ int switchWidth = Math.Max(30, (int)(40 * scale));
+ int switchHeight= Math.Max(18, (int)(24 * scale));
+
+ // NOTE: integers + InvariantCulture. Interpolating a double here emitted
+ // "40,5px" under ja_JP / de_DE locales and GTK silently dropped the rule.
+ string css = string.Format(CultureInfo.InvariantCulture, @"
+ grid, label, button, switch {{
+ font-size: {0}px;
+ }}
+ button {{
+ padding: {1}px {2}px;
+ }}
+ switch {{
+ min-width: {3}px;
+ min-height: {4}px;
+ }}
+ ", fontSize, buttonPadV, buttonPadH, switchWidth, switchHeight);
+
+ _scaleCssProvider.LoadFromData(css);
+ }
+
+ // ---------- lifecycle ----------
+
+ private void OnDeleteEvent(object sender, DeleteEventArgs a)
+ {
+ Shutdown();
+ a.RetVal = true;
+ }
+
+ private void OnCloseClicked(object sender, EventArgs e)
+ {
+ Shutdown();
+ }
+
+ private void Shutdown()
+ {
+ _arduino.Disconnect();
+ _arduino.Dispose();
+ // _settingsWindow?.Destroy();
+ Application.Quit();
+ }
+
+ private void OnNormalSessionClicked(object sender, EventArgs e)
+ {
+ Console.WriteLine("Normal Session selected");
+ }
+
+ private void OnPositionRecordingClicked(object sender, EventArgs e)
+ {
+ Console.WriteLine("Position Recording Mode selected");
+ }
+
+ private void OnExamModeClicked(object sender, EventArgs e)
+ {
+ Console.WriteLine("Exam Mode selected");
+ }
+
+ private void OnLanguageToggled(object sender, EventArgs e)
+ {
+ ApplyLanguageEverywhere(LanguageToggle.Active);
+ }
+
+ private void ApplyLanguageEverywhere(bool isEnglish)
+ {
+ Language.LanguageSwitchValue = isEnglish;
+ _Language.ApplyLanguage(isEnglish);
+ _settingsWindow?._Language?.ApplyLanguages(isEnglish);
+ }
+}
\ No newline at end of file
diff --git a/program/SettingWindow.cs b/program/SettingWindow.cs
new file mode 100644
index 0000000..1fed549
--- /dev/null
+++ b/program/SettingWindow.cs
@@ -0,0 +1,200 @@
+using System;
+using Gtk;
+using System.Collections.Generic;
+using UI = Gtk.Builder.ObjectAttribute;
+
+public class SettingsWindow
+{
+ [UI] private Label ComPortLabel = null;
+ [UI] private ComboBoxText ComPortComboBox = null;
+ [UI] private Button ConnectButton = null;
+ [UI] private Button AutoConnectButton = null;
+ [UI] private Button RefreshButton = null;
+ [UI] private Button BackButton = null;
+ [UI] private Label StatusLabel = null;
+ [UI] private Label LogLabel = null;
+ [UI] private TextView LogtextBox = null;
+
+ private readonly Dictionary _buttonsById;
+ private readonly Dictionary _labelsById;
+ internal Language _Language;
+
+ private const int MaxLogLines = 500;
+
+ private readonly ArduinoConnection _arduino;
+ private string[] _ports = new string[0];
+
+ /// Raised after the window hides itself, so the caller can re-show the main window.
+ public event EventHandler BackRequested;
+
+
+ public SettingsWindow(Builder builder, ArduinoConnection arduino)
+ {
+ builder.Autoconnect(this);
+ _arduino = arduino;
+
+ // Title = "Settings";
+ ComPortLabel.Text = "COM Port";
+ LogLabel.Text = "Log";
+ LogtextBox.Editable = false;
+ LogtextBox.WrapMode = WrapMode.WordChar;
+
+ RefreshButton.Clicked += (s, e) => RefreshPorts();
+ ConnectButton.Clicked += OnConnectClicked;
+ AutoConnectButton.Clicked += OnAutoConnectClicked;
+ BackButton.Clicked += OnBackClicked;
+
+ // DeleteEvent += (o, a) => { a.RetVal = true; OnBackClicked(o, EventArgs.Empty); };
+
+ _arduino.Log += AppendLog;
+ _arduino.LineReceived += OnLineReceived;
+ _arduino.ConnectionChanged += OnConnectionChanged;
+
+ _buttonsById = new Dictionary
+ {
+ { "ConnectButton", ConnectButton },
+ { "AutoConnectButton", AutoConnectButton },
+ { "RefreshButton", RefreshButton },
+ { "BackButton", BackButton },
+ };
+
+ // CSS ID selectors (#Option1Button etc.) need the widget Name set explicitly
+ foreach (var kvp in _buttonsById)
+ kvp.Value.Name = kvp.Key;
+
+ _Language = new Language(_buttonsById);
+ // _Language.ApplyLanguages(Language.LanguageSwitchValue);
+
+ RefreshPorts();
+ OnConnectionChanged(_arduino.IsOpen);
+ }
+
+ // ---------- UI actions ----------
+
+ private void RefreshPorts()
+ {
+ ComPortComboBox.RemoveAll();
+ _ports = ArduinoConnection.ListPorts();
+
+ foreach (string p in _ports)
+ ComPortComboBox.AppendText(p);
+
+ if (_ports.Length > 0)
+ {
+ ComPortComboBox.Active = 0;
+ AppendLog($"{_ports.Length} port(s) found");
+ }
+ else
+ {
+ AppendLog("No serial ports found");
+ }
+ }
+
+ private void OnConnectClicked(object sender, EventArgs e)
+ {
+ if (_arduino.IsOpen)
+ {
+ _arduino.Disconnect();
+ return;
+ }
+
+ string port = ComPortComboBox.ActiveText;
+ if (string.IsNullOrEmpty(port))
+ {
+ AppendLog("Select a COM port first");
+ return;
+ }
+ _arduino.Connect(port);
+ }
+
+ private async void OnAutoConnectClicked(object sender, EventArgs e)
+ {
+ AutoConnectButton.Sensitive = false;
+ ConnectButton.Sensitive = false;
+ AppendLog("Scanning ports for device...");
+
+ string found = await _arduino.DetectPortAsync();
+
+ if (found != null)
+ {
+ RefreshPorts();
+ SelectPort(found);
+ _arduino.Connect(found);
+ }
+ else
+ {
+ AppendLog("Device not found — use manual connection");
+ }
+
+ AutoConnectButton.Sensitive = true;
+ ConnectButton.Sensitive = true;
+ }
+
+ private void OnBackClicked(object sender, EventArgs e)
+ {
+ // Hide();
+ BackRequested?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void SelectPort(string portName)
+ {
+ for (int i = 0; i < _ports.Length; i++)
+ {
+ if (_ports[i] == portName)
+ {
+ ComPortComboBox.Active = i;
+ return;
+ }
+ }
+ }
+
+ // ---------- device events ----------
+
+ private void OnConnectionChanged(bool connected)
+ {
+ ConnectButton.Label = connected ? "Disconnect" : "Connect";
+ StatusLabel.Text = connected
+ ? $"Connected ({_arduino.PortName})"
+ : "Disconnected";
+
+ ComPortComboBox.Sensitive = !connected;
+ RefreshButton.Sensitive = !connected;
+
+ StyleContext ctx = StatusLabel.StyleContext;
+ ctx.RemoveClass(connected ? "status-off" : "status-on");
+ ctx.AddClass(connected ? "status-on" : "status-off");
+ }
+
+ private void OnLineReceived(string line)
+ {
+ // Same filtering Form2.cs applied before parsing.
+ if (line.StartsWith("Send Status:")) return;
+ if (line.StartsWith("\u2713") || line.StartsWith("\u2717")) return;
+
+ if (line.Contains("ESPNOW_CONNECTED")) { AppendLog("[ESP-NOW] Peer connected"); return; }
+ if (line.Contains("ESPNOW_DISCONNECTED")) { AppendLog("[ESP-NOW] Peer disconnected"); return; }
+
+ AppendLog(line);
+ }
+
+ // ---------- log ----------
+
+ public void AppendLog(string message)
+ {
+ TextBuffer buf = LogtextBox.Buffer;
+
+ TextIter end = buf.EndIter;
+ buf.Insert(ref end, $"[{DateTime.Now:HH:mm:ss.fff}] {message}\n");
+
+ if (buf.LineCount > MaxLogLines)
+ {
+ TextIter start = buf.StartIter;
+ TextIter cut = buf.GetIterAtLine(buf.LineCount - MaxLogLines);
+ buf.Delete(ref start, ref cut);
+ }
+
+ TextMark mark = buf.CreateMark(null, buf.EndIter, false);
+ LogtextBox.ScrollToMark(mark, 0, false, 0, 0);
+ buf.DeleteMark(mark);
+ }
+}
\ No newline at end of file
diff --git a/program/homepage.glade b/program/homepage.glade
new file mode 100644
index 0000000..7c21a1d
--- /dev/null
+++ b/program/homepage.glade
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+ False
+ 1280
+ 800
+
+
+
+ True
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ False
+ EARS
+ 1280
+ 800
+
+
+
+ True
+ False
+ 3
+ 5
+
+
+ Normal Session
+ True
+ True
+ True
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 1
+
+
+
+
+ Position Recording Mode
+ True
+ True
+ True
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 2
+
+
+
+
+ Exam Mode
+ True
+ True
+ True
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 3
+
+
+
+
+ True
+ True
+ True
+
+
+ 3
+ 0
+
+
+
+
+ True
+ False
+ Language
+
+
+ 2
+ 0
+
+
+
+
+ Close
+ True
+ True
+ True
+ True
+
+
+ 4
+ 4
+
+
+
+
+ Settings
+ True
+ True
+ True
+
+
+ 4
+ 3
+
+
+
+
+ True
+ False
+ network-offline
+ 6
+
+
+ 4
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/program/homepage.glade~ b/program/homepage.glade~
new file mode 100644
index 0000000..fade189
--- /dev/null
+++ b/program/homepage.glade~
@@ -0,0 +1,347 @@
+
+
+
+
+
+ False
+ 1280
+ 800
+
+
+
+ True
+ False
+ 3
+ 3
+ True
+
+
+ True
+ False
+ center
+ True
+ True
+ COM Port
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ center
+ True
+ True
+
+
+ 1
+ 0
+
+
+
+
+ Connect
+ True
+ True
+ True
+ start
+ center
+ True
+ True
+
+
+ 2
+ 0
+
+
+
+
+ Auto Connect
+ True
+ True
+ True
+ center
+ start
+ True
+ True
+
+
+ 1
+ 1
+
+
+
+
+ Back
+ True
+ True
+ True
+ end
+ end
+ True
+ True
+
+
+ 2
+ 2
+
+
+
+
+ True
+ False
+ Log
+
+
+ 0
+ 2
+
+
+
+
+ True
+ True
+ True
+ True
+ in
+
+
+ True
+ True
+ False
+ True
+
+
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ vertical
+
+
+ Refresh Ports
+ True
+ True
+ True
+ center
+ start
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Disconnected
+
+
+ False
+ True
+ 1
+
+
+
+
+ 2
+ 1
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+
+ False
+ EARS
+ 1280
+ 800
+
+
+
+ True
+ False
+ 3
+ 5
+
+
+ Normal Session
+ True
+ True
+ True
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 1
+
+
+
+
+ Position Recording Mode
+ True
+ True
+ True
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 2
+
+
+
+
+ Exam Mode
+ True
+ True
+ True
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 3
+
+
+
+
+ True
+ True
+ True
+
+
+ 3
+ 0
+
+
+
+
+ True
+ False
+ Language
+
+
+ 2
+ 0
+
+
+
+
+ Close
+ True
+ True
+ True
+ True
+
+
+ 4
+ 4
+
+
+
+
+ Settings
+ True
+ True
+ True
+
+
+ 4
+ 3
+
+
+
+
+ True
+ False
+ network-offline
+ 6
+
+
+ 4
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/program/homepageall.glade b/program/homepageall.glade
new file mode 100644
index 0000000..07009b2
--- /dev/null
+++ b/program/homepageall.glade
@@ -0,0 +1,355 @@
+
+
+
+
+
+ False
+
+
+ True
+ True
+ False
+ False
+
+
+
+ True
+ False
+ 3
+ 5
+
+
+ Normal Session
+ True
+ True
+ True
+ 50
+ 50
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 1
+
+
+
+
+ Position Recording Mode
+ True
+ True
+ True
+ 50
+ 50
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 2
+
+
+
+
+ Exam Mode
+ True
+ True
+ True
+ 50
+ 50
+ 50
+ 50
+ True
+ True
+
+
+ 1
+ 3
+
+
+
+
+ A / あ
+ True
+ True
+ True
+ True
+
+
+ 3
+ 0
+
+
+
+
+ True
+ False
+ Language
+
+
+ 2
+ 0
+
+
+
+
+ Close
+ True
+ True
+ True
+ True
+
+
+ 4
+ 4
+
+
+
+
+ Settings
+ True
+ True
+ True
+
+
+ 4
+ 3
+
+
+
+
+ True
+ False
+ network-offline
+ 6
+
+
+ 4
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ False
+ 3
+ 3
+ True
+
+
+ True
+ False
+ center
+ True
+ True
+ COM Port
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ center
+ True
+ True
+
+
+ 1
+ 0
+
+
+
+
+ Connect
+ True
+ True
+ True
+ start
+ center
+ True
+ True
+
+
+ 2
+ 0
+
+
+
+
+ Auto Connect
+ True
+ True
+ True
+ center
+ start
+ True
+ True
+
+
+ 1
+ 1
+
+
+
+
+ Back
+ True
+ True
+ True
+ end
+ end
+ True
+ True
+
+
+ 2
+ 2
+
+
+
+
+ True
+ False
+ Log
+
+
+ 0
+ 2
+
+
+
+
+ True
+ True
+ True
+ True
+ in
+
+
+ True
+ True
+ False
+ True
+
+
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ vertical
+
+
+ Refresh Ports
+ True
+ True
+ True
+ center
+ start
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Disconnected
+
+
+ False
+ True
+ 1
+
+
+
+
+ 2
+ 1
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
diff --git a/program/info_eng.csv b/program/info_eng.csv
new file mode 100644
index 0000000..3a0481a
--- /dev/null
+++ b/program/info_eng.csv
@@ -0,0 +1,11 @@
+ButtonId,Text
+Option1Button,Normal Session
+Option2Button,Position Recording Mode
+Option3Button,Exam Mode
+CloseButton,Close
+SettingButton,Settings
+ConnectButton,Connect
+AutoConnectButton,Auto Connect
+RefreshButton, Refresh Port
+LogLabel,Log
+BackButton,Back
diff --git a/program/info_jpn.csv b/program/info_jpn.csv
new file mode 100644
index 0000000..1d4e6e8
--- /dev/null
+++ b/program/info_jpn.csv
@@ -0,0 +1,11 @@
+ButtonId,Text
+Option1Button,通常セッション
+Option2Button,位置記録モード
+Option3Button,試験モード
+CloseButton,閉じる
+SettingButton,設定
+ConnectButton,接続
+AutoConnectButton,自動接続
+RefreshButton, ポートの更新
+LogLabel,情報
+BackButton,戻る
diff --git a/program/lang_eng.css b/program/lang_eng.css
new file mode 100644
index 0000000..fcf83ec
--- /dev/null
+++ b/program/lang_eng.css
@@ -0,0 +1,69 @@
+#Option1Button {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option2Button {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option3Button {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#CloseButton {
+ font-family: 'Sans';
+ font-size: 18px;
+ font-weight: bold;
+ font-style: normal;
+}
+
+#StatusLabel.status-on { color: #2e7d32; font-weight: bold; }
+#StatusLabel.status-off { color: #c62828; font-weight: bold; }
+
+/* Setting Window
+#ConnectButton {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#AutoConnectButton {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option3Button {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#CloseButton {
+ font-family: 'Sans';
+ font-size: 18px;
+ font-weight: bold;
+ font-style: normal;
+} */
+
+/* [UI] private Label ComPortLabel = null;
+ [UI] private ComboBoxText ComPortComboBox = null;
+ [UI] private Button ConnectButton = null;
+ [UI] private Button AutoConnectButton = null;
+ [UI] private Button RefreshButton = null;
+ [UI] private Button BackButton = null;
+ [UI] private Label StatusLabel = null;
+ [UI] private Label LogLabel = null;
+ [UI] private TextView LogtextBox = null; */
\ No newline at end of file
diff --git a/program/lang_jpn.css b/program/lang_jpn.css
new file mode 100644
index 0000000..13b87e2
--- /dev/null
+++ b/program/lang_jpn.css
@@ -0,0 +1,30 @@
+#Option1Button {
+ font-family: 'Noto Sans JP';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option2Button {
+ font-family: 'Noto Sans JP';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option3Button {
+ font-family: 'Noto Sans JP';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#CloseButton {
+ font-family: 'Noto Sans JP';
+ font-size: 18px;
+ font-weight: bold;
+ font-style: normal;
+}
+
+#StatusLabel.status-on { color: #2e7d32; font-weight: bold; }
+#StatusLabel.status-off { color: #c62828; font-weight: bold; }
\ No newline at end of file
diff --git a/program/program.cs b/program/program.cs
new file mode 100644
index 0000000..f9db539
--- /dev/null
+++ b/program/program.cs
@@ -0,0 +1,16 @@
+using System;
+using Gtk;
+
+class Program
+{
+ [STAThread]
+ static void Main(string[] args)
+ {
+ Application.Init();
+
+ var app = new MainWindow();
+ app.ShowAll();
+
+ Application.Run();
+ }
+}
\ No newline at end of file
diff --git a/program/style.css b/program/style.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/program/style.css