"""Minimal verified NET example for DSRemoteConnect 1.1.3.x, 64-bit Python.

Reads one synchronous scalar channel over the NET (TCP/IP) transport.

    py DSRemoteNET_MinimalExample.py 192.168.77.1 8999 8001 current_welding

Connection string is  <dewesoft_ip>:<net_command_port>:<data_port>.
The data port is a port on THIS machine: the DLL listens on it and DewesoftX
connects back to it. It must be reachable inbound from the DewesoftX PC.

Three things this example does that the older published examples do not, and
that are each sufficient on their own to produce "zero samples forever":

  1. argtypes/restype are declared. Without them ctypes truncates 64-bit
     handles to 32 bits.
  2. `count` is reset to the buffer capacity before EVERY read. It is an
     in/out parameter: on entry it is the caller's capacity, on exit it is
     the number of samples written. Passing 0 in returns 0 out, forever.
  3. dsconSetBufferSize() is called BEFORE dsconStartMeasurement(). The
     default per-channel ring buffer is 3000 samples - 15 ms at 200 kHz -
     so at high rates every incoming block overruns it.
"""
import ctypes
import os
import sys
import time
from ctypes import POINTER, byref, c_char_p, c_double, c_int, c_size_t, c_void_p

# ctypes will not resolve a bare file name from the working directory, so anchor
# the default to this script's own folder.
DLL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                        "DSRemoteConnect64.dll")

DSX_NO_ERROR = 0
DSX_NET_BUFFER_OVERWRITTEN = -21
DSX_CONNECTION_TYPE_NET = 1
CH_TYPE = {0: "async", 1: "sync", 2: "single value"}

POLL_HZ = 20.0          # how often the read loop runs
BUFFER_SECONDS = 2.0    # ring buffer depth, in seconds of signal


def load(path):
    dll = ctypes.WinDLL(path)
    sig = {
        "dsconGetVersion":                    ([c_char_p, c_size_t], c_int),
        "dsconCreateInstance":                ([POINTER(c_void_p), c_int], c_int),
        "dsconConnect":                       ([c_void_p, c_char_p], c_int),
        "dsconDisconnect":                    ([c_void_p], c_int),
        "dsconDestroyInstance":               ([c_void_p], c_int),
        "dsconGetChannelCount":               ([c_void_p, POINTER(c_size_t)], c_int),
        "dsconCreateChannelInstanceByNameUtf8": ([c_void_p, c_char_p, POINTER(c_void_p)], c_int),
        "dsconFreeChannelInstance":           ([c_void_p], c_int),
        "dsconChannelGetNameUtf8":            ([c_void_p, c_char_p, c_size_t, POINTER(c_size_t)], c_int),
        "dsconGetChUnit":                     ([c_void_p, c_char_p, c_size_t], c_int),
        "dsconGetSampleRate":                 ([c_void_p, POINTER(c_double)], c_int),
        "dsconGetChannelType":                ([c_void_p, POINTER(c_int)], c_int),
        "dsconSetBufferSize":                 ([c_void_p, c_int], c_int),
        "dsconStartMeasurement":              ([c_void_p], c_int),
        "dsconStopMeasurement":               ([c_void_p], c_int),
        "dsconGetState":                      ([c_void_p, POINTER(c_int)], c_int),
        "dsconChannelReadScalarData_2":       ([c_void_p, POINTER(c_double), POINTER(c_double), POINTER(c_size_t)], c_int),
    }
    for name, (argtypes, restype) in sig.items():
        fn = getattr(dll, name)
        fn.argtypes, fn.restype = argtypes, restype
    return dll


def main(host, cmd_port, data_port, channel_name, dll_path=DLL_PATH):
    dll = load(dll_path)

    ver = ctypes.create_string_buffer(64)
    dll.dsconGetVersion(ver, len(ver))
    print("DSRemote version:", ver.value.decode())

    inst = c_void_p()
    rc = dll.dsconCreateInstance(byref(inst), DSX_CONNECTION_TYPE_NET)
    assert rc == DSX_NO_ERROR, "dsconCreateInstance -> %d" % rc

    conn = ("%s:%s:%s" % (host, cmd_port, data_port)).encode()
    dll.dsconConnect(inst, conn)
    print("connected to", conn.decode())

    # LISTUSEDCHS is (re)parsed here. Do NOT call this again after creating
    # channel instances - it rebuilds the channel table and silently clears
    # the transferred flags, which drops your channel from the transfer set.
    count = c_size_t()
    dll.dsconGetChannelCount(inst, byref(count))
    print("channels in setup:", count.value)

    ch = c_void_p()
    rc = dll.dsconCreateChannelInstanceByNameUtf8(
        inst, channel_name.encode("utf-8"), byref(ch))
    assert rc == DSX_NO_ERROR, (
        "channel %r not found (rc=%d). Names are case sensitive and must "
        "match DewesoftX exactly." % (channel_name, rc))
    # Creating the instance already marks the channel as transferred.
    # dsconChannelSetTransferred(ch, True) is optional here.

    rate = c_double()
    dll.dsconGetSampleRate(ch, byref(rate))
    ch_type = c_int()
    dll.dsconGetChannelType(ch, byref(ch_type))
    unit = ctypes.create_string_buffer(64)
    dll.dsconGetChUnit(ch, unit, len(unit))
    print("%s: %g Hz, unit %s, type %s"
          % (channel_name, rate.value, unit.value.decode(),
             CH_TYPE.get(ch_type.value, ch_type.value)))

    # Ring buffer must hold more than one DewesoftX block. Sized in samples.
    capacity = max(3000, int(rate.value * BUFFER_SECONDS))
    rc = dll.dsconSetBufferSize(inst, capacity)
    print("ring buffer: %d samples (%.2f s), dsconSetBufferSize rc=%d"
          % (capacity, capacity / rate.value, rc))

    # Drives the whole NET handshake: STOP if measuring, PREPARETRANSFER,
    # opens the local data port, STARTTRANSFER, STARTACQ. Call it exactly once.
    rc = dll.dsconStartMeasurement(inst)
    print("dsconStartMeasurement rc=%d" % rc)

    state = c_int()
    dll.dsconGetState(inst, byref(state))
    print("DewesoftX state: %d (2 = acquiring)" % state.value)

    data = (c_double * capacity)()
    stamps = (c_double * capacity)()
    total, empty_polls = 0, 0
    print("\nreading, Ctrl+C to stop")
    try:
        while True:
            time.sleep(1.0 / POLL_HZ)
            n = c_size_t(capacity)        # reset EVERY call - see note 2 above
            rc = dll.dsconChannelReadScalarData_2(ch, data, stamps, byref(n))

            if rc == DSX_NET_BUFFER_OVERWRITTEN:
                print("  WARNING rc=-21: ring buffer overrun, samples were "
                      "dropped. Increase BUFFER_SECONDS or POLL_HZ.")
            elif rc != DSX_NO_ERROR:
                print("  read error rc=%d" % rc)
                break

            if n.value == 0:
                empty_polls += 1
                if empty_polls == int(POLL_HZ * 5):
                    print("  no samples for 5 s - DewesoftX is not delivering "
                          "data. Check that it opened a TCP connection to this "
                          "machine on port %s (netstat -an | findstr %s should "
                          "show ESTABLISHED, not just LISTENING)."
                          % (data_port, data_port))
                continue

            empty_polls = 0
            total += n.value
            print("  %6d samples  t=%.6f..%.6f  first=%.6f %s  total=%d"
                  % (n.value, stamps[0], stamps[n.value - 1], data[0],
                     unit.value.decode(), total))
    except KeyboardInterrupt:
        print("\nstopping")

    dll.dsconStopMeasurement(inst)
    dll.dsconFreeChannelInstance(ch)
    dll.dsconDisconnect(inst)
    dll.dsconDestroyInstance(inst)
    print("done, %d samples read" % total)


if __name__ == "__main__":
    args = sys.argv[1:]
    if len(args) < 4:
        print(__doc__)
        sys.exit(2)
    main(args[0], args[1], args[2], args[3],
         args[4] if len(args) > 4 else DLL_PATH)
