#!/usr/bin/env python3

from threading import Timer
from struct import unpack

import gi
gi.require_versions({'GLib': '2.0', 'Hinawa': '4.0', 'Hinoko': '1.0'})
from gi.repository import GLib, Hinawa, Hinoko

class IsoIrSingle(Hinoko.FwIsoIrSingle):
    def new(self, path, channel, maximum_bytes_per_payload):
        self.allocate(path, channel, 8)

        # Use 10msec interval for I/O operation.
        self.map_buffer(maximum_bytes_per_payload, 240)

    def destroy(self):
        self.unmap_buffer()
        self.release()

    def queue_packet(self):
        schedule_interrupt = False

        self.accumulated_packet_count += 1
        if self.accumulated_packet_count % self.packets_per_interrupt == 0:
            schedule_interrupt = True

        self.register_packet(schedule_interrupt)

    def begin(self, dispatcher, duration):
        _, self.__src = self.create_source()
        self.__src.attach(dispatcher.get_context())
        self.__dispatcher = dispatcher

        # Roughly finish event loop.
        self.__timer = Timer(duration, lambda dispatcher: dispatcher.quit(),
                             args=(dispatcher,))

        tags = Hinoko.FwIsoCtxMatchFlag.TAG0 | \
               Hinoko.FwIsoCtxMatchFlag.TAG1 | \
               Hinoko.FwIsoCtxMatchFlag.TAG2 | \
               Hinoko.FwIsoCtxMatchFlag.TAG3

        # Start one half of second later. Scheduling is available with lower 2 bits of second field.
        cycle_time = Hinawa.CycleTime.new()
        clock_id = 4    #CLOCK_MONOTONIC_RAW
        _, cycle_time = self.read_cycle_time(clock_id, cycle_time)
        (sec, cycle, tick) = cycle_time.get_fields()
        sec, cycle = self.__increment_cycle(sec, cycle, 4000)
        cycle_match = (sec & 0x3, cycle)

        self.accumulated_packet_count = 0
        self.packets_per_interrupt = 80

        # Schedule for some isochronous cycles.
        for i in range(self.packets_per_interrupt * 2):
            self.queue_packet()

        self.__timer.start()
        self.start(cycle_match, 0, tags)

    @staticmethod
    def __increment_cycle(sec, cycle, addend):
        cycle += addend
        if cycle >= 8000:
            cycle %= 8000
            sec += 1
            if sec >= 128:
                sec %= 128
        return (sec, cycle)

    def finish(self):
        self.__src.destroy()
        self.stop()

    def do_stopped(self, error):
        self.__dispatcher.quit()
        if error:
            self.__timer.cancel()
            print(error)

    @staticmethod
    def __ohci1394_tstamp_to_isoc_cycle(tstamp):
        sec = (tstamp & 0x0000e000) >> 13
        cycle = tstamp & 0x00001fff
        return (sec, cycle)

    def do_interrupted(self, sec, cycle, header, header_length, count):
        headers = unpack('>{0}I'.format(header_length // 4), header)
        for i in range(count):
            iso_header, tstamp = headers[i * 2:(i + 1) * 2]

            sec, cycle = self.__ohci1394_tstamp_to_isoc_cycle(tstamp)

            data = self.get_payload(i)
            frames = unpack('>{0}I'.format(len(data) // 4), data)

            print('{0:d},{1:d},{2:08x},{3}'.format(sec, cycle, iso_header, i))
            for i, frame in enumerate(frames):
                print('    {0:2d}: {1:08x}'.format(i, frame))

            self.queue_packet()

# 2 CIP headers + 3 quadlet per data block * 8 data block
channel = 1
maximum_bytes_per_payload = (2 + 3 * 8) * 4

ctx = IsoIrSingle()
ctx.new('/dev/fw0', channel, maximum_bytes_per_payload)

dispatcher = GLib.MainLoop.new(None, False)

ctx.begin(dispatcher, 4)
dispatcher.run()
ctx.finish()
ctx.destroy()

del dispatcher
del ctx
