diff --git a/program/BatteryMonitor.cs b/program/BatteryMonitor.cs new file mode 100644 index 0000000..16945d3 --- /dev/null +++ b/program/BatteryMonitor.cs @@ -0,0 +1,105 @@ +using System; +using System.Globalization; +using System.IO; + +public class BatteryStatus +{ + public bool Present; + public int Percent = -1; + public bool Charging; + public bool Full; +} + +public static class BatteryMonitor +{ + private const string Root = "/sys/class/power_supply"; + private static string _path; // cached battery directory + + public static BatteryStatus Read() + { + var s = new BatteryStatus(); + + string dir = FindBattery(); + if (dir == null) return s; + + int pct = ReadPercent(dir); + if (pct < 0) return s; + + string status = ReadValue(dir, "status") ?? "Unknown"; + + s.Present = true; + s.Percent = Math.Max(0, Math.Min(100, pct)); + s.Charging = string.Equals(status, "Charging", StringComparison.OrdinalIgnoreCase); + s.Full = string.Equals(status, "Full", StringComparison.OrdinalIgnoreCase); + return s; + } + + private static string FindBattery() + { + if (_path != null && Directory.Exists(_path)) return _path; + _path = null; + + if (!Directory.Exists(Root)) return null; + + try + { + foreach (string dir in Directory.GetDirectories(Root)) + { + if (!string.Equals(ReadValue(dir, "type"), "Battery", + StringComparison.OrdinalIgnoreCase)) continue; + + // "scope" is Device on peripherals (stylus, BT mouse) — skip those + string scope = ReadValue(dir, "scope"); + if (scope != null && !string.Equals(scope, "System", + StringComparison.OrdinalIgnoreCase)) continue; + + if (ReadPercent(dir) < 0) continue; + + _path = dir; + break; + } + } + catch (Exception ex) + { + Console.WriteLine("Battery scan failed: " + ex.Message); + } + + return _path; + } + + private static int ReadPercent(string dir) + { + long pct = ParseLong(ReadValue(dir, "capacity")); + if (pct >= 0) return (int)pct; + + // some firmwares expose energy/charge only + long now = ParseLong(ReadValue(dir, "energy_now")); + long full = ParseLong(ReadValue(dir, "energy_full")); + if (now < 0 || full <= 0) + { + now = ParseLong(ReadValue(dir, "charge_now")); + full = ParseLong(ReadValue(dir, "charge_full")); + } + if (now >= 0 && full > 0) return (int)Math.Round(now * 100.0 / full); + + return -1; + } + + private static string ReadValue(string dir, string file) + { + try + { + string p = Path.Combine(dir, file); + return File.Exists(p) ? File.ReadAllText(p).Trim() : null; + } + catch { return null; } + } + + private static long ParseLong(string s) + { + long v; + if (s != null && long.TryParse(s, NumberStyles.Integer, + CultureInfo.InvariantCulture, out v)) return v; + return -1; + } +} \ No newline at end of file diff --git a/program/MainWindow.cs b/program/MainWindow.cs index b7828c5..d7c98a1 100644 --- a/program/MainWindow.cs +++ b/program/MainWindow.cs @@ -29,6 +29,9 @@ [UI] private Image ConnectionIcon = null; // was GtkIconView in the old glade [UI] private Label ConnectionAlertLabel = null; + [UI] private Image BatteryIcon = null; + [UI] private Label BatteryLabel = null; + private uint _retryTimerId; private bool _probing; @@ -55,6 +58,9 @@ internal SoundWindow _soundWindow; internal MapWindow _mapWindow; + // Battery + private uint _batteryTimerId; + private const uint BatteryPollMs = 10000; public MainWindow() : this(CreateBuilder()) { } @@ -128,6 +134,8 @@ _Language.Register("LanguageLabel", LanguageLabel); _Language.Register("LanguageToggle", LanguageToggle); _Language.RegisterDynamic("ConnectionIcon", ConnectionIcon); + _Language.RegisterDynamic("BatteryIcon", BatteryIcon); + _Language.RegisterDynamic("BatteryLabel", BatteryLabel); ConnectionAlertLabel.Name = "ConnectionAlertLabel"; _Language.Register("ConnectionAlertLabel", ConnectionAlertLabel); @@ -137,9 +145,15 @@ _mapWindow = new MapWindow(builder, _arduino, _Language); _mapWindow.BackRequested += OnMapClosed; - _Language.Changed += (s, e) => OnArduinoConnectionChanged(_connected); + _Language.Changed += (s, e) => { + OnArduinoConnectionChanged(_connected); + UpdateBattery(); + }; _Language.Apply(LanguageToggle.Active); + UpdateBattery(); + _batteryTimerId = GLib.Timeout.Add(BatteryPollMs, OnBatteryTick); + // // single source of truth: the toggle drives both pages // ApplyLanguageEverywhere(LanguageToggle.Active); @@ -270,6 +284,69 @@ }); return true; } + private bool OnBatteryTick() + { + UpdateBattery(); + return true; // keep polling + } + + private void UpdateBattery() + { + if (BatteryIcon == null) return; + + BatteryStatus b = BatteryMonitor.Read(); + + if (!b.Present) // desktop / dev machine — hide it entirely + { + BatteryIcon.Visible = false; + if (BatteryLabel != null) BatteryLabel.Visible = false; + return; + } + + BatteryIcon.Visible = true; + BatteryIcon.SetFromIconName(PickBatteryIcon(b), IconSize.Dnd); + + string key = b.Full ? "BatteryIcon.Full" + : b.Charging ? "BatteryIcon.Charging" + : "BatteryIcon.Discharging"; + BatteryIcon.TooltipText = _Language.Format(key, b.Percent); + + if (BatteryLabel != null) + { + BatteryLabel.Visible = true; + BatteryLabel.Text = b.Percent.ToString(CultureInfo.InvariantCulture) + "%"; + + var ctx = BatteryLabel.StyleContext; + if (b.Percent <= 15 && !b.Charging) ctx.AddClass("battery-low"); + else ctx.RemoveClass("battery-low"); + } + } + + private static string PickBatteryIcon(BatteryStatus b) + { + string level; + if (b.Percent >= 80) level = "full"; + else if (b.Percent >= 50) level = "good"; + else if (b.Percent >= 20) level = "low"; + else if (b.Percent >= 10) level = "caution"; + else level = "empty"; + + string name = b.Full ? "battery-full-charged" + : b.Charging ? "battery-" + level + "-charging" + : "battery-" + level; + + return FirstAvailableIcon(name + "-symbolic", name, "battery"); + } + + private static string FirstAvailableIcon(params string[] names) + { + IconTheme theme = IconTheme.Default; + if (theme != null) + foreach (string n in names) + if (theme.HasIcon(n)) return n; + + return names[names.Length - 1]; + } // ---------- localisation ---------- diff --git a/program/altest.cs b/program/altest.cs deleted file mode 100644 index 8b812af..0000000 --- a/program/altest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Threading; -using OpenTK.Audio; -using OpenTK.Audio.OpenAL; - -class AlTest -{ - static void Main(string[] args) - { - if (args.Length < 1) - { - Console.WriteLine("usage: altest [gain 0..1]"); - return; - } - float gain = args.Length > 1 ? float.Parse(args[1]) : 1.0f; - - var ctx = new AudioContext(); - ctx.MakeCurrent(); - Console.WriteLine("device: " + AudioContext.CurrentContext.CurrentDevice); - - WavFile w = WavFile.Load(args[0]); - Console.WriteLine("{0} {1} Hz {2} bytes", w.Format, w.SampleRate, w.Data.Length); - - int buf = AL.GenBuffer(); - AL.BufferData(buf, w.Format, w.Data, w.Data.Length, w.SampleRate); - - int src = AL.GenSource(); - AL.Source(src, ALSourcei.Buffer, buf); - AL.Source(src, ALSourcef.Gain, gain); - AL.SourcePlay(src); - - ALError e = AL.GetError(); - if (e != ALError.NoError) Console.WriteLine("AL error: " + AL.GetErrorString(e)); - - while (AL.GetSourceState(src) == ALSourceState.Playing) - Thread.Sleep(50); - - AL.DeleteSource(src); - AL.DeleteBuffer(buf); - ctx.Dispose(); - } -} \ No newline at end of file diff --git a/program/homepageallv7.glade b/program/homepageallv7.glade index 0d81a6d..5fafdb1 100644 --- a/program/homepageallv7.glade +++ b/program/homepageallv7.glade @@ -73,6 +73,47 @@ 2 + + + True + False + horizontal + 4 + center + + + True + False + battery-missing + 6 + center + + + False + False + 0 + + + + + True + False + center + --% + + + False + False + 1 + + + + + False + False + 3 + + 0 diff --git a/program/info_eng.csv b/program/info_eng.csv index 02440d3..c0094d3 100644 --- a/program/info_eng.csv +++ b/program/info_eng.csv @@ -26,4 +26,7 @@ Log.EspNowDisconnected,[ESP-NOW] Peer disconnected SoundBackButton,Back MapBackButton,Back -ConnectionAlertLabel,Problem connecting with device \ No newline at end of file +ConnectionAlertLabel,Problem connecting with device +BatteryIcon.Charging,Battery {0}% — charging +BatteryIcon.Discharging,Battery {0}% +BatteryIcon.Full,Battery fully charged \ No newline at end of file diff --git a/program/info_jpn.csv b/program/info_jpn.csv index 23de4cb..c3c5735 100644 --- a/program/info_jpn.csv +++ b/program/info_jpn.csv @@ -26,4 +26,7 @@ Log.EspNowDisconnected,[ESP-NOW] 相手が切断しました SoundBackButton,戻る MapBackButton,戻る -ConnectionAlertLabel,デバイスに接続できません \ No newline at end of file +ConnectionAlertLabel,デバイスに接続できません +BatteryIcon.Charging,バッテリー {0}% — 充電中 +BatteryIcon.Discharging,バッテリー {0}% +BatteryIcon.Full,バッテリー 満充電 \ No newline at end of file