0.1.0-prod
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.exe
|
||||||
|
*.json
|
||||||
13
go.mod
Normal file
13
go.mod
Normal 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
17
go.sum
Normal 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
384
main.go
Normal 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
574
pkg/shtrih/driver.go
Normal 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
33
pkg/shtrih/mock_driver.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user