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 + 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 + 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