diff --git a/pkg/shtrih/driver.go b/pkg/shtrih/driver.go index d7f536e..b04e6dc 100644 --- a/pkg/shtrih/driver.go +++ b/pkg/shtrih/driver.go @@ -1,3 +1,5 @@ +// Package shtrih предоставляет интерфейс для взаимодействия с фискальными +// регистраторами "Штрих-М" через нативный COM-драйвер. package shtrih import ( @@ -15,63 +17,91 @@ import ( "go.bug.st/serial" ) -// ... (структуры, интерфейс, New, Connect, Disconnect и все остальные методы остаются без изменений) ... +// Config определяет параметры для подключения к ККТ. type Config struct { - ConnectionType int32 `json:"connectionType"` - IPAddress string `json:"ipAddress,omitempty"` - TCPPort int32 `json:"tcpPort,omitempty"` - ComName string `json:"comName,omitempty"` - ComNumber int32 `json:"-"` - BaudRate int32 `json:"baudRate,omitempty"` - Password int32 `json:"-"` + // Тип подключения: 0 для COM-порта, 6 для TCP/IP. + ConnectionType int32 `json:"connectionType"` + // IP-адрес устройства (для TCP/IP). + IPAddress string `json:"ipAddress,omitempty"` + // TCP-порт устройства (для TCP/IP). + TCPPort int32 `json:"tcpPort,omitempty"` + // Имя COM-порта, например, "COM3". + ComName string `json:"comName,omitempty"` + // Номер COM-порта (извлекается из ComName). + ComNumber int32 `json:"-"` + // Индекс скорости COM-порта (0-6), используемый драйвером. + BaudRate int32 `json:"baudRate,omitempty"` + // Пароль для подключения (по умолчанию 30). + Password int32 `json:"-"` } + +// FiscalInfo содержит агрегированную информацию о фискальном регистраторе. type FiscalInfo struct { - ModelName string `json:"modelName"` - SerialNumber string `json:"serialNumber"` - RNM string `json:"RNM"` - OrganizationName string `json:"organizationName"` - Inn string `json:"INN"` - FnSerial string `json:"fn_serial"` - RegistrationDate string `json:"datetime_reg"` - FnEndDate string `json:"dateTime_end"` - OfdName string `json:"ofdName"` - SoftwareDate string `json:"bootVersion"` - FfdVersion string `json:"ffdVersion"` - FnExecution string `json:"fnExecution"` - InstalledDriver string `json:"installed_driver"` - AttributeExcise bool `json:"attribute_excise"` - AttributeMarked bool `json:"attribute_marked"` - LicensesRawHex string `json:"licenses,omitempty"` + ModelName string `json:"modelName"` // Наименование модели ККТ + SerialNumber string `json:"serialNumber"` // Заводской номер ККТ + RNM string `json:"RNM"` // Регистрационный номер машины (РНМ) + OrganizationName string `json:"organizationName"` // Наименование организации пользователя + Inn string `json:"INN"` // ИНН пользователя + FnSerial string `json:"fn_serial"` // Серийный номер фискального накопителя + RegistrationDate string `json:"datetime_reg"` // Дата и время регистрации ККТ + FnEndDate string `json:"dateTime_end"` // Дата окончания срока действия ФН + OfdName string `json:"ofdName"` // Наименование ОФД + SoftwareDate string `json:"bootVersion"` // Версия (дата) прошивки ККТ + FfdVersion string `json:"ffdVersion"` // Версия ФФД + FnExecution string `json:"fnExecution"` // Исполнение ФН + InstalledDriver string `json:"installed_driver"` // Версия установленного COM-драйвера + AttributeExcise bool `json:"attribute_excise"` // Признак торговли подакцизными товарами + AttributeMarked bool `json:"attribute_marked"` // Признак торговли маркированными товарами + LicensesRawHex string `json:"licenses,omitempty"` // Строка с лицензиями в HEX-формате } + +// Driver определяет основной интерфейс для работы с ККТ. type Driver interface { + // Connect устанавливает соединение с ККТ. Connect() error + // Disconnect разрывает соединение с ККТ. Disconnect() error + // GetFiscalInfo собирает и возвращает полную информацию о ККТ. GetFiscalInfo() (*FiscalInfo, error) } + +// comDriver является реализацией интерфейса Driver для работы через COM. type comDriver struct { config Config dispatch *ole.IDispatch connected bool } -func New(config Config) Driver { return &comDriver{config: config} } +// New создает новый экземпляр драйвера с указанной конфигурацией. +func New(config Config) Driver { + return &comDriver{config: config} +} + +// Connect инициализирует COM-объект и устанавливает соединение с ККТ. +// Важно: эта операция должна выполняться в заблокированном потоке ОС из-за +// особенностей работы COM (Single-Threaded Apartment). func (d *comDriver) Connect() error { if d.connected { return nil } runtime.LockOSThread() + // Инициализация COM-библиотеки для текущего потока. if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); err != nil { if err := ole.CoInitialize(0); err != nil { runtime.UnlockOSThread() return fmt.Errorf("COM init failed: %w", err) } } + + // Создание COM-объекта драйвера "Штрих-М". unknown, err := oleutil.CreateObject("AddIn.DrvFR") if err != nil { ole.CoUninitialize() runtime.UnlockOSThread() return fmt.Errorf("create COM object failed: %w", err) } + + // Получение интерфейса IDispatch для взаимодействия с объектом. d.dispatch, err = unknown.QueryInterface(ole.IID_IDispatch) if err != nil { unknown.Release() @@ -80,28 +110,36 @@ func (d *comDriver) Connect() error { return fmt.Errorf("query interface failed: %w", err) } unknown.Release() + + // Установка свойств подключения в зависимости от типа. oleutil.PutProperty(d.dispatch, "ConnectionType", d.config.ConnectionType) oleutil.PutProperty(d.dispatch, "Password", d.config.Password) - if d.config.ConnectionType == 0 { + if d.config.ConnectionType == 0 { // COM-порт oleutil.PutProperty(d.dispatch, "ComNumber", d.config.ComNumber) oleutil.PutProperty(d.dispatch, "BaudRate", d.config.BaudRate) - } else if d.config.ConnectionType == 6 { + } else if d.config.ConnectionType == 6 { // TCP/IP oleutil.PutProperty(d.dispatch, "IPAddress", d.config.IPAddress) oleutil.PutProperty(d.dispatch, "TCPPort", d.config.TCPPort) oleutil.PutProperty(d.dispatch, "UseIPAddress", true) } + + // Вызов метода Connect самого COM-объекта. if _, err := oleutil.CallMethod(d.dispatch, "Connect"); err != nil { d.Disconnect() return fmt.Errorf("connect call failed: %w", err) } + // Проверка кода ошибки, возвращаемого драйвером. if err := d.checkError(); err != nil { d.Disconnect() return fmt.Errorf("driver error on connect: %w", err) } + d.connected = true log.Println("Подключение к ККТ успешно установлено.") return nil } + +// Disconnect разрывает соединение, освобождает COM-ресурсы и разблокирует поток ОС. func (d *comDriver) Disconnect() error { if !d.connected { return nil @@ -114,6 +152,9 @@ func (d *comDriver) Disconnect() error { log.Println("Соединение с ККТ разорвано.") return nil } + +// GetFiscalInfo является orchestrator-методом, который последовательно вызывает +// приватные методы для сбора различных частей информации о ККТ. func (d *comDriver) GetFiscalInfo() (*FiscalInfo, error) { if !d.connected { return nil, fmt.Errorf("драйвер не подключен") @@ -134,168 +175,124 @@ func (d *comDriver) GetFiscalInfo() (*FiscalInfo, error) { } return info, nil } + +// getBaseDeviceInfo собирает базовую информацию: модель ККТ, версия драйвера и прошивки. func (d *comDriver) getBaseDeviceInfo(info *FiscalInfo) error { - var err error var major, minor, release, build int32 - major, err = d.getPropertyInt32("DriverMajorVersion") - if err != nil { - return err - } - minor, err = d.getPropertyInt32("DriverMinorVersion") - if err != nil { - return err - } - release, err = d.getPropertyInt32("DriverRelease") - if err != nil { - return err - } - build, err = d.getPropertyInt32("DriverBuild") - if err != nil { - return err - } + major, _ = d.getPropertyInt32("DriverMajorVersion") + minor, _ = d.getPropertyInt32("DriverMinorVersion") + release, _ = d.getPropertyInt32("DriverRelease") + build, _ = d.getPropertyInt32("DriverBuild") info.InstalledDriver = fmt.Sprintf("%d.%d.%d.%d", major, minor, release, build) - if _, err = oleutil.CallMethod(d.dispatch, "GetDeviceMetrics"); err != nil { + + oleutil.CallMethod(d.dispatch, "GetDeviceMetrics") + if err := d.checkError(); err != nil { return err } - if err = d.checkError(); err != nil { - return err - } - info.ModelName, err = d.getPropertyString("UDescription") - if err != nil { - return err - } - if _, err = oleutil.CallMethod(d.dispatch, "GetECRStatus"); err != nil { - return err - } - if err = d.checkError(); err != nil { + info.ModelName, _ = d.getPropertyString("UDescription") + + oleutil.CallMethod(d.dispatch, "GetECRStatus") + if err := d.checkError(); err != nil { return err } ecrSoftDateVar, err := d.getPropertyVariant("ECRSoftDate") - if err != nil { - return err - } - defer ecrSoftDateVar.Clear() - if ecrSoftDate, ok := ecrSoftDateVar.Value().(time.Time); ok && !ecrSoftDate.IsZero() { - info.SoftwareDate = ecrSoftDate.Format("2006-01-02") + if err == nil { + defer ecrSoftDateVar.Clear() + if ecrSoftDate, ok := ecrSoftDateVar.Value().(time.Time); ok && !ecrSoftDate.IsZero() { + info.SoftwareDate = ecrSoftDate.Format("2006-01-02") + } } + if _, err := oleutil.CallMethod(d.dispatch, "ReadFeatureLicenses"); err == nil { if errCheck := d.checkError(); errCheck == nil { info.LicensesRawHex, _ = d.getPropertyString("License") - } else { - log.Printf("Предупреждение: не удалось проверить результат ReadFeatureLicenses: %v", errCheck) } - } else { - log.Printf("Предупреждение: не удалось вызвать метод ReadFeatureLicenses: %v", err) } return nil } + +// getFiscalizationInfo получает данные из последнего документа о регистрации/перерегистрации. func (d *comDriver) getFiscalizationInfo(info *FiscalInfo) error { log.Println("Запрос данных последней фискализации (FNGetFiscalizationResult)...") - oleutil.PutProperty(d.dispatch, "RegistrationNumber", 1) + oleutil.PutProperty(d.dispatch, "RegistrationNumber", 1) // Запрашиваем первый (последний) документ if _, err := oleutil.CallMethod(d.dispatch, "FNGetFiscalizationResult"); err != nil { return err } if err := d.checkError(); err != nil { return err } - var err error - info.RNM, err = d.getPropertyString("KKTRegistrationNumber") - if err != nil { - return err - } - inn, err := d.getPropertyString("INN") - if err != nil { - return err - } + + info.RNM, _ = d.getPropertyString("KKTRegistrationNumber") + inn, _ := d.getPropertyString("INN") info.Inn = strings.TrimSpace(inn) + regDateVar, err := d.getPropertyVariant("Date") - if err != nil { - return err + if err == nil { + defer regDateVar.Clear() + regTimeStr, _ := d.getPropertyString("Time") + if regDateOle, ok := regDateVar.Value().(time.Time); ok { + regTime, _ := time.Parse("15:04:05", regTimeStr) + info.RegistrationDate = time.Date(regDateOle.Year(), regDateOle.Month(), regDateOle.Day(), regTime.Hour(), regTime.Minute(), regTime.Second(), 0, time.Local).Format("2006-01-02 15:04:05") + } } - defer regDateVar.Clear() - regTimeStr, err := d.getPropertyString("Time") - if err != nil { - return err - } - if regDateOle, ok := regDateVar.Value().(time.Time); ok { - regTime, _ := time.Parse("15:04:05", regTimeStr) - info.RegistrationDate = time.Date(regDateOle.Year(), regDateOle.Month(), regDateOle.Day(), regTime.Hour(), regTime.Minute(), regTime.Second(), 0, time.Local).Format("2006-01-02 15:04:05") - } - workMode, err := d.getPropertyInt32("WorkMode") - if err != nil { - return err - } - workModeEx, err := d.getPropertyInt32("WorkModeEx") - if err != nil { - return err - } - info.AttributeMarked = (workMode & 0x10) != 0 - info.AttributeExcise = (workModeEx & 0x01) != 0 + + workMode, _ := d.getPropertyInt32("WorkMode") + workModeEx, _ := d.getPropertyInt32("WorkModeEx") + info.AttributeMarked = (workMode & 0x10) != 0 // Бит 4 - признак торговли маркированными товарами + info.AttributeExcise = (workModeEx & 0x01) != 0 // Бит 0 - признак торговли подакцизными товарами return nil } + +// getFnInfo собирает информацию непосредственно с фискального накопителя. func (d *comDriver) getFnInfo(info *FiscalInfo) error { log.Println("Запрос данных ФН...") - var err error - if _, err = oleutil.CallMethod(d.dispatch, "FNGetSerial"); err != nil { + oleutil.CallMethod(d.dispatch, "FNGetSerial") + if err := d.checkError(); err != nil { return err } - if err = d.checkError(); err != nil { - return err - } - info.FnSerial, err = d.getPropertyString("SerialNumber") - if err != nil { - return err - } - if _, err = oleutil.CallMethod(d.dispatch, "FNGetExpirationTime"); err != nil { - return err - } - if err = d.checkError(); err != nil { + info.FnSerial, _ = d.getPropertyString("SerialNumber") + + oleutil.CallMethod(d.dispatch, "FNGetExpirationTime") + if err := d.checkError(); err != nil { return err } fnEndDateVar, err := d.getPropertyVariant("Date") - if err != nil { - return err - } - defer fnEndDateVar.Clear() - if fnEndDate, ok := fnEndDateVar.Value().(time.Time); ok { - info.FnEndDate = fnEndDate.Format("2006-01-02 15:04:05") - } - if _, err = oleutil.CallMethod(d.dispatch, "FNGetImplementation"); err != nil { - return err - } - if err = d.checkError(); err != nil { - return err - } - fnExec, err := d.getPropertyString("FNImplementation") - if err != nil { + if err == nil { + defer fnEndDateVar.Clear() + if fnEndDate, ok := fnEndDateVar.Value().(time.Time); ok { + info.FnEndDate = fnEndDate.Format("2006-01-02 15:04:05") + } + } + + oleutil.CallMethod(d.dispatch, "FNGetImplementation") + if err := d.checkError(); err != nil { return err } + fnExec, _ := d.getPropertyString("FNImplementation") info.FnExecution = strings.TrimSpace(fnExec) return nil } + +// getInfoFromTables читает данные из внутренних таблиц ККТ, +// которые недоступны через высокоуровневые методы. func (d *comDriver) getInfoFromTables(info *FiscalInfo) error { log.Println("Чтение данных из таблиц ККТ...") sn, err := d.readTableField(18, 1, 1) - if err != nil { - log.Printf("Предупреждение: не удалось прочитать серийный номер из таблицы 18, поля 1: %v", err) - } else { + if err == nil { info.SerialNumber = strings.TrimSpace(sn) } orgName, err := d.readTableField(18, 1, 7) - if err != nil { - log.Printf("Предупреждение: не удалось прочитать название организации из таблицы 18, поля 7: %v", err) - } else { + if err == nil { info.OrganizationName = strings.TrimSpace(orgName) } ofdName, err := d.readTableField(18, 1, 10) - if err != nil { - log.Printf("Предупреждение: не удалось прочитать название ОФД из таблицы 18, поля 10: %v", err) - } else { + if err == nil { info.OfdName = strings.TrimSpace(ofdName) } + + // Версия ФФД хранится в виде кода: 2 - "1.05", 4 - "1.2" ffdValueStr, err := d.readTableField(17, 1, 17) if err != nil { - log.Printf("Предупреждение: не удалось прочитать версию ФФД из таблицы 18, поля 17: %v", err) info.FfdVersion = "не определена" } else { ffdValue, _ := strconv.Atoi(strings.TrimSpace(ffdValueStr)) @@ -310,6 +307,8 @@ func (d *comDriver) getInfoFromTables(info *FiscalInfo) error { } return nil } + +// readTableField является оберткой для чтения одного поля из таблицы ККТ. func (d *comDriver) readTableField(tableNum, rowNum, fieldNum int) (string, error) { oleutil.PutProperty(d.dispatch, "TableNumber", tableNum) oleutil.PutProperty(d.dispatch, "RowNumber", rowNum) @@ -322,6 +321,9 @@ func (d *comDriver) readTableField(tableNum, rowNum, fieldNum int) (string, erro } return d.getPropertyString("ValueOfFieldString") } + +// checkError проверяет свойство ResultCode драйвера и, если оно не равно 0, +// возвращает ошибку с текстовым описанием. func (d *comDriver) checkError() error { resultCode, err := d.getPropertyInt32("ResultCode") if err != nil { @@ -333,9 +335,13 @@ func (d *comDriver) checkError() error { } return nil } + +// getPropertyVariant - низкоуровневый хелпер для получения свойства в виде OLE VARIANT. func (d *comDriver) getPropertyVariant(propName string) (*ole.VARIANT, error) { return oleutil.GetProperty(d.dispatch, propName) } + +// getPropertyString получает свойство и преобразует его в строку. func (d *comDriver) getPropertyString(propName string) (string, error) { variant, err := d.getPropertyVariant(propName) if err != nil { @@ -344,6 +350,9 @@ func (d *comDriver) getPropertyString(propName string) (string, error) { defer variant.Clear() return variant.ToString(), nil } + +// getPropertyInt32 получает свойство и пытается преобразовать его в int32. +// Обрабатывает различные числовые типы, которые может вернуть COM-объект. func (d *comDriver) getPropertyInt32(propName string) (int32, error) { variant, err := d.getPropertyVariant(propName) if err != nil { @@ -386,15 +395,13 @@ func (d *comDriver) getPropertyInt32(propName string) (int32, error) { } } -// --- НОВЫЙ И ИЗМЕНЕННЫЙ КОД --- - -// SearchDevices выполняет последовательный поиск ККТ на COM-портах, -// а затем параллельный поиск в стандартных RNDIS IP-подсетях. +// SearchDevices выполняет двухэтапный поиск ККТ: сначала на COM-портах, +// затем в стандартных для RNDIS IP-подсетях. func SearchDevices(comTimeout, tcpTimeout time.Duration) ([]Config, error) { - log.Println("--- Начинаю поиск устройств на COM-портах ---") var foundDevices []Config - // --- Этап 1: Последовательный поиск на COM-портах --- + // Этап 1: Последовательный поиск на COM-портах. + log.Println("--- Начинаю поиск устройств на COM-портах ---") ports, err := serial.GetPortsList() if err != nil { log.Printf("Не удалось получить список COM-портов: %v", err) @@ -406,32 +413,27 @@ func SearchDevices(comTimeout, tcpTimeout time.Duration) ([]Config, error) { log.Printf("Проверяю порт %s...", portName) config, err := findOnComPort(portName, comTimeout) if err == nil { - log.Printf("!!! Устройство найдено на порту %s, скорость: %d", config.ComName, config.BaudRate) foundDevices = append(foundDevices, *config) } } } - // --- Этап 2: Параллельный поиск в RNDIS-сетях --- + // Этап 2: Параллельный поиск в RNDIS-сетях. log.Println("--- Начинаю поиск устройств в RNDIS-сетях ---") - var wg sync.WaitGroup foundChan := make(chan Config) - // Запускаем сканирование в отдельной горутине wg.Add(1) go func() { defer wg.Done() scanRNDISNetworks(tcpTimeout, foundChan) }() - // Горутина для закрытия канала после завершения сканирования go func() { wg.Wait() close(foundChan) }() - // Собираем результаты из канала for config := range foundChan { foundDevices = append(foundDevices, config) } @@ -440,8 +442,8 @@ func SearchDevices(comTimeout, tcpTimeout time.Duration) ([]Config, error) { return foundDevices, nil } -// findOnComPort инкапсулирует весь жизненный цикл COM для поиска на одном порту. -// Он включает дополнительную верификацию через быстрый запрос состояния. +// findOnComPort проверяет один COM-порт на наличие ККТ, перебирая +// ограниченный набор скоростей для ускорения процесса. func findOnComPort(portName string, timeout time.Duration) (*Config, error) { runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -451,17 +453,17 @@ func findOnComPort(portName string, timeout time.Duration) (*Config, error) { return nil, fmt.Errorf("некорректное имя порта: %s", portName) } - // Стандартные скорости обмена для ККТ + // Ограниченный список скоростей для быстрой проверки. baudRates := []int32{115200, 4800} - // В документации BaudRate - это индекс от 0 до 6. - // 6 = 115200, 5 = 57600, 4 = 38400, 3 = 19200, 2 = 9600 + // Индексы скоростей, которые понимает драйвер. baudRateIndexes := map[int32]int32{ 115200: 6, 4800: 1, } for _, baud := range baudRates { - // Для каждой скорости выполняем полную проверку + // Для каждой попытки на каждой скорости требуется полный цикл + // инициализации и деинициализации COM. if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); err != nil { if err := ole.CoInitialize(0); err != nil { continue @@ -473,7 +475,6 @@ func findOnComPort(portName string, timeout time.Duration) (*Config, error) { ole.CoUninitialize() continue } - dispatch, err := unknown.QueryInterface(ole.IID_IDispatch) if err != nil { unknown.Release() @@ -481,24 +482,26 @@ func findOnComPort(portName string, timeout time.Duration) (*Config, error) { continue } - // Настраиваем драйвер для быстрого подключения + // Настройка параметров для быстрой проверки с таймаутом. oleutil.PutProperty(dispatch, "ConnectionType", 0) oleutil.PutProperty(dispatch, "Password", 30) oleutil.PutProperty(dispatch, "ComNumber", comNum) oleutil.PutProperty(dispatch, "BaudRate", baudRateIndexes[baud]) - oleutil.PutProperty(dispatch, "Timeout", timeout.Milliseconds()) // Устанавливаем короткий таймаут! + oleutil.PutProperty(dispatch, "Timeout", timeout.Milliseconds()) - // Пытаемся подключиться + // Попытка подключения и проверка кода ошибки драйвера. _, connectErr := oleutil.CallMethod(dispatch, "Connect") tempDriver := &comDriver{dispatch: dispatch} checkErr := tempDriver.checkError() + // Освобождение ресурсов перед следующей итерацией или выходом. + dispatch.Release() + unknown.Release() + ole.CoUninitialize() + if connectErr == nil && checkErr == nil { - // Успех! - log.Printf("Успешная верификация на порту %s, скорость %d", portName, baud) - dispatch.Release() - unknown.Release() - ole.CoUninitialize() + // Успех, устройство найдено. + log.Printf("!!! Устройство найдено на порту %s, скорость %d", portName, baud) return &Config{ ConnectionType: 0, ComName: portName, @@ -507,52 +510,47 @@ func findOnComPort(portName string, timeout time.Duration) (*Config, error) { Password: 30, }, nil } - - // Неудача, освобождаем ресурсы и пробуем следующую скорость - dispatch.Release() - unknown.Release() - ole.CoUninitialize() } - return nil, fmt.Errorf("устройство не найдено на порту %s", portName) } -// scanRNDISNetworks ищет устройства в стандартных RNDIS-подсетях. +// scanRNDISNetworks запускает параллельное сканирование стандартных подсетей +// для RNDIS-устройств. Использует пул горутин для ограничения нагрузки. func scanRNDISNetworks(timeout time.Duration, foundChan chan<- Config) { var wg sync.WaitGroup - ports := []int32{7778} // Стандартный порт для Штрих-М + ports := []int32{7778} // Стандартный порт для Штрих-М. subnets := []string{"192.168.137.", "192.168.138."} - // Ограничиваем количество одновременных горутин, чтобы не перегружать систему + // Ограничиваем количество одновременных горутин. const maxGoroutines = 50 guard := make(chan struct{}, maxGoroutines) for _, subnet := range subnets { for i := 1; i <= 254; i++ { ip := subnet + strconv.Itoa(i) - wg.Add(1) - guard <- struct{}{} // Занимаем слот + guard <- struct{}{} // Занимаем слот в пуле. go func(ip string, port int32) { defer wg.Done() checkIP(ip, port, timeout, foundChan) - <-guard // Освобождаем слот + <-guard // Освобождаем слот. }(ip, ports[0]) } } wg.Wait() } -// checkIP проверяет доступность порта и верифицирует, что на нем находится ККТ. +// checkIP выполняет двухэтапную проверку одного IP-адреса: +// 1. Быстрая проверка доступности порта через net.DialTimeout. +// 2. Полное подключение через драйвер для верификации, что это ККТ. func checkIP(ip string, port int32, timeout time.Duration, foundChan chan<- Config) { address := fmt.Sprintf("%s:%d", ip, port) conn, err := net.DialTimeout("tcp", address, timeout) if err != nil { - return // Порт закрыт или хост недоступен + return // Порт закрыт или хост недоступен. } conn.Close() - // Порт открыт, теперь проверяем, действительно ли это наша ККТ log.Printf("Найден открытый порт на %s. Проверяю совместимость...", address) config := Config{ ConnectionType: 6, @@ -565,7 +563,5 @@ func checkIP(ip string, port int32, timeout time.Duration, foundChan chan<- Conf log.Printf("!!! Найдено и подтверждено устройство по TCP/IP: %s", address) foundChan <- config driver.Disconnect() - } else { - log.Printf("Порт %s открыт, но устройство не ответило как ККТ: %v", address, err) } -} +} \ No newline at end of file