class_name yau_07 extends Node

## Протокол ячейки ЯУ-07.

const ONLINE_TIMEOUT: = 5000        ## Время ожидания пакета от ячейки, милисекунды
const RESEND_TIMEOUT: = 300         ## Задержка для повторной отправки команды, милисекунды
const RESEND_TIMEOUT_FLASH: = 1500  ## Задержка для повторной отправки команды флеш, милисекунды
const RETRY_COUNT:    = 5           ## Количество повторных отправок
const BUFFER_SIZE:    = 2048        ## Размер буфера для приёма датаграмм


## Ячейка ЯУ-07.
class YaU07 extends unit.Unit:

    ## Коды выполняемых команд.
    enum CmdCode {
        READ_ISA  = 0,          ## Код команды "читать порт ISA" ячейки ЯУ-07
        WRITE_ISA = 1,          ## Код команды "записать порт ISA" ячейки ЯУ-07
        ERASE_FLASH = 49,       ## Код команды "стереть данные из FLASH" ячейки ЯУ-07
        WRITE_FLASH = 50,       ## Код команды "записать данные в FLASH" ячейки ЯУ-07
        READ_FLASH = 51,        ## Код команды "прочитать данные из FLASH" ячейки ЯУ-07
        LOAD_FLASH = 52,        ## Код команды "записать данные из FLASH в КЭМС" ячейки ЯУ-07
        READ_FLASH_MP550 = 56,  ## Код команды "прочитать данные из FLASH" ячейки ЯУ-07
        WRITE_FLASH_MP550 = 55, ## Код команды "записать данные из FLASH в КЭМС" ячейки ЯУ-07
        WRITE_BLOCK = 60,       ## Код команды "записать блок данных ISA" внешней шины ячейки ЯУ-07
        READ_BLOCK  = 61,       ## Код команды "читать блок данных ISA" внешней шины ячейки ЯУ-07
        LOAD_EMS_DONE  = 70,    ## Код команды "читать блок данных ISA" внешней шины ячейки ЯУ-07
        }

    var cmd_num_rx: = 0                  ## Номер команды принятый
    var cmd_num_tx: = 0                  ## Номер команды переданный
    var cmd_result: = 1                  ## Результат выполнения команды. Устанавливается в ЯУ-07
    var cmd_data:  = PackedByteArray()   ## Данные команды. Устанавливается в ЯУ-07
    var cmd_retry:  = RETRY_COUNT        ## Счётчик оставшихся попыток выполнить команду
    var cmd_code_rx: = 0                 ## Код последней выполненой команды
    var cmd_code_tx: = 0                 ## Код последней отправленной команды
    var cmd_tick_tx: = 0                 ## Время последней отправки команды
    var cmd_tick_rx: = 0                 ## Время последнего приёма команды
    var status:     = PackedByteArray()  ## Поле "состояние прибора"
    var isa_ports:  = {}                 ## Результат выполнения команды "читать порт"
    var isa_block:  = {}                 ## Результат выполнения команды "читать массив из порта"
    var flash_data: = PackedByteArray()  ## Данные прочитанные из Flash
    var flash_len:  = 0                  ## Длинна данных прочитанных из Flash


    func _to_string() -> String: return String('яу07: %s %s' % [self.name, ['отключен', 'на связи'][int(online)]])


    func _init(unit_name: StringName):
        self.name = unit_name
        tx_data.resize(BUFFER_SIZE)
        tx_data.fill(0)


    func process(tick: int):
        if online and ((tick - cmd_tick_rx) > ONLINE_TIMEOUT):
            online = false
            emit_signal('line_changed', self)

        var rc: = Error.ERR_BUSY
        if cmd_state == CmdState.SEND:
            rc = Error.OK
            cmd_tick_tx = tick
            cmd_state = CmdState.WAIT
            cmd_retry = RETRY_COUNT
        elif cmd_state == CmdState.WAIT:
            if cmd_retry:
                var res_timeout = RESEND_TIMEOUT
                if cmd_code_tx == CmdCode.WRITE_FLASH_MP550:
                    res_timeout = RESEND_TIMEOUT_FLASH
                if (tick - cmd_tick_tx) > res_timeout:
                    rc = Error.OK
                    cmd_tick_tx = tick
                    cmd_retry -= 1
            else:
                cmd_state = CmdState.FAIL
                emit_signal('command_fail', self)
                rc = Error.FAILED
        return rc

    func send_flash(cmd_code: int, fl_data: Array, num_byte_rd: int, id_sector: int = 0) -> bool:
        if (cmd_state == CmdState.WAIT) \
        or (cmd_state == CmdState.SEND) \
        or (cmd_state == CmdState.UNCK):
            return false
        if not ((cmd_code == CmdCode.ERASE_FLASH) or (cmd_code == CmdCode.WRITE_FLASH) \
            or (cmd_code == CmdCode.READ_FLASH) or (cmd_code == CmdCode.LOAD_FLASH)
            or (cmd_code == CmdCode.READ_FLASH_MP550) or (cmd_code == CmdCode.WRITE_FLASH_MP550)):
            return false
        cmd_num_tx = (cmd_num_rx + 1) % 0xffff
        tx_data.encode_u16(0, cmd_retry)
        tx_data.encode_u16(2, cmd_num_tx)
        tx_data.encode_u8(6, cmd_code)
        if cmd_code == CmdCode.ERASE_FLASH:
            tx_len = 7
        elif cmd_code == CmdCode.WRITE_FLASH:
            tx_data.encode_u8(7, 1)
            tx_data.encode_u16(8, 0)
            tx_data.encode_u16(10, fl_data.size())
            tx_len = 11 + fl_data.size() * 2
            for i in fl_data.size():
                tx_data.encode_u16(12 + i * 2, fl_data[i])
        elif cmd_code == CmdCode.WRITE_FLASH_MP550:
            tx_data.encode_u8(7, id_sector)
            tx_data.encode_u16(8, 0)
            tx_data.encode_u16(10, fl_data.size() * 2)
            tx_len = 12 + fl_data.size() * 2
            for i in fl_data.size():
                tx_data.encode_u16(12 + i * 2, fl_data[i])
        elif cmd_code == CmdCode.READ_FLASH:
            tx_len = 11
            tx_data.encode_u16(7, 0)
            tx_data.encode_u16(9, num_byte_rd)
        elif cmd_code == CmdCode.READ_FLASH_MP550:
            tx_len = 12
            tx_data.encode_u8(7, id_sector)
            tx_data.encode_u16(8, 0)
            tx_data.encode_u16(10, num_byte_rd)
        cmd_state = CmdState.SEND
        cmd_code_tx = cmd_code
        return true

    func send_isa(cmd_code: int, ports_data: Array) -> bool:
        if (cmd_state == CmdState.WAIT) \
        or (cmd_state == CmdState.SEND) \
        or (cmd_state == CmdState.UNCK):
            return false
        if not ((cmd_code == CmdCode.READ_ISA) or (cmd_code == CmdCode.WRITE_ISA)):
            return false

        cmd_num_tx = (cmd_num_rx + 1) % 0xffff

        var ports_count: = ports_data.size()
        tx_len = ports_count * 2 + 8
        tx_data.encode_u16(0, cmd_retry)  # Исправление для сетевого стека "nm-callback" процессоров STM
        tx_data.encode_u16(2, cmd_num_tx)

        if cmd_code == CmdCode.WRITE_ISA:
            ports_count /= 2
        tx_data[6] = cmd_code
        tx_data[7] = ports_count

        var i: = 0
        for word in ports_data:
            tx_data.encode_u16(8 + i, word)
            i += 2
        cmd_state = CmdState.SEND
        cmd_code_tx = cmd_code
        return true

    func send_block(cmd_code: int, block_len: int, port: Array) -> bool:
        if (cmd_state == CmdState.WAIT) \
        or (cmd_state == CmdState.SEND) \
        or (cmd_state == CmdState.UNCK):
            return false
        if not ((cmd_code == CmdCode.READ_BLOCK) or (cmd_code == CmdCode.WRITE_BLOCK)):
            return false

        cmd_num_tx = (cmd_num_rx + 1) % 0xffff

        tx_len = 11
        tx_data.encode_u16(0, cmd_retry)  # Исправление для сетевого стека "nm-callback" процессоров STM
        tx_data.encode_u16(2, cmd_num_tx)

        if cmd_code == CmdCode.WRITE_BLOCK:
            block_len /= 2
        tx_data[6] = cmd_code
        tx_data.encode_u16(7, block_len)
        tx_data.encode_u16(9, int(port[0]))
        cmd_state = CmdState.SEND
        cmd_code_tx = cmd_code
        return true

    func send_load_ems_done(cmd_code: int):
        if (cmd_state == CmdState.WAIT) \
        or (cmd_state == CmdState.SEND) \
        or (cmd_state == CmdState.UNCK):
            return false
        if cmd_code != CmdCode.LOAD_EMS_DONE:
            return false
        cmd_num_tx = (cmd_num_rx + 1) % 0xffff
        tx_data.encode_u16(0, cmd_retry)
        tx_data.encode_u16(2, cmd_num_tx)
        tx_data.encode_u8(6, cmd_code)
        tx_len = 7
        cmd_state = CmdState.SEND
        cmd_code_tx = cmd_code

    ## Возвращает код команды ЯУ-07.
    ## rx_data - Принятые данные.
    ## Возвращает - Код команды не найден, 0...255 - код команды.
    func get_cmd_code(rx_data: PackedByteArray):

        var rx_data_len: = rx_data.size()
        if rx_data_len < 7:
            return null
        var status_len: = rx_data.decode_u16(6)  # Длина массива байтового "состояние прибора"
        if rx_data_len <= (status_len + 6):      # Нет поля "ответ на команду", разбор пакета завершить
            return null
        var cmd_code_pos: = status_len + 8       # Положение в приёмном буфере "кода команды"
        if rx_data_len <= cmd_code_pos:
            return null
        return rx_data[cmd_code_pos]             # "код команды"


    func parse_isa(rx_data: PackedByteArray, status_len: int) -> bool:
        var ports_count_pos: = status_len + 10            # Положение в приёмном буфере количества портов
        if rx_data.size() < ports_count_pos:
            return false
        var ports_count: = rx_data[ports_count_pos]              # Количество портов
        var counted_len: = status_len + (ports_count * 4) + 11   # Вычисленная длина пакета
        if rx_data.size() < counted_len:
            return false
        var addr_pos: = status_len + 11  # Смещение положения адреса в приёмном буфере
        var data_pos: = status_len + 13  # Смещение положения данных в приёмном буфере
        var i: = 0
        while i < ports_count:
            var shift: = i * 4
            if rx_data.size() < (data_pos + shift):
                return false
            var isa_addr: = rx_data.decode_u16(addr_pos + shift)  # Адрес порта ISA яу-07
            var isa_data: = rx_data.decode_u16(data_pos + shift)  # Данные порта ISA яу-07
            isa_ports[isa_addr] = isa_data
            i += 1
        cmd_data = rx_data.slice(addr_pos, (addr_pos + ports_count * 4) - 1)
        return i > 0

    func parse_block(rx_data: PackedByteArray, status_len: int) -> bool:
        var ports_count_pos: = status_len + 7             # Положение в приёмном буфере количества портов
        if rx_data.size() < ports_count_pos:
            return false
        if rx_data.size() < (ports_count_pos + 5):
            return false
        var block_len: = rx_data.decode_u16(ports_count_pos + 3)
        var block_adr: = rx_data.decode_u16(ports_count_pos + 5)
        var block_data: = []
        if rx_data.size() < (ports_count_pos + 7 + (block_len - 1) * 2):
                return false
        for i: int in block_len:
            var shift: int = i * 2
            block_data.append(rx_data.decode_u16(ports_count_pos + 7 + shift))
        isa_block[block_adr] = block_data
        return true

    func parse_flash(rx_data: PackedByteArray, status_len: int, cmd_code: CmdCode) -> bool:
        if cmd_code == CmdCode.READ_FLASH:
            if rx_data.size() < (status_len + 14):
                return false
            flash_len = rx_data.decode_u16(status_len + 12)
            flash_data = rx_data.slice(status_len + 14)
        if cmd_code == CmdCode.READ_FLASH_MP550:
            if rx_data.size() < (status_len + 15):
                return false
            flash_len = rx_data.decode_u16(status_len + 13)
            flash_data = rx_data.slice(status_len + 15)
        return true

    ## Заполняет состояние прибора данными прочитанными из портов ЯУ-07.
    ## для команд "читать порты" или "записать порты".[br]
    ## [param rx_data] - Принятые данные.[br]
    ## Возвращает [b]true[/b] - Данные обработаны успешно.[br]
    ## Возвращает [b]false[/b] - Данные не обработаны из за ошибки.[br]
    func parse(rx_data: PackedByteArray, tick: int) -> bool:

        cmd_tick_rx = tick

        if not online:
            online = true
            emit_signal('line_changed', self)

        var rx_data_len: int = rx_data.size()
        if rx_data_len < 10:
            return false

        cmd_num_rx = rx_data.decode_u16(2)
        if cmd_state == CmdState.UNCK:
            cmd_state = CmdState.DONE

        var status_len: int = rx_data.decode_u16(6)   # Длина массива байтового "состояние прибора"
        if rx_data_len < (status_len + 6):
            return false
        var i: = 0
        var len_rx_data: = rx_data.size()
        status.resize(status_len)
        while i < status_len:
            var byte_i: = i + 8
            if byte_i < len_rx_data:
                status[i] = rx_data[byte_i]                 # Массив байтовый "состояние прибора"
            i += 1
        emit_signal('data_received', self)

        if rx_data_len == (status_len + 6):
            return true                                     # Нет поля "ответ на команду", разбор пакета завершить

        var cmd_code_pos: = status_len + 8                  # Положение в приёмном буфере "код команды"
        if rx_data_len <= cmd_code_pos:
            return false

        var cmd_status_pos: = status_len + 9                # Положение в приёмном буфере "состояния выполнения команды"
        if rx_data_len <= cmd_status_pos:
            return false
        cmd_result = rx_data[cmd_status_pos]
        if cmd_result != 1:                                 # Команда не выполнена
            return false

        var cmd_code: CmdCode = rx_data[cmd_code_pos] as CmdCode       # "код команды"
        var rc: = true
        if (cmd_code == CmdCode.READ_ISA) or (cmd_code == CmdCode.WRITE_ISA):
            rc = parse_isa(rx_data, status_len)

        elif (cmd_code == CmdCode.READ_BLOCK) or (cmd_code == CmdCode.WRITE_BLOCK):
            rc = parse_block(rx_data, status_len)

        elif (cmd_code == CmdCode.ERASE_FLASH) or (cmd_code == CmdCode.WRITE_FLASH) \
            or (cmd_code == CmdCode.READ_FLASH_MP550) or (cmd_code == CmdCode.WRITE_FLASH_MP550) \
            or (cmd_code == CmdCode.READ_FLASH) or (cmd_code == CmdCode.LOAD_FLASH):
            rc = parse_flash(rx_data, status_len, cmd_code)

        if rc:
            if (cmd_state == CmdState.WAIT) and (cmd_num_rx == cmd_num_tx):
                cmd_state = CmdState.DONE
                cmd_code_rx = cmd_code
                emit_signal('command_done', self)
        else:
            emit_signal('parse_failed', self)
        return true
