From 652c9e61630929e69d7381b6a7ea23695d3bfc77 Mon Sep 17 00:00:00 2001 From: Dmitri McGuckin <28746912+dmitri-mcguckin@users.noreply.github.com> Date: Sun, 12 Feb 2023 16:32:37 -0700 Subject: [PATCH] Fast Forward.... 4 years later... (#13) * hopefully more accurate * some spellchecking and cleaning, also fixing CLI * fixed whitespace * testing new functionality for motor drivers and automatic current setting * squashing bugs * bug fixin * no functionality added, just cleaning code * added sensor reset for magnetometer * works pretty well, needs some control theory and finishing touches * hopefully didn't break anything, hopefully made system faster/more responsive * Updated Updated with previous edits to remove unnecessary branches. * Added Arduino Code and README Added new folder that includes code and README description for Arduino. The purpose is for the Arduino to interact with the magnetometer. --------- Co-authored-by: PandnotPthereforeQ Co-authored-by: jejor Co-authored-by: Cordet <115206755+WonkersTheWatilla@users.noreply.github.com> --- .gitignore | 1 + Arduino_Comms/COMMS_README.txt | 49 +++++++++ Arduino_Comms/PSAS_HHCage.ino | 154 +++++++++++++++++++++++++++++ Makefile | 2 +- README.md | 6 +- cage_controller.py | 62 ++++++++---- command_line.py | 84 ++++++++++------ driver.py | 26 +++-- graph.py | 4 +- magnetic_field_current_relation.py | 41 ++------ setup.py | 4 +- utilities.py | 4 +- window.py | 58 ++++++++--- 13 files changed, 386 insertions(+), 109 deletions(-) create mode 100644 Arduino_Comms/COMMS_README.txt create mode 100644 Arduino_Comms/PSAS_HHCage.ino mode change 100755 => 100644 cage_controller.py mode change 100755 => 100644 magnetic_field_current_relation.py mode change 100755 => 100644 utilities.py mode change 100755 => 100644 window.py diff --git a/.gitignore b/.gitignore index 9546697..655e021 100755 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__/ *.so # Distribution / packaging +.vscode .Python build/ develop-eggs/ diff --git a/Arduino_Comms/COMMS_README.txt b/Arduino_Comms/COMMS_README.txt new file mode 100644 index 0000000..001f169 --- /dev/null +++ b/Arduino_Comms/COMMS_README.txt @@ -0,0 +1,49 @@ +COMMS_README.TXT + +This document is the decode key to "ArduinoComms.py" and "PSAS_HHCage.ino" + +General: +The arduino will initialize the magnetic sensor (if present) and listen on USB-Serial (115200) for one byte messages from the Raspberry PI / Python. The Arduino will control 3 H-bridges (X,Y,Z) and interact with a magnetometer (MMC5603). Some commands from the PI will recieve a serial data return. + +Note that decode values are case sensitive.The following are the commands the arduino is listening for without any serial data returned: +x - Activate X H-bridge in Positive Polarity +y - Activate Y H-bridge in Positive Polarity +z - Activate Z H-bridge in Positive Polarity +X - Activate X H-bridge in Negative Polarity +Y - Activate Y H-bridge in Negative Polarity +Z - Activate Z H-bridge in Negative Polarity +a - De-activate all H-Bridges (X,Y,Z) +b - De-activate X H-Bridge +c - De-activate Y H-Bridge +d - De-activate Z H-Bridge +The following are the commands the arduino is listening for with a serial data return: +m - Request current magnetic field reading + Data return is "X,Y,Z" magnetic field in uT. + The values of each value X,Y,Z can be positive or negative + Here is an example return: "1000.05,-200.33,500.79" + Note: Refer to the nomen on the magnetometer to interpret positive and negative field directions +q - Request magnetometer status + Data return is "0" -- magnetometer not initialized + "1" -- magnetometer is initialized + note: magnetometer is initialized on setup/startup of arduino script. Restarting the serial interface will reset the arduino and will attempt re-initialization. If failures persist inspect wiring to sensor and the physical sensor. +s - Request H-Bridge Status: + Data return is "XYZ" where + X is X axis H-Bridge status + Y is Y axis H-Bridge status + Z is Z axis H-Bridge status + Each position can be 0, 1, or 2: + 0: Bridge is de-activated + 1: Bridge is activated in positive polarity + 2: Bridge is activated in negative polarity + Example: + 021 + X axis H-Bridge is de-activated + Y axis H-Bridge is activated in negative polarity + Z axis H-Bridge is activated in positive polarity +t - Request Ambient Temperature from Magnetometer: + The magnetometer has a temperature sensor built in, might as well provide the ability to read it. + The Serial data return is in degrees Celsius: + ##.## + Example: 17.80 + +# Credit to Christian Bennett (2022) for assisting with Arduino code and testing \ No newline at end of file diff --git a/Arduino_Comms/PSAS_HHCage.ino b/Arduino_Comms/PSAS_HHCage.ino new file mode 100644 index 0000000..4a4cf75 --- /dev/null +++ b/Arduino_Comms/PSAS_HHCage.ino @@ -0,0 +1,154 @@ +#include +/* Assign a unique ID to this sensor at the same time */ +Adafruit_MMC5603 mmc = Adafruit_MMC5603(12345); + +int xina = 6; +int xinb = 7; +int yina = 4; +int yinb = 5; +int zina = 2; +int zinb = 3; +int xstat = 0; +int ystat = 0; +int zstat = 0; +int magstat = 0; + + +int incomingByte = 0; // for incoming serial data + +void setup() { + // put your setup code here, to run once: + pinMode(xina, OUTPUT); + pinMode(xinb, OUTPUT); + pinMode(yina, OUTPUT); + pinMode(yinb, OUTPUT); + pinMode(zina, OUTPUT); + pinMode(zinb, OUTPUT); + digitalWrite(xina,LOW); + digitalWrite(xinb,LOW); + digitalWrite(yina,LOW); + digitalWrite(yinb,LOW); + digitalWrite(zina,LOW); + digitalWrite(zinb,LOW); + Serial.begin(115200); + + + // Initialise the mag sensor */ + if (mmc.begin(MMC56X3_DEFAULT_ADDRESS, &Wire)) { // I2C mode + magstat = 1; + //mmc.printSensorDetails(); + } + +} + +void loop() { + // put your main code here, to run repeatedly: + + // reply only when you receive data: + if (Serial.available() > 0) { + // read the incoming byte: + incomingByte = Serial.read(); + + // say what you got: + // Serial.print("I received: "); + // Serial.println(incomingByte, DEC); + } + if (incomingByte != 0){ + //Shutdown + if (incomingByte == 97){ + digitalWrite(xina,LOW); + digitalWrite(xinb,LOW); + digitalWrite(yina,LOW); + digitalWrite(yinb,LOW); + digitalWrite(zina,LOW); + digitalWrite(zinb,LOW); + xstat = 0; + ystat = 0; + zstat = 0; + } + //X Bridge Off + if (incomingByte == 98){ + digitalWrite(xina,LOW); + digitalWrite(xinb,LOW); + xstat = 0; + } + //Y Bridge Off + if (incomingByte == 99){ + digitalWrite(yina,LOW); + digitalWrite(yinb,LOW); + ystat = 0; + } + //Z Bridge Off + if (incomingByte == 100){ + digitalWrite(zina,LOW); + digitalWrite(zinb,LOW); + zstat = 0; + } + //H-Bridge Status message + if (incomingByte == 115){ + Serial.print(xstat); + Serial.print(ystat); + Serial.println(zstat); + } + //mag Status message + if (incomingByte == 113){ + Serial.println(magstat); + } + //positive "x" + if (incomingByte == 120){ + digitalWrite(xinb,LOW); + digitalWrite(xina,HIGH); + xstat = 1; + } + //negative "X" + if (incomingByte == 88){ + digitalWrite(xina,LOW); + digitalWrite(xinb,HIGH); + xstat = 2; + } + //positive "y" + if (incomingByte == 121){ + digitalWrite(yinb,LOW); + digitalWrite(yina,HIGH); + ystat = 1; + } + //negative "Y" + if (incomingByte == 89){ + digitalWrite(yina,LOW); + digitalWrite(yinb,HIGH); + ystat = 2; + } + //positive "z" + if (incomingByte == 122){ + digitalWrite(zinb,LOW); + digitalWrite(zina,HIGH); + zstat = 1; + } + //negative "Z" + if (incomingByte == 90){ + digitalWrite(zina,LOW); + digitalWrite(zinb,HIGH); + zstat = 2; + } + //mag reading + if (incomingByte == 109){ + sensors_event_t event; + mmc.getEvent(&event); + Serial.print(event.magnetic.x); + Serial.print(","); + Serial.print(event.magnetic.y); + Serial.print(","); + Serial.println(event.magnetic.z); + } + //temp reading + if (incomingByte == 116){ + sensors_event_t event; + mmc.getEvent(&event); + float temp_c = mmc.readTemperature(); + Serial.println(temp_c); + } + //end of loop + incomingByte = 0; + Serial.flush(); +} +} diff --git a/Makefile b/Makefile index 9f640bf..4c77130 100755 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PY=python3 -m py_compile DRIVER=driver.py -SRC=driver.py cage_controler.py window.py utilities.py +SRC=driver.py cage_controller.py window.py utilities.py GUIS=window.ui GSRC=new_window.py diff --git a/README.md b/README.md index b3c8288..753266f 100755 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -## oresat-helmholtz +# oresat-helmholtz + ![alt text](https://user-images.githubusercontent.com/33878769/50576984-cde2d900-0dd2-11e9-8117-1c2e21f85c7d.png) -## Magnetic Environment Simulator for CubeSats +## Magnetic Environment Simulator for CubeSats SOP can be found [here](http://psu-epl.github.io/doc/equip/testing/ETL/) at the Electronics Prototyping Lab website ![alt text](https://user-images.githubusercontent.com/33878769/48651456-dfe9f300-e9af-11e8-9a90-02227cccc314.jpg) - MCECS BETA Project 2018 diff --git a/cage_controller.py b/cage_controller.py old mode 100755 new mode 100644 index e9faf70..716a160 --- a/cage_controller.py +++ b/cage_controller.py @@ -2,9 +2,25 @@ import time # Stuff for regulated sensor delays import smbus # Stuff for controlling temperature and magnetic sensors import utilities as utils # Stuff for debugging and/or general info +from gpiozero import LED WIRE_WARN_TEMP = 100 # Min cage wire temperatures in F for warning WIRE_HCF_TEMP = 120 # Max cage wire temperatures in F for forced halting +pin = 'BOARD' + +class Coil(): # controls for motor drivers + def __init__(self, psu_index): + self.in_a = LED(pin + utils.COIL_ADDRS[psu_index][0]) + self.in_b = LED(pin + utils.COIL_ADDRS[psu_index][1]) + self.positive() + + def positive(self): + self.in_b.off() + self.in_a.on() + + def negative(self): + self.in_a.off() + self.in_b.on() class PowerSupply(serial.Serial): def __init__(self, port_device, input_delay=utils.INPUT_DELAY, baudrate=9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=1): @@ -18,6 +34,7 @@ def __init__(self, port_device, input_delay=utils.INPUT_DELAY, baudrate=9600, pa self.timeout = timeout self.warn_temp = 35 # Min cage wire temperatures in F for warning self.halt_temp = 40 # Max cage wire temperatures in F for forced halting + self.coil = Coil(self.index()) utils.log(0, 'Initialized Power supply with the following:\n\tPort: ' + str(port_device) + '\n\tInput Delay: ' + str(input_delay) @@ -26,6 +43,8 @@ def __init__(self, port_device, input_delay=utils.INPUT_DELAY, baudrate=9600, pa + '\n\tStop Bits: ' + str(stopbits) + '\n\tByte Size: ' + str(bytesize) + '\n\tTimeout: ' + str(timeout)) + def index(self): + return int(self.port_device[-1]) # uses last character of device name for index def toggle_supply(self, mode): utils.log(0, 'Setting ' + self.name + ' to: ' + str(mode)) @@ -33,11 +52,16 @@ def toggle_supply(self, mode): def set_voltage(self, voltage): utils.log(0, 'Setting ' + self.name + ' voltage to: ' + str(voltage) + ' volts.') - self.write(str("Asu" + str(voltage * 100) + "\n").encode()) + self.write(str("Asu" + str(abs(voltage) * 100) + "\n").encode()) def set_current(self, amperage): utils.log(0, 'Setting ' + self.name + ' current to: ' + str(amperage) + ' amps.') - self.write(str("Asi" + str(amperage * 1000) + "\n").encode()) + self.write(str("Asi" + str(abs(amperage) * 1000) + "\n").encode()) + self.amperage = amperage + if(amperage < 0): + self.coil.negative() + else: + self.coil.positive() def check_temperatures(): utils.log(0, 'Checking ' + self.name + ' temperatures...') @@ -84,17 +108,30 @@ def temperature_check_bounds(temp, warning, shutoff): utils.log(1, "Reached maximum warning temperature! Auto-powering down the cage!") toggle_all_power_supply(0) -# function to get magnetic field components from sensors -def magnetometer(): +# see page 21 of https://www.nxp.com/docs/en/data-sheet/MAG3110.pdf +# "When asserted, initiates a magnetic sensor reset cycle that will restore +# correct operation after exposure to an excessive magnetic field" +# Value goes back to 0 after completion +def init_magnetometer(): # Get I2C bus bus = smbus.SMBus(1) time.sleep(utils.INPUT_DELAY) - + # MAG3110 address, 0x0E(14) # Select Control register, 0x10(16) # 0x01(01) Normal mode operation, Active mode bus.write_byte_data(0x0E, 0x10, 0x01) time.sleep(utils.INPUT_DELAY) + + # MAG3110 address, 0x0E(14) + # Select Control register2, 0x11(17) + bus.write_byte_data(0x0E, 0x11, 0b00010000) + time.sleep(utils.INPUT_DELAY) + return bus + +# function to get magnetic field components from sensors +def magnetometer(bus): + time.sleep(utils.INPUT_DELAY) # MAG3110 address, 0x0E(14) # Read data back from 0x01(1), 6 bytes # X-Axis MSB, X-Axis LSB, Y-Axis MSB, Y-Axis LSB, Z-Axis MSB, Z-Axis LSB @@ -137,7 +174,7 @@ def temperature(): # 0x03(03) Resolution = +0.0625 / C bus.write_byte_data(0x18, 0x08, 0x03) - time.sleep(0.2) + time.sleep(utils.INPUT_DELAY) # MCP9808 address, 0x18(24) # Read data back from 0x05(5), 2 bytes @@ -154,7 +191,7 @@ def temperature(): bus.write_i2c_block_data(0x1c, 0x01, config) bus.write_byte_data(0x1c, 0x08, 0x03) - time.sleep(0.2) + time.sleep(utils.INPUT_DELAY) data = bus.read_i2c_block_data(0x1c, 0x05, 2) @@ -166,14 +203,3 @@ def temperature(): temperature_check_bounds(ctemp1, WIRE_WARN_TEMP, WIRE_HCF_TEMP) temperature_check_bounds(ctemp2, PSU_WARN_TEMP, PSU_HCF_TEMP) return ctemp1, ctemp2 - -def poll_data(duration = 10.0, dt = 1.0): - time_step = [0.0] - # temp_array = [temperature()] - mag_array = [magnotometer()] - while time_step[-1] < duration: - time.sleep(dt) - time_step.append(time_step[-1] + dt) - # temp_array.append(temperature()) - mag_array.append(magnotometer()) - return time_step, mag_array #temp_array, mag_array diff --git a/command_line.py b/command_line.py index c30545b..acf826a 100755 --- a/command_line.py +++ b/command_line.py @@ -1,6 +1,10 @@ import cage_controller import utilities as utils import magnetic_field_current_relation as mfcr +import time + +global x0, y0, z0 #initial magnetic field, sorry about the globals +#x0, y0, z0 = cage_controller.magnotometer() COMMAND_MAP = { 0: 'Exit program', @@ -11,24 +15,24 @@ 5: 'Get temperature data', 6: 'Get magnetometer data', 7: 'Set uniform magnetic field', - 8: 'Print data', - 9: 'Plot graph' + 8: 'Plot graph' } +# Turn power off by request or exit +def power_off(): + if(utils.supply_available()): + utils.log(0, 'Powering Off...') + for i in utils.POWER_SUPPLIES: + i.toggle_supply(0) + else: + utils.log(3, 'There are currently no power supplies available!\n\tThis option will not be available until one or more are connected and the controller is rebooted.') + # Displays the menu def display_menu(): utils.log(0, 'Helmholtz Cage Controller:') for num, description in COMMAND_MAP.items(): print('\t' + str(num) + ': ' + description) -# Prints data? -def print_data(): - time, mag = cage_controller.poll_data(20, 1) - i = 0 - for t in time: - print(time[i], mag[i]) - i += 1 - # # Legacy cage controller interface # (Can be used by specifying `cli` in the driver) @@ -39,16 +43,24 @@ def menu(control): elif control == 1: if(utils.supply_available()): voltage = float(input('New voltage [Volts]: ')) - supply_index= int(input('Power Supply [1-3]: ')) - utils.POWER_SUPPLIES[supply_index - 1].set_voltage(voltage) + supply_index = int(input('Power Supply [1-3] or [4] for "all": ')) + if supply_index in range(1,4): + utils.POWER_SUPPLIES[supply_index - 1].set_voltage(voltage) + elif supply_index == 4: + for supply in range(supply_index): + utils.POWER_SUPPLIES[supply - 1].set_voltage(voltage) utils.log(0, 'Voltage set to ' + str(voltage) + ' Volts on Supply #' + str(supply_index)) else: utils.log(3, 'There are currently no power supplies available!\n\tThis option will not be available until one or more are connected and the controller is rebooted.') elif control == 2: if(utils.supply_available()): current = float(input('New current [Amps]: ')) - supply_index= int(input('Power Supply [1-3]: ')) - utils.POWER_SUPPLIES[supply_index - 1].set_current(current) + supply_index = int(input('Power Supply [1-3] of [4] for "all": ')) + if supply_index in range(1,4): + utils.POWER_SUPPLIES[supply_index - 1].set_current(current) + elif supply_index == 4: + for supply in range(supply_index): + utils.POWER_SUPPLIES[supply - 1].set_current(current) utils.log(0, 'Amperage set to ' + str(current) + ' Amps on Supply #' + str(supply_index)) else: utils.log(3, 'There are currently no power supplies available!\n\tThis option will not be available until one or more are connected and the controller is rebooted.') @@ -60,36 +72,34 @@ def menu(control): else: utils.log(3, 'There are currently no power supplies available!\n\tThis option will not be available until one or more are connected and the controller is rebooted.') elif control == 4: - if(utils.supply_available()): - utils.log(0, 'Powering Off...') - for i in utils.POWER_SUPPLIES: - i.toggle_supply(0) - else: - utils.log(3, 'There are currently no power supplies available!\n\tThis option will not be available until one or more are connected and the controller is rebooted.') + power_off() elif control == 5: utils.log(0, 'Checking temperatures...') cage_temp_1, cage_temp_2 = cage_controller.temperature() utils.log(0, 'Sensor 1:\t' + str(cage_temp_1) + '°C\t' + str(utils.c_to_f(cage_temp_1) + '°F')) utils.log(0, 'Sensor 2:\t' + str(cage_temp_2) + '°C\t' + str(utils.c_to_f(cage_temp_2) + '°F')) elif control == 6: - utils.log(0, 'Checking magnotometer, units in microTeslas') + utils.log(0, 'Checking magnetometer, units in microTeslas') xMag, yMag, zMag = cage_controller.magnetometer() - utils.log(0, 'Manetic field Components:\n\tX: ' + str(xMag) + '\n\tY: ' + str(yMag) + '\n\tZ: ' + str(zMag)) + utils.log(0, 'Magnetic field Components:\n\tX: ' + str(xMag) + '\n\tY: ' + str(yMag) + '\n\tZ: ' + str(zMag)) elif control == 7: if(utils.supply_available()): - # TODO: Add back the current get to power supply - currents = mfcr.fieldToCurrent(x0, y0, z0) + x0, y0, z0 = cage_controller.magnetometer() + desired_x = float(input("What is the ideal strength of the x component? (microTeslas)\n")) + desired_y = float(input("What is the ideal strength of the y component? (microTeslas)\n")) + desired_z = float(input("What is the ideal strength of the z component? (microTeslas)\n")) + currents = mfcr.automatic([x0, y0, z0], [desired_x, desired_y, desired_z]) utils.log(0, 'Power Supply Current Updates:') - for i in range(0, len(currents)): - utils.POWER_SUPPLIES.set_current(currents[i]) - print('\tSupply #' + str(i) + ': ' + str(currents[i])) + for i, PS in enumerate(utils.POWER_SUPPLIES): + PS.set_current(currents[i]) + print('\tSupply #' + str(i+1) + ': ' + str(currents[i])) else: utils.log(3, 'There are currently no power supplies available!\n\tThis option will not be available until one or more are connected and the controller is rebooted.') elif control == 8: - print_data() - elif control == 9: - plot_graph() + utils.log(3, 'Function deprecated') + #plot_graph() + # function for interacting with the user def interface(): @@ -97,7 +107,19 @@ def interface(): while (control != 0): display_menu() try: - control = int(input('Selection?[0-9]: ')) + control = int(input('Selection?[0-8]: ')) menu(control) except ValueError: utils.log(3, 'There was a problem with your input: ' + str(control)) + + +def poll_data(duration = 10.0, dt = 1.0): + time_step = [0.0] + # temp_array = [temperature()] + mag_array = [cage_controller.magnetometer()] + while time_step[-1] < duration: + time.sleep(dt) + time_step.append(time_step[-1] + dt) + # temp_array.append(temperature()) + mag_array.append(cage_controller.magnetometer()) + return time_step, mag_array #temp_array, mag_array \ No newline at end of file diff --git a/driver.py b/driver.py index 68537bc..691dbaa 100755 --- a/driver.py +++ b/driver.py @@ -3,24 +3,25 @@ import cage_controller as cc import window as gui import command_line as cli +import atexit def usage(message): utils.log(3, message + '\n\tusage: python3 driver.py [cli/gui]') def main(): if(len(sys.argv) == 2): - # Guarentee that the folder for cage data exists + # Guarantee that the folder for cage data exists if(not os.path.isdir(utils.data_file_path())): utils.log(1, 'Path: ' + utils.data_file_path() + ' does not exist, creating it now.') os.mkdir(utils.data_file_path()) - # Initialize serial ports - utils.log(0, "Attemting to initialize power supplies...") + # Initialize serial ports, toggle power off + utils.log(0, "Attempting to initialize power supplies...") for i in utils.PSU_ADDRS: try: supply = cc.PowerSupply(i) utils.POWER_SUPPLIES.append(supply) - supply.toggle_supply(1) + supply.toggle_supply(0) except serial.serialutil.SerialException as e: utils.log(3, 'Could not initialize power supply:\n\t' + str(e)) # exit(1) @@ -28,7 +29,7 @@ def main(): if((not utils.supply_available())): utils.log(3, 'No power supplies were found, and cage initialization cannot continue.\n\tGracefully exiting.') # Main controler - utils.log(0, 'Begining main runtime!') + utils.log(0, 'Beginning main runtime!') if(sys.argv[1] == 'cli'): cli.interface() # Synchronous CLI Environmnet @@ -40,6 +41,19 @@ def main(): else: usage('Invalid number of options specified for the controller!') exit(1) + + # Upon exit, power off supply + try: + atexit.register(cli.power_off) + except: + utils.log(3, 'Could not power down!') + atexit.register(cli.power_off) + exit(1) if __name__ == "__main__": - main() + try: + main() + except KeyboardInterrupt: + utils.log(3, 'Interrupt Occured') + atexit.register(cli.power_off) + exit(1) diff --git a/graph.py b/graph.py index fbd2f35..b09df10 100755 --- a/graph.py +++ b/graph.py @@ -58,8 +58,8 @@ def __init__(self, parent, graph_range=utils.GRAPH_RANGE): # Graph Legend # self.plot.addLegend() - self.plot.plot([], pen='g', symbolBrush=0.2, name='MagSensor-X') - self.plot.plot([], pen='r', symbolBrush=0.2, name='MagSensor-Y') + self.plot.plot([], pen='r', symbolBrush=0.2, name='MagSensor-X') + self.plot.plot([], pen='g', symbolBrush=0.2, name='MagSensor-Y') self.plot.plot([], pen='b', symbolBrush=0.2, name='MagSensor-Z') self.toggle_button = QtWidgets.QPushButton('Start Graph', self) diff --git a/magnetic_field_current_relation.py b/magnetic_field_current_relation.py old mode 100755 new mode 100644 index 1490ac0..955827c --- a/magnetic_field_current_relation.py +++ b/magnetic_field_current_relation.py @@ -22,36 +22,15 @@ def paramMultiplier(d): # parameter is distance from origin, we usually only car def getNeededCurrent(desiredfield): #get microteslas return amps return (desiredfield / paramMultiplier(0)) * 2 -# def axialMagField(current, d): #get amps return microteslas -# #need to get actual current for axis from program or PSU somehow -# return current * paramMultiplier(d) - -def fieldToCurrent(efx, efy, efz): - desiredfield = float(raw_input("What is the ideal strength of the uniform magnetic field? (microTeslas)\n")) - #print "What is the initial strength of Earth's magnetic field? (microTeslas) (x, y, z)" - # in the future these initial values should come from magnotometer - #efx = float(raw_input("x: ")) - #efy = float(raw_input("y: ")) - #efz = float(raw_input("z: ")) +def axialMagField(current, d): #get amps return microteslas + #need to get actual current for axis from program or PSU somehow + return current * paramMultiplier(d) +# given the environmental and desired fields, what currents must we pull? +def automatic(env_field, ideal_field): #compensation - diffx = desiredfield - efx - diffy = desiredfield - efy - diffz = desiredfield - efz - - #negative amps means amps with a 180 degree polarity from normal - neededcurrentx = getNeededCurrent(diffx) - neededcurrenty = getNeededCurrent(diffy) - neededcurrentz = getNeededCurrent(diffz) - - return neededcurrentx, neededcurrenty, neededcurrentz - - # psu1=z, psu2=y, psu3=x - #print "Required current from PSU 1: " + str(neededcurrentz) + " amps.\n" - #print "Required current from PSU 2: " + str(neededcurrenty) + " amps.\n" - #print "Required current from PSU 3: " + str(neededcurrentx) + " amps.\n" - - #this block was just for testing equation in the other direction - #wantcurrent = raw_input("What is the current from the power supply? (amps)\n") - #resultfield = axialMagField(float(wantcurrent), 0) - #print "Resultant magnetic field: " + str(resultfield) + " microteslas.\n" + diff = [ideal_field[i] - env_field[i] for i in range(3)] + + #negative amps just means amps with a 180 degree polarity + needed_current = [getNeededCurrent(diff[i]) for i in range(3)] + return needed_current diff --git a/setup.py b/setup.py index 0acc8a9..a76be3f 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import pip import utilities as utils -DEPENDENCES = ['serial', 'smbus2', 'python-qt5', 'pyqtgraph', 'i2c'] +DEPENDENCES = ['pyserial', 'smbus2', 'python-qt5', 'pyqtgraph', 'i2c'] def install(package): if(hasattr(pip, 'main')): @@ -10,7 +10,7 @@ def install(package): utils.log(0, 'No main class was found when trying to install ' + str(package) + 'switching to internal main.') pip.__internal.main(['install', package]) - utils.log(0, str(package) + ' package guarenteed installed.') + utils.log(0, str(package) + ' package guaranteed installed.') def main(): for i in DEPENDENCES: diff --git a/utilities.py b/utilities.py old mode 100755 new mode 100644 index 3c9c8ff..39bfb00 --- a/utilities.py +++ b/utilities.py @@ -1,13 +1,15 @@ import os, random, time DEBUG = False +CLOSED_LOOP = True TICK_TIME = 500 GRAPH_RANGE = 35 -INPUT_DELAY = 0.2 +INPUT_DELAY = 0.001 DATA_ACCURACY = 4 POWER_SUPPLIES = [] TEMPERATURE_SENSORS = [] PSU_ADDRS = [ 'ttyUSB0', 'ttyUSB1', 'ttyUSB2' ] +COIL_ADDRS = [ ['31','32'], ['35','36'], ['37','38'] ] # change if you change pin-out ICON_IMG_PATH='./img/icon.png' CAGE_DATA_PATH = '/cage_data/' diff --git a/window.py b/window.py old mode 100755 new mode 100644 index ec434fc..cc70d79 --- a/window.py +++ b/window.py @@ -2,12 +2,18 @@ import graph as g import utilities as utils import cage_controller as cc +import magnetic_field_current_relation as mfcr from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtGui import * from PyQt5.QtCore import * +from math import sqrt class ControllerWindow(object): def __init__(self, window): + # Actual ambient magnetometer data + self.mag_bus = cc.init_magnetometer() + self.x_0, self.y_0, self.z_0 = cc.magnetometer(self.mag_bus) + self.control_mode = 0 self.window = window self.font = QtGui.QFont() @@ -90,6 +96,7 @@ def initialize_user_interface(self): self.psu_control_mode.setObjectName("psu_control_mode") self.psu_control_mode.addItem("") self.psu_control_mode.addItem("") + self.psu_control_mode.addItem("") self.psu_control_mode.currentIndexChanged.connect(self.toggle_control_mode) # @@ -106,7 +113,7 @@ def initialize_user_interface(self): self.psu1_input.setObjectName("psu1_input") self.psu2_label = QtWidgets.QLabel(self.widget) - self.psu2_label.setObjectName("psu2_input") + self.psu2_label.setObjectName("psu2_label") self.psu2_input = QtWidgets.QLineEdit(self.widget) self.psu2_input.setObjectName("psu2_input") @@ -148,8 +155,8 @@ def initialize_user_interface(self): # Graph # self.graph = g.Graph(self.widget) - self.graph.add_line('Magnetometer X', 'g') - self.graph.add_line('Magnetometer Y', 'r') + self.graph.add_line('Magnetometer X', 'r') + self.graph.add_line('Magnetometer Y', 'g') self.graph.add_line('Magnetometer Z', 'b') self.graph.setGeometry(QtCore.QRect(0, 700, self.width, self.height / 3)) self.graph.setAutoFillBackground(True) @@ -200,7 +207,7 @@ def translate_user_interface(self): self.psu3_input.setText(_translate("window", "4")) self.apply_button.setText(_translate("window", "PSU_BUTTON_UPDATE")) - + # self.accuracy_label.setText(_translate("window", "Data Accuracy:")) # self.accuracy_input.setSuffix(_translate("window", " decimals")) # self.accuracy_input.setValue(utils.DATA_ACCURACY) @@ -209,6 +216,7 @@ def translate_user_interface(self): self.psu_control_mode.setItemText(0, _translate("window", "Voltage")) self.psu_control_mode.setItemText(1, _translate("window", "Current")) + self.psu_control_mode.setItemText(2, _translate("window", "Magnetic Field")) self.toggle_control_mode() self.affirm_power_supplies() @@ -228,7 +236,7 @@ def update_layouts(self, x_off, y_off, spacing=10, lw=150, lh=60, iw=150, ih=50) self.psu3_label.setGeometry(QtCore.QRect(x_off + iw * 2, y_off, lw, ih)) self.psu3_input.setGeometry(QtCore.QRect(x_off + iw * 2, y_off + 2 * ih / 3, iw, ih)) self.apply_button.setGeometry(QtCore.QRect(x_off + iw * 2, y_off + 5 * lh / 3, iw, ih)) - + # self.accuracy_label.setGeometry(QtCore.QRect(x_off + self.width - 200 - spacing, y_off + ih + spacing, iw, ih)) # self.accuracy_input.setGeometry(QtCore.QRect(x_off + self.width - 120 - spacing, y_off + ih + spacing + 0, iw, ih)) self.quit_button.setGeometry(QtCore.QRect(self.width - 3 * iw / 2 - spacing, self.height - 100 - ih, 3 * iw / 2, ih)) @@ -317,7 +325,7 @@ def tick(self): # z = utils.generate_static(self.graph.lines[2].y) # Actual magnetometer data - x, y, z = cc.magnetometer() + x, y, z = cc.magnetometer(self.mag_bus) self.width = self.window.width() self.height = self.window.height() @@ -328,9 +336,9 @@ def tick(self): # voltage over current # current over voltage def toggle_control_mode(self): - labels = [ 'Voltage[V]:', 'Current[A]:' ] - button_labels = [ 'Apply voltage', 'Apply current'] - input_defaults = [ '12.0', '1.0'] + labels = [ 'Voltage[V]:', 'Current[A]:', 'Magnetic Field[uT]' ] + button_labels = [ 'Apply voltage', 'Apply current', 'Apply field' ] + input_defaults = [ '12.0', '1.0', '0.0' ] self.control_mode = self.psu_control_mode.currentIndex() self.active_control_mode_label.setText('Actively controlling for: ' + labels[self.control_mode]) @@ -341,6 +349,22 @@ def toggle_control_mode(self): def update_data_accuracy(self): utils.DATA_ACCURACY = self.accuracy_input.value + + def close_the_loop(self, target): + k_p = 1/200 # control proportional gain, microtesla per amp + k_i = 0 # control integral gain + epsilon = 1.0 # convergence criteria + steps = 5*5 # how many times to iterate + ei = [[0, 0, 0]] + for i in range(steps): + measurement = cc.magnetometer(self.mag_bus) # takes 0.2 sec + error = [target[i] - measurement[i] for i in range(3)] + ei.append([ei[-1][i] + utils.INPUT_DELAY*error[i] for i in range(3)]) + norm = sqrt(sum([error[i]**2 for i in range(3)])) + if(norm < epsilon): break + + for i, PS in enumerate(utils.POWER_SUPPLIES): + PS.set_current(PS.amperage + k_p * error[i] + k_i * ei[-1][i]) # Applies changes to the power supply def apply_psu_changes(self): @@ -349,11 +373,17 @@ def apply_psu_changes(self): if(utils.supply_available()): if(self.control_mode == 0): - for i in range(0, len(utils.POWER_SUPPLIES)): - utils.POWER_SUPPLIES[i].set_voltage(values[i]) + for i, PS in enumerate(utils.POWER_SUPPLIES): + PS.set_voltage(values[i]) elif(self.control_mode == 1): - for i in range(0, len(utils.POWER_SUPPLIES)): - utils.POWER_SUPPLIES[i].set_current(values[i]) + for i, PS in enumerate(utils.POWER_SUPPLIES): + PS.set_current(values[i]) + elif(self.control_mode == 2): + new_currents = mfcr.automatic([self.x_0, self.y_0, self.z_0], values) + for i, PS in enumerate(utils.POWER_SUPPLIES): + PS.set_current(new_currents[i]) + if(utils.CLOSED_LOOP): + self.close_the_loop(values) else: utils.log(3, 'An invalid control mode was specified: ' + str(self.control_mode) + '!\n\tThis input will be ignored and the power supplies cannot be modified until this is resolved.') else: @@ -401,7 +431,7 @@ def confirm_shutdown(self, event): self.shutdown_cage() exit(0) - # Ensures all physical equiptment is in its cloesd safe state then exits + # Ensures all physical equiptment is in its closed safe state then exits def shutdown_cage(self): utils.log(0, 'Shutting cage down gracefully...') if(utils.supply_available()):