initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using RTCSync.Utils;
|
||||||
|
|
||||||
|
namespace RTCSync.Models;
|
||||||
|
|
||||||
|
public class TimeModel(ReadOnlySpan<byte> rtcBytes)
|
||||||
|
{
|
||||||
|
private static int CalculateHours(ReadOnlySpan<byte> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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)];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace RTCSync.Options;
|
||||||
|
|
||||||
|
public interface IOption
|
||||||
|
{
|
||||||
|
string Description { get; }
|
||||||
|
List<string> OptionNames { get; }
|
||||||
|
string OptionValues { get; }
|
||||||
|
void Execute(OptionArgs args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OptionArgs
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> OptionValues { get; set; }
|
||||||
|
public List<string> Arguments { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
namespace RTCSync.Options;
|
||||||
|
|
||||||
|
public class OptionDispatcher
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IOption> _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<string, string>();
|
||||||
|
|
||||||
|
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<string> SplitText(string text, int maxWidth)
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using RTCSync.Services;
|
||||||
|
using RTCSync.Utils;
|
||||||
|
|
||||||
|
namespace RTCSync.Options;
|
||||||
|
|
||||||
|
public class ReadStatusOption : IOption
|
||||||
|
{
|
||||||
|
public string Description =>
|
||||||
|
"Чтение регистров status, control и температуры с часов реального времени.";
|
||||||
|
public List<string> 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using RTCSync.Services;
|
||||||
|
using RTCSync.Utils;
|
||||||
|
|
||||||
|
namespace RTCSync.Options;
|
||||||
|
|
||||||
|
public class SyncTimeOption : IOption
|
||||||
|
{
|
||||||
|
public string Description => "Синхронизирует системное время со временем модуля реального времени. Если часы хранят UTC время, то используйте опцию universal";
|
||||||
|
public List<string> 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)
|
||||||
|
? "Время установлено в системе"
|
||||||
|
: "Ошибка: нет прав администратора");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<LangVersion>latestmajor</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<RootNamespace>RTCSync</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LibUsbDotNet" Version="3.0.224" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace RTCSync.Utils;
|
||||||
|
|
||||||
|
public class BinaryUtils
|
||||||
|
{
|
||||||
|
public static byte ToBcd(int value) => (byte)(((value / 10) << 4) | (value % 10));
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<byte> 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<byte> 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<byte> bytes)
|
||||||
|
{
|
||||||
|
var shift = 0;
|
||||||
|
var result = 0;
|
||||||
|
foreach (var bi in bis)
|
||||||
|
{
|
||||||
|
result |= (bi.Take(bytes) << shift);
|
||||||
|
shift += bi.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 _);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
Reference in New Issue
Block a user