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..8b89a70 --- /dev/null +++ b/program/Language.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using Gtk; + +public class Language +{ + // 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 static readonly string EngCssPath = Paths.Get("lang_eng.css"); + private static readonly string JpnCssPath = Paths.Get("lang_jpn.css"); + private static readonly string EngCsvPath = Paths.Get("info_eng.csv"); + private static readonly string JpnCsvPath = Paths.Get("info_jpn.csv"); + + private readonly Dictionary _engText; + private readonly Dictionary _jpnText; + private Dictionary _text; + + // widgets whose label is a plain CSV lookup + private readonly Dictionary _widgets = new Dictionary(); + + public readonly CssProvider _languageCssProvider = new CssProvider(); + + public bool IsEnglish { get; private set; } + + /// Raised after every language change, so callers can rebuild state-dependent text. + public event EventHandler Changed; + + public Language(bool isEnglish) + { + StyleContext.AddProviderForScreen(Gdk.Screen.Default, _languageCssProvider, 600); + + _engText = LoadText(EngCsvPath); + _jpnText = LoadText(JpnCsvPath); + + // set before any widget is built, so ctor-time lookups are already correct + IsEnglish = isEnglish; + _text = isEnglish ? _engText : _jpnText; + } + + // ---------- registration ---------- + + /// Label comes straight from the CSV key. Applied automatically on every change. + public void Register(string key, Widget widget) + { + if (widget == null) return; + widget.Name = key; // enables #Key selectors in the lang CSS + _widgets[key] = widget; + } + + /// Widget whose text the owner sets itself (state-dependent). Name only, no auto-apply. + public void RegisterDynamic(string key, Widget widget) + { + if (widget != null) widget.Name = key; + } + + // ---------- lookup ---------- + + public string this[string key] { get { return Get(key); } } + + public string Get(string key) + { + string value; + if (_text.TryGetValue(key, out value)) return value; + + Console.WriteLine("Warning: missing text key '" + key + "'"); + return key; + } + + public string Format(string key, params object[] args) + { + string template = Get(key); + try + { + return string.Format(CultureInfo.InvariantCulture, template, args); + } + catch (FormatException) + { + Console.WriteLine("Warning: bad placeholder in text key '" + key + "'"); + return template; + } + } + + // ---------- apply ---------- + + public void Apply(bool isEnglish) + { + IsEnglish = isEnglish; + _text = isEnglish ? _engText : _jpnText; + + foreach (var kvp in _widgets) + { + string value; + if (_text.TryGetValue(kvp.Key, out value)) + SetText(kvp.Value, value); + else + Console.WriteLine("Warning: missing text key '" + kvp.Key + "'"); + } + + string cssPath = isEnglish ? EngCssPath : JpnCssPath; + if (File.Exists(cssPath)) + _languageCssProvider.LoadFromPath(cssPath); + else + Console.WriteLine("Warning: CSS file not found: " + cssPath); + + EventHandler handler = Changed; + if (handler != null) handler(this, EventArgs.Empty); + } + + private static void SetText(Widget widget, string value) + { + Button button = widget as Button; // also covers ToggleButton + if (button != null) { button.Label = value; return; } + + Label label = widget as Label; + if (label != null) { label.Text = value; return; } + + Console.WriteLine("Warning: don't know how to set text on " + widget.GetType().Name); + } + + // ---------- csv ---------- + + private Dictionary LoadText(string path) + { + var result = new Dictionary(); + + if (!File.Exists(path)) + { + Console.WriteLine("Warning: text file not found: " + path); + return result; + } + + string[] lines = File.ReadAllLines(path); + for (int i = 1; i < lines.Length; i++) // row 0 is the header + { + string line = lines[i].Trim(); + if (string.IsNullOrEmpty(line) || line.StartsWith("#")) continue; + + string[] parts = line.Split(new[] { ',' }, 2); + if (parts.Length < 2) continue; + + result[parts[0].Trim()] = parts[1]; // value keeps any commas + } + + return result; + } +} \ No newline at end of file diff --git a/program/MainWindow.cs b/program/MainWindow.cs new file mode 100644 index 0000000..d2c513d --- /dev/null +++ b/program/MainWindow.cs @@ -0,0 +1,313 @@ +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 bool _connected; + + 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(Paths.Get("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); + + _Language = new Language(LanguageToggle.Active); + _Language.Register("Option1Button", Option1Button); + _Language.Register("Option2Button", Option2Button); + _Language.Register("Option3Button", Option3Button); + _Language.Register("CloseButton", CloseButton); + _Language.Register("SettingButton", SettingButton); + _Language.Register("LanguageLabel", LanguageLabel); + _Language.Register("LanguageToggle", LanguageToggle); + _Language.RegisterDynamic("ConnectionIcon", ConnectionIcon); + + _settingsWindow = new SettingsWindow(builder, _arduino, _Language); + _settingsWindow.BackRequested += OnSettingsClosed; + + _Language.Changed += (s, e) => OnArduinoConnectionChanged(_connected); + _Language.Apply(LanguageToggle.Active); + + // // 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) + { + _connected = connected; + if (ConnectionIcon == null) return; + + ConnectionIcon.SetFromIconName( + connected ? "network-transmit-receive" : "network-offline", + IconSize.Dnd); + + ConnectionIcon.TooltipText = connected + ? _Language.Format("ConnectionIcon.Connected", _arduino.PortName) + : _Language["ConnectionIcon.Disconnected"]; + } + + // ---------- 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) + { + _Language.Apply(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/Makefile b/program/Makefile new file mode 100644 index 0000000..f50b32c --- /dev/null +++ b/program/Makefile @@ -0,0 +1,459 @@ +# ===================================================================== config +APP := ears +VERSION := 1.0.0 +MAINTAINER := Kazi +SUMMARY := Auscultation training software +HOMEPAGE = https://nlab.tms.chiba-u.jp/ +YEAR := $(shell date +%Y) +DATESTAMP := $(shell date -R) + +# literal '#', so make never eats shebangs or comment lines as its own comments +H := \# + +PREFIX ?= /usr +DESTDIR ?= +LIBDIR := $(PREFIX)/lib/$(APP) +BINDIR := $(PREFIX)/bin + +MCS := mcs +PKGS := -pkg:gtk-sharp-3.0 +MCSFLAGS := -optimize+ -langversion:latest -warn:4 + +# ------------------------------------------------------------- source discovery +# Uses src/ and data/ when present, otherwise the project root. +SRCDIR ?= $(if $(wildcard src/*.cs),src,.) +DATADIR ?= $(if $(wildcard data/*.glade),data,.) + +# .cs files in SRCDIR that must NOT be compiled (old WinForms code, scratch files) +EXCLUDE ?= Form1.cs Form2.cs + +SRC := $(filter-out $(addprefix $(SRCDIR)/,$(EXCLUDE)),$(wildcard $(SRCDIR)/*.cs)) +DATA := $(wildcard $(DATADIR)/*.glade) $(wildcard $(DATADIR)/*.css) \ + $(wildcard $(DATADIR)/*.csv) $(wildcard $(DATADIR)/*.png) + +# ---------------------------------------------------------------- output layout +OUT := bin +LOUT := $(OUT)/linux +LINST := $(LOUT)/installer +LGEN := $(LINST)/gen +LROOT := $(LINST)/root +EXE := $(LOUT)/$(APP).exe +DEB := $(LINST)/$(APP)_$(VERSION)_all.deb + +WOUT := $(OUT)/windows +WBUNDLE := $(WOUT)/$(APP) +WINST := $(WOUT)/installer +WGEN := $(WINST)/gen +DBGEXE := $(WBUNDLE)/$(APP)-debug.exe +WINZIP := $(WOUT)/$(APP)-$(VERSION)-windows.zip +SETUP := $(WINST)/$(APP)-$(VERSION)-setup.exe + +# gtk-sharp managed assemblies + their dllmap .config files (for the Win bundle) +GTKSHARP := $(shell pkg-config --variable=Libraries gtk-sharp-3.0 2>/dev/null) +ifeq ($(strip $(GTKSHARP)),) +GTKSHARP := $(wildcard /usr/lib/cli/*/*.dll) +endif +GTKSHARP_CFG := $(wildcard $(addsuffix .config,$(GTKSHARP))) + +MSYS2 ?= /c/msys64 +MINGW := $(MSYS2)/mingw64 +ISCC ?= iscc + +.DEFAULT_GOAL := all +.PHONY: all build run debug clean distclean dep-check show +.PHONY: gen install uninstall deb deb-install deb-remove deb-test lint +.PHONY: win win-gtk installer everything + +all: deb +build: $(EXE) +everything: deb win + +$(LOUT) $(LGEN) $(LROOT) $(WBUNDLE) $(WGEN): + mkdir -p $@ + +# ================================================================== 1. compile +$(EXE): $(SRC) | $(LOUT) + @test -n "$(strip $(SRC))" || { \ + echo "ERROR: no .cs files found (SRCDIR=$(SRCDIR)). Run 'make show'."; exit 1; } + $(MCS) -target:winexe $(PKGS) $(MCSFLAGS) -out:$@ $(SRC) + +# console build: -target:exe keeps Console.WriteLine visible on Windows +$(DBGEXE): $(SRC) | $(WBUNDLE) + @test -n "$(strip $(SRC))" || { \ + echo "ERROR: no .cs files found (SRCDIR=$(SRCDIR)). Run 'make show'."; exit 1; } + $(MCS) -target:exe -debug -define:DEBUG $(PKGS) -out:$@ $(SRC) + +debug: $(DBGEXE) + +run: build + EARS_DATADIR=$(CURDIR)/$(DATADIR) mono $(EXE) + +clean: + rm -rf $(OUT) +distclean: clean + +show: + @echo "SRCDIR = $(SRCDIR)" + @echo "DATADIR = $(DATADIR)" + @echo "SRC = $(words $(SRC)) file(s)" + @$(foreach f,$(SRC),echo " $(f)";) + @echo "DATA = $(words $(DATA)) file(s)" + @$(foreach f,$(DATA),echo " $(f)";) + @echo "EXE = $(EXE)" + @echo "DEB = $(DEB)" + @echo "GTK# = $(words $(GTKSHARP)) managed assemblies" + +dep-check: + @command -v $(MCS) >/dev/null || { echo "missing: mono-devel (mcs)"; exit 1; } + @pkg-config --exists gtk-sharp-3.0 || { echo "missing: gtk-sharp3"; exit 1; } + @command -v dpkg-deb >/dev/null || { echo "missing: dpkg-dev"; exit 1; } + @echo "toolchain OK" + +# ========================================================== 2. generated files +define GEN_WRAPPER +$(H)!/bin/sh +$(H) $(APP) launcher - generated by make, do not edit +: "$${EARS_DATADIR:=$(LIBDIR)}" +export EARS_DATADIR +exec /usr/bin/mono "$(LIBDIR)/$(APP).exe" "$$@" +endef + +define GEN_DESKTOP +[Desktop Entry] +Type=Application +Name=EARS +GenericName=Serial control panel +Comment=$(SUMMARY) +Exec=$(APP) +Icon=$(APP) +Terminal=false +Categories=Utility;Electronics; +Keywords=serial;esp32;arduino; +StartupNotify=true +endef + +define GEN_ICON + + + + + + + + + +endef + +define GEN_UDEV +$(H) generated by make - serial adapters used by $(APP) +$(H) TAG+="uaccess" gives the device to the active desktop user with no group. +$(H) GROUP="dialout" is the fallback for ssh / headless / WSL sessions. +ACTION!="add", GOTO="ears_end" +SUBSYSTEM!="tty", GOTO="ears_end" +ATTRS{idVendor}=="303a", MODE="0660", GROUP="dialout", TAG+="uaccess" +ATTRS{idVendor}=="10c4", MODE="0660", GROUP="dialout", TAG+="uaccess" +ATTRS{idVendor}=="1a86", MODE="0660", GROUP="dialout", TAG+="uaccess" +ATTRS{idVendor}=="0403", MODE="0660", GROUP="dialout", TAG+="uaccess" +ATTRS{idVendor}=="2341", MODE="0660", GROUP="dialout", TAG+="uaccess" +LABEL="ears_end" +endef + +define GEN_CONTROL +Package: $(APP) +Version: $(VERSION) +Section: utils +Priority: optional +Architecture: all +Depends: mono-runtime (>= 6.0), gtk-sharp3, libgtk-3-0 | libgtk-3-0t64, libmono-system4.0-cil | mono-complete +Recommends: mono-complete +Installed-Size: @SIZE@ +Maintainer: $(MAINTAINER) +Homepage: $(HOMEPAGE) +Description: $(SUMMARY) + GTK$(H)3 front end that talks to an ESP32 over a serial port, with + English/Japanese language switching and a live device log. + . + Architecture is "all" because the payload is architecture-neutral CIL + executed by the Mono runtime. +endef + +define GEN_POSTINST +$(H)!/bin/sh +set -e +case "$$1" in +configure) + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules >/dev/null 2>&1 || true + udevadm trigger --subsystem-match=tty >/dev/null 2>&1 || true + fi + + if [ -n "$$SUDO_USER" ] && getent group dialout >/dev/null 2>&1; then + if ! id -nG "$$SUDO_USER" 2>/dev/null | tr ' ' '\n' | grep -qx dialout; then + usermod -aG dialout "$$SUDO_USER" >/dev/null 2>&1 || true + echo "$(APP): added '$$SUDO_USER' to group 'dialout'." + echo "$(APP): log out and back in for serial access to take effect." + fi + fi + + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true + fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -qtf /usr/share/icons/hicolor || true + fi + ;; +esac +exit 0 +endef + +define GEN_POSTRM +$(H)!/bin/sh +set -e +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -qtf /usr/share/icons/hicolor || true +fi +exit 0 +endef + +define GEN_CHANGELOG +$(APP) ($(VERSION)) stable; urgency=medium + + * Package generated by make. + + -- $(MAINTAINER) $(DATESTAMP) +endef + +define GEN_COPYRIGHT +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: $(APP) + +Files: * +Copyright: $(YEAR) $(MAINTAINER) +License: proprietary + All rights reserved. +endef + +gen: | $(LGEN) + @$(file >$(LGEN)/$(APP).sh,$(GEN_WRAPPER)) + @$(file >$(LGEN)/$(APP).desktop,$(GEN_DESKTOP)) + @$(file >$(LGEN)/$(APP).svg,$(GEN_ICON)) + @$(file >$(LGEN)/60-$(APP)-serial.rules,$(GEN_UDEV)) + @$(file >$(LGEN)/control,$(GEN_CONTROL)) + @$(file >$(LGEN)/postinst,$(GEN_POSTINST)) + @$(file >$(LGEN)/postrm,$(GEN_POSTRM)) + @$(file >$(LGEN)/changelog,$(GEN_CHANGELOG)) + @$(file >$(LGEN)/copyright,$(GEN_COPYRIGHT)) + @chmod 755 $(LGEN)/postinst $(LGEN)/postrm $(LGEN)/$(APP).sh + @echo "generated packaging sources in $(LGEN)" + +# =========================================================== 3. make install +install: build gen + install -d $(DESTDIR)$(LIBDIR) $(DESTDIR)$(BINDIR) + install -m 644 $(EXE) $(DESTDIR)$(LIBDIR)/$(APP).exe + @if [ -n "$(strip $(DATA))" ]; then \ + echo "install -m 644 <$(words $(DATA)) data files> $(DESTDIR)$(LIBDIR)/"; \ + install -m 644 $(DATA) $(DESTDIR)$(LIBDIR)/; \ + else \ + echo "WARNING: no data files found in '$(DATADIR)'"; \ + fi + install -m 755 $(LGEN)/$(APP).sh $(DESTDIR)$(BINDIR)/$(APP) + install -D -m 644 $(LGEN)/$(APP).desktop \ + $(DESTDIR)$(PREFIX)/share/applications/$(APP).desktop + install -D -m 644 $(LGEN)/$(APP).svg \ + $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/$(APP).svg + install -D -m 644 $(LGEN)/60-$(APP)-serial.rules \ + $(DESTDIR)/lib/udev/rules.d/60-$(APP)-serial.rules + install -D -m 644 $(LGEN)/copyright \ + $(DESTDIR)$(PREFIX)/share/doc/$(APP)/copyright + gzip -9nc $(LGEN)/changelog > $(LGEN)/changelog.gz + install -D -m 644 $(LGEN)/changelog.gz \ + $(DESTDIR)$(PREFIX)/share/doc/$(APP)/changelog.Debian.gz + +uninstall: + rm -rf $(DESTDIR)$(LIBDIR) $(DESTDIR)$(PREFIX)/share/doc/$(APP) + rm -f $(DESTDIR)$(BINDIR)/$(APP) \ + $(DESTDIR)$(PREFIX)/share/applications/$(APP).desktop \ + $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/$(APP).svg \ + $(DESTDIR)/lib/udev/rules.d/60-$(APP)-serial.rules + +# ==================================================================== 4. .deb +deb: build gen + rm -rf $(LROOT) + $(MAKE) --no-print-directory install DESTDIR=$(CURDIR)/$(LROOT) PREFIX=/usr + install -d $(LROOT)/DEBIAN + install -m 755 $(LGEN)/postinst $(LGEN)/postrm $(LROOT)/DEBIAN/ + sed "s|@SIZE@|$$(du -ks --exclude=DEBIAN $(LROOT) | cut -f1)|" \ + $(LGEN)/control > $(LROOT)/DEBIAN/control + cd $(LROOT) && find . -type f ! -path './DEBIAN/*' -printf '%P\0' \ + | LC_ALL=C sort -z | xargs -0 md5sum > DEBIAN/md5sums + find $(LROOT) -type d -exec chmod 755 {} + + dpkg-deb --build --root-owner-group $(LROOT) $(DEB) + @echo + @echo "==> $(DEB)" + @echo " sudo apt install ./$(DEB) # the ./ is required" + +deb-install: deb + sudo apt install -y ./$(DEB) + +deb-remove: + sudo apt remove -y $(APP) + +lint: deb + -lintian --no-tag-display-limit $(DEB) + +deb-test: deb + docker run --rm -v "$(CURDIR)/$(LINST)":/pkg:ro ubuntu:24.04 sh -c '\ + set -e; \ + apt-get update -qq; \ + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + /pkg/$(notdir $(DEB)) xvfb >/dev/null; \ + echo "--- files:"; dpkg -L $(APP) | head -20; \ + echo "--- runtime:"; mono --version | head -1; \ + echo "--- smoke test:"; \ + timeout 15 xvfb-run -a $(APP) || echo "exit=$$? (124 = window stayed open, OK)"' + +# ================================================================= 5. windows +define GEN_WINBAT +@echo off +setlocal +set "HERE=%~dp0" +set "EARS_DATADIR=%HERE%" +if exist "%HERE%gtk\bin" set "PATH=%HERE%gtk\bin;%PATH%" +if exist "%HERE%gtk\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache" set "GDK_PIXBUF_MODULE_FILE=%HERE%gtk\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache" +if exist "%HERE%gtk\share\glib-2.0\schemas" set "GSETTINGS_SCHEMA_DIR=%HERE%gtk\share\glib-2.0\schemas" +if exist "%HERE%gtk\share" set "XDG_DATA_DIRS=%HERE%gtk\share;%XDG_DATA_DIRS%" +set "MONO=mono" +where mono >nul 2>&1 || set "MONO=%ProgramFiles%\Mono\bin\mono.exe" +if /i not "%MONO%"=="mono" if not exist "%MONO%" ( + echo Mono for Windows was not found. + echo Install it from https://www.mono-project.com/download/stable/ + pause + exit /b 1 +) +start "" "%MONO%" "%HERE%$(APP).exe" %* +endef + +define GEN_WINBATDBG +@echo off +setlocal +set "HERE=%~dp0" +set "EARS_DATADIR=%HERE%" +if exist "%HERE%gtk\bin" set "PATH=%HERE%gtk\bin;%PATH%" +if exist "%HERE%gtk\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache" set "GDK_PIXBUF_MODULE_FILE=%HERE%gtk\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache" +if exist "%HERE%gtk\share\glib-2.0\schemas" set "GSETTINGS_SCHEMA_DIR=%HERE%gtk\share\glib-2.0\schemas" +if exist "%HERE%gtk\share" set "XDG_DATA_DIRS=%HERE%gtk\share;%XDG_DATA_DIRS%" +set "MONO=mono" +where mono >nul 2>&1 || set "MONO=%ProgramFiles%\Mono\bin\mono.exe" +set "MONO_LOG_LEVEL=warning" +echo Running $(APP) with console output. Close this window to quit. +"%MONO%" "%HERE%$(APP)-debug.exe" %* +echo. +echo exit code %ERRORLEVEL% +pause +endef + +win: build debug | $(WBUNDLE) + cp $(EXE) $(WBUNDLE)/$(APP).exe + @if [ -n "$(strip $(DATA))" ]; then cp $(DATA) $(WBUNDLE)/; \ + else echo "WARNING: no data files found in '$(DATADIR)'"; fi + @if [ -n "$(strip $(GTKSHARP))" ]; then \ + cp $(GTKSHARP) $(WBUNDLE)/; \ + echo "copied $(words $(GTKSHARP)) gtk-sharp assemblies"; \ + else echo "WARNING: gtk-sharp managed assemblies not found"; fi + @if [ -n "$(strip $(GTKSHARP_CFG))" ]; then cp $(GTKSHARP_CFG) $(WBUNDLE)/; fi + @$(file >$(WBUNDLE)/$(APP).bat,$(GEN_WINBAT)) + @$(file >$(WBUNDLE)/$(APP)-debug.bat,$(GEN_WINBATDBG)) + -$(MAKE) --no-print-directory win-gtk MSYS2=$(MSYS2) + cd $(WOUT) && rm -f $(notdir $(WINZIP)) && zip -qr $(notdir $(WINZIP)) $(APP) + @echo "==> $(WINZIP)" + +win-gtk: | $(WBUNDLE) + @test -d $(MINGW)/bin || { \ + echo "MSYS2 not found at $(MINGW) - shipping without native GTK."; \ + echo " pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-librsvg mingw-w64-x86_64-ntldd"; \ + echo " then: make win MSYS2=/path/to/msys64"; exit 1; } + rm -rf $(WBUNDLE)/gtk + mkdir -p $(WBUNDLE)/gtk/bin $(WBUNDLE)/gtk/share $(WBUNDLE)/gtk/lib + @echo "resolving native GTK dependencies..." + @if command -v ntldd >/dev/null 2>&1; then \ + for root in libgtk-3-0.dll librsvg-2-2.dll libgdk_pixbuf-2.0-0.dll; do \ + [ -f $(MINGW)/bin/$$root ] || continue; \ + ntldd -R $(MINGW)/bin/$$root \ + | grep -io '[a-z]:[\\/][^ ]*mingw64[\\/]bin[\\/][^ ]*\.dll' \ + | tr '\\' '/' | sort -u \ + | while read -r dll; do cp -u "$$dll" $(WBUNDLE)/gtk/bin/ 2>/dev/null || true; done; \ + cp -u $(MINGW)/bin/$$root $(WBUNDLE)/gtk/bin/; \ + done; \ + else \ + echo "ntldd missing - copying all of mingw64/bin (larger bundle)"; \ + cp -u $(MINGW)/bin/*.dll $(WBUNDLE)/gtk/bin/; \ + fi + -cp -r $(MINGW)/lib/gdk-pixbuf-2.0 $(WBUNDLE)/gtk/lib/ + -cp -r $(MINGW)/share/glib-2.0 $(WBUNDLE)/gtk/share/ + -cp -r $(MINGW)/share/icons $(WBUNDLE)/gtk/share/ + -cp -r $(MINGW)/share/themes $(WBUNDLE)/gtk/share/ + @echo "gtk runtime staged ($$(du -sh $(WBUNDLE)/gtk | cut -f1))" + +define GEN_ISS +[Setup] +AppName=$(APP) +AppVersion=$(VERSION) +AppPublisher=$(MAINTAINER) +DefaultDirName={autopf}\$(APP) +DefaultGroupName=$(APP) +OutputBaseFilename=$(APP)-$(VERSION)-setup +OutputDir=. +Compression=lzma2/max +SolidCompression=yes +ArchitecturesInstallIn64BitMode=x64compatible +PrivilegesRequired=admin +UninstallDisplayIcon={app}\$(APP).exe +WizardStyle=modern + +[Files] +Source: "..\..\$(APP)\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion + +[Icons] +Name: "{group}\$(APP)"; Filename: "{app}\$(APP).bat"; IconFilename: "{app}\$(APP).exe" +Name: "{group}\$(APP) (console)"; Filename: "{app}\$(APP)-debug.bat"; IconFilename: "{app}\$(APP).exe" +Name: "{autodesktop}\$(APP)"; Filename: "{app}\$(APP).bat"; IconFilename: "{app}\$(APP).exe"; Tasks: desktopicon + +[Tasks] +Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional icons:" + +[Run] +Filename: "{app}\$(APP).bat"; Description: "Launch $(APP)"; Flags: postinstall nowait skipifsilent shellexec + +[Code] +function MonoPresent(): Boolean; +begin + Result := FileExists(ExpandConstant('{commonpf}\Mono\bin\mono.exe')) or + FileExists(ExpandConstant('{commonpf32}\Mono\bin\mono.exe')) or + RegKeyExists(HKLM, 'SOFTWARE\Novell\Mono') or + RegKeyExists(HKLM, 'SOFTWARE\WOW6432Node\Novell\Mono'); +end; + +function InitializeSetup(): Boolean; +var Code: Integer; +begin + Result := True; + if not MonoPresent() then + if MsgBox('Mono for Windows was not detected. Open the download page now?', + mbConfirmation, MB_YESNO) = IDYES then + ShellExec('open', 'https://www.mono-project.com/download/stable/', + '', '', SW_SHOW, ewNoWait, Code); +end; +endef + +installer: win | $(WGEN) + @$(file >$(WGEN)/$(APP).iss,$(GEN_ISS)) + @command -v $(ISCC) >/dev/null 2>&1 || { \ + echo "Inno Setup compiler '$(ISCC)' not found."; \ + echo " Windows: make installer ISCC=\"/c/Program Files (x86)/Inno Setup 6/ISCC.exe\""; \ + echo " Linux: make installer ISCC=\"wine /path/to/ISCC.exe\""; exit 1; } + cd $(WGEN) && $(ISCC) $(APP).iss + mv $(WGEN)/$(APP)-$(VERSION)-setup.exe $(SETUP) + @echo "==> $(SETUP)" \ No newline at end of file diff --git a/program/Paths.cs b/program/Paths.cs new file mode 100644 index 0000000..16cf392 --- /dev/null +++ b/program/Paths.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; +using System.Reflection; + +public static class Paths +{ + static readonly string ExeDir = + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + public static readonly string DataDir = Resolve(); + + const string Probe = "homepageall.glade"; + + static string Resolve() + { + string[] candidates = { + Environment.GetEnvironmentVariable("EARS_DATADIR"), + ExeDir, + Path.Combine(ExeDir, "data"), + Path.Combine(ExeDir, "..", "data"), // make run from bin/linux + "/usr/lib/ears", + "/usr/local/lib/ears", + }; + + foreach (var c in candidates) + if (!string.IsNullOrEmpty(c) && File.Exists(Path.Combine(c, Probe))) + return Path.GetFullPath(c); + + return ExeDir; + } + + public static string Get(string name) => Path.Combine(DataDir, name); +} \ No newline at end of file diff --git a/program/SettingWindow.cs b/program/SettingWindow.cs new file mode 100644 index 0000000..f39a427 --- /dev/null +++ b/program/SettingWindow.cs @@ -0,0 +1,217 @@ +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 readonly Language _lang; + private bool _connected; + + 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, Language language) + { + builder.Autoconnect(this); + _arduino = arduino; + _lang = language; + + // 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; + + _lang.Register("ComPortLabel", ComPortLabel); + _lang.Register("LogLabel", LogLabel); + _lang.Register("AutoConnectButton", AutoConnectButton); + _lang.Register("RefreshButton", RefreshButton); + _lang.Register("BackButton", BackButton); + _lang.RegisterDynamic("ConnectButton", ConnectButton); // set in OnConnectionChanged + _lang.RegisterDynamic("StatusLabel", StatusLabel); + _lang.Changed += (s, e) => OnConnectionChanged(_connected); + + // _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(_lang.Format("Log.PortsFound", _ports.Length)); + } + else + { + AppendLog(_lang["Log.NoPorts"]); + } + } + + private void OnConnectClicked(object sender, EventArgs e) + { + if (_arduino.IsOpen) + { + _arduino.Disconnect(); + return; + } + + string port = ComPortComboBox.ActiveText; + if (string.IsNullOrEmpty(port)) + { + AppendLog(_lang["Log.SelectPort"]); + return; + } + _arduino.Connect(port); + } + + private async void OnAutoConnectClicked(object sender, EventArgs e) + { + AutoConnectButton.Sensitive = false; + ConnectButton.Sensitive = false; + AppendLog(_lang["Log.Scanning"]); + + string found = await _arduino.DetectPortAsync(); + + if (found != null) + { + RefreshPorts(); + SelectPort(found); + _arduino.Connect(found); + } + else + { + AppendLog(_lang["Log.DeviceNotFound"]); + } + + 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) + { + _connected = connected; + + ConnectButton.Label = connected + ? _lang["ConnectButton.Disconnect"] + : _lang["ConnectButton.Connect"]; + + StatusLabel.Text = connected + ? _lang.Format("StatusLabel.Connected", _arduino.PortName) + : _lang["StatusLabel.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(_lang["Log.EspNowConnected"]); return; } + if (line.Contains("ESPNOW_DISCONNECTED")) { AppendLog(_lang["Log.EspNowDisconnected"]); 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..a7bb3e8 --- /dev/null +++ b/program/info_eng.csv @@ -0,0 +1,26 @@ +key,text +Option1Button,Normal Session +Option2Button,Position Recording Mode +Option3Button,Exam Mode +SettingButton,Settings +CloseButton,Close +LanguageLabel,Language +LanguageToggle,あ +ComPortLabel,COM Port +LogLabel,Log +AutoConnectButton,Auto Connect +RefreshButton,Refresh Ports +BackButton,Back +ConnectButton.Connect,Connect +ConnectButton.Disconnect,Disconnect +StatusLabel.Connected,Connected ({0}) +StatusLabel.Disconnected,Disconnected +ConnectionIcon.Connected,Connected to {0} +ConnectionIcon.Disconnected,Device not connected +Log.PortsFound,{0} port(s) found +Log.NoPorts,No serial ports found +Log.SelectPort,Select a COM port first +Log.Scanning,Scanning ports for device... +Log.DeviceNotFound,Device not found — use manual connection +Log.EspNowConnected,[ESP-NOW] Peer connected +Log.EspNowDisconnected,[ESP-NOW] Peer disconnected \ No newline at end of file diff --git a/program/info_jpn.csv b/program/info_jpn.csv new file mode 100644 index 0000000..5cac763 --- /dev/null +++ b/program/info_jpn.csv @@ -0,0 +1,26 @@ +key,text +Option1Button,通常セッション +Option2Button,位置記録モード +Option3Button,試験モード +SettingButton,設定 +CloseButton,終了 +LanguageLabel,言語 +LanguageToggle,A +ComPortLabel,COMポート +LogLabel,ログ +AutoConnectButton,自動接続 +RefreshButton,ポート更新 +BackButton,戻る +ConnectButton.Connect,接続 +ConnectButton.Disconnect,切断 +StatusLabel.Connected,接続済み ({0}) +StatusLabel.Disconnected,未接続 +ConnectionIcon.Connected,{0} に接続しました +ConnectionIcon.Disconnected,デバイスが接続されていません +Log.PortsFound,ポートが {0} 個見つかりました +Log.NoPorts,シリアルポートが見つかりません +Log.SelectPort,COMポートを選択してください +Log.Scanning,デバイスを検索中... +Log.DeviceNotFound,デバイスが見つかりません — 手動で接続してください +Log.EspNowConnected,[ESP-NOW] 相手が接続しました +Log.EspNowDisconnected,[ESP-NOW] 相手が切断しました \ No newline at end of file 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 diff --git a/program/tempMakefile.txt b/program/tempMakefile.txt new file mode 100644 index 0000000..dc9aa0b --- /dev/null +++ b/program/tempMakefile.txt @@ -0,0 +1,101 @@ +APP := ears +VERSION := 1.0.0 +OUT := bin/linux/installer +DIST := dist +STAGE := $(HOME)/.cache/$(APP)-build +PKGDIR := $(STAGE)/$(APP)_$(VERSION)_all + +SRC := $(filter-out Form1.cs Form2.cs, $(wildcard *.cs)) +GLADE := $(wildcard *.glade) +ASSETS := $(wildcard map sound) + +.ONESHELL: +.SHELLFLAGS := -ec +.PHONY: all run clean deb init-packaging list + +all: $(OUT)/program.exe + +$(OUT)/program.exe: $(SRC) $(GLADE) + mkdir -p $(OUT) + mcs -target:winexe -pkg:gtk-sharp-3.0 \ + $(foreach f,$(GLADE),-resource:$(f),$(notdir $(f))) \ + $(SRC) -out:$@ +run: all + mono $(OUT)/program.exe + +deb: all + rm -rf "$(PKGDIR)" + install -Dm755 packaging/launcher.sh "$(PKGDIR)/usr/bin/$(APP)" + install -Dm644 $(OUT)/program.exe "$(PKGDIR)/usr/lib/$(APP)/program.exe" + install -Dm644 packaging/app.desktop "$(PKGDIR)/usr/share/applications/$(APP).desktop" + install -Dm644 packaging/udev.rules "$(PKGDIR)/lib/udev/rules.d/99-$(APP).rules" + install -Dm644 packaging/control "$(PKGDIR)/DEBIAN/control" + install -Dm755 packaging/postinst "$(PKGDIR)/DEBIAN/postinst" + install -Dm755 packaging/postrm "$(PKGDIR)/DEBIAN/postrm" + if [ -n "$(ASSETS)" ]; then cp -r $(ASSETS) "$(PKGDIR)/usr/lib/$(APP)/"; fi + find "$(PKGDIR)" -type d -exec chmod 755 {} + + find "$(PKGDIR)" -type f -exec chmod 644 {} + + chmod 755 "$(PKGDIR)/usr/bin/$(APP)" \ + "$(PKGDIR)/DEBIAN/postinst" "$(PKGDIR)/DEBIAN/postrm" + sed -i 's/\r$$//' "$(PKGDIR)/usr/bin/$(APP)" \ + "$(PKGDIR)/DEBIAN/postinst" "$(PKGDIR)/DEBIAN/postrm" + mkdir -p $(DIST) + dpkg-deb --build --root-owner-group "$(PKGDIR)" $(DIST)/$(APP)_$(VERSION)_all.deb + dpkg -c $(DIST)/$(APP)_$(VERSION)_all.deb + +clean: + rm -rf $(OUT) $(DIST) "$(STAGE)" + +list: + @grep -E '^[a-zA-Z_-]+:' Makefile | cut -d: -f1 + +init-packaging: + mkdir -p packaging + cat > packaging/launcher.sh <<-'EOF' + #!/bin/sh + cd /usr/lib/$(APP) || exit 1 + exec /usr/bin/mono ./program.exe "$$@" + EOF + cat > packaging/app.desktop <<-EOF + [Desktop Entry] + Type=Application + Name=EARS + Comment=Auscultation Training Software + Exec=$(APP) + Icon=$(APP) + Terminal=false + Categories=Utility;Electronics; + EOF + cat > packaging/control <<-EOF + Package: $(APP) + Version: $(VERSION) + Section: utils + Priority: optional + Architecture: all + Depends: mono-runtime (>= 6.0), gtk-sharp3, libgtk-3-0 + Maintainer: kazi + Description: Arduino serial control tool + GTK# application that connects to the device over USB serial. + EOF + cat > packaging/udev.rules <<-'EOF' + SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1" + SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1" + SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1" + SUBSYSTEM=="tty", ATTRS{idVendor}=="2341", TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1" + EOF + cat > packaging/postinst <<-'EOF' + #!/bin/sh + set -e + udevadm control --reload-rules || true + udevadm trigger --subsystem-match=tty || true + update-desktop-database -q /usr/share/applications || true + exit 0 + EOF + cat > packaging/postrm <<-'EOF' + #!/bin/sh + set -e + udevadm control --reload-rules || true + exit 0 + EOF + chmod 755 packaging/launcher.sh packaging/postinst packaging/postrm + echo "packaging/ created — edit control and udev.rules, then run: make deb" \ No newline at end of file