1
0
Fork 0

Compare commits

..

280 Commits

Author SHA1 Message Date
linuswck 26f12dbf77 add draft for thermostat usage guide 2024-10-18 12:58:20 +08:00
linuswck 388e6725ef thermostat: Clear alarm when settings are loaded 2024-10-17 17:36:17 +08:00
linuswck f4c761c5ca pid: Scale PID Parameters with Sampling Rate
- Breaking changes
2024-10-17 17:36:06 +08:00
linuswck e560d8f1eb thermostat, gui: Set max v range according to datasheet 2024-10-16 16:21:11 +08:00
linuswck 0437c6e76e thermostat, gui: Relax the max settable TEC Current Limit
- Improper settings can cause PoE power failure and over heating
- See hardware repo Issue #51
2024-10-16 16:14:58 +08:00
linuswck bad21806f8 gui: Add new form for temp adc filter cfg and settable polling rate
List of Changes:
1. Get report via polling instead of active report mode
2. Allow user to set a custom report polling rate while settings polling rate is fixed to 10Hz
- it is necessary for pid autotune to function correctly
3. Add a form for configuring temperature adc filter
4. Use two different timer for polling report and settings to optimize performance

Known Issue:
1. CPU utilization increases with the report polling rate as for each report recv-ed, gui renders and plots one frame of the 4 graphs
2024-10-10 17:25:33 +08:00
linuswck cb2bc505c9 driver: expose _odr_type for filter cfgs 2024-10-10 17:25:29 +08:00
linuswck 4f19d2c2be driver: Expose the upper and lower limit settings for Sinc3WithFineODR Filter Cfg 2024-10-10 15:23:57 +08:00
linuswck e8d3858fc9 driver: return whether task queue is full in task_dispatcher() 2024-10-10 15:20:58 +08:00
linuswck 9bec56ed6c driver: Add support for GetPollInterval cmd 2024-10-10 15:19:43 +08:00
linuswck 51b82e0447 cmd_handler: Add cmd to get temp adc polling interval 2024-10-10 13:41:15 +08:00
linuswck 31e108a4b5 thermostat: settings obj now contains the exact filter type and val 2024-10-10 13:39:37 +08:00
linuswck 253f4410ee sys_timer: Add fn to retrieve ts in us resolution 2024-10-10 13:31:20 +08:00
linuswck bd72c382b0 thermostat: Fix incorrect Dac Calibration Algo
- Thermostat Firmware repo PR #133
2024-10-08 15:57:18 +08:00
linuswck a512614de5 net: add option not to open sock when it is polled
- fix a bug, which mcu can get stuck in State::HardReset indefinitely
2024-09-11 17:54:23 +08:00
linuswck 76f7875e3a gui: await for hard rst and dfu action 2024-09-11 17:54:23 +08:00
linuswck 0d64d62fb0 gui: Add pd_mon config form
- force user to follow a sequence of steps for safely configuring pd_mon
2024-09-11 17:54:18 +08:00
linuswck 0aeffdbf7b ld_pwr_exc_protector: disable irq after pwr off
-setting 0V threshold during power off might trigger IRQ
2024-09-11 17:05:38 +08:00
linuswck 89a1270c02 driver: send cmd err msg to a pyqt sig 2024-09-11 17:05:38 +08:00
linuswck c2e78e6f17 driver: Support ApplyPdParams cmd
- Include handling mechanism for "InvalidSettings"b response msg
2024-09-11 17:05:38 +08:00
linuswck 86e6d3764e laser: check pd_mon params & pwr limit before set
- Design to prevent Issue #47 on hardware kirdy repo from happening
- responsitivity, i_dark & transconductance determine the pwr limit range
- when "ApplyPdParams" and "SetLdPwrLimit" are called, pwr limit range
    is checked before applying new parameter
- Send out a "InvalidSettings" response with error message if settings
    cannot be applied
- Add settable power range to settings json object
2024-09-11 17:05:34 +08:00
linuswck 1111967a7b gui: add arg for saving log to a file 2024-09-04 17:21:13 +08:00
linuswck 1896e2534b main: Add PrepareForDfu state 2024-09-04 16:12:06 +08:00
linuswck 6d4b1b0574 driver: end_session if ConnClose & Dfu is recv-ed 2024-09-04 16:10:42 +08:00
linuswck 0b5fda2cd9 net: close oldest sock if available socks run out
- By default, smoltcp by default reset any new connections
    if there is no available socket
2024-09-04 15:46:43 +08:00
linuswck b763350a8b net: disable IRQ when data is being enqueued
- This prevents broken json to be sent out due to IRQ
2024-09-04 15:46:43 +08:00
linuswck 838592c812 gui: Display hw_rev when connected 2024-09-04 15:46:43 +08:00
linuswck e632cbbfdd driver: use readuntil to get response msg
- The driver may recv partial ctrl msg, which causes a json decode error
2024-09-04 15:46:43 +08:00
linuswck 27bf573010 driver: retrieve hw_rev after conn is established 2024-09-04 15:45:27 +08:00
linuswck c267c30b89 driver: add get_hw_rev() fn 2024-09-04 15:39:27 +08:00
linuswck c5826876a6 firmware: Add cmd to get hw_rev 2024-09-04 15:39:27 +08:00
linuswck 6782cda790 Update pid autotuner code 2024-09-04 15:39:27 +08:00
linuswck 2fe2ef531b Update example code 2024-09-04 15:39:27 +08:00
linuswck f2ad06ecae gui: patch the GUI code 2024-09-04 15:39:27 +08:00
linuswck 5166bb7ba8 driver: rm thread use & make it asyncio callable
- make all cmds asyncio callable
- control specific cmds can be enqueued to the handler synchronously
2024-09-04 15:39:21 +08:00
linuswck 9c611fc861 main: poll iface for dfu & hard rst ctrl msgs 2024-09-03 12:15:03 +08:00
linuswck 51913f2e2f net: fix incorrect poll_iface() calls
- poll_iface might be called by interrupt while poll_iface was being called in main loop
- Disable interrupt when poll_iface is being processed
- Only use IfacePollTimer to poll_iface
2024-08-27 18:49:43 +08:00
linuswck c86d67b15c firmware: do not display smoltcp log 2024-08-27 18:49:43 +08:00
linuswck 3d2294a90c driver: fix incorrect recv_response behavior 2024-08-27 18:49:13 +08:00
linuswck 3ba8b99084 Use degree celsius as temperature base unit
- Breaking: Erase settings before flashing
- except beta in Steinhart-Hart equation
- to keep the temperature value set consistent with the value returned
- f32 floating point calculation is inaccurate
2024-08-07 17:36:12 +08:00
linuswck af73ac8127 gui: Add text for LD & Thermostat status 2024-08-07 12:48:12 +08:00
linuswck c214e4182d temp_mon: fix limits not effective at stable temp
- previously, temp is stable, temp limits are not checked
2024-08-07 12:12:07 +08:00
linuswck fb69ae3306 main: Don't change temp ctrl mode at otp 2024-08-07 11:26:47 +08:00
linuswck 8c8ed12522 gui: Disable param tree compact height
- The displayed number was clipped by the pyqtgraph spinbox
2024-08-06 13:01:57 +08:00
linuswck 572e2dbc5d gui: update for the new driver code
- Default to use active report mode
- Connection will be retried upon abnormal disconnection
- Remove poll every _s and apply btn
2024-08-06 13:01:57 +08:00
linuswck 82c46e04d0 Update PID Autotune Code for Driver Code 2024-08-06 13:01:57 +08:00
linuswck 28f8c3c497 Update Example Code for Driver Code 2024-08-06 13:01:57 +08:00
linuswck 47bf166ecb driver: kirdy driver rewrite
- Start a new thread to handle connections and schedule FIFO task execution
- Make all the cmd accessible without using async
- Connections automatically retry upon disconnection
- Add support for PyQt6 Signal
2024-08-06 13:01:54 +08:00
linuswck 1dcac25574 gui: Use a custom regex to handle the suffix with temperature characters 2024-07-31 11:35:04 +08:00
linuswck 3f9a4bf140 ld: Block pwr on request if alarm is asserted
- Align the alarm and pwr up behavior with thermostat temp_mon
2024-07-26 14:29:46 +08:00
linuswck 201148fb21 main: allow loading flash without device settings
- Fix bugs: ld & thermostat settings cannot be loaded if device settings
    are not present. But, device settings can be left not configured.
2024-07-26 11:10:32 +08:00
linuswck a90031dd6c ld_pwr_exc_protector: clear irq bit at pwr up 2024-07-26 11:10:32 +08:00
linuswck 7b52072617 ld: cfg hardware when pd_mon params changes 2024-07-26 11:10:32 +08:00
linuswck c241d34434 gui: fix default units and mv to params' titles
- siPrefix(eg. m, u, k) is no longer editable and fixed
- the displayed unit is relocated to ctrl panel params' titles
2024-07-26 11:10:28 +08:00
linuswck 20c3d42dd7 rm ld soft current limit 2024-07-25 15:53:00 +08:00
linuswck aae89256c3 gui: fix ctrl_panel value getting overwritten bug 2024-07-24 12:19:26 +08:00
linuswck 3344e7ab19 rm debug comments 2024-07-23 10:39:33 +08:00
linuswck a76b8de994 thermostat: Save the user config-ed value directly
- the firmware recorded the actual value set with quantization errors or
    other errors
- this caused confusion for the user when they read back the settings
2024-07-22 17:37:25 +08:00
linuswck 3410a271fd thermostat: rst pid_state at pwr_up 2024-07-22 10:54:01 +08:00
linuswck 8f38acd0f2 thermostat: do not rst tec_i_out to 0 at pwr down 2024-07-22 10:52:32 +08:00
linuswck b95b9dcefa gui: Display -273.15 if thermistor is not detected
- if tec_temp == None -> Hide temp_plot graph and display -273.15 on ctrl panel
2024-07-19 18:37:52 +08:00
linuswck 5e6a4c0646 gui: Do not turn on report mode for PID Autotune 2024-07-19 18:24:14 +08:00
linuswck ec5bf1d6b6 gui, ld: term_status -> term_50ohm
- term_status (Is50Ohm / Not50Ohm) -> term_50ohm (On/Off)
2024-07-19 18:10:38 +08:00
linuswck 3737c2ed59 gui: increase the connection timeout value
- 0.1s is too short. Min settable sampling rate is 1.25Sps
2024-07-19 18:10:38 +08:00
linuswck 4cd328d98c ld: apply pwr limit setting to analog watchdog
- This fixes a bug which power limit is not applied to pd_mon at startup
2024-07-19 18:10:38 +08:00
linuswck af95de0f16 thermostat: cal gain error at TEC DAC output 2024-07-19 18:10:12 +08:00
linuswck db2f76771a Update Kirdy Driver Example Code 2024-07-19 18:10:08 +08:00
linuswck 6f87cba9a6 drive: use aenum for cmd 2024-06-21 16:54:24 +08:00
linuswck 80d94270a2 fix typo 2024-06-18 15:43:36 +08:00
linuswck 456691f79f gui: import .ui file and rm auto generated py file 2024-06-17 14:49:35 +08:00
linuswck 0f3db7fec5 gui: Fix typos 2024-06-17 14:49:35 +08:00
linuswck 9002ca6992 gui: Fix a bug which GUI may recv two msgs at once 2024-06-17 14:49:31 +08:00
linuswck 92f6b83b16 gui: Fix a bug which dfu cmd can cause connrst err
- Kirdy may close and reset itself before host computer ends its session
2024-06-17 13:16:52 +08:00
linuswck da50f372d9 gui: plot ld_pwr is zero if responsitivity not set 2024-06-17 13:16:52 +08:00
linuswck 57bfd6d83c gui: Relocate LD Power limit params in ctrl panel 2024-06-17 13:16:52 +08:00
linuswck dacf9f0fa5 gui: Add pid autotune to ctrl panel 2024-06-17 13:16:52 +08:00
linuswck bfd8c0e43a pid_autotune: Update pid autotune main fn 2024-06-17 13:16:52 +08:00
linuswck dc97f42bd0 boot: Do not pwr up LD & TEC at boot stage
- This fix LD current spike at boot up
2024-06-17 13:16:52 +08:00
linuswck 3813b79623 driver: Fix IP Settings not being able to update 2024-06-17 13:16:52 +08:00
linuswck 83a764889c ld_pwr_exc_protector: set smpr to be slowest
- reduce noise on the analog watchdog
2024-06-17 13:16:52 +08:00
linuswck c09da0db98 ld_pwr_exc_protector: Apply 32-pt avg to pd_v
- Reduce noise amplitude from STM32f4 ADC
2024-06-17 13:16:52 +08:00
linuswck d5a620c76b pd_mon: make transconductance user-configurable
- hw change: different boards now have different gain settings in order
    to set a pd current range close to the ld being used
- transconductance and fin_gain params are added to flash with "Device"
    as key
2024-06-17 13:16:48 +08:00
linuswck d1660c6090 ld_pwr_exc_protector: cal volt offset for pd mon
- hw change: A voltage offset is added onto the pd mon pin
2024-06-13 15:43:44 +08:00
linuswck f35546b070 gui: lock param tree value when user inputs a new value
- prevent user input from getting overwritten from incoming data
2024-06-11 15:03:09 +08:00
linuswck 0dceb8c3c6 correct typo 2024-05-10 12:27:34 +08:00
linuswck 266110ba14 pid: rm setpoint change kick
- See Thermostat Firmware PR#107
2024-05-10 11:34:58 +08:00
linuswck e8aaf5f66b net: Implement iface polling timer
- iface needs to be polled with ref to iface.poll_at time to ensure quality of service
- iface.poll() also services TCP related Timers
2024-05-02 13:13:47 +08:00
linuswck 70fed23c51 main: Add checks before sock send/recv are called 2024-05-02 13:05:10 +08:00
linuswck b8241d1f27 cargo fmt
- rustfmt.toml is ported from artiq-zynq repo
2024-04-23 17:09:26 +08:00
linuswck ded7dd7694 gui: add waitingspinnerwidget 2024-04-23 15:55:14 +08:00
linuswck 7fa4330797 gitignore: ignore python .pyc files 2024-04-23 15:52:32 +08:00
linuswck 7be35fe7f0 thermostat: Reduce DAC calibration time
- 13 out of 18 bit of DAC is used to calibrate the output with 12bit ADC
- Calibration with more than 13 bit does not improve the accuracy
2024-04-23 15:45:37 +08:00
linuswck c5efc6ca57 MAX1968: Improve STM32 ADC calibration consistency 2024-04-23 15:35:23 +08:00
linuswck b330f45260 net: Increase ARP Cache Size from 4 to 16 2024-04-23 11:43:27 +08:00
linuswck 554f292cab net: Disable nagle algo & Set keep_alive duration 2024-04-22 17:06:07 +08:00
linuswck f303ab639a net: Reset tcp socks when new eth link is detected 2024-04-22 17:06:02 +08:00
linuswck ba30575406 GUI: Improve abnormal disconnection handling 2024-04-22 17:05:56 +08:00
linuswck bfdb1f5066 gui: move tool, save & load flash btns to menu bar
- Add a new form to update IP address
2024-04-19 15:56:48 +08:00
linuswck 5b35f32a42 driver: Handle CancelledError for send_cmd handler 2024-04-19 15:56:48 +08:00
linuswck 2df22d5dcb driver: Use inet_aton to check IPV4 address format 2024-04-19 15:56:48 +08:00
linuswck b73eacd234 driver: Wait for a sec after hard rst cmd is sent 2024-04-19 15:56:43 +08:00
linuswck 4a06a7579f gui: handle hard reset in _on_connection_changed()
- Ensure correct gui disconnection seq during Kirdy hard reset
2024-04-19 15:56:36 +08:00
linuswck 9524601cb6 gui: Improve DataWatcher operation
- Ensure there is only one instance of signal_emitter() task
- calling stop_watching should stop report_task
2024-04-19 13:25:00 +08:00
linuswck 4116962175 firmware: rst active_rpt if eth link is not active 2024-04-18 17:33:09 +08:00
linuswck 0e087c6992 ld: ld_soft_i_limit now limits ld_i_out being set 2024-04-18 17:04:52 +08:00
linuswck 6b250aa1fd gui: Add global QT Style Sheet
- Gray out the objects if they are disabled
2024-04-18 16:45:35 +08:00
linuswck de80aedafc gui: separate connection_menu into Qdiag obj 2024-04-11 16:58:48 +08:00
linuswck 31a97bdeb4 gui: GUI Initial Commit
- Adapted from Thermostat GUI PR #101
2024-04-11 16:33:35 +08:00
linuswck 784b8a357b driver: Update send_cmd fn to use msg_type params 2024-04-10 16:57:43 +08:00
linuswck ceb003e07e cmd_handler: Add msg_type params to report & settings 2024-04-10 16:57:43 +08:00
linuswck 10873f4791 firmware: Load default pwr_on settings from flash 2024-04-10 16:57:43 +08:00
linuswck e160316bfc ld: add ld_terms_short in settings report 2024-04-10 16:57:43 +08:00
linuswck 0a992c1dc8 ld: pd_responsitivity -> pd_mon_params 2024-04-10 16:57:43 +08:00
linuswck ae74455f94 driver: Separate setting pd_config into two fns 2024-04-10 16:56:20 +08:00
linuswck 6bccbceb81 main: Add a state to only save network settings 2024-04-10 16:56:20 +08:00
linuswck eaebdb390d cmd_handler: change mW to W in SetLdPwrLimit cmd 2024-04-10 16:56:20 +08:00
linuswck 489333c43d ld: save pwr_limit into settings 2024-04-10 16:56:20 +08:00
linuswck 4288d465c9 thermostat: Report temp_setpoint in degree celsius 2024-04-10 16:56:20 +08:00
linuswck 09863353cd driver: Add lock for tcp writer & reader stream 2024-04-10 16:56:20 +08:00
linuswck 6d107d55cf driver: Add StoppedConnecting exception 2024-04-10 16:56:20 +08:00
linuswck d435dc06d8 fix typos 2024-03-25 15:43:20 +08:00
linuswck 898358f4e6 pykirdy: Add reporting interval in report_mode 2024-03-25 15:43:20 +08:00
linuswck a13c6fa86c pykirdy: Update handling of json objects received
- Accept new line delimited json in read_response()
2024-03-25 15:43:20 +08:00
linuswck b1a1173075 cmd_handler: Add newline at the end of json sent 2024-03-25 15:43:20 +08:00
linuswck af68b0e90a Update rust, stm32-eth, hal, smoltcp versions 2024-03-25 15:43:12 +08:00
linuswck f8abfd4300 Update nixpkgs version 2024-03-22 12:48:53 +08:00
linuswck 7d41edef20 ld: Increase the delay before turning on LD_PWR
- Delay is tested and verified with oscilloscope
- Tested with 0.3A LD output current
2024-03-21 15:21:24 +08:00
linuswck 607da8a822 ld: Add a delay before turning on LD PWR
- DAC output might not drop to 0V before PWR is being turned on
- Thus, it might drive nondeterministic current into the laser diode during pwr up
2024-03-20 17:24:47 +08:00
linuswck 7f06fc06fd async driver: fix incorrect timeout handling 2024-03-20 15:35:12 +08:00
linuswck 67f9e65df8 Start tec_readings_conversion after report is sent
- Before this fix, tec_readings only converts when eth_is_pending
2024-03-20 15:35:12 +08:00
linuswck 7d2e14ef2f eth: Poll Iface In ETH IRQ
- Improve responsiveness of TCP packets handling
- Fix a bug when one client is connected and active report is ON, the other client's cmds do not receive Ack from Kirdy
2024-03-20 15:32:28 +08:00
linuswck e525a3f354 fix typo 2024-03-20 12:06:05 +08:00
linuswck 8431e9f43d Update README 2024-03-20 11:55:21 +08:00
linuswck e9d5f4598a Fix wrong USB Product Description 2024-03-20 11:45:09 +08:00
linuswck 1867935047 Send report to connected socket
- Remove active report in flash settings
2024-03-19 17:38:29 +08:00
linuswck 99cf17f7e4 temp_mon: Add support for constant current mode 2024-03-19 14:52:26 +08:00
linuswck 048245f674 set_tec_max_i_pos/neg->...cooling_i/heating_i 2024-03-19 14:52:26 +08:00
linuswck 0a01d299bc Fix wrong reporting of Thermostat Pwr_on status 2024-03-19 14:52:26 +08:00
linuswck dbbd438e92 ld: Fix unable to save & load ld_i flash settings 2024-03-19 14:48:25 +08:00
linuswck 3ad7c417f6 Fix a ld current ramp up bug
- During ld_i ramp up, if pwr up cmd is called, ld_i is set incorrectly
2024-03-18 16:41:55 +08:00
linuswck c06491a8b2 Update PID Autotune Code 2024-03-18 15:55:05 +08:00
linuswck 0380c8d30b Unify naming and unit in firmware and driver
- mA, uA -> A
- K -> Degree Celsius
- tec -> thermostat
2024-03-18 15:55:05 +08:00
linuswck ed5cda6364 Correct wrong comment in driver 2024-03-18 15:54:25 +08:00
linuswck 61624b0bd6 Implement Kirdy AsyncIo Driver and example code 2024-03-15 17:14:22 +08:00
linuswck 1480305c16 max1968: Fix ADC+DMA does not start converting bug 2024-03-15 17:12:42 +08:00
linuswck 74c465d16f fix typos 2024-03-12 11:47:58 +08:00
linuswck 574abc2c2f Add instructions to erase flash settings with DFU 2024-03-12 11:26:45 +08:00
linuswck a9dbff8250 Send Response Back to The Client after Recv Cmd 2024-03-08 16:38:52 +08:00
linuswck 8cd3f70721 Add Instructions for Erasing Flash Settings 2024-03-07 16:20:51 +08:00
linuswck b7231f48ff Fix Recv Dfu cmd doesn't hard reset bug 2024-03-07 10:26:32 +08:00
linuswck 7f6a385e1d Support controls from multiple TCP Socket Clients 2024-03-06 16:30:31 +08:00
linuswck eff8adc184 Update Eth Link Speed with the detected Speed
- adds support to Half/FullDuplexBase10T, HalfDuplexBase100Tx Eth Speed
- fix a bug which Kirdy requires link to up to enter main loop
2024-03-06 12:15:08 +08:00
linuswck a579e5c5d5 Add default gateway into IpSettings 2024-03-06 10:34:57 +08:00
linuswck 65b757ac3c Add cmd to Store IP Settings in Flash
- Cmds Setup Flow: Set IP -> Save Settings to Flash -> Issue HardRst
2024-03-05 16:44:28 +08:00
linuswck 911d9a7bc9 Load & Config IP Settings from Flash at StartUp 2024-03-05 16:44:03 +08:00
linuswck 111d9a4226 cleanup 2024-03-05 16:33:49 +08:00
linuswck a3885c365e Implement Save & Load Flash Settings in FSM & cmd 2024-03-05 16:33:45 +08:00
linuswck a58b0954ec Add Set Default PWR On cmd for ld & tec 2024-03-05 10:35:05 +08:00
linuswck 92c6cf12d4 Add fns to apply ld, tec settings summary configs 2024-03-05 10:35:05 +08:00
linuswck ef2410f441 Add fns, struct items for getting ld, tec settings 2024-03-05 10:35:05 +08:00
linuswck adfa4f5fa3 Move TempAdcFilter struct def to Thermostat 2024-03-05 10:35:05 +08:00
linuswck df939eb9a3 Use FSM to manage device state in main loop 2024-03-05 10:35:01 +08:00
linuswck 3aca712e1d Fix typos 2024-03-04 10:32:33 +08:00
linuswck 4526536a48 Ignore packets of 0 bytes size 2024-03-01 11:31:06 +08:00
linuswck f488786e1c Use DMA to read Tec_I & Tec_V
- Remove the single conversion polling time with DMA
2024-03-01 11:31:01 +08:00
linuswck 3528d8a68f Update Python Test Scripts 2024-02-28 13:33:51 +08:00
linuswck 46393bacdb Add comments on TCP client development 2024-02-28 13:00:02 +08:00
linuswck 6fc800e562 net: Set stm32 link speed with the Phy Speed read 2024-02-28 13:00:02 +08:00
linuswck 76477065be Change f64 to f32 for SI Unit related computation
- Save computational time
- Improve the processing speed of TecSetI cmd
2024-02-28 12:59:58 +08:00
linuswck b1fa0e51c8 Add a separate tec_set_i cmd to reduce overhead
- miniconf is slow at validating complex struct with large enum lists
2024-02-28 12:46:21 +08:00
linuswck 9f82fa58f4 cmd: Add temp adc filter config cmd 2024-02-28 12:23:09 +08:00
linuswck 19341672a9 Correct SPI Clock Freq and Set them to Max
- Set the SPI frequency to maximum to reduce data transaction time
- SPI1 SCLK is divided from PCLK2
- SPI2, SPI2 SCLK are divided from PCLK1
2024-02-28 12:23:09 +08:00
linuswck 86a9fb039e boot: Correct PCLK1, PCLK2 freq value
- PCLK is divided from HCLK in power of two
- abs max pclk1 = 42MHz
- abs max pclk2 = 84MHz
2024-02-28 12:23:09 +08:00
linuswck a2bb390ae2 ad7172: Add filter configs for single channel mode 2024-02-28 12:23:09 +08:00
linuswck 18dd0a7963 Save Thermostat and Laser Diode PWR On/Off status 2024-02-28 12:22:40 +08:00
linuswck d1cc677668 thermostat: split poll_adc & update_pid into 2 fns 2024-02-28 12:21:37 +08:00
linuswck c09ccc29cd cmd: Add cmd to get all device settings 2024-02-23 12:45:29 +08:00
linuswck c4135f6ac3 cmd: Add cmds to config pd_mon params 2024-02-22 16:44:45 +08:00
linuswck 6ee45b4814 thermostat: Limit Pid Temp SetPt to TempMon limits 2024-02-21 17:37:56 +08:00
linuswck a8787430b1 Add Temperature Monitor to Thermostat
- Issue an alarm when temperature goes out of user-defined operating range during Pid Controller startup
    or reading is outside of +-0.5 Degree from temperature set point after Pid Controller becomes stable
- If alarm is observed, power down laser and tec controller and disengage Pid Controller
- Add the corresponding cmd for configuring the temperature monitor
2024-02-21 17:32:11 +08:00
linuswck ed785b7c85 ld: correct timer and dac reset in pwr_up seq 2024-02-21 11:11:12 +08:00
linuswck 30ab228b4b ld: Set default ld_current_limit in setup() fn 2024-02-20 16:22:13 +08:00
linuswck 88cca12a60 ld: Use Timer IRQ to ramp up/down output current
- Fixes possible watchdog timeout
- Improves ethernet response time & thermostat Pid update interval consistency
2024-02-20 16:18:06 +08:00
linuswck 8139ebdc1b ld_ctrl: set_dac() fn now returns the volt set
- before this fix, set_dac() just returns the inputted voltage
2024-02-20 12:30:00 +08:00
linuswck 5f83b73011 cmd: Add active report mode cmd 2024-02-19 15:08:00 +08:00
linuswck 0642640da7 dfu: Add eth cmd, fns to put stm32 into dfu mode 2024-02-16 17:23:47 +08:00
linuswck 6dd1bf9301 hw_rev: Generate random mac address on rev0_3 2024-02-16 17:23:39 +08:00
linuswck d2efc02b6a eth_cmd_test: Reset ld current to 0A during SIGINT 2024-02-15 12:47:29 +08:00
linuswck 89d415194d update eth_cmd_test code 2024-02-15 12:14:14 +08:00
linuswck 09b3765877 pid_autotune: Add pid autotune script
- Port from thermostat repo
2024-02-15 12:14:14 +08:00
linuswck 7a76325288 cmd: rm rev from cmd json
- not in used
2024-02-15 12:14:14 +08:00
linuswck ffa5f4e8ff pid: Use Thermostat's PID algo instead of IDSP
- Has lower steady state error upon testing with PID autotune
2024-02-15 12:14:14 +08:00
linuswck e9a396f001 thermostat: Add abs timestamp to StatusReport 2024-02-15 12:14:14 +08:00
linuswck 412f5ec58b cmd: add cmds to poll tec temp, dis/enable pid 2024-02-15 12:14:14 +08:00
linuswck e29898f8f8 fix typos 2024-02-15 12:14:14 +08:00
linuswck 60a79d1780 rename get_term_status -> get_lf_mod_in_impedance 2024-02-15 12:14:14 +08:00
linuswck b0edd3dba2 hw_rev: Wait for 5s b4 gpio init instead of 1.5s
- See Issue #32
2024-02-15 12:14:14 +08:00
Sébastien Bourdeauducq 4c331dd319 add BSD build instructions 2024-02-12 18:33:06 +08:00
linuswck 6b05b2d851 Fix Power Up Hw Issue
- See Issue #32 on Kirdy Hw Repo
2024-02-07 12:46:13 +08:00
linuswck f22ab430b8 pid: Use IDSP impl of Pid, Add Eth Pid Ctrl Cmds 2024-02-07 11:56:38 +08:00
linuswck 4cd650367d thermostat: cleanup pid_engaged fn 2024-02-05 15:14:51 +08:00
linuswck de262b849d eth: Add Thermostat related json cmd 2024-02-05 15:14:23 +08:00
linuswck 4132bfacd3 boot: Set thermostat & LD init output current to 0 2024-02-02 17:37:37 +08:00
linuswck 6096711d2c eth: Poll and Handle Json based Ld ctrl cmd
- Upgrade to miniconf 9.0
- Only non report related laser diode ctrl command is implemented
2024-02-02 17:35:58 +08:00
linuswck 3d3d6f5cb5 ld: cleanup 2024-02-02 17:23:39 +08:00
linuswck 172c3e1dfc cleanup boot.rs, thermostat.rs 2024-02-01 16:23:52 +08:00
linuswck ee0ed8ebe7 eth: add drivers for init, polling & sending data 2024-02-01 16:20:36 +08:00
linuswck 771f2813f7 gpio: correct wrong eth pin definition 2024-01-31 17:15:32 +08:00
linuswck 4e0d14def2 laser: Add driver and fn to get termination status 2024-01-30 13:01:33 +08:00
linuswck f6677d874c LD: Rst LD current at power_up for soft start 2024-01-26 16:56:55 +08:00
linuswck d391e3a1fb LD: Setting LD output current now ramps up slowly 2024-01-26 16:56:55 +08:00
linuswck edd30e94a0 laser_diode: Add fn to set pd_dark_i 2024-01-26 16:56:55 +08:00
linuswck ad731c2f15 rename pd_mon -> pd_responsitivity 2024-01-26 16:56:55 +08:00
linuswck 9d8a553669 pd_mon: add fns to convert between pwr and current 2024-01-26 16:56:55 +08:00
linuswck e22f424531 rm unused cargo config target definition 2024-01-26 16:56:55 +08:00
linuswck 6af0f992d5 LdPwrExcProtector: Cleanup 2024-01-26 16:56:55 +08:00
linuswck f50505feaf Rename Analog_Wdg -> LdPwrExcProtector 2024-01-26 16:56:55 +08:00
linuswck 9ae867cd88 max1968: rm redundant adc config 2024-01-26 16:56:55 +08:00
linuswck 85b50bf824 Analog_Wdg: Finish Power Excursion Protection fns
- Verified to be working
2024-01-26 16:56:39 +08:00
linuswck 07ea733b34 thermostst: Patch hw related settings for rev0_3 2024-01-24 12:25:45 +08:00
linuswck 5bea3f0e5f boot: init analog wdg & power up laser after cfg
- Verified to be boot-able on Rev0_3 board
2024-01-24 12:19:07 +08:00
linuswck af283b17ac laser_diode: use analog wdg fns 2024-01-24 12:18:42 +08:00
linuswck c3022e9db1 Analog_Wdg: Add GPIO init 2024-01-24 12:18:42 +08:00
linuswck c02181c80c laser diode: Add Analog Watchdog initial commit
- rm pid_state for laser diode
2024-01-24 12:18:42 +08:00
linuswck 095fe8ea69 AD7172: Rm deprecated warning for Rev0_2 hw bug 2024-01-24 12:18:42 +08:00
linuswck a0b67cdb09 MAX1968: Patch DacVfb pinout for rev0_3 2024-01-24 12:18:42 +08:00
linuswck 381f25f036 AD7172: correct wrong NSS pinout definition 2024-01-24 12:18:42 +08:00
linuswck c768bdc93a Thermostat: Add fn to calibrate ADC's VDDA
- ADC1 exclusive feature
- Calibrated VDDA val can be passed to other ADCs to adjust gain error
2024-01-24 12:18:42 +08:00
linuswck 0d12c902fc pd_mon: Separate ld_power calculation to a file 2024-01-24 12:18:37 +08:00
linuswck 3cfdee917a rename current_source -> ld_ctrl 2024-01-24 12:16:06 +08:00
linuswck 5f582be143 rename ld_drive to laser_diode 2024-01-24 12:16:06 +08:00
linuswck 2f7ca2a706 Thermostat: Add fns to report status & settings
- Report all system status, PID Settings, NTC parameter, TEC Settings
2024-01-24 12:16:06 +08:00
linuswck ff3d9b790a thermostat: pid_state rename variables 2024-01-24 12:16:06 +08:00
linuswck bc7bf9a6e7 rename channel_state -> pid_state 2024-01-24 12:16:02 +08:00
linuswck ca110962f7 Thermostat: Add pid_ctrl parms & its related fns
- setup: include the pid_ctrl
- poll_adc: Fetch Temperature and Update PID to set the current output
2024-01-24 12:16:02 +08:00
linuswck d0f226ce03 Thermostat: Add default to ChannelState 2024-01-24 12:16:02 +08:00
linuswck f49fd08c69 ad7172: impl default for ch_calibration 2024-01-24 12:16:02 +08:00
linuswck ccaf728c75 thermostat: Add channel_state mod for PID Control 2024-01-24 12:15:57 +08:00
linuswck 3ac287ace2 steinhart_hart: Add Steinhart-Hart eq fns
- Port from thermostat firmware
2024-01-17 12:20:34 +08:00
linuswck cdf900a5b6 gpio: Add AD7172 phy gpio init
- Will not function on rev0_2 kirdy due to wrong hardware connection
2024-01-16 17:36:16 +08:00
linuswck af8d361b95 AD7172: Add AD7172 drivers fns
- Port from Thermostat Firmware
2024-01-16 16:19:01 +08:00
linuswck e355e83d28 memory.x: Add comment on the reserved flash sector 2024-01-16 15:27:43 +08:00
linuswck 285fc9b5d6 memory.x: Reserved Flash Sector 11 for Config 2024-01-15 17:44:44 +08:00
linuswck 8c612fc0c9 memory.x: RM CONFIG MEMORY area in linker script
- STM32f407 does not have second bank or flash/ Sector 12 unlike F427
2024-01-15 17:43:17 +08:00
linuswck 42cd55645c main: Add demo fns to use on-board flash 2024-01-15 17:21:48 +08:00
linuswck d190b8b192 flash: Add init boot seq
- Max flash erase time for PSize = 8 and 128KiB Sector is 4s
- Watchdog period is changed accordingly
2024-01-15 17:21:06 +08:00
linuswck 74325a3cee flash: Add fns to store data in flash
- Writing to the wrong flash sector can cause MCU to stall
- Only 16KiB out of 128KiB in the sector is used to save RAM for compacting
2024-01-15 17:14:58 +08:00
linuswck 709eae8566 max5719: Slow down SPI clock
- setting it to 30MHz hang MCU core
2024-01-11 17:46:35 +08:00
linuswck d3e3451d37 Add README.md for flashing instructions 2024-01-11 17:17:38 +08:00
linuswck 33ff0c3678 main: Add demo fns reading all hardware status 2024-01-11 17:17:38 +08:00
linuswck 33d9cb45c4 ld_drive: Add soft current limit 2024-01-11 17:17:38 +08:00
linuswck 8ae27725c6 Port PID Controller from thermostat firmware
- Add serde Cargo
2024-01-11 17:17:10 +08:00
linuswck 20fc6d6fac rm photodiode folder and its mod.rs
- photodiode related fns are written inside laser diode folder
- as they are closely related
2024-01-10 17:51:27 +08:00
linuswck 654964831a boot: Change thermostat boot seq for new setup seq
- Power Up -> Calibrate -> Set the TEC current to some value
2024-01-10 10:38:05 +08:00
linuswck 27c7b5929e thermostat: change setup seq
- default power off
- with all params set to default val on hw
- rm calibration from setup
2024-01-10 10:38:05 +08:00
linuswck e48f1979f0 Clean up warnings 2024-01-10 10:38:05 +08:00
linuswck 59f2385ecf ld_drive: make settings private & add set & get fn 2024-01-10 10:38:05 +08:00
linuswck d4e074cbd4 ld_drive: rebase to the point that declare set_i 2024-01-10 10:38:05 +08:00
linuswck 277796d2a4 boot: Add ld_drive boot sequence 2024-01-10 10:38:05 +08:00
linuswck be8bf0a8b6 ld_drive: add setup fn 2024-01-10 10:38:05 +08:00
linuswck b1123047c7 ld_drive: Add fns to get pd current & cal ld pwr 2024-01-10 10:38:05 +08:00
linuswck 707fac0775 ld_drive: add a param for setting to cal pd pwr
- Conversion ratio between pd current and ld output power
2024-01-10 10:38:05 +08:00
linuswck a99dde8b38 current_source: add driver to read pd voltage 2024-01-10 10:38:05 +08:00
linuswck d1f7a20c15 ld_drive: Add fns to set ld output current 2024-01-10 10:38:05 +08:00
linuswck 58a8b15c40 ld_drive: add unit type and hw related constant 2024-01-10 10:38:05 +08:00
linuswck 590ba8171c Add high level ld_drive mod
- Use current_source driver to interface with the board
2024-01-10 10:38:05 +08:00
linuswck 0807f66b3c Add digital gpio related fns to Current source
- power_up
- power_down
- ld_short_enable
- ld_short_disable
    - Rev0_2 has hardware connection bug for LD_EN net.
    - laser driver power will always be enabled
2024-01-10 10:38:05 +08:00
linuswck 2953d4edde Rewrite current_source obj and add gpio init 2024-01-10 10:38:05 +08:00
linuswck f2c026ffdd Rewrite MAX5719 Driver into a separated file 2024-01-10 10:38:05 +08:00
linuswck c7d36bc8d5 Reorganize variables and fns structure 2024-01-10 10:38:00 +08:00
linuswck 784fc03957 cargo: add miniconf 2024-01-10 10:36:13 +08:00
linuswck 6cc2bc32c5 thermostat:reorganize fns structure, add setup seq 2024-01-10 10:36:09 +08:00
linuswck 23ee568ea7 Port TEC DAC calibration procedure from Thermostat
- Needs clean up
- To be evaluated and rethink the calibration procedure
2024-01-10 10:36:05 +08:00
linuswck d3f3608136 DacVFb is changed to PC3 for rev0_2 prototype
- This requires a circuit change to connect it to the PoE_PWR_SRC
    through a 1.6k Ohm and 100nF LPF
2023-12-21 16:39:09 +08:00
linuswck 4cf7b7fdf9 Rewrite MAX1968 Driver Generically
- Set variable to proper types(Rate, ElectricalPotential, etc)
- Declare const MAX_I_POS, MAX_I_NEG, MAX_V conversion
2023-12-21 13:13:06 +08:00
linuswck 0179e7641a Add MAX1968 Startup Sequence 2023-12-20 14:51:34 +08:00
linuswck f6767b147b Fix typo in MAX1968 driver power_down fn 2023-12-20 14:48:47 +08:00
linuswck 475fe28604 Initial Commit for AD5680 and MAX1968 Drivers
- GPIO Initializations for AD5680, MAX1968 drivers
- CTLI voltage of  MAX1968 can be set with AD5680
- All features of MAX1968 can be controlled
2023-12-20 12:08:48 +08:00
linuswck 764a203dd8 Update stm32-eth cargo crate to 0.15.2
- stm32-eth crate requires stm32f4xx-hal 0.14.0 and smoltcp 0.10.0
- its dependency packages requires rustc 1.67 or above (1.67.1 is chosen)
2023-12-15 16:40:55 +08:00
linuswck f2b419f8d0 Fix nix build to compilation error at Check Phase
- "panic_handler" should not be declared during test
- "main" and "std" are needed during test
2023-12-15 13:21:19 +08:00
66 changed files with 12942 additions and 924 deletions

View File

@ -1,12 +0,0 @@
[target.thumbv7em-none-eabihf]
runner = "gdb -batch -q -x bmp.gdb"
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[build]
# Pick ONE of these compilation targets
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)

8
.cargo/config.toml Normal file
View File

@ -0,0 +1,8 @@
[target.thumbv7em-none-eabihf]
runner = "gdb -q -x openocd.gdb"
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[build]
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
target/ target/
result result
.vscode* *.pyc
*.jdebug* *.jdebug*

60
BSD.md Normal file
View File

@ -0,0 +1,60 @@
# Building Rust embedded firmware on BSD
Using the Rust stable compiler from the BSD ports is mostly a matter of removing the arbitrary limitations that have been built into the Rust toolchain.
Get rid of the "nightly toolchain" check in cargo-xbuild:
```
diff --git a/src/lib.rs b/src/lib.rs
index 204ad8f..9c61e39 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -157,28 +157,11 @@ pub fn build(args: Args, command_name: &str, crate_config: Option<Config>) -> Re
})
})?;
- // We can't build sysroot with stable or beta due to unstable features
let sysroot = rustc::sysroot(verbose)?;
- let src = match meta.channel {
- Channel::Dev => rustc::Src::from_env().ok_or(anyhow!(
+ let src = rustc::Src::from_env().ok_or(anyhow!(
"The XARGO_RUST_SRC env variable must be set and point to the \
- Rust source directory when working with the 'dev' channel",
- ))?,
- Channel::Nightly => {
- if let Some(src) = rustc::Src::from_env() {
- src
- } else {
- sysroot.src()?
- }
- }
- Channel::Stable | Channel::Beta => {
- bail!(
- "The sysroot can't be built for the {:?} channel. \
- Switch to nightly.",
- meta.channel
- );
- }
- };
+ Rust source directory",
+ ))?;
let cmode = if let Some(triple) = args.target() {
if triple == meta.host {
```
Run ``cargo install --path .`` to install the modified cargo-xbuild
Get a copy of the Rust sources that corresponds to the installed compiler binary, and set up the environment accordingly:
```
> rustc --version
rustc 1.72.1 (d5c2e9c34 2023-09-13) (built from a source tarball)
> git clone --depth=1 --recurse-submodules --shallow-submodules https://github.com/rust-lang/rust --branch 1.72.1
> export XARGO_RUST_SRC=`pwd`/rust/library
```
Disable other arbitrary checks in the Rust compiler:
```
> export RUSTC_BOOTSTRAP=1
```
And you can now simply run ``cargo xbuild`` to build the firmware.

805
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,27 +15,30 @@ default-target = "thumbv7em-none-eabihf"
[dependencies] [dependencies]
panic-halt = "0.2.0" panic-halt = "0.2.0"
cortex-m = "0.7.6" cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
cortex-m-rt = { version = "0.7.1", features = ["device"] } cortex-m-rt = { version = "0.7.3", features = ["device"] }
cortex-m-semihosting = "0.5.0" cortex-m-semihosting = "0.5.0"
log = "0.4.17" log = "0.4.17"
bare-metal = "1" bare-metal = "1"
nb = "1" nb = "1"
cortex-m-log = { version = "0.7.0", features = ["log-integration", "semihosting"] } cortex-m-log = { version = "0.7.0", features = ["log-integration", "semihosting"] }
stm32f4xx-hal = { version = "0.13.2", features = ["rt", "stm32f407", "usb_fs", "rtic-monotonic"] } stm32f4xx-hal = { version = "0.20.0", features = ["stm32f407", "otg-fs", "usb_fs"] }
stm32-eth = { git = "https://github.com/stm32-rs/stm32-eth", features = ["stm32f407", "smoltcp-phy", "smoltcp"] } stm32-eth = { version = "0.6.0", features = ["stm32f407", "smoltcp-phy", "smoltcp"] }
smoltcp = { version = "0.8.1", default-features = false, features = ["proto-ipv4", "socket-tcp", "medium-ethernet"] } ieee802_3_miim = "0.8.0"
ieee802_3_miim = "0.7.2" smoltcp = { version = "0.11.0", default-features = false, features = ["proto-ipv4", "socket-tcp", "medium-ethernet", "iface-neighbor-cache-count-16"] }
uom = { version = "0.30", default-features = false, features = ["autoconvert", "si", "f32", "use_serde"] }
num-traits = { version = "0.2.15", default-features = false, features = ["libm"] } num-traits = { version = "0.2.15", default-features = false, features = ["libm"] }
usb-device = "0.2.9" usb-device = "0.3.2"
usbd-serial = "0.1.1" usbd-serial = "0.2.1"
fugit = "0.3.6" fugit = "0.3.6"
rtt-target = { version = "0.3.1", features = ["cortex-m"] } rtt-target = { version = "0.3.1", features = ["cortex-m"] }
cortex-m-rtic = "1.0.0" miniconf = "0.9.0"
systick-monotonic = "1.0.0" serde = { version = "1.0.158", features = ["derive"], default-features = false }
sfkv = "0.1"
bit_field = "0.10"
crc = "3.0.1"
byteorder = { version = "1", default-features = false }
[features] [features]
default = ["RTT"]
semihosting = ["cortex-m-log/semihosting"] semihosting = ["cortex-m-log/semihosting"]
RTT = [] RTT = []

85
README.md Normal file
View File

@ -0,0 +1,85 @@
# Testing Firmware for the Sinara 1550 Kirdy
- This repo is for testing only. Not intended for production use.
## Building
### Reproducible build with Nix
kirdy firmware is packaged using the [Nix](https://nixos.org) Flakes system. Install Nix 2.4+ and enable flakes by adding ``experimental-features = nix-command flakes`` to ``nix.conf`` (e.g. ``~/.config/nix/nix.conf``).
Once you have Flakes enabled, you can use ``nix build`` to build the firmware.
### Development environment
Clone this repository and with Nix Flakes enabled, use the following commands:
```shell
nix develop
cargo build
```
The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/kirdy`.
Alternatively, you can install the Rust toolchain without Nix using rustup; see the Rust manifest file pulled in `flake.nix` to determine which Rust version to use.
For building on FreeBSD or OpenBSD, see BSD.md.
## Debugging
Connect SWDIO/SWCLK/RST/GND to a programmer such as ST-Link v2.1. Run OpenOCD:
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
```
You may need to power up the programmer before powering the device.
Leave OpenOCD running. Run the GNU debugger:
```shell
gdb target/thumbv7em-none-eabihf/release/kirdy
(gdb) source openocd.gdb
```
## Flashing
If the firmware to be flashed involves an update on the flash settings, it is required to erase the flash settings before flashing the new firmware to avoid unexpected hardware behavior. There are several options for flashing kirdy. DFU requires only a USB-C cable or RJ45 cable, whereas OpenOCD needs a JTAG/SWD adapter.
### dfu-util on Linux
* Install the DFU USB tool (dfu-util).
* Convert firmware from ELF to BIN: `arm-none-eabi-objcopy -O binary kirdy kirdy.bin` (you can skip this step if using the BIN from Hydra)
* Put STM32 into DFU Mode. You can either
* Connect to the USB Type C cable to kirdy next to the RJ45 Jack. After that, add BOOT0 jumper to kirdy near programming headers and then cycle board power to put it in DFU mode. OR
* Plug in RJ45 cable, which connect to a network that is accessible by your computer and send the corresponding dfu json command via TCP Socket to kirdy. Please see the python test script for the command.
* Push firmware to flash: `dfu-util -a 0 -s 0x08000000:leave -D kirdy.bin`
* If you plugged in the BOOT0 jumper, you will need to
1. Remove BOOT0 jumper
2. Cycle power to leave DFU update mode
* If you plugged in the RJ45 cable, the MCU would start its application code automatically. No power cycle is needed.
### st.com DfuSe tool on Windows
On a Windows machine install [st.com](https://st.com) DfuSe USB device firmware upgrade (DFU) software. [link](https://www.st.com/en/development-tools/stsw-stm32080.html).
- add jumper to kirdy across 2-pin jumper adjacent to JTAG connector
- cycle board power to put it in DFU update mode
- connect USB Type C to PC
- use st.com software to upload firmware
- remove jumper
- cycle power to leave DFU update mode
### OpenOCD
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/debug/kirdy verify reset; exit"
```
## Erasing Flash Settings
The flash settings are stored in the last flash sector(ID: 11) of bank 0 of stm32f405. You can erase it with JTAG/SWD adapter or by putting the device in Dfu mode. You may find it useful if you have set network settings incorrectly.
With JTAG/SWD adapter connected, issue the following command:
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "flash init; init; halt; flash erase_sector 0 11 last; reset; exit"
```
With STM32 in DFU Mode, connect the USB Type C cable and then issue the following command:
```
dfu-util -a 0 -s 0x080E0000:leave -D erase_flash_settings.bin
```

View File

@ -1,6 +0,0 @@
target extended-remote /dev/ttyBmpGdb
monitor swdp_scan
attach 1
load
compare-sections
kill

View File

@ -1,7 +1,4 @@
use std::env; use std::{env, fs::File, io::Write, path::PathBuf};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() { fn main() {
// Put the linker script somewhere the linker can find it // Put the linker script somewhere the linker can find it

56
docs/thermostat.md Normal file
View File

@ -0,0 +1,56 @@
# Thermostat
Kirdy incorporates a single channel thermostat circuitry like Sinara-hw [Thermostat](https://github.com/sinara-hw/Thermostat) with improved noise. It can deliver more than 9W of cooling/heating power with better than +-1mK temperature stability.
## Power Input
If you use Kirdy with a PoE (IEEE 802.3af Class 3) PSE, Kirdy may experience power failure due to power limit. If you need more power on Thermostat, you can plug in additional Power supply on the 12V power Jack(Recommend: 12V 2A). Although the TEC Driver has a very high 165 degree thermal shutdown threshold, it is highly recommended to provide active cooling to the TEC Driving Circuitry.
## Control Mode
Like Sinara-hw [Thermostat](https://github.com/sinara-hw/Thermostat), they both share the same control mode and algorithm, namely constant Current Control Mode
### Constant Current Control Mode
You can drive a constant current to the load. The end user to connect an external control loop. If end user wants to use it with an external control loop, it is recommended to retrieve reports via the ActiveReportMode instead of polling status report for better consistency and higher maximum update rate.
### PID Control Mode
Thermostat uses the [PID algorithm](https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw) derived by Quartiq. The included Pid Autotune Algorithm can already achieve the specified +-1mK stability.
During autotuning, Theremstat drives a user-specified step current into the load to calculate the pid parameters. Improper usage can damage the connected device.
#### Important Notes
1. Configure all the protection mechanisms correctly before running the pid autotune algorithm.
2. Supervise the PID autotune process.
3. PID Autotune algorithm sometimes can produce unstable PID parameters. Do evaluate the PID performance after autotune is completed
4. For best performance, you should do the autotune with Kirdy installed in the environment that it is gonna be used.
5. If you are using the GUI, you need to set a high enough polling rate for PID autotune to function correctly.
#### Usage Guide
You can run pid autotune via the standalone auto
1. Target Temperature
- Set it to the intended operating temperature for the connected device to operate in. You can still change temperature setpoint afterwards.
2. Test Current
- Set it to the maximum recommended operating current specified on the TEC/Resistive load.
3. Update Rate
- This can be set by configuring the filter settings in the Temperature ADC. High Update Rate can yield faster loop response with temperature regulation noise as the trade-off. Higher update rate can be useful if the load is actively cooled.
4. Temperature Swing
- Lower value can damp the control loop response.
5. Lookback Period
- Larger value can damp the control loop response. Do note that the larger lookback period also increases the autotune completion time.
You can give your system a try with the following parameters.
1. Target Temperature: (User-Specified)
2. Test Current: (User-Specified)
3. Update Rate: 16.67Hz (Sinc5Sinc1 Filter with 50Hz and 60Hz Rejection)
4. Temperature Swing: 0 Degree
5. Lookback Period: 5s
Once autotune is successful, you can improve its stability by increasing the lookback period.
#### Troubleshooting
If you obtain unstable PID parameters,
1. Check if the polling rate is close to the PID update rate. If not, increase the polling rate
2. Increase the lookback period and try again.
If the autotune does not finish running or faikls, you should
1. Check if the thermostat has a hard time heating or cooling the connected device to the targe temperature. If yes, you should consider adjust the target temperature accordingly.
2. Check if the polling rate is close to the PID update rate. If not, increase the polling rate.

BIN
erase_flash_settings.bin Normal file

Binary file not shown.

View File

@ -3,11 +3,11 @@
"mozilla-overlay": { "mozilla-overlay": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1664789696, "lastModified": 1704373101,
"narHash": "sha256-UGWJHQShiwLCr4/DysMVFrYdYYHcOqAOVsWNUu+l6YU=", "narHash": "sha256-+gi59LRWRQmwROrmE1E2b3mtocwueCQqZ60CwLG+gbg=",
"owner": "mozilla", "owner": "mozilla",
"repo": "nixpkgs-mozilla", "repo": "nixpkgs-mozilla",
"rev": "80627b282705101e7b38e19ca6e8df105031b072", "rev": "9b11a87c0cc54e308fa83aac5b4ee1816d5418a2",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,16 +18,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1666164185, "lastModified": 1713725259,
"narHash": "sha256-5v+YB4ijeUfg5LCz9ck4gIpCPhIS+qn02OyPJO48bCE=", "narHash": "sha256-9ZR/Rbx5/Z/JZf5ehVNMoz/s5xjpP0a22tL6qNvLt5E=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c5203abb1329f7ea084c04acda330ca75d5b9fb5", "rev": "a5e4bbcb4780c63c79c87d29ea409abf097de3f7",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-22.05", "ref": "nixos-23.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@ -1,15 +1,15 @@
{ {
description = "Firmware for kirdy"; description = "Firmware for kirdy";
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-22.05; inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-23.11;
inputs.mozilla-overlay = { url = github:mozilla/nixpkgs-mozilla; flake = false; }; inputs.mozilla-overlay = { url = github:mozilla/nixpkgs-mozilla; flake = false; };
outputs = { self, nixpkgs, mozilla-overlay }: outputs = { self, nixpkgs, mozilla-overlay }:
let let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import mozilla-overlay) ]; }; pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import mozilla-overlay) ]; };
rustManifest = pkgs.fetchurl { rustManifest = pkgs.fetchurl {
url = "https://static.rust-lang.org/dist/2022-09-22/channel-rust-stable.toml"; url = "https://static.rust-lang.org/dist/2024-03-21/channel-rust-nightly.toml";
sha256 = "f257a7de2f284f025238964ca2c1865c704be4156e1909d6393d92b796005055"; sha256 = "1c7db6ab80d20682b5cc5bda7360c63311d7188c0c082902d3790820527cd4e0";
}; };
targets = [ targets = [
@ -22,7 +22,7 @@
inherit targets; inherit targets;
extensions = ["rust-src"]; extensions = ["rust-src"];
}; };
rust = rustChannelOfTargets "stable" null targets; rust = rustChannelOfTargets "nightly" null targets;
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform { rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
rustc = rust; rustc = rust;
cargo = rust; cargo = rust;
@ -51,6 +51,34 @@
''; '';
dontFixup = true; dontFixup = true;
auditable = false;
};
pglive = pkgs.python3Packages.buildPythonPackage rec {
pname = "pglive";
version = "0.7.2";
format = "pyproject";
src = pkgs.fetchPypi {
inherit pname version;
hash = "sha256-jqj8X6H1N5mJQ4OrY5ANqRB0YJByqg/bNneEALWmH1A=";
};
buildInputs = [ pkgs.python3Packages.poetry-core ];
propagatedBuildInputs = with pkgs.python3Packages; [ pyqtgraph numpy ];
};
kirdy_gui = pkgs.python3Packages.buildPythonPackage {
pname = "kirdy_gui";
version = "0.0.0";
format = "pyproject";
src = "${self}/pykirdy";
nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ];
propagatedBuildInputs = [ pkgs.qt6.qtbase ] ++ (with pkgs.python3Packages; [ pyqtgraph pyqt6 qasync pglive aenum sipyco]);
dontWrapQtApps = true;
postFixup = ''
wrapQtApp "$out/bin/tec_qt"
'';
}; };
in { in {
packages.x86_64-linux = { packages.x86_64-linux = {
@ -64,16 +92,14 @@
devShell.x86_64-linux = pkgs.mkShell { devShell.x86_64-linux = pkgs.mkShell {
name = "kirdy-dev-shell"; name = "kirdy-dev-shell";
buildInputs = with pkgs; [ buildInputs = with pkgs; [
rustPlatform.rust.rustc rust openocd dfu-util glibc
rustPlatform.rust.cargo
openocd dfu-util glibc picocom gdb
] ++ (with python3Packages; [ ] ++ (with python3Packages; [
numpy matplotlib pyqtgraph numpy matplotlib pyqtgraph setuptools pyqt6 qasync pglive aenum sipyco
]); ]);
shellHook= shellHook=
'' ''
export QT_PLUGIN_PATH=${pkgs.qt5.qtbase}/${pkgs.qt5.qtbase.dev.qtPluginPrefix} export QT_PLUGIN_PATH=${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.dev.qtPluginPrefix}
export QML2_IMPORT_PATH=${pkgs.qt5.qtbase}/${pkgs.qt5.qtbase.dev.qtQmlPrefix} export QML2_IMPORT_PATH=${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.dev.qtQmlPrefix}
''; '';
}; };
defaultPackage.x86_64-linux = kirdy; defaultPackage.x86_64-linux = kirdy;

View File

@ -2,9 +2,8 @@
MEMORY MEMORY
{ {
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K /* The last flash sector is reserved for config */
/* reserved for config data */ FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 896K
CONFIG (rx) : ORIGIN = 0x8100000, LENGTH = 16K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K - 4 RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K - 4
/* reserved for DFU trigger message */ /* reserved for DFU trigger message */
DFU_MSG (wrx) : ORIGIN = 0x2001BFFC, LENGTH = 4 DFU_MSG (wrx) : ORIGIN = 0x2001BFFC, LENGTH = 4
@ -14,6 +13,5 @@ MEMORY
} }
_flash_start = ORIGIN(FLASH); _flash_start = ORIGIN(FLASH);
_config_start = ORIGIN(CONFIG);
_dfu_msg = ORIGIN(DFU_MSG); _dfu_msg = ORIGIN(DFU_MSG);
_stack_start = ORIGIN(CCMRAM) + LENGTH(CCMRAM); _stack_start = ORIGIN(CCMRAM) + LENGTH(CCMRAM);

137
pykirdy/asyncio_exmaple.py Normal file
View File

@ -0,0 +1,137 @@
from pprint import pp
from driver.kirdy import Kirdy, FilterConfig
import signal
import time
import asyncio
async def enter_dfu_mode(kirdy: Kirdy):
"""
Enter Device Firmware Upgrade(Dfu) mode
Please refer to README.md for firmware update instructions.
"""
await kirdy.device.dfu()
async def active_report(kirdy: Kirdy):
"""
Configure Kirdy to actively report status to connected socket
Press Ctrl + C to exit active report mode
"""
async for data in kirdy.report_mode():
pp(data)
async def device_cfg(kirdy: Kirdy):
"""
Configure Kirdy's network and board specific transconductance settings.
These configs are saved to flash immediately after command is processed.
"""
# Kirdy rev0_3's gain and transconductance varies between boards to maximize the
# PD current range resolution.
await kirdy.device.set_pd_mon_fin_gain(1.0)
await kirdy.device.set_pd_mon_transconductance(1/1000.0)
# Network Settings will be updated on next reboot.
await kirdy.device.set_ip_settings(
addr="192.168.1.128",
port=1337,
prefix_len=24,
gateway="192.168.1.1"
)
# Hard reset Kirdy.
await kirdy.device.hard_reset()
async def ld_thermostat_cfg(kirdy: Kirdy):
"""
Control and config laser diode and thermostat parameters.
"""
# Load the laser diode and thermostat settings from flash memory.
await kirdy.device.restore_settings_from_flash()
# Power off the laser diode & thermostat and clear any asserted alarm
await kirdy.laser.set_power_on(False)
await kirdy.laser.clear_alarm()
await kirdy.thermostat.set_power_on(False)
await kirdy.thermostat.clear_alarm()
# Set the laser diode terminals not to be shorted
await kirdy.laser.set_ld_terms_short(False)
# Do not power up the laser & thermostat during initial startup
await kirdy.laser.set_default_pwr_on(False)
await kirdy.thermostat.set_default_pwr_on(False)
await kirdy.laser.set_i(0)
# Configure the laser diode output power limit and photodiode parameters
# Exceeding the power limit triggers overpower protection alarm.
# The laser diode power will be cut off upon alarm assertion while the thermostat power remains unchanged.
await kirdy.laser.set_ld_pwr_limit(0.0)
await kirdy.laser.set_pd_mon_dark_current(0.0)
await kirdy.laser.set_pd_mon_responsitivity(0.0)
# Configure the thermostat NTC thermistor parameters.
await kirdy.thermostat.set_sh_r0(10.0 * 1000)
await kirdy.thermostat.set_sh_t0(25)
await kirdy.thermostat.set_sh_beta(3900)
# Configure the thermostat TEC settings.
# The actual output current is limited by value set below.
await kirdy.thermostat.set_tec_max_cooling_i(1.0)
await kirdy.thermostat.set_tec_max_heating_i(1.0)
await kirdy.thermostat.set_tec_max_v(4.0)
# Configure the thermostat temperature monitor limits.
# Exceeding the temperature limits trigger over temperature protection alarm.
# Both laser diode and thermostat power will be cut off upon alarm assertion.
await kirdy.thermostat.set_temp_mon_upper_limit(70)
await kirdy.thermostat.set_temp_mon_lower_limit(0)
# Configure the thermostat ADC Filter Setting / PID Update Rate / Report Rate.
# The ADC sampling rate determines the report and pid update rate.
# The chosen filter and sampling rate affects the noise of the readings.
# For details, please refer to the AD7172 datasheet.
await kirdy.thermostat.config_temp_adc_filter(FilterConfig.Sinc5Sinc1With50hz60HzRejection.f16sps)
# Configure the thermostat PID parameters.
# You can configure the PID parameter by the included autotune script.
# You should configure the filter sampling rate first before applying pid parameters.
await kirdy.thermostat.set_temperature_setpoint(25)
await kirdy.thermostat.set_pid_kp(0.15668282198105507)
await kirdy.thermostat.set_pid_ki(0.0001281321)
await kirdy.thermostat.set_pid_kd(13.82367)
await kirdy.thermostat.set_pid_output_max(1.0)
await kirdy.thermostat.set_pid_output_min(-1.0)
# Configure thermostat to run in PID control mode
await kirdy.thermostat.set_pid_control_mode()
# When control mode is switched from PID to constant current(CC) control mode,
# thermostat keeps its instantaneous output current unchanged.
# Thermostat output current should only be set if it is in CC control mode
# or the value set will be overwritten by PID output.
await kirdy.thermostat.set_constant_current_control_mode()
await kirdy.thermostat.set_tec_i_out(0.0)
# Save the current settings to flash memory
await kirdy.device.save_current_settings_to_flash()
# Power on the laser diode and thermostat
await kirdy.laser.set_power_on(True)
await kirdy.thermostat.set_power_on(True)
pp(await kirdy.device.get_settings_summary())
pp(await kirdy.device.get_status_report())
async def main():
kirdy = Kirdy()
kirdy.start_session(host='192.168.1.128', port=1337)
await kirdy.wait_until_connected()
await ld_thermostat_cfg(kirdy)
# await active_report(kirdy)
# await device_cfg(kirdy)
# await enter_dfu_mode(kirdy)
await kirdy.end_session()
if __name__ == "__main__":
asyncio.run(main())

981
pykirdy/driver/kirdy.py Normal file
View File

@ -0,0 +1,981 @@
import types
import socket
import json
import logging
import time
from threading import Thread
from aenum import StrEnum, NoAlias
import queue
import asyncio
class _dt(StrEnum):
ip_settings = "ip_settings"
temp_adc_filter = "temp_adc_filter"
f32 = "data_f32"
bool = "data_bool"
none = "None"
class State(StrEnum):
disconnected = "disconnected"
connected = "connected"
class CmdList:
class device(StrEnum, settings=NoAlias):
_target = "device_cmd"
SetIPSettings = _dt.ip_settings
SetPdFinGain = _dt.f32
SetPdTransconductance = _dt.f32
SetActiveReportMode = _dt.bool
GetHwRev = _dt.none
GetStatusReport = _dt.none
GetSettingsSummary = _dt.none
Dfu = _dt.none
SaveFlashSettings = _dt.none
LoadFlashSettings = _dt.none
HardReset = _dt.none
class ld(StrEnum, settings=NoAlias):
_target = "laser_diode_cmd"
SetDefaultPowerOn = _dt.bool
PowerUp = _dt.none
PowerDown = _dt.none
LdTermsShort = _dt.none
LdTermsOpen = _dt.none
SetI = _dt.f32
SetPdResponsitivity = _dt.f32
SetPdDarkCurrent = _dt.f32
ApplyPdParams = _dt.none
SetLdPwrLimit = _dt.f32
ClearAlarm = _dt.none
class thermostat(StrEnum, settings=NoAlias):
_target = "thermostat_cmd"
SetDefaultPowerOn = _dt.bool,
PowerUp = _dt.f32,
PowerDown = _dt.f32,
SetTecMaxV = _dt.f32,
SetTecMaxIPos = _dt.f32,
SetTecMaxINeg = _dt.f32,
SetTecIOut = _dt.f32,
SetTemperatureSetpoint = _dt.f32,
SetPidEngage = _dt.none,
SetPidDisEngage = _dt.none,
SetPidKp = _dt.f32,
SetPidKi = _dt.f32,
SetPidKd = _dt.f32,
SetPidOutMin = _dt.f32,
SetPidOutMax = _dt.f32,
ConfigTempAdcFilter = _dt.temp_adc_filter,
GetPollInterval = _dt.none,
SetTempMonUpperLimit = _dt.f32,
SetTempMonLowerLimit = _dt.f32,
ClearAlarm = _dt.none,
SetShT0 = _dt.f32,
SetShR0 = _dt.f32,
SetShBeta = _dt.f32,
class FilterConfig:
class Sinc5Sinc1With50hz60HzRejection(StrEnum):
f27sps = "F27SPS"
f21sps = "F21SPS"
f20sps = "F20SPS"
f16sps = "F16SPS"
_odr_type = "sinc5sinc1postfilter"
def _filter_type(self):
return "Sinc5Sinc1With50hz60HzRejection"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc5Sinc1(StrEnum):
f31250_0sps = "F31250_0SPS"
f15625_0sps = "F15625_0SPS"
f10417_0sps = "F10417_0SPS"
f5208_0sps = "F5208_0SPS"
f2597_0sps = "F2597_0SPS"
f1007_0sps = "F1007_0SPS"
f503_8sps = "F503_8SPS"
f381_0sps = "F381_0SPS"
f200_3sps = "F200_3SPS"
f100_2sps = "F100_2SPS"
f59_52sps = "F59_52SPS"
f49_68sps = "F49_68SPS"
f20_01sps = "F20_01SPS"
f16_63sps = "F16_63SPS"
f10_0sps = "F10_0SPS"
f5_0sps = "F5_0SPS"
f2_5sps = "F2_5SPS"
f1_25sps = "F1_25SPS"
_odr_type = "sinc5sinc1odr"
def _filter_type(self):
return "Sinc5Sinc1"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc3(StrEnum):
f31250_0sps = "F31250_0SPS"
f15625_0sps = "F15625_0SPS"
f10417_0sps = "F10417_0SPS"
f5208_0sps = "F5208_0SPS"
f2597_0sps = "F2597_0SPS"
f1007_0sps = "F1007_0SPS"
f503_8sps = "F503_8SPS"
f381_0sps = "F381_0SPS"
f200_3sps = "F200_3SPS"
f100_2sps = "F100_2SPS"
f59_52sps = "F59_52SPS"
f49_68sps = "F49_68SPS"
f20_01sps = "F20_01SPS"
f16_63sps = "F16_63SPS"
f10_0sps = "F10_0SPS"
f5_0sps = "F5_0SPS"
f2_5sps = "F2_5SPS"
f1_25sps = "F1_25SPS"
_odr_type = "sinc3odr"
def _filter_type(self):
return "Sinc3"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc3WithFineODR():
upper_limit = 31250
lower_limit = 1.907465
_odr_type = "sinc3fineodr"
def __init__(self, rate):
assert rate >= self.lower_limit and rate <= self.upper_limit
self.rate = float(rate)
def _filter_type(self):
return "Sinc3WithFineODR"
class InvalidDataType(Exception):
pass
class InvalidCmd(Exception):
pass
class Device:
def __init__(self, send_cmd_handler, send_raw_cmd_handler):
self._cmd = CmdList.device
self._send_cmd = send_cmd_handler
self._send_raw_cmd = send_raw_cmd_handler
async def set_ip_settings(self, addr="192.168.1.128", port=1337, prefix_len=24, gateway="192.168.1.1"):
"""
Upon command execution, the ip settings are saved into flash and are effective upon next reboot.
"""
try:
socket.inet_aton(addr)
socket.inet_aton(gateway)
except OSError:
raise InvalidDataType
addr = addr.split(".")
gateway = gateway.split(".")
if not(isinstance(port, int) and isinstance(prefix_len, int)):
raise InvalidDataType
return await self._send_raw_cmd(
{
"device_cmd": "SetIPSettings",
"ip_settings": {
"addr": [int(x) for x in addr],
"port": port,
"prefix_len": prefix_len,
"gateway": [int(x) for x in gateway],
}
}
)
async def set_active_report_mode(self, on):
"""
Set active report to be on. If it is on, Kirdy will send status report
to the client socket according to the temperature polling rate set.
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetActiveReportMode, on)
async def set_pd_mon_fin_gain(self, gain):
"""
Configure the photodiode monitor final analog front-end stage gain.
- gain: unitless
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPdFinGain, gain)
async def set_pd_mon_transconductance(self, transconductance):
"""
Configure the photodiode monitor transconductance value.
- transconductance: 1/Ohm
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPdTransconductance, transconductance)
async def get_hw_rev(self):
"""
Get hardware revision of the connected Kirdy
{
'msg_type': 'HwRev',
'hw_rev': {
'major': 0,
'minor': 3
}
}
"""
return await self._send_cmd(self._cmd._target, self._cmd.GetHwRev, msg_type="HwRev")
async def get_status_report(self, sig=None):
"""
Get status of all peripherals in a json object.
{
'ts': 227657, # Relative Timestamp (ms)
'msg_type': 'Report' # Indicate it is a 'Report' json object
'laser': {
'pwr_on': False, # Laser Power is On (True/False)
'pwr_excursion': False, # Was Laser experienced an Overpowered Condition? (True/False)
'ld_i_set': 0.0, # Laser Diode Output Current (A)
'pd_i': 2.0000002e-06, # Internal Photodiode Monitor current (A)
'pd_pwr': None, # Power Readings from Internal Photodiode (W). Return None if pd_mon parameter(s) are not defined.
'term_50ohm': 'On' # Is the Low Frequency Modulation Input's Impedance 50 Ohm? ("On"/"Off")
},
'thermostat': {
'pwr_on': False, # Tec Power is On (True/False)
'pid_engaged': False, # Is Pid_Engaged. If False, it is in Constant Current Mode (True/False)
'temp_mon_status': { # Temperature Monitor:
'status': 'Off', # "Off": Power is Off
# "ConstantCurrentMode": Thermostat is regulated in CC mode
# "PidStartUp": PID Regulation is not stable
# "PidStable": PID Regulation is stable and the temperature is within +-1mK to the setpoint
# "OverTempAlarm": Overtemperature Alarm is triggered
'over_temp_alarm': False # Was Laser Diode experienced an Overtemperature condition (True/False)
},
'temperature': 25.03344, # Temperature Readings (Degree Celsius)
'i_set': 0.0, # Tec Current Set by User/PID Controller(A)
'tec_i': 0.0024998188, # Tec Current Readings (A)
'tec_v': -0.00399971 # Tec Voltage Readings (V)
}
}
"""
return await self._send_cmd(self._cmd._target, self._cmd.GetStatusReport, msg_type="Report", sig=sig)
async def get_settings_summary(self, sig=None):
"""
Get the current settings of laser and thermostat in a json object.
{
'msg_type': 'Settings', # Indicate it is a 'Settings' json object
'laser': {
'default_pwr_on': False, # Power On Laser Diode at Startup
'ld_drive_current': { # Laser Diode Output Current (A)
'value': 0.0, # Value Set
'max': 0.3 # Max Value Settable
},
'pd_mon_params': { # Photodiode Parameters
'transconductance': 0.000115258765 # Board Specific Transconductance (1/ohm)
'responsitivity': 0.0141, # Responsitivity (A/W)
'i_dark': 0.0 # Max Value Settable (A)
},
'ld_pwr_limit': { # Laser Diode Power Limit (W)
'value': 0.00975, # Value Set
'max': 0.023321507 # Max Value Settable
},
'ld_terms_short: False, # Is Laser Diode Terminals short? (True/False)
},
'thermostat': {
'default_pwr_on': True, # Power on Thermostat at Startup
'pid_engaged': True, # True: PID Control Mode | False Constant Current Mode
'temperature_setpoint': 25.0, # Temperature Setpoint (Degree Celsius)
'tec_settings': {
'i_set': { # Current TEC Current Set by PID Controller/User (A)
'value': 0.04330516, # Value Set
'max': 3.0 # Max Value Settable
},
'max_v': { # Max Voltage Across Tec Terminals (V)
'value': 4.00000000, # Value Set
'max': 4.3 # Max Value Settable
},
'max_i_pos': { # Max Cooling Current Across Tec Terminals (A)
'value': 0.99628574, # Value Set
'max': 3.0 # Max Value Settable
},
'max_i_neg': { # Max Heating Current Across Tec Terminals (A)
'value': 0.99628574, # Value Set
'max': 3.0 # Max Value Settable
}
},
'pid_params': { # PID Controller Parameters
'kp': 0.15668282, # Proportional Gain
'ki': 0.0021359625, # Integral Gain
'kd': 0.8292545, # Derivative Gain
'output_min': -1.0, # Minimum Current Output (A)
'output_max': 1.0 # Maximum Current Output (A)
},
'temp_adc_settings': { # Temperature ADC Settings (Please read AD7172-2 Documentation)
'filter_type': 'Sinc5Sinc1With50hz60HzRejection', # Filter Types
'sinc5sinc1odr': None, # (Unused)
'sinc3odr': None, # (Unused)
'sinc5sinc1postfilter': None, # (Unused)
'sinc3fineodr': None, # (Unused)
'rate': 16.67 # ADC Sampling Rate (Hz)
},
'temp_mon_settings': { # Temperature Monitor Settings
'upper_limit': 40.0, # Temperature Upper Limit (Degree Celsius)
'lower_limit': 10.0 # Temperature Lower Limit (Degree Celsius)
},
'thermistor_params': { # Thermistor Steinhart-Hart equation parameters
't0': 25.0, # t0: Degree Celsius
'r0': 10000.0, # r0: Ohm
'b': 3900.0 # b: (unitless)
}
}
}
"""
return await self._send_cmd(self._cmd._target, self._cmd.GetSettingsSummary, msg_type="Settings", sig=sig)
async def dfu(self):
"""
Hard reset and put the connected Kirdy into the Dfu mode for firmware update.
"""
return await self._send_cmd(self._cmd._target, self._cmd.Dfu)
async def save_current_settings_to_flash(self):
"""
Save the current laser diode and thermostat configurations into flash.
"""
return await self._send_cmd(self._cmd._target, self._cmd.SaveFlashSettings)
async def restore_settings_from_flash(self):
"""
Restore the laser diode and thermostat settings from flash.
"""
return await self._send_cmd(self._cmd._target, self._cmd.LoadFlashSettings)
async def hard_reset(self):
"""
Hard Reset Kirdy. The socket connection will be closed by Kirdy.
Laser diode power and Tec power will be turned off.
Kirdy will send out a json({'msg_type': 'HardReset'}) to all sockets before hard reset take place.
"""
return await self._send_cmd(self._cmd._target, self._cmd.HardReset)
class Laser:
def __init__(self, send_cmd_handler):
self._cmd = CmdList.ld
self._send_cmd = send_cmd_handler
async def set_power_on(self, on):
"""
Power Up or Power Down laser diode. Powering up the Laser Diode resets the pwr_excursion status
- on (True/False)
"""
if on:
return await self._send_cmd(self._cmd._target, self._cmd.PowerUp, None)
else:
return await self._send_cmd(self._cmd._target, self._cmd.PowerDown, None)
async def set_default_pwr_on(self, on):
"""
Set whether laser diode is powered up at Startup.
- on (True/False)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetDefaultPowerOn, on)
async def set_ld_terms_short(self, short):
"""
Open/Short laser diode terminals.
- on (True/False)
"""
if short:
return await self._send_cmd(self._cmd._target, self._cmd.LdTermsShort, None)
else:
return await self._send_cmd(self._cmd._target, self._cmd.LdTermsOpen, None)
async def set_i(self, i):
"""
Set laser diode output current: Max(0, Min(i_set, 300mA)).
- i: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetI, i)
async def set_pd_mon_responsitivity(self, responsitivity):
"""
Configure the photodiode monitor responsitivity parameter.
The value is only effective if ApplyPdParams cmd is issued.
- responsitivity: A/W
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPdResponsitivity, responsitivity)
async def set_pd_mon_dark_current(self, dark_current):
"""
Configure the photodiode monitor dark current parameter.
The value is only effective if ApplyPdParams cmd is issued.
- dark_current: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPdDarkCurrent, dark_current)
async def apply_pd_params(self):
"""
Evaluate and apply photodiode monitor parameters that are set with SetPdDarkCurrent and SetPdResponsitivity cmd.
After Kirdy receives the cmd, it will check if the current power limit is within the newly calculated
power limit range. If it is out of range, the photodiode monitor parameters remains unchanged and Kirdy
sends out a "InvalidSettings" message along with an error message.
"""
return await self._send_cmd(self._cmd._target, self._cmd.ApplyPdParams)
async def set_ld_pwr_limit(self, pwr_limit):
"""
Set the power limit for the power excursion monitor.
If the power limit settings is out of range, power limit remains unchanged and Kirdy
sends out a "InvalidSettings" message along with an error message.
If the calculated power with the params of pd_mon > pwr_limit,
overpower protection is triggered.
- pwr_limit: W
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetLdPwrLimit, pwr_limit)
async def clear_alarm(self):
"""
Clear the power excursion monitor alarm.
"""
return await self._send_cmd(self._cmd._target, self._cmd.ClearAlarm)
class Thermostat:
def __init__(self, send_cmd_handler, send_raw_cmd_handler):
self._cmd = CmdList.thermostat
self._send_cmd = send_cmd_handler
self._send_raw_cmd = send_raw_cmd_handler
async def set_power_on(self, on):
"""
Power up or power down thermostat.
- Powering up the thermostat resets the pwr_excursion status
"""
if on:
return await self._send_cmd(self._cmd._target, self._cmd.PowerUp, None)
else:
return await self._send_cmd(self._cmd._target, self._cmd.PowerDown, None)
async def set_default_pwr_on(self, on):
"""
Set whether thermostat is powered up at Startup.
- on: (True/False)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetDefaultPowerOn, on)
async def set_tec_max_v(self, max_v):
"""
Set Tec Maximum Voltage Across the TEC Terminals.
- max_v: V
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxV, max_v)
async def set_tec_max_cooling_i(self, max_i_pos):
"""
Set Tec maximum cooling current (Settable Range: 0.0 - 3.0)
- max_i_pos: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxIPos, max_i_pos)
async def set_tec_max_heating_i(self, max_i_neg):
"""
Set Tec maximum heating current (Settable Range: 0.0 - 3.0)
- max_i_neg: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxINeg, max_i_neg)
async def set_tec_i_out(self, i_out):
"""
Set Tec Output Current (Settable Range: 0.0 - 3.0)
This cmd is only effective in constant current control mode
or your newly set value will be overwritten by PID Controller Output
- i_out: A
"""
if isinstance(i_out, float):
return await self._send_raw_cmd({"tec_set_i": i_out})
elif isinstance(i_out, int):
return await self._send_raw_cmd({"tec_set_i": float(i_out)})
else:
raise InvalidDataType
async def set_constant_current_control_mode(self):
"""
Disable PID Controller and output current can be controlled with set_tec_i_out() cmd.
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidDisEngage, None)
async def set_temperature_setpoint(self, temperature):
"""
Set Temperature Setpoint for PID Controller. This parameter is not active in constant current control mode
- temperature: Degree Celsius
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTemperatureSetpoint, temperature)
async def set_pid_control_mode(self):
"""
Enable PID Controller. Its PID Update Interval is controlled by the Temperature ADC polling rate.
Please refer to config_temp_adc_filter for the possible polling rate options
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidEngage, None)
async def set_pid_kp(self, kp):
"""
Set Kp parameter for PID Controller
kp: (unitless)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidKp, kp)
async def set_pid_ki(self, ki):
"""
Set Ki parameter for PID Controller
ki: (unitless)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidKi, ki)
async def set_pid_kd(self, kd):
"""
Set Kd parameter for PID Controller
kd: (unitless)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidKd, kd)
async def set_pid_output_max(self, out_max):
"""
Set max output limit at the PID Output
- out_max: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidOutMax, out_max)
async def set_pid_output_min(self, out_min):
"""
Set min output limit at the PID Output
- out_min: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidOutMin, out_min)
async def set_temp_mon_upper_limit(self, upper_limit):
"""
Set Temperature Monitor Upper Limit Threshold. Exceeding the limit for too long
will force the TEC Controller, PID Controller and Laser Diode Power to Shutdown
- upper_limit: Degree Celsius
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTempMonUpperLimit, upper_limit)
async def set_temp_mon_lower_limit(self, lower_limit):
"""
Set Temperature Monitor Lower Limit Threshold. Exceeding the limit for too long
will force the TEC Controller, PID Controller and Laser Diode Power to Shutdown
- lower_limit: Degree Celsius
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTempMonLowerLimit, lower_limit)
async def clear_alarm(self):
"""
Clear the temperature monitor alarm
"""
return await self._send_cmd(self._cmd._target, self._cmd.ClearAlarm)
async def set_sh_t0(self, t0):
"""
Set t0 Steinhart-Hart parameter for the laser diode NTC
- t0: Degree Celsius
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetShT0, t0)
async def set_sh_r0(self, r0):
"""
Set r0 Steinhart-Hart parameter for the laser diode NTC
- r0: Ohm
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetShR0, r0)
async def set_sh_beta(self, beta):
"""
Set beta Steinhart-Hart parameter for the laser diode NTC
- beta: (unitless)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetShBeta, beta)
async def config_temp_adc_filter(self, filter_config):
"""
Configure the temperature adc filter type and sampling rate.
Please refer to AD7172 datasheet for the usage of various types of filter.
The actual temperature polling rate is bottlenecked by the processing speed of the MCU and
performs differently under different kinds of workload. Please verify the polling rate with
the timestamp.
"""
cmd = {}
cmd[self._cmd._target] = self._cmd.ConfigTempAdcFilter.name
if hasattr(filter_config, 'rate'):
cmd[self._cmd.ConfigTempAdcFilter] = {
"filter_type": filter_config._filter_type(),
filter_config._odr_type: filter_config.rate,
}
else:
cmd[self._cmd.ConfigTempAdcFilter] = {
"filter_type": filter_config._filter_type(),
filter_config._odr_type: filter_config,
}
return await self._send_raw_cmd(cmd)
async def get_poll_interval(self):
return await self._send_cmd(self._cmd._target, self._cmd.GetPollInterval, msg_type="Interval")
class Kirdy:
def __init__(self):
self.device = Device(self._send_cmd, self._send_raw_cmd)
self.laser = Laser(self._send_cmd)
self.thermostat = Thermostat(self._send_cmd, self._send_raw_cmd)
self.hw_rev = None
self._task_queue, self._int_msg_queue, self._report_queue = None, None, None
self._timeout = 5.0
self._writer, self._reader = None, None
self._event_loop = None
self._msg_queue_get_report = False
self._report_mode_on = False
self._state = State.disconnected
self.read_response_task, self.handler_task = None, None
self._lock = asyncio.Lock()
# PyQt Signal
self._report_sig = None # Dict
self._connected_sig = None # Bool
self._err_msg_sig = None # Str
self.connected_event = None
def get_hw_rev(self):
return self.hw_rev
def set_report_sig(self, sig):
"""
Connect a PyQt Signal to the active report output(dict). This should be configured before the session is started.
"""
self._report_sig = sig
def set_connected_sig(self, sig):
"""
Connect a PyQt Signal to connection status(bool)
- True: Connection is established
- False: Connection is dropped
"""
self._connected_sig = sig
def set_err_msg_sig(self, sig):
"""
Emit a error message to a PyQt Signal(str) when a cmd fails to execute
"""
self._err_msg_sig = sig
def start_session(self, host='192.168.1.128', port=1337):
"""
Start Kirdy Connection Session.
In case of disconnection, all the queued tasks are cleared and the handler task retries TCP connection indefinitely.
- host: Kirdy's IP Address
- port: Kirdy's Port Number
"""
self._host, self._ctrl_port = host, port
if self._event_loop is None:
try:
self._event_loop = asyncio.get_running_loop()
except:
self._event_loop = asyncio.new_event_loop()
self._event_loop.run_forever()
self.connected_event = asyncio.Event()
self.handler_task = asyncio.create_task(self._handler())
async def end_session(self, block=False):
"""
Stop Kirdy's TCP connection and its associated thread.
"""
if self._event_loop is not None:
if block:
await self._task_queue.join()
if self.read_response_task is not None:
self.read_response_task.cancel()
await self.read_response_task
self.read_response_task = None
if self.handler_task is not None:
self.handler_task.cancel()
await self.handler_task
self.handler_task = None
self._writer = None
if self._connected_sig is not None:
self._connected_sig.emit(False)
def connecting(self):
"""
Return True if client is connecting
"""
return self._state == State.disconnected and self.read_response_task is not None
def connected(self):
"""
Returns True if client is connected.
"""
return self._writer is not None
async def wait_until_connected(self):
if not(self.connected()):
await self.connected_event.wait()
async def report_mode(self):
"""
Enable and retrieve active report from Kirdy
"""
if self.connected():
self._report_mode_on = True
await self.device.set_active_report_mode(True)
report = None
while self._report_mode_on:
report = await self._report_queue.get()
if not(isinstance(report, dict)):
self.stop_active_report()
else:
yield report
if isinstance(report, dict):
await self.device.set_active_report_mode(False)
else:
raise ConnectionError
def stop_report_mode(self):
self._report_mode_on = False
def task_dispatcher(self, awaitable_fn):
"""
Enqueue a task to be handled by the handler.
"""
if self.connected():
try:
self._task_queue.put_nowait(lambda: awaitable_fn)
return True
except asyncio.queues.QueueFull:
return False
else:
raise ConnectionError
async def _sock_disconnection_handling(self):
# Reader needn't be closed
try:
self._writer.close()
await self._writer.wait_closed()
except:
# In Hard Reset/DFU cmd, Kirdy may close its socket first
pass
self._reader = None
self._writer = None
for i in range(self._report_queue.maxsize):
if self._report_queue.full():
self._report_queue.get_nowait()
self._report_queue.put_nowait(None)
if self._connected_sig is not None:
self._connected_sig.emit(False)
async def _handler(self):
try:
self._state = State.disconnected
first_con = True
task = None
while True:
if self._state == State.disconnected:
try:
self.hw_rev = None
await self.__coninit(self._timeout)
self.read_response_task = asyncio.create_task(self._read_response_handler())
task = None
logging.info("Connected to %s:%d", self._host, self._ctrl_port)
hw_rev = await self.device.get_hw_rev()
self.hw_rev = hw_rev["hw_rev"]
if self._connected_sig is not None:
self._connected_sig.emit(True)
self.connected_event.set()
# State Transition
self._state = State.connected
except (OSError, TimeoutError):
if first_con:
first_con = False
logging.warning("Cannot connect to %s:%d. Retrying in the background.", self._host, self._ctrl_port)
await asyncio.sleep(5.0)
elif self._state == State.connected:
try:
task = await self._task_queue.get()
if isinstance(task, Exception):
raise task
await task()
self._task_queue.task_done()
except (TimeoutError, ConnectionResetError, ConnectionError):
logging.warning("Connection to Kirdy is dropped.")
first_con = True
self.read_response_task.cancel()
# State Transition
self._state = State.disconnected
await self._sock_disconnection_handling()
except asyncio.exceptions.CancelledError:
pass
except:
logging.warning("Handler experienced an error.", exc_info=True)
await self._sock_disconnection_handling()
async def _read_response_handler(self):
try:
while True:
if self._report_mode_on:
response = await asyncio.wait_for(self._read_response(), self._timeout)
else:
response = await self._read_response()
if response["msg_type"] == 'HardReset':
logging.warn("Kirdy is being hard reset.")
raise asyncio.exceptions.CancelledError
if response["msg_type"] == 'Dfu':
logging.warn("Kirdy enters Dfu Mode.")
asyncio.create_task(self.end_session())
if response["msg_type"] == 'ConnectionClose':
logging.warn("Kirdy runs out of TCP sockets and closes this connected socket.")
asyncio.create_task(self.end_session())
if response["msg_type"] == 'Report' and not self._msg_queue_get_report:
if self._report_sig is None:
self._report_queue.put_nowait_overwrite(response)
else:
self._report_sig.emit(response)
else:
if self._msg_queue_get_report:
self._msg_queue_get_report = False
self._int_msg_queue.put_nowait_overwrite(response)
except asyncio.exceptions.CancelledError:
pass
except (TimeoutError, ConnectionResetError, ConnectionError) as exec:
self._task_queue.put_nowait_overwrite(exec)
self._int_msg_queue.put_nowait_overwrite(exec)
except Exception as exec:
logging.warn("Read Response Handler experienced an error. Exiting.", exc_info=True)
self._task_queue.put_nowait_overwrite(exec)
self._int_msg_queue.put_nowait_overwrite(exec)
if self._report_mode_on:
self._report_mode_on = False
self._report_queue.put_nowait_overwrite(TimeoutError)
async def _stop_handler(self):
for task in asyncio.all_tasks():
task.cancel()
await asyncio.gather(*asyncio.all_tasks(), loop=self._event_loop)
async def __coninit(self, timeout):
def _put_nowait_overwrite(self, item):
if self.full():
self.get_nowait()
self.put_nowait(item)
asyncio.Queue.put_nowait_overwrite = _put_nowait_overwrite
if self._task_queue is not None:
while not(self._task_queue.empty()):
task = self._task_queue.get_nowait()
if isinstance(task, types.FunctionType):
task().close()
else:
self._task_queue = asyncio.Queue(maxsize=16)
self._int_msg_queue = asyncio.Queue(maxsize=4)
self._report_queue = asyncio.Queue(maxsize=16)
self._reader, self._writer = await asyncio.wait_for(asyncio.open_connection(self._host, self._ctrl_port), timeout)
writer_sock = self._writer.get_extra_info("socket")
writer_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
async def _read_response(self):
raw_response = b''
while len(raw_response) == 0:
# Ignore 0 size packet
raw_response = await self._reader.readuntil()
response = raw_response.decode('utf-8', errors='ignore').split("\n")
return json.loads(response[0])
def _response_handling(self, msg, msg_type, sig=None):
if msg["msg_type"] in ["InvalidCmd", "InvalidDatatype"]:
raise InvalidCmd
elif msg["msg_type"] == msg_type:
if sig is not None:
sig.emit(msg)
else:
logging.warn(f"Commands fail to execute. {msg['msg_type']}:{msg['msg']}")
if self._err_msg_sig is not None and msg['msg'] is not None:
self._err_msg_sig.emit(msg['msg'])
return msg
async def _send_raw_cmd(self, cmd, msg_type="Acknowledge", sig=None):
if self.connected():
async with self._lock:
self._writer.write(bytes(json.dumps(cmd), "UTF-8"))
await self._writer.drain()
msg = await asyncio.wait_for(self._int_msg_queue.get(), self._timeout)
return self._response_handling(msg, msg_type, sig)
else:
raise ConnectionError
async def _send_cmd(self, target, cmd, data=None, msg_type="Acknowledge", sig=None):
cmd_dict = {}
cmd_dict[target] = cmd.name
if cmd == _dt.f32:
if isinstance(data, float):
cmd_dict[cmd] = data
elif isinstance(data, int):
cmd_dict[cmd] = float(data)
elif cmd == _dt.bool:
if isinstance(data, bool):
cmd_dict[cmd] = data
else:
raise InvalidDataType
elif cmd == "None":
pass
if msg_type == 'Report':
self._msg_queue_get_report = True
async with self._lock:
self._writer.write(bytes(json.dumps(cmd_dict), "UTF-8"))
await self._writer.drain()
msg = await asyncio.wait_for(self._int_msg_queue.get(), self._timeout)
if isinstance(msg, Exception):
raise msg
return self._response_handling(msg, msg_type, sig)

1192
pykirdy/kirdy_qt.py Normal file

File diff suppressed because it is too large Load Diff

358
pykirdy/pid_autotune.py Normal file
View File

@ -0,0 +1,358 @@
import math
import logging
from collections import deque, namedtuple
from enum import Enum
import socket
import json
import time
import signal
from driver.kirdy import Kirdy, FilterConfig
import asyncio
from sipyco.asyncio_tools import SignalHandler
# Based on hirshmann pid-autotune libiary
# See https://github.com/hirschmann/pid-autotune
# Which is in turn based on a fork of Arduino PID AutoTune Library
# See https://github.com/t0mpr1c3/Arduino-PID-AutoTune-Library
class PIDAutotuneState(Enum):
STATE_OFF = 'off'
STATE_RELAY_STEP_UP = 'relay step up'
STATE_RELAY_STEP_DOWN = 'relay step down'
STATE_SUCCEEDED = 'succeeded'
STATE_FAILED = 'failed'
STATE_READY = 'ready'
class PIDAutotune:
PIDParams = namedtuple('PIDParams', ['Kp', 'Ki', 'Kd'])
PEAK_AMPLITUDE_TOLERANCE = 0.05
_tuning_rules = {
"ziegler-nichols": [0.6, 1.2, 0.075],
"tyreus-luyben": [0.4545, 0.2066, 0.07214],
"ciancone-marlin": [0.303, 0.1364, 0.0481],
"pessen-integral": [0.7, 1.75, 0.105],
"some-overshoot": [0.333, 0.667, 0.111],
"no-overshoot": [0.2, 0.4, 0.0667]
}
def __init__(self, setpoint, out_step=10, lookback=60,
noiseband=0.5, sampletime=1.2):
if setpoint is None:
raise ValueError('setpoint must be specified')
self._inputs = deque(maxlen=round(lookback / sampletime))
self._sampletime = sampletime
self._setpoint = setpoint
self._outputstep = out_step
self._noiseband = noiseband
self._out_min = -out_step
self._out_max = out_step
self._state = PIDAutotuneState.STATE_OFF
self._peak_timestamps = deque(maxlen=5)
self._peaks = deque(maxlen=5)
self._output = 0
self._last_run_timestamp = 0
self._peak_type = 0
self._peak_count = 0
self._initial_output = 0
self._induced_amplitude = 0
self._Ku = 0
self._Pu = 0
def setParam(self, target, step, noiseband, sampletime, lookback):
self._setpoint = target
self._outputstep = step
self._out_max = step
self._out_min = -step
self._noiseband = noiseband
self._inputs = deque(maxlen=round(lookback / sampletime))
self._sampletime = sampletime
def setReady(self):
self._state = PIDAutotuneState.STATE_READY
self._peak_count = 0
def setOff(self):
self._state = PIDAutotuneState.STATE_OFF
def setFailed(self):
self._state = PIDAutotuneState.STATE_FAILED
self._peak_count = 30
def state(self):
"""Get the current state."""
return self._state
def output(self):
"""Get the last output value."""
return self._output
def tuning_rules(self):
"""Get a list of all available tuning rules."""
return self._tuning_rules.keys()
def get_tec_pid (self):
divisors = self._tuning_rules["tyreus-luyben"]
kp = self._Ku * divisors[0]
ki = divisors[1] * self._Ku / self._Pu / (1 / self._sampletime)
kd = divisors[2] * self._Ku * self._Pu * (1 / self._sampletime)
return kp, ki, kd
def get_pid_parameters(self, tuning_rule='ziegler-nichols'):
"""Get PID parameters.
Args:
tuning_rule (str): Sets the rule which should be used to calculate
the parameters.
"""
divisors = self._tuning_rules[tuning_rule]
kp = self._Ku * divisors[0]
ki = divisors[1] * self._Ku / self._Pu / (1 / self._sampletime)
kd = divisors[2] * self._Ku * self._Pu * (1 / self._sampletime)
return PIDAutotune.PIDParams(kp, ki, kd)
def run(self, input_val, time_input):
"""To autotune a system, this method must be called periodically.
Args:
input_val (float): The temperature input value.
time_input (float): Current time in seconds.
Returns:
`true` if tuning is finished, otherwise `false`.
"""
now = time_input * 1000
if (self._state == PIDAutotuneState.STATE_OFF
or self._state == PIDAutotuneState.STATE_SUCCEEDED
or self._state == PIDAutotuneState.STATE_FAILED
or self._state == PIDAutotuneState.STATE_READY):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
self._last_run_timestamp = now
# check input and change relay state if necessary
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP
and input_val > self._setpoint + self._noiseband):
self._state = PIDAutotuneState.STATE_RELAY_STEP_DOWN
logging.debug('switched state: {0}'.format(self._state))
logging.debug('input: {0}'.format(input_val))
elif (self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN
and input_val < self._setpoint - self._noiseband):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
logging.debug('switched state: {0}'.format(self._state))
logging.debug('input: {0}'.format(input_val))
# set output
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP):
self._output = self._initial_output - self._outputstep
elif self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN:
self._output = self._initial_output + self._outputstep
# respect output limits
self._output = min(self._output, self._out_max)
self._output = max(self._output, self._out_min)
# identify peaks
is_max = True
is_min = True
for val in self._inputs:
is_max = is_max and (input_val >= val)
is_min = is_min and (input_val <= val)
self._inputs.append(input_val)
# we don't trust the maxes or mins until the input array is full
if len(self._inputs) < self._inputs.maxlen:
return False
# increment peak count and record peak time for maxima and minima
inflection = False
# peak types:
# -1: minimum
# +1: maximum
if is_max:
if self._peak_type == -1:
inflection = True
self._peak_type = 1
elif is_min:
if self._peak_type == 1:
inflection = True
self._peak_type = -1
# update peak times and values
if inflection:
self._peak_count += 1
self._peaks.append(input_val)
self._peak_timestamps.append(now)
logging.debug('found peak: {0}'.format(input_val))
logging.debug('peak count: {0}'.format(self._peak_count))
# check for convergence of induced oscillation
# convergence of amplitude assessed on last 4 peaks (1.5 cycles)
self._induced_amplitude = 0
if inflection and (self._peak_count > 4):
abs_max = self._peaks[-2]
abs_min = self._peaks[-2]
for i in range(0, len(self._peaks) - 2):
self._induced_amplitude += abs(self._peaks[i]
- self._peaks[i+1])
abs_max = max(self._peaks[i], abs_max)
abs_min = min(self._peaks[i], abs_min)
self._induced_amplitude /= 6.0
# check convergence criterion for amplitude of induced oscillation
amplitude_dev = ((0.5 * (abs_max - abs_min)
- self._induced_amplitude)
/ self._induced_amplitude)
logging.debug('amplitude: {0}'.format(self._induced_amplitude))
logging.debug('amplitude deviation: {0}'.format(amplitude_dev))
if amplitude_dev < PIDAutotune.PEAK_AMPLITUDE_TOLERANCE:
self._state = PIDAutotuneState.STATE_SUCCEEDED
# if the autotune has not already converged
# terminate after 10 cycles
if self._peak_count >= 20:
self._output = 0
self._state = PIDAutotuneState.STATE_FAILED
return True
if self._state == PIDAutotuneState.STATE_SUCCEEDED:
self._output = 0
logging.debug('peak finding successful')
# calculate ultimate gain
self._Ku = 4.0 * self._outputstep / \
(self._induced_amplitude * math.pi)
print('Ku: {0}'.format(self._Ku))
# calculate ultimate period in seconds
period1 = self._peak_timestamps[3] - self._peak_timestamps[1]
period2 = self._peak_timestamps[4] - self._peak_timestamps[2]
self._Pu = 0.5 * (period1 + period2) / 1000.0
print('Pu: {0}'.format(self._Pu))
for rule in self._tuning_rules:
params = self.get_pid_parameters(rule)
print('rule: {0}'.format(rule))
print('Kp: {0}'.format(params.Kp))
print('Ki: {0}'.format(params.Ki))
print('Kd: {0}'.format(params.Kd))
return True
return False
async def main():
"""
PID AutoTune Tools for Kirdy
The obtained temperature works best at the target temperature specified.
Before running PID AutoTune, please
1. Secure the laser diode onto the LD adapter and copper heat sink
2. Make sure Kirdy has warmed up and reached thermal equilibrium state
In case of PID Autotune Failure, you can
1. Run the PID Autotune again
2. Or increase the lookback period
3. Or increase the sampling rate
"""
# Target temperature of the autotune routine, celsius
target_temperature = 20
# Value by which output will be increased/decreased from zero, amps
output_step = 1
# Reference period for local minima/maxima, seconds
lookback = 2.0
# Determines by how much the input value must
# overshoot/undershoot the setpoint, celsius
noiseband = 2.0
kirdy = Kirdy()
kirdy.start_session(host='192.168.1.126', port=1337)
await kirdy.wait_until_connected()
while not(kirdy.connected()):
pass
await kirdy.laser.set_power_on(False)
await kirdy.laser.set_i(0)
await kirdy.thermostat.set_power_on(False)
await kirdy.thermostat.set_constant_current_control_mode()
await kirdy.thermostat.set_tec_i_out(0)
await kirdy.thermostat.clear_alarm()
signal_handler = SignalHandler()
signal_handler.setup()
async def sig_handling():
await signal_handler.wait_terminate()
tuner.setFailed()
asyncio.create_task(sig_handling())
await kirdy.device.set_active_report_mode(False)
# Configure the Thermistor Parameters
await kirdy.thermostat.set_sh_beta(3950)
await kirdy.thermostat.set_sh_r0(10.0 * 1000)
await kirdy.thermostat.set_sh_t0(25)
# Set a large enough temperature range so that it won't trigger overtemperature protection
await kirdy.thermostat.set_temp_mon_upper_limit(target_temperature + 20)
await kirdy.thermostat.set_temp_mon_lower_limit(target_temperature - 20)
await kirdy.thermostat.set_tec_max_cooling_i(output_step)
await kirdy.thermostat.set_tec_max_heating_i(output_step)
# The Polling Rate of Temperature Adc is equal to the PID Update Interval
await kirdy.thermostat.config_temp_adc_filter(FilterConfig.Sinc5Sinc1With50hz60HzRejection.f16sps)
settings = await kirdy.device.get_settings_summary()
sampling_rate = settings["thermostat"]["temp_adc_settings"]["rate"]
print("Settings: {0}".format(settings))
tuner = PIDAutotune(target_temperature, output_step,
lookback, noiseband, 1/sampling_rate)
await kirdy.thermostat.set_power_on(True)
while True:
status_report = await kirdy.device.get_status_report()
temperature = status_report["thermostat"]["temperature"]
ts = status_report['ts']
print("Ts: {0} Current Temperature: {1} degree".format(ts, temperature))
if (tuner.run(temperature, ts / 1000.0)):
print(tuner._state)
break
tuner_out = tuner.output()
await kirdy.thermostat.set_tec_i_out(float(tuner_out))
await kirdy.thermostat.set_tec_i_out(0)
await kirdy.thermostat.set_power_on(False)
if tuner.state() == PIDAutotuneState.STATE_SUCCEEDED:
pid_params = tuner.get_pid_parameters(tuning_rule="tyreus-luyben")
await kirdy.thermostat.set_pid_kp(pid_params.Kp)
await kirdy.thermostat.set_pid_ki(pid_params.Ki)
await kirdy.thermostat.set_pid_kd(pid_params.Kd)
await kirdy.thermostat.set_pid_output_max(1.0)
await kirdy.thermostat.set_pid_output_min(1.0)
await kirdy.end_session()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Cfg_Adc_Filter_Form</class>
<widget class="QDialog" name="Cfg_Adc_Filter_Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>786</width>
<height>303</height>
</rect>
</property>
<property name="windowTitle">
<string>Config Temperature ADC Filter</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>731</width>
<height>251</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="3,4,4">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Value</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Readings</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="filter_type_layout" stretch="3,4,4">
<item>
<widget class="QLabel" name="filter_type_lbl">
<property name="text">
<string>Filter Type</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="filter_type_cbox">
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="filter_type_reading_lbl">
<property name="text">
<string>Sinc5Sinc1With50hz60HzRejection</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="filter_sampling_rate_layout" stretch="3,4,4,4">
<item>
<widget class="QLabel" name="filter_sampling_rate_lbl">
<property name="text">
<string>Filter Sampling Rate</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="filter_sampling_rate_cbox"/>
</item>
<item>
<widget class="QDoubleSpinBox" name="fine_filter_sampling_rate_spinbox">
<property name="maximum">
<double>1000.000000000000000</double>
</property>
<property name="value">
<double>16.670000000000002</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="filter_sampling_rate_reading_lbl">
<property name="text">
<string>F16SPS</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="recorded_sampling_rate_layout" stretch="3,4,4">
<item>
<widget class="QLabel" name="recorded_sampling_rate_lbl">
<property name="text">
<string>Recorded Sampling Rate</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="recorded_sampling_rate_reading_lbl">
<property name="text">
<string>16.67</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="apply_btn_layout" stretch="3,2,3,2">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="apply_btn">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="close_btn">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>close_btn</sender>
<signal>clicked()</signal>
<receiver>Cfg_Adc_Filter_Form</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>677</x>
<y>246</y>
</hint>
<hint type="destinationlabel">
<x>392</x>
<y>151</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,488 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Cfg_Pd_Mon_Form</class>
<widget class="QDialog" name="Cfg_Pd_Mon_Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>520</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>500</width>
<height>520</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>520</height>
</size>
</property>
<property name="windowTitle">
<string>config_pd_mon_form</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>481</width>
<height>500</height>
</rect>
</property>
<layout class="QVBoxLayout" name="cfg_pd_mon_form_layout" stretch="0,2,2,2,2,2,1">
<item>
<widget class="QLabel" name="title_lbl">
<property name="font">
<font>
<pointsize>22</pointsize>
</font>
</property>
<property name="text">
<string>Configure Photodiode Monitor</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="pwr_off_layout" stretch="5,4">
<property name="spacing">
<number>12</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="pwr_off_lbl">
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string> Step 1: Turn off Laser Power</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pwr_off_btn">
<property name="text">
<string>Power Off</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="rst_ld_pwr_limit_layout" stretch="5,4">
<item>
<widget class="QLabel" name="rst_ld_pwr_limit_lbl">
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string> Step 2: Reset Ld Pwr Limit to 0</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="rst_ld_pwr_limit_btn">
<property name="text">
<string>Reset Ld Pwr Limit</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="cfg_pd_params_layout">
<item>
<widget class="QLabel" name="cfg_pd_params_lbl">
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string> Step 3: Configure Photodiode Parameters</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="5,2,2">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string> Value</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Reading</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="cfg_responsitivity_layout" stretch="5,2,2">
<item>
<widget class="QLabel" name="cfg_responsitivity_lbl">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Responsitivity: </string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="cfg_responsitivity_spinbox">
<property name="suffix">
<string/>
</property>
<property name="decimals">
<number>4</number>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
<property name="singleStep">
<double>0.001000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="cfg_responsitivity_reading">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>0.0000</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="cfg_dark_current_layout" stretch="5,2,2">
<item>
<widget class="QLabel" name="cfg_dark_current_lbl">
<property name="text">
<string>Dark Current: </string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="cfg_dark_current_spinbox">
<property name="decimals">
<number>4</number>
</property>
<property name="singleStep">
<double>0.001000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="cfg_dark_current_reading">
<property name="text">
<string>0.0000</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="apply_pd_params_layout" stretch="5,4">
<property name="spacing">
<number>12</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="apply_pd_params_btn">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="cfg_pwr_limit_layout">
<item>
<widget class="QLabel" name="cfg_pd_pwr_limit_lbl">
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string> Step 4: Configure Laser Diode Power Limit</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="5,2,2">
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string> Value</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Reading</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="pwr_limit_layout" stretch="5,2,2">
<item>
<widget class="QLabel" name="cfg_pwr_limit_lbl">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Power Limit:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="cfg_pwr_limit_spinbox">
<property name="decimals">
<number>4</number>
</property>
<property name="singleStep">
<double>0.001000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="cfg_pwr_limit_reading">
<property name="text">
<string>0.0000</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="settable_pwr_range_layout" stretch="5,4">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QLabel" name="settable_pwr_range_lbl">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Settable Power Range:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="settable_pwr_range_display_lbl">
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="text">
<string>( Power Range )</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="apply_pwr_limit_layout" stretch="2,3,4">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="apply_pwr_limit_max_btn">
<property name="text">
<string>Apply Max</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="apply_pwr_limit_btn">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="pwr_on_layout" stretch="5,4">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QLabel" name="pwr_on_lbl">
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string> Step 5: Turn On Laser Power</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pwr_on_btn">
<property name="text">
<string>Clear Alarm and Power On</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="close_btn_layout" stretch="5,4">
<property name="spacing">
<number>12</number>
</property>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="close_btn">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>close_btn</sender>
<signal>clicked()</signal>
<receiver>Cfg_Pd_Mon_Form</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>141</x>
<y>456</y>
</hint>
<hint type="destinationlabel">
<x>281</x>
<y>355</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,376 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Conn_Settings_Form</class>
<widget class="QDialog" name="Conn_Settings_Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>415</width>
<height>145</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>415</width>
<height>145</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>415</width>
<height>145</height>
</size>
</property>
<property name="windowTitle">
<string>Connection Settings</string>
</property>
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="ip_addr_layout">
<item>
<widget class="QLabel" name="ip_addr_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>IP Address:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_0">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>192</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_0_label">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_1">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>168</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_1_label">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_2">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_2_label">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_3">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>128</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_2">
<property name="geometry">
<rect>
<x>20</x>
<y>50</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="port_layout" stretch="3,0,6">
<item>
<widget class="QLabel" name="port_no_label">
<property name="maximumSize">
<size>
<width>97</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Port:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="port_in">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1337</string>
</property>
<property name="maxLength">
<number>5</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="port_layout_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>50</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_4">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>371</width>
<height>47</height>
</rect>
</property>
<layout class="QHBoxLayout" name="buttons_layout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connect_btn">
<property name="text">
<string>Connect</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel_btn">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>addr_in_0</tabstop>
<tabstop>addr_in_1</tabstop>
<tabstop>addr_in_2</tabstop>
<tabstop>addr_in_3</tabstop>
<tabstop>port_in</tabstop>
<tabstop>connect_btn</tabstop>
<tabstop>cancel_btn</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>cancel_btn</sender>
<signal>clicked()</signal>
<receiver>Conn_Settings_Form</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>340</x>
<y>140</y>
</hint>
<hint type="destinationlabel">
<x>364</x>
<y>96</y>
</hint>
</hints>
</connection>
<connection>
<sender>connect_btn</sender>
<signal>clicked()</signal>
<receiver>Conn_Settings_Form</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>258</x>
<y>147</y>
</hint>
<hint type="destinationlabel">
<x>237</x>
<y>99</y>
</hint>
</hints>
</connection>
</connections>
</ui>

696
pykirdy/ui/kirdy_qt.ui Normal file
View File

@ -0,0 +1,696 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>720</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1280</width>
<height>720</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>3840</width>
<height>2160</height>
</size>
</property>
<property name="windowTitle">
<string>Kirdy Control Panel</string>
</property>
<property name="windowIcon">
<iconset theme="application-x-executable"/>
</property>
<widget class="QWidget" name="main_widget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<property name="spacing">
<number>3</number>
</property>
<item row="0" column="0">
<layout class="QVBoxLayout" name="main_layout">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,2">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
</property>
<item>
<layout class="QVBoxLayout" name="ctrl_vertical_layout" stretch="1,1,10,1,1,1,10,1">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetMaximumSize</enum>
</property>
<item>
<widget class="QLabel" name="ld_section_label">
<property name="font">
<font>
<pointsize>14</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string> Laser Diode</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="ParameterTree" name="ld_status" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>57</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="ParameterTree" name="ld_tree" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="ld_btns_layout">
<property name="spacing">
<number>30</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="ld_pwr_on_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>POWER ON</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ld_pwr_off_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>POWER OFF</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ld_clear_alarm_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>CLEAR ALARM</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="tec_section_label">
<property name="font">
<font>
<pointsize>14</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string> Thermostat</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="ParameterTree" name="tec_status" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>57</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="ParameterTree" name="tec_tree" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="tec_btns_layout">
<property name="spacing">
<number>30</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="tec_pwr_on_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>POWER ON</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tec_pwr_off_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>POWER OFF</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tec_clear_alarm_btn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>CLEAR ALARM</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="graphgroup">
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetNoConstraint</enum>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="LivePlotWidget" name="tec_temp_graph" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="LivePlotWidget" name="tec_i_graph" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="LivePlotWidget" name="pd_mon_pwr_graph" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="LivePlotWidget" name="ld_i_set_graph" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="bottom_settings_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>40</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="settings_layout">
<item>
<widget class="QPushButton" name="connect_btn">
<property name="text">
<string>Connect</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="status_lbl">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>480</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>480</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>120</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>Disconnected</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="plot_settings">
<property name="toolTip">
<string>Plot Settings</string>
</property>
<property name="text">
<string>📉</string>
</property>
<property name="popupMode">
<enum>QToolButton::ToolButtonPopupMode::InstantPopup</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="limits_warning">
<property name="toolTipDuration">
<number>1000000000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="background_task_lbl">
<property name="text">
<string>Ready.</string>
</property>
</widget>
</item>
<item>
<widget class="QtWaitingSpinner" name="loading_spinner" native="true"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="polling_rate_lbl">
<property name="text">
<string>Polling Rate (Hz): </string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="report_group" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QDoubleSpinBox" name="polling_rate_spinbox">
<property name="maximum">
<double>1000.000000000000000</double>
</property>
<property name="value">
<double>20.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QPushButton" name="polling_rate_apply_btn">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>23</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<widget class="QMenu" name="menuKirdy">
<property name="enabled">
<bool>true</bool>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="title">
<string>Kirdy</string>
</property>
<addaction name="menu_action_about_kirdy"/>
<addaction name="menu_action_update_net_settings"/>
<addaction name="menu_action_save"/>
<addaction name="menu_action_load"/>
<addaction name="menu_action_dfu_mode"/>
<addaction name="menu_action_hard_reset"/>
</widget>
<widget class="QMenu" name="menuAbout">
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="title">
<string>About</string>
</property>
<addaction name="menu_action_about_gui"/>
</widget>
<addaction name="menuKirdy"/>
<addaction name="menuAbout"/>
</widget>
<action name="actionReset">
<property name="text">
<string>Reset</string>
</property>
<property name="toolTip">
<string>Reset the Kirdy</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionEnter_DFU_Mode">
<property name="text">
<string>Enter DFU Mode</string>
</property>
<property name="toolTip">
<string>Reset kirdy and enter USB device firmware update (DFU) mode</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionNetwork_Settings">
<property name="text">
<string>Network Settings</string>
</property>
<property name="toolTip">
<string>Configure IPv4 address, netmask length, and optional default gateway</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionAbout_Kirdy">
<property name="text">
<string>About Kirdy</string>
</property>
<property name="toolTip">
<string>Show Kirdy hardware revision, and settings related to i</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionLoad_all_configs">
<property name="text">
<string>Load all channel configs from flash</string>
</property>
<property name="toolTip">
<string>Restore configuration for all channels from flash</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionSave_all_configs">
<property name="text">
<string>Save all channel configs to flash</string>
</property>
<property name="toolTip">
<string>Save configuration for all channels to flash</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionConnection_Settings">
<property name="text">
<string>Connect</string>
</property>
</action>
<action name="menu_action_update_net_settings">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Update Network Settings</string>
</property>
</action>
<action name="menu_action_dfu_mode">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Enter DFU Mode</string>
</property>
</action>
<action name="menu_action_save">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save Settings to Flash</string>
</property>
</action>
<action name="menu_action_hard_reset">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Hard Reset</string>
</property>
</action>
<action name="menu_action_connect">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Connect</string>
</property>
</action>
<action name="menu_action_disconnect">
<property name="text">
<string>Disconnect</string>
</property>
</action>
<action name="menu_action_about_gui">
<property name="text">
<string>About GUI</string>
</property>
</action>
<action name="action">
<property name="text">
<string>test</string>
</property>
</action>
<action name="menu_action_about_kirdy">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>About Kirdy</string>
</property>
</action>
<action name="menu_action_load">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Load Settings from Flash</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>ParameterTree</class>
<extends>QWidget</extends>
<header>pyqtgraph.parametertree</header>
<container>1</container>
</customwidget>
<customwidget>
<class>LivePlotWidget</class>
<extends>QWidget</extends>
<header>pglive.sources.live_plot_widget</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QtWaitingSpinner</class>
<extends>QWidget</extends>
<header>waitingspinnerwidget</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,6 @@
QPushButton:disabled { color: gray }
ParameterTree:disabled { color: gray }
QToolButton:disabled { color: gray }
QDoubleSpinBox:disabled { color: gray }
QCheckBox:disabled { color: gray }
QMenu:disabled { color: gray }

View File

@ -0,0 +1,131 @@
# Form implementation generated from reading ui file 'config_adc_filter_form.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Cfg_Adc_Filter_Form(object):
def setupUi(self, Cfg_Adc_Filter_Form):
Cfg_Adc_Filter_Form.setObjectName("Cfg_Adc_Filter_Form")
Cfg_Adc_Filter_Form.resize(786, 303)
self.verticalLayoutWidget = QtWidgets.QWidget(parent=Cfg_Adc_Filter_Form)
self.verticalLayoutWidget.setGeometry(QtCore.QRect(20, 20, 731, 251))
self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.label_4 = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.label_4.setObjectName("label_4")
self.horizontalLayout.addWidget(self.label_4)
self.label_5 = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.label_5.setObjectName("label_5")
self.horizontalLayout.addWidget(self.label_5)
self.horizontalLayout.setStretch(0, 3)
self.horizontalLayout.setStretch(1, 4)
self.horizontalLayout.setStretch(2, 4)
self.verticalLayout.addLayout(self.horizontalLayout)
self.filter_type_layout = QtWidgets.QHBoxLayout()
self.filter_type_layout.setObjectName("filter_type_layout")
self.filter_type_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.filter_type_lbl.setObjectName("filter_type_lbl")
self.filter_type_layout.addWidget(self.filter_type_lbl)
self.filter_type_cbox = QtWidgets.QComboBox(parent=self.verticalLayoutWidget)
self.filter_type_cbox.setEditable(False)
self.filter_type_cbox.setObjectName("filter_type_cbox")
self.filter_type_layout.addWidget(self.filter_type_cbox)
self.filter_type_reading_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.filter_type_reading_lbl.setObjectName("filter_type_reading_lbl")
self.filter_type_layout.addWidget(self.filter_type_reading_lbl)
self.filter_type_layout.setStretch(0, 3)
self.filter_type_layout.setStretch(1, 4)
self.filter_type_layout.setStretch(2, 4)
self.verticalLayout.addLayout(self.filter_type_layout)
self.filter_sampling_rate_layout = QtWidgets.QHBoxLayout()
self.filter_sampling_rate_layout.setObjectName("filter_sampling_rate_layout")
self.filter_sampling_rate_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.filter_sampling_rate_lbl.setObjectName("filter_sampling_rate_lbl")
self.filter_sampling_rate_layout.addWidget(self.filter_sampling_rate_lbl)
self.filter_sampling_rate_cbox = QtWidgets.QComboBox(parent=self.verticalLayoutWidget)
self.filter_sampling_rate_cbox.setObjectName("filter_sampling_rate_cbox")
self.filter_sampling_rate_layout.addWidget(self.filter_sampling_rate_cbox)
self.fine_filter_sampling_rate_spinbox = QtWidgets.QDoubleSpinBox(parent=self.verticalLayoutWidget)
self.fine_filter_sampling_rate_spinbox.setMaximum(1000.0)
self.fine_filter_sampling_rate_spinbox.setProperty("value", 16.67)
self.fine_filter_sampling_rate_spinbox.setObjectName("fine_filter_sampling_rate_spinbox")
self.filter_sampling_rate_layout.addWidget(self.fine_filter_sampling_rate_spinbox)
self.filter_sampling_rate_reading_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.filter_sampling_rate_reading_lbl.setObjectName("filter_sampling_rate_reading_lbl")
self.filter_sampling_rate_layout.addWidget(self.filter_sampling_rate_reading_lbl)
self.filter_sampling_rate_layout.setStretch(0, 3)
self.filter_sampling_rate_layout.setStretch(1, 4)
self.filter_sampling_rate_layout.setStretch(2, 4)
self.filter_sampling_rate_layout.setStretch(3, 4)
self.verticalLayout.addLayout(self.filter_sampling_rate_layout)
self.recorded_sampling_rate_layout = QtWidgets.QHBoxLayout()
self.recorded_sampling_rate_layout.setObjectName("recorded_sampling_rate_layout")
self.recorded_sampling_rate_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.recorded_sampling_rate_lbl.setObjectName("recorded_sampling_rate_lbl")
self.recorded_sampling_rate_layout.addWidget(self.recorded_sampling_rate_lbl)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.recorded_sampling_rate_layout.addItem(spacerItem1)
self.recorded_sampling_rate_reading_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.recorded_sampling_rate_reading_lbl.setObjectName("recorded_sampling_rate_reading_lbl")
self.recorded_sampling_rate_layout.addWidget(self.recorded_sampling_rate_reading_lbl)
self.recorded_sampling_rate_layout.setStretch(0, 3)
self.recorded_sampling_rate_layout.setStretch(1, 4)
self.recorded_sampling_rate_layout.setStretch(2, 4)
self.verticalLayout.addLayout(self.recorded_sampling_rate_layout)
self.apply_btn_layout = QtWidgets.QHBoxLayout()
self.apply_btn_layout.setObjectName("apply_btn_layout")
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.apply_btn_layout.addItem(spacerItem2)
self.apply_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.apply_btn.setObjectName("apply_btn")
self.apply_btn_layout.addWidget(self.apply_btn)
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.apply_btn_layout.addItem(spacerItem3)
self.close_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.close_btn.setObjectName("close_btn")
self.apply_btn_layout.addWidget(self.close_btn)
self.apply_btn_layout.setStretch(0, 3)
self.apply_btn_layout.setStretch(1, 2)
self.apply_btn_layout.setStretch(2, 3)
self.apply_btn_layout.setStretch(3, 2)
self.verticalLayout.addLayout(self.apply_btn_layout)
self.retranslateUi(Cfg_Adc_Filter_Form)
self.close_btn.clicked.connect(Cfg_Adc_Filter_Form.accept) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Cfg_Adc_Filter_Form)
def retranslateUi(self, Cfg_Adc_Filter_Form):
_translate = QtCore.QCoreApplication.translate
Cfg_Adc_Filter_Form.setWindowTitle(_translate("Cfg_Adc_Filter_Form", "Config Temperature ADC Filter"))
self.label_4.setText(_translate("Cfg_Adc_Filter_Form", "Value"))
self.label_5.setText(_translate("Cfg_Adc_Filter_Form", "Readings"))
self.filter_type_lbl.setText(_translate("Cfg_Adc_Filter_Form", "Filter Type"))
self.filter_type_reading_lbl.setText(_translate("Cfg_Adc_Filter_Form", "Sinc5Sinc1With50hz60HzRejection"))
self.filter_sampling_rate_lbl.setText(_translate("Cfg_Adc_Filter_Form", "Filter Sampling Rate"))
self.filter_sampling_rate_reading_lbl.setText(_translate("Cfg_Adc_Filter_Form", "F16SPS"))
self.recorded_sampling_rate_lbl.setText(_translate("Cfg_Adc_Filter_Form", "Recorded Sampling Rate"))
self.recorded_sampling_rate_reading_lbl.setText(_translate("Cfg_Adc_Filter_Form", "16.67"))
self.apply_btn.setText(_translate("Cfg_Adc_Filter_Form", "Apply"))
self.close_btn.setText(_translate("Cfg_Adc_Filter_Form", "Close"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Cfg_Adc_Filter_Form = QtWidgets.QDialog()
ui = Ui_Cfg_Adc_Filter_Form()
ui.setupUi(Cfg_Adc_Filter_Form)
Cfg_Adc_Filter_Form.show()
sys.exit(app.exec())

View File

@ -0,0 +1,295 @@
# Form implementation generated from reading ui file 'config_pd_mon_form.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Cfg_Pd_Mon_Form(object):
def setupUi(self, Cfg_Pd_Mon_Form):
Cfg_Pd_Mon_Form.setObjectName("Cfg_Pd_Mon_Form")
Cfg_Pd_Mon_Form.resize(500, 520)
Cfg_Pd_Mon_Form.setMinimumSize(QtCore.QSize(500, 520))
Cfg_Pd_Mon_Form.setMaximumSize(QtCore.QSize(500, 520))
self.verticalLayoutWidget_2 = QtWidgets.QWidget(parent=Cfg_Pd_Mon_Form)
self.verticalLayoutWidget_2.setGeometry(QtCore.QRect(10, 10, 481, 500))
self.verticalLayoutWidget_2.setObjectName("verticalLayoutWidget_2")
self.cfg_pd_mon_form_layout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget_2)
self.cfg_pd_mon_form_layout.setContentsMargins(0, 0, 0, 0)
self.cfg_pd_mon_form_layout.setObjectName("cfg_pd_mon_form_layout")
self.title_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
font = QtGui.QFont()
font.setPointSize(22)
self.title_lbl.setFont(font)
self.title_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.title_lbl.setObjectName("title_lbl")
self.cfg_pd_mon_form_layout.addWidget(self.title_lbl)
self.pwr_off_layout = QtWidgets.QHBoxLayout()
self.pwr_off_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
self.pwr_off_layout.setSpacing(12)
self.pwr_off_layout.setObjectName("pwr_off_layout")
self.pwr_off_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
font = QtGui.QFont()
font.setPointSize(14)
self.pwr_off_lbl.setFont(font)
self.pwr_off_lbl.setObjectName("pwr_off_lbl")
self.pwr_off_layout.addWidget(self.pwr_off_lbl)
self.pwr_off_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget_2)
self.pwr_off_btn.setObjectName("pwr_off_btn")
self.pwr_off_layout.addWidget(self.pwr_off_btn)
self.pwr_off_layout.setStretch(0, 5)
self.pwr_off_layout.setStretch(1, 4)
self.cfg_pd_mon_form_layout.addLayout(self.pwr_off_layout)
self.rst_ld_pwr_limit_layout = QtWidgets.QHBoxLayout()
self.rst_ld_pwr_limit_layout.setObjectName("rst_ld_pwr_limit_layout")
self.rst_ld_pwr_limit_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
font = QtGui.QFont()
font.setPointSize(14)
self.rst_ld_pwr_limit_lbl.setFont(font)
self.rst_ld_pwr_limit_lbl.setObjectName("rst_ld_pwr_limit_lbl")
self.rst_ld_pwr_limit_layout.addWidget(self.rst_ld_pwr_limit_lbl)
self.rst_ld_pwr_limit_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget_2)
self.rst_ld_pwr_limit_btn.setObjectName("rst_ld_pwr_limit_btn")
self.rst_ld_pwr_limit_layout.addWidget(self.rst_ld_pwr_limit_btn)
self.rst_ld_pwr_limit_layout.setStretch(0, 5)
self.rst_ld_pwr_limit_layout.setStretch(1, 4)
self.cfg_pd_mon_form_layout.addLayout(self.rst_ld_pwr_limit_layout)
self.cfg_pd_params_layout = QtWidgets.QVBoxLayout()
self.cfg_pd_params_layout.setObjectName("cfg_pd_params_layout")
self.cfg_pd_params_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
font = QtGui.QFont()
font.setPointSize(14)
self.cfg_pd_params_lbl.setFont(font)
self.cfg_pd_params_lbl.setObjectName("cfg_pd_params_lbl")
self.cfg_pd_params_layout.addWidget(self.cfg_pd_params_lbl)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.label_2 = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
self.label_2.setObjectName("label_2")
self.horizontalLayout.addWidget(self.label_2)
self.label = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
self.horizontalLayout.setStretch(0, 5)
self.horizontalLayout.setStretch(1, 2)
self.horizontalLayout.setStretch(2, 2)
self.cfg_pd_params_layout.addLayout(self.horizontalLayout)
self.cfg_responsitivity_layout = QtWidgets.QHBoxLayout()
self.cfg_responsitivity_layout.setObjectName("cfg_responsitivity_layout")
self.cfg_responsitivity_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.cfg_responsitivity_lbl.sizePolicy().hasHeightForWidth())
self.cfg_responsitivity_lbl.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(12)
self.cfg_responsitivity_lbl.setFont(font)
self.cfg_responsitivity_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.cfg_responsitivity_lbl.setObjectName("cfg_responsitivity_lbl")
self.cfg_responsitivity_layout.addWidget(self.cfg_responsitivity_lbl)
self.cfg_responsitivity_spinbox = QtWidgets.QDoubleSpinBox(parent=self.verticalLayoutWidget_2)
self.cfg_responsitivity_spinbox.setSuffix("")
self.cfg_responsitivity_spinbox.setDecimals(4)
self.cfg_responsitivity_spinbox.setMaximum(1000.0)
self.cfg_responsitivity_spinbox.setSingleStep(0.001)
self.cfg_responsitivity_spinbox.setObjectName("cfg_responsitivity_spinbox")
self.cfg_responsitivity_layout.addWidget(self.cfg_responsitivity_spinbox)
self.cfg_responsitivity_reading = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.cfg_responsitivity_reading.sizePolicy().hasHeightForWidth())
self.cfg_responsitivity_reading.setSizePolicy(sizePolicy)
self.cfg_responsitivity_reading.setObjectName("cfg_responsitivity_reading")
self.cfg_responsitivity_layout.addWidget(self.cfg_responsitivity_reading)
self.cfg_responsitivity_layout.setStretch(0, 5)
self.cfg_responsitivity_layout.setStretch(1, 2)
self.cfg_responsitivity_layout.setStretch(2, 2)
self.cfg_pd_params_layout.addLayout(self.cfg_responsitivity_layout)
self.cfg_dark_current_layout = QtWidgets.QHBoxLayout()
self.cfg_dark_current_layout.setObjectName("cfg_dark_current_layout")
self.cfg_dark_current_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
self.cfg_dark_current_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.cfg_dark_current_lbl.setObjectName("cfg_dark_current_lbl")
self.cfg_dark_current_layout.addWidget(self.cfg_dark_current_lbl)
self.cfg_dark_current_spinbox = QtWidgets.QDoubleSpinBox(parent=self.verticalLayoutWidget_2)
self.cfg_dark_current_spinbox.setDecimals(4)
self.cfg_dark_current_spinbox.setSingleStep(0.001)
self.cfg_dark_current_spinbox.setObjectName("cfg_dark_current_spinbox")
self.cfg_dark_current_layout.addWidget(self.cfg_dark_current_spinbox)
self.cfg_dark_current_reading = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
self.cfg_dark_current_reading.setObjectName("cfg_dark_current_reading")
self.cfg_dark_current_layout.addWidget(self.cfg_dark_current_reading)
self.cfg_dark_current_layout.setStretch(0, 5)
self.cfg_dark_current_layout.setStretch(1, 2)
self.cfg_dark_current_layout.setStretch(2, 2)
self.cfg_pd_params_layout.addLayout(self.cfg_dark_current_layout)
self.apply_pd_params_layout = QtWidgets.QHBoxLayout()
self.apply_pd_params_layout.setSpacing(12)
self.apply_pd_params_layout.setObjectName("apply_pd_params_layout")
spacerItem1 = QtWidgets.QSpacerItem(10, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.apply_pd_params_layout.addItem(spacerItem1)
self.apply_pd_params_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget_2)
self.apply_pd_params_btn.setObjectName("apply_pd_params_btn")
self.apply_pd_params_layout.addWidget(self.apply_pd_params_btn)
self.apply_pd_params_layout.setStretch(0, 5)
self.apply_pd_params_layout.setStretch(1, 4)
self.cfg_pd_params_layout.addLayout(self.apply_pd_params_layout)
self.cfg_pd_mon_form_layout.addLayout(self.cfg_pd_params_layout)
self.cfg_pwr_limit_layout = QtWidgets.QVBoxLayout()
self.cfg_pwr_limit_layout.setObjectName("cfg_pwr_limit_layout")
self.cfg_pd_pwr_limit_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
font = QtGui.QFont()
font.setPointSize(14)
self.cfg_pd_pwr_limit_lbl.setFont(font)
self.cfg_pd_pwr_limit_lbl.setObjectName("cfg_pd_pwr_limit_lbl")
self.cfg_pwr_limit_layout.addWidget(self.cfg_pd_pwr_limit_lbl)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_2.addItem(spacerItem2)
self.label_3 = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
self.label_3.setObjectName("label_3")
self.horizontalLayout_2.addWidget(self.label_3)
self.label_4 = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
self.label_4.setObjectName("label_4")
self.horizontalLayout_2.addWidget(self.label_4)
self.horizontalLayout_2.setStretch(0, 5)
self.horizontalLayout_2.setStretch(1, 2)
self.horizontalLayout_2.setStretch(2, 2)
self.cfg_pwr_limit_layout.addLayout(self.horizontalLayout_2)
self.pwr_limit_layout = QtWidgets.QHBoxLayout()
self.pwr_limit_layout.setObjectName("pwr_limit_layout")
self.cfg_pwr_limit_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
font = QtGui.QFont()
font.setPointSize(12)
self.cfg_pwr_limit_lbl.setFont(font)
self.cfg_pwr_limit_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.cfg_pwr_limit_lbl.setObjectName("cfg_pwr_limit_lbl")
self.pwr_limit_layout.addWidget(self.cfg_pwr_limit_lbl)
self.cfg_pwr_limit_spinbox = QtWidgets.QDoubleSpinBox(parent=self.verticalLayoutWidget_2)
self.cfg_pwr_limit_spinbox.setDecimals(4)
self.cfg_pwr_limit_spinbox.setSingleStep(0.001)
self.cfg_pwr_limit_spinbox.setObjectName("cfg_pwr_limit_spinbox")
self.pwr_limit_layout.addWidget(self.cfg_pwr_limit_spinbox)
self.cfg_pwr_limit_reading = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
self.cfg_pwr_limit_reading.setObjectName("cfg_pwr_limit_reading")
self.pwr_limit_layout.addWidget(self.cfg_pwr_limit_reading)
self.pwr_limit_layout.setStretch(0, 5)
self.pwr_limit_layout.setStretch(1, 2)
self.pwr_limit_layout.setStretch(2, 2)
self.cfg_pwr_limit_layout.addLayout(self.pwr_limit_layout)
self.settable_pwr_range_layout = QtWidgets.QHBoxLayout()
self.settable_pwr_range_layout.setSpacing(12)
self.settable_pwr_range_layout.setObjectName("settable_pwr_range_layout")
self.settable_pwr_range_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
font = QtGui.QFont()
font.setPointSize(12)
self.settable_pwr_range_lbl.setFont(font)
self.settable_pwr_range_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.settable_pwr_range_lbl.setObjectName("settable_pwr_range_lbl")
self.settable_pwr_range_layout.addWidget(self.settable_pwr_range_lbl)
self.settable_pwr_range_display_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
self.settable_pwr_range_display_lbl.setMinimumSize(QtCore.QSize(0, 28))
self.settable_pwr_range_display_lbl.setObjectName("settable_pwr_range_display_lbl")
self.settable_pwr_range_layout.addWidget(self.settable_pwr_range_display_lbl)
self.settable_pwr_range_layout.setStretch(0, 5)
self.settable_pwr_range_layout.setStretch(1, 4)
self.cfg_pwr_limit_layout.addLayout(self.settable_pwr_range_layout)
self.apply_pwr_limit_layout = QtWidgets.QHBoxLayout()
self.apply_pwr_limit_layout.setObjectName("apply_pwr_limit_layout")
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.apply_pwr_limit_layout.addItem(spacerItem3)
self.apply_pwr_limit_max_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget_2)
self.apply_pwr_limit_max_btn.setObjectName("apply_pwr_limit_max_btn")
self.apply_pwr_limit_layout.addWidget(self.apply_pwr_limit_max_btn)
self.apply_pwr_limit_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget_2)
self.apply_pwr_limit_btn.setObjectName("apply_pwr_limit_btn")
self.apply_pwr_limit_layout.addWidget(self.apply_pwr_limit_btn)
self.apply_pwr_limit_layout.setStretch(0, 2)
self.apply_pwr_limit_layout.setStretch(1, 3)
self.apply_pwr_limit_layout.setStretch(2, 4)
self.cfg_pwr_limit_layout.addLayout(self.apply_pwr_limit_layout)
self.cfg_pd_mon_form_layout.addLayout(self.cfg_pwr_limit_layout)
self.pwr_on_layout = QtWidgets.QHBoxLayout()
self.pwr_on_layout.setSpacing(12)
self.pwr_on_layout.setObjectName("pwr_on_layout")
self.pwr_on_lbl = QtWidgets.QLabel(parent=self.verticalLayoutWidget_2)
font = QtGui.QFont()
font.setPointSize(14)
self.pwr_on_lbl.setFont(font)
self.pwr_on_lbl.setObjectName("pwr_on_lbl")
self.pwr_on_layout.addWidget(self.pwr_on_lbl)
self.pwr_on_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget_2)
self.pwr_on_btn.setObjectName("pwr_on_btn")
self.pwr_on_layout.addWidget(self.pwr_on_btn)
self.pwr_on_layout.setStretch(0, 5)
self.pwr_on_layout.setStretch(1, 4)
self.cfg_pd_mon_form_layout.addLayout(self.pwr_on_layout)
self.close_btn_layout = QtWidgets.QHBoxLayout()
self.close_btn_layout.setSpacing(12)
self.close_btn_layout.setObjectName("close_btn_layout")
spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.close_btn_layout.addItem(spacerItem4)
self.close_btn = QtWidgets.QPushButton(parent=self.verticalLayoutWidget_2)
self.close_btn.setObjectName("close_btn")
self.close_btn_layout.addWidget(self.close_btn)
self.close_btn_layout.setStretch(0, 5)
self.close_btn_layout.setStretch(1, 4)
self.cfg_pd_mon_form_layout.addLayout(self.close_btn_layout)
self.cfg_pd_mon_form_layout.setStretch(1, 2)
self.cfg_pd_mon_form_layout.setStretch(2, 2)
self.cfg_pd_mon_form_layout.setStretch(3, 2)
self.cfg_pd_mon_form_layout.setStretch(4, 2)
self.cfg_pd_mon_form_layout.setStretch(5, 2)
self.cfg_pd_mon_form_layout.setStretch(6, 1)
self.retranslateUi(Cfg_Pd_Mon_Form)
self.close_btn.clicked.connect(Cfg_Pd_Mon_Form.accept) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Cfg_Pd_Mon_Form)
def retranslateUi(self, Cfg_Pd_Mon_Form):
_translate = QtCore.QCoreApplication.translate
Cfg_Pd_Mon_Form.setWindowTitle(_translate("Cfg_Pd_Mon_Form", "config_pd_mon_form"))
self.title_lbl.setText(_translate("Cfg_Pd_Mon_Form", "Configure Photodiode Monitor"))
self.pwr_off_lbl.setText(_translate("Cfg_Pd_Mon_Form", " Step 1: Turn off Laser Power"))
self.pwr_off_btn.setText(_translate("Cfg_Pd_Mon_Form", "Power Off"))
self.rst_ld_pwr_limit_lbl.setText(_translate("Cfg_Pd_Mon_Form", " Step 2: Reset Ld Pwr Limit to 0"))
self.rst_ld_pwr_limit_btn.setText(_translate("Cfg_Pd_Mon_Form", "Reset Ld Pwr Limit"))
self.cfg_pd_params_lbl.setText(_translate("Cfg_Pd_Mon_Form", " Step 3: Configure Photodiode Parameters"))
self.label_2.setText(_translate("Cfg_Pd_Mon_Form", " Value"))
self.label.setText(_translate("Cfg_Pd_Mon_Form", "Reading"))
self.cfg_responsitivity_lbl.setText(_translate("Cfg_Pd_Mon_Form", "Responsitivity: "))
self.cfg_responsitivity_reading.setText(_translate("Cfg_Pd_Mon_Form", "0.0000"))
self.cfg_dark_current_lbl.setText(_translate("Cfg_Pd_Mon_Form", "Dark Current: "))
self.cfg_dark_current_reading.setText(_translate("Cfg_Pd_Mon_Form", "0.0000"))
self.apply_pd_params_btn.setText(_translate("Cfg_Pd_Mon_Form", "Apply"))
self.cfg_pd_pwr_limit_lbl.setText(_translate("Cfg_Pd_Mon_Form", " Step 4: Configure Laser Diode Power Limit"))
self.label_3.setText(_translate("Cfg_Pd_Mon_Form", " Value"))
self.label_4.setText(_translate("Cfg_Pd_Mon_Form", "Reading"))
self.cfg_pwr_limit_lbl.setText(_translate("Cfg_Pd_Mon_Form", "Power Limit:"))
self.cfg_pwr_limit_reading.setText(_translate("Cfg_Pd_Mon_Form", "0.0000"))
self.settable_pwr_range_lbl.setText(_translate("Cfg_Pd_Mon_Form", "Settable Power Range:"))
self.settable_pwr_range_display_lbl.setText(_translate("Cfg_Pd_Mon_Form", "( Power Range )"))
self.apply_pwr_limit_max_btn.setText(_translate("Cfg_Pd_Mon_Form", "Apply Max"))
self.apply_pwr_limit_btn.setText(_translate("Cfg_Pd_Mon_Form", "Apply"))
self.pwr_on_lbl.setText(_translate("Cfg_Pd_Mon_Form", " Step 5: Turn On Laser Power"))
self.pwr_on_btn.setText(_translate("Cfg_Pd_Mon_Form", "Clear Alarm and Power On"))
self.close_btn.setText(_translate("Cfg_Pd_Mon_Form", "Close"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Cfg_Pd_Mon_Form = QtWidgets.QDialog()
ui = Ui_Cfg_Pd_Mon_Form()
ui.setupUi(Cfg_Pd_Mon_Form)
Cfg_Pd_Mon_Form.show()
sys.exit(app.exec())

View File

@ -0,0 +1,161 @@
# Form implementation generated from reading ui file 'conn_settings_form.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Conn_Settings_Form(object):
def setupUi(self, Conn_Settings_Form):
Conn_Settings_Form.setObjectName("Conn_Settings_Form")
Conn_Settings_Form.resize(415, 145)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(Conn_Settings_Form.sizePolicy().hasHeightForWidth())
Conn_Settings_Form.setSizePolicy(sizePolicy)
Conn_Settings_Form.setMinimumSize(QtCore.QSize(415, 145))
Conn_Settings_Form.setMaximumSize(QtCore.QSize(415, 145))
self.horizontalLayoutWidget = QtWidgets.QWidget(parent=Conn_Settings_Form)
self.horizontalLayoutWidget.setGeometry(QtCore.QRect(20, 10, 371, 41))
self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
self.ip_addr_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
self.ip_addr_layout.setContentsMargins(0, 0, 0, 0)
self.ip_addr_layout.setObjectName("ip_addr_layout")
self.ip_addr_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ip_addr_label.sizePolicy().hasHeightForWidth())
self.ip_addr_label.setSizePolicy(sizePolicy)
self.ip_addr_label.setMaximumSize(QtCore.QSize(120, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.ip_addr_label.setFont(font)
self.ip_addr_label.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
self.ip_addr_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.ip_addr_label.setObjectName("ip_addr_label")
self.ip_addr_layout.addWidget(self.ip_addr_label)
self.addr_in_0 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_0.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_0.setMaxLength(3)
self.addr_in_0.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_0.setObjectName("addr_in_0")
self.ip_addr_layout.addWidget(self.addr_in_0)
self.dot_0_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_0_label.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_0_label.setFont(font)
self.dot_0_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_0_label.setObjectName("dot_0_label")
self.ip_addr_layout.addWidget(self.dot_0_label)
self.addr_in_1 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_1.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_1.setMaxLength(3)
self.addr_in_1.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_1.setObjectName("addr_in_1")
self.ip_addr_layout.addWidget(self.addr_in_1)
self.dot_1_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_1_label.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_1_label.setFont(font)
self.dot_1_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_1_label.setObjectName("dot_1_label")
self.ip_addr_layout.addWidget(self.dot_1_label)
self.addr_in_2 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_2.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_2.setMaxLength(3)
self.addr_in_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_2.setObjectName("addr_in_2")
self.ip_addr_layout.addWidget(self.addr_in_2)
self.dot_2_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_2_label.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_2_label.setFont(font)
self.dot_2_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_2_label.setObjectName("dot_2_label")
self.ip_addr_layout.addWidget(self.dot_2_label)
self.addr_in_3 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_3.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_3.setMaxLength(3)
self.addr_in_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_3.setObjectName("addr_in_3")
self.ip_addr_layout.addWidget(self.addr_in_3)
self.horizontalLayoutWidget_2 = QtWidgets.QWidget(parent=Conn_Settings_Form)
self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(20, 50, 371, 41))
self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2")
self.port_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2)
self.port_layout.setContentsMargins(0, 0, 0, 0)
self.port_layout.setObjectName("port_layout")
self.port_no_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_2)
self.port_no_label.setMaximumSize(QtCore.QSize(97, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.port_no_label.setFont(font)
self.port_no_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.port_no_label.setObjectName("port_no_label")
self.port_layout.addWidget(self.port_no_label)
self.port_in = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_2)
self.port_in.setMaximumSize(QtCore.QSize(50, 16777215))
self.port_in.setMaxLength(5)
self.port_in.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.port_in.setObjectName("port_in")
self.port_layout.addWidget(self.port_in)
spacerItem = QtWidgets.QSpacerItem(50, 20, QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Minimum)
self.port_layout.addItem(spacerItem)
self.port_layout.setStretch(0, 3)
self.port_layout.setStretch(2, 6)
self.horizontalLayoutWidget_4 = QtWidgets.QWidget(parent=Conn_Settings_Form)
self.horizontalLayoutWidget_4.setGeometry(QtCore.QRect(20, 90, 371, 47))
self.horizontalLayoutWidget_4.setObjectName("horizontalLayoutWidget_4")
self.buttons_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_4)
self.buttons_layout.setContentsMargins(0, 0, 0, 0)
self.buttons_layout.setObjectName("buttons_layout")
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.buttons_layout.addItem(spacerItem1)
self.connect_btn = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_4)
self.connect_btn.setObjectName("connect_btn")
self.buttons_layout.addWidget(self.connect_btn)
self.cancel_btn = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_4)
self.cancel_btn.setObjectName("cancel_btn")
self.buttons_layout.addWidget(self.cancel_btn)
self.retranslateUi(Conn_Settings_Form)
self.cancel_btn.clicked.connect(Conn_Settings_Form.reject) # type: ignore
self.connect_btn.clicked.connect(Conn_Settings_Form.accept) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Conn_Settings_Form)
Conn_Settings_Form.setTabOrder(self.addr_in_0, self.addr_in_1)
Conn_Settings_Form.setTabOrder(self.addr_in_1, self.addr_in_2)
Conn_Settings_Form.setTabOrder(self.addr_in_2, self.addr_in_3)
Conn_Settings_Form.setTabOrder(self.addr_in_3, self.port_in)
Conn_Settings_Form.setTabOrder(self.port_in, self.connect_btn)
Conn_Settings_Form.setTabOrder(self.connect_btn, self.cancel_btn)
def retranslateUi(self, Conn_Settings_Form):
_translate = QtCore.QCoreApplication.translate
Conn_Settings_Form.setWindowTitle(_translate("Conn_Settings_Form", "Connection Settings"))
self.ip_addr_label.setText(_translate("Conn_Settings_Form", "IP Address:"))
self.addr_in_0.setText(_translate("Conn_Settings_Form", "192"))
self.dot_0_label.setText(_translate("Conn_Settings_Form", "."))
self.addr_in_1.setText(_translate("Conn_Settings_Form", "168"))
self.dot_1_label.setText(_translate("Conn_Settings_Form", "."))
self.addr_in_2.setText(_translate("Conn_Settings_Form", "1"))
self.dot_2_label.setText(_translate("Conn_Settings_Form", "."))
self.addr_in_3.setText(_translate("Conn_Settings_Form", "128"))
self.port_no_label.setText(_translate("Conn_Settings_Form", "Port:"))
self.port_in.setText(_translate("Conn_Settings_Form", "1337"))
self.connect_btn.setText(_translate("Conn_Settings_Form", "Connect"))
self.cancel_btn.setText(_translate("Conn_Settings_Form", "Cancel"))

View File

@ -0,0 +1,264 @@
# Form implementation generated from reading ui file 'update_network_settings_form.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Update_Network_Settings_Form(object):
def setupUi(self, Update_Network_Settings_Form):
Update_Network_Settings_Form.setObjectName("Update_Network_Settings_Form")
Update_Network_Settings_Form.resize(415, 180)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(Update_Network_Settings_Form.sizePolicy().hasHeightForWidth())
Update_Network_Settings_Form.setSizePolicy(sizePolicy)
Update_Network_Settings_Form.setMinimumSize(QtCore.QSize(415, 180))
Update_Network_Settings_Form.setMaximumSize(QtCore.QSize(415, 180))
self.horizontalLayoutWidget = QtWidgets.QWidget(parent=Update_Network_Settings_Form)
self.horizontalLayoutWidget.setGeometry(QtCore.QRect(20, 10, 371, 41))
self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
self.ip_addr_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
self.ip_addr_layout.setContentsMargins(0, 0, 0, 0)
self.ip_addr_layout.setObjectName("ip_addr_layout")
self.ip_addr_label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ip_addr_label.sizePolicy().hasHeightForWidth())
self.ip_addr_label.setSizePolicy(sizePolicy)
self.ip_addr_label.setMaximumSize(QtCore.QSize(120, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.ip_addr_label.setFont(font)
self.ip_addr_label.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
self.ip_addr_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.ip_addr_label.setObjectName("ip_addr_label")
self.ip_addr_layout.addWidget(self.ip_addr_label)
self.addr_in_0 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_0.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_0.setMaxLength(3)
self.addr_in_0.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_0.setObjectName("addr_in_0")
self.ip_addr_layout.addWidget(self.addr_in_0)
self.dot_0_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_0_lbl.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_0_lbl.setFont(font)
self.dot_0_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_0_lbl.setObjectName("dot_0_lbl")
self.ip_addr_layout.addWidget(self.dot_0_lbl)
self.addr_in_1 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_1.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_1.setMaxLength(3)
self.addr_in_1.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_1.setObjectName("addr_in_1")
self.ip_addr_layout.addWidget(self.addr_in_1)
self.dot_1_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_1_lbl.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_1_lbl.setFont(font)
self.dot_1_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_1_lbl.setObjectName("dot_1_lbl")
self.ip_addr_layout.addWidget(self.dot_1_lbl)
self.addr_in_2 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_2.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_2.setMaxLength(3)
self.addr_in_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_2.setObjectName("addr_in_2")
self.ip_addr_layout.addWidget(self.addr_in_2)
self.dot_2_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget)
self.dot_2_lbl.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_2_lbl.setFont(font)
self.dot_2_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_2_lbl.setObjectName("dot_2_lbl")
self.ip_addr_layout.addWidget(self.dot_2_lbl)
self.addr_in_3 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget)
self.addr_in_3.setMaximumSize(QtCore.QSize(50, 16777215))
self.addr_in_3.setMaxLength(3)
self.addr_in_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.addr_in_3.setObjectName("addr_in_3")
self.ip_addr_layout.addWidget(self.addr_in_3)
self.horizontalLayoutWidget_4 = QtWidgets.QWidget(parent=Update_Network_Settings_Form)
self.horizontalLayoutWidget_4.setGeometry(QtCore.QRect(20, 130, 371, 47))
self.horizontalLayoutWidget_4.setObjectName("horizontalLayoutWidget_4")
self.buttons_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_4)
self.buttons_layout.setContentsMargins(0, 0, 0, 0)
self.buttons_layout.setObjectName("buttons_layout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.buttons_layout.addItem(spacerItem)
self.update_btn = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_4)
self.update_btn.setObjectName("update_btn")
self.buttons_layout.addWidget(self.update_btn)
self.cancel_btn = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_4)
self.cancel_btn.setObjectName("cancel_btn")
self.buttons_layout.addWidget(self.cancel_btn)
self.horizontalLayoutWidget_3 = QtWidgets.QWidget(parent=Update_Network_Settings_Form)
self.horizontalLayoutWidget_3.setGeometry(QtCore.QRect(20, 90, 371, 41))
self.horizontalLayoutWidget_3.setObjectName("horizontalLayoutWidget_3")
self.port_layout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_3)
self.port_layout_2.setContentsMargins(0, 0, 0, 0)
self.port_layout_2.setSpacing(6)
self.port_layout_2.setObjectName("port_layout_2")
self.prefix_len_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_3)
self.prefix_len_lbl.setMaximumSize(QtCore.QSize(98, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.prefix_len_lbl.setFont(font)
self.prefix_len_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.prefix_len_lbl.setObjectName("prefix_len_lbl")
self.port_layout_2.addWidget(self.prefix_len_lbl)
self.prefix_len_in = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_3)
self.prefix_len_in.setMaximumSize(QtCore.QSize(50, 16777215))
self.prefix_len_in.setMaxLength(2)
self.prefix_len_in.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.prefix_len_in.setObjectName("prefix_len_in")
self.port_layout_2.addWidget(self.prefix_len_in)
self.port_no_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_3)
self.port_no_lbl.setMinimumSize(QtCore.QSize(82, 0))
self.port_no_lbl.setMaximumSize(QtCore.QSize(50, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.port_no_lbl.setFont(font)
self.port_no_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.port_no_lbl.setObjectName("port_no_lbl")
self.port_layout_2.addWidget(self.port_no_lbl)
self.port_in = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_3)
self.port_in.setMaximumSize(QtCore.QSize(50, 16777215))
self.port_in.setMaxLength(5)
self.port_in.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.port_in.setObjectName("port_in")
self.port_layout_2.addWidget(self.port_in)
spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.port_layout_2.addItem(spacerItem1)
self.horizontalLayoutWidget_5 = QtWidgets.QWidget(parent=Update_Network_Settings_Form)
self.horizontalLayoutWidget_5.setGeometry(QtCore.QRect(20, 50, 371, 41))
self.horizontalLayoutWidget_5.setObjectName("horizontalLayoutWidget_5")
self.gateway_layout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_5)
self.gateway_layout.setContentsMargins(0, 0, 0, 0)
self.gateway_layout.setObjectName("gateway_layout")
self.gateway_lbl = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_5)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.gateway_lbl.sizePolicy().hasHeightForWidth())
self.gateway_lbl.setSizePolicy(sizePolicy)
self.gateway_lbl.setMaximumSize(QtCore.QSize(120, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
self.gateway_lbl.setFont(font)
self.gateway_lbl.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
self.gateway_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.gateway_lbl.setObjectName("gateway_lbl")
self.gateway_layout.addWidget(self.gateway_lbl)
self.gateway_in_0 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_5)
self.gateway_in_0.setMaximumSize(QtCore.QSize(50, 16777215))
self.gateway_in_0.setMaxLength(3)
self.gateway_in_0.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.gateway_in_0.setObjectName("gateway_in_0")
self.gateway_layout.addWidget(self.gateway_in_0)
self.dot_0_lbl_2 = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_5)
self.dot_0_lbl_2.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_0_lbl_2.setFont(font)
self.dot_0_lbl_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_0_lbl_2.setObjectName("dot_0_lbl_2")
self.gateway_layout.addWidget(self.dot_0_lbl_2)
self.gateway_in_1 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_5)
self.gateway_in_1.setMaximumSize(QtCore.QSize(50, 16777215))
self.gateway_in_1.setMaxLength(3)
self.gateway_in_1.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.gateway_in_1.setObjectName("gateway_in_1")
self.gateway_layout.addWidget(self.gateway_in_1)
self.dot_1_lbl_2 = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_5)
self.dot_1_lbl_2.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_1_lbl_2.setFont(font)
self.dot_1_lbl_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_1_lbl_2.setObjectName("dot_1_lbl_2")
self.gateway_layout.addWidget(self.dot_1_lbl_2)
self.gateway_in_2 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_5)
self.gateway_in_2.setMaximumSize(QtCore.QSize(50, 16777215))
self.gateway_in_2.setMaxLength(3)
self.gateway_in_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.gateway_in_2.setObjectName("gateway_in_2")
self.gateway_layout.addWidget(self.gateway_in_2)
self.dot_2_lbl_2 = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_5)
self.dot_2_lbl_2.setMaximumSize(QtCore.QSize(10, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(False)
font.setKerning(True)
self.dot_2_lbl_2.setFont(font)
self.dot_2_lbl_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.dot_2_lbl_2.setObjectName("dot_2_lbl_2")
self.gateway_layout.addWidget(self.dot_2_lbl_2)
self.gateway_in_3 = QtWidgets.QLineEdit(parent=self.horizontalLayoutWidget_5)
self.gateway_in_3.setMaximumSize(QtCore.QSize(50, 16777215))
self.gateway_in_3.setMaxLength(3)
self.gateway_in_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.gateway_in_3.setObjectName("gateway_in_3")
self.gateway_layout.addWidget(self.gateway_in_3)
self.retranslateUi(Update_Network_Settings_Form)
self.cancel_btn.clicked.connect(Update_Network_Settings_Form.reject) # type: ignore
self.update_btn.clicked.connect(Update_Network_Settings_Form.accept) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Update_Network_Settings_Form)
Update_Network_Settings_Form.setTabOrder(self.addr_in_0, self.addr_in_1)
Update_Network_Settings_Form.setTabOrder(self.addr_in_1, self.addr_in_2)
Update_Network_Settings_Form.setTabOrder(self.addr_in_2, self.addr_in_3)
Update_Network_Settings_Form.setTabOrder(self.addr_in_3, self.gateway_in_0)
Update_Network_Settings_Form.setTabOrder(self.gateway_in_0, self.gateway_in_1)
Update_Network_Settings_Form.setTabOrder(self.gateway_in_1, self.gateway_in_2)
Update_Network_Settings_Form.setTabOrder(self.gateway_in_2, self.gateway_in_3)
Update_Network_Settings_Form.setTabOrder(self.gateway_in_3, self.prefix_len_in)
Update_Network_Settings_Form.setTabOrder(self.prefix_len_in, self.port_in)
Update_Network_Settings_Form.setTabOrder(self.port_in, self.update_btn)
Update_Network_Settings_Form.setTabOrder(self.update_btn, self.cancel_btn)
def retranslateUi(self, Update_Network_Settings_Form):
_translate = QtCore.QCoreApplication.translate
Update_Network_Settings_Form.setWindowTitle(_translate("Update_Network_Settings_Form", "Update Network Settings"))
self.ip_addr_label.setText(_translate("Update_Network_Settings_Form", "IP Address:"))
self.addr_in_0.setText(_translate("Update_Network_Settings_Form", "192"))
self.dot_0_lbl.setText(_translate("Update_Network_Settings_Form", "."))
self.addr_in_1.setText(_translate("Update_Network_Settings_Form", "168"))
self.dot_1_lbl.setText(_translate("Update_Network_Settings_Form", "."))
self.addr_in_2.setText(_translate("Update_Network_Settings_Form", "1"))
self.dot_2_lbl.setText(_translate("Update_Network_Settings_Form", "."))
self.addr_in_3.setText(_translate("Update_Network_Settings_Form", "128"))
self.update_btn.setText(_translate("Update_Network_Settings_Form", "Update"))
self.cancel_btn.setText(_translate("Update_Network_Settings_Form", "Cancel"))
self.prefix_len_lbl.setText(_translate("Update_Network_Settings_Form", "Prefix Length:"))
self.prefix_len_in.setText(_translate("Update_Network_Settings_Form", "24"))
self.port_no_lbl.setText(_translate("Update_Network_Settings_Form", "Port:"))
self.port_in.setText(_translate("Update_Network_Settings_Form", "1337"))
self.gateway_lbl.setText(_translate("Update_Network_Settings_Form", "Gateway:"))
self.gateway_in_0.setText(_translate("Update_Network_Settings_Form", "192"))
self.dot_0_lbl_2.setText(_translate("Update_Network_Settings_Form", "."))
self.gateway_in_1.setText(_translate("Update_Network_Settings_Form", "168"))
self.dot_1_lbl_2.setText(_translate("Update_Network_Settings_Form", "."))
self.gateway_in_2.setText(_translate("Update_Network_Settings_Form", "1"))
self.dot_2_lbl_2.setText(_translate("Update_Network_Settings_Form", "."))
self.gateway_in_3.setText(_translate("Update_Network_Settings_Form", "1"))

View File

@ -0,0 +1,614 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Update_Network_Settings_Form</class>
<widget class="QDialog" name="Update_Network_Settings_Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>415</width>
<height>180</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>415</width>
<height>180</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>415</width>
<height>180</height>
</size>
</property>
<property name="windowTitle">
<string>Update Network Settings</string>
</property>
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="ip_addr_layout">
<item>
<widget class="QLabel" name="ip_addr_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>IP Address:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_0">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>192</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_0_lbl">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_1">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>168</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_1_lbl">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_2">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_2_lbl">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addr_in_3">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>128</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_4">
<property name="geometry">
<rect>
<x>20</x>
<y>130</y>
<width>371</width>
<height>47</height>
</rect>
</property>
<layout class="QHBoxLayout" name="buttons_layout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="update_btn">
<property name="text">
<string>Update</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel_btn">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_3">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="port_layout_2" stretch="0,0,0,0,0">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QLabel" name="prefix_len_lbl">
<property name="maximumSize">
<size>
<width>98</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Prefix Length:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="prefix_len_in">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>24</string>
</property>
<property name="maxLength">
<number>2</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="port_no_lbl">
<property name="minimumSize">
<size>
<width>82</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Port:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="port_in">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1337</string>
</property>
<property name="maxLength">
<number>5</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_5">
<property name="geometry">
<rect>
<x>20</x>
<y>50</y>
<width>371</width>
<height>41</height>
</rect>
</property>
<layout class="QHBoxLayout" name="gateway_layout">
<item>
<widget class="QLabel" name="gateway_lbl">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Gateway:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="gateway_in_0">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>192</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_0_lbl_2">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="gateway_in_1">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>168</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_1_lbl_2">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="gateway_in_2">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dot_2_lbl_2">
<property name="maximumSize">
<size>
<width>10</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="gateway_in_3">
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="maxLength">
<number>3</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>addr_in_0</tabstop>
<tabstop>addr_in_1</tabstop>
<tabstop>addr_in_2</tabstop>
<tabstop>addr_in_3</tabstop>
<tabstop>gateway_in_0</tabstop>
<tabstop>gateway_in_1</tabstop>
<tabstop>gateway_in_2</tabstop>
<tabstop>gateway_in_3</tabstop>
<tabstop>prefix_len_in</tabstop>
<tabstop>port_in</tabstop>
<tabstop>update_btn</tabstop>
<tabstop>cancel_btn</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>cancel_btn</sender>
<signal>clicked()</signal>
<receiver>Update_Network_Settings_Form</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>340</x>
<y>140</y>
</hint>
<hint type="destinationlabel">
<x>364</x>
<y>96</y>
</hint>
</hints>
</connection>
<connection>
<sender>update_btn</sender>
<signal>clicked()</signal>
<receiver>Update_Network_Settings_Form</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>258</x>
<y>147</y>
</hint>
<hint type="destinationlabel">
<x>237</x>
<y>99</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,194 @@
"""
The MIT License (MIT)
Copyright (c) 2012-2014 Alexander Turkin
Copyright (c) 2014 William Hallatt
Copyright (c) 2015 Jacob Dawid
Copyright (c) 2016 Luca Weiss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import math
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class QtWaitingSpinner(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# WAS IN initialize()
self._color = QColor(Qt.GlobalColor.black)
self._roundness = 100.0
self._minimumTrailOpacity = 3.14159265358979323846
self._trailFadePercentage = 80.0
self._revolutionsPerSecond = 1.57079632679489661923
self._numberOfLines = 20
self._lineLength = 5
self._lineWidth = 2
self._innerRadius = 5
self._currentCounter = 0
self._timer = QTimer(self)
self._timer.timeout.connect(self.rotate)
self.updateSize()
self.updateTimer()
# END initialize()
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
def paintEvent(self, QPaintEvent):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.GlobalColor.transparent)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
if self._currentCounter >= self._numberOfLines:
self._currentCounter = 0
painter.setPen(Qt.PenStyle.NoPen)
for i in range(0, self._numberOfLines):
painter.save()
painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength)
rotateAngle = float(360 * i) / float(self._numberOfLines)
painter.rotate(rotateAngle)
painter.translate(self._innerRadius, 0)
distance = self.lineCountDistanceFromPrimary(i, self._currentCounter, self._numberOfLines)
color = self.currentLineColor(distance, self._numberOfLines, self._trailFadePercentage,
self._minimumTrailOpacity, self._color)
painter.setBrush(color)
painter.drawRoundedRect(QRect(0, int(-self._lineWidth / 2), self._lineLength, self._lineWidth), self._roundness,
self._roundness, Qt.SizeMode.RelativeSize)
painter.restore()
def start(self):
if not self._timer.isActive():
self._timer.start()
self._currentCounter = 0
def stop(self):
if self._timer.isActive():
self._timer.stop()
self._currentCounter = 0
def setNumberOfLines(self, lines):
self._numberOfLines = lines
self._currentCounter = 0
self.updateTimer()
def setLineLength(self, length):
self._lineLength = length
self.updateSize()
def setLineWidth(self, width):
self._lineWidth = width
self.updateSize()
def setInnerRadius(self, radius):
self._innerRadius = radius
self.updateSize()
def color(self):
return self._color
def roundness(self):
return self._roundness
def minimumTrailOpacity(self):
return self._minimumTrailOpacity
def trailFadePercentage(self):
return self._trailFadePercentage
def revolutionsPersSecond(self):
return self._revolutionsPerSecond
def numberOfLines(self):
return self._numberOfLines
def lineLength(self):
return self._lineLength
def lineWidth(self):
return self._lineWidth
def innerRadius(self):
return self._innerRadius
def setRoundness(self, roundness):
self._roundness = max(0.0, min(100.0, roundness))
def setColor(self, color=Qt.GlobalColor.black):
self._color = QColor(color)
def setRevolutionsPerSecond(self, revolutionsPerSecond):
self._revolutionsPerSecond = revolutionsPerSecond
self.updateTimer()
def setTrailFadePercentage(self, trail):
self._trailFadePercentage = trail
def setMinimumTrailOpacity(self, minimumTrailOpacity):
self._minimumTrailOpacity = minimumTrailOpacity
def rotate(self):
self._currentCounter += 1
if self._currentCounter >= self._numberOfLines:
self._currentCounter = 0
self.update()
def updateSize(self):
self.size = (self._innerRadius + self._lineLength) * 2
self.setFixedSize(self.size, self.size)
def updateTimer(self):
self._timer.setInterval(int(1000 / (self._numberOfLines * self._revolutionsPerSecond)))
def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines):
distance = primary - current
if distance < 0:
distance += totalNrOfLines
return distance
def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput):
color = QColor(colorinput)
if countDistance == 0:
return color
minAlphaF = minOpacity / 100.0
distanceThreshold = int(math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0))
if countDistance > distanceThreshold:
color.setAlphaF(minAlphaF)
else:
alphaDiff = color.alphaF() - minAlphaF
gradient = alphaDiff / float(distanceThreshold + 1)
resultAlpha = color.alphaF() - gradient * countDistance
# If alpha is out of bounds, clip it.
resultAlpha = min(1.0, max(0.0, resultAlpha))
color.setAlphaF(resultAlpha)
return color
if __name__ == '__main__':
app = QApplication([])
waiting_spinner = QtWaitingSpinner()
waiting_spinner.show()
waiting_spinner.start()
app.exec()

64
rustfmt.toml Normal file
View File

@ -0,0 +1,64 @@
max_width = 120
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"
use_small_heuristics = "Default"
indent_style = "Block"
wrap_comments = false
format_code_in_doc_comments = false
comment_width = 100
normalize_comments = false
normalize_doc_attributes = false
format_strings = true
format_macro_matchers = true
format_macro_bodies = true
empty_item_single_line = true
struct_lit_single_line = true
fn_single_line = false
where_single_line = true
imports_indent = "Visual"
imports_layout = "Mixed"
imports_granularity="Crate"
group_imports = "StdExternalCrate"
reorder_imports = true
reorder_modules = true
reorder_impl_items = false
type_punctuation_density = "Wide"
space_before_colon = false
space_after_colon = true
spaces_around_ranges = false
binop_separator = "Front"
remove_nested_parens = true
combine_control_expr = true
overflow_delimited_expr = false
struct_field_align_threshold = 0
enum_discrim_align_threshold = 0
match_arm_blocks = true
match_arm_leading_pipes = "Never"
force_multiline_blocks = false
fn_params_layout = "Tall"
brace_style = "SameLineWhere"
control_brace_style = "AlwaysSameLine"
trailing_semicolon = true
trailing_comma = "Vertical"
match_block_trailing_comma = false
blank_lines_upper_bound = 1
blank_lines_lower_bound = 0
edition = "2018"
version = "Two"
inline_attribute_width = 0
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
condense_wildcard_suffixes = false
color = "Auto"
unstable_features = false
disable_all_formatting = false
skip_children = false
hide_parse_errors = false
error_on_line_overflow = false
error_on_unformatted = false
ignore = []
emit_mode = "Files"
make_backup = false

View File

@ -1,31 +1,30 @@
use super::{delay, gpio, usb};
use crate::laser_diode::current_sources::*;
use crate::network::network;
use crate::network::network::EthernetPeripherals;
use fugit::ExtU32; use fugit::ExtU32;
use log::info; use log::{debug, info};
use stm32f4xx_hal::{ use stm32f4xx_hal::{pac::{CorePeripherals, Peripherals},
pac::{CorePeripherals, Peripherals},
rcc::RccExt, rcc::RccExt,
time::MegaHertz, time::MegaHertz,
watchdog::IndependentWatchdog, timer::TimerExt,
}; watchdog::IndependentWatchdog};
use systick_monotonic::Systick; use uom::si::{electric_current::{ampere, milliampere},
f32::ElectricCurrent};
use super::{gpio, sys_timer, usb};
use crate::{device::{flash_store::{self, FlashStore},
hw_rev::HWRev},
laser_diode::{laser_diode::LdDrive, ld_ctrl::*},
net::net::{IpSettings, ServerHandle},
thermostat::{max1968::MAX1968, thermostat::Thermostat},
DeviceSettings};
#[cfg(not(feature = "semihosting"))] #[cfg(not(feature = "semihosting"))]
const WATCHDOG_PERIOD: u32 = 1000; const WATCHDOG_PERIOD: u32 = 4000;
#[cfg(feature = "semihosting")] #[cfg(feature = "semihosting")]
const WATCHDOG_PERIOD: u32 = 30000; const WATCHDOG_PERIOD: u32 = 30000;
pub fn bootup( pub fn bootup(
mut core_perif: CorePeripherals, mut core_perif: CorePeripherals,
perif: Peripherals, perif: Peripherals,
server_storage: &'static mut network::ServerStorage, ) -> (IndependentWatchdog, FlashStore, HWRev, LdDrive, Thermostat) {
) -> (
IndependentWatchdog,
Systick<1000_u32>,
network::ServerHandle,
) {
core_perif.SCB.enable_icache(); core_perif.SCB.enable_icache();
core_perif.SCB.enable_dcache(&mut core_perif.CPUID); core_perif.SCB.enable_dcache(&mut core_perif.CPUID);
@ -36,50 +35,90 @@ pub fn bootup(
.use_hse(MegaHertz::from_raw(8).convert()) .use_hse(MegaHertz::from_raw(8).convert())
.sysclk(MegaHertz::from_raw(168).convert()) .sysclk(MegaHertz::from_raw(168).convert())
.hclk(MegaHertz::from_raw(168).convert()) .hclk(MegaHertz::from_raw(168).convert())
.pclk1(MegaHertz::from_raw(32).convert()) .pclk1(MegaHertz::from_raw(42).convert())
.pclk2(MegaHertz::from_raw(64).convert()) .pclk2(MegaHertz::from_raw(84).convert())
.freeze(); .freeze();
let systick = Systick::new(core_perif.SYST, clocks.hclk().to_Hz()); sys_timer::setup(core_perif.SYST, clocks);
let delay = delay::AsmDelay::new(clocks.sysclk().to_Hz());
let (eth_pins, usb, current_source_phy, mdio, mdc) = gpio::setup( let (mut hw_rev, eth_pins, eth_mgmt_pins, usb, current_source_phy, ad7172_phy, max1968_phy, pd_mon_phy) =
gpio::setup(
clocks, clocks,
perif.TIM4,
perif.GPIOA, perif.GPIOA,
perif.GPIOB, perif.GPIOB,
perif.GPIOC, perif.GPIOC,
perif.GPIOD, perif.GPIOD,
perif.GPIOE,
perif.SPI1,
perif.SPI2, perif.SPI2,
perif.SPI3,
perif.OTG_FS_GLOBAL, perif.OTG_FS_GLOBAL,
perif.OTG_FS_DEVICE, perif.OTG_FS_DEVICE,
perif.OTG_FS_PWRCLK, perif.OTG_FS_PWRCLK,
); );
let ethernet = EthernetPeripherals { usb::State::setup(usb);
debug!("Setting up TEC");
let tec_driver = MAX1968::new(max1968_phy, perif.ADC1, perif.ADC2, perif.DMA2);
let mut thermostat = Thermostat::new(tec_driver, ad7172_phy);
thermostat.setup();
thermostat.calibrate_dac_value();
thermostat.set_i(ElectricCurrent::new::<ampere>(0.0));
debug!("Setting up Laser Driver");
let current_source = LdCtrl::new(current_source_phy);
let mut laser = LdDrive::new(current_source, perif.ADC3, perif.TIM2.counter(&clocks), pd_mon_phy);
laser.setup();
laser.ld_open();
laser.ld_set_i(ElectricCurrent::new::<ampere>(0.0));
laser.set_pd_i_limit(ElectricCurrent::new::<milliampere>(2.5));
laser.set_pd_mon_calibrated_vdda(thermostat.get_calibrated_vdda());
debug!("Setting up Internal Flash Driver");
let flash_store = flash_store::store(perif.FLASH);
let mut ip_settings: IpSettings = IpSettings::default();
let device_settings: DeviceSettings;
match flash_store.read_value("Device") {
Ok(Some(config)) => {
device_settings = config;
ip_settings = device_settings.ip_settings;
debug!("Found Device Settings");
}
Ok(None) => {
debug!("Flash does not have IP Settings");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
}
}
debug!("Setting up ETH");
let mac_addr = hw_rev.get_mac_address();
let ethernet_parts_in = stm32_eth::PartsIn {
dma: perif.ETHERNET_DMA, dma: perif.ETHERNET_DMA,
mac: perif.ETHERNET_MAC, mac: perif.ETHERNET_MAC,
mmc: perif.ETHERNET_MMC, mmc: perif.ETHERNET_MMC,
ptp: perif.ETHERNET_PTP,
}; };
let server = network::ServerHandle::new(eth_pins, ethernet, clocks, server_storage, mdio, mdc); ServerHandle::new(
// let (serverhandle, iface) = network::setup(eth_pins, ethernet, clocks); eth_pins,
eth_mgmt_pins,
usb::State::setup(usb); ethernet_parts_in,
clocks,
let mut laser = CurrentSource { perif.TIM5.counter(&clocks),
phy: current_source_phy, mac_addr,
setting: CurrentSourceSettings { ip_settings,
output_current: 0.0, );
},
};
laser.setup(delay);
laser.set_current(0.1).unwrap();
debug!("Setting Watchdog");
let mut wd = IndependentWatchdog::new(perif.IWDG); let mut wd = IndependentWatchdog::new(perif.IWDG);
wd.start(WATCHDOG_PERIOD.millis()); wd.start(WATCHDOG_PERIOD.millis());
wd.feed(); wd.feed();
info!("Kirdy setup complete"); info!("Kirdy setup complete");
(wd, systick, server) (wd, flash_store, hw_rev, laser, thermostat)
} }

View File

@ -1,46 +0,0 @@
//! Basic blocking delay
//!
//! This module provides a basic asm-based blocking delay.
//! Borrowed from our good friends at Quartiq:
//! https://github.com/quartiq/stabilizer/blob/master/src/hardware/delay.rs
//!
use stm32f4xx_hal::hal::blocking::delay::{DelayMs, DelayUs};
// use stm32f4xx_hal::block;
/// A basic delay implementation.
pub struct AsmDelay {
frequency_us: u32,
frequency_ms: u32,
}
impl AsmDelay {
/// Create a new delay.
///
/// # Args
/// * `freq` - The CPU core frequency.
pub fn new(freq: u32) -> AsmDelay {
AsmDelay {
frequency_us: (freq / 1_000_000),
frequency_ms: (freq / 1_000),
}
}
}
impl<U> DelayUs<U> for AsmDelay
where
U: Into<u32>,
{
fn delay_us(&mut self, us: U) {
cortex_m::asm::delay(self.frequency_us * us.into())
}
}
impl<U> DelayMs<U> for AsmDelay
where
U: Into<u32>,
{
fn delay_ms(&mut self, ms: U) {
cortex_m::asm::delay(self.frequency_ms * ms.into())
}
}

47
src/device/dfu.rs Normal file
View File

@ -0,0 +1,47 @@
use core::arch::asm;
use cortex_m_rt::pre_init;
use stm32f4xx_hal::pac::{RCC, SYSCFG};
const DFU_TRIG_MSG: u32 = 0xDECAFBAD;
extern "C" {
// This symbol comes from memory.x
static mut _dfu_msg: u32;
}
pub unsafe fn set_dfu_trigger() {
_dfu_msg = DFU_TRIG_MSG;
}
/// Called by reset handler in lib.rs immediately after reset.
/// This function should not be called outside of reset handler as
/// bootloader expects MCU to be in reset state when called.
#[cfg(target_arch = "arm")]
#[pre_init]
unsafe fn __pre_init() {
if _dfu_msg == DFU_TRIG_MSG {
_dfu_msg = 0x00000000;
// Enable system config controller clock
let rcc = &*RCC::ptr();
rcc.apb2enr.modify(|_, w| w.syscfgen().set_bit());
// Bypass BOOT pins and remap bootloader to 0x00000000
let syscfg = &*SYSCFG::ptr();
syscfg.memrm.write(|w| w.mem_mode().bits(0b01));
// Impose instruction and memory barriers
cortex_m::asm::isb();
cortex_m::asm::dsb();
asm!(
// Set stack pointer to bootloader location
"LDR R0, =0x1FFF0000",
"LDR SP,[R0, #0]",
// Jump to bootloader
"LDR R0,[R0, #4]",
"BX R0",
);
}
}

58
src/device/flash_store.rs Normal file
View File

@ -0,0 +1,58 @@
use log::error;
use sfkv::{Store, StoreBackend};
use stm32f4xx_hal::{flash::{Error, FlashExt},
pac::FLASH};
// Last flash sector is used to avoid overwriting the code in flash.
pub const FLASH_SECTOR: u8 = 11;
pub const FLASH_SECTOR_11_OFFSET: u32 = 0xE0000;
/// Only 16 KiB out of 128KiB in the Sector is used to save RAM
pub const RESERVED_MEMORY: usize = 0x4000;
static mut BACKUP_SPACE: [u8; RESERVED_MEMORY] = [0; RESERVED_MEMORY];
pub struct FlashBackend {
flash: FLASH,
}
fn get_offset() -> usize {
FLASH_SECTOR_11_OFFSET as usize
}
impl StoreBackend for FlashBackend {
type Data = [u8];
fn data(&self) -> &Self::Data {
&self.flash.read()[get_offset()..(get_offset() + RESERVED_MEMORY)]
}
type Error = Error;
fn erase(&mut self) -> Result<(), Self::Error> {
self.flash.unlocked().erase(FLASH_SECTOR)
}
fn program(&mut self, offset: usize, payload: &[u8]) -> Result<(), Self::Error> {
self.flash.unlocked().program(get_offset() + offset, payload.iter())
}
fn backup_space(&self) -> &'static mut [u8] {
unsafe { BACKUP_SPACE.as_mut() }
}
}
pub type FlashStore = Store<FlashBackend>;
pub fn store(flash: FLASH) -> FlashStore {
let backend = FlashBackend { flash };
let mut store = FlashStore::new(backend);
match store.get_bytes_used() {
Ok(_) => {}
Err(e) => {
error!("corrupt store, erasing. error: {:?}", e);
let _ = store.erase().map_err(|e| error!("flash erase failed: {:?}", e));
}
}
store
}

View File

@ -1,46 +1,71 @@
use crate::laser_diode::current_sources::*;
use fugit::RateExtU32;
use stm32_eth::EthPins; use stm32_eth::EthPins;
use stm32f4xx_hal::{ use stm32f4xx_hal::{gpio::{alt::otg_fs::{Dm, Dp},
gpio::{Alternate, GpioExt, Speed}, gpioa::*,
gpiob::*,
gpioc::*,
GpioExt, Input, Speed},
otg_fs::USB, otg_fs::USB,
pac::{GPIOA, GPIOB, GPIOC, GPIOD, OTG_FS_DEVICE, OTG_FS_GLOBAL, OTG_FS_PWRCLK, SPI2}, pac::{GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, OTG_FS_DEVICE, OTG_FS_GLOBAL, OTG_FS_PWRCLK, SPI1, SPI2,
SPI3, TIM4},
rcc::Clocks, rcc::Clocks,
spi,
spi::{NoMiso, Spi}, spi::{NoMiso, Spi},
}; timer::{pwm::PwmExt, Channel1, Channel2, Channel3}};
use crate::network::network::EthernetPins; use crate::{device::hw_rev::{HWRev, HwRevPins},
laser_diode::{ld_ctrl::{self, LdCtrlPhy},
ld_pwr_exc_protector::LdPwrExcProtectorPhy,
max5719},
net::net::EthernetMgmtPins,
thermostat::{ad5680, ad7172,
max1968::{self, MAX1968Phy, MAX1968PinSet, PWM_FREQ_KHZ}}};
pub type EthernetPins = EthPins<PA1<Input>, PA7<Input>, PB11<Input>, PB12<Input>, PB13<Input>, PC4<Input>, PC5<Input>>;
pub fn setup( pub fn setup(
clocks: Clocks, clocks: Clocks,
tim4: TIM4,
gpioa: GPIOA, gpioa: GPIOA,
gpiob: GPIOB, gpiob: GPIOB,
gpioc: GPIOC, gpioc: GPIOC,
gpiod: GPIOD, gpiod: GPIOD,
gpioe: GPIOE,
spi1: SPI1,
spi2: SPI2, spi2: SPI2,
spi3: SPI3,
otg_fs_global: OTG_FS_GLOBAL, otg_fs_global: OTG_FS_GLOBAL,
otg_fs_device: OTG_FS_DEVICE, otg_fs_device: OTG_FS_DEVICE,
otg_fs_pwrclk: OTG_FS_PWRCLK, otg_fs_pwrclk: OTG_FS_PWRCLK,
) -> ( ) -> (
HWRev,
EthernetPins, EthernetPins,
EthernetMgmtPins,
USB, USB,
CurrentSourcePhyConstruct<CurrentSourcePhyCh0>, LdCtrlPhy<ld_ctrl::Channel0>,
stm32f4xx_hal::gpio::PA2<Alternate<11>>, ad7172::AdcPhy,
stm32f4xx_hal::gpio::PC1<Alternate<11>>, // photo_diode_phy, MAX1968Phy<max1968::Channel0>,
// thermostat_phy LdPwrExcProtectorPhy,
) { ) {
let gpioa = gpioa.split(); let gpioa = gpioa.split();
let gpiob = gpiob.split(); let gpiob = gpiob.split();
let gpioc = gpioc.split(); let gpioc = gpioc.split();
let gpiod = gpiod.split(); let gpiod = gpiod.split();
let gpioe = gpioe.split();
let mut hw_rev = HWRev::detect_hw_rev(HwRevPins {
h0: gpioe.pe8.into_input(),
h1: gpioe.pe9.into_input(),
h2: gpioe.pe10.into_input(),
h3: gpioe.pe11.into_input(),
});
hw_rev.startup_delay_before_gpio_init();
let usb = USB { let usb = USB {
usb_global: otg_fs_global, usb_global: otg_fs_global,
usb_device: otg_fs_device, usb_device: otg_fs_device,
usb_pwrclk: otg_fs_pwrclk, usb_pwrclk: otg_fs_pwrclk,
pin_dm: gpioa.pa11.into_alternate(), pin_dm: Dm::PA11(gpioa.pa11.into_alternate()),
pin_dp: gpioa.pa12.into_alternate(), pin_dp: Dp::PA12(gpioa.pa12.into_alternate()),
hclk: clocks.hclk(), hclk: clocks.hclk(),
}; };
@ -54,31 +79,86 @@ pub fn setup(
rx_d1: gpioc.pc5, rx_d1: gpioc.pc5,
}; };
let mut mdio = gpioa.pa2.into_alternate::<11>(); let mut eth_mgmt_pins = EthernetMgmtPins {
let mut mdc = gpioc.pc1.into_alternate::<11>(); mdio: gpioa.pa2.into_alternate::<11>(),
mdio.set_speed(Speed::VeryHigh); mdc: gpioc.pc1.into_alternate::<11>(),
mdc.set_speed(Speed::VeryHigh); };
eth_mgmt_pins.mdio.set_speed(Speed::VeryHigh);
eth_mgmt_pins.mdc.set_speed(Speed::VeryHigh);
let current_source_phy = CurrentSourcePhyConstruct { let current_source_phy = LdCtrlPhy {
max5719_spi: Spi::new( dac: max5719::Dac::new(
Spi::new(
spi2, spi2,
( (gpiob.pb10.into_alternate(), NoMiso::new(), gpiob.pb15.into_alternate()),
gpiob.pb10.into_alternate(), max5719::SPI_MODE,
NoMiso {}, max5719::SPI_CLOCK_MHZ.convert(),
gpiob.pb15.into_alternate(),
),
spi::Mode {
polarity: spi::Polarity::IdleLow,
phase: spi::Phase::CaptureOnFirstTransition,
},
10_u32.MHz(),
&clocks, &clocks,
), ),
max5719_load: gpiob.pb14.into_push_pull_output(), gpiod.pd8.into_push_pull_output(),
max5719_cs: gpiod.pd8.into_push_pull_output(), gpiob.pb14.into_push_pull_output(),
current_source_ldo_en: gpiod.pd9.into_push_pull_output(), ),
current_source_short: gpioa.pa4.into_push_pull_output(), current_source_short_pin: gpioa.pa4.into_push_pull_output(),
termination_status_pin: gpiod.pd7.internal_pull_up(true),
}; };
(eth_pins, usb, current_source_phy, mdio, mdc) let pd_mon_phy = LdPwrExcProtectorPhy {
_pd_mon_ch0: gpioa.pa3.into_analog(),
pwr_en_ch0: gpiod.pd9.into_push_pull_output(),
};
let pwm_chs = (
Channel1::new(gpiob.pb6),
Channel2::new(gpiob.pb7),
Channel3::new(gpiob.pb8),
);
let (max_i_neg0, max_v0, max_i_pos0) = tim4.pwm_hz(pwm_chs, PWM_FREQ_KHZ.convert(), &clocks).split();
let max1968_phy = MAX1968Phy::new(MAX1968PinSet {
dac: ad5680::Dac::new(
Spi::new(
spi1,
(gpiob.pb3.into_alternate(), NoMiso::new(), gpiob.pb5.into_alternate()),
ad5680::SPI_MODE,
ad5680::SPI_CLOCK_MHZ.convert(),
&clocks,
),
gpiob.pb4.into_push_pull_output(),
),
shdn: gpioa.pa5.into_push_pull_output(),
vref_pin: gpioa.pa6.into_analog(),
itec_pin: gpiob.pb1.into_analog(),
dac_feedback_pin: gpioc.pc0.into_analog(),
vtec_pin: gpiob.pb0.into_analog(),
max_v: max_v0,
max_i_pos: max_i_pos0,
max_i_neg: max_i_neg0,
});
let ad7172_phy = ad7172::Adc::new(
Spi::new(
spi3,
(
gpioc.pc10.into_alternate(),
gpioc.pc11.into_alternate(),
gpioc.pc12.into_alternate(),
),
ad7172::SPI_MODE,
ad7172::SPI_CLOCK_MHZ.convert(),
&clocks,
),
gpioa.pa15.into_push_pull_output(),
)
.unwrap();
(
hw_rev,
eth_pins,
eth_mgmt_pins,
usb,
current_source_phy,
ad7172_phy,
max1968_phy,
pd_mon_phy,
)
} }

68
src/device/hw_rev.rs Normal file
View File

@ -0,0 +1,68 @@
use crc::{Crc, CRC_24_BLE};
use miniconf::Tree;
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::{gpio::{Input, PE10, PE11, PE8, PE9},
signature};
use crate::device::sys_timer::sleep;
pub struct HwRevPins {
pub h0: PE8<Input>,
pub h1: PE9<Input>,
pub h2: PE10<Input>,
pub h3: PE11<Input>,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct HWRev {
pub major: u8,
pub minor: u8,
}
impl HWRev {
pub fn detect_hw_rev(hwrev_pins: HwRevPins) -> Self {
let (h0, h1, h2, h3) = (
hwrev_pins.h0.is_high(),
hwrev_pins.h1.is_high(),
hwrev_pins.h2.is_high(),
hwrev_pins.h3.is_high(),
);
match (h0, h1, h2, h3) {
(true, true, true, true) => HWRev { major: 0, minor: 3 },
(_, _, _, _) => HWRev { major: 0, minor: 0 },
}
}
/// On Rev0_3, during power up, digital power rails are stabilized way before analog power rails
/// This causes improper initialization on any peripherals requiring calibrations
/// See Issue #32 on Kirdy Hw Repo
pub fn startup_delay_before_gpio_init(&mut self) {
if self.major == 0 && self.minor == 3 {
sleep(5000);
}
}
/// On Rev0_3, it lacks pre-allocated Mac Addresses accessible on PCB.
/// This functions generate a random Mac Address with 96bit unique UUID inside STM32
/// See Issue #36 on Kirdy Hw Repo
pub fn get_mac_address(&mut self) -> [u8; 6] {
if self.major == 0 && self.minor == 3 {
let uid = signature::Uid::get();
let mut uid_data: [u8; 12] = [0; 12];
uid_data[0] = uid.x() as u8;
uid_data[1] = (uid.x() >> 8) as u8;
uid_data[2] = uid.y() as u8;
uid_data[3] = (uid.y() >> 8) as u8;
uid_data[4..11].clone_from_slice(uid.lot_num().as_bytes());
let crc: Crc<u32> = Crc::<u32>::new(&CRC_24_BLE);
let mut digest = crc.digest();
digest.update(&uid_data);
let crc24 = digest.finalize();
[0x02, 0xE0, 0xD5, (crc24 >> 16) as u8, (crc24 >> 8) as u8, (crc24 as u8)]
} else {
unimplemented!()
}
}
}

View File

@ -3,13 +3,15 @@ pub fn init_log() {
use super::usb; use super::usb;
static USB_LOGGER: usb::Logger = usb::Logger; static USB_LOGGER: usb::Logger = usb::Logger;
let _ = log::set_logger(&USB_LOGGER); let _ = log::set_logger(&USB_LOGGER);
log::set_max_level(log::LevelFilter::Debug); // log::set_max_level(log::LevelFilter::Debug);
log::set_max_level(log::LevelFilter::Trace);
} }
#[cfg(feature = "RTT")] #[cfg(feature = "RTT")]
pub fn init_log() { pub fn init_log() {
use super::rtt_logger;
use rtt_target::rtt_init_print; use rtt_target::rtt_init_print;
use super::rtt_logger;
static RTT_LOGGER: rtt_logger::Logger = rtt_logger::Logger; static RTT_LOGGER: rtt_logger::Logger = rtt_logger::Logger;
rtt_init_print!(); rtt_init_print!();
let _ = log::set_logger(&RTT_LOGGER); let _ = log::set_logger(&RTT_LOGGER);
@ -18,8 +20,8 @@ pub fn init_log() {
#[cfg(feature = "semihosting")] #[cfg(feature = "semihosting")]
pub fn init_log() { pub fn init_log() {
use cortex_m_log::log::{init, Logger}; use cortex_m_log::{log::{init, Logger},
use cortex_m_log::printer::semihosting::{hio::HStdout, InterruptOk}; printer::semihosting::{hio::HStdout, InterruptOk}};
use log::LevelFilter; use log::LevelFilter;
static mut LOGGER: Option<Logger<InterruptOk<HStdout>>> = None; static mut LOGGER: Option<Logger<InterruptOk<HStdout>>> = None;
let logger = Logger { let logger = Logger {

View File

@ -1,6 +1,9 @@
pub mod boot; pub mod boot;
pub mod delay; pub mod dfu;
pub mod flash_store;
pub mod gpio; pub mod gpio;
pub mod hw_rev;
pub mod log_setup; pub mod log_setup;
pub mod rtt_logger; pub mod rtt_logger;
pub mod sys_timer;
pub mod usb; pub mod usb;

55
src/device/sys_timer.rs Normal file
View File

@ -0,0 +1,55 @@
use core::{cell::RefCell, ops::Deref};
use cortex_m::{interrupt::Mutex, peripheral::syst::SystClkSource};
use cortex_m_rt::exception;
use stm32f4xx_hal::{pac::{SYST, Peripherals}, rcc::Clocks};
/// Rate in Hz
const TIMER_RATE: u32 = 1000;
/// Interval duration in milliseconds
const TIMER_DELTA: u32 = 1000 / TIMER_RATE;
/// Elapsed time in milliseconds
static TIMER_MS: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
static mut US_COUNT: u32 = 168;
/// Setup SysTick exception
pub fn setup(mut syst: SYST, clocks: Clocks) {
unsafe {
US_COUNT = clocks.hclk().to_MHz()
}
syst.set_clock_source(SystClkSource::Core);
syst.set_reload(clocks.hclk().to_Hz() / TIMER_RATE - 1);
syst.enable_counter();
syst.enable_interrupt();
}
/// SysTick exception (Timer)
#[exception]
fn SysTick() {
cortex_m::interrupt::free(|cs| {
*TIMER_MS.borrow(cs).borrow_mut() += TIMER_DELTA;
});
}
/// Obtain current time in milliseconds
pub fn now() -> u32 {
cortex_m::interrupt::free(|cs| *TIMER_MS.borrow(cs).borrow().deref())
}
/// Obtain current time in milliseconds + microseconds
pub fn now_precise() -> (u32, u32) {
let ms = now();
unsafe {
let us = (Peripherals::steal().STK.load.read().bits() - Peripherals::steal().STK.val.read().bits()) / US_COUNT;
(ms, us)
}
}
/// block for `amount` milliseconds
pub fn sleep(amount: u32) {
if amount == 0 {
return;
}
let start = now();
while now() - start <= amount - 1 {}
}

View File

@ -1,17 +1,14 @@
use core::{ use core::{fmt::{self, Write},
fmt::{self, Write}, mem::MaybeUninit};
mem::MaybeUninit,
};
use cortex_m::interrupt::free; use cortex_m::interrupt::free;
use log::{Log, Metadata, Record}; use log::{Log, Metadata, Record};
use stm32f4xx_hal::{ use stm32f4xx_hal::{otg_fs::{UsbBus as Bus, USB},
otg_fs::{UsbBus as Bus, USB}, pac::{interrupt, Interrupt, NVIC}};
pac::{interrupt, Interrupt, NVIC}, use usb_device::{class_prelude::UsbBusAllocator,
}; descriptor::lang_id,
use usb_device::{ device::StringDescriptors,
class_prelude::UsbBusAllocator, prelude::{UsbDevice, UsbDeviceBuilder, UsbVidPid}};
prelude::{UsbDevice, UsbDeviceBuilder, UsbVidPid},
};
use usbd_serial::SerialPort; use usbd_serial::SerialPort;
static mut EP_MEMORY: [u32; 1024] = [0; 1024]; static mut EP_MEMORY: [u32; 1024] = [0; 1024];
@ -27,16 +24,20 @@ pub struct State {
impl State { impl State {
pub fn setup(usb: USB) { pub fn setup(usb: USB) {
unsafe { BUS.write(Bus::new(usb, &mut EP_MEMORY)) }; unsafe { BUS.write(Bus::new(usb, EP_MEMORY.as_mut())) };
let str_descriptor = StringDescriptors::new(lang_id::LangID::EN)
.manufacturer("M-Labs")
.product("Kirdy");
let bus = unsafe { BUS.assume_init_ref() }; let bus = unsafe { BUS.assume_init_ref() };
let serial = SerialPort::new(bus); let serial = SerialPort::new(bus);
let dev = UsbDeviceBuilder::new(bus, UsbVidPid(0x16c0, 0x27dd)) let dev = UsbDeviceBuilder::new(bus, UsbVidPid(0x16c0, 0x27dd))
.manufacturer("M-Labs")
.product("thermostat")
.device_release(0x20) .device_release(0x20)
.self_powered(true) .self_powered(true)
.device_class(usbd_serial::USB_CLASS_CDC) .device_class(usbd_serial::USB_CLASS_CDC)
.strings(&[str_descriptor])
.unwrap()
.build(); .build();
free(|_| unsafe { free(|_| unsafe {

View File

@ -1,73 +0,0 @@
use cortex_m::prelude::_embedded_hal_blocking_delay_DelayMs;
use stm32f4xx_hal::{
gpio::{gpioa::*, gpiob::*, gpiod::*, Alternate, Output, PushPull, PD9},
hal::{blocking::spi::Write, digital::v2::OutputPin},
pac::SPI2,
spi::{NoMiso, Spi, TransferModeNormal},
};
pub trait CurrentSourcePhy {
type CurrentSourceLdoEn: OutputPin;
type CurrentSourceShort: OutputPin;
type Max5719Load: OutputPin;
type Max5719Cs: OutputPin;
type Max5719Spi: Write<u8>;
}
pub struct CurrentSourcePhyConstruct<C: CurrentSourcePhy> {
pub max5719_spi: C::Max5719Spi,
pub max5719_load: C::Max5719Load,
pub max5719_cs: C::Max5719Cs,
pub current_source_ldo_en: C::CurrentSourceLdoEn,
pub current_source_short: C::CurrentSourceShort,
}
pub struct CurrentSourceSettings {
pub output_current: f32,
}
pub struct CurrentSource<C: CurrentSourcePhy> {
pub phy: CurrentSourcePhyConstruct<C>,
pub setting: CurrentSourceSettings,
}
pub struct CurrentSourcePhyCh0;
impl CurrentSourcePhy for CurrentSourcePhyCh0 {
type CurrentSourceLdoEn = PD9<Output<PushPull>>;
type CurrentSourceShort = PA4<Output<PushPull>>;
type Max5719Load = PB14<Output<PushPull>>;
type Max5719Cs = PD8<Output<PushPull>>;
type Max5719Spi =
Spi<SPI2, (PB10<Alternate<5>>, NoMiso, PB15<Alternate<5>>), TransferModeNormal>;
}
use crate::device::delay;
impl<C: CurrentSourcePhy> CurrentSource<C> {
pub fn setup(&mut self, mut delay: delay::AsmDelay) {
let _ = self.phy.max5719_load.set_high();
let _ = self.phy.max5719_cs.set_high();
let _ = self.phy.current_source_ldo_en.set_high();
delay.delay_ms(10_u32);
let _ = self.phy.current_source_short.set_high();
delay.delay_ms(10_u32);
}
pub fn set_current(
&mut self,
current: f32,
) -> Result<(), <<C as CurrentSourcePhy>::Max5719Spi as Write<u8>>::Error> {
let _ = self.phy.max5719_load.set_high();
let _ = self.phy.max5719_cs.set_low();
self.setting.output_current = current * 10.0 / 0.75;
let word = (((self.setting.output_current / 4.096) * 1048576.0) as u32) << 4;
let mut buf = [
((word >> 16) & 0xFF) as u8,
((word >> 8) & 0xFF) as u8,
((word >> 0) & 0xFF) as u8,
];
self.phy.max5719_spi.write(&mut buf)?;
let _ = self.phy.max5719_cs.set_high();
let _ = self.phy.max5719_load.set_low();
Ok(())
}
}

View File

@ -0,0 +1,311 @@
use core::marker::PhantomData;
use log::info;
use miniconf::Tree;
use num_traits::Zero;
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::{pac::{ADC3, TIM2},
timer::CounterUs};
use uom::si::{electric_current::{ampere, milliampere},
f32::{ElectricCurrent, ElectricPotential, ElectricalConductance, ElectricalResistance, Power},
power::milliwatt};
use crate::{device::sys_timer::sleep,
laser_diode::{ld_ctrl::{Impedance50Ohm, LdCtrl},
ld_current_out_ctrl_timer::LdCurrentOutCtrlTimer,
ld_pwr_exc_protector::{self, LdPwrExcProtector},
pd_mon_params}};
impl Settings {
pub const LD_CURRENT_MAX: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 0.3,
};
pub const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 4.096,
};
const LD_DRIVE_TRANSIMPEDANCE: ElectricalResistance = ElectricalResistance {
dimension: PhantomData,
units: PhantomData,
value: 10.0 / 0.75,
};
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, Tree)]
struct Settings {
pwr_on: bool,
default_pwr_on: bool,
ld_drive_current: ElectricCurrent,
pd_mon_params: pd_mon_params::Parameters,
ld_pwr_limit: Power,
ld_terms_short: bool,
incoming_pd_mon_params: pd_mon_params::Parameters,
}
impl Default for Settings {
fn default() -> Self {
Self {
pwr_on: false,
default_pwr_on: false,
ld_drive_current: ElectricCurrent::new::<milliampere>(0.0),
pd_mon_params: pd_mon_params::Parameters::default(),
incoming_pd_mon_params: pd_mon_params::Parameters::default(),
ld_pwr_limit: Power::new::<milliwatt>(0.0),
ld_terms_short: false,
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReport {
pwr_on: bool,
pwr_excursion: bool,
ld_i_set: ElectricCurrent,
pd_i: ElectricCurrent,
pd_pwr: Power,
term_50ohm: Impedance50Ohm,
}
pub struct LdDrive {
ctrl: LdCtrl,
settings: Settings,
}
impl LdDrive {
pub fn new(
current_source: LdCtrl,
pins_adc: ADC3,
tim2: CounterUs<TIM2>,
phy: ld_pwr_exc_protector::LdPwrExcProtectorPhy,
) -> Self {
LdPwrExcProtector::setup(pins_adc, phy);
LdCurrentOutCtrlTimer::setup(tim2);
LdDrive {
ctrl: current_source,
settings: Settings::default(),
}
}
pub fn setup(&mut self) {
LdPwrExcProtector::pwr_off();
self.ctrl.set_i(
ElectricCurrent::new::<milliampere>(0.0),
Settings::LD_DRIVE_TRANSIMPEDANCE,
Settings::DAC_OUT_V_MAX,
);
LdCurrentOutCtrlTimer::reset();
self.ld_short();
}
pub fn ld_short(&mut self) {
self.ctrl.ld_short_enable();
self.settings.ld_terms_short = true;
}
pub fn ld_open(&mut self) {
self.ctrl.ld_short_disable();
self.settings.ld_terms_short = false;
}
pub fn power_up(&mut self) {
let prev_i_set = self.settings.ld_drive_current;
LdCurrentOutCtrlTimer::reset();
let _ = self.ctrl.set_i(
ElectricCurrent::new::<milliampere>(0.0),
Settings::LD_DRIVE_TRANSIMPEDANCE,
Settings::DAC_OUT_V_MAX,
);
// Wait for the DAC to reset its voltage back to 0V
sleep(35);
LdPwrExcProtector::pwr_on_and_arm_protection();
// Wait for LD Power Supply to start up before driving current to laser diode
sleep(30);
self.ld_set_i(prev_i_set);
self.settings.pwr_on = true;
}
pub fn power_down(&mut self) {
LdPwrExcProtector::pwr_off();
self.settings.pwr_on = false;
}
pub fn get_pd_i(&mut self) -> ElectricCurrent {
self.settings
.pd_mon_params
.get_pd_i_from_pd_v(LdPwrExcProtector::get_status().v)
}
pub fn get_pd_pwr(&mut self) -> Power {
self.settings.pd_mon_params.get_ld_pwr_from_ld_i(
self.settings
.pd_mon_params
.get_pd_i_from_pd_v(LdPwrExcProtector::get_status().v),
)
}
pub fn ld_set_i(&mut self, i: ElectricCurrent) {
self.settings.ld_drive_current = i.min(Settings::LD_CURRENT_MAX);
LdCurrentOutCtrlTimer::set_target_i_and_listen_irq(self.settings.ld_drive_current, self.ctrl.get_i_set());
}
pub fn poll_pd_mon_v(&mut self) -> ElectricPotential {
LdPwrExcProtector::poll_pd_mon_v()
}
pub fn poll_and_update_output_current(&mut self) -> ElectricCurrent {
match LdCurrentOutCtrlTimer::get_irq_status() {
Some(i_set) => {
let i_set = self
.ctrl
.set_i(i_set, Settings::LD_DRIVE_TRANSIMPEDANCE, Settings::DAC_OUT_V_MAX);
LdCurrentOutCtrlTimer::clear_alarm_and_resume_listening();
i_set
}
None => ElectricCurrent::new::<ampere>(0.0),
}
}
// Set the calibrated VDDA value obtained from ADC1 calibration
pub fn set_pd_mon_calibrated_vdda(&mut self, val_cal: u32) {
LdPwrExcProtector::set_calibrated_vdda(val_cal)
}
pub fn pd_mon_clear_alarm(&mut self) {
LdPwrExcProtector::clear_alarm_status();
}
pub fn set_pd_transconductance(&mut self, transconductance: ElectricalConductance) {
self.settings.pd_mon_params.set_transconductance(transconductance)
}
pub fn set_pd_responsitivity(&mut self, responsitivity: pd_mon_params::ResponsitivityUnit) {
self.settings.incoming_pd_mon_params.set_responsitivity(responsitivity);
}
pub fn set_pd_dark_current(&mut self, i_dark: ElectricCurrent) {
self.settings.incoming_pd_mon_params.set_i_dark(i_dark);
}
pub fn apply_pd_params(&mut self) -> bool {
let prev_pd_params = self.settings.pd_mon_params;
self.settings.incoming_pd_mon_params.transconductance = self.settings.pd_mon_params.transconductance;
self.settings.pd_mon_params = self.settings.incoming_pd_mon_params;
let max_settable_pwr = self.get_ld_power_limit_range();
let is_legal = self.settings.ld_pwr_limit <= max_settable_pwr;
if is_legal {
self.set_ld_power_limit(self.settings.ld_pwr_limit);
} else {
self.settings.pd_mon_params = prev_pd_params;
}
is_legal
}
pub fn set_ld_power_limit(&mut self, pwr_limit: Power) -> bool {
let is_legal = LdPwrExcProtector::set_trigger_threshold_v(
self.settings.pd_mon_params.get_pd_i_from_ld_pwr(pwr_limit) / self.settings.pd_mon_params.transconductance,
);
if is_legal {
self.settings.ld_pwr_limit = pwr_limit;
}
is_legal
}
pub fn get_ld_power_limit_range(&mut self) -> Power {
let v_range = LdPwrExcProtector::get_settable_volt_range();
let i_range = self.settings.pd_mon_params.get_pd_i_from_pd_v(v_range);
let max_settable_pwr = self.settings.pd_mon_params.get_ld_pwr_from_ld_i(i_range);
if max_settable_pwr.is_nan() || max_settable_pwr.is_infinite() {
Power::zero()
} else {
max_settable_pwr
}
}
pub fn set_pd_i_limit(&mut self, i: ElectricCurrent) -> bool {
return LdPwrExcProtector::set_trigger_threshold_v(i / self.settings.pd_mon_params.transconductance);
}
pub fn set_default_pwr_on(&mut self, pwr_on: bool) {
self.settings.default_pwr_on = pwr_on;
}
pub fn get_term_status(&mut self) -> Impedance50Ohm {
self.ctrl.get_lf_mod_in_impedance()
}
pub fn get_status_report(&mut self) -> StatusReport {
let ld_i_set = if LdPwrExcProtector::get_status().pwr_engaged {
self.ctrl.get_i_set()
} else {
ElectricCurrent::new::<ampere>(0.0)
};
StatusReport {
pwr_on: LdPwrExcProtector::get_status().pwr_engaged,
pwr_excursion: LdPwrExcProtector::get_status().pwr_excursion,
ld_i_set: ld_i_set,
pd_i: self.get_pd_i(),
pd_pwr: self.get_pd_pwr(),
term_50ohm: self.get_term_status(),
}
}
pub fn get_settings_summary(&mut self) -> LdSettingsSummary {
let settings = self.settings;
LdSettingsSummary {
default_pwr_on: self.settings.default_pwr_on,
ld_drive_current: LdSettingsSummaryField {
value: settings.ld_drive_current,
max: Settings::LD_CURRENT_MAX,
},
pd_mon_params: settings.pd_mon_params,
ld_pwr_limit: LdSettingsSummaryField {
value: settings.ld_pwr_limit,
max: self.get_ld_power_limit_range(),
},
ld_terms_short: settings.ld_terms_short,
}
}
pub fn load_settings_from_summary(&mut self, settings: LdSettingsSummary) {
self.power_down();
self.settings.ld_drive_current = settings.ld_drive_current.value;
self.settings.pd_mon_params = settings.pd_mon_params;
let max_pwr_limit = self.get_ld_power_limit_range();
self.settings.ld_pwr_limit = settings.ld_pwr_limit.value.min(max_pwr_limit);
self.settings.default_pwr_on = settings.default_pwr_on;
self.set_ld_power_limit(self.settings.ld_pwr_limit);
if self.settings.ld_terms_short {
self.ld_short();
} else {
self.ld_open();
}
if settings.default_pwr_on {
self.power_up();
} else {
self.power_down();
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct LdSettingsSummary {
default_pwr_on: bool,
ld_drive_current: LdSettingsSummaryField<ElectricCurrent>,
pd_mon_params: pd_mon_params::Parameters,
ld_pwr_limit: LdSettingsSummaryField<Power>,
ld_terms_short: bool,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct LdSettingsSummaryField<T> {
value: T,
max: T,
}

View File

@ -0,0 +1,96 @@
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::{gpio::{gpioa::*, gpiob::*, gpiod::*, Input, Output, PushPull},
hal::{digital::{InputPin, OutputPin},
spi::SpiBus},
pac::SPI2,
spi::Spi};
use uom::si::{electric_current::ampere,
f32::{ElectricCurrent, ElectricPotential, ElectricalResistance},
ratio::ratio};
use crate::laser_diode::max5719::{self, Dac};
#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
pub enum Impedance50Ohm {
On,
Off,
}
pub trait ChannelPins {
type CurrentSourceShort: OutputPin;
type TerminationStatus: InputPin;
type Max5719Load: OutputPin;
type Max5719Cs: OutputPin;
type Max5719Spi: SpiBus<u8>;
}
pub struct LdCtrlPhy<C: ChannelPins> {
pub dac: Dac<C::Max5719Spi, C::Max5719Cs, C::Max5719Load>,
pub current_source_short_pin: C::CurrentSourceShort,
pub termination_status_pin: C::TerminationStatus,
}
pub struct Channel0;
impl ChannelPins for Channel0 {
type CurrentSourceShort = PA4<Output<PushPull>>;
type TerminationStatus = PD7<Input>;
type Max5719Load = DacLoad;
type Max5719Cs = DacCs;
type Max5719Spi = DacSpi;
}
type DacSpi = Spi<SPI2>;
type DacCs = PD8<Output<PushPull>>;
type DacLoad = PB14<Output<PushPull>>;
pub struct LdCtrl {
pub phy: LdCtrlPhy<Channel0>,
i_set: ElectricCurrent,
}
impl LdCtrl {
pub fn new(phy_ch0: LdCtrlPhy<Channel0>) -> Self {
LdCtrl {
phy: phy_ch0,
i_set: ElectricCurrent::new::<ampere>(0.0),
}
}
// LD Terminals are shorted together
pub fn ld_short_enable(&mut self) {
self.phy.current_source_short_pin.set_low();
}
// LD Current flows from anode to cathode
pub fn ld_short_disable(&mut self) {
self.phy.current_source_short_pin.set_high();
}
pub fn get_lf_mod_in_impedance(&mut self) -> Impedance50Ohm {
if self.phy.termination_status_pin.is_high() {
Impedance50Ohm::On
} else {
Impedance50Ohm::Off
}
}
pub fn set_dac(&mut self, voltage: ElectricPotential, dac_out_v_max: ElectricPotential) -> ElectricPotential {
let value = ((voltage / dac_out_v_max).get::<ratio>() * (max5719::MAX_VALUE as f32)) as u32;
self.phy.dac.set(value).unwrap();
value as f32 * dac_out_v_max / max5719::MAX_VALUE as f32
}
pub fn set_i(
&mut self,
current: ElectricCurrent,
transimpedance: ElectricalResistance,
dac_out_v_max: ElectricPotential,
) -> ElectricCurrent {
self.i_set = self.set_dac(current * transimpedance, dac_out_v_max) / transimpedance;
self.i_set
}
pub fn get_i_set(&mut self) -> ElectricCurrent {
self.i_set
}
}

View File

@ -0,0 +1,128 @@
use core::marker::PhantomData;
use fugit::{KilohertzU32, TimerDurationU32};
use log::debug;
use stm32f4xx_hal::{pac::{interrupt, Interrupt, TIM2},
timer::{Counter, Event},
Listen};
use uom::si::{electric_current::ampere, f32::ElectricCurrent};
pub struct LdCurrentOutCtrlTimer {
target_i: ElectricCurrent,
now_i: ElectricCurrent,
timer: Counter<TIM2, FREQ>,
timeout: bool,
}
static mut LD_CURRENT_OUT_CTRL_TIMER: Option<LdCurrentOutCtrlTimer> = None;
pub const FREQ: u32 = 1000000;
/// This timer notifies the main loop to set the correct output current so that ld output current can ramp up/down slowly.
/// The current output slope is guaranteed to be larger but not necessarily equal to than the preset value.
impl LdCurrentOutCtrlTimer {
const TIME_STEP_MS: TimerDurationU32<FREQ> = TimerDurationU32::from_rate(KilohertzU32::from_raw(1));
const STEP_SIZE: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 0.0001,
};
pub fn setup(mut tim2: Counter<TIM2, FREQ>) {
tim2.start(LdCurrentOutCtrlTimer::TIME_STEP_MS).unwrap();
unsafe {
cortex_m::peripheral::NVIC::unmask(Interrupt::TIM2);
}
unsafe {
LD_CURRENT_OUT_CTRL_TIMER = Some(LdCurrentOutCtrlTimer {
target_i: ElectricCurrent::new::<ampere>(0.0),
now_i: ElectricCurrent::new::<ampere>(0.0),
timer: tim2,
timeout: false,
});
}
}
fn get() -> Option<&'static mut Self> {
unsafe { LD_CURRENT_OUT_CTRL_TIMER.as_mut() }
}
pub fn reset() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.target_i = ElectricCurrent::new::<ampere>(0.0);
ld_current_out_ctrl_timer.now_i = ElectricCurrent::new::<ampere>(0.0);
ld_current_out_ctrl_timer.timeout = false;
ld_current_out_ctrl_timer.timer.unlisten(Event::Update);
}
}
pub fn set_target_i_and_listen_irq(target: ElectricCurrent, now: ElectricCurrent) {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
cortex_m::interrupt::free(|_| {
ld_current_out_ctrl_timer.target_i = target;
ld_current_out_ctrl_timer.now_i = now;
ld_current_out_ctrl_timer.timer.listen(Event::Update);
})
}
}
pub fn set_alarm() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.timeout = true;
}
}
pub fn clear_alarm_and_resume_listening() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.timeout = false;
ld_current_out_ctrl_timer.timer.listen(Event::Update);
}
}
pub fn get_irq_status() -> Option<ElectricCurrent> {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
if ld_current_out_ctrl_timer.timeout {
return Some(ld_current_out_ctrl_timer.now_i);
}
}
None
}
pub fn clear_irq_flag() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
match ld_current_out_ctrl_timer.timer.wait() {
Ok(_) => {}
Err(_) => {
debug!("LD CTRL TIMER Interrupt is not present when clear_irq_flag() is called")
}
}
}
}
pub fn update_next_i_set() -> bool {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
let update = ld_current_out_ctrl_timer.now_i != ld_current_out_ctrl_timer.target_i;
if ld_current_out_ctrl_timer.target_i > ld_current_out_ctrl_timer.now_i {
ld_current_out_ctrl_timer.now_i = (ld_current_out_ctrl_timer.now_i + LdCurrentOutCtrlTimer::STEP_SIZE)
.min(ld_current_out_ctrl_timer.target_i);
} else {
ld_current_out_ctrl_timer.now_i = (ld_current_out_ctrl_timer.now_i - LdCurrentOutCtrlTimer::STEP_SIZE)
.max(ld_current_out_ctrl_timer.target_i);
}
return update;
}
false
}
pub fn stop_listening() {
if let Some(ref mut ld_current_out_ctrl_timer) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.timer.unlisten(Event::Update);
}
}
}
#[interrupt]
fn TIM2() {
if LdCurrentOutCtrlTimer::update_next_i_set() {
LdCurrentOutCtrlTimer::set_alarm();
}
LdCurrentOutCtrlTimer::stop_listening();
LdCurrentOutCtrlTimer::clear_irq_flag();
}

View File

@ -0,0 +1,296 @@
use num_traits::{Float, Zero};
use stm32f4xx_hal::{adc::{config::{self, AdcConfig},
Adc},
gpio::{gpioa::PA3, gpiod::PD9, Analog, Output, PushPull},
interrupt, pac,
pac::{Peripherals, ADC3, NVIC},
rcc::Enable};
use uom::si::{electric_potential::millivolt, f32::ElectricPotential, ratio::ratio};
// 12 bit Resolution
const MAX_SAMPLE: u16 = 4095;
pub type LdPwrEnPinType = PD9<Output<PushPull>>;
pub type PdMonAdcPinType = PA3<Analog>;
const PD_MON_ADC_CH_ID: u8 = 0x03;
static mut LD_PWR_EXC_PROTECTOR: Option<LdPwrExcProtector> = None;
pub struct LdPwrExcProtectorPhy {
// To make sure Pd Mon Pin is configured to Analog mode
pub _pd_mon_ch0: PdMonAdcPinType,
pub pwr_en_ch0: LdPwrEnPinType,
}
#[derive(Clone)]
pub struct Status {
pub pwr_excursion: bool,
pub v_tripped: ElectricPotential,
pub pwr_engaged: bool,
pub v: ElectricPotential,
}
impl Default for Status {
fn default() -> Self {
Status {
pwr_excursion: false,
v_tripped: ElectricPotential::new::<millivolt>(0.0),
pwr_engaged: false,
v: ElectricPotential::new::<millivolt>(0.0),
}
}
}
pub struct LdPwrExcProtector {
pac: ADC3,
phy: LdPwrExcProtectorPhy,
alarm_status: Status,
calibrated_vdda: u32,
offset: u32,
prev_samples: [u16; 32],
ptr: u16,
}
impl LdPwrExcProtector {
/// ADC Analog Watchdog is configured to guard a single regular Adc channel on Pd Mon Pin.
/// ADC is configured to start continuous conversion without using DMA immediately.
/// Interrupt is disabled by default.
pub fn setup(pac_adc: ADC3, mut phy: LdPwrExcProtectorPhy) {
let mut offset = 0;
unsafe {
let adc_config = AdcConfig::default()
.clock(config::Clock::Pclk2_div_8)
.default_sample_time(config::SampleTime::Cycles_480);
let mut tmp_adc = Adc::adc3(Peripherals::steal().ADC3, false, adc_config);
for _ in (0..1024).rev() {
offset += tmp_adc.convert(&phy._pd_mon_ch0, stm32f4xx_hal::adc::config::SampleTime::Cycles_480) as u32;
}
offset /= 1024 as u32
}
// Do not set reset RCCs as it causes other ADCs' clock to be disabled
unsafe {
// All ADCs share the same reset interface.
// NOTE(unsafe) this reference will only be used for atomic writes with no side effects.
let rcc = &(*pac::RCC::ptr());
// Enable the ADC3 Clock
pac::ADC3::enable(rcc);
// Enable ADC Interrupt
NVIC::unmask(interrupt::ADC);
}
pac_adc.cr1.reset();
pac_adc.cr2.reset();
pac_adc.sqr1.reset();
pac_adc.sqr2.reset();
pac_adc.sqr3.reset();
pac_adc.cr1.write(|w| {
w
// 12 Bit Resolution
.res()
.twelve_bit()
// Set Analog Watchdog to guard Single Regular Channel
.awden()
.enabled()
.awdsgl()
.single_channel()
.jawden()
.disabled()
// Disable Analog Watchdog Interrupt
.awdie()
.disabled()
// Set Analog Watchdog to monitor Pd Mon Pin
.awdch()
.variant(PD_MON_ADC_CH_ID)
});
pac_adc.cr2.write(|w| {
w
// Continous Conversion Mode
.cont()
.set_bit()
// Power up ADC
.adon()
.set_bit()
// Set data alignment to the right
.align()
.right()
// End of conversion selection: Each Sequence
.eocs()
.each_sequence()
.exten()
.disabled()
.extsel()
.tim1cc1()
});
// Set the Conversion Sequence to include Pd Mon Pin
pac_adc.sqr3.write(|w| w.sq1().variant(PD_MON_ADC_CH_ID));
// Set all sampling channels to have slowest sampling interval
pac_adc.smpr1.write(|w| unsafe { w.bits(0xFFFF) });
pac_adc.smpr2.write(|w| unsafe { w.bits(0xFFFF) });
// Set the higher threshold to be max value initially
pac_adc.htr.write(|w| w.ht().variant(MAX_SAMPLE));
// Set the lower threshold to be min value initially
pac_adc.ltr.write(|w| w.lt().variant(0));
// SWStart should only be set when ADON = 1. Otherwise no conversion is launched.
pac_adc.cr2.modify(|_, w| w.swstart().set_bit());
phy.pwr_en_ch0.set_low();
unsafe {
LD_PWR_EXC_PROTECTOR = Some(LdPwrExcProtector {
pac: pac_adc,
phy: phy,
alarm_status: Status::default(),
calibrated_vdda: 3300,
offset: offset,
prev_samples: [0; 32],
ptr: 0,
});
}
}
fn get() -> Option<&'static mut Self> {
unsafe { LD_PWR_EXC_PROTECTOR.as_mut() }
}
fn convert_sample_to_volt(sample: u16) -> ElectricPotential {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
return ElectricPotential::new::<millivolt>(
(((i32::from(sample) - wdg.offset as i32).max(0) as u32 * wdg.calibrated_vdda) / u32::from(MAX_SAMPLE))
as f32,
);
}
ElectricPotential::new::<millivolt>(0.0)
}
pub fn set_trigger_threshold_v(htr: ElectricPotential) -> bool {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
let code: u32 = (((htr / (ElectricPotential::new::<millivolt>(wdg.calibrated_vdda as f32))).get::<ratio>()
* (MAX_SAMPLE as f32))
.ceil() as u32)
+ wdg.offset;
if code <= MAX_SAMPLE as u32 {
wdg.pac.htr.write(|w| unsafe { w.bits(code) });
return true;
}
}
false
}
pub fn set_calibrated_vdda(val: u32) {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.calibrated_vdda = val;
}
}
pub fn poll_pd_mon_v() -> ElectricPotential {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
if wdg.pac.sr.read().eoc().bit() {
wdg.prev_samples[wdg.ptr as usize] = wdg.pac.dr.read().data().bits();
wdg.ptr = (wdg.ptr + 1) % 32 as u16;
let mut samples: u32 = 0;
for idx in 0..32 {
samples += wdg.prev_samples[idx] as u32;
}
samples = samples >> 5;
wdg.alarm_status.v = LdPwrExcProtector::convert_sample_to_volt(samples as u16);
}
return wdg.alarm_status.v;
}
ElectricPotential::zero()
}
pub fn get_status() -> Status {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
LdPwrExcProtector::poll_pd_mon_v();
return wdg.alarm_status.clone();
}
Status::default()
}
pub fn get_settable_volt_range() -> ElectricPotential {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
return ElectricPotential::new::<millivolt>(wdg.calibrated_vdda as f32)
* ((MAX_SAMPLE as u32 - wdg.offset) as f32)
/ MAX_SAMPLE as f32;
}
ElectricPotential::zero()
}
pub fn pwr_on_and_arm_protection() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
if !wdg.alarm_status.pwr_excursion {
wdg.alarm_status = Status::default();
LdPwrExcProtector::clear_interrupt_bit();
LdPwrExcProtector::pwr_on();
// Interrupt should be enabled after power on to tackle the following edge case:
// Pd_Mon pin voltage has already exceed threshold before LD Power is on.
LdPwrExcProtector::enable_watchdog_interrupt();
}
}
}
pub fn clear_alarm_status() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.alarm_status.pwr_excursion = false;
wdg.alarm_status.v_tripped = ElectricPotential::new::<millivolt>(0.0);
}
}
fn enable_watchdog_interrupt() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.pac.cr1.modify(|_, w| w.awdie().set_bit());
}
}
fn disable_watchdog_interrupt() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.pac.cr1.modify(|_, w| w.awdie().clear_bit());
}
}
fn clear_interrupt_bit() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.pac.sr.modify(|_, w| w.awd().clear_bit());
}
}
fn pwr_on() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.alarm_status.pwr_engaged = true;
wdg.phy.pwr_en_ch0.set_high()
}
}
pub fn pwr_off() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
wdg.alarm_status.pwr_engaged = false;
wdg.phy.pwr_en_ch0.set_low();
LdPwrExcProtector::disable_watchdog_interrupt();
}
}
fn pwr_excursion_handler() {
if let Some(ref mut wdg) = LdPwrExcProtector::get() {
let sample = wdg.pac.dr.read().data().bits();
LdPwrExcProtector::pwr_off();
wdg.alarm_status.pwr_excursion = true;
wdg.alarm_status.v_tripped = LdPwrExcProtector::convert_sample_to_volt(sample);
}
}
}
#[interrupt]
fn ADC() {
cortex_m::interrupt::free(|_| {
LdPwrExcProtector::pwr_excursion_handler();
// Disable interrupt to avoid getting stuck in infinite loop
LdPwrExcProtector::disable_watchdog_interrupt();
LdPwrExcProtector::clear_interrupt_bit();
})
}

View File

@ -0,0 +1,53 @@
use fugit::MegahertzU32;
use stm32f4xx_hal::{hal::{digital::OutputPin, spi::SpiBus},
spi};
use crate::device::sys_timer::sleep;
pub const SPI_MODE: spi::Mode = spi::Mode {
polarity: spi::Polarity::IdleLow,
phase: spi::Phase::CaptureOnFirstTransition,
};
// MAX5719 Max Frequency: 50MHz | SPI2 Max Frequency: 21MHz
pub const SPI_CLOCK_MHZ: MegahertzU32 = MegahertzU32::from_raw(21);
pub const MAX_VALUE: u32 = 0xFFFFF;
pub struct Dac<SPI: SpiBus<u8>, S1: OutputPin, S2: OutputPin> {
spi: SPI,
cs_n: S1,
load_n: S2,
}
impl<SPI: SpiBus<u8>, S1: OutputPin, S2: OutputPin> Dac<SPI, S1, S2> {
pub fn new(spi: SPI, mut cs_n: S1, mut load_n: S2) -> Self {
let _ = cs_n.set_high();
let _ = load_n.set_high();
Dac { spi, cs_n, load_n }
}
fn write(&mut self, buf: &mut [u8]) -> Result<(), SPI::Error> {
let _ = self.cs_n.set_low();
self.spi.write(buf)?;
let _ = self.cs_n.set_high();
sleep(1);
// must be high for >= 20 ns
let _ = self.load_n.set_low();
// must be low for >= 20 ns
sleep(1);
let _ = self.load_n.set_high();
Ok(())
}
pub fn set(&mut self, value: u32) -> Result<u32, SPI::Error> {
let value = value.min(MAX_VALUE) << 4;
let mut buf = [
((value >> 16) & 0xFF) as u8,
((value >> 8) & 0xFF) as u8,
((value >> 0) & 0xFF) as u8,
];
self.write(&mut buf)?;
Ok(value)
}
}

View File

@ -1 +1,6 @@
pub mod current_sources; pub mod laser_diode;
pub mod ld_ctrl;
pub mod ld_current_out_ctrl_timer;
pub mod ld_pwr_exc_protector;
pub mod max5719;
pub mod pd_mon_params;

View File

@ -0,0 +1,73 @@
use core::{f32::NAN, marker::PhantomData};
use miniconf::Tree;
use num_traits::Zero;
use serde::{Deserialize, Serialize};
use uom::{si::{electric_current::microampere,
f32::{ElectricCurrent, ElectricPotential, ElectricalConductance, Power},
Quantity, ISQ, SI},
typenum::*};
// Ampere / Watt
pub type ResponsitivityUnit = Quantity<ISQ<N2, N1, P3, P1, Z0, Z0, Z0>, SI<f32>, f32>;
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Tree)]
pub struct Parameters {
pub transconductance: ElectricalConductance,
responsitivity: ResponsitivityUnit,
i_dark: ElectricCurrent,
}
impl Parameters {
pub fn get_ld_pwr_from_ld_i(&self, i: ElectricCurrent) -> Power {
let ld_power = (i - self.i_dark) / self.responsitivity;
ld_power
}
pub fn get_pd_i_from_ld_pwr(&self, pwr: Power) -> ElectricCurrent {
let ld_i = pwr * self.responsitivity + self.i_dark;
ld_i
}
pub fn get_pd_i_from_pd_v(&self, v: ElectricPotential) -> ElectricCurrent {
(v * self.transconductance).max(ElectricCurrent::zero())
}
pub fn set_responsitivity(&mut self, responsitivity: ResponsitivityUnit) {
self.responsitivity = responsitivity;
}
pub fn get_responsitivity(&mut self) -> ResponsitivityUnit {
self.responsitivity
}
pub fn set_i_dark(&mut self, i_dark: ElectricCurrent) {
self.i_dark = i_dark;
}
pub fn get_i_dark(&mut self) -> ElectricCurrent {
self.i_dark
}
pub fn set_transconductance(&mut self, transconductance: ElectricalConductance) {
self.transconductance = transconductance;
}
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
transconductance: ElectricalConductance {
dimension: PhantomData,
units: PhantomData,
value: 1.0 / 1000.0,
},
responsitivity: ResponsitivityUnit {
dimension: PhantomData,
units: PhantomData,
value: NAN,
},
i_dark: ElectricCurrent::new::<microampere>(0.0),
}
}
}

View File

@ -1,88 +1,343 @@
#![no_main] #![cfg_attr(not(test), no_main)]
#![no_std] #![cfg_attr(not(test), no_std)]
use rtic::app; use core::{marker::PhantomData, u32};
use cortex_m_rt::entry;
use log::{debug, info};
use stm32f4xx_hal::pac::{CorePeripherals, Peripherals};
use uom::si::f32::ElectricalConductance;
mod device; mod device;
mod laser_diode; mod laser_diode;
mod network; mod net;
mod thermostat;
use core::ptr::addr_of_mut;
use device::{boot::bootup, log_setup, sys_timer};
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::pac::SCB;
// If RTT is used, print panic info through RTT // If RTT is used, print panic info through RTT
#[cfg(feature = "RTT")] #[cfg(all(feature = "RTT", not(test)))]
use {core::panic::PanicInfo, rtt_target::rprintln}; use {core::panic::PanicInfo, rtt_target::rprintln};
#[cfg(feature = "RTT")]
use crate::net::net::IpSettings;
#[cfg(all(feature = "RTT", not(test)))]
#[panic_handler] #[panic_handler]
fn panic(info: &PanicInfo) -> ! { fn panic(info: &PanicInfo) -> ! {
rprintln!("{}", info); rprintln!("{}", info);
loop {} loop {}
} }
// Otherwise use panic halt // Otherwise use panic halt
#[cfg(not(feature = "RTT"))] #[cfg(all(not(feature = "RTT"), not(test)))]
use panic_halt as _; use panic_halt as _;
// #[entry] static mut ETH_DATA_BUFFER: [u8; 1024] = [0; 1024];
#[app(device = stm32f4xx_hal::pac, peripherals = true, dispatchers = [TIM8_CC, TIM8_BRK_TIM12])]
mod app {
use crate::device::{boot::bootup, log_setup}; #[derive(Deserialize, Serialize, Clone, Copy, Debug)]
use crate::network::network; pub struct DeviceSettings {
use fugit::ExtU32; ip_settings: IpSettings,
use log::info; pd_mon_fin_gain: f32,
use stm32f4xx_hal::watchdog::IndependentWatchdog; pd_mon_transconductance: ElectricalConductance,
use systick_monotonic::Systick; }
#[monotonic(binds = SysTick, default = true)] const CONFIG_KEY: [&str; 3] = ["Device", "Laser_0", "Thermostat_0"];
type SystickTimer = Systick<1000>;
#[shared] #[derive(Default)]
struct Shared {} pub enum State {
#[default]
LoadFlashSettings,
MainLoop,
SaveLdThermostatSettings,
SaveDeviceSettings,
PrepareForHardReset,
PrepareForDfu,
HardReset,
}
#[local] #[cfg(not(test))]
struct Local { #[entry]
wd: IndependentWatchdog, fn main() -> ! {
// server_storage: network::network::ServerStorage::new(),
server_handle: network::ServerHandle,
}
#[init( local = [server_storage: network::ServerStorage = network::ServerStorage::new()] )]
fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
log_setup::init_log(); log_setup::init_log();
info!("Kirdy init"); info!("Kirdy init");
let core_perif = cx.core; let core_perif = CorePeripherals::take().unwrap();
let perif = cx.device; let perif = Peripherals::take().unwrap();
let server_storage = cx.local.server_storage;
let (wd, systick, server_handle) = bootup(core_perif, perif, server_storage);
wd_feed::spawn().unwrap(); let (mut wd, mut flash_store, mut hw_rev, mut laser, mut thermostat) = bootup(core_perif, perif);
// server_poll::spawn().unwrap();
(
Shared {},
Local { wd, server_handle },
init::Monotonics(systick),
)
}
#[task(priority = 5, local = [wd])] let mut device_settings = DeviceSettings {
fn wd_feed(cx: wd_feed::Context) { ip_settings: IpSettings::default(),
let start = monotonics::now(); pd_mon_fin_gain: 1.0,
let wd = cx.local.wd; pd_mon_transconductance: ElectricalConductance {
// info!("feed wd"); dimension: PhantomData,
wd.feed(); units: PhantomData,
wd_feed::spawn_at(start + 10_u32.millis()).unwrap(); value: 1.0 / 1000.0,
} },
};
#[task(binds = ETH, priority = 2, local = [server_handle, data: [u8; 512] = [0u8; 512]])] let mut active_report: [bool; net::net::NUM_OF_SOCKETS] = [false; net::net::NUM_OF_SOCKETS];
fn server_poll(cx: server_poll::Context) {
let data = cx.local.data; let mut state = State::default();
let server_handle = cx.local.server_handle;
server_handle.poll(data); let eth_data_buffer = unsafe { addr_of_mut!(ETH_DATA_BUFFER).as_mut().unwrap() };
// server_poll::spawn_at(start + 10_u32.millis()).unwrap();
} let mut sock_ts: [u32; net::net::NUM_OF_SOCKETS] = [0; net::net::NUM_OF_SOCKETS];
#[idle]
fn idle(_: idle::Context) -> ! {
loop { loop {
cortex_m::asm::nop(); wd.feed();
if net::net::eth_poll_link_status_and_update_link_speed() {
active_report = [false; net::net::NUM_OF_SOCKETS];
}
match state {
State::LoadFlashSettings => {
// State Transition
state = State::MainLoop;
wd.feed();
let device_settings_flash: DeviceSettings;
match flash_store.read_value(CONFIG_KEY[0]) {
Ok(Some(config)) => {
device_settings_flash = config;
laser.set_pd_transconductance(config.pd_mon_fin_gain * config.pd_mon_transconductance);
debug!("Found Device Settings");
device_settings = device_settings_flash;
}
Ok(None) => {
info!("Flash does not have Device Settings");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
continue;
}
}
wd.feed();
let laser_settings: laser_diode::laser_diode::LdSettingsSummary;
match flash_store.read_value(CONFIG_KEY[1]) {
Ok(Some(config)) => {
laser_settings = config;
debug!("Found Laser Diode Settings");
}
Ok(None) => {
debug!("Does not have laser diode Settings");
continue;
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
continue;
}
}
wd.feed();
let thermostat_settings: thermostat::thermostat::ThermostatSettingsSummary;
match flash_store.read_value(CONFIG_KEY[2]) {
Ok(Some(config)) => {
thermostat_settings = config;
debug!("Found Thermostat Settings");
}
Ok(None) => {
debug!("Does not have thermostat Settings");
continue;
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
continue;
}
}
wd.feed();
thermostat.load_settings_from_summary(thermostat_settings);
laser.load_settings_from_summary(laser_settings);
}
State::MainLoop => {
laser.poll_and_update_output_current();
laser.poll_pd_mon_v();
net::net::eth_update_iface_poll_timer();
if thermostat.poll_adc() {
thermostat.update_pid();
if thermostat.get_temp_mon_status().over_temp_alarm {
laser.power_down();
thermostat.power_down();
}
net::net::for_each(|mut socket, id| {
if net::net::eth_is_socket_active(socket, true) && net::net::eth_is_socket_connected(socket) {
if active_report[id] {
if net::net::eth_can_sock_send(socket) {
net::cmd_handler::send_status_report(
eth_data_buffer,
&mut laser,
&mut thermostat,
&mut socket,
);
}
}
} else {
active_report[id] = false;
}
});
thermostat.start_tec_readings_conversion();
}
let mut num_of_connected_sock: u8 = 0;
let mut oldest_connected_sock_ts: u32 = u32::MAX;
let mut oldest_connected_sock_id: usize = 0;
net::net::for_each(|mut socket, id| {
if net::net::eth_is_socket_active(socket, true) && net::net::eth_is_socket_connected(socket) {
if net::net::eth_can_sock_recv(socket) && net::net::eth_can_sock_send(socket) {
let bytes = net::net::eth_recv(eth_data_buffer, socket);
if bytes != 0 {
info!("Ts: {:?}", sys_timer::now());
debug!("Number of bytes recv: {:?}", bytes);
// State Transition
net::cmd_handler::execute_cmd(
eth_data_buffer,
bytes,
&mut socket,
&mut hw_rev,
&mut laser,
&mut thermostat,
&mut state,
&mut device_settings,
&mut active_report[id],
);
}
}
num_of_connected_sock += 1;
if sock_ts[id] == 0 {
sock_ts[id] = sys_timer::now();
}
if oldest_connected_sock_ts > sock_ts[id] {
oldest_connected_sock_ts = sock_ts[id];
oldest_connected_sock_id = id;
}
} else {
sock_ts[id] = 0;
}
});
if num_of_connected_sock == net::net::NUM_OF_SOCKETS as u8 {
let mut sock_handle = net::net::eth_get_sock_handle(oldest_connected_sock_id);
net::cmd_handler::send_response(
eth_data_buffer,
net::cmd_handler::ResponseEnum::ConnectionClose,
None,
&mut sock_handle,
);
net::net::eth_poll_iface();
debug!("Waiting for ConnectionClose msg to be sent");
while !net::net::eth_is_data_sent() { }
net::net::eth_close_socket_by_id(oldest_connected_sock_id);
debug!("Closing socket id: {:?}", oldest_connected_sock_id);
}
}
State::SaveLdThermostatSettings => {
// State Transition
state = State::MainLoop;
wd.feed();
let mut store_value_buf = [0u8; 1024];
match flash_store.write_value(CONFIG_KEY[1], &laser.get_settings_summary(), &mut store_value_buf) {
Ok(()) => {
debug!("Laser Diode Settings is stored in flash");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
}
}
wd.feed();
match flash_store.write_value(CONFIG_KEY[2], &thermostat.get_settings_summary(), &mut store_value_buf) {
Ok(()) => {
debug!("Thermostat Settings is stored in flash");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
}
}
}
State::SaveDeviceSettings => {
// State Transition
state = State::MainLoop;
wd.feed();
let mut store_value_buf = [0u8; 1024];
match flash_store.write_value(CONFIG_KEY[0], &device_settings, &mut store_value_buf) {
Ok(()) => {
debug!("Device Settings is stored in flash");
}
Err(e) => {
debug!("Cannot Store Flash: {:?}", e);
}
}
}
State::PrepareForHardReset => {
// State Transition
state = State::HardReset;
wd.feed();
laser.power_down();
thermostat.power_down();
net::net::for_each(|mut socket, _| {
if net::net::eth_is_socket_active(socket, false) {
net::cmd_handler::send_response(
eth_data_buffer,
net::cmd_handler::ResponseEnum::HardReset,
None,
&mut socket,
);
net::net::eth_poll_iface();
}
});
}
State::PrepareForDfu => {
// State Transition
state = State::HardReset;
wd.feed();
laser.power_down();
thermostat.power_down();
net::net::for_each(|mut socket, _| {
if net::net::eth_is_socket_active(socket, false) {
net::cmd_handler::send_response(
eth_data_buffer,
net::cmd_handler::ResponseEnum::Dfu,
None,
&mut socket,
);
net::net::eth_poll_iface();
}
});
}
State::HardReset => {
wd.feed();
laser.power_down();
thermostat.power_down();
let mut any_socket_alive = false;
net::net::for_each(|socket, _| {
if net::net::eth_is_socket_active(socket, false) {
net::net::eth_close_socket(socket);
any_socket_alive = true;
}
});
// Must let loop run for one more cycle to poll server for RST to be sent,
// this makes sure system does not reset right after socket.abort() is called.
if !any_socket_alive {
SCB::sys_reset();
}
}
} }
} }
} }

895
src/net/cmd_handler.rs Normal file
View File

@ -0,0 +1,895 @@
use core::{fmt::Debug, marker::PhantomData};
use log::{debug, info};
use miniconf::{JsonCoreSlash, Tree};
use serde::{Deserialize, Serialize};
use smoltcp::iface::SocketHandle;
use uom::si::{electric_current::{ampere, ElectricCurrent},
electric_potential::{volt, ElectricPotential},
electrical_conductance::{siemens, ElectricalConductance},
electrical_resistance::{ohm, ElectricalResistance},
power::{watt, Power}};
use crate::{device::{dfu, hw_rev::HWRev, sys_timer},
laser_diode::{laser_diode::{LdDrive, LdSettingsSummary, StatusReport as LdStatusReport},
pd_mon_params::{self, ResponsitivityUnit}},
net::net,
thermostat::{ad7172::FilterType,
pid_state::PidSettings::*,
thermostat::{StatusReport as TecStatusReport, TempAdcFilter, Thermostat,
ThermostatSettingsSummary, Time}},
DeviceSettings, IpSettings, State};
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
pub enum ResponseEnum {
#[default]
Reserved,
Settings,
Report,
HwRev,
Interval,
Acknowledge,
InvalidDatatype,
InvalidSettings,
InvalidCmd,
HardReset,
Dfu,
ConnectionClose,
}
pub type MsgType = Option<&'static str>;
#[derive(Deserialize, Serialize, Copy, Clone, Debug)]
pub struct Response<'a> {
msg_type: ResponseEnum,
msg: Option<&'a str>,
}
impl Default for Response<'static> {
fn default() -> Self {
Response {
msg_type: ResponseEnum::Reserved,
msg: None,
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct ResponseObj<'a> {
#[serde(borrow)]
json: Response<'a>,
}
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
enum DeviceCmd {
#[default]
Reserved,
GetHwRev,
SetIPSettings,
SetActiveReportMode,
SetPdFinGain,
SetPdTransconductance,
GetStatusReport,
GetSettingsSummary,
Dfu,
SaveFlashSettings,
LoadFlashSettings,
HardReset,
}
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
enum LdCmdEnum {
#[default]
Reserved,
// LD Drive Related
SetDefaultPowerOn,
PowerUp,
PowerDown,
LdTermsShort,
LdTermsOpen,
SetI,
// PD Mon Related
SetPdResponsitivity,
SetPdDarkCurrent,
ApplyPdParams,
SetLdPwrLimit,
ClearAlarm,
}
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
enum ThermostatCmdEnum {
#[default]
Reserved,
SetDefaultPowerOn,
PowerUp,
PowerDown,
// TEC
SetTecMaxV,
SetTecMaxIPos,
SetTecMaxINeg,
SetTecIOut, // Constant Current Mode
SetTemperatureSetpoint,
// PID
SetPidEngage,
SetPidDisEngage,
SetPidKp,
SetPidKi,
SetPidKd,
SetPidOutMin,
SetPidOutMax,
SetPidUpdateInterval, // Update Interval is set based on the sampling rate of ADC
// Temperature ADC
ConfigTempAdcFilter,
GetPollInterval,
// TempMon
SetTempMonUpperLimit,
SetTempMonLowerLimit,
ClearAlarm,
// Steinhart-Hart Equation
SetShT0,
SetShR0,
SetShBeta,
}
const ERR_MSG_MISSING_DATA_F32: &str = "Required field \"data_f32\" does not exist";
const ERR_MSG_MISSING_DATA_BOOL: &str = "Required field \"bool\" does not exist";
const ERR_MSG_MISSING_IP_SETTINGS: &str = "Required field \"ip_settings\" does not exist";
const ERR_MSG_MISSING_TEMP_ADC_FILTER: &str = "Required field \"temp_adc_filter\" does not exist";
const ERR_MSG_MISSING_SINC5SINC1ODR: &str = "Required field \"sinc5sinc1odr\" does not exist";
const ERR_MSG_MISSING_SINC3ODR: &str = "Required field \"sinc3odr\" does not exist";
const ERR_MSG_MISSING_POSTFILTER: &str = "Required field \"PostFilter\" does not exist";
const ERR_MSG_MISSING_SINC3FINEODR: &str = "Required field \"sinc3fineodr\" does not exist";
const ERR_MSG_INVALID_PDMON_SETTINGS: &str = "Invalid PD Mon Parameter Setting(s)";
const ERR_MSG_INVALID_LD_PWR_LIMIT_SETTING: &str = "Invalid LD Power Limit Setting";
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct CmdJsonObj {
laser_diode_cmd: Option<LdCmdEnum>,
thermostat_cmd: Option<ThermostatCmdEnum>,
device_cmd: Option<DeviceCmd>,
data_bool: Option<bool>,
data_f32: Option<f32>,
data_f64: Option<f64>,
temp_adc_filter: Option<TempAdcFilter>,
ip_settings: Option<IpSettings>,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct Cmd {
json: CmdJsonObj,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReport {
ts: u32,
msg_type: ResponseEnum,
laser: LdStatusReport,
thermostat: TecStatusReport,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReportObj {
json: StatusReport,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct SettingsSummary {
msg_type: ResponseEnum,
laser: LdSettingsSummary,
thermostat: ThermostatSettingsSummary,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct SettingsSummaryObj {
json: SettingsSummary,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct IntervalType {
msg_type: ResponseEnum,
interval: Time,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct IntervalObj {
json: IntervalType,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct HwRevType {
msg_type: ResponseEnum,
hw_rev: HWRev,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct HwRevObj {
json: HwRevType,
}
pub fn send_response(buffer: &mut [u8], msg_type: ResponseEnum, msg: MsgType, socket: &mut SocketHandle) {
let response = ResponseObj {
json: Response {
msg_type: msg_type,
msg: msg,
},
};
debug!("{:?}", response.json);
let mut num_bytes = response.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
pub fn send_settings_summary(
buffer: &mut [u8],
laser: &mut LdDrive,
thermostat: &mut Thermostat,
socket: &mut SocketHandle,
) {
let settings_summary = SettingsSummaryObj {
json: SettingsSummary {
msg_type: ResponseEnum::Settings,
laser: laser.get_settings_summary(),
thermostat: thermostat.get_settings_summary(),
},
};
let mut num_bytes = settings_summary.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
pub fn send_status_report(
buffer: &mut [u8],
laser: &mut LdDrive,
thermostat: &mut Thermostat,
socket: &mut SocketHandle,
) {
let status_report = StatusReportObj {
json: StatusReport {
ts: sys_timer::now(),
msg_type: ResponseEnum::Report,
laser: laser.get_status_report(),
thermostat: thermostat.get_status_report(),
},
};
let mut num_bytes = status_report.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
pub fn send_hw_rev(buffer: &mut [u8], hw_rev_o: &mut HWRev, socket: &mut SocketHandle) {
let hw_rev = HwRevObj {
json: HwRevType {
msg_type: ResponseEnum::HwRev,
hw_rev: *hw_rev_o,
},
};
let mut num_bytes = hw_rev.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
pub fn send_interval(buffer: &mut [u8], interval: Time, socket: &mut SocketHandle) {
let hw_rev = IntervalObj {
json: IntervalType {
msg_type: ResponseEnum::Interval,
interval: interval,
},
};
let mut num_bytes = hw_rev.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
// Use a minimal struct for high speed cmd ctrl to reduce processing overhead
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct TecSetICmdJson {
tec_set_i: f32,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct TecSetICmd {
json: TecSetICmdJson,
}
/// Miniconf is very slow in debug builds (~3-4ms of cmd decoding time).
/// Make sure kirdy's firmware is flashed with release builds.
/// The received message must contain only one json cmd. TCP client should set TCP_NODELAY or equivalent flag in its TCP Socket
/// Settings to avoid unwanted buffering on TX Data and minimize TX latency.
pub fn execute_cmd(
buffer: &mut [u8],
buffer_size: usize,
socket: &mut SocketHandle,
hw_rev: &mut HWRev,
laser: &mut LdDrive,
thermostat: &mut Thermostat,
state: &mut State,
device_settings: &mut DeviceSettings,
active_report: &mut bool,
) {
let mut cmd = TecSetICmd {
json: TecSetICmdJson::default(),
};
match cmd.set_json("/json", &buffer[0..buffer_size]) {
Ok(_) => {
thermostat.set_i(ElectricCurrent::new::<ampere>(cmd.json.tec_set_i));
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
return;
}
Err(_) => { /* Do Nothing */ }
}
let mut cmd = Cmd {
json: CmdJsonObj::default(),
};
match cmd.set_json("/json", &buffer[0..buffer_size]) {
Ok(_) => {
info!(
"############ Laser Diode Command Received {:?}",
cmd.json.laser_diode_cmd
);
info!("############ Thermostat Command Received {:?}", cmd.json.thermostat_cmd);
info!("############ Device Command Received {:?}", cmd.json.device_cmd);
match cmd.json.device_cmd {
Some(DeviceCmd::SetIPSettings) => match cmd.json.ip_settings {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
device_settings.ip_settings = val;
*state = State::SaveDeviceSettings;
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_IP_SETTINGS),
socket,
);
}
},
Some(DeviceCmd::Dfu) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
unsafe {
dfu::set_dfu_trigger();
}
net::eth_poll_iface();
*state = State::PrepareForDfu;
}
Some(DeviceCmd::SetActiveReportMode) => match cmd.json.data_bool {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
*active_report = val;
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_BOOL),
socket,
);
}
},
Some(DeviceCmd::SetPdFinGain) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
device_settings.pd_mon_fin_gain = val;
laser.set_pd_transconductance(
device_settings.pd_mon_fin_gain * device_settings.pd_mon_transconductance,
);
*state = State::SaveDeviceSettings;
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(DeviceCmd::SetPdTransconductance) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
device_settings.pd_mon_transconductance = ElectricalConductance::new::<siemens>(val);
laser.set_pd_transconductance(
device_settings.pd_mon_fin_gain * device_settings.pd_mon_transconductance,
);
*state = State::SaveDeviceSettings;
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(DeviceCmd::GetHwRev) => {
send_hw_rev(buffer, hw_rev, socket);
}
Some(DeviceCmd::GetStatusReport) => {
send_status_report(buffer, laser, thermostat, socket);
}
Some(DeviceCmd::GetSettingsSummary) => {
send_settings_summary(buffer, laser, thermostat, socket);
}
Some(DeviceCmd::SaveFlashSettings) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
*state = State::SaveLdThermostatSettings;
}
Some(DeviceCmd::LoadFlashSettings) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
*state = State::LoadFlashSettings;
}
Some(DeviceCmd::HardReset) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
net::eth_poll_iface();
*state = State::PrepareForHardReset;
}
None => { /* Do Nothing */ }
_ => {
send_response(buffer, ResponseEnum::InvalidCmd, None, socket);
debug!("Unimplemented Command")
}
}
match cmd.json.laser_diode_cmd {
Some(LdCmdEnum::SetDefaultPowerOn) => match cmd.json.data_bool {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.set_default_pwr_on(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_BOOL),
socket,
);
}
},
Some(LdCmdEnum::PowerUp) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.power_up()
}
Some(LdCmdEnum::PowerDown) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.power_down()
}
Some(LdCmdEnum::LdTermsShort) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.ld_short();
}
Some(LdCmdEnum::LdTermsOpen) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.ld_open();
}
Some(LdCmdEnum::SetI) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.ld_set_i(ElectricCurrent::new::<ampere>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::SetPdResponsitivity) => match cmd.json.data_f32 {
Some(val) => {
laser.set_pd_responsitivity(ResponsitivityUnit {
dimension: PhantomData,
units: PhantomData,
value: val,
});
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::SetPdDarkCurrent) => match cmd.json.data_f32 {
Some(val) => {
laser.set_pd_dark_current(ElectricCurrent::new::<ampere>(val));
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::ApplyPdParams) => {
let is_legal = laser.apply_pd_params();
if is_legal {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
} else {
send_response(
buffer,
ResponseEnum::InvalidSettings,
Some(ERR_MSG_INVALID_PDMON_SETTINGS),
socket,
)
}
}
Some(LdCmdEnum::SetLdPwrLimit) => match cmd.json.data_f32 {
Some(val) => {
let is_legal = laser.set_ld_power_limit(Power::new::<watt>(val));
if is_legal {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
} else {
send_response(
buffer,
ResponseEnum::InvalidSettings,
Some(ERR_MSG_INVALID_LD_PWR_LIMIT_SETTING),
socket,
)
}
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(LdCmdEnum::ClearAlarm) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
laser.pd_mon_clear_alarm()
}
None => { /* Do Nothing*/ }
_ => {
send_response(buffer, ResponseEnum::InvalidCmd, Some(ERR_MSG_MISSING_DATA_F32), socket);
info!("Unimplemented Command")
}
}
match cmd.json.thermostat_cmd {
Some(ThermostatCmdEnum::SetDefaultPowerOn) => match cmd.json.data_bool {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_default_pwr_on(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::PowerUp) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.power_up()
}
Some(ThermostatCmdEnum::PowerDown) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.power_down()
}
Some(ThermostatCmdEnum::SetTecMaxV) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_max_v(ElectricPotential::new::<volt>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTecMaxIPos) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_max_i_pos(ElectricCurrent::new::<ampere>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTecMaxINeg) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_max_i_neg(ElectricCurrent::new::<ampere>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTecIOut) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_i(ElectricCurrent::new::<ampere>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTemperatureSetpoint) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temperature_setpoint(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidEngage) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid_engaged(true);
}
Some(ThermostatCmdEnum::SetPidDisEngage) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid_engaged(false);
}
Some(ThermostatCmdEnum::SetPidKp) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Kp, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidKi) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Ki, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidKd) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Kd, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidOutMin) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Min, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidOutMax) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_pid(Max, val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetPidUpdateInterval) => {
send_response(buffer, ResponseEnum::InvalidCmd, None, socket);
debug!("Not supported Yet")
}
Some(ThermostatCmdEnum::ConfigTempAdcFilter) => match cmd.json.temp_adc_filter {
Some(val) => match val.filter_type {
FilterType::Sinc5Sinc1With50hz60HzRejection => match val.sinc5sinc1postfilter {
Some(val2) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_adc_sinc5_sinc1_with_postfilter(0, val2);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_POSTFILTER),
socket,
);
}
},
FilterType::Sinc5Sinc1 => match val.sinc5sinc1odr {
Some(val2) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_adc_sinc5_sinc1_filter(0, val2);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_SINC5SINC1ODR),
socket,
);
}
},
FilterType::Sinc3WithFineODR => match val.sinc3fineodr {
Some(val2) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_adc_sinc3_fine_filter(0, val2);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_SINC3FINEODR),
socket,
);
}
},
FilterType::Sinc3 => match val.sinc3odr {
Some(val2) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_adc_sinc3_filter(0, val2);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_SINC3ODR),
socket,
);
}
},
},
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_TEMP_ADC_FILTER),
socket,
);
}
},
Some(ThermostatCmdEnum::GetPollInterval) => {
send_interval(buffer, thermostat.get_poll_interval(), socket)
}
Some(ThermostatCmdEnum::SetTempMonUpperLimit) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_mon_upper_limit(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetTempMonLowerLimit) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_temp_mon_lower_limit(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::ClearAlarm) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.clear_temp_mon_alarm();
}
Some(ThermostatCmdEnum::SetShT0) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_sh_t0(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetShR0) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_sh_r0(ElectricalResistance::new::<ohm>(val));
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
Some(ThermostatCmdEnum::SetShBeta) => match cmd.json.data_f32 {
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
thermostat.set_sh_beta(val);
}
None => {
send_response(
buffer,
ResponseEnum::InvalidDatatype,
Some(ERR_MSG_MISSING_DATA_F32),
socket,
);
}
},
None => { /* Do Nothing*/ }
_ => {
send_response(buffer, ResponseEnum::InvalidCmd, None, socket);
}
}
}
Err(_) => {
info!("cmd_recv: {:?}", buffer);
send_response(buffer, ResponseEnum::InvalidCmd, None, socket);
}
}
}

2
src/net/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod cmd_handler;
pub mod net;

668
src/net/net.rs Normal file
View File

@ -0,0 +1,668 @@
use core::mem::{self, MaybeUninit};
use fugit::TimerDurationU32;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use smoltcp::{iface::{self, Interface, SocketHandle, SocketSet, SocketStorage},
socket::tcp::{Socket, SocketBuffer, State},
time::{Duration, Instant},
wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Cidr}};
use stm32_eth::{dma::{EthernetDMA, RxRingEntry, TxRingEntry},
mac::{EthernetMACWithMii, Speed},
EthPins, Parts, PartsIn};
use stm32f4xx_hal::{gpio::{gpioa::*, gpiob::*, gpioc::*, Alternate, Input, Pin},
pac::{interrupt, Interrupt, Peripherals, TIM5},
rcc::Clocks,
timer::{Counter, Event},
Listen};
use crate::device::sys_timer;
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub struct IpSettings {
addr: [u8; 4],
port: u16,
prefix_len: u8,
gateway: [u8; 4],
}
impl Default for IpSettings {
fn default() -> Self {
IpSettings {
addr: [192, 168, 1, 128],
port: 1337,
prefix_len: 24,
gateway: [192, 168, 1, 1],
}
}
}
static mut IFACE_TIMER: Option<IfacePollTimer> = None;
pub struct ServerHandle {
socket_handles: [SocketHandle; NUM_OF_SOCKETS],
socket_set: SocketSet<'static>,
socket_addr: (IpAddress, u16),
iface: EthInterface,
dma: EthernetDMA<'static, 'static>,
phy: EthernetPhy<EthernetMACWithMii<Pin<'A', 2, Alternate<11>>, Pin<'C', 1, Alternate<11>>>>,
link_was_up: bool,
data_sent: bool,
}
pub type EthernetPins = EthPins<PA1<Input>, PA7<Input>, PB11<Input>, PB12<Input>, PB13<Input>, PC4<Input>, PC5<Input>>;
pub struct EthernetMgmtPins {
pub mdio: PA2<Alternate<11>>,
pub mdc: PC1<Alternate<11>>,
}
pub type EthInterface = Interface;
pub const NUM_OF_SOCKETS: usize = 4 + 1;
const TCP_BUFFER_SIZE: usize = 4096;
static mut RX_RING: Option<[RxRingEntry; 8]> = None;
static mut TX_RING: Option<[TxRingEntry; 8]> = None;
static mut SOCKET_STORAGE: Option<[SocketStorage<'static>; NUM_OF_SOCKETS]> = None;
fn now_fn() -> smoltcp::time::Instant {
Instant::from_millis(i64::from(sys_timer::now()))
}
static mut SERVER_HANDLE: Option<ServerHandle> = None;
pub const FREQ: u32 = 1000000;
impl ServerHandle {
pub fn new(
eth_pins: EthernetPins,
eth_mgmt_pins: EthernetMgmtPins,
ethernet_parts_in: PartsIn,
clocks: Clocks,
tim5: Counter<TIM5, FREQ>,
mac_addr: [u8; 6],
ip_settings: IpSettings,
) {
let rx_ring = unsafe { RX_RING.get_or_insert(Default::default()) };
let tx_ring = unsafe { TX_RING.get_or_insert(Default::default()) };
let socket_storage = unsafe { SOCKET_STORAGE.get_or_insert([SocketStorage::EMPTY; NUM_OF_SOCKETS]) };
let Parts { mut dma, mac, .. } = stm32_eth::new_with_mii(
ethernet_parts_in,
&mut rx_ring[..],
&mut tx_ring[..],
clocks,
eth_pins,
eth_mgmt_pins.mdio,
eth_mgmt_pins.mdc,
)
.unwrap();
let ip_init = IpCidr::Ipv4(Ipv4Cidr::new(
Ipv4Address::new(
ip_settings.addr[0],
ip_settings.addr[1],
ip_settings.addr[2],
ip_settings.addr[3],
),
ip_settings.prefix_len,
));
let socket_addr: (IpAddress, u16) = (
IpAddress::Ipv4(Ipv4Address::new(
ip_settings.addr[0],
ip_settings.addr[1],
ip_settings.addr[2],
ip_settings.addr[3],
)),
ip_settings.port,
);
let mut routes = smoltcp::iface::Routes::new();
routes
.add_default_ipv4_route(Ipv4Address::new(
ip_settings.gateway[0],
ip_settings.gateway[1],
ip_settings.gateway[2],
ip_settings.gateway[3],
))
.ok();
dma.enable_interrupt();
let config = iface::Config::new(EthernetAddress::from_bytes(&mac_addr).into());
let mut iface = Interface::new(config, &mut &mut dma, smoltcp::time::Instant::ZERO);
iface.set_hardware_addr(EthernetAddress(mac_addr).into());
debug!("MAC ADDRESS: {:02X?}", EthernetAddress(mac_addr));
debug!("IP Settings: {:?}", ip_settings);
iface.update_ip_addrs(|addr| {
addr.push(ip_init).unwrap();
});
let mut socket_set = SocketSet::new(&mut socket_storage[..]);
let tcp_handles = {
// Do not use NUM_OF_SOCKETS to define array size to
// remind developers to create/remove tcp_handles accordingly after changing NUM_OF_SOCKETS
let mut tcp_handles: [MaybeUninit<SocketHandle>; 5] = unsafe { MaybeUninit::uninit().assume_init() };
macro_rules! create_tcp_handle {
($rx_storage:ident, $tx_storage:ident, $handle:expr) => {
static mut $rx_storage: [u8; TCP_BUFFER_SIZE] = [0; TCP_BUFFER_SIZE];
static mut $tx_storage: [u8; TCP_BUFFER_SIZE] = [0; TCP_BUFFER_SIZE];
unsafe {
let rx_buffer = SocketBuffer::new(&mut $rx_storage[..]);
let tx_buffer = SocketBuffer::new(&mut $tx_storage[..]);
$handle.write(socket_set.add(Socket::new(rx_buffer, tx_buffer)));
}
};
}
create_tcp_handle!(RX_STORAGE0, TX_STORAGE0, tcp_handles[0]);
create_tcp_handle!(RX_STORAGE1, TX_STORAGE1, tcp_handles[1]);
create_tcp_handle!(RX_STORAGE2, TX_STORAGE2, tcp_handles[2]);
create_tcp_handle!(RX_STORAGE3, TX_STORAGE3, tcp_handles[3]);
create_tcp_handle!(RX_STORAGE4, TX_STORAGE4, tcp_handles[4]);
unsafe { mem::transmute::<_, [SocketHandle; 5]>(tcp_handles) }
};
for i in 0..NUM_OF_SOCKETS {
let socket = socket_set.get_mut::<Socket>(tcp_handles[i]);
socket.listen(socket_addr).ok();
socket.set_timeout(Some(Duration::from_secs(5)));
// Value from Linux net.ipv4.tcp_keepalive_time
socket.set_keep_alive(Some(Duration::from_secs(7200)));
socket.set_nagle_enabled(false);
}
iface.poll(
Instant::from_millis(i64::from(sys_timer::now())),
&mut &mut dma,
&mut socket_set,
);
IfacePollTimer::setup(tim5);
if let Ok(mut phy) = EthernetPhy::from_miim(mac, 0) {
info!("Resetting PHY as an extra step. Type: {}", phy.ident_string());
phy.phy_init();
let server = ServerHandle {
socket_handles: tcp_handles,
socket_set: socket_set,
socket_addr: socket_addr,
iface: iface,
dma: dma,
phy: phy,
link_was_up: false,
data_sent: true,
};
unsafe {
SERVER_HANDLE = Some(server);
}
} else {
panic!("Ethernet Phy is not supported");
}
}
pub fn update_link_speed(&mut self) -> bool {
if !self.link_was_up & self.phy.phy_link_up() {
if let Some(speed) = self.phy.speed().map(|s| match s {
PhySpeed::HalfDuplexBase10T => Speed::HalfDuplexBase10T,
PhySpeed::FullDuplexBase10T => Speed::FullDuplexBase10T,
PhySpeed::HalfDuplexBase100Tx => Speed::HalfDuplexBase100Tx,
PhySpeed::FullDuplexBase100Tx => Speed::FullDuplexBase100Tx,
}) {
info!("New eth link is up. Setting detected PhySpeed: {:?}", speed);
self.phy.get_miim().set_speed(speed);
self.link_was_up = self.phy.phy_link_up();
return true;
} else {
debug!("Failed to detect link speed.");
}
}
self.link_was_up = self.phy.phy_link_up();
return false;
}
pub fn poll_iface(&mut self) {
cortex_m::interrupt::free(|_| {
self.iface.poll(now_fn(), &mut &mut self.dma, &mut self.socket_set);
});
}
pub fn poll_at_iface(&mut self) -> Option<Instant> {
self.iface.poll_at(now_fn(), &mut self.socket_set)
}
pub fn poll_delay_iface(&mut self) -> Option<Duration> {
self.iface.poll_delay(now_fn(), &mut self.socket_set)
}
pub fn recv(
&mut self,
buffer: &mut [u8],
socket_handles: SocketHandle,
) -> Result<usize, smoltcp::socket::tcp::RecvError> {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
socket.recv_slice(buffer)
}
pub fn send(&mut self, buffer: &mut [u8], num_bytes: usize, socket_handles: SocketHandle) {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
if num_bytes > 0 {
cortex_m::interrupt::free(|_| {
match socket.send_slice(&buffer[..num_bytes]) {
Ok(_) => {
self.data_sent = false;
info!("Enqueued {} bytes.", num_bytes);
}
Err(err) => {
info!("Bytes cannot be sent. Error: {:?}", err)
}
};
});
}
}
pub fn is_socket_connected(&mut self, socket_handles: SocketHandle) -> bool {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
socket.state() == State::Established
}
pub fn poll_socket_status(&mut self, socket_handles: SocketHandle, reopen_sock: bool) -> bool {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
if !socket.is_listening() && !socket.is_open() || socket.state() == State::CloseWait {
if reopen_sock {
socket.abort();
socket.listen(self.socket_addr).ok();
info!("Disconnected... Reopening listening socket.");
}
return false;
} else if socket.state() == State::Closed || socket.state() == State::Closing || socket.is_listening() {
return false;
}
return true;
}
pub fn close_socket(&mut self, socket_handles: SocketHandle) {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
socket.abort();
}
pub fn can_send(&mut self, socket_handles: SocketHandle) -> bool {
self.socket_set.get_mut::<Socket>(socket_handles).can_send()
}
pub fn can_recv(&mut self, socket_handles: SocketHandle) -> bool {
self.socket_set.get_mut::<Socket>(socket_handles).can_recv()
}
}
use ieee802_3_miim::{phy::{lan87xxa::{LAN8720A, LAN8742A},
BarePhy, PhySpeed, KSZ8081R},
Miim, Pause, Phy};
/// An ethernet PHY
pub enum EthernetPhy<M: Miim> {
/// LAN8720A
LAN8720A(LAN8720A<M>),
/// LAN8742A
LAN8742A(LAN8742A<M>),
/// KSZ8081R
KSZ8081R(KSZ8081R<M>),
}
impl<M: Miim> Phy<M> for EthernetPhy<M> {
fn best_supported_advertisement(&self) -> ieee802_3_miim::AutoNegotiationAdvertisement {
unimplemented!()
}
fn get_miim(&mut self) -> &mut M {
match self {
EthernetPhy::LAN8720A(phy) => phy.get_miim(),
EthernetPhy::LAN8742A(phy) => phy.get_miim(),
EthernetPhy::KSZ8081R(phy) => phy.get_miim(),
}
}
fn get_phy_addr(&self) -> u8 {
match self {
EthernetPhy::LAN8720A(phy) => phy.get_phy_addr(),
EthernetPhy::LAN8742A(phy) => phy.get_phy_addr(),
EthernetPhy::KSZ8081R(phy) => phy.get_phy_addr(),
}
}
}
impl<M: Miim> EthernetPhy<M> {
/// Attempt to create one of the known PHYs from the given
/// MIIM.
///
/// Returns an error if the PHY does not support the extended register
/// set, or if the PHY's identifier does not correspond to a known PHY.
pub fn from_miim(miim: M, phy_addr: u8) -> Result<Self, M> {
let mut bare = BarePhy::new(miim, phy_addr, Pause::NoPause);
let phy_ident = if let Some(id) = bare.phy_ident() {
id.raw_u32()
} else {
return Err(bare.release());
};
let miim = bare.release();
match phy_ident & 0xFFFFFFF0 {
0x0007C0F0 => Ok(Self::LAN8720A(LAN8720A::new(miim, phy_addr))),
0x0007C130 => Ok(Self::LAN8742A(LAN8742A::new(miim, phy_addr))),
0x00221560 => Ok(Self::KSZ8081R(KSZ8081R::new(miim, phy_addr))),
_ => Err(miim),
}
}
/// Get a string describing the type of PHY
pub const fn ident_string(&self) -> &'static str {
match self {
EthernetPhy::LAN8720A(_) => "LAN8720A",
EthernetPhy::LAN8742A(_) => "LAN8742A",
EthernetPhy::KSZ8081R(_) => "KSZ8081R",
}
}
/// Initialize the PHY
pub fn phy_init(&mut self) {
match self {
EthernetPhy::LAN8720A(phy) => phy.phy_init(),
EthernetPhy::LAN8742A(phy) => phy.phy_init(),
EthernetPhy::KSZ8081R(phy) => {
phy.set_autonegotiation_advertisement(phy.best_supported_advertisement());
}
}
}
#[allow(dead_code)]
pub fn speed(&mut self) -> Option<ieee802_3_miim::phy::PhySpeed> {
match self {
EthernetPhy::LAN8720A(phy) => phy.link_speed(),
EthernetPhy::LAN8742A(phy) => phy.link_speed(),
EthernetPhy::KSZ8081R(phy) => phy.link_speed(),
}
}
#[allow(dead_code)]
pub fn release(self) -> M {
match self {
EthernetPhy::LAN8720A(phy) => phy.release(),
EthernetPhy::LAN8742A(phy) => phy.release(),
EthernetPhy::KSZ8081R(phy) => phy.release(),
}
}
}
pub fn eth_can_sock_send(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.can_send(socket_handles)
} else {
panic!("eth_check_if_sock_can_send is called before init");
}
}
}
pub fn eth_can_sock_recv(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.can_recv(socket_handles)
} else {
panic!("eth_check_if_sock_can_recv is called before init");
}
}
}
pub fn eth_poll_link_status_and_update_link_speed() -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
let new_link_is_up = server_handle.update_link_speed();
if new_link_is_up {
info!("Resetting TCP Sockets");
for_each(|socket, _| {
eth_close_socket(socket);
});
}
return new_link_is_up;
} else {
panic!("eth_poll_link_status_and_update_link_speed is called before init");
}
}
}
pub fn eth_update_iface_poll_timer() {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
IfacePollTimer::set_timer(server_handle.poll_at_iface(), server_handle.poll_delay_iface())
} else {
panic!("eth_update_iface_poll_timer is called before init")
}
}
}
pub fn eth_poll_iface() {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.poll_iface();
} else {
panic!("eth_poll_packet is called before init");
}
}
}
pub fn eth_send(buffer: &mut [u8], num_bytes: usize, socket_handles: SocketHandle) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.send(buffer, num_bytes, socket_handles);
} else {
panic!("eth_send is called before init");
}
}
}
pub fn eth_recv(buffer: &mut [u8], socket_handles: SocketHandle) -> usize {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
match server_handle.recv(buffer, socket_handles) {
Ok(recv_bytes) => return recv_bytes,
Err(err) => {
debug!("TCP Recv Error: {}", err);
return 0;
}
};
} else {
panic!("eth_send is called before init");
}
}
}
pub fn eth_is_socket_connected(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.is_socket_connected(socket_handles)
} else {
panic!("eth_is_socket_connected is called before init");
}
}
}
pub fn eth_is_socket_active(socket_handles: SocketHandle, reopen_sock: bool) -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.poll_socket_status(socket_handles, reopen_sock)
} else {
panic!("eth_is_socket_active is called before init");
}
}
}
pub fn eth_close_socket(socket_handles: SocketHandle) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.close_socket(socket_handles)
} else {
panic!("eth_close_socket is called before init");
}
}
}
pub fn eth_get_sock_handle(id: usize) -> SocketHandle {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.socket_handles[id]
} else {
panic!("eth_get_sock_handle is called before init");
}
}
}
pub fn eth_close_socket_by_id(id: usize) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.close_socket(server_handle.socket_handles[id])
} else {
panic!("eth_close_socket_by_id is called before init");
}
}
}
pub fn eth_is_data_sent() -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.data_sent
} else {
panic!("eth_is_data_sent is called before init");
}
}
}
fn eth_set_data_sent(val: bool) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.data_sent = val;
} else {
panic!("eth_is_data_sent is called before init");
}
}
}
pub fn for_each<F: FnMut(SocketHandle, usize)>(mut callback: F) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
for i in 0..NUM_OF_SOCKETS {
callback(server_handle.socket_handles[i], i);
}
} else {
panic!("eth_close_socket is called before init");
}
}
}
/// Potentially wake up from `wfi()`, set the interrupt pending flag,
/// clear interrupt flags.
#[interrupt]
fn ETH() {
let interrupt_reason = stm32_eth::eth_interrupt_handler();
if interrupt_reason.rx {
eth_poll_iface();
}
if interrupt_reason.tx {
eth_set_data_sent(true);
}
debug!("Ethernet Interrupt{:?}", interrupt_reason);
}
pub struct IfacePollTimer {
timer: Counter<TIM5, FREQ>,
poll_at_set: Instant,
}
impl IfacePollTimer {
fn get() -> Option<&'static mut Self> {
unsafe { IFACE_TIMER.as_mut() }
}
pub fn setup(tim5: Counter<TIM5, FREQ>) {
unsafe {
cortex_m::peripheral::NVIC::unmask(Interrupt::TIM5);
IFACE_TIMER = Some(IfacePollTimer {
timer: tim5,
poll_at_set: Instant::from_micros(u32::max_value()),
})
}
}
fn is_timer_running() -> bool {
unsafe { Peripherals::steal().TIM5.cr1.read().cen().bit() }
}
pub fn set_timer(poll_at: Option<Instant>, delay: Option<Duration>) {
if let Some(ref mut iface_timer) = IfacePollTimer::get() {
if !IfacePollTimer::is_timer_running() {
match poll_at {
Some(val) => {
IfacePollTimer::set_timer_delay(delay);
iface_timer.poll_at_set = val;
}
None => {}
}
} else {
match poll_at {
Some(val) => {
if iface_timer.poll_at_set != val {
IfacePollTimer::stop_listening();
IfacePollTimer::set_timer_delay(delay);
iface_timer.poll_at_set = val;
}
}
None => {}
}
}
}
}
fn set_timer_delay(delay: Option<Duration>) {
if let Some(ref mut iface_timer) = IfacePollTimer::get() {
match delay {
Some(val) => {
if val.micros() != 0 {
match iface_timer.timer.start(TimerDurationU32::micros(val.micros() as u32)) {
Ok(_) => {
iface_timer.timer.listen(Event::Update);
}
Err(e) => {
debug!("Timer cannot be set {:?}", e)
}
};
} else {
eth_poll_iface();
}
}
None => {}
}
}
}
pub fn clear_irq_flag() {
if let Some(ref mut iface_timer) = IfacePollTimer::get() {
iface_timer.timer.wait().ok();
}
}
pub fn stop_listening() {
if let Some(ref mut iface_timer) = IfacePollTimer::get() {
iface_timer.timer.unlisten(Event::Update);
iface_timer.timer.cancel().ok();
}
}
}
#[interrupt]
fn TIM5() {
eth_poll_iface();
cortex_m::interrupt::free(|_| {
IfacePollTimer::stop_listening();
IfacePollTimer::clear_irq_flag();
});
}

View File

@ -1 +0,0 @@
pub mod network;

View File

@ -1,296 +0,0 @@
// Modified by Wong Tat Hang (aw@m-labs.hk), with original source from
// https://github.com/stm32-rs/stm32-eth/blob/master/examples/rtic-echo.rs
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use log::{info, warn};
use smoltcp::{
iface::{
Interface, InterfaceBuilder, Neighbor, NeighborCache, Route, SocketHandle, SocketStorage,
},
socket::{TcpSocket, TcpSocketBuffer, TcpState},
wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Cidr},
};
use stm32_eth::stm32::{ETHERNET_DMA, ETHERNET_MAC, ETHERNET_MMC};
use stm32_eth::*;
use stm32f4xx_hal::{
gpio::{gpioa::*, gpiob::*, gpioc::*, Alternate, Input},
rcc::Clocks,
};
use crate::app::monotonics;
pub type EthernetPins =
EthPins<PA1<Input>, PA7<Input>, PB11<Input>, PB12<Input>, PB13<Input>, PC4<Input>, PC5<Input>>;
pub type EthInterface = Interface<'static, &'static mut EthernetDMA<'static, 'static>>;
const IPV4_ADDR: (u8, u8, u8, u8) = (192, 168, 1, 132);
const ADDRESS: (IpAddress, u16) = (
IpAddress::Ipv4(Ipv4Address::new(
IPV4_ADDR.0,
IPV4_ADDR.1,
IPV4_ADDR.2,
IPV4_ADDR.3,
)),
1337,
);
const MAC: [u8; 6] = [0x02, 0x5f, 0x25, 0x37, 0x93, 0x0e];
pub struct EthernetPeripherals {
pub dma: ETHERNET_DMA,
pub mac: ETHERNET_MAC,
pub mmc: ETHERNET_MMC,
}
pub struct ServerHandle {
// storage: &'static mut ServerStorage,
socket_handle: SocketHandle,
iface: EthInterface,
}
pub struct ServerStorage {
rx_ring: [RxRingEntry; 8],
tx_ring: [TxRingEntry; 2],
storage: NetworkStorage,
dma: core::mem::MaybeUninit<EthernetDMA<'static, 'static>>,
}
impl ServerStorage {
pub const fn new() -> Self {
ServerStorage {
rx_ring: [
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
RxRingEntry::new(),
],
tx_ring: [TxRingEntry::new(), TxRingEntry::new()],
storage: NetworkStorage::new(),
dma: core::mem::MaybeUninit::uninit(),
}
}
}
/// All storage required for networking
pub struct NetworkStorage {
pub ip_addrs: [IpCidr; 1],
pub sockets: [SocketStorage<'static>; 1],
pub tcp_socket_storage: TcpSocketStorage,
pub neighbor_cache: [Option<(IpAddress, Neighbor)>; 8],
pub routes_cache: [Option<(IpCidr, Route)>; 8],
}
impl NetworkStorage {
const IP_INIT: IpCidr = IpCidr::Ipv4(Ipv4Cidr::new(
Ipv4Address::new(IPV4_ADDR.0, IPV4_ADDR.1, IPV4_ADDR.2, IPV4_ADDR.3),
24,
));
pub const fn new() -> Self {
NetworkStorage {
ip_addrs: [Self::IP_INIT],
neighbor_cache: [None; 8],
routes_cache: [None; 8],
sockets: [SocketStorage::EMPTY; 1],
tcp_socket_storage: TcpSocketStorage::new(),
}
}
}
/// Storage of TCP sockets
#[derive(Copy, Clone)]
pub struct TcpSocketStorage {
rx_storage: [u8; 2048],
tx_storage: [u8; 2048],
}
impl TcpSocketStorage {
const fn new() -> Self {
Self {
rx_storage: [0; 2048],
tx_storage: [0; 2048],
}
}
}
fn now_fn() -> smoltcp::time::Instant {
let time = monotonics::now().duration_since_epoch().to_millis();
smoltcp::time::Instant::from_millis(time as i64)
}
impl ServerHandle {
pub fn new(
eth_pins: EthernetPins,
ethernet: EthernetPeripherals,
clocks: Clocks,
storage: &'static mut ServerStorage,
mdio: stm32f4xx_hal::gpio::PA2<Alternate<11>>,
mdc: stm32f4xx_hal::gpio::PC1<Alternate<11>>,
) -> ServerHandle {
let (dma, mac) = stm32_eth::new_with_mii(
ethernet.mac,
ethernet.mmc,
ethernet.dma,
&mut storage.rx_ring[..],
&mut storage.tx_ring[..],
clocks,
eth_pins,
mdio,
mdc,
)
.unwrap();
let mut routes = smoltcp::iface::Routes::new(&mut storage.storage.routes_cache[..]);
routes
.add_default_ipv4_route(Ipv4Address::new(192, 168, 1, 1))
.ok();
let dma = storage.dma.write(dma);
dma.enable_interrupt();
let rx_buffer =
TcpSocketBuffer::new(&mut storage.storage.tcp_socket_storage.rx_storage[..]);
let tx_buffer =
TcpSocketBuffer::new(&mut storage.storage.tcp_socket_storage.tx_storage[..]);
let socket = TcpSocket::new(rx_buffer, tx_buffer);
let mut iface = InterfaceBuilder::new(dma, &mut storage.storage.sockets[..])
.hardware_addr(EthernetAddress(MAC).into())
.ip_addrs(&mut storage.storage.ip_addrs[..])
.neighbor_cache(NeighborCache::new(&mut storage.storage.neighbor_cache[..]))
.routes(routes)
.finalize();
let mut server = ServerHandle {
socket_handle: iface.add_socket(socket),
iface: iface,
};
let socket = server.iface.get_socket::<TcpSocket>(server.socket_handle);
socket.listen(ADDRESS).ok();
server.iface.poll(now_fn()).ok();
if let Ok(mut phy) = EthernetPhy::from_miim(mac, 0) {
info!(
"Resetting PHY as an extra step. Type: {}",
phy.ident_string()
);
phy.phy_init();
} else {
info!("Not resetting unsupported PHY.");
}
server
}
pub fn poll(&mut self, buffer: &mut [u8]) {
self.iface.poll(now_fn()).ok();
let socket = self.iface.get_socket::<TcpSocket>(self.socket_handle);
if let Ok(recv_bytes) = socket.recv_slice(buffer) {
if recv_bytes > 0 {
socket.send_slice(&buffer[..recv_bytes]).ok();
info!("Echoed {} bytes.", recv_bytes);
}
}
if !socket.is_listening() && !socket.is_open() || socket.state() == TcpState::CloseWait {
socket.abort();
socket.listen(ADDRESS).ok();
warn!("Disconnected... Reopening listening socket.");
}
self.iface.poll(now_fn()).ok();
}
}
use ieee802_3_miim::{
phy::{
lan87xxa::{LAN8720A, LAN8742A},
BarePhy, KSZ8081R,
},
Miim, Pause, Phy,
};
/// An ethernet PHY
pub enum EthernetPhy<M: Miim> {
/// LAN8720A
LAN8720A(LAN8720A<M>),
/// LAN8742A
LAN8742A(LAN8742A<M>),
/// KSZ8081R
KSZ8081R(KSZ8081R<M>),
}
impl<M: Miim> Phy<M> for EthernetPhy<M> {
fn best_supported_advertisement(&self) -> ieee802_3_miim::AutoNegotiationAdvertisement {
unimplemented!()
}
fn get_miim(&mut self) -> &mut M {
match self {
EthernetPhy::LAN8720A(phy) => phy.get_miim(),
EthernetPhy::LAN8742A(phy) => phy.get_miim(),
EthernetPhy::KSZ8081R(phy) => phy.get_miim(),
}
}
fn get_phy_addr(&self) -> u8 {
match self {
EthernetPhy::LAN8720A(phy) => phy.get_phy_addr(),
EthernetPhy::LAN8742A(phy) => phy.get_phy_addr(),
EthernetPhy::KSZ8081R(phy) => phy.get_phy_addr(),
}
}
}
impl<M: Miim> EthernetPhy<M> {
/// Attempt to create one of the known PHYs from the given
/// MIIM.
///
/// Returns an error if the PHY does not support the extended register
/// set, or if the PHY's identifier does not correspond to a known PHY.
pub fn from_miim(miim: M, phy_addr: u8) -> Result<Self, ()> {
let mut bare = BarePhy::new(miim, phy_addr, Pause::NoPause);
let phy_ident = bare.phy_ident().ok_or(())?;
let miim = bare.release();
match phy_ident & 0xFFFFFFF0 {
0x0007C0F0 => Ok(Self::LAN8720A(LAN8720A::new(miim, phy_addr))),
0x0007C130 => Ok(Self::LAN8742A(LAN8742A::new(miim, phy_addr))),
0x00221560 => Ok(Self::KSZ8081R(KSZ8081R::new(miim, phy_addr))),
_ => Err(()),
}
}
/// Get a string describing the type of PHY
pub const fn ident_string(&self) -> &'static str {
match self {
EthernetPhy::LAN8720A(_) => "LAN8720A",
EthernetPhy::LAN8742A(_) => "LAN8742A",
EthernetPhy::KSZ8081R(_) => "KSZ8081R",
}
}
/// Initialize the PHY
pub fn phy_init(&mut self) {
match self {
EthernetPhy::LAN8720A(phy) => phy.phy_init(),
EthernetPhy::LAN8742A(phy) => phy.phy_init(),
EthernetPhy::KSZ8081R(phy) => {
phy.set_autonegotiation_advertisement(phy.best_supported_advertisement());
}
}
}
}

View File

46
src/thermostat/ad5680.rs Normal file
View File

@ -0,0 +1,46 @@
use fugit::MegahertzU32;
use stm32f4xx_hal::{hal::{digital::OutputPin, spi::SpiBus},
spi};
use crate::device::sys_timer::sleep;
/// SPI Mode 1
pub const SPI_MODE: spi::Mode = spi::Mode {
polarity: spi::Polarity::IdleLow,
phase: spi::Phase::CaptureOnSecondTransition,
};
// MAX Clock Frequency: 30MHz | SPI1 Max Frequency 42MHz
pub const SPI_CLOCK_MHZ: MegahertzU32 = MegahertzU32::from_raw(21);
pub const MAX_VALUE: u32 = 0x3FFFF;
pub struct Dac<SPI: SpiBus<u8>, S: OutputPin> {
spi: SPI,
sync: S,
}
impl<SPI: SpiBus<u8>, S: OutputPin> Dac<SPI, S> {
pub fn new(spi: SPI, mut sync: S) -> Self {
let _ = sync.set_low();
Dac { spi, sync }
}
fn write(&mut self, buf: &mut [u8]) -> Result<(), SPI::Error> {
// pulse sync to start a new transfer. leave sync idle low
// afterwards to save power as recommended per datasheet.
let _ = self.sync.set_high();
// must be high for >= 33 ns
sleep(1);
let _ = self.sync.set_low();
self.spi.transfer_in_place(buf)?;
Ok(())
}
pub fn set(&mut self, value: u32) -> Result<u32, SPI::Error> {
let value = value.min(MAX_VALUE);
let mut buf = [(value >> 14) as u8, (value >> 6) as u8, (value << 2) as u8];
self.write(&mut buf)?;
Ok(value)
}
}

View File

@ -0,0 +1,351 @@
use core::fmt;
use log::{info, warn};
use stm32f4xx_hal::{gpio::{Output, PushPull, PA15},
hal::{digital::OutputPin, spi::SpiBus},
pac::SPI3,
spi::Spi};
use uom::si::{electric_potential::volt, f32::ElectricPotential};
use super::{checksum::{Checksum, ChecksumMode},
regs::{self, Register, RegisterData},
sinc3_fine_odr_closest, sinc3_fine_odr_output_rate, DigitalFilterOrder, FilterType, Input, Mode,
PostFilter, RefSource, SingleChODR};
/// AD7172-2 implementation
///
/// [Manual](https://www.analog.com/media/en/technical-documentation/data-sheets/AD7172-2.pdf)
pub struct Adc<SPI: SpiBus<u8>, NSS: OutputPin> {
spi: SPI,
nss: NSS,
checksum_mode: ChecksumMode,
}
type AdcSpi = Spi<SPI3>;
type AdcNss = PA15<Output<PushPull>>;
pub type AdcPhy = Adc<AdcSpi, AdcNss>;
impl<SPI: SpiBus<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS> {
pub fn new(spi: SPI, mut nss: NSS) -> Result<Self, SPI::Error> {
let _ = nss.set_high();
let mut adc = Adc {
spi,
nss,
checksum_mode: ChecksumMode::Off,
};
adc.reset()?;
adc.set_checksum_mode(ChecksumMode::Crc).unwrap();
let mut retries = 0;
let mut adc_id;
loop {
adc_id = adc.identify()?;
if adc_id & 0xFFF0 == 0x00D0 {
break;
} else {
retries += 1;
}
}
info!("ADC id: {:04X} ({} retries)", adc_id, retries);
let mut adc_mode = <regs::AdcMode as Register>::Data::empty();
adc_mode.set_ref_en(true);
adc_mode.set_sing_cyc(false);
adc_mode.set_mode(Mode::Standby);
adc.write_reg(&regs::AdcMode, &mut adc_mode)?;
Ok(adc)
}
/// `0x00DX` for AD7172-2
pub fn identify(&mut self) -> Result<u16, SPI::Error> {
self.read_reg(&regs::Id).map(|id| id.id())
}
pub fn set_checksum_mode(&mut self, mode: ChecksumMode) -> Result<(), SPI::Error> {
// Cannot use update_reg() here because checksum_mode is
// updated between read_reg() and write_reg().
let mut ifmode = self.read_reg(&regs::IfMode)?;
ifmode.set_crc(mode);
self.checksum_mode = mode;
self.write_reg(&regs::IfMode, &mut ifmode)?;
Ok(())
}
pub fn set_sync_enable(&mut self, enable: bool) -> Result<(), SPI::Error> {
self.update_reg(&regs::GpioCon, |data| {
data.set_sync_en(enable);
})
}
pub fn setup_channel(&mut self, index: u8, in_pos: Input, in_neg: Input) -> Result<(), SPI::Error> {
self.update_reg(&regs::SetupCon { index }, |data| {
data.set_bipolar(false);
data.set_refbuf_pos(true);
data.set_refbuf_neg(true);
data.set_ainbuf_pos(true);
data.set_ainbuf_neg(true);
data.set_ref_sel(RefSource::External);
})?;
self.set_sinc5_sinc1_with_50hz_60hz_rejection(index, PostFilter::F16SPS)?;
self.update_reg(&regs::Channel { index }, |data| {
data.set_setup(index);
data.set_enabled(true);
data.set_a_in_pos(in_pos);
data.set_a_in_neg(in_neg);
})?;
Ok(())
}
pub fn get_calibration(&mut self, index: u8) -> Result<ChannelCalibration, SPI::Error> {
let offset = self.read_reg(&regs::Offset { index })?.offset();
let gain = self.read_reg(&regs::Gain { index })?.gain();
let bipolar = self.read_reg(&regs::SetupCon { index })?.bipolar();
Ok(ChannelCalibration { offset, gain, bipolar })
}
pub fn start_continuous_conversion(&mut self) -> Result<(), SPI::Error> {
let mut adc_mode = <regs::AdcMode as Register>::Data::empty();
adc_mode.set_ref_en(true);
// Set SING_CYC = 0 to maximize sampling rate with lowest noise for single channel of temperature measurement
adc_mode.set_sing_cyc(false);
adc_mode.set_mode(Mode::ContinuousConversion);
self.write_reg(&regs::AdcMode, &mut adc_mode)?;
Ok(())
}
/// Rate is only valid with single channel enabled
pub fn get_filter_type_and_rate(&mut self, index: u8) -> Result<(FilterType, Option<SingleChODR>, Option<PostFilter>, f32), SPI::Error> {
let mut filter_type: FilterType = FilterType::Sinc5Sinc1With50hz60HzRejection;
let mut rate: f32 = -1.0;
let mut single_ch_odr : Option<SingleChODR> = None;
let mut post_filter: Option<PostFilter> = None;
self.read_reg(&regs::FiltCon { index }).map(|data| {
if data.sinc3_map() {
filter_type = FilterType::Sinc3WithFineODR;
let odr: u16 = (data.sinc3_map_fine_odr_msb() as u16) << 8 | data.sinc3_map_fine_odr_lsb() as u16;
rate = sinc3_fine_odr_output_rate(odr);
} else if data.enh_filt_en() {
filter_type = FilterType::Sinc5Sinc1With50hz60HzRejection;
match data.enh_filt().output_rate() {
Some(val) => {
rate = val;
}
None => {
rate = -1.0;
}
};
post_filter = Some(data.enh_filt())
} else if data.order() == DigitalFilterOrder::Sinc5Sinc1 {
filter_type = FilterType::Sinc5Sinc1;
match data.odr().output_rate() {
Some(val) => {
rate = val;
}
None => {
rate = -1.0;
}
}
single_ch_odr = Some(data.odr())
} else {
filter_type = FilterType::Sinc3;
match data.odr().output_rate() {
Some(val) => {
rate = val;
}
None => {
rate = -1.0;
}
}
single_ch_odr = Some(data.odr())
}
})?;
Ok((filter_type, single_ch_odr, post_filter, rate))
}
pub fn set_sinc5_sinc1_with_50hz_60hz_rejection(&mut self, index: u8, rate: PostFilter) -> Result<(), SPI::Error> {
self.update_reg(&regs::FiltCon { index }, |data| {
data.set_sinc3_map(false);
data.set_enh_filt_en(true);
data.set_order(DigitalFilterOrder::Sinc5Sinc1);
data.set_enh_filt(rate);
})
}
pub fn set_sinc5_sinc1_filter(&mut self, index: u8, rate: SingleChODR) -> Result<(), SPI::Error> {
self.update_reg(&regs::FiltCon { index }, |data| {
data.set_sinc3_map(false);
data.set_enh_filt_en(false);
data.set_order(DigitalFilterOrder::Sinc5Sinc1);
data.set_odr(rate);
})
}
pub fn set_sinc3_filter(&mut self, index: u8, rate: SingleChODR) -> Result<(), SPI::Error> {
self.update_reg(&regs::FiltCon { index }, |data| {
data.set_sinc3_map(false);
data.set_enh_filt_en(false);
data.set_order(DigitalFilterOrder::Sinc3);
data.set_odr(rate);
})
}
pub fn set_sinc3_fine_filter(&mut self, index: u8, rate: f32) -> Result<f32, SPI::Error> {
let sinc3_fine_odr_u16 = sinc3_fine_odr_closest(rate);
self.update_reg(&regs::FiltCon { index }, |data| {
data.set_sinc3_map(true);
data.set_sinc3_map_fine_odr_msb((sinc3_fine_odr_u16 >> 8 & 0xFF) as u8);
data.set_sinc3_map_fine_odr_lsb((sinc3_fine_odr_u16 & 0xFF) as u8);
})
.map(|_| sinc3_fine_odr_output_rate(sinc3_fine_odr_u16))
}
/// Returns the channel the data is from
pub fn data_ready(&mut self) -> Result<Option<u8>, SPI::Error> {
self.read_reg(&regs::Status)
.map(|status| if status.ready() { Some(status.channel()) } else { None })
}
/// Get data
pub fn read_data(&mut self) -> Result<u32, SPI::Error> {
self.read_reg(&regs::Data).map(|data| data.data())
}
fn read_reg<R: regs::Register>(&mut self, reg: &R) -> Result<R::Data, SPI::Error> {
let mut reg_data = R::Data::empty();
let address = 0x40 | reg.address();
let mut checksum = Checksum::new(self.checksum_mode);
checksum.feed(&[address]);
let checksum_out = checksum.result();
loop {
let checksum_in = self.transfer(address, reg_data.as_mut(), checksum_out)?;
checksum.feed(&reg_data);
let checksum_expected = checksum.result();
if checksum_expected == checksum_in {
break;
}
// Retry
warn!(
"read_reg {:02X}: checksum error: {:?}!={:?}, retrying",
reg.address(),
checksum_expected,
checksum_in
);
}
Ok(reg_data)
}
fn write_reg<R: regs::Register>(&mut self, reg: &R, reg_data: &mut R::Data) -> Result<(), SPI::Error> {
loop {
let address = reg.address();
let mut checksum = Checksum::new(match self.checksum_mode {
ChecksumMode::Off => ChecksumMode::Off,
// write checksums are always crc
ChecksumMode::Xor => ChecksumMode::Crc,
ChecksumMode::Crc => ChecksumMode::Crc,
});
checksum.feed(&[address]);
checksum.feed(&reg_data);
let checksum_out = checksum.result();
let mut data = reg_data.clone();
self.transfer(address, data.as_mut(), checksum_out)?;
// Verification
let readback_data = self.read_reg(reg)?;
if *readback_data == **reg_data {
return Ok(());
}
warn!(
"write_reg {:02X}: readback error, {:?}!={:?}, retrying",
address, &*readback_data, &**reg_data
);
}
}
fn update_reg<R, F, A>(&mut self, reg: &R, f: F) -> Result<A, SPI::Error>
where
R: regs::Register,
F: FnOnce(&mut R::Data) -> A,
{
let mut reg_data = self.read_reg(reg)?;
let result = f(&mut reg_data);
self.write_reg(reg, &mut reg_data)?;
Ok(result)
}
pub fn reset(&mut self) -> Result<(), SPI::Error> {
let mut buf = [0xFFu8; 8];
let _ = self.nss.set_low();
let result = self.spi.transfer_in_place(&mut buf);
let _ = self.nss.set_high();
result?;
Ok(())
}
fn transfer<'w>(
&mut self,
addr: u8,
reg_data: &'w mut [u8],
checksum: Option<u8>,
) -> Result<Option<u8>, SPI::Error> {
let mut addr_buf = [addr];
let _ = self.nss.set_low();
let result = match self.spi.transfer_in_place(&mut addr_buf) {
Ok(_) => self.spi.transfer_in_place(reg_data),
Err(e) => Err(e),
};
let result = match (result, checksum) {
(Ok(_), None) => Ok(None),
(Ok(_), Some(checksum_out)) => {
let mut checksum_buf = [checksum_out; 1];
match self.spi.transfer_in_place(&mut checksum_buf) {
Ok(_) => Ok(Some(checksum_buf[0])),
Err(e) => Err(e),
}
}
(Err(e), _) => Err(e),
};
let _ = self.nss.set_high();
result
}
}
#[derive(Debug, Clone)]
pub struct ChannelCalibration {
offset: u32,
gain: u32,
bipolar: bool,
}
impl Default for ChannelCalibration {
fn default() -> Self {
ChannelCalibration {
offset: 0,
gain: 0,
bipolar: false,
}
}
}
impl ChannelCalibration {
pub fn convert_data(&self, data: u32) -> ElectricPotential {
let data = if self.bipolar {
(data as i32 - 0x80_0000) as f32
} else {
data as f32 / 2.0
};
let data = data / (self.gain as f32 / (0x40_0000 as f32));
let data = data + (self.offset as i32 - 0x80_0000) as f32;
let data = data / (2 << 23) as f32;
const V_REF: f32 = 3.3;
ElectricPotential::new::<volt>(data * V_REF / 0.75)
}
}

View File

@ -0,0 +1,60 @@
#[derive(Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum ChecksumMode {
Off = 0b00,
/// Seems much less reliable than `Crc`
Xor = 0b01,
Crc = 0b10,
}
impl From<u8> for ChecksumMode {
fn from(x: u8) -> Self {
match x {
0 => ChecksumMode::Off,
1 => ChecksumMode::Xor,
_ => ChecksumMode::Crc,
}
}
}
pub struct Checksum {
mode: ChecksumMode,
state: u8,
}
impl Checksum {
pub fn new(mode: ChecksumMode) -> Self {
Checksum { mode, state: 0 }
}
fn feed_byte(&mut self, input: u8) {
match self.mode {
ChecksumMode::Off => {}
ChecksumMode::Xor => self.state ^= input,
ChecksumMode::Crc => {
for i in 0..8 {
let input_mask = 0x80 >> i;
self.state = (self.state << 1)
^ if ((self.state & 0x80) != 0) != ((input & input_mask) != 0) {
0x07 /* x8 + x2 + x + 1 */
} else {
0
};
}
}
}
}
pub fn feed(&mut self, input: &[u8]) {
for &b in input {
self.feed_byte(b);
}
}
pub fn result(&self) -> Option<u8> {
match self.mode {
ChecksumMode::Off => None,
_ => Some(self.state),
}
}
}

View File

@ -0,0 +1,350 @@
use core::fmt;
use fugit::MegahertzU32;
use num_traits::float::Float;
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::spi;
mod checksum;
pub mod regs;
pub use checksum::ChecksumMode;
mod adc;
pub use adc::*;
/// SPI Mode 3
pub const SPI_MODE: spi::Mode = spi::Mode {
polarity: spi::Polarity::IdleHigh,
phase: spi::Phase::CaptureOnSecondTransition,
};
/// AD7172 Max Frequency: 40MHz | SPI3 Max Frequency: 21MHz
pub const SPI_CLOCK_MHZ: MegahertzU32 = MegahertzU32::from_raw(21);
pub const MAX_VALUE: u32 = 0xFF_FFFF;
#[derive(Clone, Copy, Debug)]
#[repr(u8)]
pub enum Mode {
ContinuousConversion = 0b000,
SingleConversion = 0b001,
Standby = 0b010,
PowerDown = 0b011,
InternalOffsetCalibration = 0b100,
Invalid,
SystemOffsetCalibration = 0b110,
SystemGainCalibration = 0b111,
}
impl From<u8> for Mode {
fn from(x: u8) -> Self {
use Mode::*;
match x {
0b000 => ContinuousConversion,
0b001 => SingleConversion,
0b010 => Standby,
0b011 => PowerDown,
0b100 => InternalOffsetCalibration,
0b110 => SystemOffsetCalibration,
0b111 => SystemGainCalibration,
_ => Invalid,
}
}
}
#[derive(Clone, Copy, Debug)]
#[repr(u8)]
pub enum Input {
Ain0 = 0,
Ain1 = 1,
Ain2 = 2,
Ain3 = 3,
Ain4 = 4,
TemperaturePos = 17,
TemperatureNeg = 18,
AnalogSupplyPos = 19,
AnalogSupplyNeg = 20,
RefPos = 21,
RefNeg = 22,
Invalid = 0b11111,
}
impl From<u8> for Input {
fn from(x: u8) -> Self {
match x {
0 => Input::Ain0,
1 => Input::Ain1,
2 => Input::Ain2,
3 => Input::Ain3,
4 => Input::Ain4,
17 => Input::TemperaturePos,
18 => Input::TemperatureNeg,
19 => Input::AnalogSupplyPos,
20 => Input::AnalogSupplyNeg,
21 => Input::RefPos,
22 => Input::RefNeg,
_ => Input::Invalid,
}
}
}
impl fmt::Display for Input {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use Input::*;
match self {
Ain0 => "ain0",
Ain1 => "ain1",
Ain2 => "ain2",
Ain3 => "ain3",
Ain4 => "ain4",
TemperaturePos => "temperature+",
TemperatureNeg => "temperature-",
AnalogSupplyPos => "analogsupply+",
AnalogSupplyNeg => "analogsupply-",
RefPos => "ref+",
RefNeg => "ref-",
_ => "<INVALID>",
}
.fmt(fmt)
}
}
/// Reference source for ADC conversion
#[repr(u8)]
pub enum RefSource {
/// External reference
External = 0b00,
/// Internal 2.5V reference
Internal = 0b10,
/// AVDD1 AVSS
Avdd1MinusAvss = 0b11,
Invalid = 0b01,
}
impl From<u8> for RefSource {
fn from(x: u8) -> Self {
match x {
0 => RefSource::External,
1 => RefSource::Internal,
2 => RefSource::Avdd1MinusAvss,
_ => RefSource::Invalid,
}
}
}
impl fmt::Display for RefSource {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use RefSource::*;
match self {
External => "external",
Internal => "internal",
Avdd1MinusAvss => "avdd1-avss",
_ => "<INVALID>",
}
.fmt(fmt)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[repr(u8)]
pub enum PostFilter {
/// 27 SPS, 47 dB rejection, 36.7 ms settling
F27SPS = 0b010,
/// 21.25 SPS, 62 dB rejection, 40 ms settling
F21SPS = 0b011,
/// 20 SPS, 86 dB rejection, 50 ms settling
F20SPS = 0b101,
/// 16.67 SPS, 92 dB rejection, 60 ms settling
F16SPS = 0b110,
Invalid = 0b111,
}
impl PostFilter {
pub const VALID_VALUES: &'static [Self] = &[
PostFilter::F27SPS,
PostFilter::F21SPS,
PostFilter::F20SPS,
PostFilter::F16SPS,
];
pub fn closest(rate: f32) -> Option<Self> {
let mut best: Option<(f32, Self)> = None;
for value in Self::VALID_VALUES {
let error = (rate - value.output_rate().unwrap()).abs();
let better = best.map(|(best_error, _)| error < best_error).unwrap_or(true);
if better {
best = Some((error, *value));
}
}
best.map(|(_, best)| best)
}
/// Samples per Second
pub fn output_rate(&self) -> Option<f32> {
match self {
PostFilter::F27SPS => Some(27.0),
PostFilter::F21SPS => Some(21.25),
PostFilter::F20SPS => Some(20.0),
PostFilter::F16SPS => Some(16.67),
PostFilter::Invalid => None,
}
}
}
impl From<u8> for PostFilter {
fn from(x: u8) -> Self {
match x {
0b010 => PostFilter::F27SPS,
0b011 => PostFilter::F21SPS,
0b101 => PostFilter::F20SPS,
0b110 => PostFilter::F16SPS,
_ => PostFilter::Invalid,
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default)]
pub enum FilterType {
#[default]
Sinc5Sinc1With50hz60HzRejection,
Sinc5Sinc1,
Sinc3,
Sinc3WithFineODR,
}
#[derive(PartialEq)]
#[repr(u8)]
pub enum DigitalFilterOrder {
Sinc5Sinc1 = 0b00,
Sinc3 = 0b11,
Invalid = 0b10,
}
impl From<u8> for DigitalFilterOrder {
fn from(x: u8) -> Self {
match x {
0b00 => DigitalFilterOrder::Sinc5Sinc1,
0b11 => DigitalFilterOrder::Sinc3,
_ => DigitalFilterOrder::Invalid,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[allow(unused)]
#[repr(u8)]
pub enum SingleChODR {
F31250_0SPS = 0b00101,
F15625_0SPS = 0b00110,
F10417_0SPS = 0b00111,
F5208_0SPS = 0b01000,
F2597_0SPS = 0b01001,
F1007_0SPS = 0b01010,
F503_8SPS = 0b01011,
F381_0SPS = 0b01100,
F200_3SPS = 0b01101,
F100_2SPS = 0b01110,
F59_52SPS = 0b01111,
F49_68SPS = 0b10000,
F20_01SPS = 0b10001,
F16_63SPS = 0b10010,
F10_0SPS = 0b10011,
F5_0SPS = 0b10100,
F2_5SPS = 0b10101,
F1_25SPS = 0b10110,
Invalid = 0b11111,
}
impl SingleChODR {
pub const VALID_VALUES: &'static [Self] = &[
SingleChODR::F31250_0SPS,
SingleChODR::F15625_0SPS,
SingleChODR::F10417_0SPS,
SingleChODR::F5208_0SPS,
SingleChODR::F2597_0SPS,
SingleChODR::F1007_0SPS,
SingleChODR::F503_8SPS,
SingleChODR::F381_0SPS,
SingleChODR::F200_3SPS,
SingleChODR::F100_2SPS,
SingleChODR::F59_52SPS,
SingleChODR::F49_68SPS,
SingleChODR::F20_01SPS,
SingleChODR::F16_63SPS,
SingleChODR::F10_0SPS,
SingleChODR::F5_0SPS,
SingleChODR::F2_5SPS,
SingleChODR::F1_25SPS,
];
pub fn closest(rate: f32) -> Option<Self> {
let mut best: Option<(f32, Self)> = None;
for value in Self::VALID_VALUES {
let error = (rate - value.output_rate().unwrap()).abs();
let better = best.map(|(best_error, _)| error < best_error).unwrap_or(true);
if better {
best = Some((error, *value));
}
}
best.map(|(_, best)| best)
}
/// Samples per Second
pub fn output_rate(&self) -> Option<f32> {
match self {
SingleChODR::F31250_0SPS => Some(31250.0),
SingleChODR::F15625_0SPS => Some(15625.0),
SingleChODR::F10417_0SPS => Some(10417.0),
SingleChODR::F5208_0SPS => Some(5208.0),
SingleChODR::F2597_0SPS => Some(2597.0),
SingleChODR::F1007_0SPS => Some(1007.0),
SingleChODR::F503_8SPS => Some(503.8),
SingleChODR::F381_0SPS => Some(381.0),
SingleChODR::F200_3SPS => Some(200.3),
SingleChODR::F100_2SPS => Some(100.2),
SingleChODR::F59_52SPS => Some(59.52),
SingleChODR::F49_68SPS => Some(49.68),
SingleChODR::F20_01SPS => Some(20.01),
SingleChODR::F16_63SPS => Some(16.63),
SingleChODR::F10_0SPS => Some(10.0),
SingleChODR::F5_0SPS => Some(5.0),
SingleChODR::F2_5SPS => Some(2.5),
SingleChODR::F1_25SPS => Some(1.25),
SingleChODR::Invalid => None,
}
}
}
impl From<u8> for SingleChODR {
fn from(x: u8) -> Self {
match x {
0b00101 => SingleChODR::F31250_0SPS,
0b00110 => SingleChODR::F15625_0SPS,
0b00111 => SingleChODR::F10417_0SPS,
0b01000 => SingleChODR::F5208_0SPS,
0b01001 => SingleChODR::F2597_0SPS,
0b01010 => SingleChODR::F1007_0SPS,
0b01011 => SingleChODR::F503_8SPS,
0b01100 => SingleChODR::F381_0SPS,
0b01101 => SingleChODR::F200_3SPS,
0b01110 => SingleChODR::F100_2SPS,
0b01111 => SingleChODR::F59_52SPS,
0b10000 => SingleChODR::F49_68SPS,
0b10001 => SingleChODR::F20_01SPS,
0b10010 => SingleChODR::F16_63SPS,
0b10011 => SingleChODR::F10_0SPS,
0b10100 => SingleChODR::F5_0SPS,
0b10101 => SingleChODR::F2_5SPS,
0b10110 => SingleChODR::F1_25SPS,
_ => SingleChODR::Invalid,
}
}
}
pub fn sinc3_fine_odr_output_rate(odr: u16) -> f32 {
1.0 * 1e6 / (32.0 * odr as f32)
}
pub fn sinc3_fine_odr_closest(rate: f32) -> u16 {
(1.0e6 / (32.0 * rate)).max(1.0 as f32).min(0x7FFF as f32) as u16
}

View File

@ -0,0 +1,342 @@
use core::ops::{Deref, DerefMut};
use bit_field::BitField;
use byteorder::{BigEndian, ByteOrder};
use super::*;
pub trait Register {
type Data: RegisterData;
fn address(&self) -> u8;
}
pub trait RegisterData: Clone + Deref<Target = [u8]> + DerefMut {
fn empty() -> Self;
}
macro_rules! def_reg {
($Reg:ident, $reg:ident, $addr:expr, $size:expr) => {
/// AD7172 register
pub struct $Reg;
impl Register for $Reg {
/// Register contents
type Data = $reg::Data;
/// Register address
fn address(&self) -> u8 {
$addr
}
}
mod $reg {
/// Register contents
#[derive(Clone)]
pub struct Data(pub [u8; $size]);
impl super::RegisterData for Data {
/// Generate zeroed register contents
fn empty() -> Self {
Data([0; $size])
}
}
impl core::ops::Deref for Data {
type Target = [u8];
fn deref(&self) -> &[u8] {
&self.0
}
}
impl core::ops::DerefMut for Data {
fn deref_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}
}
};
($Reg:ident,u8, $reg:ident, $addr:expr, $size:expr) => {
pub struct $Reg {
pub index: u8,
}
impl Register for $Reg {
type Data = $reg::Data;
fn address(&self) -> u8 {
$addr + self.index
}
}
mod $reg {
#[derive(Clone)]
pub struct Data(pub [u8; $size]);
impl super::RegisterData for Data {
fn empty() -> Self {
Data([0; $size])
}
}
impl core::ops::Deref for Data {
type Target = [u8];
fn deref(&self) -> &[u8] {
&self.0
}
}
impl core::ops::DerefMut for Data {
fn deref_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}
}
};
}
macro_rules! reg_bit {
($getter:ident, $byte:expr, $bit:expr, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> bool {
self.0[$byte].get_bit($bit)
}
};
($getter:ident, $setter:ident, $byte:expr, $bit:expr, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> bool {
self.0[$byte].get_bit($bit)
}
#[allow(unused)]
#[doc = $doc]
pub fn $setter(&mut self, value: bool) {
self.0[$byte].set_bit($bit, value);
}
};
}
macro_rules! reg_bits {
($getter:ident, $byte:expr, $bits:expr, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> u8 {
self.0[$byte].get_bits($bits)
}
};
($getter:ident, $setter:ident, $byte:expr, $bits:expr, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> u8 {
self.0[$byte].get_bits($bits)
}
#[allow(unused)]
#[doc = $doc]
pub fn $setter(&mut self, value: u8) {
self.0[$byte].set_bits($bits, value);
}
};
($getter:ident, $byte:expr, $bits:expr, $ty:ty, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> $ty {
self.0[$byte].get_bits($bits) as $ty
}
};
($getter:ident, $setter:ident, $byte:expr, $bits:expr, $ty:ty, $doc:expr) => {
#[allow(unused)]
#[doc = $doc]
pub fn $getter(&self) -> $ty {
self.0[$byte].get_bits($bits).into()
}
#[allow(unused)]
#[doc = $doc]
pub fn $setter(&mut self, value: $ty) {
self.0[$byte].set_bits($bits, value as u8);
}
};
}
def_reg!(Status, status, 0x00, 1);
impl status::Data {
/// Is there new data to read?
pub fn ready(&self) -> bool {
!self.not_ready()
}
reg_bit!(not_ready, 0, 7, "No data ready indicator");
reg_bits!(channel, 0, 0..=1, "Channel for which data is ready");
reg_bit!(adc_error, 0, 6, "ADC error");
reg_bit!(crc_error, 0, 5, "SPI CRC error");
reg_bit!(reg_error, 0, 4, "Register error");
}
def_reg!(AdcMode, adc_mode, 0x01, 2);
impl adc_mode::Data {
reg_bits!(delay, set_delay, 0, 0..=2, "Delay after channel switch");
reg_bit!(sing_cyc, set_sing_cyc, 0, 5, "Can only used with single channel");
reg_bit!(hide_delay, set_hide_delay, 0, 6, "Hide delay");
reg_bit!(
ref_en,
set_ref_en,
0,
7,
"Enable internal reference, output buffered 2.5 V to REFOUT"
);
reg_bits!(clockset, set_clocksel, 1, 2..=3, "Clock source");
reg_bits!(mode, set_mode, 1, 4..=6, Mode, "Operating mode");
}
def_reg!(IfMode, if_mode, 0x02, 2);
impl if_mode::Data {
reg_bits!(crc, set_crc, 1, 2..=3, ChecksumMode, "SPI checksum mode");
}
def_reg!(Data, data, 0x04, 3);
impl data::Data {
pub fn data(&self) -> u32 {
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
}
def_reg!(GpioCon, gpio_con, 0x06, 2);
impl gpio_con::Data {
reg_bit!(sync_en, set_sync_en, 0, 3, "Enables the SYNC/ERROR pin as a sync input");
}
def_reg!(Id, id, 0x07, 2);
impl id::Data {
pub fn id(&self) -> u16 {
BigEndian::read_u16(&self.0)
}
}
def_reg!(Channel, u8, channel, 0x10, 2);
impl channel::Data {
reg_bit!(enabled, set_enabled, 0, 7, "Channel enabled");
reg_bits!(setup, set_setup, 0, 4..=5, "Setup number");
/// Which input is connected to positive input of this channel
#[allow(unused)]
pub fn a_in_pos(&self) -> Input {
((self.0[0].get_bits(0..=1) << 3) | self.0[1].get_bits(5..=7)).into()
}
/// Set which input is connected to positive input of this channel
#[allow(unused)]
pub fn set_a_in_pos(&mut self, value: Input) {
let value = value as u8;
self.0[0].set_bits(0..=1, value >> 3);
self.0[1].set_bits(5..=7, value & 0x7);
}
reg_bits!(
a_in_neg,
set_a_in_neg,
1,
0..=4,
Input,
"Which input is connected to negative input of this channel"
);
}
def_reg!(SetupCon, u8, setup_con, 0x20, 2);
impl setup_con::Data {
reg_bit!(
bipolar,
set_bipolar,
0,
4,
"Unipolar (`false`) or bipolar (`true`) coded output"
);
reg_bit!(refbuf_pos, set_refbuf_pos, 0, 3, "Enable REF+ input buffer");
reg_bit!(refbuf_neg, set_refbuf_neg, 0, 2, "Enable REF- input buffer");
reg_bit!(ainbuf_pos, set_ainbuf_pos, 0, 1, "Enable AIN+ input buffer");
reg_bit!(ainbuf_neg, set_ainbuf_neg, 0, 0, "Enable AIN- input buffer");
reg_bit!(
burnout_en,
1,
7,
"enables a 10 µA current source on the positive analog input selected and a 10 µA current sink on the \
negative analog input selected"
);
reg_bits!(
ref_sel,
set_ref_sel,
1,
4..=5,
RefSource,
"Select reference source for conversion"
);
}
def_reg!(FiltCon, u8, filt_con, 0x28, 2);
impl filt_con::Data {
reg_bit!(
sinc3_map,
set_sinc3_map,
0,
7,
"If set, Sinc3 Filter's notch frequency rejection position can be fine tuned with FiltCon[14:0]. Best to be \
used with Single Channel Enabled"
);
reg_bit!(
enh_filt_en,
set_enh_filt_en,
0,
3,
"Enable postfilters for enhanced 50Hz and 60Hz rejection"
);
reg_bits!(
enh_filt,
set_enh_filt,
0,
0..=2,
PostFilter,
"Select postfilters output data rate for enhanced 50Hz and 60Hz rejection"
);
reg_bits!(
order,
set_order,
1,
5..=6,
DigitalFilterOrder,
"order of the digital filter that processes the modulator data"
);
reg_bits!(
odr,
set_odr,
1,
0..=4,
SingleChODR,
"Output data rate for normal Sin5c + Sinc1 and Sinc3 filter with SING_CYC = 0 and Single Channel Enabled"
);
reg_bits!(
sinc3_map_fine_odr_msb,
set_sinc3_map_fine_odr_msb,
0,
0..=6,
"MSB Byte Sinc3 Fine Output Config"
);
reg_bits!(
sinc3_map_fine_odr_lsb,
set_sinc3_map_fine_odr_lsb,
1,
0..=7,
"LSB Byte Sinc3 Fine Output Config"
);
}
def_reg!(Offset, u8, offset, 0x30, 3);
impl offset::Data {
#[allow(unused)]
pub fn offset(&self) -> u32 {
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
#[allow(unused)]
pub fn set_offset(&mut self, value: u32) {
self.0[0] = (value >> 16) as u8;
self.0[1] = (value >> 8) as u8;
self.0[2] = value as u8;
}
}
def_reg!(Gain, u8, gain, 0x38, 3);
impl gain::Data {
#[allow(unused)]
pub fn gain(&self) -> u32 {
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
}
#[allow(unused)]
pub fn set_gain(&mut self, value: u32) {
self.0[0] = (value >> 16) as u8;
self.0[1] = (value >> 8) as u8;
self.0[2] = value as u8;
}
}

414
src/thermostat/max1968.rs Normal file
View File

@ -0,0 +1,414 @@
use core::ptr::addr_of_mut;
use fugit::KilohertzU32;
use stm32f4xx_hal::{adc::{config::{self, AdcConfig},
Adc},
dma::{config::DmaConfig, PeripheralToMemory, Stream2, StreamsTuple, Transfer as DMA_Transfer},
gpio::{gpioa::*, gpiob::*, gpioc::*, Analog, Output, PushPull},
hal::{digital::OutputPin, spi::SpiBus},
interrupt,
pac::{Peripherals, ADC1, ADC2, DMA2, NVIC, SPI1, TIM4},
spi::Spi,
timer::pwm::PwmChannel};
use uom::si::{electric_potential::millivolt, f32::ElectricPotential, ratio::ratio};
use crate::{device::sys_timer::sleep, thermostat::ad5680};
pub const PWM_FREQ_KHZ: KilohertzU32 = KilohertzU32::from_raw(20);
pub trait ChannelPins {
type DacSpi: SpiBus<u8>;
type DacSync: OutputPin;
type ShdnPin: OutputPin;
type VRefPin;
type ItecPin;
type DacFeedbackPin;
type VTecPin;
type MaxVPin;
type MaxIPosPin;
type MAXINegPin;
}
pub struct Channel0;
impl ChannelPins for Channel0 {
type DacSpi = DacSpi;
type DacSync = DacSync;
type ShdnPin = PA5<Output<PushPull>>;
type VRefPin = PA6<Analog>;
type ItecPin = PB1<Analog>;
type DacFeedbackPin = PC0<Analog>;
type VTecPin = PB0<Analog>;
type MaxVPin = PwmChannel<TIM4, 1>;
type MaxIPosPin = PwmChannel<TIM4, 2>;
type MAXINegPin = PwmChannel<TIM4, 0>;
}
pub struct MAX1968Phy<C: ChannelPins> {
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
pub shdn: C::ShdnPin,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
pub dac_feedback_pin: C::DacFeedbackPin,
pub vtec_pin: C::VTecPin,
pub max_v: C::MaxVPin,
pub max_i_pos: C::MaxIPosPin,
pub max_i_neg: C::MAXINegPin,
}
pub struct MAX1968PinSet<C: ChannelPins> {
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
pub shdn: C::ShdnPin,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
pub dac_feedback_pin: C::DacFeedbackPin,
pub vtec_pin: C::VTecPin,
pub max_v: C::MaxVPin,
pub max_i_pos: C::MaxIPosPin,
pub max_i_neg: C::MAXINegPin,
}
type DacSpi = Spi<SPI1>;
type DacSync = PB4<Output<PushPull>>;
static mut DMA_TRANSFER_COMPLETE: bool = true;
pub struct MAX1968 {
pub phy: MAX1968Phy<Channel0>,
pub pins_adc: Adc<ADC1>,
pub dma_adc: DMA_Transfer<Stream2<DMA2>, 1, Adc<ADC2>, PeripheralToMemory, &'static mut [u16; 16]>,
pub dac_out_range: ElectricPotential,
prev_vtec_volt: ElectricPotential,
prev_itec_volt: ElectricPotential,
}
pub enum PwmPinsEnum {
MaxV,
MaxPosI,
MaxNegI,
}
#[allow(unused)]
pub enum AdcReadTarget {
VREF,
DacVfb,
ITec,
VTec,
}
impl<C: ChannelPins> MAX1968Phy<C> {
pub fn new(pins: MAX1968PinSet<C>) -> Self {
MAX1968Phy {
dac: pins.dac,
shdn: pins.shdn,
vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin,
dac_feedback_pin: pins.dac_feedback_pin,
vtec_pin: pins.vtec_pin,
max_v: pins.max_v,
max_i_pos: pins.max_i_pos,
max_i_neg: pins.max_i_neg,
}
}
}
static mut ADC2_FIRST_BUFFER: [u16; 16] = [0; 16];
static mut ADC2_LOCAL_BUFFER: [u16; 16] = [0; 16];
impl MAX1968 {
pub fn new(mut phy_ch0: MAX1968Phy<Channel0>, adc1: ADC1, adc2: ADC2, dma2: DMA2) -> Self {
let adc_config = AdcConfig::default()
.clock(config::Clock::Pclk2_div_8)
.default_sample_time(config::SampleTime::Cycles_480);
// Do not set reset RCCs as it causes other ADCs' clock to be disabled
let mut pins_adc1 = Adc::adc1(adc1, false, adc_config);
// adc1.calibrate() fn only read REFINT once to assign the calibration value.
// It does not take the STM32F4's ADC Precision Limitation into account.
// AN4073: ADC Reading Dispersion can be reduced through Averaging
let mut vdda_mv: u32 = 0;
for _ in (0..512).rev() {
pins_adc1.calibrate();
vdda_mv += pins_adc1.reference_voltage();
}
vdda_mv = vdda_mv / 512 as u32;
pins_adc1.apply_config(adc_config.reference_voltage(vdda_mv));
let adc_config = AdcConfig::default()
.clock(config::Clock::Pclk2_div_8)
.default_sample_time(config::SampleTime::Cycles_480)
.dma(config::Dma::Continuous)
.scan(config::Scan::Enabled)
.reference_voltage(pins_adc1.reference_voltage());
let dma_config = DmaConfig::default()
.transfer_complete_interrupt(true)
.memory_increment(true)
.double_buffer(false);
let mut pins_adc2 = Adc::adc2(adc2, false, adc_config);
pins_adc2.configure_channel(&phy_ch0.itec_pin, config::Sequence::One, config::SampleTime::Cycles_480);
pins_adc2.configure_channel(&phy_ch0.vtec_pin, config::Sequence::Two, config::SampleTime::Cycles_480);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Three,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Four,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Five,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(&phy_ch0.vtec_pin, config::Sequence::Six, config::SampleTime::Cycles_480);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Seven,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Eight,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Nine,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(&phy_ch0.vtec_pin, config::Sequence::Ten, config::SampleTime::Cycles_480);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Eleven,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Twelve,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Thirteen,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Fourteen,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.itec_pin,
config::Sequence::Fifteen,
config::SampleTime::Cycles_480,
);
pins_adc2.configure_channel(
&phy_ch0.vtec_pin,
config::Sequence::Sixteen,
config::SampleTime::Cycles_480,
);
let dma = StreamsTuple::new(dma2);
let dma_adc: DMA_Transfer<Stream2<DMA2>, 1, Adc<ADC2>, PeripheralToMemory, &'static mut [u16; 16]>;
unsafe {
dma_adc = DMA_Transfer::init_peripheral_to_memory(
dma.2,
pins_adc2,
addr_of_mut!(ADC2_FIRST_BUFFER).as_mut().unwrap(),
None,
dma_config,
);
NVIC::unmask(interrupt::DMA2_STREAM2);
}
phy_ch0.dac.set(ad5680::MAX_VALUE).unwrap();
sleep(500);
let mut sample = 0;
for _ in 0..512 {
sample += pins_adc1.convert(
&phy_ch0.dac_feedback_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
let sample = sample / 512 as u32;
let mv = pins_adc1.sample_to_millivolts(sample as u16);
let dac_out_range = ElectricPotential::new::<millivolt>(mv as f32);
phy_ch0.dac.set(0).unwrap();
MAX1968 {
phy: phy_ch0,
pins_adc: pins_adc1,
dma_adc: dma_adc,
dac_out_range: dac_out_range,
prev_vtec_volt: ElectricPotential::new::<millivolt>(0.0),
prev_itec_volt: ElectricPotential::new::<millivolt>(0.0),
}
}
pub fn dma_adc_start_conversion(&mut self) {
if unsafe { DMA_TRANSFER_COMPLETE } {
unsafe {
DMA_TRANSFER_COMPLETE = false;
}
self.dma_adc.start(|adc| {
adc.clear_end_of_conversion_flag();
adc.start_conversion();
});
}
}
pub fn get_tec_readings(&mut self) -> (ElectricPotential, ElectricPotential) {
if unsafe { DMA_TRANSFER_COMPLETE } {
let buffer: &[u16; 16];
unsafe {
(buffer, _) = self
.dma_adc
.next_transfer(addr_of_mut!(ADC2_LOCAL_BUFFER).as_mut().unwrap())
.unwrap();
}
let sample_to_millivolts = self.dma_adc.peripheral().make_sample_to_millivolts();
let mut itec: u16 = 0;
for data in buffer.into_iter().step_by(2) {
itec += *data;
}
itec = itec >> 3;
let mut vtec: u16 = 0;
for data in buffer.into_iter().skip(1).step_by(2) {
vtec += *data;
}
vtec = vtec >> 3;
unsafe {
ADC2_LOCAL_BUFFER = *buffer;
}
self.prev_vtec_volt = ElectricPotential::new::<millivolt>(sample_to_millivolts(vtec) as f32);
self.prev_itec_volt = ElectricPotential::new::<millivolt>(sample_to_millivolts(itec) as f32);
}
(self.prev_vtec_volt, self.prev_itec_volt)
}
// Return the calibrated VDDA Voltage
// Can be used to set reference voltage for other ADC
pub fn get_calibrated_vdda(&mut self) -> u32 {
self.pins_adc.reference_voltage()
}
pub fn is_powered_on(&mut self) -> bool {
self.phy.shdn.is_set_high()
}
pub fn power_down(&mut self) {
self.phy.shdn.set_low();
}
pub fn power_up(&mut self) {
self.phy.shdn.set_high();
}
pub fn set_dac(&mut self, voltage: ElectricPotential, dac_out_v_max: ElectricPotential) -> ElectricPotential {
let value = ((voltage / dac_out_v_max).get::<ratio>() * (ad5680::MAX_VALUE as f32)) as u32;
self.phy.dac.set(value).unwrap();
voltage
}
// AN4073: ADC Reading Dispersion can be reduced through Averaging
// Upon test, 16 Point Averaging = +-3 LSB Dispersion
pub fn adc_read(&mut self, adc_read_target: AdcReadTarget, avg_pt: u16) -> ElectricPotential {
let mut sample: u32 = 0;
sample = match adc_read_target {
AdcReadTarget::VREF => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.phy.vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::DacVfb => {
for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert(
&self.phy.dac_feedback_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::ITec => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.phy.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::VTec => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.phy.vtec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
};
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
ElectricPotential::new::<millivolt>(mv as f32)
}
pub fn set_pwm(&mut self, pwm_pin: PwmPinsEnum, duty: f64, max_duty: f64) -> f64 {
fn duty_cycle_value(duty_cycle: f64, duty_cycle_limit: f64, max_value: u16) -> u16 {
((duty_cycle.min(duty_cycle_limit) * (max_value as f64)) as u16).min(max_value)
}
let value: u16;
let max_value: u16;
match pwm_pin {
PwmPinsEnum::MaxV => {
self.phy.max_v.enable();
max_value = self.phy.max_v.get_max_duty();
value = duty_cycle_value(duty, max_duty, max_value);
self.phy.max_v.set_duty(value);
}
PwmPinsEnum::MaxPosI => {
self.phy.max_i_pos.enable();
max_value = self.phy.max_i_pos.get_max_duty();
value = duty_cycle_value(duty, max_duty, max_value);
self.phy.max_i_pos.set_duty(value);
}
PwmPinsEnum::MaxNegI => {
self.phy.max_i_neg.enable();
max_value = self.phy.max_i_neg.get_max_duty();
value = duty_cycle_value(duty, max_duty, max_value);
self.phy.max_i_neg.set_duty(value);
}
}
return (value as f64) / (max_value as f64);
}
}
#[interrupt]
fn DMA2_STREAM2() {
cortex_m::interrupt::free(|_| {
unsafe {
// Clear all DMA2_STREAM2 interrupt flags
Peripherals::steal().DMA2.lifcr.write(|w| {
w.ctcif2()
.set_bit()
.cdmeif2()
.set_bit()
.chtif2()
.set_bit()
.cteif2()
.set_bit()
});
DMA_TRANSFER_COMPLETE = true;
}
})
}

View File

@ -0,0 +1,7 @@
pub mod ad5680;
pub mod ad7172;
pub mod max1968;
pub mod pid_state;
pub mod steinhart_hart;
pub mod temp_mon;
pub mod thermostat;

227
src/thermostat/pid_state.rs Normal file
View File

@ -0,0 +1,227 @@
use miniconf::Tree;
use serde::{Deserialize, Serialize};
use uom::si::{electric_potential::volt,
electrical_resistance::ohm,
f32::{ElectricPotential, ElectricalResistance}};
use crate::thermostat::{ad7172, steinhart_hart as sh};
const R_INNER: f32 = 2.0 * 5100.0;
const VREF_SENS: f32 = 3.3 / 2.0;
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Tree)]
pub struct Parameters {
/// Gain coefficient for proportional term
pub kp: f32,
/// Gain coefficient for integral term
pub ki: f32,
/// Gain coefficient for derivative term
pub kd: f32,
/// Output limit minimum
pub output_min: f32,
/// Output limit maximum
pub output_max: f32,
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
kp: 0.0,
ki: 0.0,
kd: 0.0,
output_min: -1.0,
output_max: 1.0,
}
}
}
#[derive(Clone)]
pub struct Controller {
pub parameters: Parameters,
u1: f64,
x1: f64,
x2: f64,
pub y1: f64,
}
pub struct PidState {
adc_data: Option<u32>,
adc_calibration: ad7172::ChannelCalibration,
pid_engaged: bool,
set_point: f32,
sh: sh::Parameters,
controller: Controller,
}
impl Default for PidState {
fn default() -> Self {
PidState {
adc_data: None,
adc_calibration: ad7172::ChannelCalibration::default(),
pid_engaged: false,
set_point: 0.0,
sh: sh::Parameters::default(),
controller: Controller {
parameters: Parameters::default(),
u1: 0.0,
x1: 0.0,
x2: 0.0,
y1: 0.0,
},
}
}
}
pub enum PidSettings {
Kp,
Ki,
Kd,
Min,
Max,
}
impl PidState {
pub fn update(&mut self, adc_data: u32) {
self.adc_data = if adc_data == ad7172::MAX_VALUE {
// this means there is no thermistor plugged into the ADC.
None
} else {
Some(adc_data)
};
}
// Based on https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw PID implementation
// Input x(t), target u(t), output y(t)
// y0' = y1 - ki * u0
// + x0 * (kp + ki + kd)
// - x1 * (kp + 2kd)
// + x2 * kd
// y0 = clip(y0', ymin, ymax)
pub fn update_pid(&mut self) -> Option<f64> {
let input = self.get_temperature()?;
let setpoint = self.set_point;
let mut output: f64 = self.controller.y1 - setpoint as f64 * f64::from(self.controller.parameters.ki)
+ input as f64
* f64::from(
self.controller.parameters.kp + self.controller.parameters.ki + self.controller.parameters.kd,
)
- self.controller.x1 * f64::from(self.controller.parameters.kp + 2.0 * self.controller.parameters.kd)
+ self.controller.x2 * f64::from(self.controller.parameters.kd);
if output < self.controller.parameters.output_min.into() {
output = self.controller.parameters.output_min.into();
}
if output > self.controller.parameters.output_max.into() {
output = self.controller.parameters.output_max.into();
}
self.controller.x2 = self.controller.x1;
self.controller.x1 = input as f64;
self.controller.u1 = setpoint as f64;
self.controller.y1 = output;
Some(output)
}
pub fn get_adc(&self) -> Option<ElectricPotential> {
Some(self.adc_calibration.convert_data(self.adc_data?))
}
/// Get `SENS[01]` input resistance
pub fn get_sens(&self) -> Option<ElectricalResistance> {
let r_inner = ElectricalResistance::new::<ohm>(R_INNER);
let vref = ElectricPotential::new::<volt>(VREF_SENS);
let adc_input = self.get_adc()?;
let r = r_inner * adc_input / (vref - adc_input);
Some(r)
}
pub fn get_temperature(&self) -> Option<f32> {
let r = self.get_sens()?;
let temperature = self.sh.get_temperature(r);
Some(temperature)
}
pub fn apply_pid_params(&mut self, pid_params: Parameters) {
self.controller.parameters = pid_params;
}
pub fn set_pid_params(&mut self, param: PidSettings, val: f32, curr_rate: f32) {
match param {
PidSettings::Kp => {
self.controller.parameters.kp = val;
}
PidSettings::Ki => {
self.controller.parameters.ki = val * curr_rate;
}
PidSettings::Kd => {
self.controller.parameters.kd = val / curr_rate;
}
PidSettings::Min => {
self.controller.parameters.output_min = val;
}
PidSettings::Max => {
self.controller.parameters.output_max = val;
}
}
}
pub fn update_pid_params_with_new_sampling_rate(&mut self, old_rate: f32, new_rate: f32) {
self.controller.parameters.ki = self.controller.parameters.ki * old_rate / new_rate;
self.controller.parameters.kd = self.controller.parameters.kd * new_rate / old_rate;
}
pub fn get_abs_pid_params(&mut self, curr_rate: f32) -> Parameters {
let mut pid_params = self.controller.parameters.clone();
pid_params.ki = self.controller.parameters.ki / curr_rate;
pid_params.kd = self.controller.parameters.kd * curr_rate;
pid_params
}
pub fn reset_pid_state(&mut self) {
self.controller.u1 = 0.0;
self.controller.x1 = 0.0;
self.controller.x2 = 0.0;
self.controller.y1 = 0.0;
}
pub fn set_pid_setpoint(&mut self, temperature: f32) {
self.set_point = temperature;
}
pub fn get_pid_setpoint(&mut self) -> f32 {
self.set_point
}
pub fn set_sh_t0(&mut self, t0: f32) {
self.sh.t0 = t0
}
pub fn set_sh_r0(&mut self, r0: ElectricalResistance) {
self.sh.r0 = r0
}
pub fn set_sh_beta(&mut self, beta: f32) {
self.sh.b = beta
}
pub fn set_adc_calibration(&mut self, adc_cal: ad7172::ChannelCalibration) {
self.adc_calibration = adc_cal;
}
pub fn set_pid_engaged(&mut self, pid_engaged: bool) {
self.pid_engaged = pid_engaged;
}
pub fn get_pid_engaged(&mut self) -> bool {
self.pid_engaged
}
pub fn get_pid_settings(&mut self) -> Parameters {
self.controller.parameters
}
pub fn get_sh(&mut self) -> sh::Parameters {
self.sh
}
pub fn apply_sh(&mut self, sh: sh::Parameters) {
self.sh = sh;
}
}

View File

@ -0,0 +1,35 @@
use miniconf::Tree;
use num_traits::float::Float;
use serde::{Deserialize, Serialize};
use uom::si::{electrical_resistance::ohm,
f32::ElectricalResistance,
ratio::ratio};
/// Steinhart-Hart equation parameters
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct Parameters {
/// Base temperature (Degree Celsius)
pub t0: f32,
/// Base resistance
pub r0: ElectricalResistance,
/// Beta (Kelvin)
pub b: f32,
}
impl Parameters {
/// Perform the voltage to temperature conversion.
pub fn get_temperature(&self, r: ElectricalResistance) -> f32 {
let inv_temp = 1.0 / (self.t0 + 273.15) + (r / self.r0).get::<ratio>().ln() / self.b;
1.0 / inv_temp - 273.15
}
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
t0: 25.0,
r0: ElectricalResistance::new::<ohm>(10_000.0),
b: 3800.0,
}
}
}

197
src/thermostat/temp_mon.rs Normal file
View File

@ -0,0 +1,197 @@
use miniconf::Tree;
use num_traits::Float;
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Deserialize, Serialize, Copy, Clone, Default, Debug)]
pub enum TempStatusEnum {
#[default]
Off,
OverTemp,
Unstable,
Stable,
ConstantCurrentMode,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct TempStatus {
pub status: TempStatusEnum,
pub over_temp_alarm: bool,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct TempMonSettings {
pub upper_limit: f32,
pub lower_limit: f32,
}
pub struct TempMon {
pub upper_limit: f32,
pub lower_limit: f32,
pub set_point: f32,
pub status: TempStatus,
state: State,
count: u32,
is_set_point_changed: bool,
is_limit_changed: bool,
}
impl Default for TempMon {
fn default() -> Self {
Self {
upper_limit: 45.0,
lower_limit: 0.0,
set_point: 0.0,
status: TempStatus {
status: TempStatusEnum::Off,
over_temp_alarm: false,
},
state: State::default(),
count: 0,
// stable_temp_count: 0,
is_set_point_changed: false,
is_limit_changed: false,
}
}
}
#[derive(Default, PartialEq, Debug)]
enum State {
#[default]
PwrOff,
ConstantCurrentMode,
PidStartUp,
PidStable,
OverTempAlarm,
}
impl TempMon {
const OVER_TEMP_COUNT_LIMIT: u32 = 25;
const TEMP_STABLE_COUNT_LIMIT: u32 = 100;
pub fn set_upper_limit(&mut self, upper_limit: f32) {
self.upper_limit = upper_limit;
self.is_limit_changed = true;
}
pub fn get_upper_limit(&mut self) -> f32 {
self.upper_limit
}
pub fn set_lower_limit(&mut self, lower_limit: f32) {
self.lower_limit = lower_limit;
self.is_limit_changed = true;
}
pub fn get_lower_limit(&mut self) -> f32 {
self.lower_limit
}
pub fn set_setpoint(&mut self, set_point: f32) {
if self.set_point != set_point {
self.is_set_point_changed = true;
self.count = 0;
}
self.set_point = set_point;
}
pub fn clear_alarm(&mut self) {
self.status.over_temp_alarm = false;
self.state = State::default();
}
pub fn update_status(&mut self, pid_engaged: bool, pwr_on: bool, temp: f32) {
match self.state {
State::PwrOff => {
self.is_set_point_changed = false;
self.status.status = TempStatusEnum::Off;
self.count = 0;
// State Transition
if pwr_on {
if pid_engaged {
self.state = State::PidStartUp;
} else {
self.state = State::ConstantCurrentMode
}
}
}
State::ConstantCurrentMode => {
let is_over_temp = temp > self.upper_limit || temp < self.lower_limit;
self.status.status = TempStatusEnum::ConstantCurrentMode;
if is_over_temp {
self.state = State::OverTempAlarm;
self.status.status = TempStatusEnum::OverTemp;
} else if !pwr_on {
self.state = State::PwrOff;
self.status.status = TempStatusEnum::Off;
} else if pid_engaged {
self.state = State::PidStartUp;
self.status.status = TempStatusEnum::Unstable;
self.is_set_point_changed = false;
}
}
State::PidStartUp | State::PidStable => {
let mut is_over_temp = temp > self.upper_limit || temp < self.lower_limit;
if self.state != State::PidStartUp {
is_over_temp = is_over_temp || (temp - self.set_point).abs() > 0.5;
}
let is_within_spec: bool = (temp - self.set_point).abs() < 0.001;
if is_over_temp {
if self.count > TempMon::OVER_TEMP_COUNT_LIMIT {
self.status.status = TempStatusEnum::OverTemp;
} else {
self.count += 1;
}
} else if is_within_spec {
if self.count > TempMon::TEMP_STABLE_COUNT_LIMIT {
self.status.status = TempStatusEnum::Stable;
} else {
self.count += 1;
}
} else {
self.status.status = TempStatusEnum::Unstable;
self.count = 0;
}
// State Transition
if !pwr_on {
self.state = State::PwrOff;
} else {
if self.status.status == TempStatusEnum::OverTemp {
self.state = State::OverTempAlarm;
} else if self.is_set_point_changed {
self.is_set_point_changed = false;
self.state = State::PidStartUp;
} else if self.is_limit_changed {
self.is_limit_changed = false;
if is_over_temp {
self.state = State::PidStartUp;
self.count = 0;
}
} else if self.status.status == TempStatusEnum::Stable {
self.state = State::PidStable;
} else if !pid_engaged {
self.state = State::ConstantCurrentMode
}
}
}
State::OverTempAlarm => {
self.is_set_point_changed = false;
self.status.over_temp_alarm = true;
}
}
}
pub fn get_status(&mut self) -> TempStatus {
self.status
}
pub fn get_settings(&mut self) -> TempMonSettings {
TempMonSettings {
upper_limit: self.upper_limit,
lower_limit: self.lower_limit,
}
}
}

View File

@ -0,0 +1,645 @@
use core::{f32::NAN, marker::PhantomData};
use log::debug;
use miniconf::Tree;
use serde::{Deserialize, Serialize};
use uom::si::{electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
f32::{ElectricCurrent, ElectricPotential, ElectricalResistance},
ratio::ratio};
use crate::{sys_timer,
thermostat::{ad5680,
ad7172::{self, FilterType, PostFilter, SingleChODR},
max1968::{AdcReadTarget, PwmPinsEnum, MAX1968},
pid_state,
pid_state::{Parameters as PidParams, PidSettings, PidState},
steinhart_hart::Parameters as Sh_Params,
temp_mon::{TempMon, TempMonSettings, TempStatus}}};
pub const R_SENSE: ElectricalResistance = ElectricalResistance {
dimension: PhantomData,
units: PhantomData,
value: 0.05,
};
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct TempAdcFilter {
pub filter_type: FilterType,
pub sinc5sinc1odr: Option<SingleChODR>,
pub sinc3odr: Option<SingleChODR>,
pub sinc5sinc1postfilter: Option<PostFilter>,
pub sinc3fineodr: Option<f32>,
pub rate: Option<f32>,
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, Tree)]
pub struct TecSettings {
pub default_pwr_on: bool,
pub center_pt: ElectricPotential,
pub max_v_set: ElectricPotential,
pub max_i_pos_set: ElectricCurrent,
pub max_i_neg_set: ElectricCurrent,
pub i_set: ElectricCurrent,
pub vref: ElectricPotential,
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, Tree, Default)]
pub struct Time {
ms: u32,
us: u32,
}
impl TecSettings {
pub const TEC_VSEC_BIAS_V: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 1.65,
};
const MAX_I_SET: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 3.0,
};
const MAX_V_DUTY_TO_VOLTAGE_RATE: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 4.0 * 3.3,
};
pub const MAX_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 4.3,
};
const MAX_V_DUTY_MAX: f64 =
TecSettings::MAX_V_MAX.value as f64 / TecSettings::MAX_V_DUTY_TO_VOLTAGE_RATE.value as f64;
const MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0 / (10.0 * R_SENSE.value / 3.3),
};
pub const MAX_I_POS_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 3.0,
};
pub const MAX_I_NEG_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 3.0,
};
// .get::<ratio>() is not implemented for const
const MAX_I_POS_DUTY_MAX: f64 =
TecSettings::MAX_I_POS_CURRENT.value as f64 / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value as f64;
const MAX_I_NEG_DUTY_MAX: f64 =
TecSettings::MAX_I_NEG_CURRENT.value as f64 / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value as f64;
}
impl Default for TecSettings {
fn default() -> Self {
Self {
default_pwr_on: false,
center_pt: ElectricPotential::new::<volt>(1.5),
max_v_set: ElectricPotential::new::<volt>(5.0),
max_i_pos_set: ElectricCurrent::new::<ampere>(1.0),
max_i_neg_set: ElectricCurrent::new::<ampere>(1.0),
i_set: ElectricCurrent::new::<ampere>(0.0),
vref: ElectricPotential::new::<volt>(1.5),
}
}
}
pub struct Thermostat {
max1968: MAX1968,
ad7172: ad7172::AdcPhy,
interval: Time,
prev_ts: Time,
pub tec_settings: TecSettings,
pid_ctrl_ch0: PidState,
temp_mon: TempMon,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct ThermostatSettingsSummary {
default_pwr_on: bool,
pid_engaged: bool,
temperature_setpoint: f32,
tec_settings: TecSettingSummary,
pid_params: PidParams,
temp_adc_settings: TempAdcFilter,
temp_mon_settings: TempMonSettings,
thermistor_params: ThermistorParams,
}
impl Thermostat {
pub fn new(max1968: MAX1968, ad7172: ad7172::AdcPhy) -> Self {
Thermostat {
max1968: max1968,
ad7172: ad7172,
interval: Time::default(),
prev_ts: Time::default(),
tec_settings: TecSettings::default(),
pid_ctrl_ch0: PidState::default(),
temp_mon: TempMon::default(),
}
}
pub fn setup(&mut self) {
self.tec_setup();
let t_adc_ch0_cal = self.t_adc_setup();
self.pid_ctrl_ch0.set_adc_calibration(t_adc_ch0_cal);
}
/// start_tec_readings_conversion() should not be called before the current
/// DMA request is serviced or the conversion process will be restarted
/// Thus, no new readings is available when you call get_tec_readings() fn
pub fn start_tec_readings_conversion(&mut self) {
self.max1968.dma_adc_start_conversion();
}
fn tec_setup(&mut self) {
self.power_down();
self.tec_settings = TecSettings::default();
self.set_i(self.tec_settings.i_set);
self.set_max_v(self.tec_settings.max_v_set);
self.set_max_i_pos(self.tec_settings.max_i_pos_set);
self.set_max_i_neg(self.tec_settings.max_i_neg_set);
}
fn t_adc_setup(&mut self) -> ad7172::ChannelCalibration {
self.ad7172.set_sync_enable(false).unwrap();
self.ad7172
.setup_channel(0, ad7172::Input::Ain0, ad7172::Input::Ain1)
.unwrap();
let adc_calibration0 = self.ad7172.get_calibration(0).expect("adc_calibration0");
self.ad7172.start_continuous_conversion().unwrap();
adc_calibration0
}
pub fn poll_adc(&mut self) -> bool {
let mut data_rdy = false;
self.ad7172.data_ready().unwrap().map(|_ch| {
let data = self.ad7172.read_data().unwrap();
let state: &mut PidState = &mut self.pid_ctrl_ch0;
state.update(data);
let pid_engaged = state.get_pid_engaged();
let temp = self.get_temperature();
self.temp_mon
.update_status(pid_engaged, self.max1968.is_powered_on(), temp);
debug!("state.get_pid_engaged(): {:?}", pid_engaged);
debug!("Temperature: {:?} degree", temp);
data_rdy = true;
let (ms, us) = sys_timer::now_precise();
if us < self.prev_ts.us {
self.interval = Time {
ms: ms - self.prev_ts.ms - 1,
us: (us + 1000).abs_diff(self.prev_ts.us),
};
} else {
self.interval = Time {
ms: ms - self.prev_ts.ms,
us: (us).abs_diff(self.prev_ts.us),
};
}
self.prev_ts = Time {
ms: ms,
us: us,
};
});
data_rdy
}
pub fn get_poll_interval(&mut self) -> Time {
self.interval
}
pub fn update_pid(&mut self) {
let state: &mut PidState = &mut self.pid_ctrl_ch0;
let pid_engaged = state.get_pid_engaged();
if pid_engaged {
match state.update_pid() {
Some(pid_output) => {
self.set_i(ElectricCurrent::new::<ampere>(pid_output as f32));
debug!(
"Temperature Set Point: {:?} degree",
self.pid_ctrl_ch0.get_pid_setpoint()
);
}
None => {}
}
}
}
pub fn get_temp_mon_status(&mut self) -> TempStatus {
self.temp_mon.get_status()
}
pub fn power_up(&mut self) {
if !self.max1968.is_powered_on() {
self.max1968.power_up();
self.pid_ctrl_ch0.reset_pid_state();
}
}
pub fn power_down(&mut self) {
self.max1968.power_down();
}
fn set_center_pt(&mut self, value: ElectricPotential) {
self.tec_settings.center_pt = value;
}
pub fn set_default_pwr_on(&mut self, pwr_on: bool) {
self.tec_settings.default_pwr_on = pwr_on;
}
pub fn set_i(&mut self, i_tec: ElectricCurrent) -> ElectricCurrent {
let voltage = i_tec * 10.0 * R_SENSE + self.tec_settings.center_pt;
let _voltage = self.max1968.set_dac(voltage, self.max1968.dac_out_range);
self.tec_settings.i_set = i_tec;
self.tec_settings.i_set
}
pub fn set_max_v(&mut self, max_v: ElectricPotential) -> ElectricPotential {
let duty = (max_v / TecSettings::MAX_V_DUTY_TO_VOLTAGE_RATE).get::<ratio>();
let _duty = self
.max1968
.set_pwm(PwmPinsEnum::MaxV, duty as f64, TecSettings::MAX_V_DUTY_MAX);
self.tec_settings.max_v_set = max_v;
self.tec_settings.max_v_set
}
pub fn set_max_i_pos(&mut self, max_i_pos: ElectricCurrent) -> ElectricCurrent {
let duty = (max_i_pos / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
let _duty = self
.max1968
.set_pwm(PwmPinsEnum::MaxPosI, duty as f64, TecSettings::MAX_I_POS_DUTY_MAX);
self.tec_settings.max_i_pos_set = max_i_pos;
self.tec_settings.max_i_pos_set
}
pub fn set_max_i_neg(&mut self, max_i_neg: ElectricCurrent) -> ElectricCurrent {
let duty = (max_i_neg / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
let _duty = self
.max1968
.set_pwm(PwmPinsEnum::MaxNegI, duty as f64, TecSettings::MAX_I_NEG_DUTY_MAX);
self.tec_settings.max_i_neg_set = max_i_neg;
self.tec_settings.max_i_neg_set
}
#[allow(unused)]
fn get_dac_vfb(&mut self) -> ElectricPotential {
self.max1968.adc_read(AdcReadTarget::DacVfb, 16)
}
#[allow(unused)]
fn get_vref(&mut self) -> ElectricPotential {
self.max1968.adc_read(AdcReadTarget::VREF, 16)
}
#[allow(unused)]
pub fn get_tec_i(&mut self) -> ElectricCurrent {
let vref = self.max1968.adc_read(AdcReadTarget::VREF, 16);
(self.max1968.adc_read(AdcReadTarget::ITec, 16) - vref) / ElectricalResistance::new::<ohm>(0.4)
}
#[allow(unused)]
pub fn get_tec_v(&mut self) -> ElectricPotential {
(self.max1968.adc_read(AdcReadTarget::VTec, 16) - TecSettings::TEC_VSEC_BIAS_V) * 4.0
}
pub fn get_tec_readings(&mut self) -> (ElectricPotential, ElectricCurrent) {
let vref = self.tec_settings.vref;
let (vtec, itec) = self.max1968.get_tec_readings();
(
(vtec - TecSettings::TEC_VSEC_BIAS_V) * 4.0,
(itec - vref) / ElectricalResistance::new::<ohm>(0.4),
)
}
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
///
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
/// The CTLI input signal is centered around VREF of the MAX chip. Applying VREF to CTLI sets the output current to 0.
///
/// This calibration routine measures the VREF voltage and the DAC output with the STM32 ADC, and uses a breadth-first
/// search to find the DAC setting that will produce a DAC output voltage closest to VREF. This DAC output voltage will
/// be stored and used in subsequent i_set routines to bias the current control signal to the measured VREF, reducing
/// the offset error of the current control signal.
///
/// The input offset of the STM32 ADC is eliminated by using the same ADC for the measurements, and by only using the
/// difference in VREF and DAC output for the calibration.
///
/// This routine should be called only once after boot, repeated reading of the vref signal and changing of the stored
/// VREF measurement can introduce significant noise at the current output, degrading the stabilily performance of the
/// thermostat.
pub fn calibrate_dac_value(&mut self) {
const DAC_BIT: u32 = 18;
const ADC_BIT: u32 = 12;
let target_voltage = self.max1968.adc_read(AdcReadTarget::VREF, 512);
let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0);
for step in (DAC_BIT - ADC_BIT - 1..DAC_BIT).rev() {
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
self.max1968.phy.dac.set(value).unwrap();
sys_timer::sleep(5);
let dac_feedback = self.max1968.adc_read(AdcReadTarget::DacVfb, 64);
let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) {
break;
} else if error < best_error {
best_error = error;
start_value = value;
let vref = (value as f32 / ad5680::MAX_VALUE as f32) * self.max1968.dac_out_range;
self.set_center_pt(vref);
}
}
}
self.tec_settings.vref = target_voltage;
}
pub fn set_pid_engaged(&mut self, val: bool) {
self.pid_ctrl_ch0.set_pid_engaged(val);
}
fn get_pid_engaged(&mut self) -> bool {
self.pid_ctrl_ch0.get_pid_engaged()
}
pub fn get_status_report(&mut self) -> StatusReport {
let (tec_v, tec_i) = self.get_tec_readings();
let temperature: Option<f32>;
match self.pid_ctrl_ch0.get_temperature() {
Some(val) => temperature = Some(val),
None => {
temperature = None;
}
}
StatusReport {
interval: self.get_poll_interval(),
pwr_on: self.max1968.is_powered_on(),
pid_engaged: self.get_pid_engaged(),
temp_mon_status: self.temp_mon.get_status(),
temperature: temperature,
i_set: self.tec_settings.i_set,
tec_i: tec_i,
tec_v: tec_v,
}
}
pub fn get_temperature(&mut self) -> f32 {
match self.pid_ctrl_ch0.get_temperature() {
Some(val) => val,
None => NAN,
}
}
fn get_pid_settings(&mut self) -> pid_state::Parameters {
self.pid_ctrl_ch0.get_pid_settings()
}
fn get_steinhart_hart(&mut self) -> ThermistorParams {
let sh = self.pid_ctrl_ch0.get_sh();
ThermistorParams {
t0: sh.t0,
r0: sh.r0,
b: sh.b,
}
}
fn apply_steinhart_hart(&mut self, sh: ThermistorParams) {
self.pid_ctrl_ch0.apply_sh(Sh_Params {
t0: sh.t0,
r0: sh.r0,
b: sh.b,
})
}
fn get_tec_settings(&mut self) -> TecSettingSummary {
TecSettingSummary {
i_set: TecSettingsSummaryField {
value: self.tec_settings.i_set,
max: TecSettings::MAX_I_SET,
},
max_v: TecSettingsSummaryField {
value: self.tec_settings.max_v_set,
max: TecSettings::MAX_V_MAX,
},
max_i_pos: TecSettingsSummaryField {
value: self.tec_settings.max_i_pos_set,
max: TecSettings::MAX_I_POS_CURRENT,
},
max_i_neg: TecSettingsSummaryField {
value: self.tec_settings.max_i_neg_set,
max: TecSettings::MAX_I_NEG_CURRENT,
},
}
}
pub fn get_calibrated_vdda(&mut self) -> u32 {
self.max1968.get_calibrated_vdda()
}
pub fn set_pid(&mut self, param: PidSettings, val: f32) {
let curr_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
self.pid_ctrl_ch0.set_pid_params(param, val, curr_rate);
}
pub fn set_sh_beta(&mut self, beta: f32) {
self.pid_ctrl_ch0.set_sh_beta(beta);
}
pub fn set_sh_r0(&mut self, r0: ElectricalResistance) {
self.pid_ctrl_ch0.set_sh_r0(r0);
}
pub fn set_sh_t0(&mut self, t0: f32) {
self.pid_ctrl_ch0.set_sh_t0(t0);
}
pub fn set_temperature_setpoint(&mut self, t: f32) {
let t = t
.min(self.temp_mon.get_upper_limit())
.max(self.temp_mon.get_lower_limit());
self.pid_ctrl_ch0.set_pid_setpoint(t);
self.temp_mon.set_setpoint(t);
}
pub fn apply_temp_mon_settings(&mut self, settings: TempMonSettings) {
self.temp_mon
.set_upper_limit(settings.upper_limit);
self.temp_mon
.set_lower_limit(settings.lower_limit);
}
pub fn set_temp_mon_upper_limit(&mut self, t: f32) {
self.temp_mon.set_upper_limit(t);
}
pub fn set_temp_mon_lower_limit(&mut self, t: f32) {
self.temp_mon.set_lower_limit(t);
}
pub fn set_temp_adc_sinc5_sinc1_filter(&mut self, index: u8, odr: ad7172::SingleChODR) {
let old_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
let new_rate = odr.output_rate().unwrap();
self.pid_ctrl_ch0
.update_pid_params_with_new_sampling_rate(old_rate, new_rate);
self.ad7172.set_sinc5_sinc1_filter(index, odr).unwrap();
}
pub fn set_temp_adc_sinc3_filter(&mut self, index: u8, odr: ad7172::SingleChODR) {
let old_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
let new_rate = odr.output_rate().unwrap();
self.pid_ctrl_ch0
.update_pid_params_with_new_sampling_rate(old_rate, new_rate);
self.ad7172.set_sinc3_filter(index, odr).unwrap();
}
pub fn set_temp_adc_sinc5_sinc1_with_postfilter(&mut self, index: u8, odr: ad7172::PostFilter) {
let old_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
let new_rate = odr.output_rate().unwrap();
self.pid_ctrl_ch0
.update_pid_params_with_new_sampling_rate(old_rate, new_rate);
self.ad7172
.set_sinc5_sinc1_with_50hz_60hz_rejection(index, odr)
.unwrap();
}
pub fn set_temp_adc_sinc3_fine_filter(&mut self, index: u8, rate: f32) {
let old_rate = self.ad7172.get_filter_type_and_rate(0).unwrap().3;
self.pid_ctrl_ch0
.update_pid_params_with_new_sampling_rate(old_rate, rate);
self.ad7172.set_sinc3_fine_filter(index, rate).unwrap();
}
pub fn clear_temp_mon_alarm(&mut self) {
self.temp_mon.clear_alarm();
}
fn get_temp_mon_settings(&mut self) -> TempMonSettings {
self.temp_mon.get_settings()
}
pub fn get_settings_summary(&mut self) -> ThermostatSettingsSummary {
let mut temp_adc_settings: TempAdcFilter = TempAdcFilter::default();
match self.ad7172.get_filter_type_and_rate(0) {
Ok((filter_type, single_ch_odr, post_filter, rate)) => {
temp_adc_settings.filter_type = filter_type;
temp_adc_settings.rate = Some(rate);
match temp_adc_settings.filter_type {
FilterType::Sinc5Sinc1 => {
temp_adc_settings.sinc5sinc1odr = single_ch_odr;
}
FilterType::Sinc3 => {
temp_adc_settings.sinc3odr = single_ch_odr;
}
FilterType::Sinc3WithFineODR => {
temp_adc_settings.sinc3fineodr = Some(rate)
}
FilterType::Sinc5Sinc1With50hz60HzRejection => {
temp_adc_settings.sinc5sinc1postfilter = post_filter
}
}
}
Err(_) => {
panic!("Cannot read ADC filter type and rate");
}
}
ThermostatSettingsSummary {
default_pwr_on: self.tec_settings.default_pwr_on,
pid_engaged: self.get_pid_engaged(),
temperature_setpoint: self.pid_ctrl_ch0.get_pid_setpoint(),
tec_settings: self.get_tec_settings(),
pid_params: self.pid_ctrl_ch0.get_abs_pid_params(temp_adc_settings.rate.unwrap()),
temp_adc_settings: temp_adc_settings,
temp_mon_settings: self.get_temp_mon_settings(),
thermistor_params: self.get_steinhart_hart(),
}
}
pub fn load_settings_from_summary(&mut self, settings: ThermostatSettingsSummary) {
self.power_down();
self.set_max_i_neg(settings.tec_settings.max_i_neg.value);
self.set_max_i_pos(settings.tec_settings.max_i_pos.value);
self.set_max_v(settings.tec_settings.max_v.value);
self.apply_steinhart_hart(settings.thermistor_params);
self.apply_temp_mon_settings(settings.temp_mon_settings);
match settings.temp_adc_settings.rate {
Some(rate) => match settings.temp_adc_settings.filter_type {
FilterType::Sinc3 => self.set_temp_adc_sinc3_filter(0, SingleChODR::closest(rate).unwrap()),
FilterType::Sinc5Sinc1 => self.set_temp_adc_sinc5_sinc1_filter(0, SingleChODR::closest(rate).unwrap()),
FilterType::Sinc3WithFineODR => self.set_temp_adc_sinc3_fine_filter(0, rate),
FilterType::Sinc5Sinc1With50hz60HzRejection => {
self.set_temp_adc_sinc5_sinc1_with_postfilter(0, PostFilter::closest(rate).unwrap())
}
},
None => {
debug!(" Temperature ADC Settings is not found");
}
}
self.set_pid_engaged(settings.pid_engaged);
self.pid_ctrl_ch0.apply_pid_params(settings.pid_params);
self.pid_ctrl_ch0.update_pid_params_with_new_sampling_rate(settings.temp_adc_settings.rate.unwrap(), 1.0);
self.set_temperature_setpoint(settings.temperature_setpoint);
if !settings.pid_engaged {
self.set_i(settings.tec_settings.i_set.value);
}
self.clear_temp_mon_alarm();
self.set_default_pwr_on(settings.default_pwr_on);
if settings.default_pwr_on {
self.power_up();
} else {
self.power_down();
}
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReport {
interval: Time,
pwr_on: bool,
pid_engaged: bool,
temp_mon_status: TempStatus,
temperature: Option<f32>,
i_set: ElectricCurrent,
tec_i: ElectricCurrent,
tec_v: ElectricPotential,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct TecSettingsSummaryField<T> {
value: T,
max: T,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct TecSettingSummary {
i_set: TecSettingsSummaryField<ElectricCurrent>,
max_v: TecSettingsSummaryField<ElectricPotential>,
max_i_pos: TecSettingsSummaryField<ElectricCurrent>,
max_i_neg: TecSettingsSummaryField<ElectricCurrent>,
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, Tree)]
pub struct ThermistorParams {
t0: f32,
r0: ElectricalResistance,
b: f32,
}