0.1.0-prod

This commit is contained in:
2025-08-20 08:21:24 +03:00
commit af852b9ed6
6 changed files with 1023 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.exe
*.json

13
go.mod Normal file
View File

@@ -0,0 +1,13 @@
module shtrih-kkt
go 1.23.4
require (
github.com/go-ole/go-ole v1.3.0
go.bug.st/serial v1.6.4
)
require (
github.com/creack/goselect v0.1.2 // indirect
golang.org/x/sys v0.19.0 // indirect
)

17
go.sum Normal file
View File

@@ -0,0 +1,17 @@
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

384
main.go Normal file
View File

@@ -0,0 +1,384 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"time"
"shtrih-kkt/pkg/shtrih"
)
const (
configFileName = "connect.json"
outputDir = "date"
comSearchTimeout = 500 * time.Millisecond
tcpSearchTimeout = 2 * time.Second
)
// ConfigFile соответствует структуре файла connect.json
type ConfigFile struct {
Timeout int `json:"timeout_to_ip_port"`
Shtrih []ConnectionSettings `json:"shtrih"`
Atol []interface{} `json:"atol"`
}
// ConnectionSettings описывает один блок настроек подключения для Штрих-М
type ConnectionSettings struct {
TypeConnect int32 `json:"type_connect"`
ComPort string `json:"com_port"`
ComBaudrate string `json:"com_baudrate"`
IP string `json:"ip"`
IPPort string `json:"ip_port"`
}
// PolledDevice связывает конфигурацию, использованную для подключения,
// с фискальной информацией, полученной от устройства.
type PolledDevice struct {
Config shtrih.Config
Info *shtrih.FiscalInfo
}
func main() {
log.Println("Запуск приложения для сбора данных с ККТ Штрих-М...")
configData, err := os.ReadFile(configFileName)
if err != nil {
if os.IsNotExist(err) {
log.Printf("Файл конфигурации '%s' не найден. Запускаю режим автопоиска устройств...", configFileName)
runDiscoveryMode()
} else {
log.Fatalf("Ошибка чтения файла конфигурации '%s': %v", configFileName, err)
}
} else {
log.Printf("Найден файл конфигурации '%s'. Запускаю стационарный режим...", configFileName)
runConfigMode(configData)
}
log.Println("Работа приложения завершена.")
}
func runConfigMode(data []byte) {
var configFile ConfigFile
if err := json.Unmarshal(data, &configFile); err != nil {
log.Printf("Ошибка парсинга JSON из '%s': %v. Переключаюсь на режим автопоиска.", configFileName, err)
runDiscoveryMode()
return
}
if len(configFile.Shtrih) == 0 {
log.Printf("В файле '%s' не найдено настроек для 'shtrih'. Переключаюсь на режим автопоиска.", configFileName)
runDiscoveryMode()
return
}
log.Printf("Найдено %d конфигураций для Штрих-М. Начинаю опрос...", len(configFile.Shtrih))
configs := convertSettingsToConfigs(configFile.Shtrih)
if len(configs) == 0 {
log.Println("Не удалось создать ни одной валидной конфигурации из файла. Проверьте данные в connect.json.")
return
}
processDevices(configs)
}
func runDiscoveryMode() {
configs, err := shtrih.SearchDevices(comSearchTimeout, tcpSearchTimeout)
if err != nil {
log.Printf("Во время поиска устройств произошла ошибка: %v", err)
}
if len(configs) == 0 {
log.Println("В ходе сканирования не найдено ни одного устройства Штрих-М.")
return
}
log.Printf("Найдено %d устройств. Начинаю сбор информации...", len(configs))
polledDevices := processDevices(configs)
// Если были успешно опрошены какие-либо устройства, сохраняем их конфигурацию
if len(polledDevices) > 0 {
saveConfiguration(polledDevices)
}
}
// processDevices - основная функция, реализующая "умную" логику обновления и создания файлов.
// Теперь она возвращает срез успешно опрошенных устройств.
func processDevices(configs []shtrih.Config) []PolledDevice {
// Шаг 1: Сначала собираем информацию со всех найденных устройств.
var polledDevices []PolledDevice // Было: var freshKKTData []*shtrih.FiscalInfo
for _, config := range configs {
log.Printf("--- Опрашиваю устройство: %+v ---", config)
driver := shtrih.New(config)
if err := driver.Connect(); err != nil {
log.Printf("Не удалось подключиться к устройству: %v", err)
continue
}
info, err := driver.GetFiscalInfo()
driver.Disconnect() // Отключаемся сразу после получения данных
if err != nil {
log.Printf("Ошибка при получении фискальной информации: %v", err)
continue
}
if info == nil || info.SerialNumber == "" {
log.Println("Получена пустая информация или отсутствует серийный номер, данные проигнорированы.")
continue
}
// Сохраняем и конфигурацию, и результат
polledDevices = append(polledDevices, PolledDevice{Config: config, Info: info})
}
if len(polledDevices) == 0 {
log.Println("--- Не удалось собрать данные ни с одного устройства. Завершение. ---")
return nil // Возвращаем nil, если ничего не найдено
}
log.Printf("--- Всего собрано данных с %d устройств. Начинаю обработку файлов. ---", len(polledDevices))
// Шаг 2: Ищем "донора" данных о рабочей станции в папке /date.
sourceWSDataMap := findSourceWorkstationData()
// Шаг 3: Обрабатываем каждого "свежего" ККТ в соответствии с новой логикой.
var successCount int
for _, pd := range polledDevices { // Итерируемся по новой структуре
kktInfo := pd.Info // Получаем доступ к данным ККТ
fileName := fmt.Sprintf("%s.json", kktInfo.SerialNumber)
filePath := filepath.Join(outputDir, fileName)
// ... (остальная часть цикла остается без изменений) ...
if _, err := os.Stat(filePath); err == nil {
log.Printf("Файл для ККТ %s уже существует. Обновляю временную метку...", kktInfo.SerialNumber)
if err := updateTimestampInFile(filePath); err != nil {
log.Printf("Не удалось обновить файл %s: %v", filePath, err)
} else {
log.Printf("Файл %s успешно обновлен.", filePath)
successCount++
}
} else {
var wsDataToUse map[string]interface{}
if sourceWSDataMap != nil {
log.Printf("Создаю новый файл для ККТ %s, используя данные о рабочей станции из файла-донора.", kktInfo.SerialNumber)
wsDataToUse = sourceWSDataMap
} else {
log.Printf("Создаю первичный файл для ККТ %s с базовыми данными о рабочей станции.", kktInfo.SerialNumber)
hostname, _ := os.Hostname()
wsDataToUse = map[string]interface{}{"hostname": hostname}
}
wsDataToUse["current_time"] = time.Now().Format("2006-01-02 15:04:05")
if err := saveNewMergedInfo(kktInfo, wsDataToUse, filePath); err != nil {
log.Printf("Не удалось создать файл для ККТ %s: %v", kktInfo.SerialNumber, err)
} else {
successCount++
}
}
}
log.Printf("--- Обработка файлов завершена. Успешно создано/обновлено: %d файлов. ---", successCount)
return polledDevices // Возвращаем результат
}
// findSourceWorkstationData ищет в папке /date любой .json файл и извлекает из него
// все данные как `map[string]interface{}`.
func findSourceWorkstationData() map[string]interface{} {
files, err := os.ReadDir(outputDir)
if err != nil {
return nil
}
for _, file := range files {
if !file.IsDir() && filepath.Ext(file.Name()) == ".json" {
filePath := filepath.Join(outputDir, file.Name())
data, err := os.ReadFile(filePath)
if err != nil {
log.Printf("Предупреждение: не удалось прочитать файл-донор %s: %v", filePath, err)
continue
}
var content map[string]interface{}
if err := json.Unmarshal(data, &content); err != nil {
log.Printf("Предупреждение: не удалось распарсить JSON из файла-донора %s: %v", filePath, err)
continue
}
// Проверяем, что это не файл от нашего ККТ (у него не должно быть поля modelName)
// и что у него есть hostname. Это делает выбор донора более надежным.
_, hasModelName := content["modelName"]
_, hasHostname := content["hostname"]
if !hasModelName && hasHostname {
log.Printf("Найден файл-донор с данными о рабочей станции: %s", filePath)
return content // Возвращаем все содержимое файла как карту.
}
}
}
log.Println("В папке /date не найдено файлов-доноров. Будут использованы базовые данные.")
return nil
}
// updateTimestampInFile читает JSON-файл, обновляет в нем поле current_time и перезаписывает его.
func updateTimestampInFile(filePath string) error {
data, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("ошибка чтения файла: %w", err)
}
var content map[string]interface{}
if err := json.Unmarshal(data, &content); err != nil {
return fmt.Errorf("ошибка парсинга JSON: %w", err)
}
content["current_time"] = time.Now().Format("2006-01-02 15:04:05")
updatedData, err := json.MarshalIndent(content, "", " ")
if err != nil {
return fmt.Errorf("ошибка маршалинга JSON: %w", err)
}
return os.WriteFile(filePath, updatedData, 0644)
}
// saveNewMergedInfo объединяет данные ККТ и данные рабочей станции (в виде map) и сохраняет в новый JSON-файл.
func saveNewMergedInfo(kktInfo *shtrih.FiscalInfo, wsData map[string]interface{}, filePath string) error {
var kktMap map[string]interface{}
kktJSON, _ := json.Marshal(kktInfo)
json.Unmarshal(kktJSON, &kktMap)
// Сливаем карты. Ключи из wsData перезапишут любые совпадения в kktMap.
for key, value := range wsData {
kktMap[key] = value
}
// Удаляем поля, специфичные для ККТ, из данных донора, если они случайно туда попали.
// Это предотвратит запись, например, "serialNumber" от АТОЛ в файл Штриха.
delete(kktMap, "serialNumber")
// Возвращаем серийный номер нашего ККТ, который мы сохранили в структуре kktInfo.
kktMap["serialNumber"] = kktInfo.SerialNumber
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return fmt.Errorf("не удалось создать директорию '%s': %w", outputDir, err)
}
finalJSON, err := json.MarshalIndent(kktMap, "", " ")
if err != nil {
return fmt.Errorf("ошибка маршалинга итогового JSON: %w", err)
}
if err := os.WriteFile(filePath, finalJSON, 0644); err != nil {
return fmt.Errorf("ошибка записи в файл '%s': %w", filePath, err)
}
log.Printf("Данные для ККТ %s успешно сохранены в новый файл: %s", kktInfo.SerialNumber, filePath)
return nil
}
// convertSettingsToConfigs преобразует настройки из файла в формат, понятный библиотеке.
func convertSettingsToConfigs(settings []ConnectionSettings) []shtrih.Config {
var configs []shtrih.Config
baudRateMap := map[string]int32{
"115200": 6, "57600": 5, "38400": 4, "19200": 3, "9600": 2,
}
for _, s := range settings {
config := shtrih.Config{
ConnectionType: s.TypeConnect,
Password: 30,
}
switch s.TypeConnect {
case 0: // COM-порт
comNum, err := strconv.Atoi(s.ComPort[3:])
if err != nil {
log.Printf("Некорректное имя COM-порта '%s' в конфигурации, пропуск.", s.ComPort)
continue
}
baudRate, ok := baudRateMap[s.ComBaudrate]
if !ok {
log.Printf("Некорректная скорость '%s' для порта '%s', пропуск.", s.ComBaudrate, s.ComPort)
continue
}
config.ComName = s.ComPort
config.ComNumber = int32(comNum)
config.BaudRate = baudRate
case 6: // TCP/IP
port, err := strconv.Atoi(s.IPPort)
if err != nil {
log.Printf("Некорректный TCP-порт '%s' для IP '%s', пропуск.", s.IPPort, s.IP)
continue
}
config.IPAddress = s.IP
config.TCPPort = int32(port)
default:
log.Printf("Неизвестный тип подключения '%d', пропуск.", s.TypeConnect)
continue
}
configs = append(configs, config)
}
return configs
}
// saveConfiguration обновляет файл connect.json, записывая в него
// конфигурации успешно найденных и опрошенных устройств.
func saveConfiguration(polledDevices []PolledDevice) {
log.Printf("Сохранение %d найденных конфигураций в файл '%s'...", len(polledDevices), configFileName)
// Шаг 1: Читаем существующий файл или создаем новую структуру.
var configFile ConfigFile
data, err := os.ReadFile(configFileName)
if err == nil {
// Файл есть, парсим его, чтобы не потерять другие секции (например, "atol")
if err := json.Unmarshal(data, &configFile); err != nil {
log.Printf("Предупреждение: файл '%s' поврежден (%v). Он будет перезаписан.", configFileName, err)
configFile = ConfigFile{} // Создаем пустую структуру в случае ошибки
}
}
// Шаг 2: Формируем новый срез настроек для "shtrih".
var newShtrihSettings []ConnectionSettings
for _, pd := range polledDevices {
newShtrihSettings = append(newShtrihSettings, convertConfigToSettings(pd.Config))
}
// Шаг 3: Полностью заменяем секцию "shtrih" новыми данными.
configFile.Shtrih = newShtrihSettings
// Шаг 4: Записываем обновленную структуру обратно в файл.
updatedData, err := json.MarshalIndent(configFile, "", " ")
if err != nil {
log.Printf("Ошибка: не удалось преобразовать конфигурацию в JSON: %v", err)
return
}
if err := os.WriteFile(configFileName, updatedData, 0644); err != nil {
log.Printf("Ошибка: не удалось записать конфигурацию в файл '%s': %v", configFileName, err)
return
}
log.Printf("Конфигурация успешно сохранена в '%s'.", configFileName)
}
// convertConfigToSettings преобразует внутренний формат shtrih.Config
// в формат ConnectionSettings для записи в connect.json.
func convertConfigToSettings(config shtrih.Config) ConnectionSettings {
// Карта для обратного преобразования индекса скорости в строку
baudRateReverseMap := map[int32]string{
6: "115200", 5: "57600", 4: "38400", 3: "19200", 2: "9600",
}
settings := ConnectionSettings{
TypeConnect: config.ConnectionType,
}
switch config.ConnectionType {
case 0: // COM-порт
settings.ComPort = config.ComName
settings.ComBaudrate = baudRateReverseMap[config.BaudRate]
case 6: // TCP/IP
settings.IP = config.IPAddress
settings.IPPort = strconv.Itoa(int(config.TCPPort))
}
return settings
}

574
pkg/shtrih/driver.go Normal file
View File

@@ -0,0 +1,574 @@
package shtrih
import (
"fmt"
"log"
"net"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
"go.bug.st/serial"
)
// ... (структуры, интерфейс, New, Connect, Disconnect и все остальные методы остаются без изменений) ...
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:"-"`
}
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"`
}
type Driver interface {
Connect() error
Disconnect() error
GetFiscalInfo() (*FiscalInfo, error)
}
type comDriver struct {
config Config
dispatch *ole.IDispatch
connected bool
}
func New(config Config) Driver { return &comDriver{config: config} }
func (d *comDriver) Connect() error {
if d.connected {
return nil
}
runtime.LockOSThread()
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)
}
}
unknown, err := oleutil.CreateObject("AddIn.DrvFR")
if err != nil {
ole.CoUninitialize()
runtime.UnlockOSThread()
return fmt.Errorf("create COM object failed: %w", err)
}
d.dispatch, err = unknown.QueryInterface(ole.IID_IDispatch)
if err != nil {
unknown.Release()
ole.CoUninitialize()
runtime.UnlockOSThread()
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 {
oleutil.PutProperty(d.dispatch, "ComNumber", d.config.ComNumber)
oleutil.PutProperty(d.dispatch, "BaudRate", d.config.BaudRate)
} else if d.config.ConnectionType == 6 {
oleutil.PutProperty(d.dispatch, "IPAddress", d.config.IPAddress)
oleutil.PutProperty(d.dispatch, "TCPPort", d.config.TCPPort)
oleutil.PutProperty(d.dispatch, "UseIPAddress", true)
}
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
}
func (d *comDriver) Disconnect() error {
if !d.connected {
return nil
}
oleutil.CallMethod(d.dispatch, "Disconnect")
d.dispatch.Release()
ole.CoUninitialize()
runtime.UnlockOSThread()
d.connected = false
log.Println("Соединение с ККТ разорвано.")
return nil
}
func (d *comDriver) GetFiscalInfo() (*FiscalInfo, error) {
if !d.connected {
return nil, fmt.Errorf("драйвер не подключен")
}
info := &FiscalInfo{}
var err error
if err = d.getBaseDeviceInfo(info); err != nil {
return nil, fmt.Errorf("ошибка получения базовой информации об устройстве: %w", err)
}
if err = d.getFiscalizationInfo(info); err != nil {
return nil, fmt.Errorf("ошибка получения информации о фискализации: %w", err)
}
if err = d.getFnInfo(info); err != nil {
return nil, fmt.Errorf("ошибка получения информации о ФН: %w", err)
}
if err = d.getInfoFromTables(info); err != nil {
return nil, fmt.Errorf("ошибка получения информации из таблиц: %w", err)
}
return info, nil
}
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
}
info.InstalledDriver = fmt.Sprintf("%d.%d.%d.%d", major, minor, release, build)
if _, err = oleutil.CallMethod(d.dispatch, "GetDeviceMetrics"); 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 {
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 := 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
}
func (d *comDriver) getFiscalizationInfo(info *FiscalInfo) error {
log.Println("Запрос данных последней фискализации (FNGetFiscalizationResult)...")
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.Inn = strings.TrimSpace(inn)
regDateVar, err := d.getPropertyVariant("Date")
if err != nil {
return err
}
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
return nil
}
func (d *comDriver) getFnInfo(info *FiscalInfo) error {
log.Println("Запрос данных ФН...")
var err error
if _, err = oleutil.CallMethod(d.dispatch, "FNGetSerial"); 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 {
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 {
return err
}
info.FnExecution = strings.TrimSpace(fnExec)
return nil
}
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 {
info.SerialNumber = strings.TrimSpace(sn)
}
orgName, err := d.readTableField(18, 1, 7)
if err != nil {
log.Printf("Предупреждение: не удалось прочитать название организации из таблицы 18, поля 7: %v", err)
} else {
info.OrganizationName = strings.TrimSpace(orgName)
}
ofdName, err := d.readTableField(18, 1, 10)
if err != nil {
log.Printf("Предупреждение: не удалось прочитать название ОФД из таблицы 18, поля 10: %v", err)
} else {
info.OfdName = strings.TrimSpace(ofdName)
}
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))
switch ffdValue {
case 2:
info.FfdVersion = "105"
case 4:
info.FfdVersion = "120"
default:
info.FfdVersion = fmt.Sprintf("неизвестный код (%d)", ffdValue)
}
}
return nil
}
func (d *comDriver) readTableField(tableNum, rowNum, fieldNum int) (string, error) {
oleutil.PutProperty(d.dispatch, "TableNumber", tableNum)
oleutil.PutProperty(d.dispatch, "RowNumber", rowNum)
oleutil.PutProperty(d.dispatch, "FieldNumber", fieldNum)
if _, err := oleutil.CallMethod(d.dispatch, "ReadTable"); err != nil {
return "", err
}
if err := d.checkError(); err != nil {
return "", err
}
return d.getPropertyString("ValueOfFieldString")
}
func (d *comDriver) checkError() error {
resultCode, err := d.getPropertyInt32("ResultCode")
if err != nil {
return fmt.Errorf("не удалось прочитать ResultCode: %w", err)
}
if resultCode != 0 {
description, _ := d.getPropertyString("ResultCodeDescription")
return fmt.Errorf("ошибка драйвера: [%d] %s", resultCode, description)
}
return nil
}
func (d *comDriver) getPropertyVariant(propName string) (*ole.VARIANT, error) {
return oleutil.GetProperty(d.dispatch, propName)
}
func (d *comDriver) getPropertyString(propName string) (string, error) {
variant, err := d.getPropertyVariant(propName)
if err != nil {
return "", fmt.Errorf("не удалось получить свойство '%s': %w", propName, err)
}
defer variant.Clear()
return variant.ToString(), nil
}
func (d *comDriver) getPropertyInt32(propName string) (int32, error) {
variant, err := d.getPropertyVariant(propName)
if err != nil {
return 0, fmt.Errorf("не удалось получить свойство '%s': %w", propName, err)
}
defer variant.Clear()
v := variant.Value()
if v == nil {
return 0, nil
}
switch val := v.(type) {
case int:
return int32(val), nil
case int8:
return int32(val), nil
case int16:
return int32(val), nil
case int32:
return val, nil
case int64:
return int32(val), nil
case uint:
return int32(val), nil
case uint8:
return int32(val), nil
case uint16:
return int32(val), nil
case uint32:
return int32(val), nil
case uint64:
return int32(val), nil
case string:
i, err := strconv.ParseInt(strings.TrimSpace(val), 10, 32)
if err == nil {
return int32(i), nil
}
return 0, fmt.Errorf("не удалось сконвертировать строку '%s' в int32 для свойства %s", val, propName)
default:
return 0, fmt.Errorf("неожиданный тип для %s: %T", propName, v)
}
}
// --- НОВЫЙ И ИЗМЕНЕННЫЙ КОД ---
// SearchDevices выполняет последовательный поиск ККТ на COM-портах,
// а затем параллельный поиск в стандартных RNDIS IP-подсетях.
func SearchDevices(comTimeout, tcpTimeout time.Duration) ([]Config, error) {
log.Println("--- Начинаю поиск устройств на COM-портах ---")
var foundDevices []Config
// --- Этап 1: Последовательный поиск на COM-портах ---
ports, err := serial.GetPortsList()
if err != nil {
log.Printf("Не удалось получить список COM-портов: %v", err)
} else if len(ports) == 0 {
log.Println("В системе не найдено COM-портов.")
} else {
log.Printf("Найдены COM-порты: %v. Начинаю проверку...", ports)
for _, portName := range ports {
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-сетях ---
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)
}
log.Printf("--- Поиск завершен. Всего найдено устройств: %d ---", len(foundDevices))
return foundDevices, nil
}
// findOnComPort инкапсулирует весь жизненный цикл COM для поиска на одном порту.
// Он включает дополнительную верификацию через быстрый запрос состояния.
func findOnComPort(portName string, timeout time.Duration) (*Config, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
comNum, err := strconv.Atoi(strings.TrimPrefix(strings.ToUpper(portName), "COM"))
if err != nil {
return nil, fmt.Errorf("некорректное имя порта: %s", portName)
}
// Стандартные скорости обмена для ККТ
baudRates := []int32{115200, 57600, 38400, 19200, 9600}
// В документации BaudRate - это индекс от 0 до 6.
// 6 = 115200, 5 = 57600, 4 = 38400, 3 = 19200, 2 = 9600
baudRateIndexes := map[int32]int32{
115200: 6,
57600: 5,
38400: 4,
19200: 3,
9600: 2,
}
for _, baud := range baudRates {
// Для каждой скорости выполняем полную проверку
if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); err != nil {
if err := ole.CoInitialize(0); err != nil {
continue
}
}
unknown, err := oleutil.CreateObject("AddIn.DrvFR")
if err != nil {
ole.CoUninitialize()
continue
}
dispatch, err := unknown.QueryInterface(ole.IID_IDispatch)
if err != nil {
unknown.Release()
ole.CoUninitialize()
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()) // Устанавливаем короткий таймаут!
// Пытаемся подключиться
_, connectErr := oleutil.CallMethod(dispatch, "Connect")
tempDriver := &comDriver{dispatch: dispatch}
checkErr := tempDriver.checkError()
if connectErr == nil && checkErr == nil {
// Успех!
log.Printf("Успешная верификация на порту %s, скорость %d", portName, baud)
dispatch.Release()
unknown.Release()
ole.CoUninitialize()
return &Config{
ConnectionType: 0,
ComName: portName,
ComNumber: int32(comNum),
BaudRate: baudRateIndexes[baud],
Password: 30,
}, nil
}
// Неудача, освобождаем ресурсы и пробуем следующую скорость
dispatch.Release()
unknown.Release()
ole.CoUninitialize()
}
return nil, fmt.Errorf("устройство не найдено на порту %s", portName)
}
// scanRNDISNetworks ищет устройства в стандартных RNDIS-подсетях.
func scanRNDISNetworks(timeout time.Duration, foundChan chan<- Config) {
var wg sync.WaitGroup
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{}{} // Занимаем слот
go func(ip string, port int32) {
defer wg.Done()
checkIP(ip, port, timeout, foundChan)
<-guard // Освобождаем слот
}(ip, ports[0])
}
}
wg.Wait()
}
// checkIP проверяет доступность порта и верифицирует, что на нем находится ККТ.
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 // Порт закрыт или хост недоступен
}
conn.Close()
// Порт открыт, теперь проверяем, действительно ли это наша ККТ
log.Printf("Найден открытый порт на %s. Проверяю совместимость...", address)
config := Config{
ConnectionType: 6,
IPAddress: ip,
TCPPort: port,
Password: 30,
}
driver := New(config)
if err := driver.Connect(); err == nil {
log.Printf("!!! Найдено и подтверждено устройство по TCP/IP: %s", address)
foundChan <- config
driver.Disconnect()
} else {
log.Printf("Порт %s открыт, но устройство не ответило как ККТ: %v", address, err)
}
}

33
pkg/shtrih/mock_driver.go Normal file
View File

@@ -0,0 +1,33 @@
package shtrih
// mockDriver — это имитация драйвера для тестирования.
type mockDriver struct {
FiscalInfoToReturn *FiscalInfo
ErrorToReturn error
}
// NewMock создает новый мок-драйвер.
func NewMock(info *FiscalInfo, err error) Driver {
return &mockDriver{
FiscalInfoToReturn: info,
ErrorToReturn: err,
}
}
func (m *mockDriver) Connect() error {
if m.ErrorToReturn != nil {
return m.ErrorToReturn
}
return nil
}
func (m *mockDriver) Disconnect() error {
return nil
}
func (m *mockDriver) GetFiscalInfo() (*FiscalInfo, error) {
if m.ErrorToReturn != nil {
return nil, m.ErrorToReturn
}
return m.FiscalInfoToReturn, nil
}