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/MainWindow.cs b/program/MainWindow.cs
index e4f74dd..216d3d2 100644
--- a/program/MainWindow.cs
+++ b/program/MainWindow.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using Gtk;
using UI = Gtk.Builder.ObjectAttribute;
@@ -10,8 +11,10 @@
[UI] private Button Option2Button = null;
[UI] private Button Option3Button = null;
[UI] private Switch LanguageSwitch = null;
- [UI] private Label LanguageLabel = 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;
@@ -27,6 +30,9 @@
private Dictionary _jpnText;
private Dictionary _buttonsById;
+ private readonly ArduinoConnection _arduino = new ArduinoConnection();
+ private SettingsWindow _settingsWindow;
+
public MainWindow() : this(CreateBuilder()) { }
private static Builder CreateBuilder()
@@ -51,18 +57,22 @@
Option1Button.Clicked += OnNormalSessionClicked;
Option2Button.Clicked += OnPositionRecordingClicked;
Option3Button.Clicked += OnExamModeClicked;
- CloseButton.Clicked += OnCloseClicked;
+ CloseButton.Clicked += OnCloseClicked;
+ SettingButton.Clicked += OnSettingClicked;
LanguageSwitch.AddNotification("active", OnLanguageSwitchNotify);
- DeleteEvent += OnDeleteEvent;
+ DeleteEvent += OnDeleteEvent;
SizeAllocated += OnWindowSizeAllocated;
+ _arduino.ConnectionChanged += OnArduinoConnectionChanged;
+
_buttonsById = new Dictionary
{
{ "Option1Button", Option1Button },
{ "Option2Button", Option2Button },
{ "Option3Button", Option3Button },
- { "CloseButton", CloseButton },
+ { "CloseButton", CloseButton },
+ { "SettingButton", SettingButton },
};
// CSS ID selectors (#Option1Button etc.) need the widget Name set explicitly
@@ -73,8 +83,46 @@
_jpnText = LoadButtonText("info_jpn.csv");
ApplyLanguage(LanguageSwitch.Active);
+ OnArduinoConnectionChanged(false);
}
+ // ---------- settings panel ----------
+
+ private void OnSettingClicked(object sender, EventArgs e)
+ {
+ if (_settingsWindow == null)
+ {
+ _settingsWindow = new SettingsWindow(_arduino);
+ _settingsWindow.BackRequested += OnSettingsClosed;
+ }
+
+ this.Hide();
+ _settingsWindow.ShowAll();
+ _settingsWindow.Fullscreen();
+ _settingsWindow.Present();
+ }
+
+ private void OnSettingsClosed(object sender, EventArgs e)
+ {
+ this.ShowAll();
+ this.Present();
+ }
+
+ 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();
@@ -91,7 +139,7 @@
var line = lines[i].Trim();
if (string.IsNullOrEmpty(line)) continue;
- var parts = line.Split(new[] { ',' }, 2); // split into max 2 pieces, in case text has commas
+ var parts = line.Split(new[] { ',' }, 2);
if (parts.Length < 2) continue;
result[parts[0]] = parts[1];
@@ -112,16 +160,15 @@
}
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;
@@ -132,39 +179,57 @@
double scaleX = (double)width / BaseWidth;
double scaleY = (double)height / BaseHeight;
- double scale = Math.Min(scaleX, scaleY);
- ApplyScale(scale);
+ 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 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));
- string css = $@"
+ // 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: {fontSize}px;
+ font-size: {0}px;
}}
button {{
- padding: {buttonPadV}px {buttonPadH}px;
+ padding: {1}px {2}px;
}}
switch {{
- min-width: {40 * scale}px;
- min-height: {24 * scale}px;
+ min-width: {3}px;
+ min-height: {4}px;
}}
- ";
+ ", fontSize, buttonPadV, buttonPadH, switchWidth, switchHeight);
_scaleCssProvider.LoadFromData(css);
}
+ // ---------- lifecycle ----------
+
private void OnDeleteEvent(object sender, DeleteEventArgs a)
{
- Application.Quit();
+ 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");
@@ -180,11 +245,6 @@
Console.WriteLine("Exam Mode selected");
}
- private void OnCloseClicked(object sender, EventArgs e)
- {
- Application.Quit();
- }
-
private void OnLanguageSwitchNotify(object o, GLib.NotifyArgs args)
{
ApplyLanguage(LanguageSwitch.Active);
diff --git a/program/SettingWindow.cs b/program/SettingWindow.cs
new file mode 100644
index 0000000..10a53e6
--- /dev/null
+++ b/program/SettingWindow.cs
@@ -0,0 +1,190 @@
+using System;
+using Gtk;
+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 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;
+
+ 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
index dca2529..5b7939a 100644
--- a/program/homepage.glade
+++ b/program/homepage.glade
@@ -2,9 +2,177 @@
+
False
EARS
+ 1280
+ 800
@@ -108,10 +276,11 @@
-
+
True
- True
- 6
+ False
+ network-offline
+ 6
4
diff --git a/program/homepage.glade~ b/program/homepage.glade~
index dca2529..dcedc1b 100644
--- a/program/homepage.glade~
+++ b/program/homepage.glade~
@@ -2,9 +2,158 @@
+
+ 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
@@ -108,10 +257,12 @@
-
+
True
- True
+ False
6
+ network-offline
+ 6
4
diff --git a/program/lang_jpn.css b/program/lang_jpn.css
index 6916073..13b87e2 100644
--- a/program/lang_jpn.css
+++ b/program/lang_jpn.css
@@ -24,4 +24,7 @@
font-size: 18px;
font-weight: bold;
font-style: normal;
-}
\ No newline at end of file
+}
+
+#StatusLabel.status-on { color: #2e7d32; font-weight: bold; }
+#StatusLabel.status-off { color: #c62828; font-weight: bold; }
\ No newline at end of file