diff --git a/OS/.gitkeep b/OS/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/OS/.gitkeep
diff --git a/Readme.md b/Readme.md
index 402d96e..51512ad 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,20 +1,28 @@
# Mono + GTK# + Glade — WSL Setup Guide
## Table of Contents
-- [1. Prerequisites](#1-prerequisites)
-- [2. Install Mono](#2-install-mono)
-- [3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)](#3-install-gtk-gtk-30-for-ubuntu-2404)
-- [4. Install Glade (visual UI designer)](#4-install-glade-visual-ui-designer)
-- [5. Optional: libgdiplus (only if using System.Drawing)](#5-optional-libgdiplus-only-if-using-systemdrawing)
-- [6. Compiling & Running](#6-compiling--running)
-- [7. Minimal Working Examples](#7-minimal-working-examples)
-- [8. Custom Output Names, Targets & Exporting DLLs](#8-custom-output-names-targets--exporting-dlls)
-- [9. Common Errors & Fixes](#9-common-errors--fixes)
-- [10. Notes on Alternatives](#10-notes-on-alternatives)
+- [Mono](#mono)
+ - [1. Prerequisites](#1-prerequisites)
+ - [2. Install Mono](#2-install-mono)
+ - [3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)](#3-install-gtk-gtk-30-for-ubuntu-2404)
+ - [4. Install Glade (visual UI designer)](#4-install-glade-visual-ui-designer)
+ - [5. Optional: libgdiplus (only if using System.Drawing)](#5-optional-libgdiplus-only-if-using-systemdrawing)
+ - [6. Compiling & Running](#6-compiling--running)
+ - [7. Minimal Working Examples](#7-minimal-working-examples)
+ - [8. Custom Output Names, Targets & Exporting DLLs](#8-custom-output-names-targets--exporting-dlls)
+ - [9. Common Errors & Fixes](#9-common-errors--fixes)
+ - [10. Notes on Alternatives](#10-notes-on-alternatives)
+- [OS](#os)
+ - [Workflow](#workflow)
+ - [OS images checked](#os-images-checked)
+ - [OS images to check](#os-images-to-check)
+
---
-## 1. Prerequisites
+## Mono
+
+### 1. Prerequisites
- Windows 11 (or updated Windows 10) with WSL2 and **WSLg** enabled for GUI support
- Ubuntu 24.04 (noble) or similar WSL distro
@@ -26,7 +34,7 @@
---
-## 2. Install Mono
+### 2. Install Mono
```bash
sudo apt update
@@ -41,7 +49,7 @@
---
-## 3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)
+### 3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)
> Note: `gtk-sharp2` is **not available** on Ubuntu 24.04 (noble). Use `gtk-sharp3` instead.
@@ -59,7 +67,7 @@
gtk-sharp-3.0 Gtk - Gtk
---
-## 4. Install Glade (visual UI designer)
+### 4. Install Glade (visual UI designer)
```bash
sudo apt install glade
@@ -73,7 +81,7 @@
---
-## 5. Optional: libgdiplus (only if using System.Drawing)
+### 5. Optional: libgdiplus (only if using System.Drawing)
Only needed if your code uses `System.Drawing.Bitmap`, `Graphics`, etc. (not required for plain GTK#/Glade apps):
@@ -83,21 +91,21 @@
---
-## 6. Compiling & Running
+### 6. Compiling & Running
-### Plain console C# program
+#### Plain console C# program
```bash
mcs myprogram.cs
mono myprogram.exe
```
-### GTK# 3.0 program (no Glade)
+#### GTK# 3.0 program (no Glade)
```bash
mcs -pkg:gtk-sharp-3.0 myapp.cs
mono myapp.exe
```
-### GTK# 3.0 program using a Glade file
+#### GTK# 3.0 program using a Glade file
```bash
mcs -pkg:gtk-sharp-3.0 -pkg:glade-sharp-3.0 myapp.cs
mono myapp.exe
@@ -106,9 +114,9 @@
---
-## 7. Minimal Working Examples
+### 7. Minimal Working Examples
-### Hello World (console)
+#### Hello World (console)
```csharp
using System;
@@ -123,7 +131,7 @@
mono hello.exe
```
-### Hello World (GTK# window, no Glade)
+#### Hello World (GTK# window, no Glade)
```csharp
using System;
using Gtk;
@@ -149,7 +157,7 @@
mono gtkcheck.exe
```
-### Loading a UI built in Glade
+#### Loading a UI built in Glade
```csharp
using System;
using Gtk;
@@ -175,11 +183,11 @@
---
-## 8. Custom Output Names, Targets & Exporting DLLs
+### 8. Custom Output Names, Targets & Exporting DLLs
By default, `mcs file.cs` names the output after the source file (`file.exe`). You can control this with `-out:` and change what kind of binary is produced with `-target:`.
-### 8.1 Custom output name
+#### 8.1 Custom output name
```bash
mcs -pkg:gtk-sharp-3.0 Program.cs -out:MyCustomApp.exe
```
@@ -188,7 +196,7 @@
mono MyCustomApp.exe
```
-### 8.2 `-target` options
+#### 8.2 `-target` options
| Target | Produces | Notes |
|---|---|---|
| `exe` (default) | Console executable | Shows a console window when run on Windows |
@@ -202,7 +210,7 @@
```
This produces `programwin.exe`, which on Windows will run without popping up a console window alongside your GTK window.
-### 8.3 Exporting a DLL
+#### 8.3 Exporting a DLL
If you want to package reusable code (helper classes, business logic, etc.) as a library instead of a standalone app:
@@ -213,7 +221,7 @@
- No `Main()` method is required in a `library` target (though it's fine if one class in your project has one — it just won't be used as an entry point for the DLL itself).
- This creates `MyLibrary.dll`, a Mono/.NET assembly that other C# programs can reference.
-### 8.4 Using a DLL in another program
+#### 8.4 Using a DLL in another program
Suppose `MyLibrary.dll` contains:
```csharp
@@ -253,7 +261,7 @@
mono ConsumerApp.exe
```
-### 8.5 Combining `-target:library` with GTK# packages
+#### 8.5 Combining `-target:library` with GTK# packages
If your DLL itself uses GTK# types (e.g., a shared custom widget), include the package flag when building the library too:
```bash
mcs -target:library -pkg:gtk-sharp-3.0 MyGtkWidgets.cs -out:MyGtkWidgets.dll
@@ -263,7 +271,7 @@
mcs -pkg:gtk-sharp-3.0 ConsumerApp.cs -r:MyGtkWidgets.dll -out:ConsumerApp.exe
```
-### 8.6 Quick reference
+#### 8.6 Quick reference
```bash
# Console exe, custom name
mcs Program.cs -out:myapp.exe
@@ -278,7 +286,7 @@
mcs Consumer.cs -r:MyLibrary.dll -out:Consumer.exe
```
-### 8.7 Running the compiled .exe on Windows (outside WSL)
+#### 8.7 Running the compiled .exe on Windows (outside WSL)
Compiling in WSL produces a `.exe` that targets the .NET/Mono runtime — it is **not** a native Windows binary, and it will not run on Windows by itself. Two things are needed:
@@ -308,7 +316,7 @@
---
-## 9. Common Errors & Fixes
+### 9. Common Errors & Fixes
| Error | Cause | Fix |
|---|---|---|
@@ -322,7 +330,7 @@
---
-## 10. Notes on Alternatives
+### 10. Notes on Alternatives
GTK# is a legacy, lightly-maintained binding. For new projects, consider:
- **Avalonia UI** — modern, XAML-based, cross-platform, actively maintained
@@ -353,4 +361,22 @@
to run note:
same for all cases
- mono NAME_OF_THE_FILE.exe
\ No newline at end of file
+ mono NAME_OF_THE_FILE.exe
+
+## OS
+
+To test the application/setup across different Linux distributions, I used **Ventoy** to create a multiboot USB drive. Ventoy lets you copy multiple ISO files onto a single USB stick and choose which one to boot at startup, without needing to reformat or re-flash the drive for each OS.
+
+### Workflow
+1. Install Ventoy on the target USB drive.
+2. Copy the desired `.iso` files directly onto the Ventoy partition (no extraction needed).
+3. Boot from the USB, select the ISO from the Ventoy boot menu, and test the OS live or install it.
+
+### OS images checked
+- Fedora-Workstation-Live-44-1.7.x86_64
+- lubuntu-26.04-desktop-amd64
+- xubuntu-25.10-desktop-amd64
+
+### OS images to check
+- antiX-26_x64-full
+- xubuntu-26.04-minimal-amd64
\ No newline at end of file
diff --git a/program/ArduinoConnection.cs b/program/ArduinoConnection.cs
new file mode 100644
index 0000000..6edc385
--- /dev/null
+++ b/program/ArduinoConnection.cs
@@ -0,0 +1,373 @@
+using System;
+using System.IO.Ports;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+public class ArduinoConnection : IDisposable
+{
+ public const int BaudRate = 115200;
+
+ // Must match what your sketch replies to "REQUEST_ID" (Form2.cs: EXPECTED_DEVICE_ID)
+ // public const string ExpectedDeviceId = "EARS_READER";
+ public const string ExpectedDeviceId = "N_EARS_ESP32C3";
+
+ private const int HeartbeatTimeoutMs = 10000;
+ private const int HeartbeatCheckMs = 2000;
+ private const int MaxReconnectAttempts = 5;
+ private const int ResetSettleMs = 1500; // board reboots when DTR asserts
+
+ private readonly object _gate = new object();
+ private SerialPort _port;
+ private Thread _reader;
+ private volatile bool _reading;
+
+ private DateTime _lastData = DateTime.MinValue;
+ private uint _heartbeatId;
+ private bool _autoReconnect;
+ private int _reconnectAttempts;
+ private DateTime _nextReconnect = DateTime.MinValue;
+
+ /// Raised on the GTK main thread for every complete line from the device.
+ public event Action LineReceived;
+ /// Raised on the GTK main thread with human-readable status text.
+ public event Action Log;
+ /// Raised on the GTK main thread when the link goes up or down.
+ public event Action ConnectionChanged;
+
+ public string PortName { get; private set; }
+
+ public bool IsOpen
+ {
+ get { lock (_gate) return _port != null && _port.IsOpen; }
+ }
+
+ public static string[] ListPorts()
+ {
+ return SerialPort.GetPortNames()
+ .Distinct()
+ .OrderBy(p => p, StringComparer.Ordinal)
+ .ToArray();
+ }
+
+ // ---------- public API ----------
+
+ public bool Connect(string portName)
+ {
+ if (string.IsNullOrEmpty(portName))
+ {
+ Emit(Log, "No port selected");
+ return false;
+ }
+
+ ClosePort(silent: true);
+
+ if (!OpenPort(portName))
+ return false;
+
+ _autoReconnect = false;
+ _reconnectAttempts = 0;
+ StartHeartbeat();
+
+ Emit(Log, $"Connected to {portName} @ {BaudRate}");
+ Emit(ConnectionChanged, true);
+
+ // Give the board time to finish booting before the first command.
+ Task.Delay(ResetSettleMs).ContinueWith(_ => Send("MODE3"));
+ return true;
+ }
+
+ public void Disconnect()
+ {
+ _autoReconnect = false;
+ StopHeartbeat();
+ ClosePort(silent: false);
+ }
+
+ public void Send(string command)
+ {
+ try
+ {
+ lock (_gate)
+ {
+ if (_port == null || !_port.IsOpen)
+ {
+ Emit(Log, $"Cannot send '{command}' — not connected");
+ return;
+ }
+ _port.Write(command + "\n");
+ }
+ Emit(Log, $"Sent: {command}");
+ }
+ catch (Exception ex)
+ {
+ Emit(Log, $"Send failed: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Probes every port for a device answering REQUEST_ID with the expected ID.
+ /// Returns the port name, or null. Runs off the UI thread; the caller
+ /// connects on the main thread so no GLib call happens from a worker.
+ ///
+ public Task DetectPortAsync()
+ {
+ return Task.Run(() =>
+ {
+ foreach (string p in ListPorts())
+ {
+ if (IsOpen && p == PortName) continue;
+
+ Emit(Log, $"Probing {p}...");
+ string id;
+ if (Probe(p, out id))
+ {
+ Emit(Log, $"Found {ExpectedDeviceId} on {p}");
+ return p;
+ }
+ if (id != null)
+ Emit(Log, $" {p}: reported '{id}' (not a match)");
+ }
+ return (string)null;
+ });
+ }
+
+ // ---------- port plumbing ----------
+
+ private bool OpenPort(string portName)
+ {
+ try
+ {
+ lock (_gate)
+ {
+ _port = new SerialPort(portName, BaudRate)
+ {
+ ReadTimeout = 500,
+ WriteTimeout = 1000,
+ NewLine = "\n",
+ DtrEnable = true,
+ RtsEnable = true,
+ };
+ _port.Open();
+ }
+
+ PortName = portName;
+ _lastData = DateTime.Now;
+ StartReader();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Emit(Log, $"Open failed on {portName}: {ex.Message}");
+ lock (_gate)
+ {
+ _port?.Dispose();
+ _port = null;
+ }
+ return false;
+ }
+ }
+
+ private void ClosePort(bool silent)
+ {
+ _reading = false;
+
+ Thread t = _reader;
+ _reader = null;
+ if (t != null && t.IsAlive && t != Thread.CurrentThread)
+ t.Join(1000);
+
+ lock (_gate)
+ {
+ try { if (_port != null && _port.IsOpen) _port.Close(); }
+ catch (Exception ex) { Emit(Log, $"Close error: {ex.Message}"); }
+ _port?.Dispose();
+ _port = null;
+ }
+
+ if (!silent)
+ {
+ Emit(Log, "Disconnected");
+ Emit(ConnectionChanged, false);
+ }
+ }
+
+ private void StartReader()
+ {
+ _reading = true;
+ _reader = new Thread(ReaderLoop)
+ {
+ IsBackground = true,
+ Name = "arduino-reader"
+ };
+ _reader.Start();
+ }
+
+ private void ReaderLoop()
+ {
+ var pending = new StringBuilder();
+ var chunk = new byte[4096];
+
+ while (_reading)
+ {
+ SerialPort p;
+ lock (_gate) p = _port;
+
+ if (p == null || !p.IsOpen) { Thread.Sleep(100); continue; }
+
+ try
+ {
+ int n = p.Read(chunk, 0, chunk.Length);
+ if (n <= 0) continue;
+
+ _lastData = DateTime.Now;
+ pending.Append(Encoding.ASCII.GetString(chunk, 0, n));
+
+ string buffered = pending.ToString();
+ int nl;
+ while ((nl = buffered.IndexOf('\n')) >= 0)
+ {
+ string line = buffered.Substring(0, nl).Trim();
+ buffered = buffered.Substring(nl + 1);
+ if (line.Length > 0)
+ Emit(LineReceived, line);
+ }
+ pending.Clear();
+ pending.Append(buffered);
+ }
+ catch (TimeoutException) { /* normal when idle */ }
+ catch (Exception ex)
+ {
+ if (_reading) Emit(Log, $"Read error: {ex.Message}");
+ Thread.Sleep(200);
+ }
+ }
+ }
+
+ private bool Probe(string portName, out string deviceId)
+ {
+ deviceId = null;
+ SerialPort test = null;
+ try
+ {
+ test = new SerialPort(portName, BaudRate)
+ {
+ ReadTimeout = 500,
+ WriteTimeout = 1000,
+ NewLine = "\n",
+ DtrEnable = true,
+ RtsEnable = true,
+ };
+ test.Open();
+ Thread.Sleep(ResetSettleMs);
+ test.DiscardInBuffer();
+ test.Write("REQUEST_ID\n");
+
+ DateTime deadline = DateTime.Now.AddSeconds(3);
+ while (DateTime.Now < deadline)
+ {
+ try
+ {
+ string line = test.ReadLine().Trim();
+ if (line.StartsWith("DEVICE_ID:"))
+ {
+ deviceId = line.Substring("DEVICE_ID:".Length).Trim();
+ return deviceId == ExpectedDeviceId;
+ }
+ }
+ catch (TimeoutException) { }
+ }
+ }
+ catch (Exception ex)
+ {
+ Emit(Log, $" {portName}: {ex.Message}");
+ }
+ finally
+ {
+ try { test?.Close(); } catch { }
+ test?.Dispose();
+ }
+ return false;
+ }
+
+ // ---------- heartbeat / auto-reconnect (runs on the GTK main loop) ----------
+
+ private void StartHeartbeat()
+ {
+ StopHeartbeat();
+ _heartbeatId = GLib.Timeout.Add(HeartbeatCheckMs, OnHeartbeatTick);
+ }
+
+ private void StopHeartbeat()
+ {
+ if (_heartbeatId != 0)
+ {
+ GLib.Source.Remove(_heartbeatId);
+ _heartbeatId = 0;
+ }
+ }
+
+ private bool OnHeartbeatTick()
+ {
+ if (_autoReconnect && !IsOpen)
+ {
+ if (_reconnectAttempts >= MaxReconnectAttempts)
+ {
+ _autoReconnect = false;
+ Log?.Invoke($"Reconnection failed after {MaxReconnectAttempts} attempts. Check USB cable, port and power, then reconnect manually.");
+ StopHeartbeat();
+ return false;
+ }
+
+ if (DateTime.Now >= _nextReconnect)
+ {
+ _reconnectAttempts++;
+ Log?.Invoke($"Reconnect attempt {_reconnectAttempts}/{MaxReconnectAttempts} on {PortName}...");
+
+ if (OpenPort(PortName))
+ {
+ _autoReconnect = false;
+ _reconnectAttempts = 0;
+ Log?.Invoke("Reconnected");
+ ConnectionChanged?.Invoke(true);
+ Task.Delay(ResetSettleMs).ContinueWith(_ => Send("MODE3"));
+ }
+ else
+ {
+ _nextReconnect = DateTime.Now.AddSeconds(2);
+ }
+ }
+ return true;
+ }
+
+ if (!IsOpen) return true;
+
+ if ((DateTime.Now - _lastData).TotalMilliseconds >= HeartbeatTimeoutMs)
+ {
+ Log?.Invoke("Heartbeat timeout — no device activity");
+ ClosePort(silent: true);
+ ConnectionChanged?.Invoke(false);
+ _autoReconnect = true;
+ _reconnectAttempts = 0;
+ _nextReconnect = DateTime.Now.AddSeconds(2);
+ }
+ return true;
+ }
+
+ // ---------- helpers ----------
+
+ private static void Emit(Action handler, T arg)
+ {
+ Action h = handler;
+ if (h == null) return;
+ Gtk.Application.Invoke((s, e) => h(arg));
+ }
+
+ public void Dispose()
+ {
+ _autoReconnect = false;
+ StopHeartbeat();
+ ClosePort(silent: true);
+ }
+}
\ No newline at end of file
diff --git a/program/Language.cs b/program/Language.cs
new file mode 100644
index 0000000..70695ae
--- /dev/null
+++ b/program/Language.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Gtk;
+
+public class Language
+{
+ public static bool LanguageSwitchValue = false;
+
+ private const string EngCssPath = "lang_eng.css";
+ private const string JpnCssPath = "lang_jpn.css";
+ private const string EngCsvPath = "info_eng.csv";
+ private const string JpnCsvPath = "info_jpn.csv";
+
+ private readonly Dictionary _engText;
+ private readonly Dictionary _jpnText;
+ private readonly Dictionary _buttonsById;
+ public readonly CssProvider _languageCssProvider = new CssProvider();
+ private readonly Label _languageLabel;
+
+ public Language(Dictionary buttonsById, Label languageLabel)
+ {
+ _buttonsById = buttonsById;
+ _languageLabel = languageLabel;
+ StyleContext.AddProviderForScreen(Gdk.Screen.Default, _languageCssProvider, 600);
+
+
+ _engText = LoadButtonText(EngCsvPath);
+ _jpnText = LoadButtonText(JpnCsvPath);
+ }
+
+ private Dictionary LoadButtonText(string path)
+ {
+ var result = new Dictionary();
+ if (!File.Exists(path))
+ {
+ Console.WriteLine($"Warning: text file not found: {path}");
+ return result;
+ }
+
+ var lines = File.ReadAllLines(path);
+ for (int i = 1; i < lines.Length; i++)
+ {
+ var line = lines[i].Trim();
+ if (string.IsNullOrEmpty(line)) continue;
+ var parts = line.Split(new[] { ',' }, 2);
+ if (parts.Length < 2) continue;
+ result[parts[0]] = parts[1];
+ }
+ return result;
+ }
+
+ public void ApplyLanguage(bool isEnglish)
+ {
+ var text = isEnglish ? _engText : _jpnText;
+ string cssPath = isEnglish ? EngCssPath : JpnCssPath;
+
+ foreach (var kvp in _buttonsById)
+ if (text.TryGetValue(kvp.Key, out var buttonText))
+ kvp.Value.Label = buttonText;
+
+ if (File.Exists(cssPath))
+ _languageCssProvider.LoadFromPath(cssPath);
+ else
+ Console.WriteLine($"Warning: CSS file not found: {cssPath}");
+
+ _languageLabel.Text = isEnglish ? "あ" : "A";
+ }
+
+ //for others
+ public Language(Dictionary buttonsById)
+ {
+ _buttonsById = buttonsById;
+ StyleContext.AddProviderForScreen(Gdk.Screen.Default, _languageCssProvider, 600);
+
+
+ _engText = LoadButtonText(EngCsvPath);
+ _jpnText = LoadButtonText(JpnCsvPath);
+ }
+
+
+ public void ApplyLanguages(bool isEnglish)
+ {
+ var text = isEnglish ? _engText : _jpnText;
+ string cssPath = isEnglish ? EngCssPath : JpnCssPath;
+
+ foreach (var kvp in _buttonsById)
+ if (text.TryGetValue(kvp.Key, out var buttonText))
+ kvp.Value.Label = buttonText;
+
+ if (File.Exists(cssPath))
+ _languageCssProvider.LoadFromPath(cssPath);
+ else
+ Console.WriteLine($"Warning: CSS file not found: {cssPath}");
+
+ }
+}
\ No newline at end of file
diff --git a/program/MainWindow.cs b/program/MainWindow.cs
new file mode 100644
index 0000000..0af3392
--- /dev/null
+++ b/program/MainWindow.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using Gtk;
+using UI = Gtk.Builder.ObjectAttribute;
+
+public class MainWindow : Window
+{
+ [UI] private Button Option1Button = null;
+ [UI] private Button Option2Button = null;
+ [UI] private Button Option3Button = null;
+ [UI] private Switch LanguageSwitch = null;
+ [UI] private Label LanguageLabel = null;
+ [UI] private Button CloseButton = null;
+ [UI] private Button SettingButton = null;
+ [UI] private Image ConnectionIcon = null; // was GtkIconView in the old glade
+
+ private CssProvider _scaleCssProvider;
+ // private CssProvider _languageCssProvider;
+ private int _lastAppliedWidth = -1;
+
+ private const int BaseWidth = 1920;
+ private const int BaseHeight = 1080;
+
+ // private const string EngCssPath = "lang_eng.css";
+ // private const string JpnCssPath = "lang_jpn.css";
+
+ // private Dictionary _engText;
+ // private Dictionary _jpnText;
+ private Dictionary _buttonsById;
+
+ private readonly ArduinoConnection _arduino = new ArduinoConnection();
+ internal SettingsWindow _settingsWindow;
+ internal Language _Language;
+
+ public MainWindow() : this(CreateBuilder()) { }
+
+ private static Builder CreateBuilder()
+ {
+ var builder = new Builder();
+ builder.AddFromFile("homepage.glade");
+ return builder;
+ }
+
+ private MainWindow(Builder builder) : base(builder.GetObject("SplashWindow").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);
+
+ this.Fullscreen();
+
+ Option1Button.Clicked += OnNormalSessionClicked;
+ Option2Button.Clicked += OnPositionRecordingClicked;
+ Option3Button.Clicked += OnExamModeClicked;
+ CloseButton.Clicked += OnCloseClicked;
+ SettingButton.Clicked += OnSettingClicked;
+ LanguageSwitch.AddNotification("active", OnLanguageSwitchNotify);
+
+ 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);
+ 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();
+ }
+
+
+ _settingsWindow.ShowAll();
+ _settingsWindow.Present();
+ this.Hide();
+ }
+
+ private void OnSettingsClosed(object sender, EventArgs e)
+ {
+ this.ShowAll();
+ this.Present();
+ _settingsWindow.Hide();
+ }
+
+ private void OnArduinoConnectionChanged(bool connected)
+ {
+ if (ConnectionIcon == null) return;
+
+ ConnectionIcon.SetFromIconName(
+ connected ? "network-transmit-receive" : "network-offline",
+ IconSize.Dnd);
+
+ ConnectionIcon.TooltipText = connected
+ ? $"Connected to {_arduino.PortName}"
+ : "Device not connected";
+ }
+
+ // ---------- localisation ----------
+
+ private Dictionary LoadButtonText(string path)
+ {
+ var result = new Dictionary();
+
+ if (!File.Exists(path))
+ {
+ Console.WriteLine($"Warning: text file not found: {path}");
+ return result;
+ }
+
+ var lines = File.ReadAllLines(path);
+ for (int i = 1; i < lines.Length; i++) // skip header row
+ {
+ var line = lines[i].Trim();
+ if (string.IsNullOrEmpty(line)) continue;
+
+ var parts = line.Split(new[] { ',' }, 2);
+ if (parts.Length < 2) continue;
+
+ result[parts[0]] = parts[1];
+ }
+
+ return result;
+ }
+
+ // private void ApplyLanguage(bool isEnglish)
+ // {
+ // var text = isEnglish ? _engText : _jpnText;
+ // string cssPath = isEnglish ? EngCssPath : JpnCssPath;
+
+ // foreach (var kvp in _buttonsById)
+ // {
+ // if (text.TryGetValue(kvp.Key, out var buttonText))
+ // kvp.Value.Label = buttonText;
+ // }
+
+ // if (File.Exists(cssPath))
+ // _languageCssProvider.LoadFromPath(cssPath);
+ // else
+ // Console.WriteLine($"Warning: CSS file not found: {cssPath}");
+
+ // LanguageLabel.Text = isEnglish ? "あ" : "A";
+ // }
+
+ // ---------- scaling ----------
+
+ private void OnWindowSizeAllocated(object o, SizeAllocatedArgs args)
+ {
+ int width = args.Allocation.Width;
+ int height = args.Allocation.Height;
+
+ if (width == _lastAppliedWidth) return;
+ _lastAppliedWidth = width;
+
+ double scaleX = (double)width / BaseWidth;
+ double scaleY = (double)height / BaseHeight;
+
+ ApplyScale(Math.Min(scaleX, scaleY));
+ }
+
+ private void ApplyScale(double scale)
+ {
+ int fontSize = Math.Max(10, (int)(20 * scale));
+ int buttonPadV = Math.Max(4, (int)(12 * scale));
+ int buttonPadH = Math.Max(8, (int)(24 * scale));
+ int switchWidth = Math.Max(30, (int)(40 * scale));
+ int switchHeight= Math.Max(18, (int)(24 * scale));
+
+ // NOTE: integers + InvariantCulture. Interpolating a double here emitted
+ // "40,5px" under ja_JP / de_DE locales and GTK silently dropped the rule.
+ string css = string.Format(CultureInfo.InvariantCulture, @"
+ grid, label, button, switch {{
+ font-size: {0}px;
+ }}
+ button {{
+ padding: {1}px {2}px;
+ }}
+ switch {{
+ min-width: {3}px;
+ min-height: {4}px;
+ }}
+ ", fontSize, buttonPadV, buttonPadH, switchWidth, switchHeight);
+
+ _scaleCssProvider.LoadFromData(css);
+ }
+
+ // ---------- lifecycle ----------
+
+ private void OnDeleteEvent(object sender, DeleteEventArgs a)
+ {
+ Shutdown();
+ a.RetVal = true;
+ }
+
+ private void OnCloseClicked(object sender, EventArgs e)
+ {
+ Shutdown();
+ }
+
+ private void Shutdown()
+ {
+ _arduino.Disconnect();
+ _arduino.Dispose();
+ _settingsWindow?.Destroy();
+ Application.Quit();
+ }
+
+ private void OnNormalSessionClicked(object sender, EventArgs e)
+ {
+ Console.WriteLine("Normal Session selected");
+ }
+
+ private void OnPositionRecordingClicked(object sender, EventArgs e)
+ {
+ Console.WriteLine("Position Recording Mode selected");
+ }
+
+ private void OnExamModeClicked(object sender, EventArgs e)
+ {
+ Console.WriteLine("Exam Mode selected");
+ }
+
+ private void OnLanguageSwitchNotify(object o, GLib.NotifyArgs args)
+ {
+ Language.LanguageSwitchValue = LanguageSwitch.Active;
+ _Language.ApplyLanguage(LanguageSwitch.Active);
+ _settingsWindow?._Language?.ApplyLanguages(LanguageSwitch.Active);
+ }
+}
\ No newline at end of file
diff --git a/program/SettingWindow.cs b/program/SettingWindow.cs
new file mode 100644
index 0000000..2d94a53
--- /dev/null
+++ b/program/SettingWindow.cs
@@ -0,0 +1,210 @@
+using System;
+using Gtk;
+using System.Collections.Generic;
+using UI = Gtk.Builder.ObjectAttribute;
+
+public class SettingsWindow : Window
+{
+ [UI] private Label ComPortLabel = null;
+ [UI] private ComboBoxText ComPortComboBox = null;
+ [UI] private Button ConnectButton = null;
+ [UI] private Button AutoConnectButton = null;
+ [UI] private Button RefreshButton = null;
+ [UI] private Button BackButton = null;
+ [UI] private Label StatusLabel = null;
+ [UI] private Label LogLabel = null;
+ [UI] private TextView LogtextBox = null;
+
+ private readonly Dictionary _buttonsById;
+ private readonly Dictionary _labelsById;
+ internal Language _Language;
+
+ private const int MaxLogLines = 500;
+
+ private readonly ArduinoConnection _arduino;
+ private string[] _ports = new string[0];
+
+ /// Raised after the window hides itself, so the caller can re-show the main window.
+ public event EventHandler BackRequested;
+
+ public SettingsWindow(ArduinoConnection arduino)
+ : this(CreateBuilder(), arduino) { }
+
+ private static Builder CreateBuilder()
+ {
+ var builder = new Builder();
+ builder.AddFromFile("homepage.glade");
+ return builder;
+ }
+
+ private SettingsWindow(Builder builder, ArduinoConnection arduino)
+ : base(builder.GetObject("SettingWindow").Handle)
+ {
+ builder.Autoconnect(this);
+ _arduino = arduino;
+
+ Title = "Settings";
+ ComPortLabel.Text = "COM Port";
+ LogLabel.Text = "Log";
+ LogtextBox.Editable = false;
+ LogtextBox.WrapMode = WrapMode.WordChar;
+
+ RefreshButton.Clicked += (s, e) => RefreshPorts();
+ ConnectButton.Clicked += OnConnectClicked;
+ AutoConnectButton.Clicked += OnAutoConnectClicked;
+ BackButton.Clicked += OnBackClicked;
+
+ DeleteEvent += (o, a) => { a.RetVal = true; OnBackClicked(o, EventArgs.Empty); };
+
+ _arduino.Log += AppendLog;
+ _arduino.LineReceived += OnLineReceived;
+ _arduino.ConnectionChanged += OnConnectionChanged;
+
+ _buttonsById = new Dictionary
+ {
+ { "ConnectButton", ConnectButton },
+ { "AutoConnectButton", AutoConnectButton },
+ { "RefreshButton", RefreshButton },
+ { "BackButton", BackButton },
+ };
+
+ // CSS ID selectors (#Option1Button etc.) need the widget Name set explicitly
+ foreach (var kvp in _buttonsById)
+ kvp.Value.Name = kvp.Key;
+
+ _Language = new Language(_buttonsById);
+ _Language.ApplyLanguages(Language.LanguageSwitchValue);
+
+ RefreshPorts();
+ OnConnectionChanged(_arduino.IsOpen);
+ }
+
+ // ---------- UI actions ----------
+
+ private void RefreshPorts()
+ {
+ ComPortComboBox.RemoveAll();
+ _ports = ArduinoConnection.ListPorts();
+
+ foreach (string p in _ports)
+ ComPortComboBox.AppendText(p);
+
+ if (_ports.Length > 0)
+ {
+ ComPortComboBox.Active = 0;
+ AppendLog($"{_ports.Length} port(s) found");
+ }
+ else
+ {
+ AppendLog("No serial ports found");
+ }
+ }
+
+ private void OnConnectClicked(object sender, EventArgs e)
+ {
+ if (_arduino.IsOpen)
+ {
+ _arduino.Disconnect();
+ return;
+ }
+
+ string port = ComPortComboBox.ActiveText;
+ if (string.IsNullOrEmpty(port))
+ {
+ AppendLog("Select a COM port first");
+ return;
+ }
+ _arduino.Connect(port);
+ }
+
+ private async void OnAutoConnectClicked(object sender, EventArgs e)
+ {
+ AutoConnectButton.Sensitive = false;
+ ConnectButton.Sensitive = false;
+ AppendLog("Scanning ports for device...");
+
+ string found = await _arduino.DetectPortAsync();
+
+ if (found != null)
+ {
+ RefreshPorts();
+ SelectPort(found);
+ _arduino.Connect(found);
+ }
+ else
+ {
+ AppendLog("Device not found — use manual connection");
+ }
+
+ AutoConnectButton.Sensitive = true;
+ ConnectButton.Sensitive = true;
+ }
+
+ private void OnBackClicked(object sender, EventArgs e)
+ {
+ Hide();
+ BackRequested?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void SelectPort(string portName)
+ {
+ for (int i = 0; i < _ports.Length; i++)
+ {
+ if (_ports[i] == portName)
+ {
+ ComPortComboBox.Active = i;
+ return;
+ }
+ }
+ }
+
+ // ---------- device events ----------
+
+ private void OnConnectionChanged(bool connected)
+ {
+ ConnectButton.Label = connected ? "Disconnect" : "Connect";
+ StatusLabel.Text = connected
+ ? $"Connected ({_arduino.PortName})"
+ : "Disconnected";
+
+ ComPortComboBox.Sensitive = !connected;
+ RefreshButton.Sensitive = !connected;
+
+ StyleContext ctx = StatusLabel.StyleContext;
+ ctx.RemoveClass(connected ? "status-off" : "status-on");
+ ctx.AddClass(connected ? "status-on" : "status-off");
+ }
+
+ private void OnLineReceived(string line)
+ {
+ // Same filtering Form2.cs applied before parsing.
+ if (line.StartsWith("Send Status:")) return;
+ if (line.StartsWith("\u2713") || line.StartsWith("\u2717")) return;
+
+ if (line.Contains("ESPNOW_CONNECTED")) { AppendLog("[ESP-NOW] Peer connected"); return; }
+ if (line.Contains("ESPNOW_DISCONNECTED")) { AppendLog("[ESP-NOW] Peer disconnected"); return; }
+
+ AppendLog(line);
+ }
+
+ // ---------- log ----------
+
+ public void AppendLog(string message)
+ {
+ TextBuffer buf = LogtextBox.Buffer;
+
+ TextIter end = buf.EndIter;
+ buf.Insert(ref end, $"[{DateTime.Now:HH:mm:ss.fff}] {message}\n");
+
+ if (buf.LineCount > MaxLogLines)
+ {
+ TextIter start = buf.StartIter;
+ TextIter cut = buf.GetIterAtLine(buf.LineCount - MaxLogLines);
+ buf.Delete(ref start, ref cut);
+ }
+
+ TextMark mark = buf.CreateMark(null, buf.EndIter, false);
+ LogtextBox.ScrollToMark(mark, 0, false, 0, 0);
+ buf.DeleteMark(mark);
+ }
+}
\ No newline at end of file
diff --git a/program/homepage.glade b/program/homepage.glade
new file mode 100644
index 0000000..5b7939a
--- /dev/null
+++ b/program/homepage.glade
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+ 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..dcedc1b
--- /dev/null
+++ b/program/homepage.glade~
@@ -0,0 +1,326 @@
+
+
+
+
+
+ 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
+
+
+
+
+ Refresh Ports
+ True
+ True
+ True
+ center
+ start
+
+
+ 0
+ 1
+
+
+
+
+ True
+ False
+ Disconnected
+
+
+ 2
+ 1
+
+
+
+
+
+
+ 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
+ 6
+ network-offline
+ 6
+
+
+ 4
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/program/info_eng.csv b/program/info_eng.csv
new file mode 100644
index 0000000..3a0481a
--- /dev/null
+++ b/program/info_eng.csv
@@ -0,0 +1,11 @@
+ButtonId,Text
+Option1Button,Normal Session
+Option2Button,Position Recording Mode
+Option3Button,Exam Mode
+CloseButton,Close
+SettingButton,Settings
+ConnectButton,Connect
+AutoConnectButton,Auto Connect
+RefreshButton, Refresh Port
+LogLabel,Log
+BackButton,Back
diff --git a/program/info_jpn.csv b/program/info_jpn.csv
new file mode 100644
index 0000000..1d4e6e8
--- /dev/null
+++ b/program/info_jpn.csv
@@ -0,0 +1,11 @@
+ButtonId,Text
+Option1Button,通常セッション
+Option2Button,位置記録モード
+Option3Button,試験モード
+CloseButton,閉じる
+SettingButton,設定
+ConnectButton,接続
+AutoConnectButton,自動接続
+RefreshButton, ポートの更新
+LogLabel,情報
+BackButton,戻る
diff --git a/program/lang_eng.css b/program/lang_eng.css
new file mode 100644
index 0000000..fcf83ec
--- /dev/null
+++ b/program/lang_eng.css
@@ -0,0 +1,69 @@
+#Option1Button {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option2Button {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option3Button {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#CloseButton {
+ font-family: 'Sans';
+ font-size: 18px;
+ font-weight: bold;
+ font-style: normal;
+}
+
+#StatusLabel.status-on { color: #2e7d32; font-weight: bold; }
+#StatusLabel.status-off { color: #c62828; font-weight: bold; }
+
+/* Setting Window
+#ConnectButton {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#AutoConnectButton {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option3Button {
+ font-family: 'Sans';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#CloseButton {
+ font-family: 'Sans';
+ font-size: 18px;
+ font-weight: bold;
+ font-style: normal;
+} */
+
+/* [UI] private Label ComPortLabel = null;
+ [UI] private ComboBoxText ComPortComboBox = null;
+ [UI] private Button ConnectButton = null;
+ [UI] private Button AutoConnectButton = null;
+ [UI] private Button RefreshButton = null;
+ [UI] private Button BackButton = null;
+ [UI] private Label StatusLabel = null;
+ [UI] private Label LogLabel = null;
+ [UI] private TextView LogtextBox = null; */
\ No newline at end of file
diff --git a/program/lang_jpn.css b/program/lang_jpn.css
new file mode 100644
index 0000000..13b87e2
--- /dev/null
+++ b/program/lang_jpn.css
@@ -0,0 +1,30 @@
+#Option1Button {
+ font-family: 'Noto Sans JP';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option2Button {
+ font-family: 'Noto Sans JP';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#Option3Button {
+ font-family: 'Noto Sans JP';
+ font-size: 20px;
+ font-weight: normal;
+ font-style: normal;
+}
+
+#CloseButton {
+ font-family: 'Noto Sans JP';
+ font-size: 18px;
+ font-weight: bold;
+ font-style: normal;
+}
+
+#StatusLabel.status-on { color: #2e7d32; font-weight: bold; }
+#StatusLabel.status-off { color: #c62828; font-weight: bold; }
\ No newline at end of file
diff --git a/program/program.cs b/program/program.cs
new file mode 100644
index 0000000..f9db539
--- /dev/null
+++ b/program/program.cs
@@ -0,0 +1,16 @@
+using System;
+using Gtk;
+
+class Program
+{
+ [STAThread]
+ static void Main(string[] args)
+ {
+ Application.Init();
+
+ var app = new MainWindow();
+ app.ShowAll();
+
+ Application.Run();
+ }
+}
\ No newline at end of file
diff --git a/program/style.css b/program/style.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/program/style.css