flightStand 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. flightstand-3.0.0/FlightStand/__init__.py +15 -0
  2. flightstand-3.0.0/FlightStand/examples/PID_constant_thrust.py +128 -0
  3. flightstand-3.0.0/FlightStand/examples/__init__.py +0 -0
  4. flightstand-3.0.0/FlightStand/examples/advanced_grpc_data_streaming.py +106 -0
  5. flightstand-3.0.0/FlightStand/examples/balancing/__init__.py +0 -0
  6. flightstand-3.0.0/FlightStand/examples/balancing/real_hardware.py +145 -0
  7. flightstand-3.0.0/FlightStand/examples/balancing/simulated.py +114 -0
  8. flightstand-3.0.0/FlightStand/examples/balancing/utils.py +159 -0
  9. flightstand-3.0.0/FlightStand/examples/canbus_cyphal/__init__.py +0 -0
  10. flightstand-3.0.0/FlightStand/examples/canbus_cyphal/example_1_main.py +147 -0
  11. flightstand-3.0.0/FlightStand/examples/canbus_cyphal/example_1_plant.py +93 -0
  12. flightstand-3.0.0/FlightStand/examples/canbus_cyphal/example_2_airspeed_sensor.py +165 -0
  13. flightstand-3.0.0/FlightStand/examples/canbus_cyphal/example_3_telega_ESC.py +190 -0
  14. flightstand-3.0.0/FlightStand/examples/data_polling.py +44 -0
  15. flightstand-3.0.0/FlightStand/examples/data_recording.py +73 -0
  16. flightstand-3.0.0/FlightStand/examples/example.py +56 -0
  17. flightstand-3.0.0/FlightStand/examples/external_hardware_interface.py +124 -0
  18. flightstand-3.0.0/FlightStand/examples/gen1/get_num_of_poles.py +103 -0
  19. flightstand-3.0.0/FlightStand/examples/gen1/measure_kV.py +93 -0
  20. flightstand-3.0.0/FlightStand/examples/hid_controller_example.py +95 -0
  21. flightstand-3.0.0/FlightStand/examples/repair_stuck_in_bootloader.py +20 -0
  22. flightstand-3.0.0/FlightStand/flight_stand_api_v1_pb2.py +367 -0
  23. flightstand-3.0.0/FlightStand/flight_stand_api_v1_pb2.pyi +3794 -0
  24. flightstand-3.0.0/FlightStand/flight_stand_api_v1_pb2_grpc.py +3182 -0
  25. flightstand-3.0.0/FlightStand/flight_stand_api_v1_pb2_grpc.pyi +1632 -0
  26. flightstand-3.0.0/FlightStand/flightstand.py +1026 -0
  27. flightstand-3.0.0/PKG-INFO +11 -0
  28. flightstand-3.0.0/README.md +19 -0
  29. flightstand-3.0.0/flightStand.egg-info/PKG-INFO +11 -0
  30. flightstand-3.0.0/flightStand.egg-info/SOURCES.txt +34 -0
  31. flightstand-3.0.0/flightStand.egg-info/dependency_links.txt +1 -0
  32. flightstand-3.0.0/flightStand.egg-info/requires.txt +2 -0
  33. flightstand-3.0.0/flightStand.egg-info/top_level.txt +2 -0
  34. flightstand-3.0.0/pyproject.toml +27 -0
  35. flightstand-3.0.0/setup.cfg +4 -0
  36. flightstand-3.0.0/setup.py +3 -0
@@ -0,0 +1,15 @@
1
+ from importlib import import_module
2
+ import sys
3
+
4
+ # Make grpc-generated absolute import work:
5
+ # `import flight_stand_api_v1_pb2` -> resolves to `FlightStand.flight_stand_api_v1_pb2`
6
+ sys.modules.setdefault(
7
+ "flight_stand_api_v1_pb2",
8
+ import_module(".flight_stand_api_v1_pb2", package=__name__),
9
+ )
10
+ sys.modules.setdefault(
11
+ "flight_stand_api_v1_pb2_grpc",
12
+ import_module(".flight_stand_api_v1_pb2_grpc", package=__name__),
13
+ )
14
+
15
+ from .flightstand import FlightStand
@@ -0,0 +1,128 @@
1
+ """
2
+ This example demonstrates continuous data reading and throttle adjustment to achieve a constant thrust. The control
3
+ response is calculated using a closed-loop PID controller. To visualize the thrust response, the target thrust is
4
+ alternated between half and full thrust. Make sure the hardware is connected and the output for control is configured
5
+ and activated from the manual control tab before running the script. Additionally, configure safety cutoffs specific to
6
+ your application.
7
+ """
8
+
9
+ import time
10
+ from FlightStand import FlightStand
11
+
12
+ # The PID controller library needs to work with arbitrary timestamps, since the sensors are sampled in the hardware and
13
+ # using the computer's timestamp would yield incorrect results. Also, it needs to accept varying delta timestamps as
14
+ # some sensors don't have a fixed sample rate, for example the rotation speed sensor.
15
+ # Don't forget to install this library before running this example:
16
+ # pip install simple-pid
17
+ from simple_pid import PID
18
+
19
+ """ USER PARAMETERS """
20
+
21
+ # Set the signal names of the input and output. To find the input and output names, you can run this script once as
22
+ # it will print out all the inputs and outputs found. Refer to the API documentation for the signal type constants (
23
+ # search in the .proto file for "enum InputType"). For example a rotation speed sensor is input_type=15.
24
+ input_name = "/boards/simulated_1/inputs/1" # this is the name of the input to control
25
+ output_name = "/boards/simulated_1/outputs/1" # this is the name of the control output
26
+
27
+ """
28
+ PID terms for the PID controller. These coefficients need to be tuned by the user and are specific to each application.
29
+ Tuning PID coefficients is a crucial step to achieve desired control performance. Here are some considerations:
30
+
31
+ - Tuning Guidance: Refer to resources like [PID Tuning Guide](https://realpars.com/pid-tuning/) for pointers on PID
32
+ tuning. However, keep in mind that there is no one-size-fits-all approach, and results can vary based on different
33
+ factors.
34
+
35
+ - Factors Affecting PID Coefficients:
36
+ PID coefficients can be influenced by various factors, including:
37
+ - Voltage: The power supply voltage can affect the system's response.
38
+ - Propeller Diameter/Mass: Propeller characteristics can impact the control behavior.
39
+ - Motor Size: The size and specifications of the motor play a role in determining the optimal coefficients.
40
+ - ESC Settings: ESC configuration settings can affect the control response.
41
+ - Type of Input: The type of input being controlled (thrust, RPM, power, temperature) can influence the coefficients.
42
+ - PID update rate: The sample rate can also affect the stability and performance of the control.
43
+ - Output Settings: The output configuration (PWM, DShot, range, rate limiter, etc.) can impact the coefficients.
44
+ - User Application Requirements: Desired control characteristics (fast reaction, no overshoot, etc.) affect the ideal
45
+ coefficients.
46
+
47
+ - Examples of PID Terms:
48
+ Here are some examples of PID terms used in different scenarios:
49
+ - Simulated Hardware (Constant Thrust): 2000, 1500, 0
50
+ - Simulated Hardware (Constant Thrust with Output Rate Limit of 200): 2000, 500, 0
51
+ - Simulated Hardware (Constant Rotation Speed): 2, 5, 0
52
+ - Real Hardware (Mejzlik 48 x 16,4 Propeller, Xoar 180-35 34kv Motor, Xoar Pulse P200 ESC)
53
+ - Constant Thrust with Rate Limiter of 200us/s at 80V: 6, 5.5, 0.5
54
+ - Constant Rotation Speed with Rate Limiter of 200us/s at 80V: 2, 5, 0.3
55
+ """
56
+ Kp = 2000.0 # Proportional terms
57
+ Ki = 1500.0 # Integral term
58
+ Kd = 0.0 # Derivative term
59
+
60
+ # This example performs a square wave pattern alternating between 50% of the target_value and 100% of the target_value.
61
+ # You can set the period for this pattern in seconds.
62
+ pattern_period = 10.0
63
+ target_value = 1.0 # The value the input signal should reach
64
+
65
+ """ SCRIPT """
66
+
67
+ print(" **** Running example PID script in Python **** ")
68
+ core = FlightStand()
69
+
70
+ controller_output = core.get_output(output_name)
71
+ controller_input = core.get_input(input_name)
72
+ if not controller_output or not controller_input:
73
+ print("Input or output not found.")
74
+ exit()
75
+
76
+
77
+ def set_throttle(value):
78
+ controller_output.output_target.target_value = value
79
+ core.update_output(controller_output, ['output_target'])
80
+
81
+
82
+ # Setup the PID controller
83
+ sample_time = None
84
+ target_changed = time.time()
85
+ limits = (controller_output.min_user_value, controller_output.max_user_value)
86
+ pid = PID(Kp, Ki, Kd, output_limits=limits, sample_time=None)
87
+
88
+
89
+ def update():
90
+ global target_changed, sample_time
91
+
92
+ # Calculate the set point to have a square wave pattern with a low point half of the target thrust
93
+ target = target_value
94
+ t = time.time()
95
+ if t - target_changed > pattern_period / 2:
96
+ target = 0.5 * target
97
+ if t - target_changed >= pattern_period:
98
+ target_changed += pattern_period
99
+ pid.setpoint = target
100
+
101
+ # Get the latest available sample.
102
+ input_sample = core.get_latest_input_sample(controller_input)
103
+
104
+ # Listing samples may repeat the same samples as the rpc returns the latest values available, so we only
105
+ # update the PID controller when the sample time has changed.
106
+ new_sample_time = input_sample.sample_time.ToDatetime()
107
+ if sample_time is None or new_sample_time != sample_time:
108
+ if sample_time is not None:
109
+ dt = (new_sample_time - sample_time).total_seconds()
110
+ val = input_sample.filtered_value
111
+
112
+ # Update the PID controller with the new sample value, to obtain the updated control output
113
+ output = pid(val, dt)
114
+
115
+ # Apply the output throttle
116
+ set_throttle(output)
117
+
118
+ print({
119
+ "Delta time (s)": dt,
120
+ "Target input": pid.setpoint,
121
+ "Input": val,
122
+ "Output": output,
123
+ })
124
+ sample_time = new_sample_time
125
+
126
+
127
+ # We run a continuous loop that reads the latest thrust sensor value and sets the output to achieve a target thrust
128
+ core.run_at_interval(0.001, update)
File without changes
@@ -0,0 +1,106 @@
1
+ # This example is for advanced users, and is directly interacting with grpc instead of using flightstand.py wrapper
2
+ # class. For most users, it is better to refer to the example_data_polling.py example to continuously read sensor data.
3
+ # This example shows how to use the WatchSamples rpc call to continuously stream data. It is important to note the core
4
+ # will automatically discard samples if the Python execution is blocked. For this reason, if you want to continuously
5
+ # stream data while performing other tasks, multithreading is recommended.
6
+ import threading
7
+ import time
8
+ import grpc
9
+ from FlightStand import flight_stand_api_v1_pb2 as fs
10
+ from FlightStand import flight_stand_api_v1_pb2_grpc as fs_grpc
11
+
12
+ print(" **** Running example script in Python **** ")
13
+
14
+ print("Connecting to core...")
15
+ channel = grpc.insecure_channel('localhost:50051')
16
+ stub = fs_grpc.FlightStandStub(channel)
17
+
18
+ # Confirming connection to core
19
+ GetServerStatusRequest = fs.GetServerStatusRequest()
20
+ ServerStatus = stub.GetServerStatus(GetServerStatusRequest)
21
+ print("Core connected!") # If we reach this line without an error thrown, it means the core responded.
22
+
23
+ # Listing available hardware
24
+ print("Listing available hardware:")
25
+ ListBoardsResponse = stub.ListBoards(fs.ListBoardsRequest())
26
+ for listedBoard in ListBoardsResponse.boards:
27
+ print(listedBoard.name + ": " + listedBoard.display_name)
28
+
29
+ # We want to work with a known hardware, in the case of this example we work with simulated_1 circuit, so we check if
30
+ # it is available.
31
+ boardName = "/boards/simulated_1" # this is the name of the board we want to work with.
32
+ board = False
33
+ for listedBoard in ListBoardsResponse.boards:
34
+ if listedBoard.name == boardName:
35
+ board = listedBoard
36
+
37
+ # If the simulated board does not exist, we add it
38
+ if not board:
39
+ # Connect a simulated board
40
+ print("No hardware found. Connecting simulated hardware...")
41
+ stub.CreateSimulatedBoard(fs.CreateSimulatedBoardRequest())
42
+ print("Waiting for simulated hardware to connect...")
43
+ time.sleep(4)
44
+ print("Done.")
45
+
46
+ # Confirm it has connected
47
+ print("Listing available hardware:")
48
+ ListBoardsResponse = stub.ListBoards(fs.ListBoardsRequest())
49
+ for listedBoard in ListBoardsResponse.boards:
50
+ print(listedBoard.name + ": " + listedBoard.display_name)
51
+ if listedBoard.name == boardName:
52
+ board = True
53
+ if not board:
54
+ raise RuntimeError("Cannot add simulated board")
55
+
56
+ # Lets list all available inputs (sensors)
57
+ ListInputsResponse = stub.ListInputs(fs.ListInputsRequest())
58
+ print("Listing available inputs:")
59
+ for listedInput in ListInputsResponse.inputs:
60
+ # Print some of the properties an input has. See the .proto for details.
61
+ print(listedInput.name + ": type=" + str(listedInput.input_type) + " signalName=" + listedInput.signal_name)
62
+
63
+ # We want to focus this example on the thrust sensor
64
+ # First, we need to know the ID of the sensor type "thrust". Based on the .proto, we
65
+ # can find the enumeration of constants:
66
+ # enum InputType {
67
+ # [...]
68
+ # FORCE_FX = 9;
69
+ # FORCE_FY = 10;
70
+ # FORCE_FZ = 11;
71
+ # [...]
72
+ # }
73
+ # Then we need to find the thrust sensor for this simulated hardware and determine the corresponding signal name.
74
+ # Each sensor is streaming data as a signal. A signal is a stream of values. For efficiency reasons, we kept the list
75
+ # of inputs and the signals as separate elements. Therefore, we need to find the signal name for this specific input.
76
+ thrust_signal_name = ""
77
+ for listedInput in ListInputsResponse.inputs:
78
+ # According to the .proto each input name is of format: /boards/BOARD-ID/inputs/INPUT-ID
79
+ # We know we want to use the thrust sensor for a specific hardware, so we check the BOARD-ID is correct:
80
+ if boardName + "/" in listedInput.name:
81
+ if listedInput.input_type == fs.FORCE_FZ:
82
+ thrust_signal_name = listedInput.signal_name
83
+ print("Thrust sensor signal name: " + thrust_signal_name)
84
+ if not thrust_signal_name:
85
+ raise RuntimeError("Cannot find the required thrust sensor")
86
+
87
+ # This method is a generator function that yields only once we received a response from the server
88
+ evt = threading.Event()
89
+
90
+
91
+ def watch_samples_request_stream():
92
+ while True:
93
+ evt.clear()
94
+ yield fs.WatchSamplesRequest()
95
+ evt.wait(timeout=1)
96
+
97
+
98
+ # Here we print a loop continuously handles the WatchSamples rpc
99
+ # rpc WatchSamples(stream WatchSamplesRequest) returns (stream WatchSamplesResponse);
100
+ WatchSamplesResponseStream = stub.WatchSamples(watch_samples_request_stream())
101
+ for WatchSamplesResponse in WatchSamplesResponseStream:
102
+ evt.set() # As per the API documentation, we need to acknowledge the response (ping pong communication)
103
+ for sampleGroup in WatchSamplesResponse.sample_groups:
104
+ for sample in sampleGroup.samples:
105
+ if sample.signal_name == thrust_signal_name:
106
+ print("Thrust: " + str(sample.filtered_value) + " N") # In Newtons
@@ -0,0 +1,145 @@
1
+ # This example interacts with the user to achieve the balancing of a propeller by asking the user a series of
2
+ # questions and asking the user to perform various actions on the hardware. Use this script in parallel to running
3
+ # the user interface to control the motor using the manual control slider.
4
+ # Note: the user interface has a balancing tab which takes care of all this sequence. This script is for illustrative
5
+ # purposes only. Also, this works with a simulated hardware too, but is less automated.
6
+ import math
7
+ import time
8
+
9
+ from examples.balancing.utils import show_balancing_report, input_float, format_weights
10
+ from FlightStand import FlightStand
11
+
12
+ print("**** Running example propeller balancing script with real hardware ****")
13
+
14
+ core = FlightStand()
15
+
16
+ # Fetch the required parameters for the balancing session from the user
17
+ title = input("Enter the session title (optional, max 100 chars): ")
18
+ operator_display_name = input("Enter the operator's name (optional, max 50 chars): ")
19
+ motor_display_name = input("Enter the motor's name (optional, max 50 chars): ")
20
+ propeller_display_name = input("Enter the propeller's name (optional, max 50 chars): ")
21
+ rotor_mass_kg = input_float("Enter the rotor mass (in kg): ")
22
+ operating_speed_rpm = input_float("Enter the operating speed (rpm): ")
23
+ quality_grade_g = input_float("Enter the quality grade (optional, recommended 6.3): ")
24
+ radius_m = input_float("Enter the correction radius (m): ")
25
+ blades_count = int(
26
+ input("Enter the number of blades for the propeller (enter zero to balance a disk without blades): "))
27
+ rotation_sensor_name = input("Enter the rotation sensor name (format '/boards/id/inputs/id'): ")
28
+ vibration_sensor_name = input("Enter the vibration sensor name (format '/boards/id/inputs/id'): ")
29
+ control_output_name = input("Enter the control output name (optional, format '/boards/id/outputs/id'): ")
30
+ control_throttle_value = input_float(f"Enter the throttle value that will spin the motor to {operating_speed_rpm} rpm. Enter 0 to control manually using the GUI: ")
31
+
32
+
33
+ def rpm_to_rad_s(rpm):
34
+ return rpm * (2.0 * math.pi) / 60.0
35
+
36
+
37
+ # Create the balancing session with the user's parameters
38
+ core.create_balancing_session(params=core.Proto.BalancingSession(
39
+ title=title,
40
+ operator_display_name=operator_display_name,
41
+ motor_display_name=motor_display_name,
42
+ propeller_display_name=propeller_display_name,
43
+ rotor_mass=rotor_mass_kg,
44
+ operating_speed_rad_s=rpm_to_rad_s(operating_speed_rpm),
45
+ quality_grade_g=quality_grade_g,
46
+ correction_radius_m=radius_m,
47
+ rotation_sensor_name=rotation_sensor_name,
48
+ vibration_sensor_name=vibration_sensor_name,
49
+ control_output_name=control_output_name,
50
+ blades_count=blades_count
51
+ ))
52
+
53
+
54
+ def grams_to_unbalance(grams):
55
+ return grams * radius_m / 1000.0
56
+
57
+
58
+ def degrees_to_rad(degrees):
59
+ return degrees * math.pi / 180.0
60
+
61
+
62
+ def run_motor():
63
+ # Ask the user to bring the motor to their specified operating speed
64
+ if control_output_name == "" or control_throttle_value == 0:
65
+ input(f"\nPlease bring the motor to {operating_speed_rpm} rpm and press Enter")
66
+ return
67
+ print("Starting motor automatically")
68
+ esc_output = core.get_output(control_output_name)
69
+ esc_output.output_target.target_value = control_throttle_value
70
+ core.update_output(esc_output, ['output_target'])
71
+ time.sleep(1.0)
72
+
73
+
74
+ def stop_motor():
75
+ if control_output_name == "":
76
+ input("Please stop the motor and press Enter")
77
+ return
78
+ print("Stopping motor automatically")
79
+ esc_output = core.get_output(control_output_name)
80
+ esc_output.output_target.target_value = esc_output.cutoff_target.target_value
81
+ core.update_output(esc_output, ['output_target'])
82
+
83
+
84
+ # Execute the balance run
85
+ run_motor()
86
+ core.execute_balance_run(False, None)
87
+ stop_motor()
88
+
89
+ # Retrieve the data to know the trial weight to put
90
+ session = core.get_balancing_session()
91
+ initial_run = session.runs[0]
92
+ trial_weights = initial_run.next_corrections
93
+
94
+ # Ask the user to install the calculated trial weight
95
+ print(f"\nWe recommend a trial weight of {format_weights(trial_weights, correction_radius=radius_m)}.")
96
+ actual_trial_mass_g = input_float("Enter the installed trial weight's mass (in grams): ")
97
+ actual_trial_angle = input_float("Enter the installed trial weight's angle (in degrees): ")
98
+ trial_weight = core.Proto.BalanceWeight(unbalance=grams_to_unbalance(actual_trial_mass_g),
99
+ phase=degrees_to_rad(actual_trial_angle))
100
+
101
+ # Execute the trial run
102
+ print("Executing trial run")
103
+ run_motor()
104
+ core.execute_balance_run(False, [trial_weight])
105
+ stop_motor()
106
+
107
+ # Do multiple correction runs until the user decides to stop
108
+ while True:
109
+ # Retrieve the data to know the correction weight to put
110
+ session = core.get_balancing_session()
111
+ last_run = session.runs[-1]
112
+ correction_weights = last_run.next_corrections
113
+
114
+ print(f"\nInstall correction weight(s) of {format_weights(correction_weights, correction_radius=radius_m)}")
115
+ actual_correction_mass_g = input_float("Enter the first installed correction weight's mass (in grams): ")
116
+ actual_correction_angle = input_float("Enter the first installed correction weight's angle (in degrees): ")
117
+ correction_weight = core.Proto.BalanceWeight(unbalance=grams_to_unbalance(actual_correction_mass_g),
118
+ phase=degrees_to_rad(actual_correction_angle))
119
+ actual_correction_weights = [correction_weight]
120
+
121
+ actual_correction_mass_g = input_float(
122
+ "Enter the second installed correction weight's mass (in grams). Write 0 if none: ")
123
+ if actual_correction_mass_g != 0.0:
124
+ actual_correction_angle = input_float("Enter the second installed correction weight's angle (in degrees): ")
125
+ correction_weight = core.Proto.BalanceWeight(unbalance=grams_to_unbalance(actual_correction_mass_g),
126
+ phase=degrees_to_rad(actual_correction_angle))
127
+ actual_correction_weights.append(correction_weight)
128
+
129
+ # Execute the correction run
130
+ print("Executing correction run")
131
+ run_motor()
132
+ core.execute_balance_run(True, actual_correction_weights)
133
+ stop_motor()
134
+
135
+ # Ask the user if they want to continue with more runs
136
+ answer = input(f"Current balancing grade: {last_run.quality_grade_g}. Target grade: {session.quality_grade_g}. "
137
+ f"Do you want to continue with more corrections (Y/N)?:")
138
+ if answer == "Y" or answer == "y":
139
+ continue
140
+ break
141
+
142
+ # Retrieve the data to print the report
143
+ show_balancing_report(core)
144
+
145
+ print("\nDone.")
@@ -0,0 +1,114 @@
1
+ # This example shows how to connect a simulated hardware and use the propeller balancing feature. In this example
2
+ # the simulated hardware starts with a virtual imbalance, and we run the balancing steps to get it balanced.
3
+ # In your scenario, you should replace the simulated circuit with your own hardware.
4
+ import time
5
+
6
+ from examples.balancing.utils import show_balancing_report, format_weights
7
+ from FlightStand import FlightStand
8
+
9
+ print(" **** Running example propeller balancing script with simulated hardware **** ")
10
+ core = FlightStand()
11
+
12
+ # Connect a simulated board if none already connected
13
+ print("\nConnecting simulated hardware...")
14
+ board = core.create_simulated_board()
15
+ core.print_hardware_list()
16
+
17
+ # Activate the output at 1000. This is done by first modifying the output fields to set a new target value, and then
18
+ # the update_output command must be called to send the updated values.
19
+ print("\nActivating ESC at 1000")
20
+ output_name = board.name + "/outputs/1"
21
+ esc_output = core.get_output(output_name)
22
+ esc_output.output_target.target_value = 1000
23
+ esc_output.output_target.active = True
24
+ core.update_output(esc_output, ['output_target'])
25
+ time.sleep(0.2)
26
+
27
+ # Increase the throttle to 1500
28
+ print("\nSetting ESC to 1500")
29
+ esc_output.output_target.target_value = 1500
30
+ core.update_output(esc_output, ['output_target'])
31
+ time.sleep(0.5)
32
+
33
+ # Do an initial run in the balancing session to get the initial unbalance (simulated hardware has a random unbalance
34
+ # when created)
35
+ rotor_mass_kg = 0.02 # Important to correctly calculate the correction weights
36
+ radius_m = 0.04 # So we can calculate the weight
37
+ core.create_balancing_session(params=core.Proto.BalancingSession(
38
+ title="Demo balance session on 3-bladed simulated hardware", # Optional
39
+ operator_display_name="My name", # Optional
40
+ motor_display_name="Motor 1", # Optional
41
+ propeller_display_name="Propeller 1", # Optional
42
+ rotor_mass=rotor_mass_kg,
43
+ operating_speed_rad_s=1091, # In our demo, we go to ESC value 1500 which gives a repeatable rotation speed.
44
+ quality_grade_g=6.3, # Optional. 6.3 is common in the industry for propellers (ISO 1940 standard)
45
+ correction_radius_m=radius_m,
46
+ rotation_sensor_name=board.name + "/inputs/5",
47
+ vibration_sensor_name=board.name + "/inputs/8",
48
+ control_output_name=output_name,
49
+ blades_count=3
50
+ ))
51
+ core.execute_balance_run(False, None)
52
+
53
+
54
+ # Retrieve the data to know the trial weight to put
55
+ session = core.get_balancing_session()
56
+ # print_session(session)
57
+ initial_run = session.runs[0]
58
+
59
+ trial_weights = initial_run.next_corrections
60
+ print("Trial weights: " + format_weights(trial_weights, radius_m))
61
+
62
+ # Normally done by user, but here we simulate adding a trial weight
63
+ print(f"Putting a simulated trial weight on the simulated hardware")
64
+ sim_settings = core.Proto.UpdateSimulatedBoardSettingsRequest(board_name=board.name)
65
+ sim_settings.settings.correction_weights.extend(trial_weights)
66
+ core.stub.UpdateSimulatedBoardSettings(sim_settings)
67
+ time.sleep(0.5) # Gives time for the data samples to update
68
+
69
+ # Confirm the simulated hardware has correction weights applied
70
+ sim_settings = core.stub.GetSimulatedBoardSettings(core.Proto.GetSimulatedBoardSettingsRequest(board_name=board.name))
71
+ print("Confirmed correction weights applied:", sim_settings)
72
+ print("")
73
+
74
+ # Do the trial run
75
+ print("Executing trial run")
76
+ core.execute_balance_run(False, trial_weights)
77
+
78
+ # Retrieve the data to know the correction weight to put
79
+ session = core.get_balancing_session()
80
+ # print_session(session)
81
+ trial_run = session.runs[1]
82
+ correction_weights = trial_run.next_corrections
83
+ print("Correction weights: " + format_weights(correction_weights, radius_m))
84
+
85
+ # Normally done by user, apply the correction weight to achieve a balanced rotor
86
+ sim_settings = core.Proto.UpdateSimulatedBoardSettingsRequest(board_name=board.name)
87
+ sim_settings.settings.correction_weights.extend(correction_weights)
88
+ core.stub.UpdateSimulatedBoardSettings(sim_settings)
89
+ time.sleep(0.5) # Gives time for the data samples to update
90
+
91
+ # Do the correction run (will confirm balance is good)
92
+ print("Executing correction run")
93
+ core.execute_balance_run(True, correction_weights)
94
+
95
+ # Retrieve the data to print the report
96
+ show_balancing_report(core)
97
+
98
+ # Restore the throttle to 1000
99
+ print("\nSetting ESC to 1000")
100
+ esc_output.output_target.target_value = 1000
101
+ core.update_output(esc_output, ['output_target'])
102
+ time.sleep(1.0) # Give time for the output change to be applied
103
+
104
+ # Turn off the ESC output signal
105
+ print("\nTurning off ESC")
106
+ esc_output.output_target.active = False
107
+ core.update_output(esc_output, ['output_target'])
108
+ time.sleep(1.0) # Give time for the output change to be applied
109
+
110
+ # Delete the balance run
111
+ core.delete_balance_run(session.name)
112
+
113
+ print("\nDone.")
114
+ exit()
@@ -0,0 +1,159 @@
1
+ import math
2
+ from datetime import timedelta
3
+
4
+ import numpy as np
5
+ from matplotlib import pyplot as plt
6
+
7
+ from FlightStand import FlightStand
8
+
9
+
10
+ def input_float(prompt):
11
+ val = input(prompt)
12
+ if val == "":
13
+ return 0.0
14
+ return float(val)
15
+
16
+
17
+ # Calculates datapoints for superposing a sine wave function on top of the data, to show correct curvefitting
18
+ def simulate_sine_result(run: FlightStand.Proto.BalanceRun, rotation_speed: list[FlightStand.Proto.DataPoint]):
19
+ # Sort rotation speed data timestamps
20
+ rotation_speed_times = [dp.sample_time.ToDatetime() for dp in rotation_speed]
21
+
22
+ # Get duration (converting it to seconds) by subtracting smallest timestamp from largest
23
+ duration = (rotation_speed_times[-1] - rotation_speed_times[0]).total_seconds()
24
+
25
+ # Sampling frequency
26
+ fs = 10000 # 10000 Hz
27
+
28
+ # Time vector
29
+ t = np.arange(0, duration, 1 / fs)
30
+
31
+ # Analysis data from the run
32
+ amp = run.vibration_signal_amplitude
33
+ phase_offset = run.vibration_signal_phase
34
+ omega = run.average_rotation_speed
35
+
36
+ # Create sine wave
37
+ sine_values = amp * np.sin(omega * t + phase_offset)
38
+
39
+ # Convert the float seconds back to timestamps
40
+ sine_times = [rotation_speed_times[0] + timedelta(seconds=s) for s in t]
41
+
42
+ return sine_times, sine_values
43
+
44
+
45
+ def format_number(number) -> str:
46
+ if number == 0:
47
+ return "0"
48
+ digits = math.floor(math.log10(abs(number))) + 1
49
+ precision = max(0, 3 - digits)
50
+ format_str = "{:." + str(precision) + "f}"
51
+ formatted_num = format_str.format(number)
52
+ return formatted_num[:-1] if formatted_num[-2:] == '.0' else formatted_num
53
+
54
+
55
+ def format_weight(weight: FlightStand.Proto.BalanceWeight, correction_radius: float) -> str:
56
+ if weight.unbalance != 0 and correction_radius != 0:
57
+ unbalance = format_number(weight.unbalance * 1000.0 / correction_radius) # Convert from kg⋅m to g⋅m
58
+ phase_degrees = format_number(math.degrees(weight.phase))
59
+ result = f"{unbalance} grams {phase_degrees}°"
60
+ print(weight, " -> ", result)
61
+ return result
62
+ return '0 grams 0°' # return a default string
63
+
64
+ def format_weights(weights: list[FlightStand.Proto.BalanceWeight], correction_radius: float) -> str:
65
+ return " & ".join([format_weight(weight, correction_radius) for weight in weights])
66
+
67
+ def show_balancing_report(core: FlightStand):
68
+ session = core.get_balancing_session()
69
+
70
+ # Create matplotlib figure and axes
71
+ fig, axs = plt.subplots(len(session.runs))
72
+
73
+ #title_text = f'Session ID: {session.name}\n'
74
+ title_text = f'Title: {session.title}\n'
75
+ title_text += f'Motor: {session.motor_display_name}\n'
76
+ title_text += f'Propeller: {session.propeller_display_name}\n'
77
+ title_text += f'Operator: {session.operator_display_name}\n'
78
+ title_text += f'Rotor mass (grams): {round(session.rotor_mass * 1000.0, 3)}\n'
79
+ title_text += f'Operating speed (rpm): {round(session.operating_speed_rad_s * 9.5493, 3)}\n'
80
+ title_text += f'Target quality grade (g): {round(session.quality_grade_g, 3)}\n'
81
+ title_text += f'Permissible unbalance (g⋅m): {round(session.permissible_unbalance*1000.0, 6)}\n'
82
+ title_text += f'Correction radius (cm): {round(session.correction_radius_m * 100.0, 3)}\n'
83
+ title_text += f'Blades count: {session.blades_count}\n'
84
+ #title_text += f'Control Output: {session.control_output_name}\n'
85
+ #title_text += f'Rotation Sensor: {session.rotation_sensor_name}\n'
86
+ #title_text += f'Vibration Sensor: {session.vibration_sensor_name}'
87
+
88
+ # compute the overall maximum from all runs
89
+ ymax = 0
90
+
91
+ fig.suptitle(title_text, fontsize=8, y=0.9)
92
+ if len(session.runs) == 1:
93
+ axs = [axs]
94
+ for i, (run, ax) in enumerate(zip(session.runs, axs), 1):
95
+ run_data = core.list_balance_run_data(session.name, i-1)
96
+ vibration_data = run_data.vibration
97
+ rotation_speed = run_data.rotation_speed
98
+ run_vib_max = max(dp.value for dp in vibration_data)
99
+ if run_vib_max > ymax:
100
+ ymax = run_vib_max
101
+ title = "Initial run"
102
+ if i == 2:
103
+ title = "Trial run"
104
+ if i > 2:
105
+ title = f"Correction run {i - 2}/{len(session.runs) - 2}"
106
+ tol = "in tolerance ✔"
107
+ if session.quality_grade_g < run.quality_grade_g:
108
+ tol = "out of tolerance ✘"
109
+ run_details = (
110
+ f"\n{title}\n\n"
111
+ f"Correction weights: {format_weights(run.corrections, session.correction_radius_m)}\n"
112
+ f"Previous corrections removed: {run.previous_weight_removed}\n"
113
+ #f"Rotation speed (rpm): {round(run.average_rotation_speed * 9.5493, 3)}\n"
114
+ #f"Rotation samples: {len(run.rotation_speed_data)}\n"
115
+ f"Vibration amplitude: {round(run.vibration_signal_amplitude, 3)}\n"
116
+ #f"Vibration phase: {round(run.vibration_signal_phase, 3)}\n"
117
+ #f"Vibration SNR: {round(run.vibration_signal_to_noise_ratio, 3)}\n"
118
+ #f"Vibration samples: {len(run.vibration_data)}\n"
119
+ f"Quality grade G: {round(run.quality_grade_g, 3)} → {tol}\n"
120
+ f"Result unbalance: {format_weight(run.unbalance, session.correction_radius_m)}\n"
121
+ f"Next corrections: {format_weights(run.next_corrections, session.correction_radius_m)}\n"
122
+ )
123
+ ax.text(1.05, 0.5, run_details, transform=ax.transAxes, verticalalignment='center', fontsize=6,
124
+ bbox=dict(facecolor='white', edgecolor='black', boxstyle='round,pad=1'))
125
+
126
+ vibration_times = [dp.sample_time.ToDatetime() for dp in vibration_data]
127
+ vibration_values = [dp.value for dp in vibration_data]
128
+ rotation_speed_times = [dp.sample_time.ToDatetime() for dp in rotation_speed]
129
+ sine_times, sine_values = simulate_sine_result(run, rotation_speed)
130
+
131
+ start_time = rotation_speed_times[0]
132
+ vibration_time_diff = [(t - start_time).total_seconds() for t in vibration_times]
133
+ sine_time_diff = [(t - start_time).total_seconds() for t in sine_times]
134
+
135
+ # Calculate new vibration time differences
136
+ revolution_start_times = [(t - start_time).total_seconds() for t in rotation_speed_times]
137
+ new_vibration_time_diff = []
138
+ for vt in vibration_time_diff:
139
+ revolution_start_time = max((rt for rt in revolution_start_times if rt <= vt), default=0)
140
+ new_vibration_time_diff.append(vt - revolution_start_time)
141
+
142
+ # Plot vibration data with updated time differences
143
+ ax.plot(new_vibration_time_diff, vibration_values, 'o', markersize=2.0)
144
+
145
+ # Plot the sinewave
146
+ ax.plot(sine_time_diff, sine_values)
147
+
148
+ # Add labels
149
+ ax.set_xlabel('Time (s)')
150
+ ax.set_ylabel('Vibration')
151
+
152
+ # set the same y-axis limits for all sub-plots
153
+ ax.set_ylim(-ymax, ymax)
154
+ one_revolution_duration = (rotation_speed_times[1] - rotation_speed_times[0]).total_seconds()
155
+ ax.set_xlim(0, one_revolution_duration)
156
+
157
+ # Show plot
158
+ plt.tight_layout()
159
+ plt.show()