Compare commits

...

2 Commits

Author SHA1 Message Date
6fab3c0a0d --added actual comments to driver 2025-08-20 09:53:43 +03:00
3d778c8388 0.1.1-test
reduce test speed on com-ports to 4800 and 115200
reduce timeouts to 220ms on com and 300 on net
added actual comments to drivers
2025-08-20 09:47:32 +03:00
2 changed files with 180 additions and 187 deletions

View File

@@ -15,8 +15,8 @@ import (
const ( const (
configFileName = "connect.json" configFileName = "connect.json"
outputDir = "date" outputDir = "date"
comSearchTimeout = 500 * time.Millisecond comSearchTimeout = 220 * time.Millisecond
tcpSearchTimeout = 2 * time.Second tcpSearchTimeout = 300 * time.Millisecond
) )
// ConfigFile соответствует структуре файла connect.json // ConfigFile соответствует структуре файла connect.json

View File

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