
# nsf_recording.py
# Created by Caleb Payne
# Copyright (C) 2019, Power Monitors, Inc.
# All rights reserved.
#

__version__				= "3.0.0_0"
__author__				= ("Caleb Payne", "cpayne@powermonitors.com")

import numpy as np
from ctypes import *
import sys
import os
import json
import math

# Libray Initialization Routines
__PATHS							= ["libnsf.so"]

def __libload():
	lib = None
	for path in __PATHS:
		if os.path.exists(path):
			lib = cdll.LoadLibrary(path)

	if not lib:
		raise Exception("Failed to load libnsf.so from: %s" % ",".join(__PATHS))

	return lib

libnsf = __libload()

# Structure definitions
class waveform_time_rec(Structure):
	_pack_ = 1
	_fields_ = [
		("start_time", c_uint),
		("cycle", c_ubyte),
		("num_cycles", c_ushort),
		("trigger_chan", c_ubyte),
		("trigger_type", c_ubyte),
		("trigger_value", c_byte)
	]

class waveform_time(Structure):
	_pack_ = 1
	_fields_ = [
		("count", c_uint),
		("records", POINTER(waveform_time_rec))
	]

class waveform_capture(Structure):
	_pack_ = 1
	_fields_ = [
		("num_cycles", c_int),
		("points_per_cycle", c_int),
		("num_points", c_int),
		("vchan", c_int),
		("ichan", c_int),
		("pchan", c_int),
		("start_cycle", c_int),
		("start_time", c_uint),
		("pre_cycles", c_ushort),
		("post_cycles", c_ushort),
		("periodic", c_ubyte),
		("voltage_waveform", POINTER(POINTER(c_float))),
		("current_waveform", POINTER(POINTER(c_float))),
		("rms_voltage", (c_float * 4)),
		("rms_current", (c_float * 4)),
		("channel", c_uint),
		("trigger_type", c_uint),
		("trigger_value", c_float)
	]

#
# NSF Recording Class
#
class nsf_recording():

	__file_handle = None
	__name = None

	def __init__(self,name,mode="r+b"):
		libnsf.nsf_get_recording_waveform_by_timestamp_cycle.argtypes = [POINTER(c_ulong), c_uint, c_ubyte, POINTER(waveform_capture), c_char_p, c_int]
		libnsf.nsf_init.argtypes = [POINTER(c_ulong), c_char_p, c_char_p, c_char_p, c_int]

		self.__file_handle = c_ulong(0)
		self.__name = name
		err_str = create_string_buffer(256)

		filename = create_string_buffer(len(name)+1)
		filename.value = name,
		mstr = create_string_buffer(len(mode) + 1)
		mstr.value = mode

		res = libnsf.nsf_init(byref(self.__file_handle),
			filename, mstr, err_str, 256)

		if res == -1:
			raise Exception("%s" % err_str.value)

	def measure_string(self, measure):
		measures = [
			"voltage",
			"current",
			"real_power",
			"reactive_power",
			"apparent_power",
			"pf",
			"dpf",
			"phase_angle",
			"vthd",
			"ithd",
			"phase"
		]

		return measures[measure]

	def list_waveforms(self,index=0,count=0):
		"""
		Returns a list of waveforms available to be retrieved from
		this recording.

		Parameters
		==========

		index           : int  - optional index.  Default = 0
		count           : int  - optional count.  Default = 0 (disabled)

		Returns
		=======

		array of elements with the following attributes:
		    "timestamp": int,
		    "cycle": int,
		    "duration": int,
		    "triggerChannel": int,
		    "triggerType": int
		"""

		wave_list = waveform_time()
		err_str = create_string_buffer(256)
		waves = []

		ret = libnsf.nsf_get_recording_waveform_timestamps(
			byref(self.__file_handle),
			byref(wave_list),
			err_str, 256
		)

		if ret == -1:
			raise Exception("%s" % err_str.value)

		elif ret == -2:
			waves = []

		else:
			for i in range(0, wave_list.count):
				rec = wave_list.records[i]

				duration = ((1. / 60) * rec.num_cycles) * 10**3 # milliseconds

				waves.append({
					"timestamp": rec.start_time,
					"cycle": rec.cycle,
					"duration": duration,
					"triggerChannel": rec.trigger_chan+1,
					"triggerType": rec.trigger_type
				})

			libnsf.nsf_free_recording_waveform_timestamps(byref(wave_list))

		if index or count:
			return waves[index:(index+count)]
		else:
			return waves

	def __waveform_by_ts_cycle(self,ts,cycle,channel,measure):
		wave = waveform_capture()
		err_str = create_string_buffer(256)

		ret = libnsf.nsf_get_recording_waveform_by_timestamp_cycle(
			byref(self.__file_handle),
			ts,
			cycle,
			byref(wave),
			err_str, 256
		)

		waveform = None
		if ret == -1:
			raise Exception("%s" % err_str.value)
		elif ret == -2:
			waveform = None
		else:
			duration = ((1./60) * wave.num_cycles) * 10**3
			points = None
			c = channel - 1

			if measure in [0, 56]: # Voltage
				points = [float(wave.voltage_waveform[c][x]) for x in range(wave.num_points)]
			elif measure in [1, 57]: # Current
				points = [float(wave.current_waveform[c][x]) for x in range(wave.num_points)]
			else:
				raise Exception("Unknown waveform measure: %u" % measure)

			waveform = {
				"cycle": wave.start_cycle,
				"duration": duration,
				"timestamp": wave.start_time,
				"frequencyHz": 60,
				"pointsPerCycle": wave.points_per_cycle,
				"points": points
			}

			libnsf.nsf_free_recording_waveform(byref(wave))

		return waveform

	def waveform(self,index,channel,measure):
		"""
		Retrieves a waveform trace from the recording file.

		Paramters
		=========

		index        : int  - Index of the waveform to retrieve.
		channel      : int  - 1-based channel index
		measure      : int

		Returns
		=======

		dict with the following elements:
		    "cycle": int,
		    "duration": int,
		    "timestamp": int,
		    "frequency": int,
		    "pointsPerCycle": int,
		    "points": []
		"""

		wave_list = self.list_waveforms()

		if index < 0 or index >= len(wave_list):
			raise Exception("Invalid waveform index: %d" % index)

		winfo = wave_list[index]
		return self.__waveform_by_ts_cycle(winfo["timestamp"], winfo["cycle"],
			channel,measure)

	def close(self):
		libnsf.nsf_finalize(byref(self.__file_handle))

