Web Server

Discussions on extending SharpCap using the built in Python scripting functionality
metastable
Posts: 45
Joined: Thu Oct 26, 2023 1:24 am

Re: Web Server

#31

Post by metastable »

Hmm I'm not sure what happened. I tried it again and it seems to work now. Perhaps it's because previously I was experimenting with Tasks that it somehow put the sequencer in a bad state? Not sure, but either way its working now. Thanks for the quick turnaround! I'll work on implementing this later today.
metastable
Posts: 45
Joined: Thu Oct 26, 2023 1:24 am

Re: Web Server

#32

Post by metastable »

Threading seems to be working flawlessly! Last thing I need to solve to completely control my setup workflow is getting some histogram data. I'm trying to figure out how to get enough info to know if my flats are being exposed enough. I've been looking at `SharpCap.DisplayStretch.LastHistogram` but I can't seem to find anything that gets me numbers that match the Mean and Standard deviation(?) numbers displayed in the Histogram tool. I'm not sure what the 2D array from `Values` represents. Am I at least in the same ballpark for what I want? :lol:
User avatar
admin
Site Admin
Posts: 13349
Joined: Sat Feb 11, 2017 3:52 pm
Location: Vale of the White Horse, UK
Contact:

Re: Web Server

#33

Post by admin »

Hi,

the 'Values' on the histogram is a 2D array - the first dimension is channel (0=luminance, 1,2,3 are RGB - not present for mono). The second dimension is brightness level, so if the data is 8 bit then the second dimension will be of size 256 (65536 for 10/12/14/16 bit). The values are the counts of pixels at each brightness level.

So, to work out if your flats are under/over exposed, look at the values and check they are all zero (or close to zero) above about 80% of the brightness range and below about 20%.

You can also calculate the histogram of a frame itself (if you are handling the FrameCaptured event), and ask the frame for the statistics (call GetStats()) to get mean and standard deviation.

Hope this helps,

Robin
metastable
Posts: 45
Joined: Thu Oct 26, 2023 1:24 am

Re: Web Server

#34

Post by metastable »

Hey Robin,

That helped a ton! After fighting with python, and failing to use .NET Arrays and Spans (I was getting reflection errors), I managed to send the histogram data over the wire as a bytearray and display it on the front end. If the weather is good enough tonight, I'll finally be testing the full flow with my app. Here's a screen of what the histogram looks like. Along with the full script in case anyone else wants to do something similar.

Code: Select all

from http.server import HTTPServer, BaseHTTPRequestHandler
import json
from urllib.parse import urlparse, parse_qs
import os
from pathlib import Path
from System.Threading import Thread, ThreadStart
from System import Array
import struct

HOST = ""
PORT = 4001
CAPTURE_PATH = Path(os.environ['USERPROFILE'], 'AppData\Local\Temp\SharpCap\capture.png')
CAPTURE_PATH_WITH_STRETCH = Path(os.environ['USERPROFILE'], 'AppData\Local\Temp\SharpCap\capture_WithDisplayStretch.png')
SEQ_PATH = Path(os.environ['USERPROFILE'], 'AppData\Local\SharpCap\sequences')

class RequestHandler(BaseHTTPRequestHandler):
	SUCCESS = "Success".encode()
	FAILED = "Failure".encode()

	def do_GET(self):
		print('Received request ' + self.path)

		try:
			parse_result = urlparse(self.path)
			url_path = parse_result.path
			self.query_params = parse_qs(parse_result.query)

			if url_path == "/state":                    
				self.respond_with_state()
			elif url_path == "/current_frame":
				self.clear_capture()
				
				if SharpCap.SelectedCamera.SaveAsViewed(str(CAPTURE_PATH)):
					self.send_current_capture()
				else:
					self.respond_with_error(409, "Failed to save as viewed")
			elif url_path == "/histogram":
				self.send_histogram()
			else:
				self.respond_with_error(422, 'Unhandled path')
		except BaseException as error:
			print('An exception occurred: {}'.format(error))
			self.respond_with_error(500, 'Unknown Server Error')

	def do_POST(self):
		print('Received request ' + self.path)

		try:        
			parse_result = urlparse(self.path)
			url_path = parse_result.path
			self.query_params = parse_qs(parse_result.query)
			
			if url_path == "/pause_livestack":
				SharpCap.LiveStacking.Action.Pause()
				
				self.respond_with_state()
			elif url_path == "/start_livestack":
				if SharpCap.LiveStacking.IsPaused:
					SharpCap.LiveStacking.Action.Resume()
				else:
					SharpCap.LiveStacking.Activate()
				
				self.respond_with_state()
			elif url_path == "/reset_livestack":
				SharpCap.LiveStacking.Action.Reset()
				
				self.respond_with_state()
			elif url_path == "/set_camera_settings" and self.has_param('exp_len') and self.has_param('gain'):
				SharpCap.SelectedCamera.Controls.Exposure.ExposureMs = int(self.get_param('exp_len'))
				SharpCap.SelectedCamera.Controls.Gain.Value = int(self.get_param('gain'))
				
				self.respond_with_state()
			elif url_path == "/set_temperature_settings" and self.has_param("on_off") and self.has_param("target_temperature"):
				SharpCap.SelectedCamera.Controls.TargetTemperature.Value = int(self.get_param("target_temperature"))
				
				if self.get_param("on_off") == "True":
					SharpCap.SelectedCamera.Controls.CoolerOnOff.Value = "On"
				else:
					SharpCap.SelectedCamera.Controls.CoolerOnOff.Value = "Off"
					
				self.respond_with_state()
			elif url_path == "/capture_frame":
				if SharpCap.SelectedCamera == None:
					self.respond_with_error(409, "No Camera is Selected!")
				else:
					self.clear_capture()
					
					SharpCap.SelectedCamera.TakeFramingShot()
					if SharpCap.SelectedCamera.SaveAsViewed(str(CAPTURE_PATH)):
						self.send_current_capture()
					else:
						self.respond_with_error(409, "Failed to save as viewed")
					
			elif url_path == "/select_camera" and self.has_param('index'):
				index = int(self.get_param('index'))
				SharpCap.SelectedCamera = SharpCap.Cameras[index]
					
				self.respond_with_state()
			elif url_path == "/close_camera":
				SharpCap.SelectedCamera = None
				
				self.respond_with_state()
			elif url_path == "/shutdown":
				data = {"data": {"outcome": "Success"}}
				self.respond(200, "application/json", json.dumps(data).encode())
				httpd.server_close()
			elif url_path == "/start_polar_align":
				current_transform = SharpCap.Transforms.SelectedTransform.Name
				if current_transform != "Polar Align":
					SharpCap.Transforms.SelectTransform("Polar Align")
					
				self.respond_with_state()
			elif url_path == "/stop_polar_align":
				current_transform = SharpCap.Transforms.SelectedTransform.Name
				if current_transform == "Polar Align":
					SharpCap.Transforms.SelectTransform("None")
					
					self.respond_with_state()
				else:
					self.respond_with_error(409, "Another tool is currently open: " + current_transform)
			elif url_path == "/run_sequence" and self.has_param("seq"):
				if SharpCap.SequencerIsRunning:
					self.respond_with_error(422, "Already running a sequence")
				else:
					Thread(ThreadStart(lambda : SharpCap.Sequencer.RunSequenceFile(str(Path(SEQ_PATH, self.get_param("seq")))))).Start()
					
					self.respond_with_state()
			elif url_path == "/move_focuser" and self.has_param("position"):
				SharpCap.Focusers.SelectedFocuser.Move(int(self.get_param("position")))
				
				self.respond_with_state()
			elif url_path == "/set_focuser_connection" and self.has_param("on_off"):
				SharpCap.Focusers.SelectedFocuser.Connected = self.get_param("on_off") == "True"
				
				self.respond_with_state()
			else:
				self.respond_with_error(422, 'Unhandled path')
		except BaseException as error:
			print('An exception occurred: {}'.format(error))
			self.respond_with_error(500, 'Unknown Server Error')
   
	def send_histogram(self):
		histo_array = SharpCap.DisplayStretch.LastHistogram.Values

		is_rgb = len(histo_array) > 1
		array_size = len(histo_array[0])
		step_size = int(array_size / 256)

		if is_rgb:
			channels = 4
		else:
			channels = 1

		byte_array = bytearray(1)
		byte_array[0] = channels
  
		maximum_value = 0

		for c in range(channels):
			for i in range(256):
				accumulation = 0
				for j in range(step_size):
					accumulation += histo_array[c][j + (step_size * i)]
     
				if accumulation > maximum_value:
					maximum_value = accumulation

				byte_array += struct.pack('i', accumulation)
    
		byte_array += struct.pack('i', maximum_value)
		
		# with open(str(Path(CAPTURE_PATH.parent, "histo.bin")), 'wb') as f:
			# f.write(byte_array)
					
		print("is_rgb:{}, array_size:{}, step_size:{}, max_value:{}".format(is_rgb, array_size, step_size, maximum_value))
		self.respond(200, "application/octet-stream", byte_array)
            
	def send_current_capture(self):
		f = open(str(CAPTURE_PATH_WITH_STRETCH), 'rb')
		self.respond(200, "image/png", f.read())
		f.close()
		
	def has_param(self, expected):
		return expected in self.query_params and len(self.query_params[expected]) == 1

	def get_param(self, key):
		return self.query_params[key][0]
		
	def respond_with_state(self):
		response = json.dumps(self.get_current_state())
        
		self.respond(200, "application/json", response.encode())
		
	def respond_with_error(self, code, error):
		data = {"data": {"errors": {"server": [error]} } }
		self.respond(code, "application/json", json.dumps(data).encode())
		
	def respond(self, code, content_type, data):
		self.send_response(code)
		self.send_header("Content-Type", content_type)
		self.end_headers()
		self.wfile.write(data)

	def clear_capture(self):
		if Path.is_file(CAPTURE_PATH_WITH_STRETCH):
			try:
				os.remove(str(CAPTURE_PATH_WITH_STRETCH))
			except OSError:
				pass
			
	def get_current_state(self):
		data = {}
     
		cameras = []
		index = 0
		is_selected_camera = False
		for camera in SharpCap.Cameras:
			is_selected = camera == SharpCap.SelectedCamera
			if is_selected:
				is_selected_camera = True
   
			camera_dict = { "name": camera.DeviceName, "index": index, "provider": camera.Provider, "is_direct_driver": camera.IsDirectDriver, "is_selected": is_selected }
			if (is_selected):
				camera_dict["exposure"] = SharpCap.SelectedCamera.Controls.Exposure.ExposureMs
				camera_dict["gain"] = SharpCap.SelectedCamera.Controls.Gain.Value
			cameras.append(camera_dict)
			index += 1
  
		if is_selected_camera:
			if SharpCap.SelectedCamera.Controls.CoolerOnOff == None:
				data["cooler_state"] = {"state": "Unavailable"}
			else:
				data["cooler_state"] = {"state": SharpCap.SelectedCamera.Controls.CoolerOnOff.Value, "target_temperature": SharpCap.SelectedCamera.Controls.TargetTemperature.Value, "current_temperature": SharpCap.SelectedCamera.Controls.Temperature.Value, "max_temperature": SharpCap.SelectedCamera.Controls.Temperature.Maximum, "min_temperature": SharpCap.SelectedCamera.Controls.Temperature.Minimum}
			data["live_stacking"] = {"is_live_stacking": SharpCap.LiveStacking.IsActive, "is_livestacking_paused": SharpCap.LiveStacking.IsPaused}
			data["is_live_viewing"] = SharpCap.SelectedCamera.LiveView
   
		if SharpCap.Focusers.SelectedFocuser != None:
			selected_focuser = SharpCap.Focusers.SelectedFocuser
			focuser_dict = {"is_connected": selected_focuser.Connected}
   
			if selected_focuser.Connected:
				focuser_dict["position"] = selected_focuser.Position
				focuser_dict["minimum"] = selected_focuser.Mininum
				focuser_dict["maximum"] = selected_focuser.Maximum
				
			data["focuser"] = focuser_dict
   
		data["cameras"] = cameras
		data["current_tool"] = SharpCap.Transforms.SelectedTransform.Name
		
		data["sequences"] = os.listdir(str(SEQ_PATH))
		data["is_sequencer_running"] = SharpCap.SequencerIsRunning
			
		return {"data": data}

os.makedirs(str(CAPTURE_PATH.parent), exist_ok=True)

try:
	httpd = HTTPServer((HOST, PORT), RequestHandler)
	httpd.serve_forever()
except BaseException as error:
	print('An exception occurred: {}'.format(error))

Attachments
Screenshot 2023-11-19 104623.jpg
Screenshot 2023-11-19 104623.jpg (76.78 KiB) Viewed 14906 times
User avatar
admin
Site Admin
Posts: 13349
Joined: Sat Feb 11, 2017 3:52 pm
Location: Vale of the White Horse, UK
Contact:

Re: Web Server

#35

Post by admin »

Hi,

that looks very impressive - have you built it as a web UI to run in the browser, ir is that a custom application you've written to be the client side?

As I mentioned in another thread, I am usually happy to try to add scripting functionality if it is just a matter of adding a function call to allow access to something that is already there (not much chance of me breaking anything else with that sort of change). Some requests unfortunately are not easy if the way a feature is implemented is very much oriented around the UI of the feature.

cheers,

Robin
metastable
Posts: 45
Joined: Thu Oct 26, 2023 1:24 am

Re: Web Server

#36

Post by metastable »

Hey Robin,

Thanks! This is a custom application written using Unity3D, since that's what I primarily use professionally. It's a bit overkill but it provided me with a quick and easy way to generate the UI. If anyone is interested, I can host the source code on GitHub after I clean it up a bit and apply an OSS license.

For my uses, more control over the polar alignment process would be great. Being able to advance the steps rather than relying on the auto-advance or even being able to check and uncheck the auto-advance toggle. Also, more info on the current running sequence would be nice for UI feedback (currently I just throw up a progress bar and wait until the sequence is completed before I clear it.)

Thanks,
Alejandro
User avatar
admin
Site Admin
Posts: 13349
Joined: Sat Feb 11, 2017 3:52 pm
Location: Vale of the White Horse, UK
Contact:

Re: Web Server

#37

Post by admin »

Hi Alejandro,

it's funny how we all tend to code things in the languages/frameworks that we are familiar with!

I think a progress event on the sequencer should be possible, will look into that. The Polar alignment might require more thought.

cheers,

Robin
metastable
Posts: 45
Joined: Thu Oct 26, 2023 1:24 am

Re: Web Server

#38

Post by metastable »

Hey Robin,

I've been living with my app for a while now and I find that everything works rather well except polar alignment. I'm consistently getting into a bad state, with the auto advance, when slewing and it thinks I'm finished slewing, which then makes it think I've done a rotation in the following step. At that point I need to remote into the PC and reset the polar alignment. Was there any possibility of being able to progress the steps manually via scripting?

If it's too much trouble I think I can use another program that can do UI actions via code, and that should make it possible to get the same behavior in the end.

Thanks,
Alejandro
User avatar
admin
Site Admin
Posts: 13349
Joined: Sat Feb 11, 2017 3:52 pm
Location: Vale of the White Horse, UK
Contact:

Re: Web Server

#39

Post by admin »

Hi,

it's on a post-it note round here somewhere as a 'todo' item... Unfortunately the planetary live stacking has become a hot feature recently, so has been using up most of my time. I will try to get around to it soon!

cheers,

Robin
metastable
Posts: 45
Joined: Thu Oct 26, 2023 1:24 am

Re: Web Server

#40

Post by metastable »

Hey Robin,

Rightfully so, it's a great feature. No worries!

Thanks,
Alejandro
Post Reply