From 1160477945b2e536ea4d531f0708847c7aa21889 Mon Sep 17 00:00:00 2001 From: MuseCat Date: Thu, 25 Jun 2026 12:19:29 +0300 Subject: [PATCH] initial commit --- .gitignore | 5 + RTCSync.cli/Models/TimeModel.cs | 32 +++++ RTCSync.cli/Models/TimeModelBI.cs | 54 +++++++++ RTCSync.cli/Options/IOption.cs | 15 +++ RTCSync.cli/Options/OptionDispatcher.cs | 145 +++++++++++++++++++++++ RTCSync.cli/Options/ReadStatusOption.cs | 25 ++++ RTCSync.cli/Options/ReadTimeOption.cs | 30 +++++ RTCSync.cli/Options/SetTimeOption.cs | 98 +++++++++++++++ RTCSync.cli/Options/SyncTimeOption.cs | 36 ++++++ RTCSync.cli/Program.cs | 14 +++ RTCSync.cli/RTCSync.cli.csproj | 18 +++ RTCSync.cli/Services/DeviceDispatcher.cs | 33 ++++++ RTCSync.cli/Services/DeviceReaders.cs | 96 +++++++++++++++ RTCSync.cli/Services/DeviceWriters.cs | 47 ++++++++ RTCSync.cli/Utils/BinaryUtils.cs | 7 ++ RTCSync.cli/Utils/BitIndex.cs | 67 +++++++++++ RTCSync.cli/Utils/I2CUtils.cs | 134 +++++++++++++++++++++ RTCSync.cli/Utils/WindowsTimeUtils.cs | 26 ++++ RTCSync.sln | 16 +++ 19 files changed, 898 insertions(+) create mode 100644 .gitignore create mode 100644 RTCSync.cli/Models/TimeModel.cs create mode 100644 RTCSync.cli/Models/TimeModelBI.cs create mode 100644 RTCSync.cli/Options/IOption.cs create mode 100644 RTCSync.cli/Options/OptionDispatcher.cs create mode 100644 RTCSync.cli/Options/ReadStatusOption.cs create mode 100644 RTCSync.cli/Options/ReadTimeOption.cs create mode 100644 RTCSync.cli/Options/SetTimeOption.cs create mode 100644 RTCSync.cli/Options/SyncTimeOption.cs create mode 100644 RTCSync.cli/Program.cs create mode 100644 RTCSync.cli/RTCSync.cli.csproj create mode 100644 RTCSync.cli/Services/DeviceDispatcher.cs create mode 100644 RTCSync.cli/Services/DeviceReaders.cs create mode 100644 RTCSync.cli/Services/DeviceWriters.cs create mode 100644 RTCSync.cli/Utils/BinaryUtils.cs create mode 100644 RTCSync.cli/Utils/BitIndex.cs create mode 100644 RTCSync.cli/Utils/I2CUtils.cs create mode 100644 RTCSync.cli/Utils/WindowsTimeUtils.cs create mode 100644 RTCSync.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/RTCSync.cli/Models/TimeModel.cs b/RTCSync.cli/Models/TimeModel.cs new file mode 100644 index 0000000..6064ace --- /dev/null +++ b/RTCSync.cli/Models/TimeModel.cs @@ -0,0 +1,32 @@ +using RTCSync.Utils; + +namespace RTCSync.Models; + +public class TimeModel(ReadOnlySpan rtcBytes) +{ + private static int CalculateHours(ReadOnlySpan rtcBytes) + { + var hours = 0; + if (TimeModelBI.TwentyHourBI.Take(rtcBytes) == 1) + { + hours = 20; + } + else if (TimeModelBI.TenHourBI.Take(rtcBytes) == 1) + { + hours = 10; + } + + hours += TimeModelBI.HourBI.Take(rtcBytes); + return hours; + } + + public DateTime Time = new DateTime( + 2000 + TimeModelBI.TenYearBI.Take(rtcBytes) * 10 + TimeModelBI.YearBI.Take(rtcBytes), + TimeModelBI.TenMonthBI.Take(rtcBytes) * 10 + TimeModelBI.MonthBI.Take(rtcBytes), + TimeModelBI.TenDateBI.Take(rtcBytes) * 10 + TimeModelBI.DateBI.Take(rtcBytes), + CalculateHours(rtcBytes), + TimeModelBI.TenMinutesBI.Take(rtcBytes) * 10 + TimeModelBI.MinutesBI.Take(rtcBytes), + TimeModelBI.TenSecondsBI.Take(rtcBytes) * 10 + TimeModelBI.SecondsBI.Take(rtcBytes), + DateTimeKind.Local + ); +} \ No newline at end of file diff --git a/RTCSync.cli/Models/TimeModelBI.cs b/RTCSync.cli/Models/TimeModelBI.cs new file mode 100644 index 0000000..a2aafc2 --- /dev/null +++ b/RTCSync.cli/Models/TimeModelBI.cs @@ -0,0 +1,54 @@ +using RTCSync.Utils; + +namespace RTCSync.Models; + +public class TimeModelBI +{ + // 6 5 4 | 3 2 1 0 + // 10 Seconds | Seconds + // 00 - 59 + public static BitIndex[] SecondsBI { get; } = [new(1, 0..3)]; + public static BitIndex[] TenSecondsBI { get; } = [new(1, 4..6)]; + + // 6 5 4 | 3 2 1 0 + // 10 Minutes | Minutes + // 00 - 59 + public static BitIndex[] MinutesBI { get; } = [new(2, 0..3)]; + public static BitIndex[] TenMinutesBI { get; } = [new(2, 4..6)]; + + // 6 | 5 | 4 | 3 2 1 0 + // 12/24 | 20 hour | 10 hour | Hour + // 1-12 + AM/PM + // 00 - 23 + public static BitIndex[] HourBI { get; } = [new(3, 0..3)]; + public static BitIndex[] TenHourBI { get; } = [new(3, 4..4)]; + public static BitIndex[] TwentyHourBI { get; } = [new(3, 5..5)]; + // When high - 12 hour format is selected + public static BitIndex[] HoursFormatBI { get; } = [new(3, 6..6)]; + + // 2 1 0 + // Day + // 1 - 7 + // 1 = Saturday, 2 = Monday + public static BitIndex[] DayOfWeekBI { get; } = [new(4, 0..3)]; + + // 5 4 | 3 2 1 0 + // 10 Date | Date + // 01 - 31 + public static BitIndex[] DateBI { get; } = [new(5, 0..4)]; + public static BitIndex[] TenDateBI { get; } = [new(5, 4..5)]; + + // 7 | 4 | 3 2 1 0 + // Century | 10 Month | Month + // 01 - 12 + Century + public static BitIndex[] MonthBI { get; } = [new(6, 0..3)]; + public static BitIndex[] TenMonthBI { get; } = [new(6, 4..4)]; + // High when the years register overflows from 99 to 00 + public static BitIndex[] CenturyBI { get; } = [new(6, 7..7)]; + + // 7 6 5 4 | 3 2 1 0 + // 10 Year | Year + // 0-99 + public static BitIndex[] YearBI { get; } = [new(7, 0..3)]; + public static BitIndex[] TenYearBI { get; } = [new(7, 4..7)]; +} \ No newline at end of file diff --git a/RTCSync.cli/Options/IOption.cs b/RTCSync.cli/Options/IOption.cs new file mode 100644 index 0000000..bce4235 --- /dev/null +++ b/RTCSync.cli/Options/IOption.cs @@ -0,0 +1,15 @@ +namespace RTCSync.Options; + +public interface IOption +{ + string Description { get; } + List OptionNames { get; } + string OptionValues { get; } + void Execute(OptionArgs args); +} + +public class OptionArgs +{ + public Dictionary OptionValues { get; set; } + public List Arguments { get; set; } = []; +} \ No newline at end of file diff --git a/RTCSync.cli/Options/OptionDispatcher.cs b/RTCSync.cli/Options/OptionDispatcher.cs new file mode 100644 index 0000000..ddcf034 --- /dev/null +++ b/RTCSync.cli/Options/OptionDispatcher.cs @@ -0,0 +1,145 @@ +namespace RTCSync.Options; + +public class OptionDispatcher +{ + private readonly Dictionary _registredOptions = new(); + + public void Register(IOption option) + { + foreach (var optionName in option.OptionNames) + { + _registredOptions[optionName] = option; + } + } + + public void Execute(string[] args) + { + if (args.Length == 0) + { + PrintHelp(); + return; + } + + var flag = args[0]; + + if (!_registredOptions.TryGetValue(flag, out var command)) + { + Console.WriteLine($"Неизвестный параметр: {flag}"); + PrintHelp(); + return; + } + + // Парсим все остальные аргументы как опции + var commandArgs = ParseArguments(args.Skip(1).ToArray()); + command.Execute(commandArgs); + } + + private static OptionArgs ParseArguments(string[] args) + { + var optionValues = new Dictionary(); + + for (var i = 0; i < args.Length; i++) + { + if (!args[i].StartsWith("--")) + { + optionValues.Add("unnamed", args[i]); + continue; + } + optionValues[args[i]] = args[i + 1]; + i++; + } + + return new OptionArgs + { + OptionValues = optionValues, + Arguments = [] + }; + } + + private static void PrintOption(string optionLine, string description) + { + var terminalWidth = Console.WindowWidth; + // Console.WriteLine(terminalWidth); + // for (var i = 0; i < terminalWidth; i++) + // Console.Write("_"); + // Console.WriteLine(); + const int columnWidth = 44; + var descriptionWidth = terminalWidth - columnWidth; + + // Если название опции слишком длиное, перенести описание на следующую строку + if (optionLine.Length > columnWidth) + { + Console.WriteLine(optionLine); + var lines = SplitText(description, descriptionWidth); + foreach (var line in lines) + { + Console.WriteLine($"{new string(' ', 2)}{line}"); + } + return; + } + + var formatted = optionLine.PadRight(columnWidth); + var descLines = SplitText(description, descriptionWidth); + + // Первая строка с опцией и началом описания + Console.WriteLine($"{formatted}{descLines[0]}"); + + // Остальные строки описания с выравниванием + for (var i = 1; i < descLines.Count; i++) + { + Console.WriteLine($"{new string(' ', columnWidth)}{descLines[i]}"); + } + Console.WriteLine(); + } + + private static List SplitText(string text, int maxWidth) + { + var lines = new List(); + + if (maxWidth < 1) return [text]; + + var words = text.Split(' '); + var current = ""; + + foreach (var word in words) + { + if ((current + word + " ").Length <= maxWidth) + { + current += word + " "; + } + else + { + if (!string.IsNullOrEmpty(current)) + lines.Add(current.TrimEnd()); + current = word + " "; + } + } + + if (!string.IsNullOrEmpty(current)) + lines.Add(current.TrimEnd()); + + return lines.Count > 0 ? lines : [text]; + } + + private void PrintHelp() + { + Console.WriteLine("RTCSync - утилита для работы с часами реального времени DS3231, подключенными по шине I^2C через CH431. Требует winusb драйвер.\n"); + Console.WriteLine("Использование: RTCSync.cli.exe <параметр> [опции]\n"); + Console.WriteLine("Параметры:"); + + foreach (var cmd in _registredOptions.Values.Distinct()) + { + var optionNames = cmd.OptionNames.Aggregate((current, next) => current + ", " + next); + var optionLine = $" {optionNames} {cmd.OptionValues}"; + PrintOption(optionLine, cmd.Description); + + // Console.WriteLine($" {cmd.OptionNames.Aggregate((current, next) => current + $" {cmd.OptionValues}, " + next + $" {cmd.OptionValues}"), -54} {cmd.Description}"); + } + + Console.WriteLine("\nПримеры:"); + Console.WriteLine(" RTCSync.cli.exe --set-time"); + Console.WriteLine(" RTCSync.cli.exe -s"); + Console.WriteLine(" RTCSync.cli.exe --set-time 0"); + Console.WriteLine(" RTCSync.cli.exe -s 0"); + } +} diff --git a/RTCSync.cli/Options/ReadStatusOption.cs b/RTCSync.cli/Options/ReadStatusOption.cs new file mode 100644 index 0000000..29e12c9 --- /dev/null +++ b/RTCSync.cli/Options/ReadStatusOption.cs @@ -0,0 +1,25 @@ +using RTCSync.Services; +using RTCSync.Utils; + +namespace RTCSync.Options; + +public class ReadStatusOption : IOption +{ + public string Description => + "Чтение регистров status, control и температуры с часов реального времени."; + public List OptionNames => ["-i", "--read-status"]; + + public string OptionValues => ""; + + public void Execute(OptionArgs args) + { + // bind and init CH431 + var device = DeviceDispatcher.SetUpDevice(); + if (device == null) + return; + DeviceReaders.PrintControlValues(device); + DeviceReaders.PrintStatusValues(device); + DeviceReaders.PrintTemperatureValue(device); + + } +} \ No newline at end of file diff --git a/RTCSync.cli/Options/ReadTimeOption.cs b/RTCSync.cli/Options/ReadTimeOption.cs new file mode 100644 index 0000000..f954c26 --- /dev/null +++ b/RTCSync.cli/Options/ReadTimeOption.cs @@ -0,0 +1,30 @@ +using RTCSync.Models; +using RTCSync.Services; +using RTCSync.Utils; + +namespace RTCSync.Options; + +public class ReadTimeOption : IOption +{ + public string Description => + "Чтение времени с часов реального времени."; + + public List OptionNames => ["-r", "--read-time"]; + + public string OptionValues => ""; + + public void Execute(OptionArgs args) + { + // bind and init CH431 + var device = DeviceDispatcher.SetUpDevice(); + if (device == null) + return; + while (true) + { + DeviceReaders.PrintTimeRTC(device); + Thread.Sleep(500); + } + + device.ReleaseInterface(0); + } +} diff --git a/RTCSync.cli/Options/SetTimeOption.cs b/RTCSync.cli/Options/SetTimeOption.cs new file mode 100644 index 0000000..828b49b --- /dev/null +++ b/RTCSync.cli/Options/SetTimeOption.cs @@ -0,0 +1,98 @@ +using System.Runtime.InteropServices.ComTypes; +using RTCSync.Models; +using RTCSync.Services; +using RTCSync.Utils; +using static RTCSync.Utils.BinaryUtils; + +namespace RTCSync.Options; + +public class SetTimeOption : IOption +{ + public string Description => + "Установка времени как в системе, на часах реального времени. Часы будут хранить локальное время (не UTC!). Можно сбросить время до минимального значения используя опцию \"0\"."; + + public List OptionNames => ["-s", "--set-time"]; + + public string OptionValues => "[0]"; + + + public void Execute(OptionArgs args) + { + // Console.WriteLine("Вы уверены, что хотите перенастроить время на модуле реального времени? (Д/н)\n"); + // var isConfirmed = false; + // while (!isConfirmed) + // { + // var proof = Console.ReadLine(); + // switch (proof) + // { + // case "Н" or "н" or "N" or "n": + // return; + // case "Д" or "д" or "Y" or "y": + // isConfirmed = true; + // break; + // } + // } + + var device = DeviceDispatcher.SetUpDevice(); + if (device == null) + return; + + // ставим текущее время в RTC модуль. Записывается не с первого раза, поэтому хак + // важно каждый раз пытаться забить актуальное время + // TODO: убрать хак + if (args.OptionValues.TryGetValue("unnamed", out var value) && value is "0") + { + for (var i = 0; i < 3; i++) + { + var dt = DateTime.MinValue; + DeviceWriters.SetTime(device, dt); + } + } + else + { + for (var i = 0; i < 3; i++) + { + // FIXME: вызывает падение на windows 7 + var dt = DateTime.Now; + DeviceWriters.SetTime(device, dt); + } + } + + Console.WriteLine("Время установлено"); + + + // читаем статус + var status = DeviceReaders.GetStatusRegister(device); + DeviceReaders.PrintStatusValues(status); + + // Сбрасываем OSF (Oscillator Stop Flags) бит 7 регистра 0x0F + // Иначе часы будут считаться ненадёжными + Console.WriteLine("Сбрасываем OSF"); + DeviceWriters.ResetOSF(device, status); + + // заново выводим статус для визуального контроля + DeviceReaders.PrintStatusValues(device); + + + // читаем данные контроля + var control = DeviceReaders.GetControlRegister(device); + DeviceReaders.PrintControlValues(control); + + // если осциллятор не включен, то включаем + if ((control[0] & 0x80) != 0) + { + Console.WriteLine("Включаем осциллятор"); + DeviceWriters.EnableEOSC(device, control); + // заново выводим данные контроля для визуального контроля + DeviceReaders.PrintControlValues(device); + } + + + // все сделали - выводим время + while (true) + { + DeviceReaders.PrintTimeRTC(device); + Thread.Sleep(500); + } + } +} \ No newline at end of file diff --git a/RTCSync.cli/Options/SyncTimeOption.cs b/RTCSync.cli/Options/SyncTimeOption.cs new file mode 100644 index 0000000..a3d21c5 --- /dev/null +++ b/RTCSync.cli/Options/SyncTimeOption.cs @@ -0,0 +1,36 @@ +using RTCSync.Services; +using RTCSync.Utils; + +namespace RTCSync.Options; + +public class SyncTimeOption : IOption +{ + public string Description => "Синхронизирует системное время со временем модуля реального времени. Если часы хранят UTC время, то используйте опцию universal"; + public List OptionNames => ["-S", "--sync-time"]; + public string OptionValues => "{local, l, universal, u}"; + + public void Execute(OptionArgs args) + { + var device = DeviceDispatcher.SetUpDevice(); + if (device == null) + return; + + Console.WriteLine("Время на модуле:"); + DeviceReaders.PrintTimeRTC(device); + var timeModel = DeviceReaders.GetTimeRTC(device); + + // время в формате понятном для kernel32 + WindowsTimeUtils.SystemTime systime; + + if (args.OptionValues.TryGetValue("unnamed", out var value) && value is "universal" or "u") + systime = new WindowsTimeUtils.SystemTime(timeModel.Time); + else if (value is "local" or "l") + systime = new WindowsTimeUtils.SystemTime(timeModel.Time.ToUniversalTime()); + else + systime = new WindowsTimeUtils.SystemTime(timeModel.Time.ToUniversalTime()); + + Console.WriteLine(WindowsTimeUtils.SetSystemTime(ref systime) + ? "Время установлено в системе" + : "Ошибка: нет прав администратора"); + } +} \ No newline at end of file diff --git a/RTCSync.cli/Program.cs b/RTCSync.cli/Program.cs new file mode 100644 index 0000000..10c896b --- /dev/null +++ b/RTCSync.cli/Program.cs @@ -0,0 +1,14 @@ +using RTCSync.Options; + +internal class Program +{ + static void Main(string[] args) + { + var dispatcher = new OptionDispatcher(); + dispatcher.Register(new ReadTimeOption()); + dispatcher.Register(new SetTimeOption()); + dispatcher.Register(new SyncTimeOption()); + dispatcher.Register(new ReadStatusOption()); + dispatcher.Execute(args); + } +} \ No newline at end of file diff --git a/RTCSync.cli/RTCSync.cli.csproj b/RTCSync.cli/RTCSync.cli.csproj new file mode 100644 index 0000000..609d0d9 --- /dev/null +++ b/RTCSync.cli/RTCSync.cli.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0 + latestmajor + enable + enable + true + true + RTCSync + + + + + + + diff --git a/RTCSync.cli/Services/DeviceDispatcher.cs b/RTCSync.cli/Services/DeviceDispatcher.cs new file mode 100644 index 0000000..ecd9a82 --- /dev/null +++ b/RTCSync.cli/Services/DeviceDispatcher.cs @@ -0,0 +1,33 @@ +using LibUsbDotNet; +using LibUsbDotNet.LibUsb; +using RTCSync.Utils; + +namespace RTCSync.Services; + +public abstract class DeviceDispatcher +{ + public static IUsbDevice? SetUpDevice() + { + var context = new UsbContext(); + context.SetDebugLevel(LogLevel.Info); // уровень логгирования winusb + + var list = context.List(); + Console.WriteLine($"Всего USB устройств: {list.Count}"); + + foreach (var d in list) + Console.WriteLine($" VID={d.VendorId:X4} PID={d.ProductId:X4}"); + + var device = list.FirstOrDefault(d => d.VendorId == 0x1A86 && d.ProductId == 0x5512); + if (device == null) + { + Console.WriteLine("Не найдено"); + return null; + } + Console.WriteLine("Найдено, открываем..."); + device.Open(); + device.ClaimInterface(0); + Console.WriteLine("Открыто успешно"); + I2CUtils.CH341Init(device); + return device; + } +} \ No newline at end of file diff --git a/RTCSync.cli/Services/DeviceReaders.cs b/RTCSync.cli/Services/DeviceReaders.cs new file mode 100644 index 0000000..26a3225 --- /dev/null +++ b/RTCSync.cli/Services/DeviceReaders.cs @@ -0,0 +1,96 @@ +using LibUsbDotNet.LibUsb; +using RTCSync.Models; +using RTCSync.Utils; + +namespace RTCSync.Services; + +public class DeviceReaders +{ + public static TimeModel GetTimeRTC(IUsbDevice device) + { + // Читаем все 7 байт времени начиная с регистра 0x00 + var time = I2CUtils.ReadWithRetry(device, 0x68, 0x00, 8); + var timeModel = new TimeModel(time); + return timeModel; + } + + public static void PrintTimeRTC(IUsbDevice device) + { + var timeModel = GetTimeRTC(device); + PrintTimeRTC(timeModel); + } + + public static void PrintTimeRTC(TimeModel timeModel) + { + Console.WriteLine(timeModel.Time.ToString("HH:mm:ss dd.MM.yyyy")); + } + + + public static byte[] GetStatusRegister(IUsbDevice device) + { + return I2CUtils.ReadWithRetry(device, 0x68, 0x0F, 1); + } + + public static void PrintStatusValues(IUsbDevice device) + { + var status = GetStatusRegister(device); + PrintStatusValues(status); + } + + public static void PrintStatusValues(byte[] status) + { + var statusBits = string.Concat(status.Select(b => Convert.ToString(b, 2).PadLeft(8, '0'))); + Console.WriteLine("Status Register (0Fh):"); + Console.WriteLine("| OSF | 0 | 0 | 0 | EN32kHZ | BSY | A2F | A1F |"); + Console.WriteLine($"| {statusBits[0], -3} | {statusBits[1]} | {statusBits[2]} | {statusBits[3]} | {statusBits[4], -7} | {statusBits[5], -3} | {statusBits[6], -3} | {statusBits[7], -3} |"); + Console.WriteLine(); + } + + + public static byte[] GetControlRegister(IUsbDevice device) + { + return I2CUtils.ReadWithRetry(device, 0x68, 0x0E, 1); + } + + public static void PrintControlValues(IUsbDevice device) + { + var control= GetControlRegister(device); + PrintControlValues(control); + } + + public static void PrintControlValues(byte[] control) + { + var controlBits = string.Concat(control.Select(b => Convert.ToString(b, 2).PadLeft(8, '0'))); + Console.WriteLine("Control Register (0Eh):"); + Console.WriteLine("| EOSC | BBSQW | CONV | RS2 | RS1 | INTCN | A2IE | A1IE |"); + Console.WriteLine($"| {controlBits[0], -4} | {controlBits[1], -5} | {controlBits[2], -4} | {controlBits[3], -3} | {controlBits[4], -3} | {controlBits[5], -5} | {controlBits[6], -4} | {controlBits[7], -4} |"); + Console.WriteLine(); + } + + + public static byte[] GetTemperatureRegisters(IUsbDevice device) + { + return I2CUtils.ReadWithRetry(device, 0x68, 0x11, 2); + } + public static void PrintTemperatureValue(IUsbDevice device) + { + var temp = GetTemperatureRegisters(device); + PrintTemperatureValue(temp); + } + public static void PrintTemperatureValue(byte[] temp) + { + float tempValue = temp[0]; + if (temp[1] == 0x40) + tempValue += 0.25F; + else if (temp[1] == 0x80) + tempValue += 0.5F; + else if (temp[1] == 0xC0) + tempValue += 0.75F; + + Console.WriteLine("Температура:"); + Console.WriteLine(tempValue); + Console.WriteLine(); + } + + +} \ No newline at end of file diff --git a/RTCSync.cli/Services/DeviceWriters.cs b/RTCSync.cli/Services/DeviceWriters.cs new file mode 100644 index 0000000..d2d481b --- /dev/null +++ b/RTCSync.cli/Services/DeviceWriters.cs @@ -0,0 +1,47 @@ +using LibUsbDotNet.LibUsb; +using RTCSync.Utils; +using static RTCSync.Utils.BinaryUtils; + + +namespace RTCSync.Services; + +public class DeviceWriters +{ + public static void SetTime(IUsbDevice device, DateTime dt) + { + var data = new byte[] + { + ToBcd(dt.Second), // 0x00 секунды + ToBcd(dt.Minute), // 0x01 минуты + ToBcd(dt.Hour), // 0x02 часы, бит6=0 → 24h + (byte)(dt.DayOfWeek + 1), // 0x03 день недели (в dt 0=вс, 1=пн..., а в DS3231 на 1 больше) + ToBcd(dt.Day), // 0x04 число + ToBcd(dt.Month), // 0x05 месяц + ToBcd(dt.Year % 100) // 0x06 год (последние 2 цифры) + }; + + I2CUtils.Write(device, 0x68, 0x00, data); + } + + public static void ResetOSF(IUsbDevice device, byte[]? status) + { + status ??= DeviceReaders.GetStatusRegister(device); + status[0] &= 0x7F; // очистить бит 7 + // TODO: убрать хак + I2CUtils.Write(device, 0x68, 0x0F, status); + I2CUtils.Write(device, 0x68, 0x0F, status); + I2CUtils.Write(device, 0x68, 0x0F, status); + } + + public static void EnableEOSC(IUsbDevice device, byte[]? control) + { + control ??= DeviceReaders.GetControlRegister(device); + control[0] &= 0x7F; // EOSC = 0 (осциллятор включён) — бит 7 регистра 0x0E + // TODO: убрать хак + I2CUtils.Write(device, 0x68, 0x0E, control); + I2CUtils.Write(device, 0x68, 0x0E, control); + I2CUtils.Write(device, 0x68, 0x0E, control); + } + + +} \ No newline at end of file diff --git a/RTCSync.cli/Utils/BinaryUtils.cs b/RTCSync.cli/Utils/BinaryUtils.cs new file mode 100644 index 0000000..d094958 --- /dev/null +++ b/RTCSync.cli/Utils/BinaryUtils.cs @@ -0,0 +1,7 @@ +namespace RTCSync.Utils; + +public class BinaryUtils +{ + public static byte ToBcd(int value) => (byte)(((value / 10) << 4) | (value % 10)); + +} \ No newline at end of file diff --git a/RTCSync.cli/Utils/BitIndex.cs b/RTCSync.cli/Utils/BitIndex.cs new file mode 100644 index 0000000..2074c4f --- /dev/null +++ b/RTCSync.cli/Utils/BitIndex.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics; + +namespace RTCSync.Utils; + +public readonly struct BitIndex +{ + private readonly ushort _byteIdx; + private readonly byte _startBitIdx0; + private readonly byte _endBitIdx0; + + // порядок указывания байт в структуре + // сначала старший, потом младше + // 1<-------------------------------41 + // порядок указывания бит в структуре + // сначала младший, потом старше + // 7<------0 + // таким образом все биты вписываются по порядку справа налево + // BitIndex[new(3, 0..7), new(2, 0..7), new(1, 0..6)] + public BitIndex(int byteIdx1, Range bits) + { + _byteIdx = (ushort)(byteIdx1 - 1); + Debug.Assert(!bits.Start.IsFromEnd); + Debug.Assert(!bits.End.IsFromEnd); + _startBitIdx0 = (byte)bits.Start.Value; + _endBitIdx0 = (byte)bits.End.Value; + } + + public int Length => _endBitIdx0 - _startBitIdx0 + 1; + + public int Take(ReadOnlySpan bytes) + { + // example: Len=3, 0b00001000 - 1 = 0b00000111 + var mask = (1U << (_endBitIdx0 - _startBitIdx0 + 1)) - 1U; + return (int)((bytes[_byteIdx] >> _startBitIdx0) & mask); + } + + public static int Take(BitIndex[] bis, ReadOnlySpan bytes) + { + var shift = 0; + var result = 0; + foreach (var bi in bis) + { + result |= (bi.Take(bytes) << shift); + shift += bi.Length; + } + + return result; + } + +} + +public static class BitIndexV2Extensions +{ + public static int Take(this BitIndex[] bis, ReadOnlySpan bytes) + { + var shift = 0; + var result = 0; + foreach (var bi in bis) + { + result |= (bi.Take(bytes) << shift); + shift += bi.Length; + } + + return result; + } +} \ No newline at end of file diff --git a/RTCSync.cli/Utils/I2CUtils.cs b/RTCSync.cli/Utils/I2CUtils.cs new file mode 100644 index 0000000..621ef5e --- /dev/null +++ b/RTCSync.cli/Utils/I2CUtils.cs @@ -0,0 +1,134 @@ +using LibUsbDotNet.LibUsb; +using LibUsbDotNet.Main; + +namespace RTCSync.Utils; + +public static class I2CUtils +{ + private const int VID = 0x1A86; + private const int PID = 0x5512; // CH341T + + // Команды CH341 для I2C + private const byte CH341_CMD_I2C_STREAM = 0xAA; + private const byte CH341_CMD_I2C_STM_STA = 0x74; // START + private const byte CH341_CMD_I2C_STM_STO = 0x75; // STOP + private const byte CH341_CMD_I2C_STM_OUT = 0x80; // write byte (len в младших битах) + private const byte CH341_CMD_I2C_STM_IN = 0xC0; // read byte + private const byte CH341_CMD_I2C_STM_END = 0x00; // конец стрима + + public static void ScanDevice(IUsbDevice device) + { + var writer = device.OpenEndpointWriter(WriteEndpointID.Ep02); + var reader = device.OpenEndpointReader(ReadEndpointID.Ep02); + + Console.WriteLine("Сканирование I2C шины (0x03 - 0x77)..."); + + for (byte addr = 0x03; addr < 0x78; addr++) + { + // Пробуем послать START + адрес + STOP + // Если устройство есть — придёт ACK, если нет — NACK + var cmd = new byte[] + { + CH341_CMD_I2C_STREAM, // CH341_CMD_I2C_STREAM + CH341_CMD_I2C_STM_STA, // START + CH341_CMD_I2C_STM_OUT | 1, // OUT, 1 байт + (byte)(addr << 1), // адрес + write bit + CH341_CMD_I2C_STM_STO, // STOP + CH341_CMD_I2C_STM_END // END + }; + + writer.Write(cmd, 500, out int written); + + var buf = new byte[4]; + reader.Read(buf, 200, out int read); + + // CH341 возвращает статус: 0x00 = ACK получен (устройство есть) + // Первый байт ответа — статус последней I2C операции + if (read > 0 && buf[0] == 0x00) + { + Console.WriteLine($" Найдено устройство: 0x{addr:X2}"); + } + } + + Console.WriteLine("Сканирование завершено."); + } + + public static void CH341Init(IUsbDevice device) + { + Console.WriteLine("Пытаемся инициализировать устройство"); + var writer = device.OpenEndpointWriter(WriteEndpointID.Ep02); + + // Установка скорости I2C: 0x00=20kHz, 0x01=100kHz, 0x02=400kHz, 0x03=750kHz + var cmd = new byte[] + { + CH341_CMD_I2C_STREAM, + 0x60 | 0x01, // CH341_CMD_I2C_STM_SET | 100kHz + CH341_CMD_I2C_STM_END + }; + writer.Write(cmd, 1000, out _); + } + + public static byte[] Read(IUsbDevice device, byte address, byte reg, int length) + { + // Формируем пакет: START → write addr+reg → RESTART → read → STOP + var cmd = new byte[] + { + CH341_CMD_I2C_STREAM, + CH341_CMD_I2C_STM_STA, + CH341_CMD_I2C_STM_OUT | 2, // 2 байта: адрес + регистр + (byte)(address << 1), // write + reg, + CH341_CMD_I2C_STM_STA, // repeated START + CH341_CMD_I2C_STM_OUT | 1, + (byte)((address << 1) | 1), // read + (byte)(CH341_CMD_I2C_STM_IN | (length - 1)), + CH341_CMD_I2C_STM_STO, + CH341_CMD_I2C_STM_END + }; + + var writer = device.OpenEndpointWriter(WriteEndpointID.Ep02); + var reader = device.OpenEndpointReader(ReadEndpointID.Ep02); + + writer.Write(cmd, 1000, out _); + var buf = new byte[length]; + reader.Read(buf, 1000, out var transferred); + return buf; + } + + public static byte[] ReadWithRetry(IUsbDevice device, byte address, byte reg, int length) + { + for (var attempt = 0; attempt < 100; attempt++) + { + var result = Read(device, address, reg, length); + + if (result[0] != 0x03) + return result; + + Thread.Sleep(10); + } + + throw new Exception("Ошибка чтения CH431 после 100 попыток"); + } + + public static void Write(IUsbDevice device, byte address, byte reg, byte[] data) + { + // START → write addr → write reg → write data[0..n] → STOP + // CH341_CMD_I2C_STM_OUT | N означает "записать N байт" + // N = 1 (адрес) + 1 (регистр) + data.Length + var cmd = new byte[5 + data.Length + 2]; + int i = 0; + + cmd[i++] = CH341_CMD_I2C_STREAM; + cmd[i++] = CH341_CMD_I2C_STM_STA; + cmd[i++] = (byte)(CH341_CMD_I2C_STM_OUT | (2 + data.Length)); // адрес + регистр + данные + cmd[i++] = (byte)(address << 1); // write bit = 0 + cmd[i++] = reg; + foreach (var b in data) + cmd[i++] = b; + cmd[i++] = CH341_CMD_I2C_STM_STO; + cmd[i] = CH341_CMD_I2C_STM_END; + + var writer = device.OpenEndpointWriter(WriteEndpointID.Ep02); + writer.Write(cmd, 1000, out _); + } +} \ No newline at end of file diff --git a/RTCSync.cli/Utils/WindowsTimeUtils.cs b/RTCSync.cli/Utils/WindowsTimeUtils.cs new file mode 100644 index 0000000..c7b35e8 --- /dev/null +++ b/RTCSync.cli/Utils/WindowsTimeUtils.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; + +namespace RTCSync.Utils; + +public abstract class WindowsTimeUtils +{ + // c# datetime set + [DllImport("kernel32.dll")] + public static extern bool SetSystemTime(ref SystemTime time); + + [DllImport("kernel32.dll")] + public static extern void GetSystemTime(ref SystemTime time); + + [StructLayout(LayoutKind.Sequential)] + public struct SystemTime(DateTime dt) + { + public ushort Year = (ushort)dt.Year; + public ushort Month = (ushort)dt.Month; + public ushort DayOfWeek = (ushort)dt.DayOfWeek; + public ushort Day = (ushort)dt.Day; + public ushort Hour = (ushort)dt.Hour; + public ushort Minute = (ushort)dt.Minute; + public ushort Second = (ushort)dt.Second; + public ushort Milliseconds = (ushort)dt.Millisecond; + } +} \ No newline at end of file diff --git a/RTCSync.sln b/RTCSync.sln new file mode 100644 index 0000000..459ba98 --- /dev/null +++ b/RTCSync.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTCSync.cli", "RTCSync.cli\RTCSync.cli.csproj", "{DD33C287-F723-41D0-B832-E762D5D3DEB5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DD33C287-F723-41D0-B832-E762D5D3DEB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD33C287-F723-41D0-B832-E762D5D3DEB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD33C287-F723-41D0-B832-E762D5D3DEB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD33C287-F723-41D0-B832-E762D5D3DEB5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal