ldcon 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ldcon/__init__.py +302 -0
- ldcon/_moved.py +49 -0
- ldcon/agent_tools.py +4 -0
- ldcon/agents/__init__.py +1 -0
- ldcon/agents/adjustment_guidance.py +73 -0
- ldcon/agents/adjustment_quality.py +399 -0
- ldcon/agents/agent_adapters.py +709 -0
- ldcon/agents/agent_background.py +307 -0
- ldcon/agents/agent_bundle.py +75 -0
- ldcon/agents/agent_cards.py +171 -0
- ldcon/agents/agent_config_access.py +22 -0
- ldcon/agents/agent_configuration.py +224 -0
- ldcon/agents/agent_configuration_policy.py +67 -0
- ldcon/agents/agent_connect.py +93 -0
- ldcon/agents/agent_device_parameters.py +41 -0
- ldcon/agents/agent_execution.py +77 -0
- ldcon/agents/agent_history.py +112 -0
- ldcon/agents/agent_inbox_watch.py +121 -0
- ldcon/agents/agent_interactive.py +386 -0
- ldcon/agents/agent_interactive_hooks.py +381 -0
- ldcon/agents/agent_language.py +48 -0
- ldcon/agents/agent_log_context.py +308 -0
- ldcon/agents/agent_log_read.py +250 -0
- ldcon/agents/agent_log_watch.py +27 -0
- ldcon/agents/agent_metrics.py +111 -0
- ldcon/agents/agent_monitor.py +272 -0
- ldcon/agents/agent_opencode_http.py +270 -0
- ldcon/agents/agent_opencode_mcp.py +64 -0
- ldcon/agents/agent_owner_status.py +216 -0
- ldcon/agents/agent_profiles.py +186 -0
- ldcon/agents/agent_providers.py +53 -0
- ldcon/agents/agent_repairs.py +105 -0
- ldcon/agents/agent_routing.py +90 -0
- ldcon/agents/agent_runtime.py +70 -0
- ldcon/agents/agent_sandbox.py +141 -0
- ldcon/agents/agent_startup_notice.py +142 -0
- ldcon/agents/agent_task.py +336 -0
- ldcon/agents/agent_tools.py +488 -0
- ldcon/agents/agent_window.py +70 -0
- ldcon/agents/agent_workspace.py +340 -0
- ldcon/agents/card_fallback.py +284 -0
- ldcon/agents/dlc_pi_adjustment.py +250 -0
- ldcon/agents/dlc_pi_plan.py +24 -0
- ldcon/agents/dlc_pi_response_plan.py +26 -0
- ldcon/agents/failure_policy.py +473 -0
- ldcon/agents/fallback_service.py +146 -0
- ldcon/agents/fallback_targets.py +96 -0
- ldcon/agents/gateway_role.py +31 -0
- ldcon/agents/manual_adjustment_grant.py +66 -0
- ldcon/agents/measurement_grant.py +95 -0
- ldcon/agents/monitor_quality_binding.py +33 -0
- ldcon/agents/notification_policy.py +346 -0
- ldcon/agents/operating_mode_client.py +99 -0
- ldcon/agents/report_pdf.py +105 -0
- ldcon/agents/resident_adjustment.py +572 -0
- ldcon/agents/resident_agent.py +438 -0
- ldcon/agents/resident_quality_apply.py +233 -0
- ldcon/agents/task_decisions.py +218 -0
- ldcon/agents/teams_card.py +100 -0
- ldcon/agents/teams_destinations.py +157 -0
- ldcon/cld_owner.py +4 -0
- ldcon/cli.py +19 -0
- ldcon/core/__init__.py +1 -0
- ldcon/core/adjustment_plan.py +47 -0
- ldcon/core/adjustment_registry.py +100 -0
- ldcon/core/agent_authority.py +903 -0
- ldcon/core/card_admission.py +15 -0
- ldcon/core/card_definition.py +14 -0
- ldcon/core/card_reporting.py +238 -0
- ldcon/core/configuration.py +357 -0
- ldcon/core/configuration_transaction.py +225 -0
- ldcon/core/contracts.py +86 -0
- ldcon/core/current_settings.py +86 -0
- ldcon/core/deck_yaml.py +45 -0
- ldcon/core/device_conditions.py +133 -0
- ldcon/core/execution_context.py +3 -0
- ldcon/core/instrument_client.py +190 -0
- ldcon/core/interface_cards.py +520 -0
- ldcon/core/interface_schema.py +640 -0
- ldcon/core/laser_assets.py +238 -0
- ldcon/core/metric_quantities.py +38 -0
- ldcon/core/modulation.py +107 -0
- ldcon/core/operation_registry.py +195 -0
- ldcon/core/paths.py +26 -0
- ldcon/core/period_context.py +118 -0
- ldcon/core/records.py +4 -0
- ldcon/core/result_reservations.py +469 -0
- ldcon/core/site_bindings.py +25 -0
- ldcon/core/state_layout.py +70 -0
- ldcon/core/target_policy.py +79 -0
- ldcon/core/telegraf.py +106 -0
- ldcon/core/telemetry.py +130 -0
- ldcon/core/temperature_settings.py +12 -0
- ldcon/dashboard_metrics.py +4 -0
- ldcon/device_registry.py +4 -0
- ldcon/drivers/__init__.py +1 -0
- ldcon/drivers/cld1015.py +296 -0
- ldcon/drivers/cld1015_config.py +31 -0
- ldcon/drivers/dlcpro.py +177 -0
- ldcon/drivers/dlcpro_analog.py +33 -0
- ldcon/drivers/dlcpro_console.py +133 -0
- ldcon/drivers/dlcpro_priority.py +46 -0
- ldcon/drivers/ecdl.py +334 -0
- ldcon/drivers/esp32_servo.py +187 -0
- ldcon/drivers/fizeau.py +114 -0
- ldcon/drivers/frequency_sweep.py +69 -0
- ldcon/drivers/influx_wavelength.py +70 -0
- ldcon/drivers/metric_sources.py +82 -0
- ldcon/drivers/native_dither_telemetry.py +50 -0
- ldcon/drivers/rigol_scope.py +126 -0
- ldcon/drivers/siglent.py +418 -0
- ldcon/drivers/staircase.py +155 -0
- ldcon/drivers/wsu_udp.py +164 -0
- ldcon/esp32_time_telemetry.py +4 -0
- ldcon/example_config.py +41 -0
- ldcon/fallback_service.py +4 -0
- ldcon/instrument_owner.py +4 -0
- ldcon/instrument_service.py +4 -0
- ldcon/interface_worker.py +4 -0
- ldcon/job_service.py +4 -0
- ldcon/jobs/__init__.py +1 -0
- ldcon/jobs/adaptive_pid.py +234 -0
- ldcon/jobs/adaptive_window_session.py +922 -0
- ldcon/jobs/adjustment_continuation.py +284 -0
- ldcon/jobs/axis_frequency.py +219 -0
- ldcon/jobs/axis_frequency_report.py +86 -0
- ldcon/jobs/background_parameter_session.py +142 -0
- ldcon/jobs/background_pid_session.py +447 -0
- ldcon/jobs/branch_maps.py +71 -0
- ldcon/jobs/branch_target_migration.py +33 -0
- ldcon/jobs/card_runtime.py +157 -0
- ldcon/jobs/change_measure.py +293 -0
- ldcon/jobs/change_measure_adapter.py +360 -0
- ldcon/jobs/change_measure_dlc.py +94 -0
- ldcon/jobs/change_measure_parameters.py +222 -0
- ldcon/jobs/change_measure_target.py +135 -0
- ldcon/jobs/current_cards.py +159 -0
- ldcon/jobs/current_devices.py +71 -0
- ldcon/jobs/dashboard_api.py +61 -0
- ldcon/jobs/device_parameters.py +426 -0
- ldcon/jobs/dlc_pi_measurement.py +151 -0
- ldcon/jobs/dlcpro_awg_analysis.py +77 -0
- ldcon/jobs/dlcpro_awg_card.py +116 -0
- ldcon/jobs/dlcpro_cards.py +240 -0
- ldcon/jobs/ecdl_branch_hop.py +175 -0
- ldcon/jobs/ecdl_branch_recovery.py +176 -0
- ldcon/jobs/ecdl_devices.py +306 -0
- ldcon/jobs/ecdl_locked_sweep_proxy.py +526 -0
- ldcon/jobs/ecdl_mapped_recovery.py +6 -0
- ldcon/jobs/ecdl_mode_map.py +469 -0
- ldcon/jobs/ecdl_mode_recovery.py +20 -0
- ldcon/jobs/ecdl_online.py +158 -0
- ldcon/jobs/ecdl_operating_point.py +403 -0
- ldcon/jobs/ecdl_pi_commission.py +161 -0
- ldcon/jobs/ecdl_recovery.py +25 -0
- ldcon/jobs/ecdl_recovery_plan.py +90 -0
- ldcon/jobs/ecdl_recovery_test.py +59 -0
- ldcon/jobs/ecdl_relock.py +7 -0
- ldcon/jobs/ecdl_response_study.py +369 -0
- ldcon/jobs/ecdl_steady.py +343 -0
- ldcon/jobs/ecdl_thermal_context.py +242 -0
- ldcon/jobs/ecdl_window_center.py +734 -0
- ldcon/jobs/emission_card.py +102 -0
- ldcon/jobs/fringe_sweep_reference.py +89 -0
- ldcon/jobs/interface_devices.py +122 -0
- ldcon/jobs/interface_worker.py +471 -0
- ldcon/jobs/job_service.py +125 -0
- ldcon/jobs/laser_live.py +270 -0
- ldcon/jobs/learned_settling.py +450 -0
- ldcon/jobs/lock_target.py +461 -0
- ldcon/jobs/locked_triangle.py +36 -0
- ldcon/jobs/measurement_acquisition.py +51 -0
- ldcon/jobs/measurement_analysis.py +59 -0
- ldcon/jobs/measurement_execution.py +40 -0
- ldcon/jobs/measurement_notice.py +78 -0
- ldcon/jobs/measurement_presets.py +52 -0
- ldcon/jobs/measurement_report.py +76 -0
- ldcon/jobs/measurement_statistics.py +22 -0
- ldcon/jobs/metric_cards.py +34 -0
- ldcon/jobs/metric_interface.py +100 -0
- ldcon/jobs/modulation_sweep.py +274 -0
- ldcon/jobs/native_background_session.py +91 -0
- ldcon/jobs/native_current_acquire.py +143 -0
- ldcon/jobs/native_current_card.py +328 -0
- ldcon/jobs/native_current_online.py +129 -0
- ldcon/jobs/native_ff_proxy.py +1361 -0
- ldcon/jobs/native_ff_sweep.py +802 -0
- ldcon/jobs/online_pid.py +178 -0
- ldcon/jobs/operating_point.py +1399 -0
- ldcon/jobs/operating_point_proxy.py +236 -0
- ldcon/jobs/operation_execution.py +126 -0
- ldcon/jobs/operation_history.py +419 -0
- ldcon/jobs/optical_broker.py +23 -0
- ldcon/jobs/pi_algorithms.py +30 -0
- ldcon/jobs/pi_response.py +239 -0
- ldcon/jobs/progress.py +63 -0
- ldcon/jobs/quality_baseline.py +411 -0
- ldcon/jobs/recovery_decisions.py +131 -0
- ldcon/jobs/recovery_history.py +152 -0
- ldcon/jobs/recovery_procedure.py +285 -0
- ldcon/jobs/recovery_reference.py +123 -0
- ldcon/jobs/recovery_stationary.py +138 -0
- ldcon/jobs/response_algorithms.py +47 -0
- ldcon/jobs/response_comparison.py +62 -0
- ldcon/jobs/response_record.py +90 -0
- ldcon/jobs/runtime_parameters.py +434 -0
- ldcon/jobs/settling_profile.py +140 -0
- ldcon/jobs/shot_analysis.py +1300 -0
- ldcon/jobs/simulation.py +127 -0
- ldcon/jobs/slow_control.py +208 -0
- ldcon/jobs/slow_current.py +88 -0
- ldcon/jobs/study_adoption.py +122 -0
- ldcon/jobs/study_execution.py +182 -0
- ldcon/jobs/sweep_ff_analysis.py +320 -0
- ldcon/jobs/sweep_ff_optimization.py +202 -0
- ldcon/jobs/sweep_ff_plan.py +166 -0
- ldcon/jobs/sweep_ff_report.py +254 -0
- ldcon/jobs/sweep_ff_transport.py +326 -0
- ldcon/jobs/sweep_tracking.py +424 -0
- ldcon/jobs/sweep_verdict.py +288 -0
- ldcon/jobs/target_response.py +638 -0
- ldcon/jobs/target_response_analysis.py +65 -0
- ldcon/jobs/target_response_control.py +215 -0
- ldcon/jobs/target_response_decisions.py +152 -0
- ldcon/jobs/target_response_experiment.py +110 -0
- ldcon/jobs/target_response_feedforward.py +130 -0
- ldcon/jobs/target_response_search.py +225 -0
- ldcon/jobs/target_response_shift.py +475 -0
- ldcon/jobs/target_response_thermal.py +113 -0
- ldcon/jobs/target_response_thermal_experiment.py +120 -0
- ldcon/jobs/target_sequence.py +166 -0
- ldcon/jobs/temperature_identification.py +86 -0
- ldcon/jobs/wavelength_history.py +17 -0
- ldcon/jobs/window_actions.py +641 -0
- ldcon/jobs/window_center.py +784 -0
- ldcon/jobs/window_control.py +130 -0
- ldcon/jobs/window_decisions.py +105 -0
- ldcon/jobs/window_sampling.py +66 -0
- ldcon/jobs/workflow_artifacts.py +73 -0
- ldcon/migration/__init__.py +810 -0
- ldcon/migration/migration_decks.py +239 -0
- ldcon/migration/migration_readiness.py +397 -0
- ldcon/migration/migration_relocation.py +283 -0
- ldcon/monitor_owner.py +4 -0
- ldcon/owner_telemetry.py +4 -0
- ldcon/owners/__init__.py +1 -0
- ldcon/owners/adc_log_threshold.py +209 -0
- ldcon/owners/background_grants.py +44 -0
- ldcon/owners/cld_instrument_owner.py +96 -0
- ldcon/owners/cld_owner.py +706 -0
- ldcon/owners/cld_projection.py +194 -0
- ldcon/owners/cld_system_owner.py +276 -0
- ldcon/owners/current_owner.py +68 -0
- ldcon/owners/device_registry.py +229 -0
- ldcon/owners/dlcpro_awg_plan.py +115 -0
- ldcon/owners/dlcpro_awg_port.py +98 -0
- ldcon/owners/dlcpro_awg_protocol.py +190 -0
- ldcon/owners/dlcpro_awg_return.py +63 -0
- ldcon/owners/dlcpro_awg_sweep.py +437 -0
- ldcon/owners/dlcpro_awg_timing.py +35 -0
- ldcon/owners/dlcpro_awg_tracking.py +175 -0
- ldcon/owners/dlcpro_awg_tracking_guard.py +87 -0
- ldcon/owners/dlcpro_awg_tracking_report.py +49 -0
- ldcon/owners/dlcpro_awg_zero.py +82 -0
- ldcon/owners/dlcpro_deployment.py +66 -0
- ldcon/owners/dlcpro_ff.py +64 -0
- ldcon/owners/dlcpro_ff_adjustment.py +159 -0
- ldcon/owners/dlcpro_ff_delta_adjustment.py +194 -0
- ldcon/owners/dlcpro_frequency_window.py +61 -0
- ldcon/owners/dlcpro_observation.py +62 -0
- ldcon/owners/dlcpro_owner.py +830 -0
- ldcon/owners/dlcpro_pi_protocol.py +44 -0
- ldcon/owners/dlcpro_pi_trial.py +161 -0
- ldcon/owners/dlcpro_readback.py +50 -0
- ldcon/owners/dlcpro_service.py +203 -0
- ldcon/owners/dlcpro_target_changes.py +330 -0
- ldcon/owners/dlcpro_target_step.py +44 -0
- ldcon/owners/dlcpro_target_store.py +75 -0
- ldcon/owners/dlcpro_telemetry.py +72 -0
- ldcon/owners/ecdl_background_parameters.py +248 -0
- ldcon/owners/ecdl_background_pid.py +184 -0
- ldcon/owners/ecdl_hot_control.py +255 -0
- ldcon/owners/ecdl_independent_pid.py +95 -0
- ldcon/owners/ecdl_locked_study.py +122 -0
- ldcon/owners/ecdl_locked_sweep.py +1875 -0
- ldcon/owners/ecdl_native_pid.py +181 -0
- ldcon/owners/ecdl_optical.py +37 -0
- ldcon/owners/ecdl_owner.py +1058 -0
- ldcon/owners/ecdl_policy_reload.py +494 -0
- ldcon/owners/ecdl_prelock.py +612 -0
- ldcon/owners/ecdl_projection.py +636 -0
- ldcon/owners/ecdl_steady_owner.py +1859 -0
- ldcon/owners/ecdl_steady_policy.py +212 -0
- ldcon/owners/ecdl_target_ff.py +214 -0
- ldcon/owners/ecdl_target_settling.py +163 -0
- ldcon/owners/ecdl_tec_pair.py +88 -0
- ldcon/owners/ecdl_thermal_study.py +251 -0
- ldcon/owners/emission.py +365 -0
- ldcon/owners/emission_adapters.py +268 -0
- ldcon/owners/esp32_native_client.py +335 -0
- ldcon/owners/esp32_native_owner.py +2166 -0
- ldcon/owners/esp32_time_telemetry.py +253 -0
- ldcon/owners/esp32_udp_gate.py +136 -0
- ldcon/owners/external_sweep_guard.py +44 -0
- ldcon/owners/flexible_native_trial.py +172 -0
- ldcon/owners/fringe_peaks.py +92 -0
- ldcon/owners/full_data_log.py +133 -0
- ldcon/owners/instrument_identity.py +125 -0
- ldcon/owners/instrument_owner.py +1041 -0
- ldcon/owners/instrument_projection.py +104 -0
- ldcon/owners/instrument_service.py +638 -0
- ldcon/owners/migration_physical_lock.py +162 -0
- ldcon/owners/monitor_owner.py +322 -0
- ldcon/owners/native_assembly.py +37 -0
- ldcon/owners/native_assembly_host.py +260 -0
- ldcon/owners/native_background.py +233 -0
- ldcon/owners/native_current_tuning.py +251 -0
- ldcon/owners/native_ff_proof.py +433 -0
- ldcon/owners/native_instrument_owner.py +161 -0
- ldcon/owners/native_modulation_limits.py +45 -0
- ldcon/owners/native_policy_reload.py +186 -0
- ldcon/owners/native_policy_safety.py +213 -0
- ldcon/owners/native_policy_update.py +473 -0
- ldcon/owners/native_system_owner.py +420 -0
- ldcon/owners/operating_mode_owner.py +128 -0
- ldcon/owners/owner_projection.py +530 -0
- ldcon/owners/owner_telemetry.py +166 -0
- ldcon/owners/pi_handover.py +154 -0
- ldcon/owners/projection_transport.py +143 -0
- ldcon/owners/remote_instruments.py +323 -0
- ldcon/owners/sdg_client.py +122 -0
- ldcon/owners/sdg_owner.py +468 -0
- ldcon/owners/tec_rate.py +29 -0
- ldcon/owners/wsu_udp_monitor.py +113 -0
- ldcon/report_pdf.py +4 -0
- ldcon/resources/agent-model-options.json +142 -0
- ldcon/resources/agent_contract/adjustment.txt +45 -0
- ldcon/resources/agent_contract/common.txt +42 -0
- ldcon/resources/agent_contract/diagnosis.txt +11 -0
- ldcon/resources/agent_contract/research.txt +20 -0
- ldcon/resources/agent_contract/response_optimization.txt +13 -0
- ldcon/resources/examples/README.html +10 -0
- ldcon/resources/examples/assignments/sr1092.yaml +100 -0
- ldcon/resources/examples/assignments/sr422.yaml +677 -0
- ldcon/resources/examples/assignments/thf685.yaml +7 -0
- ldcon/resources/examples/interface-decks/dfb-dbr-ldc-tec.yaml +430 -0
- ldcon/resources/examples/interface-decks/ecdl-dlcpro.yaml +288 -0
- ldcon/resources/examples/interface-decks/ecdl-pzt-ldc-tec.yaml +698 -0
- ldcon/resources/examples/lasers/sr1092.yaml +267 -0
- ldcon/resources/examples/lasers/sr422.yaml +403 -0
- ldcon/resources/examples/lasers/thf685.yaml +158 -0
- ldcon/resources/report_template/ldconreport.cls +42 -0
- ldcon/resources/report_template/report.tex +47 -0
- ldcon/web/__init__.py +1 -0
- ldcon/web/agent_approval_api.py +126 -0
- ldcon/web/agent_settings_api.py +54 -0
- ldcon/web/agent_task_api.py +211 -0
- ldcon/web/control_readback.py +63 -0
- ldcon/web/dashboard_activity.py +109 -0
- ldcon/web/dashboard_controls_revision.py +29 -0
- ldcon/web/dashboard_metrics.py +146 -0
- ldcon/web/dashboard_snapshot.py +163 -0
- ldcon/web/dashboard_state.py +104 -0
- ldcon/web/dashboard_wavelength.py +150 -0
- ldcon/web/device_parameters_api.py +52 -0
- ldcon/web/dither_quality.py +46 -0
- ldcon/web/dlcpro_target_api.py +91 -0
- ldcon/web/dlcpro_target_query.py +23 -0
- ldcon/web/failure_api.py +62 -0
- ldcon/web/gateway_lan.py +208 -0
- ldcon/web/grafana_activity.py +84 -0
- ldcon/web/grafana_dashboard.py +492 -0
- ldcon/web/grafana_dlcpro.py +158 -0
- ldcon/web/grafana_quality.py +167 -0
- ldcon/web/interface_projection.py +27 -0
- ldcon/web/job_registry.py +68 -0
- ldcon/web/job_report.py +122 -0
- ldcon/web/laser_operations.py +445 -0
- ldcon/web/lock_target_api.py +554 -0
- ldcon/web/lock_target_queue.py +212 -0
- ldcon/web/log_archive.py +191 -0
- ldcon/web/metric_query_profiles.py +378 -0
- ldcon/web/online_position.py +26 -0
- ldcon/web/operating_mode_api.py +72 -0
- ldcon/web/operation_api.py +34 -0
- ldcon/web/operation_observation.py +57 -0
- ldcon/web/operations_settings.py +92 -0
- ldcon/web/quality_threshold_metrics.py +64 -0
- ldcon/web/shot_analysis_api.py +136 -0
- ldcon/web/sweep_api.py +1186 -0
- ldcon/web/sweep_guard.py +45 -0
- ldcon/web/target_reference_panel.py +38 -0
- ldcon/web/teams_destinations_api.py +81 -0
- ldcon/web/web_fleet.py +482 -0
- ldcon/web/web_runtime.py +811 -0
- ldcon/web/web_server.py +402 -0
- ldcon/web/window_api.py +28 -0
- ldcon/web_assets/agent-settings.css +4 -0
- ldcon/web_assets/agent-settings.html +53 -0
- ldcon/web_assets/agent-settings.js +270 -0
- ldcon/web_assets/app.css +9 -0
- ldcon/web_assets/app.js +802 -0
- ldcon/web_assets/card-browser.js +160 -0
- ldcon/web_assets/card-guide.js +9 -0
- ldcon/web_assets/cards/acquire.metrics.html +82 -0
- ldcon/web_assets/cards/agent.history_review.html +13 -0
- ldcon/web_assets/cards/agent.log_watch.html +1 -0
- ldcon/web_assets/cards/agent.monitor.html +13 -0
- ldcon/web_assets/cards/agent.request.html +1 -0
- ldcon/web_assets/cards/agent.resident.html +1 -0
- ldcon/web_assets/cards/agent.task.html +17 -0
- ldcon/web_assets/cards/commission.ecdl_pi.html +1 -0
- ldcon/web_assets/cards/identify.axis_frequency.html +1 -0
- ldcon/web_assets/cards/identify.current_response.html +1 -0
- ldcon/web_assets/cards/laser.emission.html +8 -0
- ldcon/web_assets/cards/measure.batch.html +1 -0
- ldcon/web_assets/cards/measure.change.html +1 -0
- ldcon/web_assets/cards/measure.target_response.html +1 -0
- ldcon/web_assets/cards/monitor.wavelength.html +1 -0
- ldcon/web_assets/cards/notify.teams.html +1 -0
- ldcon/web_assets/cards/optimize.settling_criteria.html +1 -0
- ldcon/web_assets/cards/optimize.target_feedforward.html +1 -0
- ldcon/web_assets/cards/optimize.target_step.html +1 -0
- ldcon/web_assets/cards/optimize.target_thermal.html +1 -0
- ldcon/web_assets/cards/plan.ecdl_recovery.html +14 -0
- ldcon/web_assets/cards/prepare.ecdl_recovery.html +18 -0
- ldcon/web_assets/cards/recover.ecdl_mode_hop.html +12 -0
- ldcon/web_assets/cards/report.grafana_dashboard.html +22 -0
- ldcon/web_assets/cards/servo.current_pid.html +1 -0
- ldcon/web_assets/cards/servo.dlcpro_frequency.html +1 -0
- ldcon/web_assets/cards/servo.ecdl_steady_lock.html +11 -0
- ldcon/web_assets/cards/servo.native_current.html +1 -0
- ldcon/web_assets/cards/study.ecdl_current_response.html +1 -0
- ldcon/web_assets/cards/study.ecdl_locked_current_map.html +1 -0
- ldcon/web_assets/cards/study.ecdl_mode_map.html +6 -0
- ldcon/web_assets/cards/study.ecdl_thermal_context.html +5 -0
- ldcon/web_assets/cards/study.target_response.html +1 -0
- ldcon/web_assets/cards/sweep.current_modulation.html +1 -0
- ldcon/web_assets/cards/sweep.dlcpro_frequency.html +1 -0
- ldcon/web_assets/cards/sweep.servo_modulation.html +1 -0
- ldcon/web_assets/cards/test.ecdl_recovery.html +4 -0
- ldcon/web_assets/cards/tune.ecdl_initial_point.html +1 -0
- ldcon/web_assets/cards/tune.ecdl_operating_point.html +13 -0
- ldcon/web_assets/cards/tune.ecdl_window_center.html +7 -0
- ldcon/web_assets/cards/tune.native_current.html +1 -0
- ldcon/web_assets/cards/tune.pid_coordinate_search.html +1 -0
- ldcon/web_assets/cards/tune.pid_factor_reduction.html +1 -0
- ldcon/web_assets/cards/tune.pid_online.html +1 -0
- ldcon/web_assets/cards/tune.temperature_once.html +18 -0
- ldcon/web_assets/committing-input.js +35 -0
- ldcon/web_assets/configuration-editor.js +194 -0
- ldcon/web_assets/dashboard-state.js +39 -0
- ldcon/web_assets/failures.html +1 -0
- ldcon/web_assets/failures.js +19 -0
- ldcon/web_assets/fallback.css +3 -0
- ldcon/web_assets/fallback.html +1 -0
- ldcon/web_assets/fallback.js +21 -0
- ldcon/web_assets/fleet-settings.js +20 -0
- ldcon/web_assets/fleet.css +9 -0
- ldcon/web_assets/fleet.html +8 -0
- ldcon/web_assets/fleet.js +130 -0
- ldcon/web_assets/gateway-presentation.js +27 -0
- ldcon/web_assets/grafana-help.js +31 -0
- ldcon/web_assets/help.html +22 -0
- ldcon/web_assets/i18n.js +279 -0
- ldcon/web_assets/index.html +14 -0
- ldcon/web_assets/laser-workspace.js +279 -0
- ldcon/web_assets/modulation.html +1 -0
- ldcon/web_assets/modulation.js +14 -0
- ldcon/web_assets/reservations.html +16 -0
- ldcon/web_assets/reservations.js +77 -0
- ldcon/web_assets/teams-settings.js +91 -0
- ldcon/web_assets/window-actions.js +37 -0
- ldcon/web_assets/workspace.css +99 -0
- ldcon/web_fleet.py +4 -0
- ldcon/web_server.py +4 -0
- ldcon-0.2.0.dist-info/METADATA +85 -0
- ldcon-0.2.0.dist-info/RECORD +482 -0
- ldcon-0.2.0.dist-info/WHEEL +5 -0
- ldcon-0.2.0.dist-info/entry_points.txt +5 -0
- ldcon-0.2.0.dist-info/licenses/LICENSE +21 -0
- ldcon-0.2.0.dist-info/top_level.txt +1 -0
ldcon/drivers/cld1015.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""CLD1015 DOC-104674 Rev B, pp40-44: direct USBTMC/VISA, no DLL required."""
|
|
2
|
+
import math
|
|
3
|
+
import os
|
|
4
|
+
import hashlib
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from ldcon.drivers.cld1015_config import check
|
|
8
|
+
from ldcon.core.device_conditions import TemperatureConditions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InvalidMeasurement(RuntimeError):
|
|
12
|
+
"""Invalid numeric evidence; the caller must still track any prior write."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PhysicalLock:
|
|
16
|
+
"""Keep the legacy resource flock while an owner replaces a read session."""
|
|
17
|
+
def __init__(self, resource):
|
|
18
|
+
if not resource or resource == "auto":
|
|
19
|
+
raise ValueError("Physical ownership requires a pinned VISA resource")
|
|
20
|
+
from ldcon.owners.migration_physical_lock import acquire_visa
|
|
21
|
+
self.resource = resource
|
|
22
|
+
self.handle = acquire_visa(resource)
|
|
23
|
+
|
|
24
|
+
def check(self, resource):
|
|
25
|
+
if self.handle.closed or self.resource != resource:
|
|
26
|
+
raise ValueError("Physical lock does not own the requested VISA resource")
|
|
27
|
+
|
|
28
|
+
def close(self):
|
|
29
|
+
self.handle.close()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class VisaIO:
|
|
33
|
+
def __init__(self, resource, timeout_s, *, physical_lock=None):
|
|
34
|
+
import pyvisa
|
|
35
|
+
self.rm = pyvisa.ResourceManager()
|
|
36
|
+
self.inst = None
|
|
37
|
+
self.process_lock = None
|
|
38
|
+
try:
|
|
39
|
+
if resource in (None, "auto"):
|
|
40
|
+
candidates = []
|
|
41
|
+
for address in self.rm.list_resources("USB?*::INSTR"):
|
|
42
|
+
try:
|
|
43
|
+
if int(address.split("::")[1], 0) == 0x1313:
|
|
44
|
+
candidates.append(address)
|
|
45
|
+
except (ValueError, IndexError):
|
|
46
|
+
continue
|
|
47
|
+
if len(candidates) != 1:
|
|
48
|
+
raise ValueError(f"Expected one Thorlabs USB VISA device, found {len(candidates)}; check USB/driver or specify resource")
|
|
49
|
+
resource = candidates[0]
|
|
50
|
+
if physical_lock is not None:
|
|
51
|
+
if not isinstance(physical_lock, PhysicalLock):
|
|
52
|
+
raise ValueError("Expected a held CLD1015 PhysicalLock")
|
|
53
|
+
physical_lock.check(resource)
|
|
54
|
+
elif os.name == "posix":
|
|
55
|
+
self.process_lock = PhysicalLock(resource)
|
|
56
|
+
self.inst = self.rm.open_resource(resource)
|
|
57
|
+
self.inst.timeout = int(timeout_s * 1000)
|
|
58
|
+
self.inst.read_termination = self.inst.write_termination = "\n"
|
|
59
|
+
try:
|
|
60
|
+
self.inst.lock_excl(timeout=int(timeout_s * 1000))
|
|
61
|
+
except NotImplementedError:
|
|
62
|
+
if self.process_lock is None and physical_lock is None:
|
|
63
|
+
raise
|
|
64
|
+
except pyvisa.errors.VisaIOError as exc:
|
|
65
|
+
if (self.process_lock is None and physical_lock is None) or exc.error_code != pyvisa.constants.StatusCode.error_nonsupported_operation:
|
|
66
|
+
raise
|
|
67
|
+
except Exception:
|
|
68
|
+
self.close()
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
def query(self, command):
|
|
72
|
+
return self.inst.query(command).strip()
|
|
73
|
+
|
|
74
|
+
def write(self, command):
|
|
75
|
+
self.inst.write(command)
|
|
76
|
+
|
|
77
|
+
def close(self):
|
|
78
|
+
if self.inst is not None:
|
|
79
|
+
self.inst.close()
|
|
80
|
+
self.rm.close()
|
|
81
|
+
if self.process_lock is not None:
|
|
82
|
+
self.process_lock.close()
|
|
83
|
+
self.process_lock = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Controller:
|
|
87
|
+
def __init__(self, config, factory=None):
|
|
88
|
+
self.config, self.factory = config, factory
|
|
89
|
+
self.io = None
|
|
90
|
+
self.lock = threading.RLock()
|
|
91
|
+
|
|
92
|
+
def connect(self):
|
|
93
|
+
with self.lock:
|
|
94
|
+
if self.io is None:
|
|
95
|
+
self.io = (self.factory() if self.factory else MockIO() if self.config["mock"] else
|
|
96
|
+
VisaIO(self.config["resource"], self.config["timeout_s"]))
|
|
97
|
+
try:
|
|
98
|
+
if "CLD1015" not in self.io.query("*IDN?").upper():
|
|
99
|
+
raise RuntimeError("USB instrument is not a CLD1015")
|
|
100
|
+
except Exception:
|
|
101
|
+
self.close()
|
|
102
|
+
raise
|
|
103
|
+
return self.read()
|
|
104
|
+
|
|
105
|
+
def close(self):
|
|
106
|
+
with self.lock:
|
|
107
|
+
if self.io is not None:
|
|
108
|
+
self.io.close()
|
|
109
|
+
self.io = None
|
|
110
|
+
|
|
111
|
+
def query(self, command):
|
|
112
|
+
if self.io is None:
|
|
113
|
+
raise RuntimeError("Connect to CLD1015 first")
|
|
114
|
+
return self.io.query(command).strip()
|
|
115
|
+
|
|
116
|
+
def command(self, command):
|
|
117
|
+
self.io.write(command)
|
|
118
|
+
if self.query("*OPC?") != "1":
|
|
119
|
+
raise RuntimeError("CLD1015 command did not complete")
|
|
120
|
+
error = self.query("SYST:ERR?")
|
|
121
|
+
if int(error.split(",", 1)[0]) != 0:
|
|
122
|
+
raise RuntimeError(f"CLD1015: {error}")
|
|
123
|
+
|
|
124
|
+
def _unit(self):
|
|
125
|
+
unit = self.query("UNIT:TEMP?").upper()
|
|
126
|
+
if unit in ("C", "CEL", "CELSIUS"):
|
|
127
|
+
return 1., 0.
|
|
128
|
+
if unit in ("K", "KEL", "KELVIN"):
|
|
129
|
+
return 1., -273.15
|
|
130
|
+
if unit in ("F", "FAR", "FAHR", "FAHRENHEIT"):
|
|
131
|
+
return 5 / 9, -160 / 9
|
|
132
|
+
raise ValueError(f"Unknown CLD1015 temperature unit: {unit}")
|
|
133
|
+
|
|
134
|
+
def read(self):
|
|
135
|
+
with self.lock:
|
|
136
|
+
scale, offset = self._unit()
|
|
137
|
+
def number(cmd, field):
|
|
138
|
+
raw = self.query(cmd)
|
|
139
|
+
try:
|
|
140
|
+
value = float(raw)
|
|
141
|
+
except (ValueError, TypeError, OverflowError) as exc:
|
|
142
|
+
raise InvalidMeasurement(f"Invalid CLD1015 {field} ({cmd}): {raw!r}") from exc
|
|
143
|
+
if not math.isfinite(value):
|
|
144
|
+
raise InvalidMeasurement(f"Nonfinite CLD1015 {field} ({cmd}): {raw!r}")
|
|
145
|
+
return value
|
|
146
|
+
enabled = lambda cmd: self.query(cmd).upper() in ("1", "1.0", "ON")
|
|
147
|
+
data = dict(current_a=number("SOUR1:CURR?", "current_a"), current_read_a=number("SENS3:CURR:DATA?", "current_read_a"),
|
|
148
|
+
temperature_c=number("SOUR2:TEMP:SPO?", "temperature_c") * scale + offset,
|
|
149
|
+
temperature_read_c=number("SENS2:TEMP:DATA?", "temperature_read_c") * scale + offset,
|
|
150
|
+
tec_current_a=number("SOUR2:CURR:DATA?", "tec_current_a"),
|
|
151
|
+
current_limit_a=number("SOUR1:CURR:LIM?", "current_limit_a"),
|
|
152
|
+
ld_enabled=enabled("OUTP1?"),
|
|
153
|
+
tec_enabled=enabled("OUTP2?"),
|
|
154
|
+
modulation_enabled=enabled("SOUR1:AM:STAT?"),
|
|
155
|
+
ld_mode=self.query("SOUR1:FUNC:MODE?"), tec_mode=self.query("SOUR2:FUNC:MODE?"))
|
|
156
|
+
invalid = [key for key, value in data.items() if isinstance(value, float) and not math.isfinite(value)]
|
|
157
|
+
if invalid:
|
|
158
|
+
raise InvalidMeasurement("Nonfinite converted CLD1015 fields: " + ", ".join(invalid))
|
|
159
|
+
return data
|
|
160
|
+
|
|
161
|
+
def validate(self, request):
|
|
162
|
+
if not request or set(request) - {"current_a", "temperature_c"}:
|
|
163
|
+
raise ValueError("Setpoints must contain current_a and/or temperature_c only")
|
|
164
|
+
state = self.read()
|
|
165
|
+
if "current_a" in request:
|
|
166
|
+
value = check(request["current_a"], self.config["current_limits_a"], "current")
|
|
167
|
+
if state["ld_mode"].upper() not in ("CURR", "CURRENT"):
|
|
168
|
+
raise ValueError("LD must already be in constant-current mode")
|
|
169
|
+
if not 0 <= value <= state["current_limit_a"]:
|
|
170
|
+
raise ValueError("Requested current exceeds controller current limit")
|
|
171
|
+
if "temperature_c" in request:
|
|
172
|
+
value = check(request["temperature_c"], self.config["temperature_limits_c"], "temperature")
|
|
173
|
+
if state["tec_mode"].upper() not in ("TEMP", "TEMPERATURE"):
|
|
174
|
+
raise ValueError("TEC must already be in temperature mode")
|
|
175
|
+
scale, offset = self._unit()
|
|
176
|
+
lo = float(self.query("SOUR2:TEMP:LIM:LOW?")) * scale + offset
|
|
177
|
+
hi = float(self.query("SOUR2:TEMP:LIM:UPP?")) * scale + offset
|
|
178
|
+
if not lo <= value <= hi:
|
|
179
|
+
raise ValueError("Temperature exceeds controller temperature limits")
|
|
180
|
+
return state
|
|
181
|
+
|
|
182
|
+
def apply(self, request, *, ramp_s=0):
|
|
183
|
+
with self.lock:
|
|
184
|
+
if not self.config.get('temperature_writes_enabled',True):raise ValueError('Laser output writes are not enabled')
|
|
185
|
+
if self.config.get('current_conditions') is not None and 'current_a' in request:
|
|
186
|
+
from ldcon.owners.current_owner import apply_current
|
|
187
|
+
return apply_current(self,request,ramp_s)
|
|
188
|
+
before = self.validate(request) # Validate the whole request before writing.
|
|
189
|
+
ramp_s = float(ramp_s)
|
|
190
|
+
if not math.isfinite(ramp_s) or not 0 <= ramp_s <= 3600:
|
|
191
|
+
raise ValueError("ramp_s must be finite in 0..3600 s")
|
|
192
|
+
policy = self.config.get('temperature_conditions')
|
|
193
|
+
guard = TemperatureConditions(policy,self.config.get('control_settings')) if policy is not None and 'temperature_c' in request else None
|
|
194
|
+
delta_temperature = abs(float(request.get('temperature_c', before['temperature_c']))-before['temperature_c'])
|
|
195
|
+
if guard and not guard.low<=float(request['temperature_c'])<=guard.high:
|
|
196
|
+
raise ValueError('Final temperature exceeds absolute device envelope')
|
|
197
|
+
if guard and delta_temperature and ramp_s<=0:
|
|
198
|
+
raise ValueError('Temperature device policy requires a timed ramp')
|
|
199
|
+
steps = max(1, math.ceil(ramp_s / .1))
|
|
200
|
+
if guard:
|
|
201
|
+
steps=max(1,math.ceil(ramp_s/.5),math.ceil(max(0.,delta_temperature-1e-12)/guard.step))
|
|
202
|
+
interval=self.config.get('control_settings',{}).get('temperature',{}).get('output_update_interval_s')
|
|
203
|
+
if interval is not None:
|
|
204
|
+
interval=float(interval)
|
|
205
|
+
if not math.isfinite(interval) or not .1<=interval<=2:raise ValueError('Invalid device temperature update interval')
|
|
206
|
+
steps=max(1,math.ceil(max(0.,delta_temperature-1e-12)/guard.step))
|
|
207
|
+
if ramp_s+1e-9<steps*interval:raise ValueError('Temperature ramp is faster than device update interval')
|
|
208
|
+
scale, offset = self._unit()
|
|
209
|
+
previous_temperature=before['temperature_c'];previous_time=time.monotonic()
|
|
210
|
+
for step in range(1, steps + 1):
|
|
211
|
+
if ramp_s:time.sleep(ramp_s/steps)
|
|
212
|
+
candidate=before['temperature_c']+(float(request.get('temperature_c',before['temperature_c']))-before['temperature_c'])*step/steps
|
|
213
|
+
if guard:
|
|
214
|
+
observed=self.read()
|
|
215
|
+
state={'setpoint_c':observed['temperature_c'],'temperature_c':observed['temperature_read_c'],
|
|
216
|
+
'tec_current_a':observed['tec_current_a'],'current_a':observed['current_read_a'],'ld_enabled':observed['ld_enabled'],'tec_enabled':observed['tec_enabled']}
|
|
217
|
+
if abs(observed['temperature_c']-previous_temperature)>.002:
|
|
218
|
+
raise ValueError('External temperature setpoint change detected')
|
|
219
|
+
# No client-supplied elapsed time; use the owner process clock.
|
|
220
|
+
guard.check(state,candidate,time.monotonic()-previous_time,previous=previous_temperature)
|
|
221
|
+
for key in ('current_a','temperature_c'):
|
|
222
|
+
if key in request:
|
|
223
|
+
value = before[key] + (float(request[key]) - before[key]) * step / steps
|
|
224
|
+
native = value if key == 'current_a' else (value - offset) / scale
|
|
225
|
+
command = 'SOUR1:CURR' if key == 'current_a' else 'SOUR2:TEMP:SPO'
|
|
226
|
+
self.command(f'{command} {native:.12g}')
|
|
227
|
+
if key=='temperature_c':previous_temperature=value;previous_time=time.monotonic()
|
|
228
|
+
result = self.read()
|
|
229
|
+
for key in request:
|
|
230
|
+
tolerance = 6e-5 if key == "current_a" else .002
|
|
231
|
+
if abs(result[key] - float(request[key])) > tolerance:
|
|
232
|
+
raise RuntimeError(f"CLD1015 setpoint verification failed: {key}")
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
def enable(self, output, enabled):
|
|
236
|
+
with self.lock:
|
|
237
|
+
if output not in ("ld", "tec", "modulation") or not isinstance(enabled, bool):
|
|
238
|
+
raise ValueError("Select ld/tec/modulation and a boolean state")
|
|
239
|
+
state = self.read()
|
|
240
|
+
if enabled:
|
|
241
|
+
self.validate(dict(current_a=state["current_a"], temperature_c=state["temperature_c"]))
|
|
242
|
+
if output == "tec" and not enabled and state["ld_enabled"]:
|
|
243
|
+
raise ValueError("Turn LD off explicitly before turning TEC off")
|
|
244
|
+
command = {"ld": "OUTP1", "tec": "OUTP2", "modulation": "SOUR1:AM:STAT"}[output]
|
|
245
|
+
self.command(f"{command} {int(enabled)}")
|
|
246
|
+
result=self.read()
|
|
247
|
+
key={'ld':'ld_enabled','tec':'tec_enabled','modulation':'modulation_enabled'}[output]
|
|
248
|
+
if result[key] is not enabled:
|
|
249
|
+
raise RuntimeError('CLD1015 output enable verification failed: '+output)
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
def validate_sweep(self, channels, waves, resolved):
|
|
253
|
+
state = self.read()
|
|
254
|
+
if not state["ld_enabled"] or not state["modulation_enabled"] or state["ld_mode"].upper() not in ("CURR", "CURRENT"):
|
|
255
|
+
raise ValueError("CLD1015 sweep requires LD on, constant-current mode and modulation enabled")
|
|
256
|
+
role = self.config["modulation_role"]
|
|
257
|
+
for c, actual in zip(channels, resolved):
|
|
258
|
+
if c["role"] != role:
|
|
259
|
+
continue
|
|
260
|
+
lo, hi = actual["offset_v"] - actual["amplitude_vpp"] / 2, actual["offset_v"] + actual["amplitude_vpp"] / 2
|
|
261
|
+
# DOC-104674 p56: 150 mA/V +/-5%. Check the total LD current,
|
|
262
|
+
# including the actual AWG baseline and controller DC setpoint.
|
|
263
|
+
extremes = [state["current_a"] + v * gain for v in (lo, hi) for gain in (.1425, .1575)]
|
|
264
|
+
for value in extremes:
|
|
265
|
+
check(value, self.config["current_limits_a"], "current")
|
|
266
|
+
if not 0 <= value <= state["current_limit_a"]:
|
|
267
|
+
raise ValueError("AWG envelope exceeds CLD1015 current limit")
|
|
268
|
+
return
|
|
269
|
+
raise ValueError("Embedded CLD1015 modulation_role does not match a sweep channel")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class MockIO:
|
|
273
|
+
def __init__(self, unit="C"):
|
|
274
|
+
self.unit, self.log, self.closed = unit, [], False
|
|
275
|
+
self.state = {"SOUR1:CURR": .05, "SOUR2:TEMP:SPO": 25., "OUTP1": 1, "OUTP2": 1,
|
|
276
|
+
"SOUR1:AM:STAT": 1}
|
|
277
|
+
|
|
278
|
+
def query(self, command):
|
|
279
|
+
self.log.append(command)
|
|
280
|
+
if command == "*IDN?": return "THORLABS,CLD1015,DUMMY,1"
|
|
281
|
+
if command == "*OPC?": return "1"
|
|
282
|
+
if command == "SYST:ERR?": return '0,"No error"'
|
|
283
|
+
if command == "UNIT:TEMP?": return self.unit
|
|
284
|
+
fixed = {"SOUR1:CURR:LIM?": .1, "SOUR2:TEMP:LIM:LOW?": 10.,
|
|
285
|
+
"SOUR2:TEMP:LIM:UPP?": 40., "SOUR1:FUNC:MODE?": "CURR", "SOUR2:FUNC:MODE?": "TEMP",
|
|
286
|
+
"SENS3:CURR:DATA?": self.state["SOUR1:CURR"],
|
|
287
|
+
"SENS2:TEMP:DATA?": self.state["SOUR2:TEMP:SPO"], "SOUR2:CURR:DATA?": .01}
|
|
288
|
+
return str(fixed[command] if command in fixed else self.state[command[:-1]])
|
|
289
|
+
|
|
290
|
+
def write(self, command):
|
|
291
|
+
self.log.append(command)
|
|
292
|
+
key, value = command.split()
|
|
293
|
+
self.state[key] = float(value)
|
|
294
|
+
|
|
295
|
+
def close(self):
|
|
296
|
+
self.closed = True
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Compile-time configuration; SI current and degrees Celsius at the public API."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def limits(value, name):
|
|
6
|
+
if value is None:
|
|
7
|
+
return None
|
|
8
|
+
result = tuple(float(v) for v in value)
|
|
9
|
+
if len(result) != 2 or not all(math.isfinite(v) for v in result) or result[0] >= result[1]:
|
|
10
|
+
raise ValueError(f"{name} must be an increasing finite pair")
|
|
11
|
+
return result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configuration(resource=None, *, current_limits_a=None, temperature_limits_c=None,
|
|
15
|
+
timeout_s=5, mock=False, modulation_role=None,
|
|
16
|
+
endpoint=None, instrument=None, temperature_conditions=None, temperature_writes_enabled=True, current_conditions=None, control_settings=None, current_writes_enabled=False, ld_enable_writes_enabled=False):
|
|
17
|
+
if type(ld_enable_writes_enabled) is not bool:
|
|
18
|
+
raise ValueError('ld_enable_writes_enabled must be a boolean')
|
|
19
|
+
return dict(resource=resource, current_limits_a=limits(current_limits_a, "current_limits_a"),
|
|
20
|
+
temperature_limits_c=limits(temperature_limits_c, "temperature_limits_c"),
|
|
21
|
+
timeout_s=float(timeout_s), mock=bool(mock), modulation_role=modulation_role,
|
|
22
|
+
endpoint=endpoint, instrument=instrument, temperature_conditions=temperature_conditions, temperature_writes_enabled=temperature_writes_enabled, current_conditions=current_conditions, control_settings=control_settings or {}, current_writes_enabled=current_writes_enabled, ld_enable_writes_enabled=ld_enable_writes_enabled)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check(value, bounds, name):
|
|
26
|
+
value = float(value)
|
|
27
|
+
if bounds is None:
|
|
28
|
+
raise ValueError(f"configure {name} safety limits before changing the setpoint")
|
|
29
|
+
if not math.isfinite(value) or not bounds[0] <= value <= bounds[1]:
|
|
30
|
+
raise ValueError(f"{name} {value} outside {bounds}")
|
|
31
|
+
return value
|
ldcon/drivers/dlcpro.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""DLC Pro 1.9/3.2 USER console adapter; no connection is opened on import.
|
|
2
|
+
|
|
3
|
+
Command reference pp. 38-43, 51-61, 118-119, 262. Exactly one console
|
|
4
|
+
connection belongs to the instrument owner. Never retry a write after timeout.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from ldcon.core.contracts import DeviceError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from ldcon.drivers.dlcpro_console import Console, UncertainDLCWrite
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DLCParameterError(DeviceError):
|
|
17
|
+
def __init__(self, path, code):
|
|
18
|
+
super().__init__(f'DLC parameter {path} returned error {code}')
|
|
19
|
+
self.path, self.code = path, code
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _name(path):
|
|
23
|
+
if not isinstance(path, str) or not re.fullmatch(r'[a-z][a-z0-9:-]*', path):
|
|
24
|
+
raise ValueError('Invalid DLC parameter name')
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _atom(value):
|
|
29
|
+
if type(value) is bool:
|
|
30
|
+
return '#t' if value else '#f'
|
|
31
|
+
if type(value) not in (int, float) or not math.isfinite(value):
|
|
32
|
+
raise ValueError('DLC writes require finite numbers or booleans')
|
|
33
|
+
return repr(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_atom(text):
|
|
37
|
+
text = text.strip()
|
|
38
|
+
if text in ('#t', '#f'):
|
|
39
|
+
return text == '#t'
|
|
40
|
+
if text.startswith('"') and text.endswith('"'):
|
|
41
|
+
return json.loads(text)
|
|
42
|
+
try:
|
|
43
|
+
value = float(text)
|
|
44
|
+
except ValueError:
|
|
45
|
+
raise DeviceError('Unexpected DLC scalar reply') from None
|
|
46
|
+
if not math.isfinite(value):
|
|
47
|
+
raise DeviceError('Nonfinite DLC scalar reply')
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DLCPro:
|
|
52
|
+
"""Unit-preserving parameter access. There is deliberately no eval/exec RPC."""
|
|
53
|
+
AXES = {'piezo': ('pc:voltage-set', 'pc:voltage-act', 'V'),
|
|
54
|
+
'temperature': ('tc:temp-set', 'tc:temp-act', 'degC'),
|
|
55
|
+
'current': ('cc:current-offset', 'cc:current-act', 'mA')}
|
|
56
|
+
|
|
57
|
+
def __init__(self, console):
|
|
58
|
+
self.console = console
|
|
59
|
+
self.unavailable_parameters = {}
|
|
60
|
+
self.compact_write_supported = False
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def path(channel, suffix):
|
|
64
|
+
if type(channel) is not int or channel not in (1, 2, 3, 4):
|
|
65
|
+
raise ValueError('Invalid DLC laser channel')
|
|
66
|
+
return _name(f'laser{channel}:dl:{suffix}')
|
|
67
|
+
|
|
68
|
+
def read(self, path):
|
|
69
|
+
path = _name(path)
|
|
70
|
+
reply = self.console.command(f"(param-ref '{path})")
|
|
71
|
+
error = re.fullmatch(r'Error: (-?\d+)(?: .*)?', reply)
|
|
72
|
+
if error:
|
|
73
|
+
raise DLCParameterError(path, int(error[1]))
|
|
74
|
+
return parse_atom(reply)
|
|
75
|
+
|
|
76
|
+
def read_optional(self, path):
|
|
77
|
+
"""Only an explicit unknown/unavailable parameter means unsupported.
|
|
78
|
+
|
|
79
|
+
Transport failure, malformed data, and access denial still propagate.
|
|
80
|
+
The negative capability cache lives only for this connection.
|
|
81
|
+
"""
|
|
82
|
+
if path in self.unavailable_parameters:
|
|
83
|
+
return None
|
|
84
|
+
try:
|
|
85
|
+
return self.read(path)
|
|
86
|
+
except DLCParameterError as error:
|
|
87
|
+
if error.code not in (-3, -20):
|
|
88
|
+
raise
|
|
89
|
+
self.unavailable_parameters[path] = error.code
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def identity(self, channel):
|
|
93
|
+
self.path(channel, 'serial-number') # Validate before building names.
|
|
94
|
+
return dict(system_type=self.read('system-type'),
|
|
95
|
+
serial_number=self.read('serial-number'),
|
|
96
|
+
firmware_version=self.read('fw-ver'),
|
|
97
|
+
product_name=self.read(f'laser{channel}:product-name'),
|
|
98
|
+
head_serial_number=self.read(self.path(channel, 'serial-number')))
|
|
99
|
+
|
|
100
|
+
def verify_identity(self, serial_number, firmware_version=None):
|
|
101
|
+
identity = dict(serial_number=self.read('serial-number'),
|
|
102
|
+
firmware_version=self.read('fw-ver'))
|
|
103
|
+
if identity['serial_number'] != serial_number:
|
|
104
|
+
raise DeviceError('DLC chassis identity mismatch')
|
|
105
|
+
if firmware_version is not None and identity['firmware_version'] != firmware_version:
|
|
106
|
+
raise DeviceError('DLC firmware differs from commissioned version')
|
|
107
|
+
return identity
|
|
108
|
+
|
|
109
|
+
def control_transaction(self):
|
|
110
|
+
return self.console.lock
|
|
111
|
+
|
|
112
|
+
def verify_compact_write_support(self):
|
|
113
|
+
# Constants only: this startup capability check reads/writes no outputs.
|
|
114
|
+
reply = self.console.command('(let ((status 0)) (list status 12.5))')
|
|
115
|
+
if self._pair(reply) != (0., 12.5):
|
|
116
|
+
raise DeviceError('DLC ordered result/readback expression unsupported')
|
|
117
|
+
self.compact_write_supported = True
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _pair(reply):
|
|
121
|
+
match = re.fullmatch(r'\(\s*([^\s()]+)\s+([^\s()]+)\s*\)', reply.strip())
|
|
122
|
+
if not match:
|
|
123
|
+
raise DeviceError('DLC verification pair malformed')
|
|
124
|
+
return parse_atom(match[1]), parse_atom(match[2])
|
|
125
|
+
|
|
126
|
+
def write(self, path, value, *, tolerance=0., compact=False):
|
|
127
|
+
# The acknowledgement and readback form one indivisible transaction.
|
|
128
|
+
# Low-priority monitoring may not interleave between them.
|
|
129
|
+
with self.console.lock:
|
|
130
|
+
return self._verified_write(path, value, tolerance=tolerance, compact=compact)
|
|
131
|
+
|
|
132
|
+
def _verified_write(self, path, value, *, tolerance, compact):
|
|
133
|
+
path = _name(path)
|
|
134
|
+
# Any response other than exact success needs review. Code 2 is CLIPPED,
|
|
135
|
+
# not success; do not accept even if its subsequent readback happens to match.
|
|
136
|
+
if compact:
|
|
137
|
+
if not self.compact_write_supported:
|
|
138
|
+
raise DeviceError('Compact write capability was not verified')
|
|
139
|
+
# let forces param-set! to finish before param-ref. Merely putting
|
|
140
|
+
# both calls in list arguments would leave Scheme evaluation order unspecified.
|
|
141
|
+
expression = f"(let ((status (param-set! '{path} {_atom(value)}))) (list status (param-ref '{path})))"
|
|
142
|
+
else:
|
|
143
|
+
expression = f"(param-set! '{path} {_atom(value)})"
|
|
144
|
+
reply = self.console.command(expression, writing=True)
|
|
145
|
+
try:
|
|
146
|
+
status, actual = self._pair(reply) if compact else (parse_atom(reply), None)
|
|
147
|
+
if type(status) is bool or status != 0:
|
|
148
|
+
raise ValueError('DLC rejected, clipped or warned on write')
|
|
149
|
+
if not compact:
|
|
150
|
+
actual = self.read(path)
|
|
151
|
+
if type(value) is bool:
|
|
152
|
+
equal = actual is value
|
|
153
|
+
else:
|
|
154
|
+
equal = type(actual) is not bool and abs(actual - value) <= tolerance
|
|
155
|
+
if not equal:
|
|
156
|
+
raise ValueError('DLC setpoint readback mismatch')
|
|
157
|
+
except Exception as error:
|
|
158
|
+
raise UncertainDLCWrite('DLC write not verified; inspection required') from error
|
|
159
|
+
return actual
|
|
160
|
+
|
|
161
|
+
def snapshot(self, channel):
|
|
162
|
+
values = {}
|
|
163
|
+
for axis, (setting, measured, unit) in self.AXES.items():
|
|
164
|
+
values[axis] = dict(setpoint=self.read(self.path(channel, setting)),
|
|
165
|
+
measured=self.read(self.path(channel, measured)), unit=unit)
|
|
166
|
+
values['current']['total_setpoint'] = self.read(self.path(channel, 'cc:current-set'))
|
|
167
|
+
values['piezo']['calculated_output'] = self.read_optional(self.path(channel, 'pc:voltage-out'))
|
|
168
|
+
values['piezo']['calculated_output_available'] = values['piezo']['calculated_output'] is not None
|
|
169
|
+
values['feedforward_master'] = self.read_optional(f'laser{channel}:feedforward-master')
|
|
170
|
+
values['unavailable_parameters'] = dict(self.unavailable_parameters)
|
|
171
|
+
values['emission'] = self.read(self.path(channel, 'cc:emission'))
|
|
172
|
+
values['pzt_enabled'] = self.read(self.path(channel, 'pc:enabled'))
|
|
173
|
+
values['tec_enabled'] = self.read(self.path(channel, 'tc:enabled'))
|
|
174
|
+
values['tec_fault'] = self.read(self.path(channel, 'tc:fault'))
|
|
175
|
+
values['ff_enabled'] = self.read(self.path(channel, 'cc:feedforward-enabled'))
|
|
176
|
+
values['ff_factor_ma_per_v'] = self.read(self.path(channel, 'cc:feedforward-factor'))
|
|
177
|
+
return values
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Documented Fine inputs and ARC readback, using the owner's console.
|
|
2
|
+
|
|
3
|
+
Fine readbacks have no device acquisition timestamp. The host request interval
|
|
4
|
+
is retained and must not be presented as a synchronous ADC/WSU acquisition.
|
|
5
|
+
"""
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
INPUTS = {'fine1': ('io:fine-1:value-act', 0),
|
|
10
|
+
'fine2': ('io:fine-2:value-act', 1)}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def inspect(driver, channel, *, clock=time.monotonic, utc=time.time):
|
|
14
|
+
result = {'inputs': {}, 'routes': {}}
|
|
15
|
+
for name, (path, _) in INPUTS.items():
|
|
16
|
+
start, wall = clock(), utc()
|
|
17
|
+
value = driver.read(path)
|
|
18
|
+
result['inputs'][name] = dict(value_v=value, started_s=start,
|
|
19
|
+
completed_s=clock(), started_unix_s=wall, completed_unix_s=utc(),
|
|
20
|
+
acquisition_timestamp_available=False)
|
|
21
|
+
for axis in ('pc', 'cc', 'tc'):
|
|
22
|
+
result['routes'][axis] = {key: driver.read(driver.path(channel,
|
|
23
|
+
f'{axis}:external-input:{key}')) for key in ('enabled', 'signal', 'factor')}
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def read_input(driver, name, *, clock=time.monotonic, utc=time.time):
|
|
28
|
+
path, _ = INPUTS[name]
|
|
29
|
+
start, wall = clock(), utc()
|
|
30
|
+
value = driver.read(path)
|
|
31
|
+
return dict(value_v=value, started_s=start, completed_s=clock(),
|
|
32
|
+
started_unix_s=wall, completed_unix_s=utc(),
|
|
33
|
+
acquisition_timestamp_available=False)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Bounded DLC Telnet framing, without telnetlib or terminal emulation."""
|
|
2
|
+
import socket
|
|
3
|
+
import time
|
|
4
|
+
from ldcon.drivers.dlcpro_priority import PriorityLock
|
|
5
|
+
|
|
6
|
+
from ldcon.core.contracts import DeviceError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UncertainDLCWrite(DeviceError):
|
|
10
|
+
"""A write may have reached the controller; never replay automatically."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Console:
|
|
14
|
+
def __init__(self, stream, *, max_reply_bytes=262144, timeout_s=2.):
|
|
15
|
+
self.stream = stream
|
|
16
|
+
self.max_reply_bytes = max_reply_bytes
|
|
17
|
+
self.timeout_s = timeout_s
|
|
18
|
+
self.lock = PriorityLock()
|
|
19
|
+
self.broken = False
|
|
20
|
+
self._telnet_state = 'data'
|
|
21
|
+
self._verb = None
|
|
22
|
+
self.greeting = self._reply()
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def connect(cls, host, *, port=1998, timeout_s=2.):
|
|
26
|
+
stream = socket.create_connection((host, port), timeout=timeout_s)
|
|
27
|
+
stream.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
28
|
+
try:
|
|
29
|
+
console = cls(stream, timeout_s=timeout_s)
|
|
30
|
+
# FW 1.9 echoes each character despite rejecting Telnet ECHO.
|
|
31
|
+
# This console-local setting avoids per-character reply overhead;
|
|
32
|
+
# it changes no laser parameter and leaves other consoles alone.
|
|
33
|
+
if console.command("(param-set! 'echo #f)") != '0':
|
|
34
|
+
raise DeviceError('DLC console echo disable rejected')
|
|
35
|
+
if console.command("(param-ref 'echo)") != '#f':
|
|
36
|
+
raise DeviceError('DLC console echo disable not verified')
|
|
37
|
+
return console
|
|
38
|
+
except BaseException:
|
|
39
|
+
stream.close()
|
|
40
|
+
raise
|
|
41
|
+
|
|
42
|
+
def _decode_telnet(self, raw):
|
|
43
|
+
text = bytearray()
|
|
44
|
+
for byte in raw:
|
|
45
|
+
state = self._telnet_state
|
|
46
|
+
if state == 'data':
|
|
47
|
+
if byte == 255:
|
|
48
|
+
self._telnet_state = 'iac'
|
|
49
|
+
else:
|
|
50
|
+
text.append(byte)
|
|
51
|
+
elif state == 'iac':
|
|
52
|
+
if byte == 255:
|
|
53
|
+
text.append(byte)
|
|
54
|
+
self._telnet_state = 'data'
|
|
55
|
+
elif byte in (251, 252, 253, 254):
|
|
56
|
+
self._verb = byte
|
|
57
|
+
self._telnet_state = 'option'
|
|
58
|
+
elif byte == 250:
|
|
59
|
+
self._telnet_state = 'subnegotiation'
|
|
60
|
+
else:
|
|
61
|
+
self._telnet_state = 'data'
|
|
62
|
+
elif state == 'option':
|
|
63
|
+
# We need no terminal options. Reject WILL with DONT and DO with
|
|
64
|
+
# WONT; acknowledgments are ignored to avoid negotiation loops.
|
|
65
|
+
if self._verb in (251, 253):
|
|
66
|
+
self.stream.sendall(bytes((255, 254 if self._verb == 251 else 252, byte)))
|
|
67
|
+
self._telnet_state = 'data'
|
|
68
|
+
elif state == 'subnegotiation':
|
|
69
|
+
if byte == 255:
|
|
70
|
+
self._telnet_state = 'subnegotiation_iac'
|
|
71
|
+
elif state == 'subnegotiation_iac':
|
|
72
|
+
self._telnet_state = 'data' if byte == 240 else 'subnegotiation'
|
|
73
|
+
return text
|
|
74
|
+
|
|
75
|
+
def _reply(self):
|
|
76
|
+
result = bytearray()
|
|
77
|
+
received = 0
|
|
78
|
+
deadline = time.monotonic() + self.timeout_s
|
|
79
|
+
while not ((result == b'> ' or result.endswith(b'\n> '))
|
|
80
|
+
and self._telnet_state == 'data'):
|
|
81
|
+
remaining = deadline - time.monotonic()
|
|
82
|
+
if remaining <= 0:
|
|
83
|
+
raise TimeoutError('DLC reply deadline exceeded')
|
|
84
|
+
if hasattr(self.stream, 'settimeout'):
|
|
85
|
+
self.stream.settimeout(remaining)
|
|
86
|
+
block = self.stream.recv(4096)
|
|
87
|
+
if not block:
|
|
88
|
+
raise ConnectionError('DLC console disconnected before prompt')
|
|
89
|
+
self._quick_ack()
|
|
90
|
+
received += len(block)
|
|
91
|
+
if received > self.max_reply_bytes:
|
|
92
|
+
raise DeviceError('DLC reply exceeds framing bound')
|
|
93
|
+
result.extend(self._decode_telnet(block))
|
|
94
|
+
return result[:-2].replace(b'\r\x00', b'\r').decode('utf-8').strip()
|
|
95
|
+
|
|
96
|
+
def _quick_ack(self):
|
|
97
|
+
# Linux QUICKACK is transient: request it for each received fragment.
|
|
98
|
+
# This can unblock a peer's small remaining reply/prompt under Nagle;
|
|
99
|
+
# our TCP_NODELAY only controls requests sent from this socket. It is
|
|
100
|
+
# an optional transport optimization, never a change to reply bounds.
|
|
101
|
+
option = getattr(socket, 'TCP_QUICKACK', None)
|
|
102
|
+
configure = getattr(self.stream, 'setsockopt', None)
|
|
103
|
+
if option is not None and configure is not None:
|
|
104
|
+
try:
|
|
105
|
+
configure(socket.IPPROTO_TCP, option, 1)
|
|
106
|
+
except OSError:
|
|
107
|
+
pass # Unsupported socket/platform keeps the original framing.
|
|
108
|
+
|
|
109
|
+
def command(self, expression, *, writing=False):
|
|
110
|
+
if not isinstance(expression, str) or any(c in expression for c in '\r\n\x00'):
|
|
111
|
+
raise ValueError('DLC command must be one line')
|
|
112
|
+
with self.lock:
|
|
113
|
+
if self.broken:
|
|
114
|
+
raise DeviceError('DLC console requires inspection/reconnection')
|
|
115
|
+
try:
|
|
116
|
+
if hasattr(self.stream, 'settimeout'):
|
|
117
|
+
self.stream.settimeout(self.timeout_s)
|
|
118
|
+
self.stream.sendall((expression + '\n').encode('ascii'))
|
|
119
|
+
reply = self._reply()
|
|
120
|
+
lines = reply.splitlines()
|
|
121
|
+
# Firmware 1.9 may echo even after ECHO negotiation is rejected.
|
|
122
|
+
# Remove only the exact request on its own first line.
|
|
123
|
+
if lines and lines[0] == expression:
|
|
124
|
+
reply = '\n'.join(lines[1:]).strip()
|
|
125
|
+
return reply
|
|
126
|
+
except Exception as error:
|
|
127
|
+
self.broken = True
|
|
128
|
+
if writing:
|
|
129
|
+
raise UncertainDLCWrite('DLC write outcome unknown; no replay') from error
|
|
130
|
+
raise DeviceError('DLC read failed') from error
|
|
131
|
+
|
|
132
|
+
def close(self):
|
|
133
|
+
self.stream.close()
|