From 9907c230282ded5b55eb9720b8507fcb0d66d093 Mon Sep 17 00:00:00 2001 From: Antoine Drouin Date: Tue, 25 Jan 2005 10:57:55 +0000 Subject: [PATCH] This commit was generated by cvs2svn to compensate for changes in r2, which included commits to RCS files with non-trunk default branches. --- AUTHORS | 2 + BUGS | 15 + COPYING | 339 ++ Makefile | 119 + Makefile.ac | 53 + Makefile.gen | 47 + Makefile.pl | 72 + README | 208 ++ TODO | 96 + conf/Makefile.avr | 140 + conf/Makefile.local | 19 + conf/airframes/microjet1.xml | 64 + conf/airframes/microjet2.xml | 63 + conf/airframes/twinstar1.xml | 61 + conf/airframes/twinstar2.xml | 61 + conf/conf.xml | 42 + conf/control_panel.xml | 105 + conf/control_panel.xml.sys | 84 + conf/flight_plans/circles.xml | 35 + conf/flight_plans/flight_plan.dtd | 116 + conf/flight_plans/hippo.xml | 27 + conf/flight_plans/huit.xml | 44 + conf/flight_plans/muret1.xml | 91 + conf/flight_plans/muret2.xml | 92 + conf/flight_plans/muret3.xml | 92 + conf/flight_plans/procedure.dtd | 82 + conf/hosts_wavecard.xml | 9 + conf/install.xml | 226 ++ conf/messages.xml | 314 ++ conf/radios/cockpitMM.xml | 18 + conf/radios/fc28.xml | 22 + conf/radios/mc3030.xml | 12 + conf/ubx.dtd | 26 + conf/ubx.xml | 68 + conf/wavecard.xml | 54 + data/maps/muret_UTM.gif | Bin 0 -> 77403 bytes data/maps/muret_UTM.xml | 5 + data/pictures/cockpitMM.gif | Bin 0 -> 93045 bytes data/pictures/fc28.gif | Bin 0 -> 160341 bytes data/pictures/penguin_logo.gif | Bin 0 -> 37842 bytes data/pictures/t7cap.jpg | Bin 0 -> 208118 bytes doc/checklist/checklist.tex | 102 + doc/user_manual/Makefile | 47 + doc/user_manual/fly_by_wire.dia | Bin 0 -> 2153 bytes doc/user_manual/overall.dia | Bin 0 -> 4232 bytes doc/user_manual/overall.png | Bin 0 -> 38701 bytes doc/user_manual/paparazzi.tex | 472 +++ sw/README | 25 + sw/airborne/autopilot/Makefile | 92 + sw/airborne/autopilot/README | 24 + sw/airborne/autopilot/adc.c | 126 + sw/airborne/autopilot/adc.h | 53 + sw/airborne/autopilot/autopilot.h | 110 + sw/airborne/autopilot/downlink.h | 35 + sw/airborne/autopilot/estimator.c | 170 + sw/airborne/autopilot/estimator.h | 67 + sw/airborne/autopilot/gps.h | 58 + sw/airborne/autopilot/gps_sirf.c | 235 ++ sw/airborne/autopilot/gps_ubx.c | 207 ++ sw/airborne/autopilot/if_calib.c | 94 + sw/airborne/autopilot/if_calib.h | 18 + sw/airborne/autopilot/infrared.c | 71 + sw/airborne/autopilot/infrared.h | 44 + sw/airborne/autopilot/link_fbw.c | 122 + sw/airborne/autopilot/link_fbw.h | 44 + sw/airborne/autopilot/lls.c | 68 + sw/airborne/autopilot/lls.h | 42 + sw/airborne/autopilot/main.c | 377 ++ sw/airborne/autopilot/mainloop.c | 85 + sw/airborne/autopilot/modem.c | 88 + sw/airborne/autopilot/modem.h | 140 + sw/airborne/autopilot/nav.c | 204 ++ sw/airborne/autopilot/nav.h | 56 + sw/airborne/autopilot/pid.c | 88 + sw/airborne/autopilot/pid.h | 58 + sw/airborne/autopilot/sirf.h | 36 + sw/airborne/autopilot/spi.c | 40 + sw/airborne/autopilot/spi.h | 74 + sw/airborne/autopilot/test/Makefile | 52 + sw/airborne/autopilot/test/check_spi.c | 46 + sw/airborne/autopilot/test/check_uart.c | 51 + sw/airborne/autopilot/test/test_modem.c | 50 + sw/airborne/autopilot/test/test_v2xe.c | 277 ++ sw/airborne/autopilot/test/tx_adcs.c | 66 + sw/airborne/autopilot/test/uart_tunnel.c | 29 + sw/airborne/autopilot/timer.h | 91 + sw/airborne/autopilot/uart.c | 138 + sw/airborne/autopilot/uart.h | 48 + sw/airborne/autopilot/ubx.h | 38 + sw/airborne/fly_by_wire/Makefile | 77 + sw/airborne/fly_by_wire/README | 24 + sw/airborne/fly_by_wire/adc_fbw.c | 120 + sw/airborne/fly_by_wire/adc_fbw.h | 61 + sw/airborne/fly_by_wire/link_autopilot.h | 65 + sw/airborne/fly_by_wire/main.c | 182 + sw/airborne/fly_by_wire/ppm.c | 135 + sw/airborne/fly_by_wire/ppm.h | 102 + sw/airborne/fly_by_wire/servo.c | 193 + sw/airborne/fly_by_wire/servo.h | 61 + sw/airborne/fly_by_wire/spi.c | 112 + sw/airborne/fly_by_wire/spi.h | 48 + sw/airborne/fly_by_wire/test/Makefile | 43 + sw/airborne/fly_by_wire/test/check_spi.c | 43 + sw/airborne/fly_by_wire/test/check_uart.c | 51 + sw/airborne/fly_by_wire/test/rc_transmitter.c | 71 + sw/airborne/fly_by_wire/test/setup_servos.c | 119 + sw/airborne/fly_by_wire/test/tx_adcs.c | 66 + sw/airborne/fly_by_wire/timer.h | 92 + sw/airborne/fly_by_wire/uart.c | 110 + sw/airborne/fly_by_wire/uart.h | 39 + sw/airborne/quadrirotor_autopilot/Makefile | 95 + sw/airborne/quadrirotor_autopilot/control.c | 47 + sw/airborne/quadrirotor_autopilot/control.h | 18 + sw/airborne/quadrirotor_autopilot/imu.c | 76 + sw/airborne/quadrirotor_autopilot/imu.h | 19 + sw/airborne/quadrirotor_autopilot/kalman.c | 111 + sw/airborne/quadrirotor_autopilot/kalman.h | 19 + .../quadrirotor_autopilot/kalman_phi.c | 62 + sw/airborne/quadrirotor_autopilot/main.c | 172 + sw/configurator/Makefile | 65 + sw/configurator/adc.ml | 36 + sw/configurator/airframe.ml | 38 + sw/configurator/attitude.ml | 46 + sw/configurator/autopilot.ml | 29 + sw/configurator/console.ml | 76 + sw/configurator/console.mli | 34 + sw/configurator/env.ml | 57 + sw/configurator/env.mli | 41 + sw/configurator/flasher.ml | 74 + sw/configurator/flasher.mli | 30 + sw/configurator/flightplan.ml | 78 + sw/configurator/hardware.ml | 260 ++ sw/configurator/infrared.ml | 3 + sw/configurator/logalizer.ml | 29 + sw/configurator/main.ml | 56 + sw/configurator/medit.ml | 537 +++ sw/configurator/monitor.ml | 29 + sw/configurator/notebook.ml | 62 + sw/configurator/notebook.mli | 28 + sw/configurator/radio.ml | 188 + sw/configurator/servos.ml | 136 + sw/configurator/simulator.ml | 29 + sw/configurator/tkXml.ml | 86 + sw/configurator/tkXml.mli | 35 + sw/configurator/tty.ml | 101 + sw/configurator/tty.mli | 42 + sw/configurator/upload.ml | 34 + sw/configurator/varXml.ml | 91 + sw/configurator/varXml.mli | 37 + sw/configurator/welcome.ml | 38 + sw/ground_segment/cockpit/Makefile | 35 + sw/ground_segment/cockpit/Paparazzi/APPage.pm | 46 + .../cockpit/Paparazzi/EnginePage.pm | 78 + .../cockpit/Paparazzi/Geometry.pm | 44 + .../cockpit/Paparazzi/HistoryView.pm | 80 + .../cockpit/Paparazzi/Horizon.pm | 199 + sw/ground_segment/cockpit/Paparazzi/IRPage.pm | 152 + .../cockpit/Paparazzi/LensScale.pm | 136 + .../cockpit/Paparazzi/MapView.pm | 479 +++ .../cockpit/Paparazzi/MissionD.pm | 108 + sw/ground_segment/cockpit/Paparazzi/ND.pm | 132 + sw/ground_segment/cockpit/Paparazzi/NDPage.pm | 62 + sw/ground_segment/cockpit/Paparazzi/PFD.pm | 199 + .../cockpit/Paparazzi/PFD_Panel.pm | 237 ++ .../cockpit/Paparazzi/RCSlider.pm | 138 + .../cockpit/Paparazzi/RCStick.pm | 131 + .../cockpit/Paparazzi/RCTransmitter.pm | 84 + .../cockpit/Paparazzi/RotaryGauge.pm | 124 + .../cockpit/Paparazzi/SatPage.pm | 145 + .../cockpit/Paparazzi/SatSigView.pm | 67 + sw/ground_segment/cockpit/Paparazzi/Scale.pm | 206 ++ sw/ground_segment/cockpit/Paparazzi/Strip.pm | 147 + .../cockpit/Paparazzi/StripPanel.pm | 80 + sw/ground_segment/cockpit/cockpit.pl | 359 ++ sw/ground_segment/cockpit/map.pl | 129 + sw/ground_segment/cockpit/map2d.ml | 225 ++ sw/ground_segment/cockpit/radio_control.pl | 68 + sw/ground_segment/modem/Makefile | 40 + sw/ground_segment/modem/README | 28 + sw/ground_segment/modem/adc.c | 39 + sw/ground_segment/modem/adc.h | 8 + sw/ground_segment/modem/link_tmtc.h | 92 + sw/ground_segment/modem/main.c | 70 + sw/ground_segment/modem/soft_uart.c | 80 + sw/ground_segment/modem/soft_uart.h | 21 + sw/ground_segment/modem/timer.h | 91 + sw/ground_segment/modem/uart.c | 81 + sw/ground_segment/modem/uart.h | 19 + sw/ground_segment/speech/README | 25 + sw/ground_segment/speech/paparazzi_speak.pl | 216 ++ sw/ground_segment/tmtc/Makefile | 54 + sw/ground_segment/tmtc/bilink.ml | 44 + sw/ground_segment/tmtc/messages.ml | 139 + sw/ground_segment/tmtc/modem.ml | 112 + sw/ground_segment/tmtc/receive.ml | 306 ++ sw/ground_segment/visu3d/Help_Keys.txt | 26 + sw/ground_segment/visu3d/Makefile | 48 + sw/ground_segment/visu3d/TODO | 20 + sw/ground_segment/visu3d/mapGL.ml | 493 +++ sw/ground_segment/wind/Makefile | 68 + sw/ground_segment/wind/wind.ml | 262 ++ sw/ground_segment/wind/wind.mli | 3 + sw/include/std.h | 38 + sw/lib/ocaml/Makefile | 94 + sw/lib/ocaml/convert.c | 46 + sw/lib/ocaml/cserial.c | 76 + sw/lib/ocaml/debug.ml | 46 + sw/lib/ocaml/env.ml | 37 + sw/lib/ocaml/extXml.ml | 87 + sw/lib/ocaml/geometry_2d.ml | 920 +++++ sw/lib/ocaml/geometry_2d.mli | 303 ++ sw/lib/ocaml/geometry_3d.ml | 610 ++++ sw/lib/ocaml/geometry_3d.mli | 234 ++ sw/lib/ocaml/gtk_3d.ml | 1443 ++++++++ sw/lib/ocaml/gtk_3d.mli | 252 ++ sw/lib/ocaml/gtk_draw.ml | 210 ++ sw/lib/ocaml/gtk_draw.mli | 137 + sw/lib/ocaml/gtk_image.ml | 350 ++ sw/lib/ocaml/gtk_image.mli | 117 + sw/lib/ocaml/gtk_tools.ml | 3199 +++++++++++++++++ sw/lib/ocaml/gtk_tools.mli | 1354 +++++++ sw/lib/ocaml/gtk_tools_GL.ml | 149 + sw/lib/ocaml/gtk_tools_GL.mli | 90 + sw/lib/ocaml/gtk_tools_icons.ml | 238 ++ sw/lib/ocaml/gtkgl_Hack.ml | 30 + sw/lib/ocaml/gtkgl_Hack.mli | 29 + sw/lib/ocaml/latlong.ml | 334 ++ sw/lib/ocaml/latlong.mli | 107 + sw/lib/ocaml/mapCanvas.ml | 218 ++ sw/lib/ocaml/mapTrack.ml | 77 + sw/lib/ocaml/mapWaypoints.ml | 122 + sw/lib/ocaml/ml_gtkgl_hack.c | 87 + sw/lib/ocaml/ocaml_tools.ml | 708 ++++ sw/lib/ocaml/ocaml_tools.mli | 393 ++ sw/lib/ocaml/platform.ml | 31 + sw/lib/ocaml/platform.mli | 32 + sw/lib/ocaml/pprz.ml | 192 + sw/lib/ocaml/pprz.mli | 57 + sw/lib/ocaml/serial.ml | 114 + sw/lib/ocaml/serial.mli | 76 + sw/lib/ocaml/srtm.ml | 82 + sw/lib/ocaml/srtm.mli | 42 + sw/lib/ocaml/ubx.ml | 159 + sw/lib/ocaml/utm_of.ml | 6 + sw/lib/ocaml/wavecard.ml | 131 + sw/lib/ocaml/wavecard.mli | 7 + sw/lib/ocaml/xml2h.ml | 69 + sw/lib/ocaml/xml_get.ml | 16 + sw/lib/perl/Makefile | 21 + sw/lib/perl/Paparazzi/Environment.pm | 69 + sw/lib/perl/Paparazzi/IvyProtocol.pm | 134 + sw/lib/perl/Paparazzi/Utils.pm | 40 + sw/logalizer/Makefile | 32 + sw/logalizer/README | 27 + sw/logalizer/play.ml | 117 + sw/logalizer/plot.pl | 342 ++ sw/simulator/Makefile | 123 + sw/simulator/data.ml | 46 + sw/simulator/events.ml | 105 + sw/simulator/flightModel.ml | 157 + sw/simulator/flightModel.mli | 22 + sw/simulator/gen_downlink.ml | 92 + sw/simulator/gps.ml | 48 + sw/simulator/gui.ml | 49 + sw/simulator/hitl.ml | 142 + sw/simulator/hitl.mli | 5 + sw/simulator/sim.ml | 188 + sw/simulator/sim.mli | 24 + sw/simulator/sim_ap.c | 93 + sw/simulator/sim_gps.c | 42 + sw/simulator/sim_ir.c | 24 + sw/simulator/simhitl.ml | 14 + sw/simulator/simsitl.ml | 12 + sw/simulator/simsitl.pl | 31 + sw/simulator/sirf.ml | 102 + sw/simulator/sitl.ml | 116 + sw/simulator/sitl.mli | 6 + sw/simulator/stdlib.ml | 16 + sw/simulator/timer.h | 1 + sw/simulator/types.ml | 116 + sw/supervision/Paparazzi/CpGui.pm | 219 ++ sw/supervision/Paparazzi/CpPgmMgr.pm | 79 + sw/supervision/Paparazzi/CpSessionMgr.pm | 192 + sw/supervision/paparazzi.pl | 80 + sw/tools/Makefile | 39 + sw/tools/fp_lexer.mll | 30 + sw/tools/fp_parser.mly | 51 + sw/tools/fp_proc.ml | 389 ++ sw/tools/fp_syntax.ml | 77 + sw/tools/fp_syntax.mli | 44 + sw/tools/gen_aircraft.ml | 33 + sw/tools/gen_airframe.ml | 157 + sw/tools/gen_calib.ml | 101 + sw/tools/gen_flight_plan.ml | 450 +++ sw/tools/gen_messages.ml | 254 ++ sw/tools/gen_radio.ml | 88 + sw/tools/gen_ubx.ml | 126 + 297 files changed, 36428 insertions(+) create mode 100644 AUTHORS create mode 100644 BUGS create mode 100644 COPYING create mode 100644 Makefile create mode 100644 Makefile.ac create mode 100644 Makefile.gen create mode 100755 Makefile.pl create mode 100644 README create mode 100644 TODO create mode 100644 conf/Makefile.avr create mode 100644 conf/Makefile.local create mode 100644 conf/airframes/microjet1.xml create mode 100644 conf/airframes/microjet2.xml create mode 100644 conf/airframes/twinstar1.xml create mode 100644 conf/airframes/twinstar2.xml create mode 100644 conf/conf.xml create mode 100644 conf/control_panel.xml create mode 100644 conf/control_panel.xml.sys create mode 100644 conf/flight_plans/circles.xml create mode 100644 conf/flight_plans/flight_plan.dtd create mode 100644 conf/flight_plans/hippo.xml create mode 100644 conf/flight_plans/huit.xml create mode 100644 conf/flight_plans/muret1.xml create mode 100644 conf/flight_plans/muret2.xml create mode 100644 conf/flight_plans/muret3.xml create mode 100644 conf/flight_plans/procedure.dtd create mode 100644 conf/hosts_wavecard.xml create mode 100644 conf/install.xml create mode 100644 conf/messages.xml create mode 100644 conf/radios/cockpitMM.xml create mode 100644 conf/radios/fc28.xml create mode 100644 conf/radios/mc3030.xml create mode 100644 conf/ubx.dtd create mode 100644 conf/ubx.xml create mode 100644 conf/wavecard.xml create mode 100644 data/maps/muret_UTM.gif create mode 100644 data/maps/muret_UTM.xml create mode 100644 data/pictures/cockpitMM.gif create mode 100644 data/pictures/fc28.gif create mode 100644 data/pictures/penguin_logo.gif create mode 100644 data/pictures/t7cap.jpg create mode 100644 doc/checklist/checklist.tex create mode 100644 doc/user_manual/Makefile create mode 100644 doc/user_manual/fly_by_wire.dia create mode 100644 doc/user_manual/overall.dia create mode 100644 doc/user_manual/overall.png create mode 100644 doc/user_manual/paparazzi.tex create mode 100644 sw/README create mode 100644 sw/airborne/autopilot/Makefile create mode 100644 sw/airborne/autopilot/README create mode 100644 sw/airborne/autopilot/adc.c create mode 100644 sw/airborne/autopilot/adc.h create mode 100644 sw/airborne/autopilot/autopilot.h create mode 100644 sw/airborne/autopilot/downlink.h create mode 100644 sw/airborne/autopilot/estimator.c create mode 100644 sw/airborne/autopilot/estimator.h create mode 100644 sw/airborne/autopilot/gps.h create mode 100644 sw/airborne/autopilot/gps_sirf.c create mode 100644 sw/airborne/autopilot/gps_ubx.c create mode 100644 sw/airborne/autopilot/if_calib.c create mode 100644 sw/airborne/autopilot/if_calib.h create mode 100644 sw/airborne/autopilot/infrared.c create mode 100644 sw/airborne/autopilot/infrared.h create mode 100644 sw/airborne/autopilot/link_fbw.c create mode 100644 sw/airborne/autopilot/link_fbw.h create mode 100644 sw/airborne/autopilot/lls.c create mode 100644 sw/airborne/autopilot/lls.h create mode 100644 sw/airborne/autopilot/main.c create mode 100644 sw/airborne/autopilot/mainloop.c create mode 100644 sw/airborne/autopilot/modem.c create mode 100644 sw/airborne/autopilot/modem.h create mode 100644 sw/airborne/autopilot/nav.c create mode 100644 sw/airborne/autopilot/nav.h create mode 100644 sw/airborne/autopilot/pid.c create mode 100644 sw/airborne/autopilot/pid.h create mode 100644 sw/airborne/autopilot/sirf.h create mode 100644 sw/airborne/autopilot/spi.c create mode 100644 sw/airborne/autopilot/spi.h create mode 100644 sw/airborne/autopilot/test/Makefile create mode 100644 sw/airborne/autopilot/test/check_spi.c create mode 100644 sw/airborne/autopilot/test/check_uart.c create mode 100644 sw/airborne/autopilot/test/test_modem.c create mode 100644 sw/airborne/autopilot/test/test_v2xe.c create mode 100644 sw/airborne/autopilot/test/tx_adcs.c create mode 100644 sw/airborne/autopilot/test/uart_tunnel.c create mode 100644 sw/airborne/autopilot/timer.h create mode 100644 sw/airborne/autopilot/uart.c create mode 100644 sw/airborne/autopilot/uart.h create mode 100644 sw/airborne/autopilot/ubx.h create mode 100644 sw/airborne/fly_by_wire/Makefile create mode 100644 sw/airborne/fly_by_wire/README create mode 100644 sw/airborne/fly_by_wire/adc_fbw.c create mode 100644 sw/airborne/fly_by_wire/adc_fbw.h create mode 100644 sw/airborne/fly_by_wire/link_autopilot.h create mode 100644 sw/airborne/fly_by_wire/main.c create mode 100644 sw/airborne/fly_by_wire/ppm.c create mode 100644 sw/airborne/fly_by_wire/ppm.h create mode 100644 sw/airborne/fly_by_wire/servo.c create mode 100644 sw/airborne/fly_by_wire/servo.h create mode 100644 sw/airborne/fly_by_wire/spi.c create mode 100644 sw/airborne/fly_by_wire/spi.h create mode 100644 sw/airborne/fly_by_wire/test/Makefile create mode 100644 sw/airborne/fly_by_wire/test/check_spi.c create mode 100644 sw/airborne/fly_by_wire/test/check_uart.c create mode 100644 sw/airborne/fly_by_wire/test/rc_transmitter.c create mode 100644 sw/airborne/fly_by_wire/test/setup_servos.c create mode 100644 sw/airborne/fly_by_wire/test/tx_adcs.c create mode 100644 sw/airborne/fly_by_wire/timer.h create mode 100644 sw/airborne/fly_by_wire/uart.c create mode 100644 sw/airborne/fly_by_wire/uart.h create mode 100644 sw/airborne/quadrirotor_autopilot/Makefile create mode 100644 sw/airborne/quadrirotor_autopilot/control.c create mode 100644 sw/airborne/quadrirotor_autopilot/control.h create mode 100644 sw/airborne/quadrirotor_autopilot/imu.c create mode 100644 sw/airborne/quadrirotor_autopilot/imu.h create mode 100644 sw/airborne/quadrirotor_autopilot/kalman.c create mode 100644 sw/airborne/quadrirotor_autopilot/kalman.h create mode 100644 sw/airborne/quadrirotor_autopilot/kalman_phi.c create mode 100644 sw/airborne/quadrirotor_autopilot/main.c create mode 100644 sw/configurator/Makefile create mode 100644 sw/configurator/adc.ml create mode 100644 sw/configurator/airframe.ml create mode 100644 sw/configurator/attitude.ml create mode 100644 sw/configurator/autopilot.ml create mode 100644 sw/configurator/console.ml create mode 100644 sw/configurator/console.mli create mode 100644 sw/configurator/env.ml create mode 100644 sw/configurator/env.mli create mode 100644 sw/configurator/flasher.ml create mode 100644 sw/configurator/flasher.mli create mode 100644 sw/configurator/flightplan.ml create mode 100644 sw/configurator/hardware.ml create mode 100644 sw/configurator/infrared.ml create mode 100644 sw/configurator/logalizer.ml create mode 100644 sw/configurator/main.ml create mode 100644 sw/configurator/medit.ml create mode 100644 sw/configurator/monitor.ml create mode 100644 sw/configurator/notebook.ml create mode 100644 sw/configurator/notebook.mli create mode 100644 sw/configurator/radio.ml create mode 100644 sw/configurator/servos.ml create mode 100644 sw/configurator/simulator.ml create mode 100644 sw/configurator/tkXml.ml create mode 100644 sw/configurator/tkXml.mli create mode 100644 sw/configurator/tty.ml create mode 100644 sw/configurator/tty.mli create mode 100644 sw/configurator/upload.ml create mode 100644 sw/configurator/varXml.ml create mode 100644 sw/configurator/varXml.mli create mode 100644 sw/configurator/welcome.ml create mode 100644 sw/ground_segment/cockpit/Makefile create mode 100644 sw/ground_segment/cockpit/Paparazzi/APPage.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/EnginePage.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/Geometry.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/HistoryView.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/Horizon.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/IRPage.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/LensScale.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/MapView.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/MissionD.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/ND.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/NDPage.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/PFD.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/PFD_Panel.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/RCSlider.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/RCStick.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/RCTransmitter.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/RotaryGauge.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/SatPage.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/SatSigView.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/Scale.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/Strip.pm create mode 100644 sw/ground_segment/cockpit/Paparazzi/StripPanel.pm create mode 100755 sw/ground_segment/cockpit/cockpit.pl create mode 100755 sw/ground_segment/cockpit/map.pl create mode 100644 sw/ground_segment/cockpit/map2d.ml create mode 100755 sw/ground_segment/cockpit/radio_control.pl create mode 100644 sw/ground_segment/modem/Makefile create mode 100644 sw/ground_segment/modem/README create mode 100644 sw/ground_segment/modem/adc.c create mode 100644 sw/ground_segment/modem/adc.h create mode 100644 sw/ground_segment/modem/link_tmtc.h create mode 100644 sw/ground_segment/modem/main.c create mode 100644 sw/ground_segment/modem/soft_uart.c create mode 100644 sw/ground_segment/modem/soft_uart.h create mode 100644 sw/ground_segment/modem/timer.h create mode 100644 sw/ground_segment/modem/uart.c create mode 100644 sw/ground_segment/modem/uart.h create mode 100644 sw/ground_segment/speech/README create mode 100755 sw/ground_segment/speech/paparazzi_speak.pl create mode 100644 sw/ground_segment/tmtc/Makefile create mode 100644 sw/ground_segment/tmtc/bilink.ml create mode 100644 sw/ground_segment/tmtc/messages.ml create mode 100644 sw/ground_segment/tmtc/modem.ml create mode 100644 sw/ground_segment/tmtc/receive.ml create mode 100644 sw/ground_segment/visu3d/Help_Keys.txt create mode 100644 sw/ground_segment/visu3d/Makefile create mode 100644 sw/ground_segment/visu3d/TODO create mode 100644 sw/ground_segment/visu3d/mapGL.ml create mode 100644 sw/ground_segment/wind/Makefile create mode 100644 sw/ground_segment/wind/wind.ml create mode 100644 sw/ground_segment/wind/wind.mli create mode 100644 sw/include/std.h create mode 100644 sw/lib/ocaml/Makefile create mode 100644 sw/lib/ocaml/convert.c create mode 100644 sw/lib/ocaml/cserial.c create mode 100644 sw/lib/ocaml/debug.ml create mode 100644 sw/lib/ocaml/env.ml create mode 100644 sw/lib/ocaml/extXml.ml create mode 100644 sw/lib/ocaml/geometry_2d.ml create mode 100644 sw/lib/ocaml/geometry_2d.mli create mode 100644 sw/lib/ocaml/geometry_3d.ml create mode 100644 sw/lib/ocaml/geometry_3d.mli create mode 100644 sw/lib/ocaml/gtk_3d.ml create mode 100644 sw/lib/ocaml/gtk_3d.mli create mode 100644 sw/lib/ocaml/gtk_draw.ml create mode 100644 sw/lib/ocaml/gtk_draw.mli create mode 100644 sw/lib/ocaml/gtk_image.ml create mode 100644 sw/lib/ocaml/gtk_image.mli create mode 100644 sw/lib/ocaml/gtk_tools.ml create mode 100644 sw/lib/ocaml/gtk_tools.mli create mode 100644 sw/lib/ocaml/gtk_tools_GL.ml create mode 100644 sw/lib/ocaml/gtk_tools_GL.mli create mode 100644 sw/lib/ocaml/gtk_tools_icons.ml create mode 100644 sw/lib/ocaml/gtkgl_Hack.ml create mode 100644 sw/lib/ocaml/gtkgl_Hack.mli create mode 100644 sw/lib/ocaml/latlong.ml create mode 100644 sw/lib/ocaml/latlong.mli create mode 100644 sw/lib/ocaml/mapCanvas.ml create mode 100644 sw/lib/ocaml/mapTrack.ml create mode 100644 sw/lib/ocaml/mapWaypoints.ml create mode 100644 sw/lib/ocaml/ml_gtkgl_hack.c create mode 100644 sw/lib/ocaml/ocaml_tools.ml create mode 100644 sw/lib/ocaml/ocaml_tools.mli create mode 100644 sw/lib/ocaml/platform.ml create mode 100644 sw/lib/ocaml/platform.mli create mode 100644 sw/lib/ocaml/pprz.ml create mode 100644 sw/lib/ocaml/pprz.mli create mode 100644 sw/lib/ocaml/serial.ml create mode 100644 sw/lib/ocaml/serial.mli create mode 100644 sw/lib/ocaml/srtm.ml create mode 100644 sw/lib/ocaml/srtm.mli create mode 100644 sw/lib/ocaml/ubx.ml create mode 100644 sw/lib/ocaml/utm_of.ml create mode 100644 sw/lib/ocaml/wavecard.ml create mode 100644 sw/lib/ocaml/wavecard.mli create mode 100644 sw/lib/ocaml/xml2h.ml create mode 100644 sw/lib/ocaml/xml_get.ml create mode 100644 sw/lib/perl/Makefile create mode 100644 sw/lib/perl/Paparazzi/Environment.pm create mode 100644 sw/lib/perl/Paparazzi/IvyProtocol.pm create mode 100644 sw/lib/perl/Paparazzi/Utils.pm create mode 100644 sw/logalizer/Makefile create mode 100644 sw/logalizer/README create mode 100644 sw/logalizer/play.ml create mode 100755 sw/logalizer/plot.pl create mode 100644 sw/simulator/Makefile create mode 100644 sw/simulator/data.ml create mode 100644 sw/simulator/events.ml create mode 100644 sw/simulator/flightModel.ml create mode 100644 sw/simulator/flightModel.mli create mode 100644 sw/simulator/gen_downlink.ml create mode 100644 sw/simulator/gps.ml create mode 100644 sw/simulator/gui.ml create mode 100644 sw/simulator/hitl.ml create mode 100644 sw/simulator/hitl.mli create mode 100644 sw/simulator/sim.ml create mode 100644 sw/simulator/sim.mli create mode 100644 sw/simulator/sim_ap.c create mode 100644 sw/simulator/sim_gps.c create mode 100644 sw/simulator/sim_ir.c create mode 100644 sw/simulator/simhitl.ml create mode 100644 sw/simulator/simsitl.ml create mode 100755 sw/simulator/simsitl.pl create mode 100644 sw/simulator/sirf.ml create mode 100644 sw/simulator/sitl.ml create mode 100644 sw/simulator/sitl.mli create mode 100644 sw/simulator/stdlib.ml create mode 100644 sw/simulator/timer.h create mode 100644 sw/simulator/types.ml create mode 100755 sw/supervision/Paparazzi/CpGui.pm create mode 100644 sw/supervision/Paparazzi/CpPgmMgr.pm create mode 100644 sw/supervision/Paparazzi/CpSessionMgr.pm create mode 100755 sw/supervision/paparazzi.pl create mode 100644 sw/tools/Makefile create mode 100644 sw/tools/fp_lexer.mll create mode 100644 sw/tools/fp_parser.mly create mode 100644 sw/tools/fp_proc.ml create mode 100644 sw/tools/fp_syntax.ml create mode 100644 sw/tools/fp_syntax.mli create mode 100644 sw/tools/gen_aircraft.ml create mode 100644 sw/tools/gen_airframe.ml create mode 100644 sw/tools/gen_calib.ml create mode 100644 sw/tools/gen_flight_plan.ml create mode 100644 sw/tools/gen_messages.ml create mode 100644 sw/tools/gen_radio.ml create mode 100644 sw/tools/gen_ubx.ml diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000000..88464f7ed63 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Pascal Brisset +Antoine Drouin diff --git a/BUGS b/BUGS new file mode 100644 index 00000000000..beaeeef8054 --- /dev/null +++ b/BUGS @@ -0,0 +1,15 @@ +paparazzi.pl +CpGui et CpSeesionManager n'ont pas les meme variables !!! + + + + +receive +ne cree pas son repertoire de log et meurt + + + + + +visu3D +ne trouve pas son fichier d'aide. a mettre dans conf ?? diff --git a/COPYING b/COPYING new file mode 100644 index 00000000000..916d1f0f284 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000000..cc01450549d --- /dev/null +++ b/Makefile @@ -0,0 +1,119 @@ +# Paparazzi main $Id$ +# Copyright (C) 2004 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +include conf/Makefile.local + +LIB=sw/lib +AIRBORNE=sw/airborne +CONFIGURATOR=sw/configurator +FBW=$(AIRBORNE)/fly_by_wire +AP=$(AIRBORNE)/autopilot +COCKPIT=sw/ground_segment/cockpit +TMTC=sw/ground_segment/tmtc +WIND=sw/ground_segment/wind +VISU3D=sw/ground_segment/visu3d +LOGALIZER=sw/logalizer +SIMULATOR=sw/simulator +MAKE=make + +static : lib tools configurator cockpit tmtc visu3d logalizer sim_static wind + +configure : configurator + PAPARAZZI_DIR=`pwd` $(CONFIGURATOR)/configurator + +lib: + cd $(LIB)/ocaml; $(MAKE) + cd $(LIB)/perl; $(MAKE) + +tools: + cd $(TOOLS); make + +logalizer: lib + cd $(LOGALIZER); $(MAKE) + +configurator: lib + cd $(CONFIGURATOR); $(MAKE) + +sim_static : + cd $(SIMULATOR); $(MAKE) + +sim_sitl : + cd $(SIMULATOR); $(MAKE) sim_sitl + +fbw fly_by_wire: + cd $(FBW); $(MAKE) all + +ap autopilot: + cd $(AP); $(MAKE) all + +upload_fbw: fbw + cd $(FBW); $(MAKE) upload + +upload_ap: ap + cd $(AP); $(MAKE) upload + +erase_fbw: + cd $(FBW); $(MAKE) erase + +erase_ap: + cd $(AP); $(MAKE) erase + +airborne: fbw ap + +cockpit: lib + cd $(COCKPIT); $(MAKE) all + +tmtc: lib + cd $(TMTC); $(MAKE) all + +visu3d: lib + cd $(VISU3D); $(MAKE) +wind: + cd $(WIND); $(MAKE) + +receive: tmtc + $(TMTC)/receive + +static_h : + make -f Makefile.gen + +ac_h : + $(TOOLS)/gen_aircraft.out $(AIRCRAFT) + +ac: static_h ac_h ap fbw sim_sitl + +clean_ac : + rm -fr $(PAPARAZZI_HOME)/var/$(AIRCRAFT) + +run_sitl : + $(PAPARAZZI_HOME)/var/$(AIRCRAFT)/sim/simsitl.out + +t1: ac + +install : static t1 + ./Makefile.pl -install -destdir $(DESTDIR) + +uninstall : + ./Makefile.pl -uninstall -destdir $(DESTDIR) + +clean: + find . -name Makefile -mindepth 2 -exec sh -c '$(MAKE) -C `dirname {}` $@' \; + find . -name '*~' -exec rm -f {} \; + diff --git a/Makefile.ac b/Makefile.ac new file mode 100644 index 00000000000..f81cb227d3c --- /dev/null +++ b/Makefile.ac @@ -0,0 +1,53 @@ +# Paparazzi main $Id$ +# Copyright (C) 2004 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +# Preprocessing of XML configuration files + +include conf/Makefile.local + +CONF=$(PAPARAZZI_HOME)/conf +CONF_XML=$(CONF)/conf.xml +ACINCLUDE = $(PAPARAZZI_HOME)/var/$(AIRCRAFT) +AIRFRAME_H=$(ACINCLUDE)/airframe.h +RADIO_H=$(ACINCLUDE)/radio.h +FLIGHT_PLAN_H=$(ACINCLUDE)/flight_plan.h +INFLIGHT_CALIB_H=$(ACINCLUDE)/inflight_calib.h + +all: $(AIRFRAME_H) $(RADIO_H) $(FLIGHT_PLAN_H) $(INFLIGHT_CALIB_H) + echo $(AIRFRAME_H) $(CONF)/$(AIRFRAME) + +$(AIRFRAME_H) : $(CONF)/$(AIRFRAME) $(CONF_XML) + $(TOOLS)/gen_airframe.out $(AIRCRAFT) $< > /tmp/airframe.h + mv /tmp/airframe.h $@ + +$(RADIO_H) : $(CONF)/$(RADIO) $(CONF_XML) + $(TOOLS)/gen_radio.out $< > /tmp/radio.h + mv /tmp/radio.h $@ + +$(FLIGHT_PLAN_H) : $(CONF)/$(FLIGHT_PLAN) $(CONF_XML) + $(TOOLS)/gen_flight_plan.out $< > /tmp/fp.h + mv /tmp/fp.h $@ + +$(INFLIGHT_CALIB_H) : $(CONF)/$(FLIGHT_PLAN) $(CONF_XML) + $(TOOLS)/gen_calib.out $< > /tmp/c.h + mv /tmp/c.h $@ + +clean : + rm -f $(ACINCLUDE)/*.h diff --git a/Makefile.gen b/Makefile.gen new file mode 100644 index 00000000000..a1842aa7709 --- /dev/null +++ b/Makefile.gen @@ -0,0 +1,47 @@ +# Paparazzi main $Id$ +# Copyright (C) 2004 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +# Preprocessing of XML configuration files + +include conf/Makefile.local + +CONF=conf +XML_GET=sw/lib/ocaml/xml_get.out + + +STATICINCLUDE =$(PAPARAZZI_HOME)/var/include +MESSAGES_H=$(STATICINCLUDE)/messages.h +UBX_PROTOCOL_H=$(STATICINCLUDE)/ubx_protocol.h +MESSAGES_XML = $(CONF)/messages.xml +UBX_XML = $(CONF)/ubx.xml + + +static: $(MESSAGES_H) $(UBX_PROTOCOL_H) + +$(MESSAGES_H) : $(MESSAGES_XML) $(CONF_XML) + $(TOOLS)/gen_messages.out $< telemetry_ap > /tmp/messages.h + mv /tmp/messages.h $@ + +$(UBX_PROTOCOL_H) : $(UBX_XML) $(CONF_XML) + $(TOOLS)/gen_ubx.out $< > /tmp/ubx.h + mv /tmp/ubx.h $@ + +clean : + rm -f $(H_OF_XML) diff --git a/Makefile.pl b/Makefile.pl new file mode 100755 index 00000000000..c9a1bdea363 --- /dev/null +++ b/Makefile.pl @@ -0,0 +1,72 @@ +#!/usr/bin/perl -w + +use strict; +use File::Basename; +use Getopt::Long; +use Data::Dumper; +use XML::DOM; + +my $destdir="/usr"; +my $install = undef; +my $uninstall = undef; +my @sections; + +GetOptions("install" => \$install, + "uninstall" => \$uninstall, + "destdir=s" => \$destdir); + +read_xml("./conf/install.xml"); + +foreach my $section (@sections) { + my ($inst_dir, $files) = @{$section}; + do_install($inst_dir, $files) if ($install); + do_uninstall($inst_dir, $files) if ($uninstall); +} + +sub do_install { + my ($dest_dir, $files) = @_; + `install -d $dest_dir`;# or warn "creation of directory $dest_dir failed"; + foreach my $file (@{$files}) { + my ($path, $new_name) = @{$file}; + print "installing file $path in $dest_dir ".($new_name?"as $new_name":"")."\n"; + my $cmd = "install $path $dest_dir".($new_name?"/$new_name":""); + `$cmd`;# or warn "intall of $path failed"; + } +} + +sub do_uninstall { + my ($dest_dir, $files) = @_; + foreach my $file (@{$files}) { + my ($path, $new_name) = @{$file}; + my $to_be_removed = $dest_dir."/".($new_name?"$new_name":basename($path)); + print "removing $to_be_removed\n"; + `rm -f $to_be_removed`; + } +} + +sub read_xml { + my ($filename) = @_; + my $parser = XML::DOM::Parser->new(); + my $doc = $parser->parsefile($filename); + my $cp = $doc->getElementsByTagName("install")->[0]; + my $sections = $cp->getElementsByTagName("section"); + foreach my $section (@{$sections}) { + my $section_name = $section->getAttribute('name'); + my $dest_loc = $destdir."/".$section->getAttribute('dest'); + my $files = $section->getElementsByTagName("file"); + my $file_a = []; + foreach my $file (@{$files}) { + push @{$file_a}, [$file->getAttribute('name'), $file->getAttribute('new_name')]; + } + my $dirs = $section->getElementsByTagName("directory"); + foreach my $dir (@{$dirs}) { + my $dirname=$dir->getAttribute('name'); + opendir(DIR,$dirname); + my @dir_files = grep { -f "$dirname/$_" } readdir(DIR); + map { s#^(.*)#$dirname/$1# } @dir_files; + closedir(DIR); + push @{$file_a}, @dir_files; + } + push @sections, [$dest_loc, $file_a]; + } +} diff --git a/README b/README new file mode 100644 index 00000000000..8da310bac02 --- /dev/null +++ b/README @@ -0,0 +1,208 @@ +# Paparazzi $Id$ +# Copyright (C) 2003 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +Intro +----- + +Paparazzi is an attempt to develop a cheap fixed wing UAV (Unmanned Air +Vehicle). As of today we have successfully flown autonomously several small +electro powered fixed wing aircraft: the Twinstar and the Microjet of +Multiplex. + +Up to date informations are available from the website + + www.nongnu.org/paparazzi + +and from the mailing list. + +Directories description: +----------------------- + +conf: the configuration directory (airframe, radio, ... descriptions). YOU HAVE +TO EDIT THERE the Makefile.local file + +data: where to put read-only data (e.g. maps and terrain files) + +doc: the paparazzi documentation. + +hw: hardware definitions (electronic schemas, PCBs, ...) + +sw: software (onboard, ground station, simulation, ...) + + +Required Software +----------------- + - AVR micro-controller development environnment (avr-gcc, uisp, avr-libc) + - OCaml (ocaml.org), xml-light library (http://tech.motion-twin.com/xmllight) + - gcc, GTK2, Glib2, libgnomecanvas, libxml2 + - ... + +For Debian users: Required packages are available at + http://www.rechercher.enac.fr/paparazzi/debian +Installation of the meta-package "paparazzi" will install everything +needed (if something is missing, please ask) + +Compilation +----------- + + 0) Configuration. Default PAPARAZZI_HOME is $(HOME)/PAPARAZZI. You +can change it by setting an environment variable + + 1) "make" in the top directory + + 2) Set the PAPARAZZI_SRC environment variable to the top directory +(default is /usr/share/paparazzi) + + 2) "make init" creates a directory $PAPARAZZI_HOME for your own files. +Configure there the conf/conf.xml + + 3) "make ac AIRCRAFT=" compiles everything for the specified +aircraft (default is twinstar2 for which conf files are provided) and +set the files in $PAPARAZZI_HOME/var/ + + 5) "make clean_ac AIRCRAFT=" cleans files for the specified aircraft. + + 6) $PAPARAZZI_HOME/var//sim/simsitl.out runs the soft simulator. + +Uploading of the embedded software +---------------------------------- + + 1) Power the flight controller board. Plug the pc-link to the board + and to the host parallel port. + + 2) Upload with + + make upload_fbw # Fly by wire + make upload_ap # Autopilot + + Important notes: + - The pclink must be switched accordingly with the target + - The "fly by wire" controller cannot be uploaded when the + "autopilot" controller is running. They cannot independently be + modified; then an upload of the fly by wire usually requires + + make erase_ap + make upload_fbw + make upload_ap + + +Running the ground segment monitoring +------------------------------------- + 1) The transmitter must be plugged to the flight controller and both must + be powered. + + 2) The ground modem must be powered and plugged to the antenna and + to the host (trough a serial port) + + 3) Launch the supervision + + sw/supervision/paparazzi.pl + + 4) Launch "receive", "cockpit", "map", ... + + +Log replay +---------- + 1) Run the supervision + + sw/supervision/paparazzi.pl + + 2) Launch "play", "cockpit", "map", ... + + +Software in the loop simulator +------------------------------ +This simulator allows to run the stabilization and navigation controllers +and play with the ground control station. + +0) Use the conf.xml and ground_segment.xml examples for the configuration. +Recompile everything to be sure you run what you want ("make clean; make" in +the top directory). The mission takes place in Braunschweig, Germany +(flight competion of EMAV'04). + +1) Run the "control panel" (sw/supervision/paparazzi.pl) + This window helps to launch the different components. + +2) Launch the "cockpit" to display flight parameters + +3) Launch "sim" (aircraft simulator) + You get two windows standing for + - The aircraft + - The radio-controller (RC, displayed as one slider for each channel, +even if some of them are buttons on the real RC) + +4) "Boot" the aircraft (button in the aircraft window) + The cockpit now displays some parameters. You can check + - The autopilot mode: "auto1" (stabilized manual mode) + - The altitude (on the right of the horizon) + - The speed: null (on the left of the horizon) + +5) "Launch" the aircraft (button in the aircraft window) + The speed is now 10m/s. + The altitude is going down: push the THROTTLE to go up ! + Ok, you were probably too slow: the aircraft went too far from HOME +and the autopilot mode is now "home" (cockpit window): it is going +back home automatically and you do not control anything with the RC. + +6) Launch "map" (from the control panel) + In this window, you can zoom with mouse wheel and pan with the middle +button. + The aicraft is going around the "HOME" waypoint. + Now, reset the autopilot mode with GAIN1 slider (push full left for +one second, put it back around 0 when "auto1" is displayed in the +cockpit window). The aircraft is going away: turn right or left with +the "ROLL" slider which directly controls the "roll" angle (set it to 0 +to go straight) + Look at the altitude. Control its variation with the "THROTTLE". + +7) Launch "mission" (from the control panel) + This window displays the flight plan the aircraft will follow in +autonomous mode. If your current altitude is realistic, the second +block should be active (if not, go up with more THROTTLE) + +8) Switch to autonomous mode "auto2" with the "MODE" slider (push right) + + The aircraft successively goes to waypoints 1 and 3 while trying to +stay at a constant altitude of 200m. The trajectory is better if you +active the automatic calibration of the attitude with the "LLS" slider +(with a large positive value, you get "ON" on the Cockpit window) + +9) Activate the next block of the flight plan with "GAIN1" slider (full left) + The active block is now the "height". On the "map" window (type +CRTL-C to clear the track), you can observe a red point (the "carrot") +which moves in front of the aircraft: it is the guide of the aircraft +(that you probably should consider as a donkey in this case), always +5 second before the aircraft on the desired track. + +10) Add some west wind (with the slider on the aircraft window) + 5m/s is an acceptable value for this approximative flight model. The +aircraft no longer can follow the "height" trajectory. + +11) Activate the next block of the mission ("GAIN1" slider, full left) + In this "xyz" mode, you can control the carrot position with the +"YAW" (west-east) and "PITCH" (south-north) sliders: the slider value is +the speed of the carrot. + +12) Activate the next block + In this block, the aircraft follows a circle around the "HOME" +waypoint at a fixed distance. + +13) Close the control panel to quit diff --git a/TODO b/TODO new file mode 100644 index 00000000000..4a5bfe1082a --- /dev/null +++ b/TODO @@ -0,0 +1,96 @@ +dans flybywire +chop servo ne depends pas du servo + + +pour les missions +possibilité de "transformer" (rotation Z, translation XYZ) une mission + +possibilitite de faire la meme chose pour une partie des waypoints (on en a une partie pour les evolutions et une partie pour le circuit d'atterissage. On deplace ceux du circuit d'atterissage pour qu'il colle a la piste. et on deplace ceux des evolution pour etre en face du jury :) + + +On stocke la/les transformations et on peut avoir des missions communes muret/ricou + +des declarations de points locales aux blocs et des transformations par blocs + + +faire medit avec visu3d + +proposer d'ajouter un waypoint en relatif par rapport a un autre - et en coordonnees polaires (dist, QDM) + + + + + +l'interface du captureur de video - c'est aussi visu3d . Il a une liste de textures (photos) et on peut les transformer. La description est sauvées dans un fichier xml et peut etre rechargée. les photos vont dans var/photos + + +dans visu3d, il faudrait pouvoir pivoter en Z sur la position courante (en tenant compte du zoom ) + + +###################### + +logger les simus comme les vols - y penser en refaisant receive - code commun + +Pour les simus a plusieurs avions, il doit y avoir partage d'un certain nombre d'informations entre les differentes instances des simu (par exemple le vent) + +Dans un circle, il faut afficher le QDR - calcul au sol?? + +Pour les missions, il faudrait pouvoir dire . faire un cercle pendant 180° ou faire un cercle pendant n secondes. Il faudrait donc disposer du temps depuis le block et du de l'angle parcouru depuis le debut du cercle. +C'est pour faire un palier en haut de la monté. Pour laisser le terme accumulateur se recaler avant la descente. + +########################################### + +Sujet : procedure automatique d'interuption de vol pour microdrone + +-identifier des scenarios: + +cause de l'interuption : autonomie, meteo, defaillance systeme + +-modeliser la zone d'evolution et les autres contraintres (systemes defaillants, meteo) + +(dans un meeting, on veut a tout prix eviter le public, les routes etc...) + +- initialisation +- iteratif ? + +####################################### + +integrer les gazs pour estimer l'autonomie restante + + +########################################### + + +Sujet Drone Thales + +Les eleves (1A ou 2A) construisent un avion et apprennent a le faire voler d'ici juillet. + +commande PCB +commande composants radiospare/melexys/coronis/ublox +labo pour assembler (labo micro onde ?) + +commande garat (avion, moteur, servos, batteries, radiocommande etc....) +achat de petit outillage (dremel, fer a souder etc...) +assemblage au labo drone + +cours de pilotages sur le twinstar + +On leur donne les petits projets pendant l'année sur Paparazzi. + + + +############################################ + +mettre les projets enac sur la page web enac + +############################################ + + +Pourquoi on ne laisse pas message.xml modifiable, pour les taux de telemesure par exemple. + +on split les Makefiles + + +########### + +manque gerbmerge dans les dependance??? ha non ca n'existe pas... a packager ! diff --git a/conf/Makefile.avr b/conf/Makefile.avr new file mode 100644 index 00000000000..91d39d7b32a --- /dev/null +++ b/conf/Makefile.avr @@ -0,0 +1,140 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + + +# +# This is the common Makefile for the avr-target. +# Edit the configuration part to suit your local install +# + +OBJDIR = $(PAPARAZZI_HOME)/var/$(AIRCRAFT)/$(TARGET) + +CC = $(ATMELBIN)/avr-gcc -mmcu=$(ARCH) +LD = $(CC) $(ATMEL_LIBPATH) +SIZE = $(ATMELBIN)/avr-size +OBJCOPY = $(ATMELBIN)/avr-objcopy + + +SERIAL_FLAGS = \ + -dprog=avr910 \ + -dpart=auto \ + -dserial=/dev/ttyS0 \ + -dspeed=38400 \ + +ISP_FLAGS = \ + -dlpt=$(PROG_PORT) -dprog=stk200 -v=3 \ + +UISP = uisp +UISP_FLAGS = $(ISP_FLAGS) +#UISP_FLAGS = $(SERIAL_FLAGS) + + +# +# End of configuration part. +# + + +CFLAGS = \ + -W -Wall \ + $(ATMEL_INCLUDES) \ + $(INCLUDES) \ + -Wall \ + -Wstrict-prototypes \ + $(LOCAL_CFLAGS) \ + -O2 \ + +LDFLAGS = -lm \ + +# +# General rules +# + +all compile: $(OBJDIR)/$(TARGET).elf + +load upload: \ + $(TARGET).install + +$(TARGET).objs = \ + $($(TARGET).srcs:%.c=$(OBJDIR)/%.o) \ + +# +# Fuses +# + +rd_fuses: check_arch + $(UISP) $(ISP_FLAGS) --rd_fuses + +wr_fuses : check_arch + $(UISP) $(ISP_FLAGS) --wr_fuse_h=$(HIGH_FUSE) + $(UISP) $(ISP_FLAGS) --wr_fuse_l=$(LOW_FUSE) + $(UISP) $(ISP_FLAGS) --wr_fuse_e=$(EXT_FUSE) + $(UISP) $(ISP_FLAGS) --wr_lock=$(LOCK_FUSE) + + +$(OBJDIR)/%.elf: $($(TARGET).objs) + $(LD) \ + $(LOCAL_LDFLAGS) \ + $^ \ + -o $@ \ + $(LDFLAGS) + $(SIZE) $@ + + +$(OBJDIR)/%.s: %.c + $(CC) $(CFLAGS) -S -o $@ $< + +$(OBJDIR)/%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/%.hex: $(OBJDIR)/%.elf + $(OBJCOPY) -O ihex -R .eeprom $< $@ + + +%.install: $(OBJDIR)/%.hex check_arch + # stk200 needs to be erased first + $(UISP) $(UISP_FLAGS) --erase + $(UISP) $(UISP_FLAGS) --upload if=$< + +erase: check_arch + $(UISP) $(ISP_FLAGS) --erase + +check_arch : + if ($(UISP) $(UISP_FLAGS) 2>&1 | tr '[:upper:]' '[:lower:]' | grep $(ARCH)); then : ; else echo "Wrong architecture (mcu0 vs mcu1 ?)"; exit 1; fi + +avr_clean: + cd $(OBJDIR); rm -f *.hex *.elf *.out core *.o *.a *~ *.s *.cm* .depend + + +# +# Dependencies +# + +$(OBJDIR)/.depend: + $(CC) -M $(CFLAGS) $($(TARGET).srcs) > $@ + +ifneq ($(MAKECMDGOALS),clean) +ifneq ($(MAKECMDGOALS),erase) +-include $(OBJDIR)/.depend +endif +endif + + diff --git a/conf/Makefile.local b/conf/Makefile.local new file mode 100644 index 00000000000..8914f769d78 --- /dev/null +++ b/conf/Makefile.local @@ -0,0 +1,19 @@ +DESTDIR=/usr + +ifeq ($(PAPARAZZI_HOME),) +PAPARAZZI_HOME=$(HOME)/paparazzi +endif + +ifeq ($(PAPARAZZI_SRC),) +TOOLS=$(DESTDIR)/share/paparazzi/bin +else +TOOLS=$(PAPARAZZI_SRC)/sw/tools +endif + +AIRCRAFT=twinstar1 + +ATMELBIN = /usr/bin +ATMEL_INCLUDES = -I /usr/avr/include +ATMEL_LIBPATH = -B /usr/avr/lib/avr4 -B /usr/avr/lib/avr5 +PROG_PORT = /dev/parport0 + diff --git a/conf/airframes/microjet1.xml b/conf/airframes/microjet1.xml new file mode 100644 index 00000000000..5a3378d816d --- /dev/null +++ b/conf/airframes/microjet1.xml @@ -0,0 +1,64 @@ + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + +
+ + +
+
+ + + + + + +
+
+ + + + +
+
+ + + + + +
+
+ + + +
+
+ + + + +
+
+ + +
+
diff --git a/conf/airframes/microjet2.xml b/conf/airframes/microjet2.xml new file mode 100644 index 00000000000..044f6f6fab5 --- /dev/null +++ b/conf/airframes/microjet2.xml @@ -0,0 +1,63 @@ + +
+ + +
+ + + + + + + + + + + + +
+ + +
+
+ + + + + + +
+
+ + + + +
+
+ + + + + +
+
+ + + +
+
+ + + + +
+
+ + + +
+
+ + +
+
diff --git a/conf/airframes/twinstar1.xml b/conf/airframes/twinstar1.xml new file mode 100644 index 00000000000..ce87717acfb --- /dev/null +++ b/conf/airframes/twinstar1.xml @@ -0,0 +1,61 @@ + +
+ + +
+ + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + +
+
+ + + + +
+
+ + + + + +
+
+ + + +
+
+ + + + +
+
+ + +
+
diff --git a/conf/airframes/twinstar2.xml b/conf/airframes/twinstar2.xml new file mode 100644 index 00000000000..4c278173764 --- /dev/null +++ b/conf/airframes/twinstar2.xml @@ -0,0 +1,61 @@ + +
+ + +
+ + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + +
+
+ + + + +
+
+ + + + + +
+
+ + + +
+
+ + + + +
+
+ + +
+
diff --git a/conf/conf.xml b/conf/conf.xml new file mode 100644 index 00000000000..2f89ca9835a --- /dev/null +++ b/conf/conf.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/conf/control_panel.xml b/conf/control_panel.xml new file mode 100644 index 00000000000..ac78d3f94e6 --- /dev/null +++ b/conf/control_panel.xml @@ -0,0 +1,105 @@ + + + + +
+ + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
diff --git a/conf/control_panel.xml.sys b/conf/control_panel.xml.sys new file mode 100644 index 00000000000..990ff0818a3 --- /dev/null +++ b/conf/control_panel.xml.sys @@ -0,0 +1,84 @@ + + + + +
+ +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
diff --git a/conf/flight_plans/circles.xml b/conf/flight_plans/circles.xml new file mode 100644 index 00000000000..ccfde0e8b11 --- /dev/null +++ b/conf/flight_plans/circles.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/flight_plans/flight_plan.dtd b/conf/flight_plans/flight_plan.dtd new file mode 100644 index 00000000000..562d26e1d6a --- /dev/null +++ b/conf/flight_plans/flight_plan.dtd @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/flight_plans/hippo.xml b/conf/flight_plans/hippo.xml new file mode 100644 index 00000000000..1c578029e90 --- /dev/null +++ b/conf/flight_plans/hippo.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/flight_plans/huit.xml b/conf/flight_plans/huit.xml new file mode 100644 index 00000000000..dc93a0eb1ab --- /dev/null +++ b/conf/flight_plans/huit.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/flight_plans/muret1.xml b/conf/flight_plans/muret1.xml new file mode 100644 index 00000000000..ff095d7e8ab --- /dev/null +++ b/conf/flight_plans/muret1.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/flight_plans/muret2.xml b/conf/flight_plans/muret2.xml new file mode 100644 index 00000000000..07f9ec994ba --- /dev/null +++ b/conf/flight_plans/muret2.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/flight_plans/muret3.xml b/conf/flight_plans/muret3.xml new file mode 100644 index 00000000000..07f9ec994ba --- /dev/null +++ b/conf/flight_plans/muret3.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/flight_plans/procedure.dtd b/conf/flight_plans/procedure.dtd new file mode 100644 index 00000000000..6a21c87d21f --- /dev/null +++ b/conf/flight_plans/procedure.dtd @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/hosts_wavecard.xml b/conf/hosts_wavecard.xml new file mode 100644 index 00000000000..c1bfc79bbc4 --- /dev/null +++ b/conf/hosts_wavecard.xml @@ -0,0 +1,9 @@ + + +
+ + + + + +
diff --git a/conf/install.xml b/conf/install.xml new file mode 100644 index 00000000000..630a53866c0 --- /dev/null +++ b/conf/install.xml @@ -0,0 +1,226 @@ + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + + +
+ +
+ + + +
+ +
+ +
+ +
+ + + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/conf/messages.xml b/conf/messages.xml new file mode 100644 index 00000000000..897d239767e --- /dev/null +++ b/conf/messages.xml @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/radios/cockpitMM.xml b/conf/radios/cockpitMM.xml new file mode 100644 index 00000000000..e6d87992c3f --- /dev/null +++ b/conf/radios/cockpitMM.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/conf/radios/fc28.xml b/conf/radios/fc28.xml new file mode 100644 index 00000000000..17a80d4d701 --- /dev/null +++ b/conf/radios/fc28.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/radios/mc3030.xml b/conf/radios/mc3030.xml new file mode 100644 index 00000000000..e8840404083 --- /dev/null +++ b/conf/radios/mc3030.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/conf/ubx.dtd b/conf/ubx.dtd new file mode 100644 index 00000000000..3585d22e8d4 --- /dev/null +++ b/conf/ubx.dtd @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/conf/ubx.xml b/conf/ubx.xml new file mode 100644 index 00000000000..1ac484b8bb3 --- /dev/null +++ b/conf/ubx.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/wavecard.xml b/conf/wavecard.xml new file mode 100644 index 00000000000..431efd56383 --- /dev/null +++ b/conf/wavecard.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + 0x40, "REQ_WRITE_RADIO_PARAM"; + 0x41, "RES_WRITE_RADIO_PARAM"; + 0x50, "REQ_READ_RADIO_PARAM"; + 0x51, "RES_READ_RADIO_PARAM"; + 0x60, "REQ_SELECT_CHANNEL"; + 0x61, "RES_SELECT_CHANNEL"; + 0x62, "REQ_READ_CHANNEL"; + 0x63, "RES_READ_CHANNEL"; + 0x64, "REQ_SELECT_PHYCONFIG"; + 0x65, "RES_SELECT_PHYCONFIG"; + 0x66, "REQ_READ_PHYCONFIG"; + 0x67, "RES_READ_PHYCONFIG"; + 0x68, "REQ_READ_REMOTE_RSSI"; + 0x69, "RES_READ_REMOTE_RSSI"; + 0x6A, "REQ_READ_LOCAL_RSSI"; + 0x6B, "RES_READ_LOCAL_RSSI"; + 0xA0, "REQ_FIRMWARE_VERSION"; + 0xA1, "RES_ FIRMWARE_VERSION"; + + + + + + + + + + + + + + + + + diff --git a/data/maps/muret_UTM.gif b/data/maps/muret_UTM.gif new file mode 100644 index 0000000000000000000000000000000000000000..ccec3aa29c7b33eac7d44ee8b3b9c32b56c51a47 GIT binary patch literal 77403 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 literal 0 HcmV?d00001 diff --git a/data/maps/muret_UTM.xml b/data/maps/muret_UTM.xml new file mode 100644 index 00000000000..939e1953533 --- /dev/null +++ b/data/maps/muret_UTM.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/data/pictures/cockpitMM.gif b/data/pictures/cockpitMM.gif new file mode 100644 index 0000000000000000000000000000000000000000..d478f10d33a1b3f657c93ac8b200bf5a918f6120 GIT binary patch literal 93045 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 z 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 Sz`a-Z{+(_9mjs1E*8LxUtdKbX literal 0 HcmV?d00001 diff --git a/data/pictures/fc28.gif b/data/pictures/fc28.gif new file mode 100644 index 0000000000000000000000000000000000000000..0f262d3f7c8d6c574c2f59cdafb167309d9781c0 GIT binary patch literal 160341 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 z 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 z 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 literal 0 HcmV?d00001 diff --git a/doc/checklist/checklist.tex b/doc/checklist/checklist.tex new file mode 100644 index 00000000000..4dfb9740495 --- /dev/null +++ b/doc/checklist/checklist.tex @@ -0,0 +1,102 @@ +\documentclass{article} + +\usepackage{myfrench} +\usepackage{a4wide} + +\title{Check-List Paparazzi} +\author{} +\date{\today} +\twocolumn + +\begin{document} + +\maketitle + + + +\section{Vecteur} + +\begin{enumerate} + \item Cellule + \item Batteries propulsion, émetteur + \item Verrière + \item Scotch + \item Radio +\end{enumerate} + +\section{Segment sol} + +\begin{enumerate} + \item Portable + \item Batterie + \item Récepteur + \item Moniteur +\end{enumerate} + +\section{Prévol} +\begin{enumerate} + \item Sol +\begin{enumerate} + \item Brancher récepteur + \item Récepteur ON + \item Console: \verb"rm log_test" + \item Console: \verb"rm ./new_display" +\end{enumerate} + + \item Cellule +\begin{enumerate} + \item Brancher émetteur Paparazzi + \item Radio-commande ON + \item Radio-commande, tout OFF, mode AUTO1 + \item Brancher batterie propulsion + \item Radio-commande, mode MANUAL + \item Scotcher Verrière + \item Chronomètre ON +\end{enumerate} + + \item Calibration +\begin{enumerate} + \item Avion sur le nez + \item Radio-commande: commande aileron +\end{enumerate} + + \item Contrôle cellule +\begin{enumerate} + \item Armer variateur + \item Mode MANUEL: gouvernes actives et dans le bon sens + \item Mode AUTO1: gouvernes actives et dans le bon sens +\end{enumerate} +\end{enumerate} + +\section{Décollage} +\begin{enumerate} + \item Console: GPS 4D OK + \item Radio-commande: tout OFF + \item Briefing avant décollage + \item Plein gaz +\end{enumerate} + +\section{Procédure} +\begin{enumerate} + \item Montée en MANUEL + \item Dès attitude de sécurité, AUTO1 + \item LLS ON + \item AUTO2 ON + \item Poser la radio +\end{enumerate} + +\section{Monitoring} +\begin{enumerate} + \item Altitude + \item Vitesse + \item Modes + \item GPS + \item Batterie + \item Temps de vol +\end{enumerate} + +\section{Atterissage} +\begin{enumerate} + \item Radio-commande: tout OFF +\end{enumerate} +\end{document} diff --git a/doc/user_manual/Makefile b/doc/user_manual/Makefile new file mode 100644 index 00000000000..8d7766992fc --- /dev/null +++ b/doc/user_manual/Makefile @@ -0,0 +1,47 @@ + +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + + + + +all: fly_by_wire.png fly_by_wire.eps overall.png overall.eps + latex paparazzi + makeindex paparazzi + latex paparazzi + latex paparazzi + pdflatex paparazzi + + +paparazzi.dvi: paparazzi.tex + latex $< + +paparazzi.pdf: paparazzi.tex + pdflatex $< + +%.png:%.dia + dia -e $@ -t png $< +%.eps:%.dia + dia -e $@ -t png $< + +clean: + rm -rf *~ paparazzi.dvi paparazzi.pdf fly_by_wire.png *.log *.aux *.info *.eps *.idx *.ilg *.ind *.out *.texi *.hind diff --git a/doc/user_manual/fly_by_wire.dia b/doc/user_manual/fly_by_wire.dia new file mode 100644 index 0000000000000000000000000000000000000000..e94ff56808607ac1807358eb0b6ee280068711ed GIT binary patch literal 2153 zcmV-v2$uIBiwFP!000001MOYkQ`(piFs@bR_PdUx1(JNBG6@I8r~X%jvX%;eQU>pG5a-gG*5cX!Btm^pD6A>X@2 zGq>}fh)6$#HyhrBQ5-p5 zybg02hQ8|rDN-EWy5(_ZBgY4od1@)7y!0zCj>8J`zjFN9HVa7K{$V}4TH++~#!nmK z(ue{nciuR@9^PxZU8(d3P13)6Gw;%O^Jeb_ab@Bk6veCV^Q|{_XB&2Uc}#(%>ugwO z)52HBZM%Z>tre0=AbjV+jWY4?7xdK3?r~=HkM2Tv`Hwq_v*lgHPB3<&ar1TaeR$tm zuq|+{H$G_f@$&6iT@VNZ=d*TyZo(h?C=hAe$XZ?U`d$`^zr0+P9>M&bVB))T1k3@# zYuB4x$Hn3~GD?b{U59tW zoDgU;)#Te`XQ7T;=;y$_k8>21s4A$5SesB}Fl=a4(gcMd(_*$%Q*6;TVTdzkmjjLE z-B^KK^<67tigq;#1eeCG#Ej?8*bAGZFo>hjZ@|J_54NEoj(ma(ikKe+=fTVOvmwXf zZRCMfc2scI3?s)l@MFU+1G21ZTkBgHs3hsg1ACl};N(jR2*SQeFm>~H$qH_#*}dBm zC71K6@e%nPMpaz|#YkYwZgXS_PPj%l0Z5#uNCc3&lp~u8t4(Y~O`GfFwMrJb3cqng zh1yy+Q5<+yCJgoqu^dxYVTh6}VnO6$m{g|%XE+ulwVabKBgkR~mE6b7vT@C`lHJjV z)Bf=D>EQU!M*8}Xqi@-(^M0>a(_T5+D^S3i_;k7oL)C3SGG*Cv)4-Ogtq)y&JBgApMy|HiE<$5&EmS>( zA&_D5B<(bb&Im&Su@s0ByUQUl!7RSOAPOSAXoLPbXa_Mu3UWQ9xKvxRM zaNIBoB>zD62B6;?N?x%k3A4 z77sB8ntKRNk!f0de>{ZaMOP7wt(}Bg5K%h`ZBC-+1#VvJigI%mF_7T0O3tayQ)z(V z)Zj@2iy^G8tg|p-%MKQ1m?g{eqc8}DZZJ-kv-={853uD(!z`Jkg4&F3Rwt9!l(>sf zANGG!@{Up2ErfeGbfPG{TNS>r0iq~*=)1w>>DgPV5F78$mIzX>Kz+f|W@`*I|4s86 zY*RKzf+@yf(&pG){1*J-nx5r+mSmr$%b#WI-JL5Fs-*BnlpEg|Ll%3E${PTg)}r#p zY()6;RNkssVqJOD#D3~+S#n!tbCin1-!6^j$mGOK2#_0HP%B#yk?#=TV;5;!onvc_GK>m51dx3atwPwU*FzAFlRg$~WAG43&_UW1CSY1j zbCglE%>0qESVU!su&mFlgN(fjGAc4m<=!%8g8rp?$Tf4fiLgWh;~=K66YkEC1r@e# z%Pd-F@lcp(`EaH-&ELf|zj>2y&ZNjdMO6hRrEGMk3X?>_vc$v@#;SG@M)CDLLHhMaNiE<=I2xzbugh_!=wD-(J;0xV~GrLtD-nP-u8m{r7;xm&Cwu(T6vkku<` z9Rf53r^h#?k#`C#w^e1B7*@R{TIg#Vg5fp8Y;8SORl%S1de0R68NexI5&S9n+ns~I zwRJu*6ee0*&#!Dfb_LdB&mFC1>k&v;*hrsvK#Zvs)f#-`g6q3DUwpQM~SM5 zEF#i}G-AxtF4eTf4>R_H^3*EasumwIi=8OmXy;5>xIR;V4DwTqooqICt^IVgif)-C zT(cpe*IhcufatuR&V*F5O)$cHQ98JCJb&g~xpiVtx`dFcX?0$pH7}^5Z6`fH!P+Yb zB0CVlKTd9vKZTahqQuW}a+AN#Cx#M*Bc=M}<^`SH6!EKaPYS2mX*!08QLFVlLSnKu zFDMd1c8Id1WBuh5Z1R{H`SjN8i8I5K-pSXaLH~SyeRSG8Iq#nednX?U?fUVIDr8)! fmNgr1=TFk>@D=~Z5BigNw%h!J`; z&DUq;qFA3lJo^3R&kyX;pC3*Bv?#Ja!k_ahTTS6N)*HNfcyv+Mmw!Aye)HxESzd3l zx~#~u*pf|t{NHT3%#LBv@zJBnbo${67FnI)XS+|cx~_`TZJkfo*(!f{bef(0>%1zr z>%~#CYPasJT$a`JDqB80`tG&=@921Uo8w#e`N+L5v&+1?d)w7zxhY^teSNvNq~kvL z{}0yeR&8L>`ux#%C-S@IEA2jbf3LPL+DW8dWz~7H-ltRL*|K>MoLJ8JyXWxvBu?X# zSjHz&#V1j0UR{<|U1deRPv*2NmwC2sDAmB=zye$^_<`ZxF_L`16UF?<}e`@1**B!cp=A%1G%9B`T*Lk(O{SWWe({%TYcS8Tc zQz%dWnV;3W?Y*e8^&+bl(+ATZ%C|@FwJqq{V)5|kZ}j%#xw|2_8Gz5;^SLZ<-lOD% z3+-l#Xd|t=yY>Bd+C}E{^e&YO_K~g6m-z}MW*bTzD9yDXQVB7WhA^gOKg#(sU%fgj ztM$I(9eR_Vl$Y!Mjo#=AMwkxT%SB$CU)0?cBqPEx)sMnySuOJFQ=6cK^}GG%qI~nJ z#RfY-SG1$T@q2;jEg>vaLf~;&MWy`$A_AnuJ5pXo;B9Ne7|IA`+QpQSVYYdQkie#- z&@8TyaL1F394aF`R7QYJXUq|$A_@nC+j}B&#)+D7ORS115P^F-L=qSZ+ry{iz!9$E zv*~TOWe6^PnZK=D*i?kFDOei7LYBnDa89HVZpI98+}Jr|;67@GoW$X}AK@CG#~reF zE#W(By56e~E8JRlCj#;F@_KrDJ$+MDc}FPXBkn|(^H{FyHk$bPZpq_vTNQcr;D>U# z_|Q)Od}4b-(I$9Agb}&TX9x0HA3{EK)kf;Wm=^W_g%Qe@#rb-bx4)=-y||S)m&LNIlO7AuV{N>a6xkr~@N2Qo zTi`(lYcjAw!9kLQMFj{TBc$OoF5ss#b5J!#af^hVQKBE^vvR$Dm9H0mJDAAUoG?X`Nub4!-;(I zA@wz%){97w=;qGx9?*LdLp>gpI}mRGqRpOqe)lia3xF=nSpZVirH>m;-~a2wBe^Gs zEx*flzbkM5uGmky-1$Po*j^?WT=fhrqo}wliIvPCW+@*+Kv&WVBx^?zLU{4)Niw+t zOs+i^OZHYwcgzO6V+;%*6jvVAAESghF45f5N~k%SUDRl#E8+V7!@?@t)Oq!PK!*6B za7)cb^26GgECEAIN6dxW3WPSkhmHyg7#~wJN5EFl#5P>go8sXfSo`j}7P`J^m+?7Z z$+rmiE%a^wfPRi|!RYN@)=48F%b3}V2_6a&Ml68E z{D!0+1#~1Z=xs>saaOV&@&I;#$pKXk3o2=ayj|_*|VpANKnxa zDmu9?!9`TKu;G2e#y6PDm{tS;gtBtR3UCP+5f1PI3qP%|?i)doZ(d!M$%^RR9lXzB z2^FHEf*T+nY2(9!BNly&cd&o~N%Sp7gA2*G2#QDA1PfoZ#+TC}d<*mkbwp4ApOZUm ztwb-vvAHB#P~#&GhEfx`(6?oWt&0SHKSxz6^ZmBUdb^Ke%kY?ww9 zMwWR^7>K-=C5<{540Otr7elSNi!Ah=`)pmxoB4QY;K4UM@Zht$ZdiLq|KN=?T{sY2 zI>&+VeV30Di${1OcD=$gNClkH1IVSV0pwL8(qHXWz7k{0s%0<3P*w7OjT9_;WL(C&#f{W;+A`Uw) zqk8^8#QMHYHP;;H+5jZTsN-xxw{_4weNAGXA;v}TV`uK=L>nGl+NC7;h`uR2X5?65 z8HezLD*V|nN`bwrIdaoT&W3sVB8{^N&|9J6~9 zE(G908;d1eh>8org?d60yR-3PHn`&1c4#hu6X9{ez_s<^PnB@tYsZB)7E8DgH7%kC zID_08)zs0(ZM;L9L3y=d6(9px4n{W}H=0GS-|gCE`%=~P1G;ah*ER)4Q(*K<2S&{R z;KhuuC(|k}a_z`q&;y}M8Oib5jv@&1%d;2Brs-L**vet4WHG8y6I`<>Eb%@^)Sd?* zLY?Bi(t}DnmkI|6_>75DG> z%&Y4(cfBWawDDl_yrSX;8)6;=({jda)WaWd$e5;BO-LfC^<^q<6xcEOF{`s_cJ}XW z0e6R&(0uT^SOR1qqD(f$>JqlQDmM6zHnYiOom~~@nO{5sibcI$^CY=kwDJtC5qVea+$4X zlXYIdDXV|YCh!pR^`^K&Bw1ZIQ0)3oqd3gAXHYKExy)wZd4Q;tP0`PDv(YaAFg!VsyOx<2h# z^doILcGFXZ)S`Q$VCVqDgRx4pE?@f0OMoFa}Pj^o0o@4 z%SJGR3t7Ry#+N2(+;WT^8R43&i4r)dSI#8!X8rxg1xxAEzx-!X;n~*HG>9|AsBdMs z6se2~6Tx+@nl|#QNX#Kd3gCe^-abe3LE}ub#@nZ_{LG3aaB7O#=YnSaScNq+oG8id?ks>0IOd2)7vD+*Eo^vTa=+ud?+vTPB;Q@1Uc79!#!K z#8U}W_!;3iUfF?9Vu|fNbN#Tfh(L)qlnl18F!+hr64<483(H|=6b!P-Lk=oXeTTvv zc1}UxyAOOyLBvzWhj9c{a3v(exqQCc6A&$?P~c&F5(!bw#hl^f7b%fZfnh~tKcHii z?F+f|^22+J>iGPCAs9MY&(V5D_3fTf`h|K%l^8(hDs9C~av~#ZT*Z(M1A84bF4*lt zYBj@Vb~=Wp(VyPdWIFk6yE@IQ>Ev;>U7ua#Rq7%NFwr}BFInkPy?Z%;J)%$pOC4Y= z9|juOW-L5LSSIEi2a+m@gausRf6Vy)uP47gJ^5|=^OIj+{`CCGZ@;9r(Y{d8#d9f( z5fv=d5YCIAtL2D}?9)qPrPfG^3P1~Hh@hD9ba<%v2+(4)%&zhT5WNASozD_LL`mDlr3N&}kj> z$P|DFp|nFvlvy|DoB%;H!%-sIx-n!sicJcC`(dwuBN9s}*(lR7_Cu zluAD&fk&Oj0F>yCk~WUb72(ope^!ZeLdUFD9AuM+R4Z~~rBgOy#i9f$lj>d+Ee@pT z!Yd0EVTOT$A~4|L;?@>2&U~qdvP{lDpsWOw_g~Q^XSt^e+{I!^yG6x@@BxZM3t`nv z7(6jL=46uxlu*aHlGb-4D=~MV@SO9d$Ciq*)CN9(JcXU*#Z@W`49nGZabALqsE`pp zxX@vOozIvAfu<(*>_RZ`z}0+f(0xEL_t6oA|;?w0uq9RAR!1yt0)};L#Ik8h@g}-LraR3GnAl+ z^bFld58XA?z8=^6zTdaw_`dzy$6o)qW_aR`E6(%0u6y37smN2FU^sz7p(yVv+)+oN zj?$x0WLn1#!#A_5S4ZF)<2^98-==vx_jsL1DCkDLFe)d&nhIB2E*_{ zK1rK_o9usaUXoHc#3_A7YW(l($L<_S)xCdT?atAAFOrgcl6pAW?gSoD@Chmky~y-L z`lL9=Z%qqg=wB`S`{#PfqNC&Ea|P1UY#c-! zjPi8Gy2Xn&yS{CY$`Yj%B|Yv<*i5Na1p@wT;PLjQv8QxZcNA{IM$fRz%9I7gKhIX< z2#I@GpN|;T)q_n-MIl$jR(Qk7}B(2T%(3jjJFbq~%nL3V&b7LVawBc`{wiK0+zH#`aWcMX)QDz8(-TTa52mc$_clF)Si zk^<*Ii|8kn!CHExG^vKEGuF91l19s!?_97blo@OdhX2kKu_cY@Lpdmh8+~drZ7PT_1*_3fc~^x;Tx)pp9o};`H!v~wG2k3^=DxdS9Y!VH zO*KnuJbJsxCG4|)J>_?MwlMdoe$0UVT#TBDN)JQKZE@1)ge;gOGb`>zf?5oJ$|fpk z;jgQ{hb`t_OfYHeT+-CEDOC!yr-g2Q?HyR zd0*4BkuDns?3!w3MnCp5xF6lFT$q=$%VmZXX^$E>e zKtgVS8UDFJZ0Q>razj(BFrRW@OT?QP^A*z?2}c$7vtq6f_cRZo@V6$3q;QSshQWqbM^rP6i@fuwtw_H%f|+UtSo$7g$k8KwYrnntx0+uc6N}h(RmYey8cu zcDIVjMWF=V&{CqSE}H&)_k&8>>e?f*hZ;D2H!Fdhv%(Q#x0DM7FE9?qkRnj1Bttng zSx0|7V{qMcZ1A8Y16C=;-s&O+DoJonmP|lNyyA!3$3v*TFj`7HErxl_LZg7$1m&~w z^p3Dp|C=Gz?6t>eALm&vG8EC3l>CC1p7ExPh6CMBune^l_1%vtVota@wJu8L^jkt3 zOZIvxG^xlcsSDsIJ`_F^h)Y`B0H8 zzL>4ArwOZ8&Wu8RxJy7ax0Q$ManKG7oj|MyI`5M)&VDO~^hz%2w^1!qtF@`RxC?IQ z`W{#b;Z*H3q){qfdo(R4QBwYIiR211lvK5o_V>%5XasW|NBLMy2l?~TD|%`^E9^O4 zi@DS=A$Qfe*=#(AG~@pGE%+Auhh$8wECqYFlhw3+Xq*zu&xA}%IaX735Ag##*szGx zgLs*&6TbEIYmt*U-(yUJ*upzO`BPZ*!YthWzS4|na-21fdnV@Vxf8q9(&pN(d6%~X zm?ztUb@_xn#G4BW0_3SrIWDqafFWu?K%F?__J-!ch5PzVjs?~wn#oGMS7O}TOG78k zyF9FU&)1{A5ffDTvm)^3{!kb=TkBt1 zoduedjwF23HOp`%T3 zootScay$-E@-I$?zS<)Ai*jtFBb@;U@%z;wk8JcMoF3W}Z8<%H5i+I@8GN80?VTVBDLn9bApN4o(Sj^A2A$Z3r=xs?$;JK(E~cU4EB$n?hNi}_ra(|K#jQop z&1HpKE0(-V-}^cWHK9NVt+0dVGYa$0a@CyWGc2zrXAIAqUR?CvnJ@KT4l;VGsV*-4 zK4+jS)9meA|04TnoO&W|+dvASjSCt&>OSaG!y>%1!S6{|gLCkyK^@1`s>ia6#;y%-k%+)JJ zItxAeOi^sYDU)rv^;5aICa9!t1;V#8(oBR`i2kR%eVhUdU41n zk6o5biDUk?q1$v?z^U`<0}tH9@>*uzO!G`vpbb=b96>0g1cg z?AzE zQZ;*`$Bl=)-KUw3Im#P7uRahlVtiTI2Jx0z5GDjlcZ zXEdV8WVfBy#CgsSi|_g_SgRj2ij+F zLrNH%SlteHKYPNjE&imRw7mv~DVd$i`dS~upu7=)3n{Jw9dS>e3^8I52g(O4=|PF(k&D-^gH2*INZg8vEM@Q6Q110N57(M)s!Pn>Pb z{^StBo6S4Op1M>?vP|EDkd8t{(8<{Ed-it8Xf+WiA?!qCtyo_(a&BOh2v`uD7a~;L z4TKRWWEaWfzTRk7dyexzJqQoi{AlfwT9|j(>~n;+p$@#q$3$dz>AUvB$9~8ik49?z z>R7xzWMUA6e{pCefMp*ZaC4T+humeoQ=2g&xtSLRhcf@O#G#S2u(JQ94;^`G98V1n zODX-IJb&Yh*cp7zea@^KvHbP#<`5+GoAjA$*|LZjI$`dCnzk8 zEw$HIDgA}TdBo(bGt@X}*{LmE3JdscyRVAAzljz`hV-R7^dbWzHa5mqM#E2d@eW=B z*>^MgiI-;ag^z{Jugv#|83?96GcyGgjq)i8eO@-HzkRnRS>vgE_hr>0}%4=4v_6vIt>+Cf?2O*N-)t9`iypjQ|Y-h!Y%ICf&O5%(7=2?!5LK>PZI z4PBSWz(Yy@y%#jTwn6D)&@YkD*E*F#mY@{i5oM$Nu?C0M&}{#h)+_Qrl@wj1cWuh4 zfn9HPmJAinDocjfYcNQN_b|>P-k-L8+WD}!lNu!uNJ}ZzUQ1qO=8P`L5G^vr0^{pU zuk&44u%A*E(a^QL@*w-6cV1;T1?u$#&J3lS`f>EeTZaizt9JIbwH;v@@pUqU7Lnl{ zo)~-BmxS0fFO~T)%ut{hYGITy**$`~3vEx9sZqp>|3TPWO%jFYfK5=={_a?gx%ctP-~J21`n2wCz8j5nS_?ysxfk9KeIpqgv1A4|UiTRq zhrz_Kxyhoj?phH;>se|XN5n1GvSNUqBR&4SPVr=nLvE%fqunuYSc zP#2?T|KGg+MlvI6v{t3TT3-OQb`!P=g$j+VG()}4=s%Bo4Segr==!M8u5URfQNMn& zMd`2LWvHJS9L4)pZa3TJRgz|Ov=2!YDiEYXaN{48(pBPP9O# z&#xvP?EbEP1;LAWrfPpZ-^gVIiCIZlpi3E^`>Tn=o|}Vz{u-rk4-5vs-t_+&s&d0x zr)Y$ZesDHZF#taS?sbtY&HS?(#}6B>JnG!zZ1wmNI#mXDTu)?Uv5~H-L}CcDUS8_B zuRh7cSXKAdeNx6Llt3IT$PIAwmmKGg5b)^(A8rd2rEgset#lV+aF)S}cGz2mQ%YN{ zbsTTaT>I8jBORN}6TYClnPzfYYDlERKDL3ZXkaEVUtMn9#=}sW|MaJ6w&YlIL zT8JH)gGs^ixJk8FHUfSY8I7mae4-(c3Q5_N^Qvqt>XTXW(<9 z>gY3H@FaDNr=sam?ox-^&oL&V1q_!}c_OnP+(O~4!1$u?DrD8>d>ptQ8x$LpeW_T} zirwkwoTyk}tue~X8!kBi`e$tn*;8xJLe5Rq;p6%T{AVzSl2|_)obnOsOpu9+MeEqx zCpt}|pIL9kprlj@^psNBvKhZr_|2Wxmh`vn28-iMizXftl+tA=u9WKII3IYdxH_=i z*7YU#v4X%~q`R`0%$29=?NUr3lobyKxLuBtn*9?r|A%Y>69t%{jxMRO=T4!}66=)q zr@4^sDu-7@94%?}$laTN;hd%T+e{jEE(*wmC7kZKhorR@E$d&jiWufU!JVCRgVc4c z13P*tP3*_JQC{cvoq-R7gwDtZeJO_hW~=sTUF|o1ASo{??8yDk{+6*yA;E*XeJ0pr z1~{3|`l{mCO${5XM)S)T9-20)z54?c?i~qUXfHp;pj;(*;bA|O8V}+jQW+LQIVNiF zki3XaRXf1mQMNpXf4 zl7!b=0*;8g=-k~pue86cwEA7gCJ|3EX%g4ci#_g__DQ6f^WvRA8=s#1%`aTvhxq|u z$W#dwzfX=*W4OaT^d=#F1u!b!Ew@VH^G!VNnd#M)7h!!0Fqv=ex-3^^bwuBZ{6pA0 z3Atg{BAn6TE_=iEnsFlKz3UwVUYZUG@)S&IBIW1#ZUR&~nM!PsrZ-3583;`OVffYV z!5rHx<}Pc4LD$g9d}@@G)(!03`4|{byzi&RhC!D6OQuZ`Z{9WMW%MXyzLm!OH6y_5 zO!HEJ1}nRhsKK0YMVAbPPP}&|oocY>ylBj9a$`eP{@q~qTUM{_HygOSANS9>>9Vfn z1icYgyIwjax{Zaj$CJc|>pW8#U@6Cku3Z%>PDonrSrm)y+cf-cQ^|4^B`Ry@VN^qr z)uUV+Q9rhIrYl_Uyl&#|-TZao_Lo0DBsXSR=-W{S_I^>W(JP$g^z3fgnE#?|_#EZa zcSnpXnWh?IgjbIe@xig+Ii+5E^ReSQ;g)AbgH8{g=h!UiBq^RznfIhbCB;YV6rGLr z&9=VPoSJ)FhAAwF7Bh_|C39QO@VOR>;j+c=YltN)Rbi}t>a?x?q_j@X66;@o=O$rf z;&>IWWwD<_H^XML)&DXYH%+CE7784D9!SmT6^IvmH2q0WN7XWqEWhOAxb6A!LYc-p zl6ezZ+f3o4wpHERs^M;c}nC3%EvFn&?8Ta{?otX0N@B^R3Bp6A#Nc ztLin;KT~_?@Z|Q%j@1CxY~vRj?>(+NJg{1no@2C6am#8npwG>SQMn{*n*J9y-kzU< z&miov_&n=l^5O-rQ9S1p$z3{OS?w#{V>+#W#fH-~{q#EfR^@>nX)+AIX;|=E!02iu z_mz=iMem^=GVYeTlLA_1RtD%y17gY}B0ayWrJb^L=sWRiBpZ#q)s%X6|!i42m(L?L@rYkIZt**wUS6E`XU0dIH*#|4v zP7H-MdRB#cSjBX6?YSCHowbJ%Y3xnq^OPvFq zvMGpm);CK;7+BkFt#i7{jq83+-s3uii(UMUB=bUo4gG$xo;d1-+fcl&lU1X{hUNv| z<9KX&2H|!pF;bCvtJ`f}i(0Ct=XcfzYppsVJ<{|`l+Tsx$&5Zu_e~A0l?Q^U%vHX2 zh+tDKPTKsYxi|b^?fjdCKPi=`(jMDWWs}(_9eQaA@m0Jtwq+;F= z%`D9#2c>E`pF6zwnjSZ%vB;b0>2W)AGWn=s=z9(OX5+>~6VJXHlB*rCGU} z%CcHhIVHu)*?OG?HT*>n*}cp=-v~PYo$sLWC&Y@U=BB$7b##;y@5+Fm<7unS=Ho)d zq}tI>OpV#Q2HupnD1$zZ67W;KCbb$Ko165N(@E~tXv}#3B%7*V=rwI=-j#~Sb5~E* z^GiI4`+@Oy?#BDlPMmn1koQn^T6&IZf+|EJXUh9ha{--ErrW;vaU`7JA14={iL~L0 z$6nL6e`B?r>5C43URUiK*(iQb!-1ps=h^7zZGqBt=^mA7swL=451dv;F!3}Ak4RZu z%&|*_@A3AvrguKsC=i4P(e2H`!*PyuO9`!B8-?HfDI0MoP)Qk`XT6>nYc{VQ(<(M4 zJ!jU_6sZ^1$?UP`sQjsAsQ0)&CftB==b*n4cS*MO<8|YPa8AG-i`xj7mz7aGPIl0D zXCJt{eZfDw{9S(Y~!3E8$6s z6LI*GcUbgp(ds4b=BZc{_Md%n#c3>x6nOs`j}S$oOrFalyAJePW`f()J?3+&V#Ea6 zib1D-CeJkj@(lOMXwy`)O!-Wjlw5R!JZ>ZBgqbNJ+tpH~Mwyfh`uB$rW#+pHat+_P z_Z_pR(z3TZGv~J_cXOxHX_ftVMGpm^*|E#`%8C2pe_W;gb%{|yDns8}>r1XP9r|h8 zk7)X?IzPg0RAkHapD^fUyz2XNopxABhT0+GrvbZFZjXK5uAaVqb6Unb&(%++IWDBB z*xjebrW=YbmXy{flm{ZzG|?I>or9VcEys^$Y#*>)loeL5Sv188e;tj9;iG!Hm!pBZ zv5}o0n3$Mccg3Ul>`uaAwJLj~=nU8XPgAoS;~QahR^44DwJ|{f+8)YJRO6pNS}Vs| z?4LT68@0>G&}{Em5?VRoYtrr<+0`peS*PXkMtZSmnlZ#WwT~=YRH!-o!Rmo6NdsL` znRAlJl4=q3=kNGm)RWjeamanN>K(6Nmq#=V0jbbiJI(aa2zSrKSkID#BS=4#LU^>IlX{J;CHjiGVJ)sCBdyw5y`bp(YLt64`U3h=rEzGLZ zXyY+dZYm$H;&SUp`ZN7JaknU?YFp(xovzB~2c6A$v8EXCeL}s7Cf4giTVP!}=hc#u z@oClUmoX<3mx96RWjQgS)O73$+9X-b8v2`rC?A;)yM}dFR?Ng7+GMN_p zJKrOsDhyOdmS>o!me&{11G5Ie5CJqKdN+A(Jw6iWa1g$|<1 zht{dpZI7Ke;jkyDq~`6NWB9>TqKY~Pt=mjFu#tJWz;vfJsCD5j$xRHTggK}NxM)wL zH6Gu25Fy!EtTW$5bhWg>j;8xY{jIBsMXI!Y!L);@+lNo_GV`Wa`%Yvrs7jO!=)`zh zEpy7Bq1$LgKagH#k0Q^ATKc|FVbfkya)%LKz+_;^?Sz(U{AjeY$BQ!a~ zIi~EH-m^L&Y0q4ibrz~V2X{uiCEDQdkEZnnnSK#obWQ8!Sye$D!P)GkBH} zuKC-OK&kfDW5OhjO13UbG*l+cAmGY^qK(L2m)YIpQ^CR2nB(82#iFuB7}nw8ebF## zb9Fm$nf$PfF-05feZ6+o<7b;^IunGOsiYP&Hx_(2`%j&{;aY0`li$Lr5o!RZ2aD)g z4!tr|i9I(+TQTOD z_wW>opHa<-bop^y?n+?{_^#XX9Ue zMmKy%?s%daUYWHZ<%vFFDUvh?=+LY}(!v~|_9K&p(UCF~fry$u$nZ%Hqwav#m4Ttpe;?or+?l(X4 zO}Jlt{p+aftHvzLY)X{q`%2U&CkLpj=<*nr5W)U7}MK0Zi22};HUmxG0$;sw*UAvY} zx#md*i>y)C><^W{NUfG>b{Ht#MZcFie zx!Tn$EL13znj|OgkVS%%;IpP|Z>oNV4k1meTC8C^0_C#BFTkR(vErtW^PZ$Yp%|nC z)7f7`VMz)>DTw&+Z?(~&Ha-r%h-tVmSQI069~>fH$q2o{(D`D3vt5n;gx|YN8hDiA zKRPN5|D!!w#H;n%h&1Y;fN&>y`5p82Q!l6-@6flNG04oC>|r{Sa9>jpycm7KlMsB` zw@1p%*NH-qd6stP@+eQltCNy)f zlk8~+=Rw-lf?bg-5zi}b=I$zwS(k+C2Ne+xX8ECCf7N2)hc1V|O2Aa)+u7r-5I!)^ zy6?lI;-v#^-Uha71xP!tAk~)H*YCs)?B5@!OP7=_<{=Z%(;{=a!NPF_O1r>a)tI!u zKqWXVSuFM)mCsY13RO36avvK6@0o)?YjXw0$7bc;tHs#p!FxDL5p9;n9jW`GPL3w42c?rhicCKmF4G`#fWlrkAH z+B!#Jr*Vhc)x)TYCXm_ORL}x0;<;&(=8$2NR>r zL4r?`k6?_TM1@L$Fe87r1tmoZBmZA!D4d$Nw|5sCY7mMDxQ3TUPk!M&bTPv3Bt7U* z2Ti!ZJVE!3Imd2;&A0q54~xw1cNL=s(>*oT-pey@j|TCX7#tOl_4YnEO0T~(#&U3T zRvOp5w>exj8Dr_S8awPkn$9T9JDQYp^yFEj5y#LtSkP_pS-4?`xZzh+rn; ze`RN2CAkii4x)u_n}qD4sJtgd6M%*?L%HW^HwLxa569+ z;}CJ7s74qZ4pB?)KIe_nD|cBA z=hXO;m+gLQ>-Q^-3>C^qdS3mlvf(`&WWs&jm6NeIlbFgk`Yh|1oCjb~;^u9s+J>%d z54RV}7F7~%iS*at+K@Lt`sULn*m`TV=~BRj8)o<<4Hc0IAP!Ag*OJ7Fi~}pj-iets z$7p(NwTLb5LZRc_Ete%2{N^v#kf#sNb);8S9keO&F^Qsy^p#6~uB(#~Hl%has=_;; zX+Df{oruuZGV)w+6AU`bBf7WN-mPa_k?%3Vy*ky}n)NvlDpXx;Qgw|{uO;3ZXe=xd zskU!UTe|P9cdOnU@h$E)+##lfP&E&J?z70YuH2sN6xUQg2W={0suHfhE(KgIoBNVB zd$8$!u=)8rm7Zz5ql)Nss_>9Y%KZ$>DHh43^L^K4L(W|vZQ^geAYi68-B24upXj~6 z4ZGayzF5Hm1;s3ewXSwg(TS=rY8?7dD+H>9G{TTk-%`Vw|Ni!Zf(eah4Y~EDO_BSV zreRdCMRsn2>tu9cizxTn!ZIvN9DHK5`dodRf@$psjp|cPX8Z#LfD1$R!+gC$no3#A zv={T$RT+bYy#>`La!Q7^?=1V*1o|6ivh%%ajq{5|Cf9(Z?S5Tw12G(WVvV=L+-VEACJ?BDF;^du@$?9PA8> z$2YhJ`)mm29BcKZertIQF`jTr|Do6?!wJNz^9S0gFj>1=y6*whh$y^RwpiJwOh!R9 z;C-;i0?&cjS;jkd)o0b@6?Ld9!4ljC+glQuiYr{e%|_a|O~pI6NbJl}3ibTkK!XtnEj9wwpDu@pE43e!Nk79ldw|>+=E51 zsbHRVU<;C?uUWOA#75#udwHSp!I6nnj79hDh0!HNtRrLzeOty~Z zxrs2YqN*1<^lDw-UXX3}+g5pQ^iF+H;uTr=I;OY-%|Qj=8GCc4l6w7D>#F-TV(YGn zKESAb1&?J{j#(?M+T$rQ|K@WD>|iqlJX7skO2NxTPALB+p5!Zk4%9b9f@Egb4($T1 z;+q2}dA~k_<#&VS)et&YZhXX+A?;+^PIzV7onb?1%fLEv?y%u^XLNa)6Fi(Qs_o93 zZPfX|gO+j%TS&tPd&78=rk2hG*s0hWUIv_T;!3=z-rk@8{G>5kw#3J4!+~M~S@Aq2 zJx`W?SGQ-VR>5LJ;K_Z`%vZTJN|0yS3xkR~BoK03zDHq9XynyHC@+>m+$ZKY%BU5UvNp9Gc8M7 zaHA@xs9NoWqQcEiQCPa=d8oJQXPfrnAAx&6vmVUx^(D&&8A3gMU?(g!QWt4Fpk|SH^5Zqc^?M($VJ;PX z?iD*C6Aw)%!_b3?op5sB9CUWT4$!FHB6N3m6Ko}HAuPOMPjLBp-qZuGl0X($&${?j z(5Lx2YO}OWFD2OR4x_Nqvge8pQ!Spbf6(0UWx&3%^EnRkQRUu7f3=>|ucX_rTEMRJcyw4?{tYuQ%4B4~eDHR-H=W$XqB7MhXbZk>4E-A%45Bw&! zSne?7I;9+aqgMG4?%S6}UNPIDawOtO?vF_(fGv9;>on{t7X&RHn0dxff2CV}2u*9w_behO zBXX=0-EKJL%;uHZjQ)57J}19oQ&b3z4Y#1_4uaQhEYk-KbPS*86TR%O-YH(Elju{c zKK`xNIlSq+4xwAuwqkuCsZwmNe6atTa3;Yy0if?EL)2lSW%LzQ<{#9nK4B zX|i}KI=`bD+2A~3q0zaSI+2&~x)|CoAm)5Gg>Z{#SQ&|4xI@KGInaaz{~?_aY74_| zPMO<1+K^$u{9YS=NobrX0fzzWunXcTG5g8gUJk>fS=LnLIv07tV*JOm!y}JZ17@G!nMcJ7&T&c>Q;hY zA<_U7a%<%m1n065e?~3G-_Q=@HTs5KC02;1&vd^4x^`F&w(1U1Tu|ec?q*#xzqz(g zH`upy*iGw-Hpp1~2{c+(>}z@~20HWK$2C%iXU1rKX&dg*>h7BGtg)AUg@R$}c&RZ# zavAD&JcUg+vh#2=0GAex--%4dIka%@=`BIV8-iTsFxiBpc4Q*yR9@!c5n8&OfUjUf zSayF?G|uxs|LvZ5xqq7#|B71Trt8Z1Cyfj_Nf}t562!8_-Ow^B;qwqv6(O1-0%Wep za3FWy$Hoc&Q|E6$XbKOw1;DWGfv0Rn!&OHaFNVD73;T0LCC;Vay40@=6d%w^ zt--MoY|HFE_x`SR(Zj;3533lV5!I(x8Y)WJ^FKWfIN}bb0pKD9>6DY)u2)sGRjYDY z9$f_x^YSEb4}gad(V5KgyOf`XAxV2QU9Z#kSgs}PmUn9DDZ>;4n4HY!&i9W%TzvsL zqr%E~m?|?s14f&#BZy(B`3`xkHs`}E^lY%tpph@XN9FO|dcDSqy@>t?cHG5xw^ZNk zN3qR?X75+tsh2!uK3CMPQVN5IxL3B`r57M{lpBIm9o(K)@>35{f=v`X@DCXe3SC8JL=EiguVzi?V@BbRy)7KUBr!Oa(Q{c~19o+U_^+ zeCRg%_7LFBEdVRfCh5GtJr!WW>jA0c02bRC?tOCE1EvqoPoyyOg50J?pY|=LUK=$4`CLx=*YaE(pSz(s-|GMAY|3E(QkXu zUA13~_LN9u5w;na>n-F1f5ONFm}oNVoL>5c2c%QARm&#F0Hl{<5(QtY<4d)+R;-oGhBZ{nXws zJRnDRfRGV+$MdwSb{5L=zm5hz{_g)w^Bf)BuyhKygHmN(`alfcD#o}mo?Cg$yyObE zcu4|##AnWp?MSimebXO4hsc1biWW!QJJ|2d3{azTsk983>s zy@Nelgk*VM>nJ+d=^kM=r%;8MMW}v7VFbJj^8285DMTbbK^UklZaN(* z65)Bnriq>i9m&a3G#)B<|9B|O>F?A=hb-UA%wrA zC;Gj(2c1--dS=sN9@Dbt37b9RxfsueT-C2!mq#8;H^ZE=u@f~r4z#k)3acJ8RQapt z?{{=}3w!Nu0R=P!R#CgSP9l+Xg@l!5#-rAjL%hqg_PF)$gjpOMB-XZ4 zgxJA!?a?=Yg%OVT=^!>;uboW>MDX5GgzjG4&4T=VTQEx;&^e@+q*TFr4-c18l3O4VVQWM-~zLafm9{U6y{6`mbjA8wYV$ zZB{JGI_H&q4(GP)m+XZA;sN+;zsfjfaZ)-q{ zU-Qo?a3;2fFo`+M^%Ow*Fxz-zKj9skrI?$R!YIw1=ra1oV?4RUch5LG%jrO%a(9AH z+x?gn!#txJ0^)O&e6E072P3|nlbvaW0FW#9e&GVSUgP80Sg?wX4?>Tk8zz z<2_!hN(yMLs6G&vw4IiZmdih{(xa=XSzq_lFgxzcL}pBO)@pgt!`g!!B2_TyAlUE) zHD0ijGi^)#+gM*G(Les^uqyv58swrA}6fdZfr#6KyqZw!tNFIayiY zAyo|A^O5&-H0?aGKe-cxx1XbkAUdJ30*RhM$=?qH>JNd)&4j(Hg#aLn(iWGefT_d!>&L0-#scW^KpMIDsDZ4SDODt;XSm8dcf+**D;z{LC3 zu5@ArvSvw8QOEDioG;w`Mn+w}RCBWW5?HN`$_q8E7?y5}#YY3NVf9K^D$>`uJBZ$H zHMY0y@zxvEA6OnFu@>0|s))R}7AjZsvBEy_!9a+wlbYLRF?mUEiMLVKp4`O>lkyKm zSpS2}jF7Lzp4x2EWbw)QePqmTL=2*Z@VGdaZH)#Y5);sVzGjKjYvDCJftGFFKXKcW zu`+eiVoUtT>p?4-)AJ-nU}OHaDf)EE9eao&GmzF3PztcbJWw=5;6Pi3YT{6Nj6GH# zaF-p3KM(=;5P5`MV)+f&bb}sON74X$26P;mN|*GS#9rw*wz!w%&-*)n;~SCG<0lZ% zZRF5UBW)}r9V;CW(9*MHdS{AjPnCKP5f6tV*LSzR`idN(YF!IH0PcLiwT#fO!^M1H3`N8ddf{&;za(0V-q^|S%2~dI3!|gk+qkkZ}a?)-I~1_=KhqjgwSJ4 zb8!q)U~Af1c6JXtywok>bj7EEx-9=wVv1(vEs=+AXkU_*T=~U{F#9X9R`!4lA}eQ` zc` z3Q%jrYn`bia_+qe_9_;D<6Y1W{|9Nvd4fWE&qicJU)e(ooucA#a{fomA)lDqR3ZYU zjrLcfD5#k|04(@@nECad75L^1$SQjpiKGXG+>8<)ThF2lgq#-!TD|vIJw25=wxLJS z6V7T}08tQ;h5$>NXrG~DOvPBUd2c4B`$Z!eR%8`Ki>ETs1MAaLct_L|YWbDR!L~V7 z`y>$Tx**eP;LKsMv_X&pwV@c`Xz7E zIAmvcGcNRA_p#s)nKwmwajgJ+SoG;VSDS^az@f~ku9JtXB`=opxbHN}=W(dGKC*B2 z`)V1%sQy$_v-7#(D|yt#w9N(jJ&=KCw;;HKKsgNRCY~9*DH^yFgd=8BINm5MX>WUV ziVrBHCexN5pT`Iy!yw5TGB36$z+)e@IuJA`>>=6h)cBo@(lLonuQK7}9S^%eWRPca zd|;=tKi!@Ic}u9@jVOWHqR0KXh|?dq^w@HLkMmj}wG#cx;&8oCpUIved+7z9tEJ<^ z;R5+`|Bmbz(_SFy>o{nCedZZFx<~~5OL51eN0Fa453gF7upEZQ{7MjX$_JHmNXKh$)K)I~tV2t4|<6@P6 z7bot%0o}O>56q%M1P%6kAC~~L3l>2Sc0QOiB9nr>#{Y#On=mna)3T^;bNHivwA~=) zuF<#y?k7Z`UD9lR8Pc_RcyVyrqT9f^0{9)^AOHI)a()|Mfd|28%HY{Z2M(M=$zqr8 zo&+@b#D=pa3$6)~vSMv; zoL`OBKZVgEs3B+@&BVDAXnHHo!G_)BEk4?wI)LIKm>Ml8q| zzHU!M3ITag>=^kTP?~;@s0=XNXxWQUeY6dF2^vYs*(Jc@W?=!k^lS&sGvcLVAy6g2 z5id#>2@lu=2(QiDmVmj(>O$a~Es!LoF`mG=%O)cAAn;UK0XJ&qMjhLQRwD*}lO_Oz z^1%#_kiwoCKw0$Z)hn_TU=Y~|Nd_BW5OcC3?yDkzAIccKUw^-xjBtdVH2?__vs;dU zOgMiEm4Gp1K;;@_X+rS-7o|fO1REToR(kE4tHU*4cZeby>G!a^D~&+~PR)Yd7bLwr zRvSo@!d0#-_3V;6bA{F4L5>~$X#8%wBSWIIod0maJIVd693?1v38|Ox;X2?H5Oia^ zA!!(9?CI%=)nC{dKG+A6(qH}sF-$~v!0OwK|9X$eN?@_P(lS>9yJcHr1RIgDsA1qS zU>6<)S$ZZo=>>>%i_~2OdHnD&sjC$B4tqB4m;wav-QS?mD1G*Tk>1HbhLE5x8%UJ{ zViD-^hWv`hbu2!m-7)($09kJZCVsRrJGVr)q!uc~lUXVRDOHgXUm7UHL4O3myUWQmqfoy-(C#JR2g{{}d$TZ~%ud3Q?*x-eG%%;l{IUhyqirB- zg8(iHbD;+Pg-x~!PH}{%54fGN#XlHRe09|tXcXvtC%l-@cgRpav`l=oS&W?~WO}c< zwvfZX$RS0%Tq==^*8QXL>hqeeakBO~lnxf+JpI*&zoDZabxeVP6lww=LqzBUfvWky?^@Y9Mo%?aB%+Ij8z?7KF(RjX5l73?MRRbpfY$C zq4_267)}{hKpe#ICR8=*MZ1`WH$-^3Uq6OmtScPjUJ^anR809#0+g*s2 za3%&C+&?q{RspvAukix3cCOkPM11*S0&pq_3t#@V_GEYcshD)3yFQ5OpT(xUyj{!Z z_5NRL40jG-9gKj+K5_+j*!xBy5&BQaGrUK@=&2WP@AlZdlr5nWU&HGZnjCF}gpCNu zCF@`^C2riZ^0pL%-y&nf?Nfx~{k8hQr-CGh{v5g)QY2u1#mA*-C^$86X26LhsQABU zRwNV*)?x%F{8j123R1*qyTi#>+P=$>{mK^QdujvjmS4W2xWqbxY%&o;YjEGZiAr|i zBC2jd?Sh>FFA5f|+pEU)lEd(Ji}OBs0ePeFu*WKhC}Drb&BI9&FDMY9HJfcJ#cZ@E z(%%rU>iG=l;7IA)uG7#)t}YNd-l3t|{Lik6hbJ4CxjODje*s!u4t1mIFUSdwqyTmF zMf7QVJ5LVd?&!RBmZ6?i>F-STzb+v<&u0&i3$37%+S>)41HCi3v`n~7`?W7q<03^t zr-j$9(PPcE<}M5hCIaa)%L2872TE|&s`2lTNx{_$3Yhv;EB3|6l^(a2lewGOMtfYd zXP^rtc|fB>arr!iO{CU(b@nxs+<`gS0~C1nXWdV8x*g9gCxpl95>D$Q$4=5xi3c>n#c9GZ zCw>goaNz`dX!~`ElMi!`x1~AP&%~@5E_6L1J=!+Ex%Fq z`1@aWjs9TvC2)vU)XDPmC$yzio=vdX`9X}!;zAGVg6?_GJsC^fbcA^hqy;`uSgTK_ zu|i%;CC*&ld|2|>+axm~spiydz2Y3rW<%<7?C|cDf7U^PuxKtv;_;sut(pK$(Un_^ zPFF)T3p)TEMi@fJ1i5yUrAU$Bv@W_DHfWxdd(MiztlF~ zS{mWbEmH5IJ5*F=xDz&FX@;C}?Y#TWV!rEie9*~nmoSf8F$=wqo}m-fEP{j6kMqM^H&@{RZ_)><<|M11q$eg@cQ5`XVANv6idEoqyFV-oyGi`hkNzyEV44 zhURaybYnq=nT)lep$7bF$P+(0tz(9VfQ6zi%4%!b#XWE2`lNwn9?J>5yMOrav@4y% znJc(&$bsp9(#7Ah!UiRWt~Php2nsXfmdgCU2Q>;7so90Q)a(}s)=`<|W6SfdLt6yE zTX8t2gVexn{Z|3&31#Y4Wj$ky zu_`&E-1?@=6&GSVo**&VRhG91fQ=<-8s&-q&_X&B|xl4m|vIX_!Z@taL41m zdo$Wq4LDz_jfi}YXA1+^>J{LTLeOsgkM%gp1Tw=tAZdQdMGCLC+a5x3Xk*y>z$)ky zWaNtV!o-J3TiFa zuDXOYJK*0wi865N1%zG&g0ZQl`V?0MuNz_}IiID${ZLYoq%XWlW5`btAXs)f27#0S zA{(<37-)B_;KfEp!n+E)NHzZJ^T*RM`}_L~L}S|uXmwHgegjR0gz8paf_a!+`OLBx%ew(4c@{lvbv`bL!GD z{l}Q!zYDq7O9H^M54wDvP0UTZC)aZAc4pIOlM}(X5ja#EfD}5WlYEeJ6c|}cavqfv zK;u@oVJO9b_6msmIw#HvDdj=6aLr!Nv^7B-D7jXbMxo725C~~uL3eEd&XrH6$L2wE z2aq1Fhcsw;Q*8rzQDV|7xvE7dM>OG}@$c8i(}Trg&_5E$8bERCoLXNB<454ANU^Ox zxL>C`l}K;ou?$U0%F83~#1>otO|EZl+F~an!Bq4 zCl0nk4!+K0We0AS_1QP_AZ;FxTtQWZ5aubw=9JXKeE=3~dY2$b0$6hYg2?Xs5Ra#D z?3y#32o{xg)2|bxA;g@8LedBA4Ax*zcOhS9Z7nT*r}hRA4keWJe@edaYXP2sbE$+7 z0)(IfQ0imvr!gUXzzsP6t4nmqq*faF5*=k><=HrKXA#a0bVSQz3u*KMqUN|fQd+s5 z3&nKN*Xf{F{zlrXx((f*sXen8B+z@#e9EYVj9k3}F1QT+Vd4V2E>Our?EcjR7-CbL zNMS9VQzrBWJqJ2dy#k^qKtaZT!fmz&t44{UJQYG5_aRc~x-GBPNo*8AIX`3T`O0`b zocQZkq)#OxOaNdCkHqeRb5&W*3GRFGqU%t|6oDAlDmt5kFc%;loD0`L5IfIr^7Ov> zRIgQGvs^_hf*|4amIdbh_u-wP%}Dq;g0>|c4%#cgM4_?G9LiBZL6GX=@0TaRbgNET zC}*5hUxQ^Rc~t%K(Kf;(kX|-@Ugw_20pcKfR5u`fIKUMEQN$x`?1&{i?K2W#)U8cH z+Hv?dX)q`@f(QtEWd9lQCq(DNvVAuNwGsL!kT8ezMLl!?O1c2OAY>FJ)7gX5-Le4C zXOY5HAT_kc;r}Vg&O@F81^-xH5LMn}JQPr&EX8 z&!abVx%AKwFg=yXc<3XnfG&j)7*MOnB)_(Sqvp~Q>_tQ}waCZsHD@flV>;Zc`x(zC- zoAUqw$XK?dKJP;n*#&+%0?y*YazUvM3H+eVK?xeH(G7x^ICMJ%xyBwrtT;eo4f9Cn zpFq~R4W@+q$zXXe`UaA7K;SU@tI6JuQ#7iP$+6=;>`x1LH`0HCbaI^!k5$RYAR-AA zk$zg7EBPTqAo-zoUH*dI4~p;fB!yws^ zc#Y>cj^p`!`_kZc2{8fIr)8^oR6}xy`epbBi+`fZQps1bw+RfH5T~u#=_ofa|IJx2 za;e#Ou^Fe&0Vq)DAu=*G6u**yqJTpflv)7L@WDB*55)&|UjY|DnE1n4nKwESU&8GXEzc$W)Y#V>+<)P=Fc*-WWWU4Oy>XaH_K38M2Q!bF#d63k(Yp6kM1? z!v&&y!@pw%@slMiotsNV6^R@Nn_UZD6r)S9`LIeBWX)4;prn^4k1Y%9{>`S8pk5BT z!l*>C{{{`CH&Ey6eHz~1wrIK7e>}GQ=Rc9!N$cMm98Pr{FYPVue@@6x_MHsEQYDs9 zE`N&l)~rBK&zPvc72*9(S}vw2f4Bv^Tb!=azZ%f&C8yL$tV|aWcMbo<@SWSWT-Zyo zZ3v-hPia5kk&M5U-Hlmp%`O=vBn}mat6(Q%j_goY``1s(4^(*!U z+z`%Pzv|4j;{snv*W=wgo;cldV%^DkQDv2;<|&!}omb3H8aj5#j&khXCuF5F<*C6CnaPtOgesXovm zVJxxHkfGG^)9ERE5Pa708D$!wGD?)CGm?s@Tn(qNY|7k5QbPg{$fpGTRNQwYM_1L~ zGQ}ME>%c!p6(81jTc>#h6e!Yjed8tg8c?{LMI2`9Oi*IG_OMp|F(t|#LMiG413O>b zFZ}Un!nrh^+(TMg@RIHM)(HaCO^jD+ESIbwa1DPkFc=ovW!mYm;0)k{Gc11bc)-SI zxw*Ob51k*ZKXe{#9#VdQpT0fJ{B*T;m92KX+t{Y}HQM`x)kCOE&tbCbTOm{{g%KBQ%wm+|;dPhDAC0f%8wBh$wCt z`!JLo7VbGSJclIcq;c{dwo${jH~=wwZ{Px2E^8a0r5pH?H2*7nzlsv&g$FFCSrkdM z_^pLD>!`>|u{k0FPz;#4{e?#|wfC=06SHsY?~TVr4wvmbR{_}k+_`hn$<-z~pea7@ zWMt)+DGgq%sQ5&&_sJMRR7JY3@XznhJ0H1K z!8R{ysinr$j!DdZqJ?{>x+-}zLuQ;h5OK+?{>JRP*o9y&63P1%Mo!S|)=bD1TpQpP znDGSbw)yEH!tNkVv)*V=!AuZHF7Kg3z&MXVagCM9JNvW;ao0&=D}b_Adn`6kk1_v# z)qhY`@Ug`p%R>iy-7#aXcJ6LZKPRa%O?g_k@D@~P3kBd&_j|gqvarOswOPZ5ko#<|NQVsTTkzWz|X$E7r!4q|Mlw^q$gMi;BL~HMip~CXGc4qomjKDaFCHgIq@Zw zKYO4%cYW14lH5tm;WLE*nn*@*@$W~LPbgKo1ca>;#lK71JzG@qQb+Lwobnd-6t&06 zCf=o3Qq_lVKr$T>uRRIBTF<{n#r!#ac*;roeJ^C^-W{ZRPn)z3Y!|TXVWjutBgTlA zr_^h9)H24Y27K1jtOpMsL~G^C@|UKktNJQrCtDM~nvhL-zZf{g%WLx`@vba?^iScW zVPI*QnVGaGuj#?0Z;LE_x+kK%HV9AeSO4w2RjM*o%W}P8*RGVqO79A9RA`^_oZUDx zs{L&xDR&D`+wRmvD>Z6BV4z|^=JV&emEI*zZaFbAF|I>21REzzRFsvKD?F!H@PA>2 zfTU!YIsO8YI)g$xN|gAeF7NMQr|L4+94J(y(Z&`F%Q(Zldbzbivm7mYiOMA_C7&A4}KG zF@c4^Mv0G)-?&TiU8a(9h1YCXj_I1-_>~F1r;2JWPz|;DcYF}pe}7>oMfT*;_Md0H zhQ)`z{6sxi^X5(&hZ2#idqw}S(UGq0QCv9jj3vWbUs?IC5`|!5z!6kulFQssIdCTZ z<3quH0vb(B??DnxB4g1uCQ1&D2dFn7KjbO%9}dqxkyZP=$3^!?_$}AB1uDYD_Tm=p zvPJwFH6xcMSf<_svC&~$s zaBwyLjJUcu-;{P<<6nC#^;}wNTH5r{##YzgA&& zNSWA7@=GTy*o1)XZ|(eK>;3!p?^X0gL}aoMEo%Gr7q;)z`x+Kk1R_Wc%yZMubb$pZ zvTsioxYU}fSZ(=a57*uYaxn+I)l)SpsjJS{(Esv~AAPek{l*HrxE}km`ciR|-KHct z8`$6@g^GtJf-NE7Pl<7^tGj{^VYTlGPd)K98=XQQOpW?H2v?|9;>Ug5tYBXpdCL@ERv-I1yZ(xrl zM`A^7A57fVqCkB(3lBP24{-~LM^Y56#vI94`RG=o_$IbsEbUHN;pGsC>5DYOg%17M z6+5aenMfDpF_-~7`qcdJ@NnTz($rEPn5E7Ampmq5K%v|tMi2@!D4Y#^JlYHGK_?f( zTRHPnZZl|mliL?ctm_?0(T7a>xiUsUYvyPUu^H~twGv%KCML|TlfHghT z8^16$KuuC%ZQaWA;>uVTXpn?t>k0RmTYxot+(^_Cg|c z%6Ktud2V;f^lz(wseD4oH-DYHx3k1&!Odq54`th$o7CZn6Ol@ABin_*!S6X?c0Mc~ z2s^U8Ns!M(wF{sev#1-6fCoNdyG^}!-Tt?)j~O}O2_hz#ttUT}ug6q*LPmyVt9Mc$ z?D1nMr1;1c6)Y4)yr)om@{WH`-Vywm8Ur-J|31isQK z`uj1eeyRBFP_pVHXY?Yg&)jOojLWVoVtL9(#?>txPAod}KjbK=tvVmq9?F`uB+@tY$KB}gY= z1`nh?X>6p?0{S{S4Da4Id-o+tWf=&?-6oK7ws88)L)S9%^BeQc=cfj=cpH@+szBnjvZr+D$9K@*=Di60s(q=|GA1|HQ z7M6@>+j>anp;#qoIbcEYk=QDO0X8ULgbQk9^I#ugv42c+8|xyJt34RT3~)3+d+e|HpgO2f zJcUGZWG)!vKcmUFjmqwy!9aTxQ^+m-vYx@dJ*gswHuKQ`=ah3$y#UvCR1zf1@_Q0B zKeMCvwN|>sVR1k|(U(695l(mZ#S@{^iThsn#-Gp7FED%0+)f56a@&1ga>b8E+_c2y zbo4{FaEIdf^d%btMvs&ub>sz5pchxm%?upa8J}A7Kawb!gB6!~nTo`h#h@)OX zv61}TCpK{hzMOErBDaw?#lUTPMeLXCr2#Aq(J~mapEoLsDpo;LgRLDoHTqd<94eH&;AGdrbEnnQeitgN@3@ znJ->AjTN$SXsS_ax_xo}YUhg=pH*gG>XzLaHL3}G{@BVb{P9DOY<6Da1J zn(jwNHX=YWUWAM(s3?tCreRp7(GPF66{oh+>&NocwN-W$56bQ-ocKVCC^K&EUDZij z*SVeN$TbG+Frd&+Kj%@r(*OA@kgQDx*!{LPHrF$m zNl&8>dSAIV7&K{Wz-q^Ixpg#uP-t^eJ-|DDHvyDWW-8bwitIku-}u1i247VA(Ume8QCpP+p>~f zG~pYj&h)iHejXmm)kP0#cP?Tn1dcxS0FD;{%&amv028kHdvpcoWz;|& zyYmwin@*j*^v9=&9R9u|fS-HFt~Iw0JC`*LA`S|ByqYp6H+SvBm^jbD7tr`|y%dVr zmg7W0sm~k{2J6^U^{-yN>g1Hu5>K0~*lO;5v_qLYR>b?aV*|fpk!|xRW@;Nd!uq#9 zhe#y5u=s1C&I6VlT)oBpYZG_%b$~5E_Ba0vg?K(t$qm%gxQ-xniBOkRwktuhbR$DV zVzX_aSaJaf-Xg-N)8bhSYc zI^EYL(W}#}rj> zn5nG*^m6H*EaZ-3r9=E$E*HccI)3B51b5Gc(Sj;Qr{ju@Dm*=f3r4Ykx^M-*`xa4u z{`?tfFY1Pd1Az*m2|=B}vrFPvyWTF7fx8f;6ouAQi&xj_2|1i#?aqDAOh|r`jZ2sg z>H`7e%#c82S{Dpm#^K6zbaV>gVTo!dpsdeY9NM3_LF3m2I$)?dD-N{D@#Duo7aZ7k znx_leiCdSNk5d~{Q1gcVy%Mv#Ohc7BFe!g6^a7@$4sEgor_{pXDHchAH zGxT`yjr4l;%S%O647*(Y3exi+mf$mYg_Ffxpqjk=nWzkj6)>`b|NaEa}MDaWJZK8M}j6xZ&JCyZ~pHA$o#)>di z%7N6XQ<~KaD@aPadD#3O!w(KMSmo#rgJ`*H)1!HBP}z@#QP15N_!?f7AR9H>kxU~~ zFPdc4yHN#ewnXcP1amt{=CO{RyBl06^Rm;|hzG(DUXCplzROpW2R5!t3CZM*7vXPP zXMH&4pVRCsR~=}JJQD1sb}Hbdoy@c`XmB_eSm@RX$zVGM#K9sIBd%4&~yCK@TetkvBv|NfO z^O1Xj_iO@m7G(l>|GB%+Pdp8!Z>*$wWM!r;HhMir;MZm#&2p*pQf4= zua?ZZ?Y8#@-MvzNDxS#P~3u3UiRHx(AY*=U_U`w?F`@fdkkbj(BS(z^fUtw9aV_*3F$Uw!CEi6JbE zFVz$kZ$4xRcA|WJ=E=kQr*0#rnz1gTfKYzpXG>{v9YOX zAc1CmXCqTz_)DNsE-1ghb1ub+QczG3R-&EYnYHj-Ym`{z{_(9VNUJzi_Vyt_jXG?g zV?=y7n#4|Q<%wKX=+H?}9lM;~ZjST*b+^T;J;4j11I$qs5pa)n?{>Mr&DN+<9CCmL4>o2_`SX8lChrNqZWt9@$mm;02ALw za2Huqe;}{gSgKR?;unWG`=|rn$^u2d|4pmiM`Wb*yGJW|jrKpN7%t#2?Hs+TZ~FM; z*{WbzS*1T#$UyYItk``ul`9|34+b<&!*rl{eIC?YsmnloYj9u}i~;zqv`ei}OQPCp zw|kCkOQF31o@ClQW!4Jd%*~rOr9H{p&Mg6dejQY3<$s;yv1@XS#px8A7$@zHP|+qIJpnRumXS^O27g0C*r9IAh1h!=w59 zMPXr~8|v0jL}!>|QcYk2&>{`A+jl3xrW4Gp)Rj@9mf)6&0O-02MA#iuJ1bQ7)34 z-l<+_DLyYVSv;meF!7+S?zB${P`y~GD;u-5tk}>hx@V1+NUnZ|yqn@z=8)Po8+YnX z3Agt3u&&>KEhc8C>E)?}5ZO|o^}SulB#}=gnA8KrnEb`^IL{PWnqkj9p9Qd!Ad8oAtJtoa&=5h7Oqd*o(9k zP5+TbPPMeSP@hR(b>`4IU|1(bN@%74j^>|RO$}XIfai;G4SiDj-sVxCi0>#FCT**0gwnQuwMx)kX&Cv?q7s}r(_>MrylR{l(^Q~G7> z5_xD+&HSxbI4UA~MmBtAH=B&rXHH@|>-JwOFE1xT4BAM%K+(()rJ6Td)~D1;X&K%C zL5h7&T59Sb01lS4hy!XisPO(geVi)d`ht6{( zs!*bn)zD=CD5|=bFH}q@N6n-~jD+L7KD4o!Vn0Nq^0`5eM0%S7>R8_G5+qm#n z;)Y+}f>G53SdnX9Kc(KeA#~%`t?g6-IH_9lOsUATXnf!x#qK@}{_F=H8$7XypCoLHU~Kz+8-)dk|3$9bdAHtKg2*bYv=} z2A?PJBeQHLBiX5dRC;CRngfGN?Q|snH9Ty;l8&WPWztV_*42eaMd3k(e5|Hm_|2nk z7(ai!icQS!sM9xv-UkOo6^8Ia5{m`C{zI9?@!8o&etTX>9Ie_#l5-D@r#mWiFVHB* z$pk0w??J~hX0(QVY;=xwe^c(XCV_*Ap--+nuz=G_U;z6WI1wo z|M6xNwWH~T{BjYs*vYiD4{yRz2so*`otiA_Avv=35l!qx>6n^3dBS-}kmjE5d7&`r zm;jJkcWM47-8YU9k{Pq8&<8}l7b-`_kgCSX`j|C`Zzc$OKt{VgzHjy^c>1&l>m|&9 z*#+_LPS9Gzyu>TJYz1GxmI<>s80)F}y72zPjrWZF!Ra8(mp;Ju=!~wed-c}y3cUk5 z>I4)D`i1}#sreZ8*CHu^`OmQOu-Uxp*l_WK%#ZlYOozE&ee+od#l>e4Y=)6H$D2ay z!>{2V;0I7~5&9=6^B<#-%N>Frpl2k~Z!CNIhtVF34HW|-oPj_3mBbVf=#t5YYZMX8 zRpsVtImqO;T>b9tQ9CVs>eriyGx_`S9lj(g!%fVhdg4p!xkIf50A9O8<2P@Uow+w7 zyQz)MZ?WM`YRVAIHueL%PzVEcyH}M%pF;1mZ;oFu+;HlDL2U*wnOFBB2&U}(DRRD$ z$NFf)u(_eDZ%J(hji=K7wrU#3ee(C8KZW_(lukK035uU4k?w4V`4(0y9=RGs4I7mZ zE2z+HsOcIY;!nUhL4j^R7bWZqi^zsXqgO5bcW!1Q`SoLySUXwYxhKay4I~QS6q515 zJVwiB{#We7J=Gsi|KS%1`2(b%M5@EFJVGr2cY;SypQfjk9{xOL{;h7ywj@FOFD|GIb z5qvTO`UW0N&B!2Qb8xr`rWp9m2eL3|NZeY*xfYm+EG4Mfw4p_n9 zvcJ<3;$5C3Ta1jS>!da^sM|XnE4ZrLE<0#cI?5g6hCA@bdxHED;ARE+;UVX9>%-np zwFu9y$?))Heq7lZS(aWAoguJ6B4wLJ$gz9!9$%(B9GAI@HFmKol%VxB!V1zoHW4dC ziexRu)ULR<55{k<6WFD$9-I@s-%(QZ6E90*#jK`y!C5a~c$BqU5saJywIW&G;hCLm ze$z9%UFV^5!=B}fbR?w!YJ#{(S=k8xjVE?Y_Z|+=gNAX$os>?VqiCxTJ~w9spQ3%H7fPHw~|)vYm>Qf z&4sI6dDpn{IpT4FGj7FlU|_g1(zd^dL+?+i@9s9I?5Tt5w*`(zpJ9A?rA~B|PUqR; zXrwDobeV(S6@<-r0bhJaLV5$LqeZRG_*Lx-%`et?M8&)cjAApU&m~QBezhOE5E7I=C_dr(C^Mk2> z4=!bq!D>M4S(mwY9RXOGNBIp;gQ&XeL}`c?ZO<{i-Qn32Td*(Asi}?jwb@nea>fqU z^xJwxDdo+*u5@zi!iMP>6qIR6{h%~Jjgj59`mqIl3Y7gGdV-ioK{LsQJbU}~j-|7g zpvB;C_!8I3GcRebK2#q&qtC6R@@V304SlcRfU4JzDSd%vdJ<_|k(Ojd>H^Ur#IHX& z(_JGNj-SDDjXa7AbJ%Exqvw|9Q)HDqM@=?@;fJ?@Up} zURk@EXa1P<472Lw5ZmP%`NIeR%ne1tO=4{u@}Ei^dt2?6W!)y6!b2@L#A`iYvJxf% zJTTz%Bx`-XZKj4I<>N=2&r4FD?Dr;ZxEw?`PC=z4yQmFU*?LZc;qEFT{*>;E)E093 zh1{k}T`^#Zsdt}^9rykFXSQ&*BMt+{05?P^gq(-M-t~oPCcj}(bC|;W77ZXp$s1<& zOaE88%Y~kVJ@1b4&qEk;pL|cXX7ttINpn_d@(z+J1fr#YZR;{$tJ&MzBaehw(TsJ) z?_TuNAbPI=)k6Rdy0&oxIqB(B&cz7ALP!p0zuC?j8`+;^OM0gdSzGKdGqY~?g;!v^ z!Dv?cQe{y_Y02)LIKp`r!vYonAIKii z{?ER(Pw7DPQ~SsE7df;Rd2UT~-ooj@Z*ijP*0mWqO!XlCh6J5AhvQ0OKg*7);Wqw< zm5h(?iLc9c`0s%Anhq_39o8xM#Ry&I(0kDCgVJD14kl`YzrT+FxqT>! z>q@?2ipqD(y&~}`i12~}uR`2~?HBQaS};a_sx6!6)xFKLk|Z|;77a@O+T#5jA)inI zs3jp+3JT(w?~?b>4Avp_l#H|zMJ#-=)Ov)%sPG1s0b2(kcRfFME_01V%99q_J9``I zOl6dgE$K`(F_M{N+qT~T2nds-HAwfx1=7#$3p(3n(5eO6KDF#VcliMI>-DQjlIc0w z)~{cWGa_Pp&wq+dP^FN%ZC0;1BHB{QV)lrQp!?g>=Hf%K6*U)mNPeHOa)@?J>FvD} zzmZ;E&sw9T4b~1FjlSg?y{Hq8~p{33poA zTu(Pzn|Rsv!1k4-0;1U+;?B#yCfnp@}py87VWf25HpIu&}@1YQ+ zqfoD`GKywzi>I2fF^uF0*0w42HA?!%b%n)1Ekg)6ytpENcT;^6RyxkyxK;eIEuzO9pxT*l^v+U1ZaDQ zT(iRi@3i|3{lhA9%pon8&vs?Egs$+vR^H8;!4y0St=X`1?q;ejUYU`6s~`3y(A<=v z3y{Fd!wgF2)YKXXGE@h>jE;#}eU?jyo+1zV>%6nQ{RsZ|M&_rju!!lOZaA5El~a|= z4ePz^uP$c$ojPn#Xjlk3!n_`s41g`=zavor8mAgGu9ES`_wNO}IypeX=o*!}SYsc3 zuBLnM1?>^~{yY^x*zAk=SVE%8Uz_UhL3CWpvrzlIUlMOHaC8B_T!V~l)9RXA#40SZ zqR6LnK=+F`+@9+=-^%IsE(k>}P zQ&^-Ah>}{pL4xas3T3KKmk-c`h<4YJnJeTR^iuk&wKszLPxQPZq%%zyA8(Wk4x zIAkWIwl_!j!^0Th2jvJNF4ViAUa{vn0!V4%ooxd_ms!wolTallvk<`gMkNO2RNlXF!0s;s#YR#iig(Id9 z1=2$gscnH>2&_gqwqo#K%gc7Zo(2uUj_Bps5-`+~n1>c9g! zF_WlX>PB$_gd96D*z;~fDM;Xumjk|-MBUN|Bnd@$JOC3t42A0zHU0 z??%m28OBvc@UtYd$2cD$&on7WOjIz=ZeWM4^Mpiadb}JzC)A6ePw2KWxHsXVs2g#@ z6B6Q@7ORC9G!dd5l^yKc)9L;41|ojOe0sTN&|rP@HBkQ)NR+00LTL+B94fq9P`yS0 zFiB85sF1+lb_#I(tRrGoLOtjtdEE<6)2?m-)&4*l5z@YGhr=zv?0MR-OdG9xnhzI%k9hx)ApZivjLPZH# zJy=_aGCUB4nj~0@ndX$E+Tg)q1Gd=}sg9>9dlU>@M5?bIDBECDP2GbK2Yavdk5{7Ht&T~WaqgW_ z-L_Fd+sVnv!6CEq4|;!6_&AZZUg1902DN_49zAvil{(Ay~0=S;eQirz=^9xKO{ zo@uylZ;$Vc$fg?o5)QowZ+R&lzh4eTUL*yj@|FndY{yDrzv##!UhZ99`oC@Z*VyL zq2ffr$b_=1&J~HOsO#j9)(a|bgXiNK&y_FB?x8>`;DxFIqDk`nAo@E)1~XOk0&kr~ zg-7Vo+~VS+0vbun*HG=ee|;b0%Y8Rxn=ZzXKg4>C2GsUmr5A1=s8coQNRiC*DWOhu z**Z%2CMRi!M0In&eu{1KM_`!H7o!sAZ_reaRmsB^AA(j`*hm^i!Pr((H-D|}36qZ- zJR-JVSyhKNqZRhPf~jdn%lF4UjnoT6EDp)te$>P3LZ%b{Ech!B2rPJm@@9qPNd^bgn*d!FyOlOC6`79cO`D;@yFR7|jam?cG z5}=)h>gx0I&jRodKnB=mA*OOdztr&42$`dk&vucdwfePp)o^sm&eW;r1w}GVKI9U1 z^47Ae$+O$hoX6@ZA z>PIg^W?yrdc%V(p|DOCWTKR%b5=m-NWd2@fZHJAbvAi#ws@`v8c6)IwcG4h3x$sCx zd5qY@)1jK^>ag_^4GT=lq2p)?aS#6)7Wek}*y0bnc6o=GOk4fhO(Jb8P*ym8HtJ9t zEh1ZR&N{A ziXsGg7%GTQ?M@8A09cL4iz^T z4XcfDgI~UA>C~57B zj*bpU_4Ly}#vSv8$}Rdt0RN#wTNW>PP4#PnTsM3sCj=$$6;NV#o4}C`4%!T*w6$#V zK|?whsK(yyB_o-GKy{p`ydgTX`^@1Du7vPiw2Y+!1RoI6z+h~UR}a>4S)yi_o}T`1 zLBu3lfjyLM+PZaX%@WJAd8LSdoraf>kLavkJ%|$ALNs`{4X8wTu8=VGFD=Qq-_WY? zFMzt2XP1}abYq1r?>TY#lF8_|?1^oGV8i+8bP3R{Uqqw;D@3SrldbRHzh78CFPLFa zRM_c2E_`yzH-jo@R9Lp#G^1hBa$=|yE=cCbL(}V?S(w8f)RI|sm6ueO!>yzD&||GB z=pg67>jWMK+W%og!1!Y!^V2Z&Q+)(#55=fSGh+J({A6C86HS{kplvyFr?wXX+7f)` z)VX%^#qB-^BVEf}TRk}z3)E}fTSu=36^PBoRc8(T=_C|X!hR|@Vw4g zaqmV!y`p4m(4~YrjHz>kmeCGvDG@swAs{1qYu;_bed+P}qBPCK15qj#*}D8(g6`GLf)jq<|v~&BUreFavJ4rHTkI za&*(Pv(I!_JHPIOSd`G^0_0i1*pYo3Q1O$2W1&K-*v`3#VD%0em6}aRYNz{9?dAYu z$~?GI3kY`F-7VZ*AoX4W!jiEtW2V`y%k zG|U?r84+3>@sbrKuo>HPEm=O^vI=HlR64Jx z*NU79yF?@3puaulK>{ zf5xCjG3AC61wm~@T%56?pK%iPn@ zWw=?(D}%Z&BW*Nmnw1Fm_4*O5qS$_4kU=KY){vROuT>J^E3t#5(W2}KlLTH898Ub2 zZ(AUH;vfCq<#$t)ceb#xGFQ@!gYc4JvuB9ncAO%TuW#{z{NcJxJ%JFvf@f2v<`XXO z?VZiJ^-d=OR)1%3HgwTC-qv?PTZ|^0N45EJ{cqOMI)E|QQjO!@1|t_S8l(tnb{+R&le^Y`{MAd5iph7(p=_;ex@ z=a!SfY+HZbLgCCq#@(T+NRNw@EWiCJ+$>I=@*DY5+b)1hy@5k(N!JsO#;e;~4~UCj zL8CL^KcVEcp{|fJc|YP{wU6EYp_I%QxH4ThKrbSDlnP*yhyxq5sTvRM>z2w0=r{%&$3WN=V>F-y%42lEP~2RgK*&4NsF<04kWD zCyX|q{K}1-ddYFDyX^nwmn6D`!)&1f?|v$t0+x#-HXR|(iYGtCPW+papz+CCs^X5T z*qJdeIsy@WS6kd4DTpHCO@iOPveHs6Y%g-}wtf6Qm|d;)@xhH;3b?=ybiIF73_3P) zJ2h+IQ5cV~xoJaq+BAL2Ig$>tqF-R7%>NGKI_=)PCpod;jdAusY?PYfewQP}+4m>U zSnf8>e%JJ^s>|3`=ArjJ6hy854wk%*Sn_u`YNMPeiN=5 z6u7OU%%heuy)3$=S3)33F+b22a<^)tgaYlv?)C#i-!x<_eWOq5!3%c-SRqVMqDcFLytxib6pRy|Im3+t5U* z+%0(iIY77=yp^PQ1*U6&E{=M(*5{5Gr9`wFwJm<&rw_CVX6X7fY?Mc}tuSPLx0B70 z^d3LiNc?2)`|YB!57boYL|^FW4cSJVPONc!-1w-|R$cdgz3a80c&x>Lj|OL&nZae- z!o~74^cbUmKMSYVL-g?8bMec&AjfZM+??IW=zz4RUQy}FzPPf!D~fo<2T^GQ$G>r~ zVg;h9^w=b-M~3l)=tWkCsA&7!x(ixcLl3mY)4fM!AwfT@QU6vS>XXTxpd0km-FA@t z)^6gVxS7~!r#7CC-FQU3ENM~F_kgrV?qwJDEE9btcMF`!*KDkrUg$O!txna|MTD`| z(aBd-ki&PvuIm2lG)fOiYHM0pN_8X zL7mIa&Tn~f|HLM}@c9<<3jELczX2hMRF3e5_(RXuzcCz%l=b)hR3wtSBJsv4Pz8s8 z$PgbRI0xdt?X0j~>mo zek2a<|EhdmZu_Hf``p@gby|kr$Ki<@A#TgXS7N>UYfg|zts-V9Y^P+_*yx6spq>16c zUvX0wtSjXYbA;5Q#+L8L7mxPdvt}kyEtzp0E{EHrj*>qPY)`pCzE`xwqD)?kAAn$D zduNpV&_=DCHs#^U(%&*VcKNROCrwV+87>31M3F5_3C?g8k~4tokIyvLm8uy5W1hjN-=nMHn0 zl92rRChd!*RN%uOM}{7bMC7dM28v7~2?ZBoY!qHMvPyq^m$Z9$>8XX&!xpPpajCDW zZOZf)1lE+@_GQb*oo?cQE8%te6rrs4bmTmpddM?p53#KpnRrsaQe5Qq!PeSSCBxI( zGb*^p6Tf8D-cWAD13aX2k2O8nQO2XxQPOt3oi7yB|`|b(|zo;j+u?6mh+f z9H0Dixj$L$J8M=3G4>R)78uo=CV2k%IOf88ZM8iPcZWxNUB5{8u`;GRntRdy-i$ND z%h}0@0E|(n1a{r9(0u=aA%+UOiK1-PxbB6E#NEmN|6m0EA1v{;eAwUC{hVjYdr6#c zdz~hXN$)s3av4Z0+XAaqjdulYKEx3ccy93s)o7?SD6MAY6uGeZwEK@lo9tZ1;BPmT zM_lK~^lm7~XY38D5pT<1j{&noNc`vflVSzuJT;S=ioz7pQK3GYC}H+DpV#h#Gfq zR|SDVvcmY7uiKJ4LT6&SK1Zkg?X0kuS$mE&>9|pIl4fTy+gAf-9Oj5I7TPPF@mXH7 zRbxzJ)JtYvk$btr`YQYIBlueDO|jKWU;e{2*5RTB_gy=jw`bZ$^YU#8j}0OWDzc{4 ze}2mT)X~xtdGR4;{Dbu^M`IH+n~|E)cI|~{xX=GO@Ve=O8K3dqBmv8f60;?hi}M~H zp<8j14g{b|VNur#_U!a#e;v-{Ja1pT9B`)oW$QcDj@#AW>?biuPv<{7ksSp78vY(KaHOAn6qz@EpMAq!Wqo=Xo>W2M|q@F zNFU?5Nlh*h-y`tmmUjElRk>Wtncx1Fn2&I@+#uP(KPv62NXXmNS%PuR-3bX2!!_Ra z@p~|2W)lB*m;Aq5mH*YN|K}7QTA75%h^S{OAsjmMJn$&#f2&nxW1WcLW^v z)Mp;3bVItTe0^WCLI)wYUcb=p(fC7@WI=T<;%7qQ+MYFT4Tv`~S&}kLY%k4twD=t-vepU{sg!x`oiPOD03(}0G}O}t+ZX0U9Wbk`iay(gUT@hoqRk5*(wg)rrQ(O_=bo#)RgwCjRY7dy%C zpz^Xntai6uLy4E0&F%fen;ibcf4j=7fE++huKcay;zN;r`2+GFCeiFU9ci07F>wXSl=8sxjy?@8d1P34NY2C*KhecabhX-cMBu_Pvqj80)iBK3Cs;7U8U8 z^foE#mAHyyo1>+A^#Z-PNSqN~DK5A*e@3jZO$Uvi#1T=Ho-^Dm@$nPFU2^VVBpmnp li+|Ife}%ArzdXOZ+w$0ffQ8FLgaaihE2=AG%3rwte*sB{-J1Xa literal 0 HcmV?d00001 diff --git a/doc/user_manual/paparazzi.tex b/doc/user_manual/paparazzi.tex new file mode 100644 index 00000000000..8c85546e940 --- /dev/null +++ b/doc/user_manual/paparazzi.tex @@ -0,0 +1,472 @@ +% +% +% +% $Id$ +% Copyright (C) 2003 Pascal Brisset, Antoine Drouin +% +% This file is part of paparazzi. +% +% paparazzi is free software; you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation; either version 2, or (at your option) +% any later version. +% +% paparazzi is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with paparazzi; see the file COPYING. If not, write to +% the Free Software Foundation, 59 Temple Place - Suite 330, +% Boston, MA 02111-1307, USA. +% + +% +% +% This may become the paparazzi user manual. +% +% + + +\documentclass{article} + +\usepackage{a4wide} +\usepackage{graphicx} +\usepackage{makeidx} +\usepackage[pagebackref=true,hyperindex=true]{hyperref} + +\title{Paparazzi User's Manual} +\author{Pascal Brisset and Antoine Drouin} +\date{\today} + +\makeindex + +\begin{document} + +\maketitle + + + +\begin{abstract} +The system described in this document is an autopilot for model aircrafts. +It consist of custom airborne hardware, a laptop as ground station and a +retail radio control transmitter for uplink (manual/assisted control, reconfiguration, etc..). +The sensor used are a GPS receiver and infrared thermopiles (melexys mlx90247) +for horizon sensing. This system is able to fly autonomously a small electro +powered aircraft. It transmits live video and telemetry data. +The ground station permits decoding, logging, replay and analysis of these data. +It also permits airborne code configuration, generation, simulation and flashing +on target MCU. + +\end{abstract} + +\section{Description} + +\subsection{Architecture : two 8 bits MCUs} + + goals: + - maximum availability (modes degrades, manual control) + - ease of development + + logical tasks of increasing conplexity and decreasing importance in separate devices. + + \subsubsection{fly by wire} + (avr mega8) responsible for radio control decoding, mixings, servos (c.f. figure ref{fbw}). + + short code, well tested, features similar to a programmable radio control transmitter + + allows manual control and programmed failsafe (radio link loss). + + + health monitoring (battery voltage, drawn current etc...) + +\ + \subsubsection{autopilot} + (avr mega128) measures + control loops + telemetry + navigation + + The model can be safely flown in manual mode with only the fly by wire MCU. + + We want to keep and improve this simple system for cheap/small aircrafts. + + We also want to expend it with a third 32 bit processor (like arm or xscale) + to get network communications and processing power (FMS like). + +\subsection{Technology : cheap and widely available parts} + 4800bps FSK Telemetry signal. Can be fed in most transmitter. + We use the audio channel of a 50 mW 2.4GHz video transmitter. + The video Channel is used for real time video. We reach 600m in line of sight. + The rx antenna is a small patch mounted on top of a helm (cheap self pointing antenna). + FIXME: ADD PIC + + Modified retail RC receiver : solder a wire after the HF section. + + controller board, sensor, GPS : custom PCBs, all(most) parts smd. + home pcbs and home soldering for protos + + Free software for tool (GNU/Linux, gcc, gtk, ocaml....) + No Windows port known but should be faisible. + +\section{Ground station} + \subsection{hardware} + Gnu/linux laptop + + 2.4GHz video receiver + CMX469 modem board or rtty + ==FIXME== add pic + camcorder + + \subsection{obtaining and installing} + + The source code is available from the Project page ( http://savannah.nongnu.org/projects/paparazzi/ ) + + Use the anonymous CVS server to get the up to date source code and documentation: + +{\em export CVS\_RSH="ssh"} +{\em cvs -z3 -d:ext:anoncvs@savannah.nongnu.org:/cvsroot/paparazzi co paparazzi2} + + You can also download a tarball from this website ( http://www.recherche.enac.fr/paparazzi/paparazzi.tar.gz ). + Debian sarge users can get the required extra packages from there ( http://www.recherche.enac.fr/paparazzi ). + + *********************************************************************** + Set the PAPARAZZI\_HOME environment variable to the top directory + of the distribution (this variable is used by some of the components). + *********************************************************************** + {\em export PAPARAZZI\_HOME=/some/dir/paparazzi2 } +Default configurations files (in conf/ directory) should allow to +compile both embedded and ground software: +\begin{itemize} + \item 1) HAVE A LOOK at conf/Makefile.local + + \item 2) Create conf/conf.xml and the related files for your convenience +("make configure" runs a graphics interface which may help; however +this gui is in a very early alpha stage). Some examples are provided. + + \item 3) "make" in top directory should compile everything + +\end{itemize} + +\subsection{The ivy software bus} + + + +\subsection{configuration interface} + This windowed program allows to graphicaly edit the configuration of a Paparazzi. + It also can also be used to program your controller board with various test and calibration + programs. + type {\em{make configure}} in the source top directory. + + \subsection{telemetry interface : recording and display} + \index{telemetry} + receive : retrieve telemetry data, store them on disk and broadcast them over a network + gui : display telemetry data. Can be feed live by the receive programm or by the replay programm + + map calibration : + uses 3 points. trivial projection but sufficient for short range. + \subsection{replay interface} + + \subsection{hitl simulator} + Airborn programs runs on their target MCUs. + Their inputs and outputs (GPS, infrared and servos) are bypassed to the laptop. + A dumb flight model allows debugging and non regression testing. Also useful for + tunnig navigation + +\section{Airborne software} + \subsection{Fly by wire} + + + + only supports PPM - subject to jamming - filtering - would be better with a PCM encoding + +\begin{figure} +\includegraphics[width=15cm]{fly_by_wire} +\caption{\label{fbw}Fly by wire data processing} +\end{figure} + + + \subsection{Autopilot} + + \subsubsection{Low level control loop} + 20Hz . P controller for pitch and roll + PI for throttle + + mostly unfiltered attitude data from infrared sensor + GPS climb rate for throttle + + + \subsubsection{Navigtion loop} + 1Hz P controllers on heading to waypoint and altitude + GPS data + + + \subsubsection{Infrared calibration} + contrast + LLS + + + \subsubsection{FMS} + modes (auto1 auto2) + waypoints circling + waypoint crossing + mode home + automatic take off + +\section{Assembling boards} +PCBS : homebuild eurocircuits + +see part list + +solder one(a group of) component at a time. test with voltmeter or scope +use provided programms. + +mcu fuses - used to define type of clock - factory supplied with 1MHz internal oscillator - +must switch to {\em{ceramic resonator}} for ``fly by wire''. He will be generating clock for autopilot. +``autopilot'' will have to be programmed to ``external clok''. If you mess up, you can make a zombie +out of your MCU unless you can provide the awaited signal or crystal. You can read current fuses configuration by typing {\em{make read\_fuses}} in an avr source directory. The correct values are contained +in the Makefile and can be programmed by typing {\em{make wr\_fuses}}. +The graphical configurator also allows these operations. + + +mcu flashing - The method (and corresponding wiring) we use for flashing and fuses programming is +called serial (SPI) programming. It is possible to programm a resident bootloader who will take care +of following programmations using serial RS232 + +\subsection{power supply} + +\subsection{pc link} + This board is a level converter. It converts between the TTL 5V of the controller board and respectively, the + 3.3V of the parallel port and the 10V of the rs232 port. + The parallel port is used for SPI programming of MCUs. The rs232 port are used fo serial + communications with MCUs, for example during simulations. + This board is meant to stay on ground. + + +\subsection{controller board} + solder the fly by wire MCU (mega8), its crystal and the programmation socket + Connect to a current limited power supply and check current. + + build a wire harness to the pc link board + plug the pc link in your parallel port + try connecting to MCU in serial programming mode (SPI) (button on gui) + FIXME: if it fails + programm the fuses of the MCU (describe the crystal connected to the mcu) (button on gui) + check crystal oscillating with scope if available. + + try programming the uart test + plug a straight serial cable in the serial1 connector of the pc link and the other end in one of your computer rs232 port. If your computer doesn't have any, use a usb to rsr232 converter. + you should see a message comming from your board telling you the link is ok. Check the other direction, writing to the board. + + solder servo driver and connector (maybe later...) + run the test programm (servo calibration) + + find ppm signal and supply in receiver. solder wire (computer cdrom wire) + solder the other end to the controller board. + run the test programm (radio calibration) + + + solder the autopilot MCU + try uart 1 + + try spi (write a test with SPI and UART) + + solder the modem + try modem (in line input - with rtty ?? ) + + + +\subsection{ground modem} + same story with mega8 and crystal + check connection, write fuse, program serial test + solder modem + connect to airborne modem + watch telemetry + +\subsection{infrared sensor} + solder amp, resistor and capa and thermopiles. + connect to autopilot MCU ADCs + watch telemetrie values; + +\section{Fitting system in the airframe} + +All the processing available on programmable radio transmitters (travel adj, mixing etc..) are here +done by the fly by wire MCU. This is cool because you don't need to change your transmitter programm +when you change aircraft, but it also enables the autopilot to use these features. + +\subsection{radio control transmitter calibration} +tab in gui. +use the default programm of your rc transmitter with travels set to 100\% and trim centered. +programm the controller board (actually fly by wire MCU) with the test programm (button on gui). +if everything goes well you will see values of the channels in the signal send by your transmitter. +record min max neutral for each channel and setup control +give it a name +generate a configuration file. + + +\subsection{servos travel and mixer setup} + +Mount the board in the airframe and connect servos. programm the board with the servo setting programm (tab in gui) +for each servo, define name of the control, travel neutral and direction. +Try to use maximal travel and long control arms. + +\subsection{infrared sensor} +describe the way to mout it on the airframe and the needed configuration. + + +\section{Simulation} +Don't attempt to fly your aircraft until you've succesfully simulated with your configuration and learned how the system works. There are two type of simulation : + +\subsection{``Software in loop'' simulation} +needs no hardware but the laptop. +It is great to learn while you are building the hardware. +Thanx to our magnificient C compiler, we are able to compile the same code for the AVR mcu and for the i386 laptop. + +\subsection{``Hardware in the loop'' simulation} + + + +\section{Test Flight} + +\subsection{checklist} + +This checklist applies to our twinstar. + +Switch ground station on - connect modem - launch receive and gui - check modem messages + check ground batterie + +Switch rc transmitter on - check programm - all switches pushed - mode auto1 - throttle low + +Switch airplane on - check model name and rc transmitter name + +check "waiting calibration" on ground station - switch to mode manual + +Put airplane on nose - push roll stick - check contrast on ground station + +switch briefly to full throttle to trigger speed controllers + +Check command direction and travel + +Switch to auto1. Check corrections direction (if you put the plane nose down, the elevator should raise, if you bank to the right, the left aileron should raise). + +Check GPS status on ground station. + +Flight briefing - check mission on map + +check autopilot mode + +take off. For automatic take off (auto 2), full throttle will signal take off and trigger full throttle. + + +\subsection{adjusting trim} + + +This first flight is flown in manual mode. It is used to trim the airframe and get an estimation of infrared neutrals. +It is very important that you trim your model perfectly. Choose a day without wind or turbulence. Fly long +straight lines trying not to touch your sticks. + +Watch your batterie voltage on the laptop (ask someone or use a vocal synthetiser). + +After the flight + offset servos to recenter your rc transmitter trims. + get an estimation of contrast\_gain (ir\_gain = contrast\_gain/contrast) comparing your contrast measure and lls + get an estimation of infrared neutrals (pitch and roll) (play the telemetry record during straight lines or maybe write a tool). + get an estimation of throttle for level flight + watch the record for anomalies (describe) + plot parameters (like airspeed, climb rate, current consumption...). This is a great tool for tunning an airframe. + + + +\subsection{adjusting low level loop (attitude loop)} +In this flight you will adjust the infrared neutrals and low level loop gains. +The number of parameter that you are able to tune in a single flight depends on how many switches and sliders are available on your rc transmitter. With the Multiplex MC3030 (9 channels) we have two spare sliders and one three positions +switch. It allows us to tune 4 parameters at a time. + +Update you airframe description. Use the values from the previous flight for infrared neutral and contrast\_gain. Use low values for the low level loop P gains. Reflash your airplane. + +For this flight we will programm an autopilot mode in auto1 which will hold the plane in an attitude described by the +roll and pitch sticks. If you leave your sticks centered, the plane will fly level. If you push your roll stick, the plane will bank to a given value (full travel -> 30°) and stay in this attitude. + + +Take off in manual mode. Gain altitude . +Check that the plane is flying level and that your tranmistter trims are centered. +If this is not the case, redo the programm of the previous flight. + +Engage auto1 mode. Be ready to switch back to manual if the plane doesn't react like you expect. +Switch to neutral calibration and fine tune values so that the plane flies level. +This stage is very important for the navigation to work. +Raise the values of the low level loop P gains until the plane reacts quickly to an attitude change but +without oscillating. + +Now your plane should be capable of holding attitude. This is a very strange feeling for the pilote. + + +After the flight: + plot params + fine tune your contrast gain with LLS measure + update low level P gains and infrared neutrals in model description. + + +\subsection{adjusting autopilot gains} + +In this flight, we will programm a navigation mode in the auto2 bank which will navigate the airplane around waypoints. + +describe a mission: home and waypoints + You can click on a map (needs calibration) or walk with the plane GPS to find the position of your points. + + For our twinstar, the typical missions are two waypoints distant from 300m and at 80m above ground level. + + Flash your airplane. If you have a map, check that the mission that the airplane transmits on boot shows up on the map at the right place. + + After the checklist, check that the position transmitted is near your home (relative position - NAV message) and that the coordinates go in the right direction (X->NORTH Y->EAST FIXME:check!!) + +Take of in manual mode - check plane level and rc trim centered +Switch to auto1 - check plane level +Switch to auto2 - adjust nav P gain and max bank angle for smooth nav + + + +Make more flights taking off in manual - +When your are confident you can take off in auto1 or auto2. Specify desired climb rate and security altitude + + + +\section{Part list and supplyers} + +\subsection{power supply} + +\subsection{controller board} + + +\subsection{infrared sensor} +\begin{tabular}{| l | l | l | l |} +\hline +4 & thermopiles MLX90247ESF-B & www.digikey.com (PN: MLX90247ESF-B-ND) & 16,5 euros each, 12.5 euros >= 10 \\ +\hline +1 & op amp AD8552 TSSOP case & le fabriquant envoie des échantillons - formulaire sur leur site.& \\ +\hline +1 & MOLEX 6 contacts connector 1.25mm pitch & radiospares (PN: 53047-0610) & by 10 - 2,34 euros \\ +\hline +\end{tabular} + + +résistances et condensateurs CMS + + + + + +\section{glossaire} +\begin{description} + \item[ADC] Analog to Digital Converter: A chip or MCU peripheral that converts an analog voltage to its binary representation. + + \item[CPU] Control Processor Unit: + + + \item[MCU] Micro Controller Unit: A chip containing a CPU and varous peripherals like memory, io ports, timer, ADCs etc.. + The paparazzi controller board uses two of these chip. + + +\item[LLS] Linear Least Square: + +\end{description} + +\printindex + + +\end{document} diff --git a/sw/README b/sw/README new file mode 100644 index 00000000000..850deeecdb8 --- /dev/null +++ b/sw/README @@ -0,0 +1,25 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + + + + diff --git a/sw/airborne/autopilot/Makefile b/sw/airborne/autopilot/Makefile new file mode 100644 index 00000000000..51a59436020 --- /dev/null +++ b/sw/airborne/autopilot/Makefile @@ -0,0 +1,92 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + + +FBW=../fly_by_wire + + +LOCAL_CFLAGS= $(CTL_BRD_FLAGS) $(GPS_FLAGS) $(SIMUL_FLAGS) + +VARINCLUDE=$(PAPARAZZI_HOME)/var/include +ACINCLUDE = $(PAPARAZZI_HOME)/var/$(AIRCRAFT) + +ARCH = atmega128 +TARGET = autopilot + +LOW_FUSE = e0 +HIGH_FUSE = 99 + +ifeq ($(CTL_BRD_VERSION),V1_1) +LOW_FUSE = ff +HIGH_FUSE = 89 +CTL_BRD_FLAGS=-DCTL_BRD_V1_1 +endif + +ifeq ($(SIMUL),1) +SIMUL_FLAGS= -DSIMUL +endif + +EXT_FUSE = ff +LOCK_FUSE = ff +INCLUDES = -I $(FBW) -I ../../include -I $(VARINCLUDE) -I $(ACINCLUDE) + +GPS = gps_ubx.c +GPS_FLAGS=-DUBX + +$(TARGET).srcs = \ + main.c \ + modem.c \ + link_fbw.c \ + spi.c \ + adc.c \ + $(GPS) \ + infrared.c \ + pid.c \ + nav.c \ + uart.c \ + estimator.c \ + if_calib.c \ + mainloop.c + +include ../../../conf/Makefile.local +include ../../../conf/Makefile.avr + +autopilot.install : warn_conf + +warn_conf : + @echo + @echo '###########################################################' + @grep AIRFRAME_NAME $(ACINCLUDE)/airframe.h + @grep RADIO_NAME $(ACINCLUDE)/radio.h + @grep FLIGHT_PLAN_NAME $(ACINCLUDE)/flight_plan.h + @echo '###########################################################' + @echo + + +.depend : $(VARINCLUDE)/messages.h $(ACINCLUDE)/flight_plan.h $(VARINCLUDE)/ubx_protocol.h $(ACINCLUDE)/inflight_calib.h $(ACINCLUDE)/airframe.h $(ACINCLUDE)/radio.h +main.o : $(VARINCLUDE)/messages.h +nav.o : $(ACINCLUDE)/flight_plan.h +gps_ubx.o : $(VARINCLUDE)/ubx_protocol.h +if_calib.o : $(ACINCLUDE)/inflight_calib.h + +clean : avr_clean + rm -f *.out *.cm* messages.h flight_plan.h ubx_protocol.h inflight_calib.h diff --git a/sw/airborne/autopilot/README b/sw/airborne/autopilot/README new file mode 100644 index 00000000000..d56db63899e --- /dev/null +++ b/sw/airborne/autopilot/README @@ -0,0 +1,24 @@ +# $Id$ +# Copyright (C) 2003 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + + + + diff --git a/sw/airborne/autopilot/adc.c b/sw/airborne/autopilot/adc.c new file mode 100644 index 00000000000..a15592f15ef --- /dev/null +++ b/sw/airborne/autopilot/adc.c @@ -0,0 +1,126 @@ +/* + * Paparazzi mcu0 adc functions + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * Copyright (C) 2002 Trammell Hudson + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + + +#include +#include +#include +#include "airframe.h" +#include "adc.h" + + +/************************************************************************* + * + * Analog to digital conversion code. + * + * We allow interrupts during the 2048 usec windows. If we run the + * ADC clock faster than Clk/64 we have too much overhead servicing + * the interrupts from it and end up with servo jitter. + * + * For now we've slowed the clock to Clk/128 because it lets us + * be lazy in the interrupt routine. + */ +#define VOLTAGE_TIME 0x07 +#define ANALOG_PORT PORTF +#define ANALOG_PORT_DIR DDRF + + +#ifdef CTL_BRD_V1_1 +#define ANALOG_VREF 0 +#endif + +#if defined CTL_BRD_V1_2 || defined CTL_BRD_V1_2_1 +#define ANALOG_VREF _BV(REFS0) +#endif + +uint16_t adc_samples[ NB_ADC ]; + +static struct adc_buf* buffers[NB_ADC]; + +void adc_buf_channel(uint8_t adc_channel, struct adc_buf* s) { + buffers[adc_channel] = s; +} + +void +adc_init( void ) +{ + uint8_t i; + /* Ensure that our port is for input with no pull-ups */ + ANALOG_PORT = 0x00; + ANALOG_PORT_DIR = 0x00; + + /* Select our external voltage ref, which is tied to Vcc */ + ADMUX = ANALOG_VREF; + + /* Turn off the analog comparator */ + sbi( ACSR, ACD ); + + /* Select out clock, turn on the ADC interrupt and start conversion */ + ADCSR = 0 + | VOLTAGE_TIME + | ( 1 << ADEN ) + | ( 1 << ADIE ) + | ( 1 << ADSC ); + + /* Init to 0 (usefull ?) */ + for(i = 0; i < NB_ADC; i++) + buffers[i] = (struct adc_buf*)0; +} + +/** + * Called when the voltage conversion is finished + * + * 8.913kHz on mega128@16MHz 1kHz/channel ?? +*/ + + +SIGNAL( SIG_ADC ) +{ + uint8_t adc_input = ADMUX & 0x7; + struct adc_buf* buf = buffers[adc_input]; + uint16_t adc_value = ADCW; + /* Store result */ + adc_samples[ adc_input ] = adc_value; + + if (buf) { + uint8_t new_head = buf->head + 1; + if (new_head >= AV_NB_SAMPLE) new_head = 0; + buf->sum -= buf->values[new_head]; + buf->values[new_head] = adc_value; + buf->sum += adc_value; + buf->head = new_head; + } + + /* Find the next input */ + adc_input++; + if( adc_input >= 8 ) + adc_input = 0; + /* Select it */ + ADMUX = adc_input | ANALOG_VREF; + /* Restart the conversion */ + sbi( ADCSR, ADSC ); +} diff --git a/sw/airborne/autopilot/adc.h b/sw/airborne/autopilot/adc.h new file mode 100644 index 00000000000..c64a7719f84 --- /dev/null +++ b/sw/airborne/autopilot/adc.h @@ -0,0 +1,53 @@ +/* + * Paparazzi mcu0 adc functions + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * Copyright (C) 2002 Trammell Hudson + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef _ADC_H_ +#define _ADC_H_ + +#include + + +#define NB_ADC 8 + +/* Array containing the last measured value */ +extern uint16_t adc_samples[ NB_ADC ]; + +void adc_init( void ); + +#define AV_NB_SAMPLE 0x20 + +struct adc_buf { + uint16_t sum; + uint16_t values[AV_NB_SAMPLE]; + uint8_t head; +}; + +/* Facility to store last values in a circular buffer for a specific + channel: allocate a (struct adc_buf) and register it with the following + function */ +void adc_buf_channel(uint8_t adc_channel, struct adc_buf* s); +#endif diff --git a/sw/airborne/autopilot/autopilot.h b/sw/airborne/autopilot/autopilot.h new file mode 100644 index 00000000000..722a2694b4e --- /dev/null +++ b/sw/airborne/autopilot/autopilot.h @@ -0,0 +1,110 @@ +/* + * $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef AUTOPILOT_H +#define AUTOPILOT_H + +#include "link_autopilot.h" + +#define TRESHOLD1 TRESHOLD_MANUAL_PPRZ +#define TRESHOLD2 200 * CLOCK + + +#define PPRZ_MODE_MANUAL 0 +#define PPRZ_MODE_AUTO1 1 +#define PPRZ_MODE_AUTO2 2 +#define PPRZ_MODE_HOME 3 +#define PPRZ_MODE_NB 4 + +#define PPRZ_MODE_OF_PULSE(pprz, mega8_status) \ + (pprz > TRESHOLD2 ? PPRZ_MODE_AUTO2 : \ + (pprz > TRESHOLD1 ? PPRZ_MODE_AUTO1 : PPRZ_MODE_MANUAL)) + +extern uint8_t pprz_mode; + + +#define VERTICAL_MODE_MANUAL 0 +#define VERTICAL_MODE_AUTO_GAZ 1 +#define VERTICAL_MODE_AUTO_CLIMB 2 +#define VERTICAL_MODE_AUTO_ALT 3 +#define VERTICAL_MODE_NB 4 + +#define VERTICAL_MODE_OF_PULSE(pprz) (pprz < TRESHOLD2 ? VERTICAL_MODE_MANUAL: \ + VERTICAL_MODE_AUTO_ALT) + +#define IR_ESTIM_MODE_OFF 0 +#define IR_ESTIM_MODE_ON 1 + +#define IR_ESTIM_MODE_OF_PULSE(pprz) (pprz < TRESHOLD2 ? IR_ESTIM_MODE_OFF: \ + IR_ESTIM_MODE_ON) + +extern uint8_t ir_estim_mode; + +#define STICK_PUSHED(pprz) (pprz < TRESHOLD1 || pprz > TRESHOLD2) + + +#define TRIM_PPRZ(pprz) (pprz < MIN_PPRZ ? MIN_PPRZ : \ + (pprz > MAX_PPRZ ? MAX_PPRZ : \ + pprz)) + +#define TRIM_UPPRZ(pprz) (pprz < 0 ? 0 : \ + (pprz > MAX_PPRZ ? MAX_PPRZ : \ + pprz)) + + +#define FLOAT_OF_PPRZ(pprz, center, travel) ((float)pprz / (float)MAX_PPRZ * travel + center) + +extern uint8_t fatal_error_nb; + +#define GAZ_THRESHOLD_TAKEOFF (pprz_t)(MAX_PPRZ * 0.9) + +extern uint8_t inflight_calib_mode; +//extern uint16_t flight_time; +extern uint8_t vertical_mode; +extern uint8_t vsupply; + +extern bool_t rc_event_1, rc_event_2; + +extern float slider_1_val, slider_2_val; + +extern bool_t launch; + + +#define ModeUpdate(_mode, _value) { \ + uint8_t new_mode = _value; \ + if (_mode != new_mode) { _mode = new_mode; return TRUE; } \ + return FALSE; \ +} + +#define CheckEvent(_event) (_event ? _event = FALSE, TRUE : FALSE) + +#ifdef CTL_BRD_V1_1 +extern struct adc_buf buf_bat; +#endif + +void periodic_task( void ); +void use_gps_pos(void); +void radio_control_task(void); + +#endif /* AUTOPILOT_H */ diff --git a/sw/airborne/autopilot/downlink.h b/sw/airborne/autopilot/downlink.h new file mode 100644 index 00000000000..eee7acd7838 --- /dev/null +++ b/sw/airborne/autopilot/downlink.h @@ -0,0 +1,35 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef DOWNLINK_H +#define DOWNLINK_H + +#include "modem.h" + +#define STX 0x05 +#define ETX 0x06 + +#include "messages.h" + +#endif /* DOWNLINK_H */ diff --git a/sw/airborne/autopilot/estimator.c b/sw/airborne/autopilot/estimator.c new file mode 100644 index 00000000000..370996ce43c --- /dev/null +++ b/sw/airborne/autopilot/estimator.c @@ -0,0 +1,170 @@ +/* + * Paparazzi autopilot $Id$ + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include + +#include "estimator.h" +#include "gps.h" +#include "pid.h" +#include "infrared.h" +#include "autopilot.h" + + +/* position in meters */ +float estimator_x; +float estimator_y; +float estimator_z; + +/* attitude in radian */ +float estimator_phi; +float estimator_psi; +float estimator_theta; + +/* speed in meters per second */ +float estimator_x_dot; +float estimator_y_dot; +float estimator_z_dot; + +/* rotational speed in radians per second */ +float estimator_phi_dot; +float estimator_psi_dot; +float estimator_theta_dot; + +/* flight time in seconds */ +uint16_t estimator_flight_time; +/* flight time in seconds */ +float estimator_t; + +/* horizontal speed in module and dir */ +float estimator_hspeed_mod; +float estimator_hspeed_dir; + +float estimator_rad_of_ir, estimator_ir, estimator_rad; + +#define EstimatorSetPos(x, y, z) { estimator_x = x; estimator_y = y; estimator_z = z; } +#define EstimatorSetAtt(phi, psi, theta) { estimator_phi = phi; estimator_psi = psi; estimator_theta = theta; } + + +// FIXME maybe vz = -climb for NED?? +#define EstimatorSetSpeedCart(vx, vy, vz) { \ + estimator_vx = vx; \ + estimator_vy = vy; \ + estimator_vz = vz; \ +} +// estimator_hspeed_mod = sqrt( estimator_vx * estimator_vx + estimator_vy * estimator_vy); +// estimator_hspeed_dir = atan2(estimator_vy, estimator_vx); + + +#define EstimatorSetSpeedPol(vhmod, vhdir, vz) { \ + estimator_hspeed_mod = vhmod; \ + estimator_hspeed_dir = vhdir; \ + estimator_z_dot = vz; \ +} +//FIXME is this true ?? estimator_vx = estimator_hspeed_mod * cos(estimator_hspeed_dir); +//FIXME is this true ?? estimator_vy = estimator_hspeed_mod * sin(estimator_hspeed_dir); + +#define EstimatorSetRotSpeed(phi_dot, psi_dot, theta_dot) { \ + estimator_phi_dot = phi_dot; \ + estimator_psi_dot = psi_dot; \ + estimator_theta_dot = theta_dot; \ +} + +inline void estimator_update_lls( void ); + +void estimator_init( void ) { + + EstimatorSetPos (0., 0., 0.); + + EstimatorSetAtt (0., 0., 0); + + EstimatorSetSpeedPol ( 0., 0., 0.); + + EstimatorSetRotSpeed (0., 0., 0.); + + estimator_flight_time = 0; + + estimator_rad_of_ir = ir_rad_of_ir; +} + +#define EstimatorIrGainIsCorrect() (TRUE) + +void estimator_update_state_infrared( void ) { + float rad_of_ir = (ir_estim_mode == IR_ESTIM_MODE_ON && EstimatorIrGainIsCorrect()) ? + estimator_rad_of_ir : ir_rad_of_ir; + estimator_phi = rad_of_ir * ir_roll; + + estimator_theta = rad_of_ir * ir_pitch; +} + +#define INIT_WEIGHT 100. /* The number of times the initial value has to be taken */ +#define INIT_IR2 (50.*50.)/* Ir value used for initialization */ +#define RHO 0.999 /* The higher, the slower the estimation is changing */ + +#define g 9.81 + +void estimator_update_ir_estim( void ) { + static float last_hspeed_dir; + static float last_t; + static bool_t initialized = FALSE; + static float sum_xy, sum_xx; + + if (initialized) { + float dt = gps_ftow - last_t; + if (dt > 0.1) { // Against division by zero + float phi = (estimator_hspeed_dir - last_hspeed_dir)/dt*NOMINAL_AIRSPEED/g; /* tan linearized */ + NORM_RAD_ANGLE(phi); + estimator_ir = (float)ir_roll; + estimator_rad = phi; + float absphi = fabs(phi); + if (absphi < 1.0 && absphi > 0.05 && (- ir_contrast/2 < ir_roll && ir_roll < ir_contrast/2)) { + sum_xy = estimator_rad * estimator_ir + RHO * sum_xy; + sum_xx = estimator_ir * estimator_ir + RHO * sum_xx; + estimator_rad_of_ir = sum_xy / sum_xx; + } + } + } else { + initialized = TRUE; + sum_xy = INIT_WEIGHT * estimator_rad_of_ir * INIT_IR2; + sum_xx = INIT_WEIGHT * INIT_IR2; + } + + last_hspeed_dir = estimator_hspeed_dir; + last_t = gps_ftow; +} + + +void estimator_update_state_gps( void ) { + if (GPS_FIX_VALID(gps_mode)) { + EstimatorSetPos(gps_east, gps_north, gps_falt); + EstimatorSetSpeedPol(gps_fspeed, gps_fcourse, gps_fclimb); + + if (estimator_flight_time) + estimator_update_ir_estim(); + } +} + +void estimator_propagate_state( void ) { + +} diff --git a/sw/airborne/autopilot/estimator.h b/sw/airborne/autopilot/estimator.h new file mode 100644 index 00000000000..4aa40438ba5 --- /dev/null +++ b/sw/airborne/autopilot/estimator.h @@ -0,0 +1,67 @@ +/* + * $Id$ + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef ESTIMATOR_H +#define ESTIMATOR_H + +#include + +/* position in meters */ +extern float estimator_x; +extern float estimator_y; +extern float estimator_z; + +/* attitude in radians */ +extern float estimator_phi; +extern float estimator_psi; +extern float estimator_theta; + +/* speed in meters per second */ +extern float estimator_x_dot; +extern float estimator_y_dot; +extern float estimator_z_dot; + +/* rotational speed in radians per second */ +extern float estimator_phi_dot; +extern float estimator_psi_dot; +extern float estimator_teta_dot; + +/* flight time in seconds */ +extern uint16_t estimator_flight_time; +extern float estimator_t; + +/* horizontal speed in module and dir (m/s, rad) */ +extern float estimator_hspeed_mod; +extern float estimator_hspeed_dir; + +void estimator_init( void ); +void estimator_update_state_infrared( void ); +void estimator_update_state_gps( void ); +void estimator_propagate_state( void ); + +extern float estimator_rad_of_ir, estimator_ir, estimator_rad; + + + +#endif /* ESTIMATOR_H */ diff --git a/sw/airborne/autopilot/gps.h b/sw/airborne/autopilot/gps.h new file mode 100644 index 00000000000..ad873dfefaa --- /dev/null +++ b/sw/airborne/autopilot/gps.h @@ -0,0 +1,58 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +/* + * Parse SIRF protocol from ublox SAM module + * +*/ + + +#ifndef GPS_H +#define GPS_H + +#include "std.h" + +extern uint8_t gps_mode; +extern float gps_ftow; /* ms */ +extern float gps_falt; /* m */ +extern float gps_fspeed; /* m/s */ +extern float gps_fclimb; /* m/s */ +extern float gps_fcourse; /* rad */ +extern int32_t gps_utm_east, gps_utm_north; +extern float gps_east, gps_north; /* m */ + +void gps_init( void ); +void parse_gps_msg( void ); +extern volatile uint8_t gps_msg_received; +extern bool_t gps_pos_available; +extern uint8_t gps_nb_ovrn; + +#ifdef UBX +#include "ubx.h" +#else +#include "sirf.h" +#endif + + +#endif /* GPS_H */ diff --git a/sw/airborne/autopilot/gps_sirf.c b/sw/airborne/autopilot/gps_sirf.c new file mode 100644 index 00000000000..50ba3ca99e1 --- /dev/null +++ b/sw/airborne/autopilot/gps_sirf.c @@ -0,0 +1,235 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include +#include + + +#include "uart.h" +#include "gps.h" + +float gps_falt; +float gps_fspeed; +float gps_fclimb; +float gps_fcourse; +uint8_t gps_mode; +volatile bool_t gps_msg_received; +bool_t gps_pos_available; + + +#define SIRF_MAX_PAYLOAD 255 +uint8_t sirf_msg_buf[SIRF_MAX_PAYLOAD]; + +#define READ_INT32_AT_OFFSET(offset, dest) \ +{ \ + dest[0] = sirf_msg_buf[offset+3]; \ + dest[1] = sirf_msg_buf[offset+2]; \ + dest[2] = sirf_msg_buf[offset+1]; \ + dest[3] = sirf_msg_buf[offset]; \ +} \ +/* ext nav type = 0x62 + offset len + type 0 1 + lat 1 4 + lon 5 4 + alt 9 4 + speed 13 4 + climb 17 4 + course 21 4 + mode 25 1 +*/ +void parse_gps_msg( void ) { + static int32_t tmp_int32; + uint8_t *tmp = (uint8_t*)&tmp_int32; + + READ_INT32_AT_OFFSET(1, tmp); + gps_lat = tmp_int32; + + READ_INT32_AT_OFFSET(5, tmp); + gps_lon = tmp_int32; + + READ_INT32_AT_OFFSET(9, tmp); + gps_falt = (float)tmp_int32 / 1e3; + + READ_INT32_AT_OFFSET(13, tmp); + gps_fspeed = (float)tmp_int32 / 1e3; + + READ_INT32_AT_OFFSET(17, tmp); + gps_fclimb = (float)tmp_int32 / 1e3; + + READ_INT32_AT_OFFSET(21, tmp); + gps_fcourse = (float)tmp_int32 / 1e8; + + gps_mode = sirf_msg_buf[25]; + + gps_pos_available = TRUE; +} + + + + + + +void gps_init( void ) { + /* Enable uart */ +#ifdef SIMUL + uart0_init(); +#else + uart1_init(); +#endif +} + +#define SIRF_START1 0xA0 +#define SIRF_START2 0xA2 +#define SIRF_END1 0xB0 +#define SIRF_END2 0xB3 + +#ifdef SIMUL +#define IR_START 0xA1 /* simulator/mc.ml */ +volatile int16_t simul_ir_roll; +volatile int16_t simul_ir_pitch; +#endif + +#define SIRF_TYP_NAV 0x02 +#define SIRF_TYP_EXT_NAV 0x62 + +#define UNINIT 0 +#define GOT_START1 1 +#define GOT_START2 2 +#define GOT_LEN1 3 +#define GOT_LEN2 4 +#define GOT_PAYLOAD 5 +#define GOT_CHECKSUM1 6 +#define GOT_CHECKSUM2 7 +#define GOT_END1 8 +#ifdef SIMUL +#define GOT_IR_START 9 +#define GOT_IR1 10 +#define GOT_IR2 11 +#define GOT_IR3 12 +#endif + +static uint8_t sirf_status; +static uint16_t sirf_len; +static uint16_t sirf_checksum; +static uint8_t sirf_type; +static uint8_t sirf_msg_idx; + + +static inline void parse_sirf( uint8_t c ) { + switch (sirf_status) { + case UNINIT: + if (c == SIRF_START1) + sirf_status++; +#ifdef SIMUL + if (c == IR_START) + sirf_status = GOT_IR_START; +#endif + break; + case GOT_START1: + if (c != SIRF_START2) + goto error; + sirf_status++; + break; + case GOT_START2: + sirf_len = (c<<8) & 0xFF00; + sirf_status++; + break; + case GOT_LEN1: + sirf_len += (c & 0x00FF); + if (sirf_len > SIRF_MAX_PAYLOAD) + goto error; + sirf_msg_idx = 0; + sirf_status++; + break; + case GOT_LEN2: + if (sirf_msg_idx==0) { + sirf_type = c; + } + if (sirf_type == SIRF_TYP_EXT_NAV) + sirf_msg_buf[sirf_msg_idx] = c; + sirf_msg_idx++; + if (sirf_msg_idx >= sirf_len) { + sirf_status++; + } + break; + case GOT_PAYLOAD: + sirf_checksum = (c<<8) & 0xFF00; + sirf_status++; + break; + case GOT_CHECKSUM1: + sirf_checksum += (c & 0x00FF); + /* fixme: check correct */ + sirf_status++; + break; + case GOT_CHECKSUM2: + if (c != SIRF_END1) + goto error; + sirf_status++; + break; + case GOT_END1: + if (c != SIRF_END2) + goto error; + + if (sirf_type == SIRF_TYP_EXT_NAV) + gps_msg_received = TRUE; + goto restart; + break; +#ifdef SIMUL + case GOT_IR_START: + simul_ir_roll = c << 8; + sirf_status++; + break; + case GOT_IR1: + simul_ir_roll |= c; + sirf_status++; + break; + case GOT_IR2: + simul_ir_pitch = c << 8; + sirf_status++; + break; + case GOT_IR3: + simul_ir_pitch |= c; + goto restart; + break; +#endif + } + return; + error: + // modem_putc('r'); + restart: + // modem_putc('\n'); + sirf_status = UNINIT; + sirf_checksum = 0; + return; +} + +#ifdef SIMUL +ReceiveUart0(parse_sirf); +#else +ReceiveUart1(parse_sirf); +#endif diff --git a/sw/airborne/autopilot/gps_ubx.c b/sw/airborne/autopilot/gps_ubx.c new file mode 100644 index 00000000000..3d952cd53ee --- /dev/null +++ b/sw/airborne/autopilot/gps_ubx.c @@ -0,0 +1,207 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include +#include +#include + + +#include "flight_plan.h" +#include "uart.h" +#include "gps.h" +#include "ubx_protocol.h" +#include "flight_plan.h" + +float gps_ftow; +float gps_falt; +float gps_fspeed; +float gps_fclimb; +float gps_fcourse; +int32_t gps_utm_east, gps_utm_north; +float gps_east, gps_north; +uint8_t gps_mode; +volatile bool_t gps_msg_received; +bool_t gps_pos_available; +const int32_t utm_east0 = NAV_UTM_EAST0; +const int32_t utm_north0 = NAV_UTM_NORTH0; + +#define UBX_MAX_PAYLOAD 255 +static uint8_t ubx_msg_buf[UBX_MAX_PAYLOAD]; + +#define RadianOfDeg(d) ((d)/180.*3.1415927) + +#ifdef SIMUL +#include "infrared.h" +#define IR_START 0xA1 /* simulator/mc.ml */ +volatile int16_t simul_ir_roll; +volatile int16_t simul_ir_pitch; +#endif + +#define UNINIT 0 +#define GOT_SYNC1 1 +#define GOT_SYNC2 2 +#define GOT_CLASS 3 +#define GOT_ID 4 +#define GOT_LEN1 5 +#define GOT_LEN2 6 +#define GOT_PAYLOAD 7 +#define GOT_CHECKSUM1 8 +#ifdef SIMUL +#define GOT_IR_START 20 +#define GOT_IR1 21 +#define GOT_IR2 22 +#define GOT_IR3 23 +#endif + +static uint8_t ubx_status; +static uint16_t ubx_len; +static uint8_t ubx_msg_idx; +static uint8_t ck_a, ck_b, ubx_id, ubx_class; + +void gps_init( void ) { + /* Enable uart */ +#ifdef SIMUL + uart0_init(); + simul_ir_roll = ir_roll_neutral; + simul_ir_pitch = ir_pitch_neutral; +#else + uart1_init(); +#endif + ubx_status = UNINIT; +} + +void parse_gps_msg( void ) { + if (ubx_class == UBX_NAV_ID) { + if (ubx_id == UBX_NAV_POSUTM_ID) { + gps_utm_east = UBX_NAV_POSUTM_EAST(ubx_msg_buf); + gps_utm_north = UBX_NAV_POSUTM_NORTH(ubx_msg_buf); + gps_falt = (float)(UBX_NAV_POSUTM_ALT(ubx_msg_buf)/100); + } else if (ubx_id == UBX_NAV_STATUS_ID) { + gps_mode = UBX_NAV_STATUS_GPSfix(ubx_msg_buf); + } else if (ubx_id == UBX_NAV_VELNED_ID) { + gps_fspeed = ((float)UBX_NAV_VELNED_GSpeed(ubx_msg_buf)) / 1e2; + gps_fclimb = ((float)UBX_NAV_VELNED_VEL_D(ubx_msg_buf)) / -1e2; + gps_fcourse = RadianOfDeg(((float)UBX_NAV_VELNED_Heading(ubx_msg_buf)) / 1e5); + gps_ftow = ((float)UBX_NAV_VELNED_ITOW(ubx_msg_buf)) / 1e3; + + gps_east = gps_utm_east / 100 - NAV_UTM_EAST0; + gps_north = gps_utm_north / 100 - NAV_UTM_NORTH0; + + + gps_pos_available = TRUE; /* The 3 UBX messages are sent in one rafale */ + } + } +#ifdef SIMUL + if (ubx_class == UBX_USR_ID) { + if (ubx_id == UBX_USR_IRSIM_ID) { + simul_ir_roll = UBX_USR_IRSIM_ROLL(ubx_msg_buf); + simul_ir_pitch = UBX_USR_IRSIM_PITCH(ubx_msg_buf); + } + } +#endif + + + +} + + +uint8_t gps_nb_ovrn; + + +static inline void parse_ubx( uint8_t c ) { + if (ubx_status < GOT_PAYLOAD) { + ck_a += c; + ck_b += ck_a; + } + switch (ubx_status) { + case UNINIT: + if (c == UBX_SYNC1) + ubx_status++; + break; + case GOT_SYNC1: + if (c != UBX_SYNC2) + goto error; + ck_a = 0; + ck_b = 0; + ubx_status++; + break; + case GOT_SYNC2: + if (gps_msg_received) { + /* Previous message has not yet been parsed: discard this one */ + gps_nb_ovrn++; + goto error; + } + ubx_class = c; + ubx_status++; + break; + case GOT_CLASS: + ubx_id = c; + ubx_status++; + break; + case GOT_ID: + ubx_len = c; + ubx_status++; + break; + case GOT_LEN1: + ubx_len |= (c<<8); + if (ubx_len > UBX_MAX_PAYLOAD) + goto error; + ubx_msg_idx = 0; + ubx_status++; + break; + case GOT_LEN2: + ubx_msg_buf[ubx_msg_idx] = c; + ubx_msg_idx++; + if (ubx_msg_idx >= ubx_len) { + ubx_status++; + } + break; + case GOT_PAYLOAD: + if (c != ck_a) + goto error; + ubx_status++; + break; + case GOT_CHECKSUM1: + if (c != ck_b) + goto error; + gps_msg_received = TRUE; + goto restart; + break; + } + return; + error: + restart: + ubx_status = UNINIT; + return; +} + +#ifdef SIMUL +ReceiveUart0(parse_ubx); +#else +ReceiveUart1(parse_ubx); +#endif + diff --git a/sw/airborne/autopilot/if_calib.c b/sw/airborne/autopilot/if_calib.c new file mode 100644 index 00000000000..e13535365df --- /dev/null +++ b/sw/airborne/autopilot/if_calib.c @@ -0,0 +1,94 @@ +/* + * $Id$ + * Flight-time calibration facility + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + + +#include +#include "radio.h" +#include "autopilot.h" +#include "if_calib.h" +#include "infrared.h" +#include "pid.h" +#include "nav.h" + + +#define ParamValInt16(param_init_val, param_travel, cur_pulse, init_pulse) \ +(param_init_val + (int16_t)(((float)(cur_pulse - init_pulse)) * param_travel / (float)MAX_PPRZ)) + +#define ParamValFloat(param_init_val, param_travel, cur_pulse, init_pulse) \ +(param_init_val + ((float)(cur_pulse - init_pulse)) * param_travel / (float)MAX_PPRZ) + + + +uint8_t inflight_calib_mode = IF_CALIB_MODE_NONE; + +static int16_t slider1_init, slider2_init; + +#include "inflight_calib.h" + + +/*** +inline uint8_t inflight_calib(void) { + static int16_t slider1_init, slider2_init; + //static float ir_gain_init; + //static float roll_pgain_init; + static float course_pgain_init; + static int16_t roll_neutral_init; + static float pitch_pgain_init; + static int16_t pitch_neutral_init; + + int8_t mode_changed = inflight_calib_mode_update(); + + if (inflight_calib_mode == IF_CALIB_MODE_NEUTRAL) { + if (mode_changed) { + pitch_neutral_init = ir_pitch_neutral; + roll_neutral_init = ir_roll_neutral; + slider1_init = from_fbw.channels[RADIO_GAIN1]; + slider2_init = from_fbw.channels[RADIO_GAIN2]; + } + ir_pitch_neutral = PARAM_VAL_INT16( pitch_neutral_init, -60., from_fbw.channels[RADIO_GAIN1], slider1_init); + ir_roll_neutral = PARAM_VAL_INT16( roll_neutral_init, 60., from_fbw.channels[RADIO_GAIN2], slider2_init); + } + else if (inflight_calib_mode == IF_CALIB_MODE_GAIN) { + if (mode_changed) { + // ir_gain_init = ir_gain; + course_pgain_init = course_pgain; + // roll_pgain_init = roll_pgain; + pitch_pgain_init = pitch_pgain; + slider1_init = from_fbw.channels[RADIO_GAIN1]; + slider2_init = from_fbw.channels[RADIO_GAIN2]; + } + course_pgain = PARAM_VAL_FLOAT( course_pgain_init, -0.1, from_fbw.channels[RADIO_GAIN1], slider1_init); + // ir_gain = PARAM_VAL_FLOAT( ir_gain_init, 0.0015, from_fbw.channels[RADIO_GAIN2], slider2_init); + // roll_pgain = PARAM_VAL_FLOAT( roll_pgain_init, -5000., from_fbw.channels[RADIO_GAIN2], slider1_init); + pitch_pgain = PARAM_VAL_FLOAT( pitch_pgain_init, -5000., from_fbw.channels[RADIO_GAIN1], slider1_init); + } + return (mode_changed); +} +***/ + + + + + diff --git a/sw/airborne/autopilot/if_calib.h b/sw/airborne/autopilot/if_calib.h new file mode 100644 index 00000000000..9a897994105 --- /dev/null +++ b/sw/airborne/autopilot/if_calib.h @@ -0,0 +1,18 @@ +#ifndef IF_CALIB_H + +#include "link_fbw.h" + +extern uint8_t inflight_calib_mode; +void inflight_calib(bool_t calib_mode_changed); + + +#define IF_CALIB_MODE_NONE 0 +#define IF_CALIB_MODE_DOWN 1 +#define IF_CALIB_MODE_UP 2 + +#define IF_CALIB_MODE_OF_PULSE(pprz) (pprz < TRESHOLD1 ? IF_CALIB_MODE_UP : \ + (pprz < TRESHOLD2 ? IF_CALIB_MODE_NONE : \ + IF_CALIB_MODE_DOWN)) + + +#endif // IF_CALIB_H diff --git a/sw/airborne/autopilot/infrared.c b/sw/airborne/autopilot/infrared.c new file mode 100644 index 00000000000..0c120c13d1f --- /dev/null +++ b/sw/airborne/autopilot/infrared.c @@ -0,0 +1,71 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include "adc.h" +#include "infrared.h" +#include "autopilot.h" +#include "estimator.h" + +int16_t ir_roll; +int16_t ir_pitch; + +int16_t ir_contrast = IR_DEFAULT_CONTRAST; +int16_t ir_roll_neutral = IR_ROLL_NEUTRAL_DEFAULT; +int16_t ir_pitch_neutral = IR_PITCH_NEUTRAL_DEFAULT; + +#define RadOfIrFromConstrast(c) ir_rad_of_ir = IR_RAD_OF_IR_CONTRAST / c; + +float ir_rad_of_ir = IR_RAD_OF_IR_CONTRAST / IR_DEFAULT_CONTRAST; + +static struct adc_buf buf_ir1; +static struct adc_buf buf_ir2; + +void ir_init(void) { + RadOfIrFromConstrast(IR_DEFAULT_CONTRAST); + adc_buf_channel(ADC_CHANNEL_IR1, &buf_ir1); + adc_buf_channel(ADC_CHANNEL_IR2, &buf_ir2); +} + +void ir_update(void) { +#ifndef SIMUL + int16_t x1_mean = buf_ir1.sum/AV_NB_SAMPLE; + int16_t x2_mean = buf_ir2.sum/AV_NB_SAMPLE; + ir_roll = IR_RollOfIrs(x1_mean, x2_mean) - ir_roll_neutral; + ir_pitch = IR_PitchOfIrs(x1_mean, x2_mean) - ir_pitch_neutral; +#else + extern volatile int16_t simul_ir_roll, simul_ir_pitch; + ir_roll = simul_ir_roll - ir_roll_neutral; + ir_pitch = simul_ir_pitch - ir_pitch_neutral; +#endif +} + +/* + Contrast measurement +*/ + +void ir_gain_calib(void) { // Plane nose down + /* plane nose down -> negativ value */ + ir_contrast = - ir_pitch; + RadOfIrFromConstrast(ir_contrast); +} diff --git a/sw/airborne/autopilot/infrared.h b/sw/airborne/autopilot/infrared.h new file mode 100644 index 00000000000..2823e8cd11b --- /dev/null +++ b/sw/airborne/autopilot/infrared.h @@ -0,0 +1,44 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef INFRARED_H +#define INFRARED_H + +#define AXIS_1_CHANNEL 4 /* P */ +#define AXIS_2_CHANNEL 5 /* other one */ + +extern int16_t ir_roll; /* averaged roll adc */ +extern int16_t ir_pitch; /* averaged pitch adc */ + + +extern float ir_rad_of_ir; +extern int16_t ir_contrast; +extern int16_t ir_roll_neutral; +extern int16_t ir_pitch_neutral; + +void ir_init(void); +void ir_update(void); +void ir_gain_calib(void); + +#endif /* INFRARED_H */ diff --git a/sw/airborne/autopilot/link_fbw.c b/sw/airborne/autopilot/link_fbw.c new file mode 100644 index 00000000000..a64f92a8818 --- /dev/null +++ b/sw/airborne/autopilot/link_fbw.c @@ -0,0 +1,122 @@ +/* + * $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include + +#include "link_fbw.h" +#include "spi.h" + +struct inter_mcu_msg from_fbw; +struct inter_mcu_msg to_fbw; +volatile uint8_t link_fbw_receive_complete = TRUE; +volatile uint8_t link_fbw_receive_valid = FALSE; +volatile uint8_t link_fbw_nb_err; +uint8_t link_fbw_fbw_nb_err; + +static uint8_t idx_buf; +static uint8_t xor_in, xor_out; + +void link_fbw_init(void) { + link_fbw_nb_err; + link_fbw_receive_complete = FALSE; +} + +void link_fbw_send(void) { + if (spi_cur_slave != SPI_NONE) { + spi_nb_ovrn++; + return; + } + + /* Enable SPI, Master, set clock rate fck/16 */ + SPI_START(_BV(SPE) | _BV(MSTR) | _BV(SPR0)); // | _BV(SPR1); + SPI_SELECT_SLAVE0(); + + idx_buf = 0; + xor_in = 0; + xor_out = ((uint8_t*)&to_fbw)[idx_buf]; + SPDR = xor_out; + link_fbw_receive_valid = FALSE; + // Other bytes will follow SIG_SPI interrupts +} + +void link_fbw_on_spi_it( void ) { + /* setup OCR1A to pop in 200 clock cycles */ + /* this leaves time for the slave (fbw) */ + /* to process the byte we've sent and to */ + /* prepare a new one to be sent */ + OCR1A = TCNT1 + 200; + /* clear interrupt flag */ + sbi(TIFR, OCF1A); + /* enable OC1A interrupt */ + sbi(TIMSK, OCIE1A); +} + + +/* send the next byte */ +SIGNAL(SIG_OUTPUT_COMPARE1A) { + uint8_t tmp; + + /* disable OC1A interrupt */ + cbi(TIMSK, OCIE1A); + + idx_buf++; + + /* we have sent/received a complete frame */ + if (idx_buf == FRAME_LENGTH) { + /* read checksum from receive register */ + tmp = SPDR; + /* notify valid frame */ + if (tmp == xor_in) { + link_fbw_receive_valid = TRUE; + link_fbw_fbw_nb_err = from_fbw.nb_err; + } + else + link_fbw_nb_err++; + link_fbw_receive_complete = TRUE; + /* unselect slave0 */ + SPI_UNSELECT_SLAVE0(); + SPI_STOP(); + return; + } + + /* we are sending/receiving payload */ + if (idx_buf < FRAME_LENGTH - 1) { + /* place new payload byte in send register */ + tmp = ((uint8_t*)&to_fbw)[idx_buf]; + SPI_SEND(tmp); + xor_out ^= tmp; + } + /* we are done sending the payload */ + else { // idx_buf == FRAME_LENGTH - 1 + /* place checksum in send register */ + SPI_SEND(xor_out); + } + + /* read the byte from receive register */ + tmp = SPDR; + ((uint8_t*)&from_fbw)[idx_buf-1] = tmp; + xor_in ^= tmp; +} diff --git a/sw/airborne/autopilot/link_fbw.h b/sw/airborne/autopilot/link_fbw.h new file mode 100644 index 00000000000..ce7acf52ad8 --- /dev/null +++ b/sw/airborne/autopilot/link_fbw.h @@ -0,0 +1,44 @@ +/* + * $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef LINK_FBW_H +#define LINK_FBW_H + +#include + +#include "link_autopilot.h" + +void link_fbw_init(void); +void link_fbw_send(void); +void link_fbw_on_spi_it(void); + +extern volatile uint8_t link_fbw_nb_err; +extern uint8_t link_fbw_fbw_nb_err; + +extern struct inter_mcu_msg from_fbw; +extern struct inter_mcu_msg to_fbw; +extern volatile uint8_t link_fbw_receive_complete; +extern volatile uint8_t link_fbw_receive_valid; + +#endif /* LINK_FBW_H */ diff --git a/sw/airborne/autopilot/lls.c b/sw/airborne/autopilot/lls.c new file mode 100644 index 00000000000..85100f9d91e --- /dev/null +++ b/sw/airborne/autopilot/lls.c @@ -0,0 +1,68 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +/* Linear Least Square regression */ + +#include "lls.h" +#include "infrared.h" + +float lls_a; +float lls_b; +float lls_x; +float lls_y; + +void lls_init() { + float lls_a_init = ir_gain; + float lls_b_init = (float)(-ir_roll_neutral) * lls_a_init; + lls_x = ir_roll_neutral + LLS_IR_HALF_INTERVAL; + lls_y = lls_a_init * lls_x + lls_b_init; + lls_update(); + lls_x = ir_roll_neutral - LLS_IR_HALF_INTERVAL; + lls_y = lls_a_init * lls_x + lls_b_init; + lls_update(); +} + +void lls_update() { + static float sum_x = 0.; + static float sum_y = 0.; + static float sum_xy = 0.; + static float sum_x2 = 0.; + static uint16_t n = 0; + float fn; + float mean_x, mean_y, c_xy, s2_x; + + n++; + sum_x += lls_x; + sum_y += lls_y; + sum_xy += lls_x * lls_y; + sum_x2 += lls_x * lls_x; + fn = (float)n; + + mean_x = sum_x / fn; + mean_y = sum_y / fn; + c_xy = mean_x * mean_y + (sum_xy - mean_x * sum_y - mean_y * sum_x ) / fn; + s2_x = mean_x * mean_x + (sum_x2 - 2* mean_x * sum_x) / fn; + lls_a = c_xy / s2_x; + lls_b = mean_y - lls_a * mean_x; +} diff --git a/sw/airborne/autopilot/lls.h b/sw/airborne/autopilot/lls.h new file mode 100644 index 00000000000..f57c3018444 --- /dev/null +++ b/sw/airborne/autopilot/lls.h @@ -0,0 +1,42 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +/* Linear Least Square regression : y = lls_a * x + lls_b */ + +#ifndef LLS_H +#define LLS_H + +#include + +#define LLS_IR_HALF_INTERVAL 150 + +extern float lls_a; +extern float lls_b; +extern float lls_x; +extern float lls_y; + +void lls_update(void); +void lls_init(void); + +#endif diff --git a/sw/airborne/autopilot/main.c b/sw/airborne/autopilot/main.c new file mode 100644 index 00000000000..d4cdae1e651 --- /dev/null +++ b/sw/airborne/autopilot/main.c @@ -0,0 +1,377 @@ +/* + * $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include + + +#include "link_autopilot.h" + +#include "timer.h" +#include "adc.h" +#include "pid.h" +#include "gps.h" +#include "infrared.h" +#include "downlink.h" +#include "nav.h" +#include "autopilot.h" +#include "estimator.h" +#include "if_calib.h" + +// +// +// FIXME estimator_flight_time should not be manipuled here anymore +// +#define MIN_SPEED_FOR_TAKEOFF 5. // m/s + + +uint8_t fatal_error_nb = 0; +static const uint16_t version = 1; + +static uint16_t cputime = 0; // seconds + +uint8_t pprz_mode = PPRZ_MODE_MANUAL; +uint8_t vertical_mode = VERTICAL_MODE_MANUAL; +uint8_t ir_estim_mode = IR_ESTIM_MODE_ON; + +bool_t rc_event_1, rc_event_2; + +uint8_t vsupply; + +static uint8_t mcu1_status, mcu1_ppm_cpt; + +static bool_t low_battery = FALSE; + +#ifdef CTL_BRD_V1_1 +struct adc_buf buf_bat; +#endif + +float slider_1_val, slider_2_val; + +bool_t launch = FALSE; + + +#define Min(x, y) (x < y ? x : y) +#define Max(x, y) (x > y ? x : y) + + +#define NO_CALIB 0 +#define WAITING_CALIB_CONTRAST 1 +#define CALIB_DONE 2 + +#define MAX_DELAY_FOR_CALIBRATION 10 + + +inline void ground_calibrate(void) { + static uint8_t calib_status = NO_CALIB; + switch (calib_status) { + case NO_CALIB: + if (cputime < MAX_DELAY_FOR_CALIBRATION && pprz_mode == PPRZ_MODE_AUTO1 ) { + calib_status = WAITING_CALIB_CONTRAST; + DOWNLINK_SEND_CALIB_START(); + } + break; + case WAITING_CALIB_CONTRAST: + if (STICK_PUSHED(from_fbw.channels[RADIO_ROLL])) { + ir_gain_calib(); + estimator_rad_of_ir = ir_rad_of_ir; + DOWNLINK_SEND_RAD_OF_IR(&estimator_ir, &estimator_rad, &estimator_rad_of_ir, &ir_roll_neutral, &ir_pitch_neutral); + calib_status = CALIB_DONE; + DOWNLINK_SEND_CALIB_CONTRAST(&ir_contrast); + } + break; + case CALIB_DONE: + break; + } +} + + + +inline uint8_t pprz_mode_update( void ) { + /* We remain in home mode until explicit reset from the RC */ + if (pprz_mode != PPRZ_MODE_HOME || CheckEvent(rc_event_1)) { + ModeUpdate(pprz_mode, PPRZ_MODE_OF_PULSE(from_fbw.channels[RADIO_MODE], from_fbw.status)); + nav_stage = 0; /* Restart the last current block */ + } else + return FALSE; +} + +#ifdef RADIO_LLS +inline uint8_t ir_estim_mode_update( void ) { + ModeUpdate(ir_estim_mode, IR_ESTIM_MODE_OF_PULSE(from_fbw.channels[RADIO_LLS])); +} +#endif + + +inline uint8_t mcu1_status_update( void ) { + uint8_t new_mode = from_fbw.status; + if (mcu1_status != new_mode) { + bool_t changed = ((mcu1_status&MASK_FBW_CHANGED) != (new_mode&MASK_FBW_CHANGED)); + mcu1_status = new_mode; + return changed; + } + return FALSE; +} + +#define EVENT_DELAY 20 + +#define EventUpdate(_cpt, _cond, _event) \ + if (_cond) { \ + if (_cpt < EVENT_DELAY) { \ + _cpt++; \ + if (_cpt == EVENT_DELAY) \ + _event = TRUE; \ + } \ + } else { \ + _cpt = 0; \ + _event = FALSE; \ + } +#define EventPos(_cpt, _channel, _event) \ + EventUpdate(_cpt, (inflight_calib_mode==IF_CALIB_MODE_NONE && from_fbw.channels[_channel]>MAX_PPRZ/2), _event) + +#define EventNeg(_cpt, _channel, _event) \ + EventUpdate(_cpt, (inflight_calib_mode==IF_CALIB_MODE_NONE && from_fbw.channels[_channel]<-MAX_PPRZ/2), _event) + +static inline void events_update(void) { + static uint16_t event1_cpt = 0; + EventPos(event1_cpt, RADIO_GAIN1, rc_event_1); + static uint16_t event2_cpt = 0; + EventNeg(event2_cpt, RADIO_GAIN1, rc_event_2); +} + + +/* Send back uncontrolled channels (only rudder) */ +inline void copy_from_to_fbw ( void ) { + to_fbw.channels[RADIO_YAW] = from_fbw.channels[RADIO_YAW]; + to_fbw.status = 0; +} + +#ifdef EST_TEST +float est_pos_x; +float est_pos_y; +float est_fcourse; +uint8_t ticks_last_est; // 20Hz +#endif /* EST_TEST */ + + + +/* + called at 20Hz. + sends a serie of initialisation messages followed by a stream of periodic ones +*/ + +#define INIT_MSG_NB 2 +#define HI_FREQ_PHASE_NB 5 + +static char ac_ident[16] = AIRFRAME_NAME; + +#define PERIODIC_SEND_BAT() DOWNLINK_SEND_BAT(&vsupply, &estimator_flight_time, &low_battery, &block_time, &stage_time) +#define PERIODIC_SEND_DEBUG() DOWNLINK_SEND_DEBUG(&link_fbw_nb_err, &link_fbw_fbw_nb_err, &modem_nb_ovrn, &gps_nb_ovrn, &mcu1_ppm_cpt); +#define PERIODIC_SEND_ATTITUDE() DOWNLINK_SEND_ATTITUDE(&estimator_phi, &estimator_psi, &estimator_theta); +#define PERIODIC_SEND_ADC() DOWNLINK_SEND_ADC(&ir_roll, &ir_pitch); +#define PERIODIC_SEND_STABILISATION() DOWNLINK_SEND_STABILISATION(&roll_pgain, &pitch_pgain); +#define PERIODIC_SEND_CLIMB_PID() DOWNLINK_SEND_CLIMB_PID(&desired_gaz, &desired_climb, &climb_sum_err, &climb_pgain); +#define PERIODIC_SEND_PPRZ_MODE() DOWNLINK_SEND_PPRZ_MODE(&pprz_mode, &vertical_mode, &inflight_calib_mode, &mcu1_status, &ir_estim_mode); +#define PERIODIC_SEND_DESIRED() DOWNLINK_SEND_DESIRED(&desired_roll, &desired_pitch, &desired_x, &desired_y, &desired_altitude); +#define PERIODIC_SEND_PITCH() DOWNLINK_SEND_PITCH(&ir_pitch, &ir_pitch_neutral, &ir_gain); +#define PERIODIC_SEND_NAVIGATION_REF() DOWNLINK_SEND_NAVIGATION_REF(&utm_east0, &utm_north0); +#define PERIODIC_SEND_IDENT() DOWNLINK_SEND_IDENT(ac_ident); + +#ifdef RADIO_CALIB +#define PERIODIC_SEND_SETTINGS() if (inflight_calib_mode != IF_CALIB_MODE_NONE) DOWNLINK_SEND_SETTINGS(&inflight_calib_mode, &slider_1_val, &slider_2_val); +#else +#define PERIODIC_SEND_SETTINGS() +#endif + + +inline void reporting_task( void ) { + static uint8_t boot = TRUE; + + /* initialisation phase */ + if (boot) { + DOWNLINK_SEND_BOOT(&version); + DOWNLINK_SEND_RAD_OF_IR(&estimator_ir, &estimator_rad, &estimator_rad_of_ir, &ir_roll_neutral, &ir_pitch_neutral); + boot = FALSE; + } + /* periodic reporting */ + else { + PeriodicSend(); + } +} + +inline uint8_t inflight_calib_mode_update (void) { + ModeUpdate(inflight_calib_mode, IF_CALIB_MODE_OF_PULSE(from_fbw.channels[RADIO_CALIB])); +} + + +inline void radio_control_task( void ) { + if (link_fbw_receive_valid) { + uint8_t mode_changed = FALSE; + copy_from_to_fbw(); + if ((bit_is_set(from_fbw.status, RADIO_REALLY_LOST) && (pprz_mode == PPRZ_MODE_AUTO1 || pprz_mode == PPRZ_MODE_MANUAL)) || too_far_from_home) { + pprz_mode = PPRZ_MODE_HOME; + mode_changed = TRUE; + } + if (bit_is_set(from_fbw.status, AVERAGED_CHANNELS_SENT)) { + bool_t pprz_mode_changed = pprz_mode_update(); + mode_changed |= pprz_mode_changed; +#ifdef RADIO_LLS + mode_changed |= ir_estim_mode_update(); +#endif +#ifdef RADIO_CALIB + bool_t calib_mode_changed = inflight_calib_mode_update(); + inflight_calib(calib_mode_changed || pprz_mode_changed); + mode_changed |= calib_mode_changed; +#endif + } + mode_changed |= mcu1_status_update(); + if ( mode_changed ) + DOWNLINK_SEND_PPRZ_MODE(&pprz_mode, &vertical_mode, &inflight_calib_mode, &mcu1_status, &ir_estim_mode); + + if (pprz_mode == PPRZ_MODE_AUTO1) { + desired_roll = FLOAT_OF_PPRZ(from_fbw.channels[RADIO_ROLL], 0., -0.6); + desired_pitch = FLOAT_OF_PPRZ(from_fbw.channels[RADIO_PITCH], 0., 0.5); + } // else asynchronously set by course_pid_run() + if (pprz_mode == PPRZ_MODE_MANUAL || pprz_mode == PPRZ_MODE_AUTO1) + desired_gaz = from_fbw.channels[RADIO_THROTTLE]; + // else asynchronously set by climb_pid_run(); + + mcu1_ppm_cpt = from_fbw.ppm_cpt; +#ifndef CTL_BRD_V1_1 + vsupply = from_fbw.vsupply; +#endif + + events_update(); + + if (!estimator_flight_time) { + ground_calibrate(); + if (pprz_mode == PPRZ_MODE_AUTO2 && from_fbw.channels[RADIO_THROTTLE] > GAZ_THRESHOLD_TAKEOFF) { + launch = TRUE; + } + } + } + +} + +void navigation_task( void ) { + + /* Compute desired_course */ + if (pprz_mode == PPRZ_MODE_HOME) + nav_home(); + else + nav_update_desired_course(); + + DOWNLINK_SEND_NAVIGATION(&nav_block, &nav_stage, &estimator_x, &estimator_y, &desired_course, &dist2_to_wp, &course_pgain, &dist2_to_home); + + if (pprz_mode == PPRZ_MODE_AUTO2 || pprz_mode == PPRZ_MODE_HOME) { + course_pid_run(); /* aka compute desired_roll */ + desired_pitch = nav_pitch; + + if (vertical_mode == VERTICAL_MODE_AUTO_ALT) + altitude_pid_run(); + if (vertical_mode >= VERTICAL_MODE_AUTO_CLIMB) + climb_pid_run(); + if (vertical_mode == VERTICAL_MODE_AUTO_GAZ) + desired_gaz = nav_desired_gaz; + if (low_battery || (!estimator_flight_time && !launch)) + desired_gaz = 0.; + } + + + +} + +#define PERIOD (256. * 1024. / CLOCK / 1000000.) +inline void periodic_task( void ) { // 61 Hz + static uint8_t _20Hz = 0; + static uint8_t _4Hz = 0; + static uint8_t _1Hz = 0; + + estimator_t += PERIOD; + + _20Hz++; + if (_20Hz>=3) _20Hz=0; + _4Hz++; + if (_4Hz>=15) _4Hz=0; + _1Hz++; + if (_1Hz>=61) _1Hz=0; + + if (!_1Hz) { + if (estimator_flight_time) estimator_flight_time++; + cputime++; + stage_time++; + block_time++; + +#ifdef CTL_BRD_V1_1 + uint16_t av_bat_value = buf_bat.sum/AV_NB_SAMPLE; + vsupply = VoltageOfAdc(av_bat_value) * 10.; +#endif + low_battery |= (vsupply < LOW_BATTERY); + } + switch(_4Hz) { + case 0: + estimator_propagate_state(); + navigation_task(); + break; + // default: + } + switch (_20Hz) { + case 0: + break; + case 1: { + static uint8_t odd; + odd++; + if (odd & 0x01) + reporting_task(); + break; + } + case 2: + ir_update(); + estimator_update_state_infrared(); + roll_pitch_pid_run(); // Set desired_aileron & desired_elevator + to_fbw.channels[RADIO_THROTTLE] = desired_gaz; // desired_gaz is set upon GPS message reception + to_fbw.channels[RADIO_ROLL] = desired_aileron; + to_fbw.channels[RADIO_PITCH] = desired_elevator; + + // Code for camera stabilization, FIXME put that elsewhere + to_fbw.channels[RADIO_GAIN1] = TRIM_PPRZ(MAX_PPRZ/0.75*(-estimator_phi)); + + link_fbw_send(); + break; + default: + fatal_error_nb++; + } +} + + +void use_gps_pos(void) { + DOWNLINK_SEND_GPS(&gps_mode, &gps_utm_east, &gps_utm_north, &gps_fcourse, &gps_falt, &gps_fspeed,&gps_fclimb, &gps_ftow); + estimator_update_state_gps(); + DOWNLINK_SEND_RAD_OF_IR(&estimator_ir, &estimator_rad, &estimator_rad_of_ir, &ir_roll_neutral, &ir_pitch_neutral); + if (!estimator_flight_time && (estimator_hspeed_mod > MIN_SPEED_FOR_TAKEOFF)) { + estimator_flight_time = 1; + launch = TRUE; /* Not set in non auto launch */ + DOWNLINK_SEND_TAKEOFF(&cputime); + } +} diff --git a/sw/airborne/autopilot/mainloop.c b/sw/airborne/autopilot/mainloop.c new file mode 100644 index 00000000000..baf336c33b9 --- /dev/null +++ b/sw/airborne/autopilot/mainloop.c @@ -0,0 +1,85 @@ +/* + * $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include "std.h" + +#include "timer.h" +#include "modem.h" +#include "adc.h" +#include "airframe.h" +#include "autopilot.h" +#include "spi.h" +#include "link_fbw.h" +#include "gps.h" +#include "nav.h" +#include "infrared.h" +#include "estimator.h" +#include "downlink.h" + + +int main( void ) { + /* init peripherals */ + timer_init(); + modem_init(); + adc_init(); +#ifdef CTL_BRD_V1_1 + adc_buf_channel(ADC_CHANNEL_BAT, &buf_bat); +#endif + spi_init(); + link_fbw_init(); + gps_init(); + nav_init(); + ir_init(); + estimator_init(); + + /* start interrupt task */ + sei(); + + /* Wait 0.5s (for modem init ?) */ + uint8_t init_cpt = 30; + while (init_cpt) { + if (timer_periodic()) + init_cpt--; + } + + /* enter mainloop */ + while( 1 ) { + if(timer_periodic()) + periodic_task(); + if (gps_msg_received) { + parse_gps_msg(); + gps_msg_received = FALSE; + if (gps_pos_available) { + use_gps_pos(); + gps_pos_available = FALSE; + } + } + if (link_fbw_receive_complete) { + link_fbw_receive_complete = FALSE; + radio_control_task(); + } + } + return 0; +} diff --git a/sw/airborne/autopilot/modem.c b/sw/airborne/autopilot/modem.c new file mode 100644 index 00000000000..5c6039316fd --- /dev/null +++ b/sw/airborne/autopilot/modem.c @@ -0,0 +1,88 @@ +/* + * Paparazzi mcu0 cmx469 modem functions + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include +#include +#include "modem.h" + +uint8_t modem_nb_ovrn; + +uint8_t tx_head; +volatile uint8_t tx_tail; +uint8_t tx_buf[ TX_BUF_SIZE ]; + +uint8_t tx_byte; +uint8_t tx_byte_idx; + +uint8_t ck_a, ck_b; + +void modem_init( void ) { +#if defined CTL_BRD_V1_2 || defined CTL_BRD_V1_2_1 + MODEM_OSC_DDR |= _BV(MODEM_OSC); + OCR0 = 1; /* 4MhZ */ + TCCR0 = _BV(WGM01) | _BV(COM00) | _BV(CS00); +#endif + + /* setup TX_EN and TX_DATA pin as output */ + MODEM_TX_DDR |= _BV(MODEM_TX_EN) | _BV(MODEM_TX_DATA); + /* data idles hight */ + sbi(MODEM_TX_PORT, MODEM_TX_DATA); + /* enable transmitter */ + cbi(MODEM_TX_PORT, MODEM_TX_EN); + /* set interrupt on failing edge of clock */ + MODEM_CLK_INT_REG |= MODEM_CLK_INT_CFG; +} + +SIGNAL( MODEM_CLK_INT_SIG ) { + /* start bit */ + if (tx_byte_idx == 0) + cbi(MODEM_TX_PORT, MODEM_TX_DATA); + /* 8 data bits */ + else if (tx_byte_idx < 9) { + if (tx_byte & 0x01) + sbi(MODEM_TX_PORT, MODEM_TX_DATA); + else + cbi(MODEM_TX_PORT, MODEM_TX_DATA); + tx_byte >>= 1; + } + /* stop_bit */ + else { + sbi(MODEM_TX_PORT, MODEM_TX_DATA); + } + tx_byte_idx++; + /* next byte */ + if (tx_byte_idx >= 10) { + /* if we have nothing left to transmit */ + if( tx_head == tx_tail ) { + /* disable clock interrupt */ + cbi( EIMSK, MODEM_CLK_INT ); + } else { + /* else load next byte */ + MODEM_LOAD_NEXT_BYTE(); + } + } +} diff --git a/sw/airborne/autopilot/modem.h b/sw/airborne/autopilot/modem.h new file mode 100644 index 00000000000..2d84056e160 --- /dev/null +++ b/sw/airborne/autopilot/modem.h @@ -0,0 +1,140 @@ +/* + * Paparazzi mcu0 cmx469 modem functions + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef MODEM_H +#define MODEM_H + +#include "airframe.h" + +void modem_init( void ); + +extern uint8_t modem_nb_ovrn; + +#define TX_BUF_SIZE 255 +extern uint8_t tx_head; +extern volatile uint8_t tx_tail; +extern uint8_t tx_buf[ TX_BUF_SIZE ]; + +extern uint8_t tx_byte; +extern uint8_t tx_byte_idx; + +extern uint8_t ck_a, ck_b; + +#define ModemStartMessage(id) \ + { MODEM_PUT_1_BYTE(STX); MODEM_PUT_1_BYTE(id); ck_a = id; ck_b = id; } + +#define ModemEndMessage() \ + { MODEM_PUT_1_BYTE(ck_a); MODEM_PUT_1_BYTE(ck_b); MODEM_CHECK_RUNNING(); } + + +#define MODEM_TX_PORT PORTD +#define MODEM_TX_DDR DDRD +#define MODEM_TX_EN 7 +#define MODEM_TX_DATA 6 + +#ifdef CTL_BRD_V1_1 +#define MODEM_CLK_DDR DDRD +#define MODEM_CLK_PORT PORTD +#define MODEM_CLK 0 +#define MODEM_CLK_INT INT0 +#define MODEM_CLK_INT_REG EICRA +#define MODEM_CLK_INT_CFG _BV(ISC01) +#define MODEM_CLK_INT_SIG SIG_INTERRUPT0 +#endif /* CTL_BRD_V1_1 */ + +#ifdef CTL_BRD_V1_2 +#define MODEM_CLK_DDR DDRD +#define MODEM_CLK_PORT PORTD +#define MODEM_CLK 0 +#define MODEM_CLK_INT INT0 +#define MODEM_CLK_INT_REG EICRA +#define MODEM_CLK_INT_CFG _BV(ISC01) +#define MODEM_CLK_INT_SIG SIG_INTERRUPT0 + +#define MODEM_OSC_DDR DDRB +#define MODEM_OSC_PORT PORTB +#define MODEM_OSC 4 +#endif /* CTL_BRD_V1_2 */ + +#ifdef CTL_BRD_V1_2_1 +#define MODEM_CLK_DDR DDRE +#define MODEM_CLK_PORT PORTE +#define MODEM_CLK 4 +#define MODEM_CLK_INT INT4 +#define MODEM_CLK_INT_REG EICRB +#define MODEM_CLK_INT_CFG _BV(ISC41) +#define MODEM_CLK_INT_SIG SIG_INTERRUPT4 +#define MODEM_OSC_DDR DDRB +#define MODEM_OSC_PORT PORTB +#define MODEM_OSC 4 +#endif /* CTL_BRD_V1_2_1 */ + + + +#define MODEM_CHECK_FREE_SPACE(_space) (tx_head>=tx_tail? _space < (TX_BUF_SIZE - (tx_head - tx_tail)) : _space < (tx_tail - tx_head)) + +#define MODEM_PUT_1_BYTE(_byte) { \ + tx_buf[tx_head] = _byte; \ + tx_head++; \ + if (tx_head >= TX_BUF_SIZE) tx_head = 0; \ +} + +#define MODEM_PUT_1_BYTE_BY_ADDR(_byte) { \ + tx_buf[tx_head] = *(_byte); \ + ck_a += *(_byte); \ + ck_b += ck_a; \ + tx_head++; \ + if (tx_head >= TX_BUF_SIZE) tx_head = 0; \ +} + +#define MODEM_PUT_2_BYTE_BY_ADDR(_byte) { \ + MODEM_PUT_1_BYTE_BY_ADDR(_byte); \ + MODEM_PUT_1_BYTE_BY_ADDR(_byte+1); \ +} + +#define MODEM_PUT_4_BYTE_BY_ADDR(_byte) { \ + MODEM_PUT_1_BYTE_BY_ADDR(_byte); \ + MODEM_PUT_1_BYTE_BY_ADDR(_byte+1); \ + MODEM_PUT_1_BYTE_BY_ADDR(_byte+2); \ + MODEM_PUT_1_BYTE_BY_ADDR(_byte+3); \ +} + +#define MODEM_LOAD_NEXT_BYTE() { \ + tx_byte = tx_buf[tx_tail]; \ + tx_byte_idx = 0; \ + tx_tail++; \ + if( tx_tail >= TX_BUF_SIZE ) \ + tx_tail = 0; \ +} + +#define MODEM_CHECK_RUNNING() { \ + if (!(EIMSK & _BV(MODEM_CLK_INT))) { \ + MODEM_LOAD_NEXT_BYTE() \ + sbi(EIFR, INTF0); \ + sbi(EIMSK, MODEM_CLK_INT); \ + } \ +} + + +#endif /* MODEM_H */ diff --git a/sw/airborne/autopilot/nav.c b/sw/airborne/autopilot/nav.c new file mode 100644 index 00000000000..3c0227bb55e --- /dev/null +++ b/sw/airborne/autopilot/nav.c @@ -0,0 +1,204 @@ +/* + * $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#define NAV_C + +#include + +#include "nav.h" +#include "estimator.h" +#include "pid.h" +#include "autopilot.h" +#include "link_fbw.h" +#include "airframe.h" + +uint8_t nav_stage, nav_block; +uint8_t excpt_stage; /*To save the current stage when an exception is raised */ +static float last_x, last_y; +static uint8_t last_wp; +float rc_pitch; +uint16_t stage_time, block_time; + +#define RcEvent1() CheckEvent(rc_event_1) +#define RcEvent2() CheckEvent(rc_event_2) +#define Block(x) case x: nav_block=x; +#define InitBlock() { nav_stage = 0; block_time = 0; InitStage(); } +#define NextBlock() { nav_block++; InitBlock(); } +#define GotoBlock(b) { nav_block=b; InitBlock(); } + +#define Stage(s) case s: nav_stage=s; +#define InitStage() { last_x = estimator_x; last_y = estimator_y; stage_time = 0; return; } +#define NextStage() { nav_stage++; InitStage() } +#define NextStageFrom(wp) { last_wp = wp; NextStage() } +#define GotoStage(s) { nav_stage = s; InitStage() } + +#define Label(x) label_ ## x: +#define Goto(x) { goto label_ ## x; } + +#define Exception(x) { excpt_stage = nav_stage; goto label_ ## x; } +#define ReturnFromException(_) { GotoStage(excpt_stage) } + +static bool_t approaching(uint8_t); +static inline void fly_to_xy(float x, float y); +static void fly_to(uint8_t wp); +static void route_to(uint8_t last_wp, uint8_t wp); +static void glide_to(uint8_t last_wp, uint8_t wp); + +#define MIN_DX ((int16_t)(MAX_PPRZ * 0.05)) + +#define DegOfRad(x) ((x) / M_PI * 180.) +#define RadOfDeg(x) ((x)/180. * M_PI) +#define NormCourse(x) { \ + while (x < 0) x += 360; \ + while (x >= 360) x -= 360; \ +} + +static float qdr; /* Degrees from 0 to 360 */ +#define CircleXY(x, y, radius) { \ + float alpha = atan2(estimator_y - y, \ + estimator_x - x); \ + float alpha_carrot = alpha + CARROT / -radius * estimator_hspeed_mod; \ + fly_to_xy(x+cos(alpha_carrot)*fabs(radius), \ + y+sin(alpha_carrot)*fabs(radius)); \ + qdr = DegOfRad(M_PI/2 - alpha_carrot); \ + NormCourse(qdr); \ +} + +static float carrot_x, carrot_y; + +#define Goto3D(radius) { \ + int16_t yaw = from_fbw.channels[RADIO_YAW]; \ + if (yaw > MIN_DX || yaw < -MIN_DX) \ + carrot_x += FLOAT_OF_PPRZ(yaw, 0, -20.); \ + int16_t pitch = from_fbw.channels[RADIO_PITCH]; \ + if (pitch > MIN_DX || pitch < -MIN_DX) \ + carrot_y += FLOAT_OF_PPRZ(pitch, 0, -20.); \ + vertical_mode = VERTICAL_MODE_AUTO_ALT; \ + int16_t roll = from_fbw.channels[RADIO_ROLL]; \ + if (roll > MIN_DX || roll < -MIN_DX) \ + desired_altitude += FLOAT_OF_PPRZ(roll, 0, -1.0); \ + CircleXY(carrot_x, carrot_y, radius); \ +} +#define Circle(wp, radius) \ + CircleXY(waypoints[wp].x, waypoints[wp].y, radius) + +#define And(x, y) ((x) && (y)) +#define Or(x, y) ((x) || (y)) +#define Min(x,y) (x < y ? x : y) +#define Max(x,y) (x > y ? x : y) +#define Qdr(x) (Min(x, 350) < qdr && qdr < x+10) + +#include "flight_plan.h" + + +#define MIN_DIST2_WP (15.*15.) + +#define DISTANCE2(p1_x, p1_y, p2) ((p1_x-p2.x)*(p1_x-p2.x)+(p1_y-p2.y)*(p1_y-p2.y)) + +const int32_t nav_east0 = NAV_UTM_EAST0; +const int32_t nav_north0 = NAV_UTM_NORTH0; + +float desired_altitude, desired_x, desired_y; +uint16_t nav_desired_gaz; +float nav_pitch = NAV_PITCH; + +float dist2_to_wp, dist2_to_home; +bool_t too_far_from_home; +const uint8_t nb_waypoint = NB_WAYPOINT; + +struct point waypoints[NB_WAYPOINT+1] = WAYPOINTS; + +static float carrot; +static bool_t approaching(uint8_t wp) { + float pw_x = waypoints[wp].x - estimator_x; + float pw_y = waypoints[wp].y - estimator_y; + + dist2_to_wp = pw_x*pw_x + pw_y *pw_y; + carrot = CARROT * estimator_hspeed_mod; + carrot = (carrot < 40 ? 40 : carrot); + if (dist2_to_wp < carrot*carrot) + return TRUE; + + float scal_prod = (waypoints[wp].x - last_x) * pw_x + (waypoints[wp].y - last_y) * pw_y; + + return (scal_prod < 0); +} + +static inline void fly_to_xy(float x, float y) { + desired_x = x; + desired_y = y; + desired_course = M_PI/2.-atan2(y - estimator_y, x - estimator_x); +} + +static void fly_to(uint8_t wp) { + fly_to_xy(waypoints[wp].x, waypoints[wp].y); +} + +static float alpha, leg; +static void route_to(uint8_t _last_wp, uint8_t wp) { + float last_wp_x = waypoints[_last_wp].x; + float last_wp_y = waypoints[_last_wp].y; + float leg_x = waypoints[wp].x - last_wp_x; + float leg_y = waypoints[wp].y - last_wp_y; + float leg2 = leg_x * leg_x + leg_y * leg_y; + alpha = ((estimator_x - last_wp_x) * leg_x + (estimator_y - last_wp_y) * leg_y) / leg2; + alpha = Max(alpha, 0.); + leg = sqrt(leg2); + alpha += Max(carrot / leg, 0.); /* carrot computed in approaching() */ + alpha = Min(1., alpha); + fly_to_xy(last_wp_x + alpha*leg_x, last_wp_y + alpha*leg_y); +} + +static void glide_to(uint8_t _last_wp, uint8_t wp) { + float last_alt = waypoints[_last_wp].a; + desired_altitude = last_alt + alpha * (waypoints[wp].a - last_alt); + pre_climb = NOMINAL_AIRSPEED * (waypoints[wp].a - last_alt) / leg; +} + +static inline void compute_dist2_to_home(void) { + float ph_x = waypoints[WP_HOME].x - estimator_x; + float ph_y = waypoints[WP_HOME].y - estimator_y; + dist2_to_home = ph_x*ph_x + ph_y *ph_y; + too_far_from_home = dist2_to_home > (MAX_DIST_FROM_HOME*MAX_DIST_FROM_HOME); +} + +void nav_home(void) { + Circle(WP_HOME, 50); /* FIXME: radius should be defined elsewhere */ + nav_pitch = 0.; /* Nominal speed */ + vertical_mode = VERTICAL_MODE_AUTO_ALT; + desired_altitude = GROUND_ALT+50; + compute_dist2_to_home(); + dist2_to_wp = dist2_to_home; +} + +void nav_update_desired_course(void) { + compute_dist2_to_home(); + auto_nav(); +} + + +void nav_init(void) { + nav_block = 0; + nav_stage = 0; +} diff --git a/sw/airborne/autopilot/nav.h b/sw/airborne/autopilot/nav.h new file mode 100644 index 00000000000..93ac4ef428a --- /dev/null +++ b/sw/airborne/autopilot/nav.h @@ -0,0 +1,56 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef NAV_H +#define NAV_H + +#include + +struct point { + float x; + float y; + float a; +}; +extern float cur_pos_x; +extern float cur_pos_y; +extern uint8_t nav_stage, nav_block; +extern float dist2_to_wp, dist2_to_home; + +extern const int32_t nav_east0; +extern const int32_t nav_north0; + +extern const uint8_t nb_waypoint; +extern struct point waypoints[]; +extern float desired_altitude, desired_x, desired_y; + +extern uint16_t nav_desired_gaz; +extern float nav_pitch, rc_pitch; +extern bool_t too_far_from_home; +extern uint16_t stage_time, block_time; /* s */ + +void nav_update_desired_course(void); +void nav_home(void); +void nav_init(void); + +#endif /* NAV_H */ diff --git a/sw/airborne/autopilot/pid.c b/sw/airborne/autopilot/pid.c new file mode 100644 index 00000000000..bd6be865578 --- /dev/null +++ b/sw/airborne/autopilot/pid.c @@ -0,0 +1,88 @@ +/* + * $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include + +#include "pid.h" + +#include "autopilot.h" +#include "infrared.h" +//#include "gps.h" +#include "estimator.h" +#include "nav.h" + +float desired_roll = 0.; +float desired_pitch = 0.; +int16_t desired_gaz, desired_aileron, desired_elevator; +float roll_pgain = ROLL_PGAIN; +float pitch_pgain = PITCH_PGAIN; +float pitch_of_roll = PITCH_OF_ROLL; + +void roll_pitch_pid_run( void ) { + float err = estimator_phi - desired_roll; + desired_aileron = TRIM_PPRZ(roll_pgain * err); + if (pitch_of_roll <0.) + pitch_of_roll = 0.; + err = -(estimator_theta - desired_pitch - pitch_of_roll * fabs(estimator_phi)); + desired_elevator = TRIM_PPRZ(pitch_pgain * err); +} + +float course_pgain = COURSE_PGAIN; +float desired_course = 0.; +float max_roll = MAX_ROLL; + +void course_pid_run( void ) { + float err = estimator_hspeed_dir - desired_course; + NORM_RAD_ANGLE(err); + float roll = course_pgain * err; // * fspeed / AIR_SPEED; + if (roll > max_roll) + desired_roll = max_roll; + else if (roll < -max_roll) + desired_roll = -max_roll; + else desired_roll = roll; +} + +const float climb_pgain = CLIMB_PGAIN; +const float climb_igain = CLIMB_IGAIN; +float desired_climb = 0., pre_climb = 0.; +static const float level_gaz = CLIMB_LEVEL_GAZ; +float climb_sum_err = 0; + +void climb_pid_run ( void ) { + float err = estimator_z_dot - desired_climb; + float fgaz = climb_pgain * (err + climb_igain * climb_sum_err) + CLIMB_LEVEL_GAZ + (0.9-CLIMB_LEVEL_GAZ)/CLIMB_GAZ_MAX*desired_climb; + climb_sum_err += err; + desired_gaz = TRIM_UPPRZ(fgaz * MAX_PPRZ); +} + +float altitude_pgain = ALTITUDE_PGAIN; + + +void altitude_pid_run(void) { + float err = estimator_z - desired_altitude; + desired_climb = pre_climb + altitude_pgain * err; + if (desired_climb < -CLIMB_MAX) desired_climb = -CLIMB_MAX; + if (desired_climb > CLIMB_MAX) desired_climb = CLIMB_MAX; +} diff --git a/sw/airborne/autopilot/pid.h b/sw/airborne/autopilot/pid.h new file mode 100644 index 00000000000..00efbcabd59 --- /dev/null +++ b/sw/airborne/autopilot/pid.h @@ -0,0 +1,58 @@ +/* + * Paparazzi mcu0 $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef PID_H +#define PID_H + +#include + +#define NORM_RAD_ANGLE(x) { \ + while (x > M_PI) x -= 2 * M_PI; \ + while (x < -M_PI) x += 2 * M_PI; \ + } + +extern float desired_roll; +extern float max_roll; +extern float desired_pitch; +extern float roll_pgain; +extern float pitch_pgain; +extern float pitch_of_roll; +void roll_pitch_pid_run( void ); + + +extern float course_pgain; +extern float desired_course; +void course_pid_run( void ); + + +extern const float climb_pgain; +extern const float climb_igain; +extern float climb_sum_err; +extern float desired_climb, pre_climb; +extern int16_t desired_gaz, desired_aileron, desired_elevator; + +void climb_pid_run(void); +void altitude_pid_run(void); + +#endif /* PID_H */ diff --git a/sw/airborne/autopilot/sirf.h b/sw/airborne/autopilot/sirf.h new file mode 100644 index 00000000000..b0969acb56d --- /dev/null +++ b/sw/airborne/autopilot/sirf.h @@ -0,0 +1,36 @@ +/* + * Paparazzi autopilot $Id$ + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +/* + * SIRF protocol specific code + * +*/ + + +#ifndef SIRF_H +#define SIRF_H + +#define GPS_FIX_VALID(gps_mode) (gps_mode & 1<<5) + +#endif /* SIRF_H */ diff --git a/sw/airborne/autopilot/spi.c b/sw/airborne/autopilot/spi.c new file mode 100644 index 00000000000..9466c59cf28 --- /dev/null +++ b/sw/airborne/autopilot/spi.c @@ -0,0 +1,40 @@ +#include +#include +#include +#include + + +#include "spi.h" +#include "autopilot.h" +#include "link_fbw.h" + +volatile uint8_t spi_cur_slave; +uint8_t spi_nb_ovrn; + +void spi_init( void) { + /* Set MOSI and SCK output, all others input */ + SPI_DDR |= _BV(SPI_MOSI_PIN)| _BV(SPI_SCK_PIN); + + /* enable pull up for miso */ + // SPI_PORT |= _BV(SPI_MISO_PIN); + + /* Set SS0 output */ + sbi( SPI_SS0_DDR, SPI_SS0_PIN); + /* SS0 idles high (don't select slave yet)*/ + SPI_UNSELECT_SLAVE0(); + + /* Set SS1 output */ + sbi( SPI_SS1_DDR, SPI_SS1_PIN); + /* SS1 idles high (don't select slave yet)*/ + SPI_UNSELECT_SLAVE1(); + + spi_cur_slave = SPI_NONE; +} + + +SIGNAL(SIG_SPI) { + if (spi_cur_slave == SPI_SLAVE0) + link_fbw_on_spi_it(); + else + fatal_error_nb++; +} diff --git a/sw/airborne/autopilot/spi.h b/sw/airborne/autopilot/spi.h new file mode 100644 index 00000000000..556f60c12d8 --- /dev/null +++ b/sw/airborne/autopilot/spi.h @@ -0,0 +1,74 @@ +#ifndef SPI_H +#define SPI_H + +//#include "link_autopilot.h" + +#define SPI_SS0_PIN 0 +#define SPI_SS0_PORT PORTB +#define SPI_SS0_DDR DDRB +#define SPI_IT0_PIN 1 +#define SPI_IT0_PORT PORTD +#define SPI_IT0_DDR DDRD + +#define SPI_SS1_PIN 7 +#define SPI_SS1_PORT PORTE +#define SPI_SS1_DDR DDRE +#define SPI_IT1_PIN 6 +#define SPI_IT1_PORT PORTE +#define SPI_IT1_DDR DDRE + +#define SPI_SCK_PIN 1 +#define SPI_MOSI_PIN 2 +#define SPI_MISO_PIN 3 +#define SPI_PORT PORTB +#define SPI_DDR DDRB + + + +#define SPI_NONE 0 +#define SPI_SLAVE0 1 +#define SPI_SLAVE1 2 + +extern volatile uint8_t spi_cur_slave; +extern uint8_t spi_nb_ovrn; + +void spi_init( void); + +#define SPI_START(_SPCR_VAL) { \ + uint8_t foo; \ + SPCR = _SPCR_VAL; \ + if (bit_is_set(SPSR, SPIF)) \ + foo = SPDR; \ + SPCR |= _BV(SPIE); \ +} + +#define SPI_SELECT_SLAVE0() { \ + spi_cur_slave = SPI_SLAVE0; \ + cbi( SPI_SS0_PORT, SPI_SS0_PIN );\ +} + +#define SPI_UNSELECT_SLAVE0() { \ + spi_cur_slave = SPI_NONE; \ + sbi( SPI_SS0_PORT, SPI_SS0_PIN );\ +} + +#define SPI_SELECT_SLAVE1() { \ + spi_cur_slave = SPI_SLAVE1; \ + cbi( SPI_SS1_PORT, SPI_SS1_PIN );\ +} + +#define SPI_UNSELECT_SLAVE1() { \ + spi_cur_slave = SPI_NONE; \ + sbi( SPI_SS1_PORT, SPI_SS1_PIN );\ +} + +#define SPI_SEND(data) { \ + SPDR = data; \ +} + +#define SPI_STOP() { \ + cbi(SPCR,SPIE); \ + cbi(SPCR, SPE); \ +} + +#endif /* SPI_H */ diff --git a/sw/airborne/autopilot/test/Makefile b/sw/airborne/autopilot/test/Makefile new file mode 100644 index 00000000000..293b3b43639 --- /dev/null +++ b/sw/airborne/autopilot/test/Makefile @@ -0,0 +1,52 @@ +# $Id$ + +all: + @echo "call with 'make TARGET=... compile (or load)'" + +TARGET=check_uart + +ARCH = atmega128 +INCLUDES = -I ../ -I ../../../include + +LOCAL_CFLAGS= $(CTL_BRD_FLAGS) + +CONF_DIR = ../../../../conf +CONF_XML = $(CONF_DIR)/conf.xml + +ifneq ($(MAKECMDGOALS),clean) + AIRFRAME_XML = $(CONF_DIR)/$(shell echo `../../../lib/ocaml/xml_get.out $(CONF_XML) "files" "airframe"`) + CTL_BRD_VERSION=$(shell echo `../../../lib/ocaml/xml_get.out $(AIRFRAME_XML) "airframe" "ctl_board"`) +endif + + + +ifeq ($(CTL_BRD_VERSION),V1_2_1) +CTL_BRD_FLAGS=-DCTL_BRD_V1_2_1 +endif + +ifeq ($(CTL_BRD_VERSION),V1_2) +CTL_BRD_FLAGS=-DCTL_BRD_V1_2 +endif + +test_modem.srcs = test_modem.c ../modem.c + +check_uart.srcs = check_uart.c ../uart.c + +tx_adcs.srcs = tx_adcs.c ../uart.c ../adc.c + +test_v2xe.srcs = test_v2xe.c + +uart_tunnel.srcs = uart_tunnel.c ../uart.c +check_spi.srcs = check_spi.c ../uart.c ../spi.c ../link_fbw.c + +check_spi.o : INCLUDES = -I ../../../include -I ../ -I ../../fly_by_wire + +test_fp: test_fp.c ../flight_plan.h + cc -o $@ -I sim_avr -I .. -I ../../fly_by_wire ../nav.c test_fp.c -lm + +include ../../../../conf/Makefile.local +include ../../../../conf/Makefile.avr + + + +clean: avr_clean diff --git a/sw/airborne/autopilot/test/check_spi.c b/sw/airborne/autopilot/test/check_spi.c new file mode 100644 index 00000000000..afe71b070d3 --- /dev/null +++ b/sw/airborne/autopilot/test/check_spi.c @@ -0,0 +1,46 @@ +#include +#include "timer.h" +#include "link_fbw.h" +#include "uart.h" + +uint8_t fatal_error_nb; /* Used in spi.c */ + +/* Fill the message with dummy values */ +void fill_spi_msg(void) { + static pprz_t x; + + uint8_t i; + for(i = 0; i < RADIO_CTL_NB; i++) + to_fbw.channels[i] = x++; + to_fbw.status = 0xff; + to_fbw.ppm_cpt = 0xff; + to_fbw.vsupply = 0xff; +} + +int main( void ) { + uart0_init(); + uart0_print_string("Booting AP MCU: $Id$\n"); + link_fbw_init(); + timer_init(); + sei(); + + uint8_t _1Hz = 0; + while( 1 ) { + if(timer_periodic()) { + _1Hz++; + if (_1Hz >= 60) { + _1Hz = 0; + uart0_print_string("AP MCU Alive\n"); + fill_spi_msg(); + link_fbw_send(); + } + } + if (link_fbw_receive_complete) { + link_fbw_receive_complete = FALSE; + if (link_fbw_receive_valid) + uart0_print_string("SPI OK from fbw\n"); + else + uart0_print_string("SPI error from fbw\n"); + } + } +} diff --git a/sw/airborne/autopilot/test/check_uart.c b/sw/airborne/autopilot/test/check_uart.c new file mode 100644 index 00000000000..af9229f4efb --- /dev/null +++ b/sw/airborne/autopilot/test/check_uart.c @@ -0,0 +1,51 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include "timer.h" +#include "uart.h" + +int main( void ) { + uart0_init(); + uart0_print_string("Booting AP MCU: $Id$\n"); + timer_init(); + sei(); + uint8_t _1Hz = 0; + uint8_t foo = 0; + while( 1 ) { + if(timer_periodic()) { + _1Hz++; + if (_1Hz == 60) { + _1Hz = 0; + foo++; + uart0_print_string("AP MCU uart0 check : alive ["); + uart0_print_hex(foo); + uart0_print_string("]\n"); + } + } + } + return 0; +} diff --git a/sw/airborne/autopilot/test/test_modem.c b/sw/airborne/autopilot/test/test_modem.c new file mode 100644 index 00000000000..f764fa075bb --- /dev/null +++ b/sw/airborne/autopilot/test/test_modem.c @@ -0,0 +1,50 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + + +#include +#include +#include + +#include "modem.h" +#include "timer.h" + +const uint8_t *msg = "ap modem alive\n"; + +int main( void ) { + timer_init(); + modem_init(); + sei(); + while (1) { + if (timer_periodic()) { + uint8_t i = 0; + while (msg[i]) { + MODEM_PUT_1_BYTE(msg[i]); + i++; + } + MODEM_CHECK_RUNNING(); + } + } + return 0; +} diff --git a/sw/airborne/autopilot/test/test_v2xe.c b/sw/airborne/autopilot/test/test_v2xe.c new file mode 100644 index 00000000000..e58bd73621b --- /dev/null +++ b/sw/airborne/autopilot/test/test_v2xe.c @@ -0,0 +1,277 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + + +#include +#include +#include + +#include "spi.h" +#include "timer.h" + +void uart_send(uint8_t c); + +volatile uint8_t spi_cur_slave = SPI_NONE; + +void my_spi_init(void) { + /* Set MOSI and SCK output, all others input */ + SPI_DDR = _BV(SPI_MOSI_PIN)| _BV(SPI_SCK_PIN); + + /* enable pull up for miso */ + // SPI_PORT |= _BV(SPI_MISO_PIN); + + /* Set SS0 output */ + sbi( SPI_SS0_DDR, SPI_SS0_PIN); + /* SS0 idles high (don't select slave yet)*/ + SPI_UNSELECT_SLAVE0(); + + /* Set SS1 output */ + sbi( SPI_SS1_DDR, SPI_SS1_PIN); + /* SS1 idles high (don't select slave yet)*/ + SPI_UNSELECT_SLAVE1(); + +} + + +void spi_start( void ) { + uint8_t foo; + /* Enable SPI, Master, MSB first, clock idle low, sample on leading edge, clock rate fck/128 */ + SPCR = (_BV(SPE)| _BV(MSTR) | _BV(SPR1) | _BV(SPR0)); + if (bit_is_set(SPSR, SPIF)) + foo = SPDR; + SPI_SELECT_SLAVE1(); +} + +uint8_t spi_transmit(uint8_t c) { + uint8_t foo; + SPDR = c; + while (bit_is_clear(SPSR, SPIF)); + foo = inp(SPDR); + + uart_send(c); + uart_send(foo); + return foo; +} + +void spi_stop(void) { + SPI_UNSELECT_SLAVE1(); + SPI_STOP(); +} + + +inline void delay_long(uint8_t n) { + uint8_t ctx; + while (n > 0) { + ctx=0; + do {ctx++;} while (ctx); + n--; + } +} + +#define SYNC_FLAG 0xAA +#define TERMINATOR 0x00 + +#define GET_MODE_INFO 0x01 +#define MOD_INFO_RESP 0x02 +#define SET_DATA_COMPONENTS 0x03 +#define GET_DATA 0x04 +#define DATA_RESP 0x05 +#define SET_CONFIG 0x06 +#define GET_CONFIG 0x07 +#define CONFIG_RESP 0x08 +#define SAVE_CONFIG 0x09 +#define START_CAL 0x0A +#define STOP_CAL 0x0B +#define GET_CAL_DATA 0x0C +#define CAL_DATA_RESP 0x0D +#define SET_CAL_DATA 0x0E + +#define DATA_XRAW 0x01 // Slnt32 counts 32768 to 32767 +#define DATA_YRAW 0x02 // Slnt32 counts 32768 to 32767 +#define DATA_XCAL 0x03 // Float32 scaled to 1.0 +#define DATA_YCAL 0x04 // Float32 scaled to 1.0 +#define DATA_HEADING 0x05 // Float32 degrees 0.0 ° to 359.9 ° +#define DATA_MAGNITUDE 0x06 // Float32 scaled to 1.0 +#define DATA_TEMPERATURE 0x07 // Float32 ° Celsius +#define DATA_DISTORTION 0x08 // Boolean +#define DATA_CAL_STATUS 0x09 // Boolean + +uint8_t err_cnt; +uint8_t errno; + +#define DATA_FIELD_NB 3 +#define DATA_LEN 12 +void v2xe_setup_data_components(void) { + uint8_t c; + spi_start(); + c = spi_transmit(SYNC_FLAG); + c = spi_transmit(SET_DATA_COMPONENTS); + c = spi_transmit(DATA_FIELD_NB); + c = spi_transmit(DATA_XRAW); + c = spi_transmit(DATA_YRAW); + c = spi_transmit(DATA_TEMPERATURE); + c = spi_transmit(TERMINATOR); + spi_stop(); + uart_send(0xFF); +} + +void v2xe_read_data(void) { + uint8_t c, i=0; + spi_start(); + + /* querry */ + c = spi_transmit(SYNC_FLAG); + c = spi_transmit(GET_DATA); + c = spi_transmit(TERMINATOR); + + delay_long(255); + + /* answer */ + do { c = spi_transmit(0x00); i++;} + while (c!=SYNC_FLAG && i < 20); + // if (i>20) TIMEOUT; + /* frame type */ + c = spi_transmit(0x00); + /* nb fields */ + c = spi_transmit(0x00); + /* fields + data */ + for (i=0; i < DATA_LEN + DATA_FIELD_NB + 1; i++) { + c = spi_transmit(0x00); + } + + spi_stop(); + uart_send(0xFF); +} + +#define ID_LEN 8 +uint8_t id_str[ID_LEN]; +void v2xe_read_id (void) { + uint8_t c, i=0; + + spi_start(); + c = spi_transmit(SYNC_FLAG); + c = spi_transmit(GET_MODE_INFO); + c = spi_transmit(TERMINATOR); + + // delay_long(10); + + // c = spi_transmit(0x00); + + + do { c = spi_transmit(0x00); i++;} + while (c!=SYNC_FLAG && i < 20); + + /* if (c != SYNC_FLAG) { */ +/* err_cnt++; */ +/* errno = 1; */ +/* spi_stop(); */ +/* return; */ +/* } */ +// c = spi_transmit(0x00); +/* if (c != MOD_INFO_RESP) { */ +/* err_cnt++; */ +/* errno = 2; */ +/* spi_stop(); */ +/* return; */ +/* } */ + for (c = 0; c < ID_LEN; c++) + id_str[c] = spi_transmit(0x00); + c = spi_transmit(0x00); +/* if (c != TERMINATOR) { */ +/* err_cnt++; */ +/* errno = 3; */ +/* spi_stop(); */ +/* return; */ +/* } */ + + spi_stop(); + + uart_send(0xFF); +} + + +#define UBRRH UBRR0H +#define UBRRL UBRR0L +#define UCSRA UCSR0A +#define UCSRB UCSR0B +#define UCSRC UCSR0C +#define UDR UDR0 + +void uart_init(void) { + /* Baudrate is 38.4k */ + UBRRH = 0; + UBRRL = 25; + /* single speed */ + UCSRA = 0; + /* Enable receiver and transmitter */ + UCSRB = _BV(RXEN) | _BV(TXEN); + /* Set frame format: 8data, 1stop bit */ + UCSRC = _BV(UCSZ1) | _BV(UCSZ0); + +} + +void uart_send(uint8_t c) { + /* Wait for empty transmit buffer */ + while ( !( UCSRA & _BV(UDRE)) ) ; + /* Put data into buffer, sends the data */ + UDR = c; +} + + +inline void periodic_task(void) { + static uint8_t foo; + if (foo == 0) + v2xe_read_id(); + if (foo == 10) + v2xe_setup_data_components(); + if (foo > 10 && !(foo%10)) + v2xe_read_data(); + // uart_send(err_cnt); + // uart_send (errno); + // { + // uint8_t i; + // for (i=0; i<8; i++) + // uart_send(id_str[i]); + // } + // uart_send('\n'); + foo++; + if (foo > 60) foo=0; +} + +int main( void ) { + + timer_init(); + my_spi_init(); + uart_init(); + // sei(); + + delay_long(255); + delay_long(255); + + while (1) { + if (timer_periodic()) + periodic_task(); + } + return 0; +} diff --git a/sw/airborne/autopilot/test/tx_adcs.c b/sw/airborne/autopilot/test/tx_adcs.c new file mode 100644 index 00000000000..1a537d16f23 --- /dev/null +++ b/sw/airborne/autopilot/test/tx_adcs.c @@ -0,0 +1,66 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include "timer.h" +#include "uart.h" +#include "adc.h" + +static struct adc_buf buffers[NB_ADC]; + +void transmit_adc(void) { + uint8_t i; + uart0_transmit((uint8_t)0); uart0_transmit((uint8_t)0); + for(i = 0; i < NB_ADC; i++) { + uint16_t value = buffers[i].sum / AV_NB_SAMPLE; + uart0_transmit((uint8_t)(value >> 8)); + uart0_transmit((uint8_t)(value & 0xff)); + } + uart0_transmit((uint8_t)'\n'); +} + +int main( void ) { + uint8_t i; + uart0_init(); + timer_init(); + adc_init(); + for(i = 0; i < NB_ADC; i++) + adc_buf_channel(i, &buffers[i]); + sei(); + + while( 1 ) { + static uint8_t _1Hz = 0; + + if(timer_periodic()) { + _1Hz++; + if (_1Hz == 60) { + _1Hz = 0; + transmit_adc(); + } + } + } + return 0; +} diff --git a/sw/airborne/autopilot/test/uart_tunnel.c b/sw/airborne/autopilot/test/uart_tunnel.c new file mode 100644 index 00000000000..2bb79257a82 --- /dev/null +++ b/sw/airborne/autopilot/test/uart_tunnel.c @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include + + +#include "../uart.h" +#include "../timer.h" + +void on_uart0_rx(uint8_t c) { + uart1_transmit(c); +} + +void on_uart1_rx(uint8_t c) { + uart0_transmit(c); +} + +ReceiveUart0(on_uart0_rx) +ReceiveUart1(on_uart1_rx) + +int main( void ) { + uart0_init(); + uart1_init(); + + sei(); + return 0; + +} diff --git a/sw/airborne/autopilot/timer.h b/sw/airborne/autopilot/timer.h new file mode 100644 index 00000000000..7ec726d3b31 --- /dev/null +++ b/sw/airborne/autopilot/timer.h @@ -0,0 +1,91 @@ +/* + * Paparazzi mcu0 timer functions + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * Copyright (C) 2002 Trammell Hudson + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef TIMER_H +#define TIMER_H + +#include "std.h" +#include +#include + + +/* + * Enable Timer1 (16-bit) running at Clk/1 for the global system + * clock. This will be used for computing the servo pulse widths, + * PPM decoding, etc. + * + * Low frequency periodic tasks will be signaled by timer 0 + * running at Clk/1024. For 16 Mhz clock, this will be every + * 262144 microseconds, or 61 Hz. + */ +static inline void timer_init( void ) { + + /* Timer0: Modem clock is started in modem.h in ctc mode*/ + + /* Timer1 @ Clk/1: System clock, ppm and servos */ + TCCR1A = 0x00; + TCCR1B = 0x01; + + /* Timer2 @ Clk/1024: Periodic clock */ + TCCR2 = 0x05; +} + + +/* + * Retrieve the current time from the global clock in Timer1, + * disabling interrupts to avoid stomping on the TEMP register. + * If interrupts are already off, the non_atomic form can be used. + */ +static inline uint16_t +timer_now( void ) +{ + return TCNT1; +} + +static inline uint16_t +timer_now_non_atomic( void ) +{ + return TCNT1L; +} + + +/* + * Periodic tasks occur when Timer2 overflows. Check and unset + * the overflow bit. We cycle through four possible periodic states, + * so each state occurs every 30 Hz. + */ +static inline bool_t +timer_periodic( void ) +{ + if( !bit_is_set( TIFR, TOV2 ) ) + return FALSE; + + TIFR = 1 << TOV2; + return TRUE; +} + +#endif diff --git a/sw/airborne/autopilot/uart.c b/sw/airborne/autopilot/uart.c new file mode 100644 index 00000000000..c42ecf00f06 --- /dev/null +++ b/sw/airborne/autopilot/uart.c @@ -0,0 +1,138 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ +#include +#include +#include +#include "uart.h" + +#define TX_BUF_SIZE 256 +static uint8_t tx_head0; /* next free in buf */ +static volatile uint8_t tx_tail0; /* next char to send */ +static uint8_t tx_buf0[ TX_BUF_SIZE ]; + +static uint8_t tx_head1; /* next free in buf */ +static volatile uint8_t tx_tail1; /* next char to send */ +static uint8_t tx_buf1[ TX_BUF_SIZE ]; + +void uart0_transmit( unsigned char data ) { + if (UCSR0B & _BV(TXCIE)) { + /* we are waiting for the last char to be sent : buffering */ + if (tx_tail0 == tx_head0 + 1) { /* BUF_SIZE = 256 */ + /* Buffer is full (almost, but tx_head = tx_tail means "empty" */ + return; + } + tx_buf0[tx_head0] = data; + tx_head0++; /* BUF_SIZE = 256 */ + } else { /* Channel is free: just send */ + UDR0 = data; + sbi(UCSR0B, TXCIE); + } +} + +void uart1_transmit( unsigned char data ) { + if (UCSR1B & _BV(TXCIE)) { + /* we are waiting for the last char to be sent : buffering */ + if (tx_tail1 == tx_head1 + 1) { /* BUF_SIZE = 256 */ + /* Buffer is full (almost, but tx_head = tx_tail means "empty" */ + return; + } + tx_buf1[tx_head1] = data; + tx_head1++; /* BUF_SIZE = 256 */ + } else { /* Channel is free: just send */ + UDR1 = data; + sbi(UCSR1B, TXCIE); + } +} + + +void uart0_print_string(const uint8_t* s) { + uint8_t i = 0; + while (s[i]) { + uart0_transmit(s[i]); + i++; + } +} + +void uart0_print_hex(const uint8_t c) { + const uint8_t hex[16] = { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + uint8_t high = (c & 0xF0)>>4; + uint8_t low = c & 0x0F; + uart0_transmit(hex[high]); + uart0_transmit(hex[low]); +} + + +SIGNAL(SIG_UART0_TRANS) { + if (tx_head0 == tx_tail0) { + /* Nothing more to send */ + cbi(UCSR0B, TXCIE); /* disable interrupt */ + } else { + UDR0 = tx_buf0[tx_tail0]; + tx_tail0++; /* warning tx_buf_len is 256 */ + } +} + +SIGNAL(SIG_UART1_TRANS) { + if (tx_head1 == tx_tail1) { + /* Nothing more to send */ + cbi(UCSR1B, TXCIE); /* disable interrupt */ + } else { + UDR1 = tx_buf1[tx_tail1]; + tx_tail1++; /* warning tx_buf_len is 256 */ + } +} + +void uart0_init( void ) { + /* Baudrate is 38.4k */ + UBRR0H = 0; + UBRR0L = 25; // 38.4 + // UBRR0L = 103; //9600 + /* single speed */ + UCSR0A = 0; + /* Enable receiver and transmitter */ + UCSR0B = _BV(RXEN) | _BV(TXEN); + /* Set frame format: 8data, 1stop bit */ + UCSR0C = _BV(UCSZ1) | _BV(UCSZ0); + /* Enable uart receive interrupt */ + sbi(UCSR0B, RXCIE ); +} + +void uart1_init( void ) { + /* Baudrate is 38.4k */ + UBRR1H = 0; + UBRR1L = 25; // 38.4 + // UBRR1L = 103; //9600 + + + /* single speed */ + UCSR1A = 0; + /* Enable receiver and transmitter */ + UCSR1B = _BV(RXEN) | _BV(TXEN); + /* Set frame format: 8data, 1stop bit */ + UCSR1C = _BV(UCSZ1) | _BV(UCSZ0); + /* Enable uart receive interrupt */ + sbi(UCSR1B, RXCIE ); +} + diff --git a/sw/airborne/autopilot/uart.h b/sw/airborne/autopilot/uart.h new file mode 100644 index 00000000000..29e892915f0 --- /dev/null +++ b/sw/airborne/autopilot/uart.h @@ -0,0 +1,48 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ +#ifndef _UART_H_ +#define _UART_H_ + +#include + +extern void uart0_init(void); +extern void uart1_init(void); + +extern void uart0_print_string(const uint8_t*); +extern void uart0_print_hex(const uint8_t); +extern void uart0_transmit(const uint8_t); +extern void uart1_transmit(const uint8_t); + +#define ReceiveUart0(cb) \ + SIGNAL( SIG_UART0_RECV ) { \ + uint8_t c = inp(UDR0); \ + cb(c); \ +} +#define ReceiveUart1(cb) \ + SIGNAL( SIG_UART1_RECV ) { \ + uint8_t c = inp(UDR1); \ + cb(c); \ +} + +#endif diff --git a/sw/airborne/autopilot/ubx.h b/sw/airborne/autopilot/ubx.h new file mode 100644 index 00000000000..4b618ae4e37 --- /dev/null +++ b/sw/airborne/autopilot/ubx.h @@ -0,0 +1,38 @@ +/* + * Paparazzi autopilot $Id$ + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +/* + * UBX protocol specific code + * +*/ + + +#ifndef UBX_H +#define UBX_H + +#define GPS_FIX_VALID(gps_mode) (gps_mode == 3) +extern const int32_t utm_east0; +extern const int32_t utm_north0; + +#endif /* UBX_H */ diff --git a/sw/airborne/fly_by_wire/Makefile b/sw/airborne/fly_by_wire/Makefile new file mode 100644 index 00000000000..e1072929f33 --- /dev/null +++ b/sw/airborne/fly_by_wire/Makefile @@ -0,0 +1,77 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +LOCAL_CFLAGS= $(CTL_BRD_FLAGS) + +ARCH = atmega8 +TARGET = fbw +#LOW_FUSE = 3f # crystal # +#LOW_FUSE = 31 # internal 1MHz # +#LOW_FUSE = 1e # ceramic resonator slow rising power p26 # +LOW_FUSE = 2e # ceramic resonator slow rising power p26 # +HIGH_FUSE = cb +EXT_FUSE = ff +LOCK_FUSE = ff +VARINCLUDE = $(PAPARAZZI_HOME)/var/include +ACINCLUDE = $(PAPARAZZI_HOME)/var/$(AIRCRAFT) +INCLUDES = -I ../../include -I $(VARINCLUDE) -I $(ACINCLUDE) + +ifeq ($(CTL_BRD_VERSION),V1_2_1) +CTL_BRD_FLAGS=-DCTL_BRD_V1_2_1 +endif + +ifeq ($(CTL_BRD_VERSION),V1_2) +CTL_BRD_FLAGS=-DCTL_BRD_V1_2 +endif + +ifeq ($(CTL_BRD_VERSION),V1_1) +CTL_BRD_FLAGS=-DCTL_BRD_V1_1 +endif + + +$(TARGET).srcs = \ + main.c \ + ppm.c \ + servo.c \ + spi.c \ + uart.c \ + adc_fbw.c \ + + +include ../../../conf/Makefile.local +include ../../../conf/Makefile.avr + +fbw.install : warn_conf + +warn_conf : + @echo + @echo '###########################################################' + @grep AIRFRAME_NAME $(ACINCLUDE)/airframe.h + @grep RADIO_NAME $(ACINCLUDE)/radio.h + @echo '###########################################################' + @echo + + +main.o .depend : $(ACINCLUDE)/radio.h $(ACINCLUDE)/airframe.h + +clean : avr_clean + diff --git a/sw/airborne/fly_by_wire/README b/sw/airborne/fly_by_wire/README new file mode 100644 index 00000000000..d56db63899e --- /dev/null +++ b/sw/airborne/fly_by_wire/README @@ -0,0 +1,24 @@ +# $Id$ +# Copyright (C) 2003 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + + + + diff --git a/sw/airborne/fly_by_wire/adc_fbw.c b/sw/airborne/fly_by_wire/adc_fbw.c new file mode 100644 index 00000000000..911ac705d41 --- /dev/null +++ b/sw/airborne/fly_by_wire/adc_fbw.c @@ -0,0 +1,120 @@ +/* + * Paparazzi fly by wire adc functions + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * Copyright (C) 2002 Trammell Hudson + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +//// ADC3 MVSUP +//// ADC6 MVSERVO + + +#include +#include +#include +#include "adc_fbw.h" + + + +#if defined CTL_BRD_V1_2 || defined CTL_BRD_V1_2_1 + +#define VOLTAGE_TIME 0x07 +#define ANALOG_PORT PORTC +#define ANALOG_PORT_DIR DDRC + + +// +#define ANALOG_VREF _BV(REFS0) | _BV(REFS1) + +uint16_t adc_samples[ NB_ADC ]; + +static struct adc_buf* buffers[NB_ADC]; + +void adc_buf_channel(uint8_t adc_channel, struct adc_buf* s) { + buffers[adc_channel] = s; +} + +void +adc_init( void ) +{ + uint8_t i; + /* Ensure that our port is for input with no pull-ups */ + ANALOG_PORT = 0x00; + ANALOG_PORT_DIR = 0x00; + + /* Select our external voltage ref */ + ADMUX = ANALOG_VREF; + + /* Select out clock, turn on the ADC interrupt and start conversion */ + ADCSRA = 0 + | VOLTAGE_TIME + | _BV(ADEN ) + | _BV(ADIE ) + | _BV(ADSC ); + + /* Init to 0 (usefull ?) */ + for(i = 0; i < NB_ADC; i++) + buffers[i] = (struct adc_buf*)0; +} + +/** + * Called when the voltage conversion is finished + * + * 8.913kHz on mega128@16MHz 1kHz/channel ?? +*/ + + +SIGNAL( SIG_ADC ) +{ + uint8_t adc_input = ADMUX & 0x7; + struct adc_buf* buf = buffers[adc_input]; + uint16_t adc_value = ADCW; + /* Store result */ + adc_samples[ adc_input ] = adc_value; + + if (buf) { + uint8_t new_head = buf->head + 1; + if (new_head >= AV_NB_SAMPLE) new_head = 0; + buf->sum -= buf->values[new_head]; + buf->values[new_head] = adc_value; + buf->sum += adc_value; + buf->head = new_head; + } + + /* Find the next input */ + adc_input++; + if (adc_input == 4) + adc_input = 6; // ADC 4 and 5 for i2c + if( adc_input >= 8 ) { + adc_input = 0; +#ifdef CTL_BRD_V1_2 + adc_input = 1; // WARNING ADC0 is for rservo driver reset on v1.2.0 +#endif /* CTL_BRD_V1_2 */ + } + /* Select it */ + ADMUX = adc_input | ANALOG_VREF; + /* Restart the conversion */ + sbi( ADCSR, ADSC ); +} + +#endif /* CTL_BRD_V1_2 || CTL_BRD_V1_2_1 */ diff --git a/sw/airborne/fly_by_wire/adc_fbw.h b/sw/airborne/fly_by_wire/adc_fbw.h new file mode 100644 index 00000000000..6dd77eb3354 --- /dev/null +++ b/sw/airborne/fly_by_wire/adc_fbw.h @@ -0,0 +1,61 @@ +/* + * Paparazzi fly by wire adc functions + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * Copyright (C) 2002 Trammell Hudson + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef _ADC_H_ +#define _ADC_H_ + +#include "airframe.h" + +#if defined CTL_BRD_V1_2 || defined CTL_BRD_V1_2_1 + +#include + + +#define NB_ADC 8 + +/* Array containing the last measured value */ +extern uint16_t adc_samples[ NB_ADC ]; + +void adc_init( void ); + +#define AV_NB_SAMPLE 0x20 + +struct adc_buf { + uint16_t sum; + uint16_t values[AV_NB_SAMPLE]; + uint8_t head; +}; + +/* Facility to store last values in a circular buffer for a specific + channel: allocate a (struct adc_buf) and register it with the following + function */ +void adc_buf_channel(uint8_t adc_channel, struct adc_buf* s); + + +#endif /* CTL_BRD_V1_2 || CTL_BRD_V1_2 */ + +#endif /* _ADC_H_ */ diff --git a/sw/airborne/fly_by_wire/link_autopilot.h b/sw/airborne/fly_by_wire/link_autopilot.h new file mode 100644 index 00000000000..09edd010f4f --- /dev/null +++ b/sw/airborne/fly_by_wire/link_autopilot.h @@ -0,0 +1,65 @@ +/* $Id$ + * + * (c) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef LINK_AUTOPILOT_H +#define LINK_AUTOPILOT_H + +#include + +#include "std.h" +#include "radio.h" +#include "airframe.h" + +/* + * System clock in MHz. + */ +#define CLOCK 16 + +typedef int16_t pprz_t; // type of commands + +/* !!!!!!!!!!!!!!!!!!! Value used in gen_airframe.ml !!!!!!!!!!!!!!!!! */ +#define MAX_PPRZ (600 * CLOCK) +#define MIN_PPRZ -MAX_PPRZ + +struct inter_mcu_msg { + int16_t channels[RADIO_CTL_NB]; + uint8_t ppm_cpt; + uint8_t status; + uint8_t nb_err; + uint8_t vsupply; /* 1e-1 V */ +}; + +// Status bits from FBW to AUTOPILOT +#define STATUS_RADIO_OK 0 +#define RADIO_REALLY_LOST 1 +#define AVERAGED_CHANNELS_SENT 2 +#define MASK_FBW_CHANGED 0x3 + +// Statut bits from AUTOPILOT to FBW +#define STATUS_AUTO_OK 0 + +#define FRAME_LENGTH (sizeof(struct inter_mcu_msg)+1) + +#define TRESHOLD_MANUAL_PPRZ (MIN_PPRZ / 2) + +#endif // LINK_AUTOPILOT_H diff --git a/sw/airborne/fly_by_wire/main.c b/sw/airborne/fly_by_wire/main.c new file mode 100644 index 00000000000..5bfe6d6ad70 --- /dev/null +++ b/sw/airborne/fly_by_wire/main.c @@ -0,0 +1,182 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include + +#include "timer.h" +#include "servo.h" +#include "ppm.h" +#include "spi.h" +#include "link_autopilot.h" +#include "radio.h" + + +#include "uart.h" + + +#ifndef CTL_BRD_V1_1 +#include "adc_fbw.h" +struct adc_buf vsupply_adc_buf; +struct adc_buf vservos_adc_buf; +#endif + +uint8_t mode; +static uint8_t time_since_last_mega128; +static uint16_t time_since_last_ppm; +bool_t radio_ok, mega128_ok, radio_really_lost; + +static const pprz_t failsafe[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + +static uint8_t ppm_cpt, last_ppm_cpt; + +#define STALLED_TIME 30 // 500ms with a 60Hz timer +#define REALLY_STALLED_TIME 300 // 5s with a 60Hz timer + + +/* static inline void status_transmit( void ) { */ +/* uint8_t i; */ +/* uart_transmit(7); */ +/* uart_transmit(7); */ +/* for (i=0; i>8); */ +/* uart_transmit(ppm_pulses[i] & 0xff); */ +/* } */ +/* uart_transmit('\n'); */ +/* } */ + + +/* Prepare data to be sent to mcu0 */ +static inline void to_autopilot_from_last_radio (void) { + uint8_t i; + for(i = 0; i < RADIO_CTL_NB; i++) + to_mega128.channels[i] = last_radio[i]; + to_mega128.status = (radio_ok ? _BV(STATUS_RADIO_OK) : 0); + to_mega128.status |= (radio_really_lost ? _BV(RADIO_REALLY_LOST) : 0); + if (last_radio_contains_avg_channels) { + to_mega128.status |= _BV(AVERAGED_CHANNELS_SENT); + last_radio_contains_avg_channels = FALSE; + } + to_mega128.ppm_cpt = last_ppm_cpt; +#ifndef CTL_BRD_V1_1 + to_mega128.vsupply = VoltageOfAdc(vsupply_adc_buf.sum/AV_NB_SAMPLE) * 10; +#else + to_mega128.vsupply = 0; +#endif +} + + +int main( void ) +{ + uart_init_tx(); + uart_print_string("FBW Booting $Id$\n"); + +#ifndef CTL_BRD_V1_1 + adc_init(); + adc_buf_channel(3, &vsupply_adc_buf); + adc_buf_channel(6, &vservos_adc_buf); +#endif + timer_init(); + servo_init(); + ppm_init(); + spi_init(); + sei(); + while( 1 ) { + if( ppm_valid ) { + ppm_valid = FALSE; + ppm_cpt++; + radio_ok = TRUE; + radio_really_lost = FALSE; + time_since_last_ppm = 0; + last_radio_from_ppm(); + if (last_radio_contains_avg_channels) { + mode = MODE_OF_PPRZ(last_radio[RADIO_MODE]); + } + if (mode == MODE_MANUAL) { + servo_set(last_radio); + } + } else if (mode == MODE_MANUAL && radio_really_lost) { + mode = MODE_AUTO; + } + + if ( !SpiIsSelected() && spi_was_interrupted ) { + spi_was_interrupted = FALSE; + if (mega128_receive_valid) { + time_since_last_mega128 = 0; + mega128_ok = TRUE; + if (mode == MODE_AUTO) + servo_set(from_mega128.channels); + } + to_autopilot_from_last_radio(); + spi_reset(); + } + + if (time_since_last_ppm >= STALLED_TIME) { + radio_ok = FALSE; + } + if (time_since_last_ppm >= REALLY_STALLED_TIME) { + radio_really_lost = TRUE; + } + if (time_since_last_mega128 == STALLED_TIME) { + mega128_ok = FALSE; + } + + if ((mode == MODE_MANUAL && !radio_ok) || + (mode == MODE_AUTO && !mega128_ok)) { + servo_set(failsafe); + } + + if(timer_periodic()) { + static uint8_t _1Hz; + static uint8_t _20Hz; + _1Hz++; + _20Hz++; + if (_1Hz >= 60) { + _1Hz = 0; + last_ppm_cpt = ppm_cpt; + ppm_cpt = 0; + } + if (_20Hz >= 3) { + _20Hz = 0; + servo_transmit(); + // status_transmit(); + } + if (time_since_last_mega128 < STALLED_TIME) + time_since_last_mega128++; + if (time_since_last_ppm < REALLY_STALLED_TIME) + time_since_last_ppm++; + } + } + return 0; +} diff --git a/sw/airborne/fly_by_wire/ppm.c b/sw/airborne/fly_by_wire/ppm.c new file mode 100644 index 00000000000..adeae0fbd7d --- /dev/null +++ b/sw/airborne/fly_by_wire/ppm.c @@ -0,0 +1,135 @@ +/* $Id$ + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * (c) 2003 Trammell Hudson + * (c) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include "radio.h" +#include "ppm.h" + +#define AVERAGING_PERIOD (PPM_FREQ/4) + +/* + * Pulse width is computed as the difference between now and the + * previous pulse. If no pulse has been received between then and + * now, the time of the last pulse will be equal to the last pulse + * we measured. Unfortunately, the Input Capture Flag (ICF1) will + * not be set since the interrupt routine disables it. + * + * Sync pulses are timed with Timer2, which runs at Clk/1024. This + * is slow enough at both 4 and 8 Mhz to measure the lengthy (10ms + * or longer) pulse. + * + * Otherwise, compute the pulse width with the 16-bit timer1, + * push the pulse width onto the stack and increment the + * pulse counter until we have received eight pulses. + */ + +uint16_t ppm_pulses[ PPM_NB_PULSES ]; +pprz_t last_radio[ PPM_NB_PULSES ]; +pprz_t avg_last_radio[ PPM_NB_PULSES ]; +bool_t last_radio_contains_avg_channels = FALSE; +volatile bool_t ppm_valid; + +/* MC3030, Trame PPM7: 25ms, 10.4 au neutre, + sync pulse = 16.2ms with low value on every channels */ + + + + +#define RestartPpmCycle() { state = 0; sync_start = TCNT2; return; } + +SIGNAL( SIG_INPUT_CAPTURE1 ) +{ + static uint16_t last; + uint16_t this; + uint16_t width; + static uint8_t state; + static uint8_t sync_start; + + this = ICR1; + width = this - last; + last = this; + + if( state == 0 ) { + uint8_t end = inp( TCNT2 ); + uint8_t diff = (end - sync_start); + sync_start = end; + + /* The frame period of the mc3030 seems to be 25ms. + * One pulse lasts from 1.05ms to 2.150ms. + * Sync pulse is at least 7ms : (7000*CLOCK)/1024 = 109 + */ + if( diff > (uint8_t)(((uint32_t)(7000ul*CLOCK))/1024ul) ) { + state = 1; + } + } + else { + /* Read a data pulses */ + if( width < 700ul*CLOCK || width > 2300ul*CLOCK) + RestartPpmCycle(); + ppm_pulses[state - 1] = width; + + if (state >= PPM_NB_PULSES) { + ppm_valid = 1; + RestartPpmCycle(); + } else + state++; + } + return; +} + +#define Int16FromPulse(i) (int16_t)((ppm_pulses[(i)] - PpmOfUs(((int[])RADIO_NEUTRALS_US)[i]))*(2*MAX_PPRZ)/(PpmOfUs(((int[])RADIO_MAXS_US[i])-((int[])RADIO_MINS_US[i])))) + + +/* Copy from the ppm receiving buffer to the buffer sent to mcu0 */ +void last_radio_from_ppm() { + static uint8_t avg_cpt = 0; /* Counter for averaging */ + uint8_t i; + + for(i = 0; i < RADIO_CTL_NB; i++) { + int16_t pprz = Int16FromPulse(i); + if (pprz > MAX_PPRZ) + pprz = MAX_PPRZ; + else if (pprz < MIN_PPRZ) + pprz = MIN_PPRZ; + + if (i == RADIO_THROTTLE) { + int16_t gaz = pprz/2; + last_radio[i] = (gaz < 0 ? 0 : gaz); + } else if (AveragedChannel(i)) { + avg_last_radio[i] += pprz / AVERAGING_PERIOD; + } else + last_radio[i] = pprz; + } + + avg_cpt++; + if (avg_cpt == AVERAGING_PERIOD) { + avg_cpt = 0; + for(i = 0; i < RADIO_CTL_NB; i++) + if (AveragedChannel(i)) { + last_radio[i] = avg_last_radio[i]; + avg_last_radio[i] = 0; + } + last_radio_contains_avg_channels = TRUE; + } +} diff --git a/sw/airborne/fly_by_wire/ppm.h b/sw/airborne/fly_by_wire/ppm.h new file mode 100644 index 00000000000..fab02455e13 --- /dev/null +++ b/sw/airborne/fly_by_wire/ppm.h @@ -0,0 +1,102 @@ +/* $Id$ + * + * Decoder for the trainer ports or hacked receivers for both + * Futaba and JR formats. The ppm_valid flag is set whenever + * a valid frame is received. + * + * Pulse widths are stored as unscaled 16-bit values in ppm_pulses[]. + * If you require actual microsecond values, divide by CLOCK. + * For an 8 Mhz clock and typical servo values, these will range + * from 0x1F00 to 0x4000. + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * (c) 2002 Trammell Hudson + * (c) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef PPM_H +#define PPM_H + + +/** + * Receiver types + */ +#define RXFUTABA 0 +#define RXJR 1 + +#define PPM_RX_TYPE RXFUTABA +#define PPM_FREQ 40 // 25ms + +#include +#include + +#include "timer.h" +#include "link_autopilot.h" + +#define PpmOfUs(x) ((x)*CLOCK) + +#define PPM_DDR DDRB +#define PPM_PORT PORTB +#define PPM_PIN PB0 + +/* + * PPM pulses are falling edge clocked on the ICP, which records + * the state of the global clock. We do not use any noise + * canceling features. + * + * JR might be rising edge clocked; set that as an option + */ +static inline void +ppm_init( void ) +{ +#if PPM_RX_TYPE == RXFUTABA + cbi( TCCR1B, ICES1 ); +#elif PPM_RX_TYPE == RXJR + sbi( TCCR1B, ICES1 ); +#else +# error "ppm.h: Unknown receiver type in PPM_RX_TYPE" +#endif + + /* No noise cancelation */ + sbi( TCCR1B, ICNC1 ); + + /* Set ICP to input, no internal pull up */ + cbi( PPM_DDR, PPM_PIN); + + /* Enable interrupt on input capture */ + sbi( TIMSK, TICIE1 ); +} + +#define PPM_NB_PULSES RADIO_CTL_NB + +extern volatile bool_t ppm_valid; +extern pprz_t last_radio[PPM_NB_PULSES]; +extern bool_t last_radio_contains_avg_channels; + + +#define MODE_MANUAL 0 +#define MODE_AUTO 1 + +#define MODE_OF_PPRZ(mode) ((mode) < TRESHOLD_MANUAL_PPRZ ? MODE_MANUAL : MODE_AUTO) + +extern void last_radio_from_ppm(void); +#endif diff --git a/sw/airborne/fly_by_wire/servo.c b/sw/airborne/fly_by_wire/servo.c new file mode 100644 index 00000000000..ae7e5253758 --- /dev/null +++ b/sw/airborne/fly_by_wire/servo.c @@ -0,0 +1,193 @@ +/* $Id$ + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * (c) 2002 Trammell Hudson + * (c) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + + +#include +#include +#include "servo.h" +#include "link_autopilot.h" + +#include "airframe.h" + +#include "uart.h" + + +/* + * Paparazzi boards have one 4017 servo driver. + * It is driven by OCR1A (PB1) with reset on PORTD5. + */ +#define _4017_NB_CHANNELS 10 + +#ifdef CTL_BRD_V1_1 +#define _4017_RESET_PORT PORTD +#define _4017_RESET_DDR DDRD +#define _4017_RESET_PIN 5 +#endif /* CTL_BRD_V1_1 */ + +#ifdef CTL_BRD_V1_2 +#define _4017_RESET_PORT PORTC +#define _4017_RESET_DDR DDRC +#define _4017_RESET_PIN 0 +#endif /* CTL_BRD_V1_2 */ + +#ifdef CTL_BRD_V1_2_1 +#define _4017_RESET_PORT PORTD +#define _4017_RESET_DDR DDRD +#define _4017_RESET_PIN 7 +#endif /* CTL_BRD_V1_2 */ + +#define _4017_CLOCK_PORT PORTB +#define _4017_CLOCK_DDR DDRB +#define _4017_CLOCK_PIN PB1 + +#define SERVO_OCR OCR1A +#define SERVO_ENABLE OCIE1A +#define SERVO_FLAG OCF1A +#define SERVO_FORCE FOC1A +#define SERVO_COM0 COM1A0 +#define SERVO_COM1 COM1A1 + +/* Following macro is required since the compiler does not solve at + compile-time indexation in a known array with a known index */ +#define SERVO_NEUTRAL_(i) SERVOS_NEUTRALS_ ## i +#define SERVO_NEUTRAL(i) (SERVO_NEUTRAL_(i)*CLOCK) + +#define SERVO_NEUTRAL_I(i) (((int[])SERVOS_NEUTRALS[i])*CLOCK) +#define SERVO_MIN_I(i) (((int[])SERVOS_MINS[i])*CLOCK) + +#define SERVO_MIN (SERVO_MIN_US*CLOCK) +#define SERVO_MAX (SERVO_MAX_US*CLOCK) +#define ChopServo(x) ((x) < SERVO_MIN ? SERVO_MIN : ((x) > SERVO_MAX ? SERVO_MAX : (x))) + +/* holds the servo pulses width in clock ticks */ +static uint16_t servo_widths[_4017_NB_CHANNELS]; + +/* + * We use the output compare registers to generate our servo pulses. + * These should be connected to a decade counter that routes the + * pulses to the appropriate servo. + * + * Initialization involves: + * + * - Reseting the decade counters + * - Writing the first pulse width to the counters + * - Setting output compare to set the clock line by calling servo_enable() + * - Bringing down the reset lines + * + * Ideally, you can use two decade counters to drive 20 servos. + */ +void +servo_init( void ) +{ + uint8_t i; + + /* Configure the reset and clock lines */ + _4017_RESET_DDR |= _BV(_4017_RESET_PIN); + _4017_CLOCK_DDR |= _BV(_4017_CLOCK_PIN); + + /* Reset the decade counter */ + sbi( _4017_RESET_PORT, _4017_RESET_PIN ); + + /* Lower the regular servo line */ + cbi( _4017_CLOCK_PORT, _4017_CLOCK_PIN ); + + /* Set all servos at their midpoints */ + for( i=0 ; i < _4017_NB_CHANNELS ; i++ ) + servo_widths[i] = SERVO_MIN; + + /* Set servos to go off some long time from now */ + SERVO_OCR = 32768ul; + + /* + * Configure output compare to toggle the output bits. + */ + TCCR1A |= _BV(SERVO_COM0 ); + + /* Clear the interrupt flags in case they are set */ + TIFR = _BV(SERVO_FLAG); + + /* Unassert the decade counter reset to start it running */ + cbi( _4017_RESET_PORT, _4017_RESET_PIN ); + + /* Enable our output compare interrupts */ + TIMSK |= _BV(SERVO_ENABLE ); +} + + +/* + * Interrupt routine + * + * write the next pulse width to OCR register and + * assert the servo signal. It will be cleared by + * the following compare match. + */ +SIGNAL( SIG_OUTPUT_COMPARE1A ) +{ + static uint8_t servo = 0; + uint16_t width; + + if (servo >= _4017_NB_CHANNELS) { + sbi( _4017_RESET_PORT, _4017_RESET_PIN ); + servo = 0; + // FIXME: 500 ns required by 4017 reset ???? why does it work without! + // asm( "nop; nop; nop; nop;nop; nop; nop; nop;nop; nop; nop; nop;nop; nop; nop; nop;" ); + cbi( _4017_RESET_PORT, _4017_RESET_PIN ); + } + + width = servo_widths[servo]; + + SERVO_OCR += width; + + TCCR1A |= _BV(SERVO_FORCE); + + servo++; +} + +void servo_set_one(uint8_t servo, uint16_t value_us) { + servo_widths[servo] = ChopServo(CLOCK*value_us); +} + +void +servo_transmit(void) { + uint8_t servo; + uart_transmit((uint8_t)0); uart_transmit((uint8_t)0); + + for(servo = 0; servo < _4017_NB_CHANNELS; servo++) { + uart_transmit((uint8_t)(servo_widths[servo] >> 8)); + uart_transmit((uint8_t)(servo_widths[servo] & 0xff)); + } + uart_transmit((uint8_t)'\n'); +} + + +/* + * + * defines how servos react to radio control or autopilot channels + * + */ + +void servo_set(const pprz_t values[]) { + ServoSet(values); /*Generated from airframe.xml */ +} diff --git a/sw/airborne/fly_by_wire/servo.h b/sw/airborne/fly_by_wire/servo.h new file mode 100644 index 00000000000..cd15fa2baab --- /dev/null +++ b/sw/airborne/fly_by_wire/servo.h @@ -0,0 +1,61 @@ +/* $Id$ + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * (c) 2002 Trammell Hudson + * (c) 2003 Pascal Brisset, Antoine Drouin + * + * This is the new decade counter based servo driving code. It uses + * one 16-bit output compare registers to determine when the regular + * servo clock line should be toggled, causing the output to move to the + * next servo. The other 16-bit output compare is used to drive a + * JR or Futaba compatible high-speed digital servo. + * + * User visibile routines: + * + * - servo_init(); + * + * Call once at the start of the program to bring the servos online + * and start the external decade counters. This will also start the + * high speed servo. + * + * - servo_make_pulse_width( length ); + * + * Converts a position value between 0 and 65536 to actual pulsewidth. 0 is + * all the way left (1.0 ms pulse) and 65536 is all the way right (2.0 ms + * pulse). Use it like this: + * + * servo_widths[ i ] = servo_make_pulse_width( val ) + * + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef SERVO_H +#define SERVO_H + +#include +#include "timer.h" + +extern void servo_init( void ); +extern void servo_set(const pprz_t values[]); +extern void servo_set_one(uint8_t servo, uint16_t value_us); +extern void servo_transmit(void); + + +#endif /* SERVO_H */ diff --git a/sw/airborne/fly_by_wire/spi.c b/sw/airborne/fly_by_wire/spi.c new file mode 100644 index 00000000000..955615d5971 --- /dev/null +++ b/sw/airborne/fly_by_wire/spi.c @@ -0,0 +1,112 @@ +/* + * $Id$ + * + * Paparazzi mcu1 spi functions + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include + +#include "spi.h" + +#define IT_PORT PORTD +#define IT_DDR DDRD +#define IT_PIN 7 + +#define SPI_DDR DDRB +#define SPI_MOSI_PIN 3 +#define SPI_MISO_PIN 4 +#define SPI_SCK_PIN 5 + +struct inter_mcu_msg from_mega128; +struct inter_mcu_msg to_mega128; +volatile bool_t mega128_receive_valid = FALSE; +volatile bool_t spi_was_interrupted = FALSE; + +static volatile uint8_t idx_buf = 0; +static volatile uint8_t xor_in, xor_out; + +void spi_reset(void) { + idx_buf = 0; + xor_in = 0; + xor_out = ((uint8_t*)&to_mega128)[idx_buf]; + SPDR = xor_out; + mega128_receive_valid = FALSE; +} + +void spi_init(void) { + to_mega128.status = 0; + to_mega128.nb_err = 0; + + /* set it pin output */ + // IT_DDR |= _BV(IT_PIN); + + /* set MISO pin output */ + SPI_DDR |= _BV(SPI_MISO_PIN); + /* enable SPI, slave, MSB first, sck idle low */ + SPCR = _BV(SPE); + /* enable interrupt */ + SPCR |= _BV(SPIE); +} + +SIGNAL(SIG_SPI) { + static uint8_t tmp; + + idx_buf++; + + spi_was_interrupted = TRUE; + + if (idx_buf > FRAME_LENGTH) + return; + /* we have sent/received a complete frame */ + if (idx_buf == FRAME_LENGTH) { + /* read checksum from receive register */ + tmp = SPDR; + /* notify valid frame */ + if (tmp == xor_in) + mega128_receive_valid = TRUE; + else + to_mega128.nb_err++; + return; + } + + /* we are sending/receiving payload */ + if (idx_buf < FRAME_LENGTH - 1) { + /* place new payload byte in send register */ + tmp = ((uint8_t*)&to_mega128)[idx_buf]; + SPDR = tmp; + xor_out = xor_out ^ tmp; + } + /* we are done sending the payload */ + else { // idx_buf == FRAME_LENGTH - 1 + /* place checksum in send register */ + SPDR = xor_out; + } + + /* read the byte from receive register */ + tmp = SPDR; + ((uint8_t*)&from_mega128)[idx_buf-1] = tmp; + xor_in = xor_in ^ tmp; +} diff --git a/sw/airborne/fly_by_wire/spi.h b/sw/airborne/fly_by_wire/spi.h new file mode 100644 index 00000000000..84ca3a2bdc2 --- /dev/null +++ b/sw/airborne/fly_by_wire/spi.h @@ -0,0 +1,48 @@ +/* $Id$ + * + * Paparazzi fbw spi functions + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef SPI_H +#define SPI_H + +#include "link_autopilot.h" + + +#define SPI_PORT PORTB +#define SPI_PIN PINB +#define SPI_SS_PIN 2 + +#define SpiIsSelected() (bit_is_clear(SPI_PIN, SPI_SS_PIN)) + +extern struct inter_mcu_msg from_mega128; +extern struct inter_mcu_msg to_mega128; +extern volatile bool_t mega128_receive_valid; +extern volatile bool_t spi_was_interrupted; + + +void spi_init(void); +void spi_reset(void); + + +#endif /* SPI_H */ diff --git a/sw/airborne/fly_by_wire/test/Makefile b/sw/airborne/fly_by_wire/test/Makefile new file mode 100644 index 00000000000..3d1ba17283d --- /dev/null +++ b/sw/airborne/fly_by_wire/test/Makefile @@ -0,0 +1,43 @@ + + +all: + @echo "call with 'make TARGET=... compile (or load)'" + +TARGET=check_uart + +LOCAL_CFLAGS= $(CTL_BRD_FLAGS) + +ARCH = atmega8 +INCLUDES = -I ../ -I ../../../include -I ../../../var/include + +CONF_DIR = ../../../../conf +CONF_XML = $(CONF_DIR)/conf.xml +XML_GET=../../../lib/ocaml/xml_get.out + +ifeq ($(CTL_BRD_VERSION),V1_2_1) +CTL_BRD_FLAGS=-DCTL_BRD_V1_2_1 +endif + +ifeq ($(CTL_BRD_VERSION),V1_2) +CTL_BRD_FLAGS=-DCTL_BRD_V1_2 +endif + +ifeq ($(CTL_BRD_VERSION),V1_1) +CTL_BRD_FLAGS=-DCTL_BRD_V1_1 +endif + +rc_transmitter.srcs = rc_transmitter.c ../ppm.c ../uart.c + +setup_servos.srcs = setup_servos.c ../uart.c ../servo.c + +check_uart.srcs = check_uart.c ../uart.c + +tx_adcs.srcs = tx_adcs.c ../uart.c ../adc_fbw.c + +check_spi.srcs = check_spi.c ../uart.c ../spi.c + + +include ../../../../conf/Makefile.local +include ../../../../conf/Makefile.avr + +clean: avr_clean diff --git a/sw/airborne/fly_by_wire/test/check_spi.c b/sw/airborne/fly_by_wire/test/check_spi.c new file mode 100644 index 00000000000..7f0d3f5d099 --- /dev/null +++ b/sw/airborne/fly_by_wire/test/check_spi.c @@ -0,0 +1,43 @@ +#include +#include "timer.h" +#include "spi.h" +#include "uart.h" + +/* Fill the message with dummy values */ +void fill_spi_msg(void) { + uint8_t i; + for(i = 0; i < RADIO_CTL_NB; i++) + to_mega128.channels[i] = i * (MAX_PPRZ / RADIO_CTL_NB); + to_mega128.status = 0xff; + to_mega128.ppm_cpt = 0xff; + to_mega128.vsupply = 0xff; +} + +int main( void ) +{ + uart_init_tx(); + uart_print_string("Booting FBW MCU: $Id$\n"); + spi_init(); + timer_init(); + sei(); + + uint8_t _1Hz = 0; + while( 1 ) { + if(timer_periodic()) { + _1Hz++; + if (_1Hz >= 60) { + _1Hz = 0; + uart_print_string("FBW MCU Alive\n"); + } + } + if ( !SpiIsSelected() && spi_was_interrupted ) { + spi_was_interrupted = FALSE; + if (mega128_receive_valid) { + uart_print_string("SPI OK from mega128\n"); + } else + uart_print_string("SPI error from mega128\n"); + fill_spi_msg(); + spi_reset(); + } + } +} diff --git a/sw/airborne/fly_by_wire/test/check_uart.c b/sw/airborne/fly_by_wire/test/check_uart.c new file mode 100644 index 00000000000..b0f067a7ba9 --- /dev/null +++ b/sw/airborne/fly_by_wire/test/check_uart.c @@ -0,0 +1,51 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include "timer.h" +#include "uart.h" + +int main( void ) { + uart_init_tx(); + uart_print_string("Booting FBW MCU: $Id$\n"); + timer_init(); + sei(); + uint8_t _1Hz = 0; + uint8_t foo = 0; + while( 1 ) { + if(timer_periodic()) { + _1Hz++; + if (_1Hz >= 60) { + _1Hz = 0; + foo++; + uart_print_string("FBW MCU uart check : alive ["); + uart_print_hex(foo); + uart_print_string("]\n"); + } + } + } + return 0; +} diff --git a/sw/airborne/fly_by_wire/test/rc_transmitter.c b/sw/airborne/fly_by_wire/test/rc_transmitter.c new file mode 100644 index 00000000000..4f487c443ac --- /dev/null +++ b/sw/airborne/fly_by_wire/test/rc_transmitter.c @@ -0,0 +1,71 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include + +#include "timer.h" +#include "ppm.h" +#include "radio.h" + +#include "uart.h" + +inline void transmit_radio(void) { + uint8_t ctl; + uart_transmit((uint8_t)0); uart_transmit((uint8_t)0); + for(ctl = 0; ctl < RADIO_CTL_NB; ctl++) { + extern uint16_t ppm_pulses[]; + uint16_t x = ppm_pulses[ctl] / 16; + uart_transmit((uint8_t)(x >> 8)); + uart_transmit((uint8_t)(x & 0xff)); + } + uart_transmit((uint8_t)'\n'); + // uart_transmit('A');uart_transmit('\n'); +} + +int main( void ) { + uart_init_tx(); + uart_print_string("Calib_radio Booting $Id$\n"); + timer_init(); + ppm_init(); + sei(); + int n = 0; + while( 1 ) { + if( ppm_valid ) { + ppm_valid = FALSE; + n++; + if (n == 4) { + n = 0; + transmit_radio(); + } + } + + // A rajouter pour envoyer un message de vie quand la radio n'est pas recue + // if(timer_periodic()) { + // uart_transmit('B');uart_transmit('\n'); + // } + } + return 0; +} diff --git a/sw/airborne/fly_by_wire/test/setup_servos.c b/sw/airborne/fly_by_wire/test/setup_servos.c new file mode 100644 index 00000000000..e8e55bf0795 --- /dev/null +++ b/sw/airborne/fly_by_wire/test/setup_servos.c @@ -0,0 +1,119 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include + +#include "timer.h" +#include "servo.h" +#include "uart.h" + +#define MSG_START '\0' +#define MSG_END '\n' + +#define UNINIT 0 +#define GOT_START 1 +#define GOT_CHANNEL 2 +#define GOT_LOW 3 +#define GOT_HI 4 + + +static uint8_t msg_status; +static uint8_t servo; +static uint16_t value; /* micro-seconds */ +static volatile bool_t msg_valid; + +static inline void parse_msg(uint8_t c) { + switch (msg_status) { + case UNINIT: + if (c==MSG_START) + msg_status++; + else + goto restart; + break; + case GOT_START: + servo=c; + msg_status++; + break; + case GOT_CHANNEL: + value=c << 8; + msg_status++; + break; + case GOT_LOW: + value |= c; + msg_status++; + break; + case GOT_HI: + if (c == MSG_END) + msg_valid = TRUE; + goto restart; + } + return; + restart: + msg_status = UNINIT; +} + +/* RxUartCb(parse_msg) */ + +SIGNAL( SIG_UART_RECV ) { + uint8_t c = inp( UDR ); + parse_msg(c); +} + + +int main( void ) { + uart_init_tx(); + uart_init_rx(); + timer_init(); + servo_init(); + sei(); + + uart_print_string("$Id$\n"); + + while (1) { + if (msg_valid) { + msg_valid = FALSE; + servo_set_one(servo, value); + } + + +/* if (timer_periodic()) { */ +/* servo_set_one(SERVO, value); */ +/* value += 10; */ +/* if (value > 2000) value = 1000; */ +/* } */ + +/* if (timer_periodic()) { */ +/* static uint8_t foo; */ +/* if (!foo) { */ +/* uart_transmit('A'); */ +/* uart_transmit('\n'); */ +/* } */ +/* foo++; */ +/* } */ + } + + return 0; +} diff --git a/sw/airborne/fly_by_wire/test/tx_adcs.c b/sw/airborne/fly_by_wire/test/tx_adcs.c new file mode 100644 index 00000000000..2f2d0e97581 --- /dev/null +++ b/sw/airborne/fly_by_wire/test/tx_adcs.c @@ -0,0 +1,66 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include "timer.h" +#include "uart.h" +#include "adc_fbw.h" + +static struct adc_buf buffers[NB_ADC]; + +void transmit_adc(void) { + uint8_t i; + uart_transmit((uint8_t)0); uart_transmit((uint8_t)0); + for(i = 0; i < NB_ADC; i++) { + uint16_t value = buffers[i].sum / AV_NB_SAMPLE; + uart_transmit((uint8_t)(value >> 8)); + uart_transmit((uint8_t)(value & 0xff)); + } + uart_transmit((uint8_t)'\n'); +} + +int main( void ) { + uint8_t i; + uart_init_tx(); + timer_init(); + adc_init(); + for(i = 0; i < NB_ADC; i++) + adc_buf_channel(i, &buffers[i]); + sei(); + + while( 1 ) { + static uint8_t _1Hz = 0; + + if(timer_periodic()) { + _1Hz++; + if (_1Hz == 60) { + _1Hz = 0; + transmit_adc(); + } + } + } + return 0; +} diff --git a/sw/airborne/fly_by_wire/timer.h b/sw/airborne/fly_by_wire/timer.h new file mode 100644 index 00000000000..84079382ad9 --- /dev/null +++ b/sw/airborne/fly_by_wire/timer.h @@ -0,0 +1,92 @@ +/* $Id$ + * + * Paparazzi fbw timer functions + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * Copyright (C) 2002 Trammell Hudson + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef TIMER_H +#define TIMER_H + +#include +#include +#include +#include "link_autopilot.h" + + +/* + * Enable Timer1 (16-bit) running at Clk/1 for the global system + * clock. This will be used for computing the servo pulse widths, + * PPM decoding, etc. + * + * Low frequency periodic tasks will be signaled by timer 2 + * running at Clk/1024. For 16 Mhz clock, this will be every + * 16384 microseconds, or 61 Hz. + */ +static inline void +timer_init( void ) +{ + /* Timer1 @ Clk/1: System clock, ppm and servos pulses */ + TCCR1A = 0x00; + TCCR1B = 0x01; + + /* Timer2 @ Clk/1024: Periodic clock */ + TCCR2 = 0x07; +} + + +/* + * Retrieve the current time from the global clock in Timer1, + * disabling interrupts to avoid stomping on the TEMP register. + * If interrupts are already off, the non_atomic form can be used. + */ +static inline uint16_t +timer_now( void ) +{ + return TCNT1; +} + +static inline uint16_t +timer_now_non_atomic( void ) +{ + return TCNT1L; +} + + +/* + * Periodic tasks occur when Timer2 overflows. Check and unset + * the overflow bit. We cycle through four possible periodic states, + * so each state occurs every 60 Hz. + */ +static inline bool_t +timer_periodic( void ) +{ + if( !bit_is_set( TIFR, TOV2 ) ) + return FALSE; + + TIFR = 1 << TOV2; + return TRUE; +} + +#endif diff --git a/sw/airborne/fly_by_wire/uart.c b/sw/airborne/fly_by_wire/uart.c new file mode 100644 index 00000000000..12450d2f419 --- /dev/null +++ b/sw/airborne/fly_by_wire/uart.c @@ -0,0 +1,110 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include + + +#include "std.h" +#include "uart.h" + +#define TX_BUF_SIZE 256 +static uint8_t tx_head; /* next free in buf */ +static volatile uint8_t tx_tail; /* next char to send */ +static uint8_t tx_buf[ TX_BUF_SIZE ]; + +/* + * UART Baud rate generation settings: + * + * With 16.0 MHz clock,UBRR=25 => 38400 baud + * + */ +void uart_init_tx( void ) { + /* Baudrate is 38.4k */ + UBRRH = 0; + UBRRL = 25; + /* single speed */ + UCSRA = 0; + /* Enable transmitter */ + UCSRB = _BV(TXEN); + /* Set frame format: 8data, 1stop bit */ + UCSRC = _BV(URSEL) | _BV(UCSZ1) | _BV(UCSZ0); +} + +void uart_init_rx() { + /* Enable receiver */ + UCSRB |= _BV(RXEN); + /* Enable uart receive interrupt */ + sbi( UCSRB, RXCIE ); +} + +void uart_transmit( unsigned char data ) { + if (UCSRB & _BV(TXCIE)) { + /* we are waiting for the last char to be sent : buffering */ + if (tx_tail == tx_head + 1) { /* BUF_SIZE = 256 */ + /* Buffer is full (almost, but tx_head = tx_tail means "empty" */ + return; + } + tx_buf[tx_head] = data; + tx_head++; /* BUF_SIZE = 256 */ + } else { /* Channel is free: just send */ + UDR = data; + sbi(UCSRB, TXCIE); + } +} + +void uart_print_hex ( uint8_t c ) { + const uint8_t hex[16] = { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + uint8_t high = (c & 0xF0)>>4; + uint8_t low = c & 0x0F; + uart_transmit(hex[high]); + uart_transmit(hex[low]); +} + +void uart_print_hex16 ( uint16_t c ) { + uint8_t high = (uint8_t)(c>>8); + uint8_t low = (uint8_t)(c); + uart_print_hex(high); + uart_print_hex(low); +} + +void uart_print_string(const uint8_t* s) { + uint8_t i = 0; + while (s[i]) { + uart_transmit(s[i]); + i++; + } +} + +SIGNAL(SIG_UART_TRANS) { + if (tx_head == tx_tail) { + /* Nothing more to send */ + cbi(UCSRB, TXCIE); /* disable interrupt */ + } else { + UDR = tx_buf[tx_tail]; + tx_tail++; /* warning tx_buf_len is 256 */ + } +} diff --git a/sw/airborne/fly_by_wire/uart.h b/sw/airborne/fly_by_wire/uart.h new file mode 100644 index 00000000000..9c95d4e9e66 --- /dev/null +++ b/sw/airborne/fly_by_wire/uart.h @@ -0,0 +1,39 @@ +/* + * Paparazzi $Id$ + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef _UART_H_ +#define _UART_H_ + +#include + +void uart_init_tx( void ); +void uart_init_rx( void ); +void uart_transmit( unsigned char data ); + +void uart_print_hex ( uint8_t c ); +void uart_print_hex16 ( uint16_t c ); +void uart_print_string(const uint8_t* s); +void uart_print_float( const float * f); + +#endif diff --git a/sw/airborne/quadrirotor_autopilot/Makefile b/sw/airborne/quadrirotor_autopilot/Makefile new file mode 100644 index 00000000000..5b24cf1aa3b --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/Makefile @@ -0,0 +1,95 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + + +LOCAL_CFLAGS= $(CTL_BRD_FLAGS) $(GPS_FLAGS) $(SIMUL_FLAGS) + +CONF_DIR = ../../../conf +CONF_XML = $(CONF_DIR)/conf.xml + +ARCH = atmega128 +TARGET = autopilot + +ifeq ($(CTL_BRD_VERSION),V1_2_1) +LOW_FUSE = e0 +HIGH_FUSE = 99 +CTL_BRD_FLAGS=-DCTL_BRD_V1_2_1 +endif + +ifeq ($(CTL_BRD_VERSION),V1_2) +LOW_FUSE = e0 +HIGH_FUSE = 99 +CTL_BRD_FLAGS=-DCTL_BRD_V1_2 +endif + +EXT_FUSE = ff +LOCK_FUSE = ff +INCLUDES = -I ../autopilot -I ../fly_by_wire -I ../../include +OCAMLC = ocamlc -I ../../lib/ocaml + +AP=../autopilot + + +$(TARGET).srcs = \ + main.c \ + $(AP)/modem.c \ + $(AP)/link_fbw.c\ + $(AP)/spi.c \ + $(AP)/adc.c \ + $(AP)/uart.c \ + kalman.c \ + imu.c \ + control.c \ + + +include ../../../conf/Makefile.local +include ../../../conf/Makefile.avr + +autopilot.install : warn_conf + +warn_conf : + @echo + @echo '###########################################################' + @grep AIRFRAME_NAME ../fly_by_wire/airframe.h + @grep RADIO_NAME ../fly_by_wire/radio.h +# @grep FLIGHT_PLAN_NAME flight_plan.h + @echo '###########################################################' + @echo + + +#.depend : messages.h flight_plan.h ubx_protocol.h inflight_calib.h +main.o : messages.h +nav.o : flight_plan.h +gps_ubx.o : ubx_protocol.h +if_calib.o : inflight_calib.h + +GEN_MESSAGES = ./gen_messages.out +MESSAGES_XML = $(CONF_DIR)/messages.xml + +messages.h : $(MESSAGES_XML) $(GEN_MESSAGES) + $(GEN_MESSAGES) $< > $@ + +$(GEN_MESSAGES) : $(AP)/gen_messages.ml + $(OCAMLC) -o $@ xml-light.cma $< + +clean : avr_clean + rm -f *.out *.cm* messages.h flight_plan.h ubx_protocol.h inflight_calib.h diff --git a/sw/airborne/quadrirotor_autopilot/control.c b/sw/airborne/quadrirotor_autopilot/control.c new file mode 100644 index 00000000000..9faa3770d4b --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/control.c @@ -0,0 +1,47 @@ +#include + + +#include "control.h" +#include "kalman.h" +#include "imu.h" +#include "autopilot.h" + +float control_desired_phi; +float control_desired_phi_dot; +float control_desired_theta; +float control_desired_theta_dot; + +int16_t control_command_roll; +int16_t control_command_pitch; + +#define phi_dot_p_gain -2500. +#define theta_dot_p_gain 2500. + +void control_run_rotational_speed_loop() { + float err = control_desired_phi_dot - kalman_public.phi_dot; + control_command_roll = TRIM_PPRZ(phi_dot_p_gain * err); + err = control_desired_theta_dot - kalman_public.theta_dot; + control_command_pitch = TRIM_PPRZ(theta_dot_p_gain * err); +} + +void control_run_raw_rotational_speed_loop() { + float err = control_desired_phi_dot - imu_sample.gyro_x; + control_command_roll = TRIM_PPRZ(phi_dot_p_gain * err); + err = control_desired_theta_dot - imu_sample.gyro_y; + control_command_pitch = TRIM_PPRZ(theta_dot_p_gain * err); +} + + +#define TRIM(val, limit) ( val < -limit ? -limit : val > limit ? limit : val ) + +#define MAX_PHI_DOT 2. +#define phi_p_gain 3. +#define MAX_THETA_DOT 2. +#define theta_p_gain 3. + +void control_run_attitude_loop() { + float err = control_desired_phi - kalman_public.phi; + control_desired_phi_dot = TRIM(phi_p_gain * err, MAX_PHI_DOT); + err = control_desired_theta - kalman_public.theta; + control_desired_theta_dot = TRIM(theta_p_gain * err, MAX_THETA_DOT); +} diff --git a/sw/airborne/quadrirotor_autopilot/control.h b/sw/airborne/quadrirotor_autopilot/control.h new file mode 100644 index 00000000000..0ea2c3df1f7 --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/control.h @@ -0,0 +1,18 @@ +#ifndef CONTROL_H +#define CONTROL_H + +extern float control_desired_phi; +extern float control_desired_phi_dot; +extern float control_desired_theta; +extern float control_desired_theta_dot; + +extern int16_t control_command_roll; +extern int16_t control_command_pitch; + +void control_run_rotational_speed_loop( void ); +void control_run_raw_rotational_speed_loop( void ); +void control_run_attitude_loop( void ); + + + +#endif /* CONTROL_H */ diff --git a/sw/airborne/quadrirotor_autopilot/imu.c b/sw/airborne/quadrirotor_autopilot/imu.c new file mode 100644 index 00000000000..af426210856 --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/imu.c @@ -0,0 +1,76 @@ +#include + +#include "imu.h" +#include "airframe.h" +#include "adc.h" + +struct ImuSample imu_sample; + +//static struct adc_buf buf_gyro_roll; +//static struct adc_buf buf_accel_y; +//static struct adc_buf buf_accel_z; + + +/* carte sur le coté Y<->Z */ + +#define GYRO_X_NEUTRAL 425 +#define GYRO_X_GAIN 0.0170 + +#define GYRO_Y_NEUTRAL 463 +#define GYRO_Y_GAIN -0.0179 + +#define GYRO_Z_NEUTRAL 460 +#define GYRO_Z_GAIN 0.0180 + +#define ACCEL_X_MIN 272 +#define ACCEL_X_MAX 689 + +#define ACCEL_X_NEUTRAL ((ACCEL_X_MIN + ACCEL_X_MAX)/2) +#define ACCEL_X_GAIN (2 * 9.81 / (ACCEL_X_MAX - ACCEL_X_MIN)) + +#define ACCEL_Y_MIN 725 +#define ACCEL_Y_MAX 272 + +#define ACCEL_Y_NEUTRAL ((ACCEL_Y_MIN + ACCEL_Y_MAX)/2) +#define ACCEL_Y_GAIN (2 * 9.81 / (ACCEL_Y_MAX - ACCEL_Y_MIN)) + +#define ACCEL_Z_MIN 635 +#define ACCEL_Z_MAX 226 + +#define ACCEL_Z_NEUTRAL ((ACCEL_Z_MIN + ACCEL_Z_MAX)/2) +#define ACCEL_Z_GAIN (2 * 9.81 / (ACCEL_Z_MAX - ACCEL_Z_MIN)) + +/* carte debout +Gy : + - 0: 460 + - 16 T/mn: 460 + - 33 T/mn : 370 + - broken +Gz: + - 0: 463 + - 16: 363 + - 33: 270 + - gain 1.79e-2 + + +Gx: + - 0: 425 + - 16: 325 + - 33: 220 + - gain 1.69e-2 + */ + +void imu_init( void ) { + // adc_buf_channel(ADC_CHANNEL_GYRO_X, &buf_gyro_roll); + // adc_buf_channel(ADC_CHANNEL_ACCEL_Y, &buf_accel_y); + // adc_buf_channel(ADC_CHANNEL_ACCEL_Z, &buf_accel_z); +} + +void imu_update( void ) { + imu_sample.gyro_x = (float)((int16_t)adc_samples[ADC_CHANNEL_GYRO_X] - GYRO_X_NEUTRAL) * GYRO_X_GAIN; + imu_sample.gyro_y = (float)((int16_t)adc_samples[ADC_CHANNEL_GYRO_Y] - GYRO_Y_NEUTRAL) * GYRO_Y_GAIN; + imu_sample.gyro_z = (float)((int16_t)adc_samples[ADC_CHANNEL_GYRO_Z] - GYRO_Z_NEUTRAL) * GYRO_Z_GAIN; + imu_sample.accel_x = (float)((int16_t)adc_samples[ADC_CHANNEL_ACCEL_X] - ACCEL_X_NEUTRAL) * ACCEL_X_GAIN; + imu_sample.accel_y = (float)((int16_t)adc_samples[ADC_CHANNEL_ACCEL_Y] - ACCEL_Y_NEUTRAL) * ACCEL_Y_GAIN; + imu_sample.accel_z = (float)((int16_t)adc_samples[ADC_CHANNEL_ACCEL_Z] - ACCEL_Z_NEUTRAL) * ACCEL_Z_GAIN; +} diff --git a/sw/airborne/quadrirotor_autopilot/imu.h b/sw/airborne/quadrirotor_autopilot/imu.h new file mode 100644 index 00000000000..a5b9094ed68 --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/imu.h @@ -0,0 +1,19 @@ +#ifndef IMU_H +#define IMU_H + +struct ImuSample { + float gyro_x; + float gyro_y; + float gyro_z; + float accel_x; + float accel_y; + float accel_z; +}; + +extern struct ImuSample imu_sample; + +//void imu_init( void ); + +void imu_update( void ); + +#endif /* IMU_H */ diff --git a/sw/airborne/quadrirotor_autopilot/kalman.c b/sw/airborne/quadrirotor_autopilot/kalman.c new file mode 100644 index 00000000000..139a420e864 --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/kalman.c @@ -0,0 +1,111 @@ +#include + +#include "kalman.h" + + +struct KalmanPublic kalman_public; + +static const float dt = ( 1024.0 * 256.0 ) / 16000000.0; + +static float P_phi[2][2] = { + { 1, 0 }, + { 0, 1 }, +}; + +static const float R_angle_phi = 1.3 * 1.3; +static const float Q_angle_phi = 0.001; +static const float Q_gyro_phi = 0.0075; + +static float P_theta[2][2] = { + { 1, 0 }, + { 0, 1 }, +}; + +static const float R_angle_theta = 1.3 * 1.3; +static const float Q_angle_theta = 0.001; +static const float Q_gyro_theta = 0.0075; + +void kalman_state_update( float gyro_phi_measure, float gyro_theta_measure ) { + const float unbiased_gyro_phi = gyro_phi_measure - kalman_public.gyro_phi_bias; + const float P_phi_dot[2 * 2] = { + Q_angle_phi - P_phi[0][1] - P_phi[1][0],/* 0,0 */ + - P_phi[1][1], /* 0,1 */ + - P_phi[1][1], /* 1,0 */ + Q_gyro_phi /* 1,1 */ + }; + kalman_public.phi_dot = unbiased_gyro_phi; + kalman_public.phi += unbiased_gyro_phi * dt; + P_phi[0][0] += P_phi_dot[0] * dt; + P_phi[0][1] += P_phi_dot[1] * dt; + P_phi[1][0] += P_phi_dot[2] * dt; + P_phi[1][1] += P_phi_dot[3] * dt; + + const float unbiased_gyro_theta = gyro_theta_measure - kalman_public.gyro_theta_bias; + const float P_theta_dot[2 * 2] = { + Q_angle_theta - P_theta[0][1] - P_theta[1][0],/* 0,0 */ + - P_theta[1][1], /* 0,1 */ + - P_theta[1][1], /* 1,0 */ + Q_gyro_theta /* 1,1 */ + }; + kalman_public.theta_dot = unbiased_gyro_theta; + kalman_public.theta += unbiased_gyro_theta * dt; + P_theta[0][0] += P_theta_dot[0] * dt; + P_theta[0][1] += P_theta_dot[1] * dt; + P_theta[1][0] += P_theta_dot[2] * dt; + P_theta[1][1] += P_theta_dot[3] * dt; + +} + + +void kalman_kalman_update( float ax_measure, float ay_measure, float az_measure ) { + const float angle_phi_measure = atan2( ay_measure, az_measure ); + const float angle_phi_err = angle_phi_measure - kalman_public.phi; + const float C_0 = 1; + const float PCt_0 = C_0 * P_phi[0][0]; /* + C_1 * P[0][1] = 0 */ + const float PCt_1 = C_0 * P_phi[1][0]; /* + C_1 * P[1][1] = 0 */ + const float E = + R_angle_phi + + C_0 * PCt_0 + /* + C_1 * PCt_1 = 0 */ + ; + const float K_0 = PCt_0 / E; + const float K_1 = PCt_1 / E; + const float t_0 = PCt_0; /* C_0 * P[0][0] + C_1 * P[1][0] */ + const float t_1 = C_0 * P_phi[0][1]; /* + C_1 * P[1][1] = 0 */ + + P_phi[0][0] -= K_0 * t_0; + P_phi[0][1] -= K_0 * t_1; + P_phi[1][0] -= K_1 * t_0; + P_phi[1][1] -= K_1 * t_1; + + kalman_public.phi += K_0 * angle_phi_err; + kalman_public.gyro_phi_bias += K_1 * angle_phi_err; + + + const float angle_theta_measure = atan2( -ax_measure, az_measure ); + const float angle_theta_err = angle_theta_measure - kalman_public.theta; + + const float t_C_0 = 1; + const float t_PCt_0 = t_C_0 * P_theta[0][0]; + const float t_PCt_1 = t_C_0 * P_theta[1][0]; + const float t_E = + R_angle_theta + + t_C_0 * t_PCt_0 + /* + C_1 * PCt_1 = 0 */ + ; + ; + const float t_K_0 = t_PCt_0 / t_E; + const float t_K_1 = t_PCt_1 / t_E; + const float t_t_0 = t_PCt_0; /* C_0 * P[0][0] + C_1 * P[1][0] */ + const float t_t_1 = t_C_0 * P_theta[0][1]; /* + C_1 * P[1][1] = 0 */ + + P_theta[0][0] -= t_K_0 * t_t_0; + P_theta[0][1] -= t_K_0 * t_t_1; + P_theta[1][0] -= t_K_1 * t_t_0; + P_theta[1][1] -= t_K_1 * t_t_1; + + kalman_public.theta += t_K_0 * angle_theta_err; + kalman_public.gyro_theta_bias += t_K_1 * angle_theta_err; + + +} diff --git a/sw/airborne/quadrirotor_autopilot/kalman.h b/sw/airborne/quadrirotor_autopilot/kalman.h new file mode 100644 index 00000000000..7f5c1267398 --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/kalman.h @@ -0,0 +1,19 @@ +#ifndef KALMAN_H +#define KALMAN_H + +void kalman_init( void ); +void kalman_state_update( float gyro_phi_measure, float gyro_theta_measure ); +void kalman_kalman_update( float ax_measure, float ay_measure, float az_measure ); + +struct KalmanPublic { + float phi; + float phi_dot; + float gyro_phi_bias; + float theta; + float theta_dot; + float gyro_theta_bias; +}; + +extern struct KalmanPublic kalman_public; + +#endif /* KALMAN_H */ diff --git a/sw/airborne/quadrirotor_autopilot/kalman_phi.c b/sw/airborne/quadrirotor_autopilot/kalman_phi.c new file mode 100644 index 00000000000..5ab88ed9197 --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/kalman_phi.c @@ -0,0 +1,62 @@ +#include + +#include "kalman.h" + + +struct KalmanPublic kalman_public; + +static const float dt = ( 1024.0 * 256.0 ) / 16000000.0; + +static float P[2][2] = { + { 1, 0 }, + { 0, 1 }, +}; + +static const float R_angle_phi = 0.3; + +static const float Q_angle_phi = 0.001; +static const float Q_gyro_phi = 0.003; + + +void kalman_state_update( float gyro_phi_measure ) { + const float unbiased_gyro_phi = gyro_phi_measure - kalman_public.gyro_phi_bias; + const float Pdot[2 * 2] = { + Q_angle_phi - P[0][1] - P[1][0],/* 0,0 */ + - P[1][1], /* 0,1 */ + - P[1][1], /* 1,0 */ + Q_gyro_phi /* 1,1 */ + }; + kalman_public.phi_dot = unbiased_gyro_phi; + kalman_public.phi += unbiased_gyro_phi * dt; + P[0][0] += Pdot[0] * dt; + P[0][1] += Pdot[1] * dt; + P[1][0] += Pdot[2] * dt; + P[1][1] += Pdot[3] * dt; + +} + + +void kalman_kalman_update( float ax_measure, float ay_measure ) { + const float angle_phi_measure = atan2( ax_measure, ay_measure ); + const float angle_phi_err = angle_phi_measure - kalman_public.phi; + const float C_0 = 1; + const float PCt_0 = C_0 * P[0][0]; /* + C_1 * P[0][1] = 0 */ + const float PCt_1 = C_0 * P[1][0]; /* + C_1 * P[1][1] = 0 */ + const float E = + R_angle_phi + + C_0 * PCt_0 + /* + C_1 * PCt_1 = 0 */ + ; + const float K_0 = PCt_0 / E; + const float K_1 = PCt_1 / E; + const float t_0 = PCt_0; /* C_0 * P[0][0] + C_1 * P[1][0] */ + const float t_1 = C_0 * P[0][1]; /* + C_1 * P[1][1] = 0 */ + + P[0][0] -= K_0 * t_0; + P[0][1] -= K_0 * t_1; + P[1][0] -= K_1 * t_0; + P[1][1] -= K_1 * t_1; + + kalman_public.phi += K_0 * angle_phi_err; + kalman_public.gyro_phi_bias += K_1 * angle_phi_err; +} diff --git a/sw/airborne/quadrirotor_autopilot/main.c b/sw/airborne/quadrirotor_autopilot/main.c new file mode 100644 index 00000000000..e558b103dbe --- /dev/null +++ b/sw/airborne/quadrirotor_autopilot/main.c @@ -0,0 +1,172 @@ +#include +#include +#include + +#include "messages.h" +#include "downlink.h" +#include "airframe.h" +#include "timer.h" +#include "adc.h" +#include "uart.h" +#include "link_fbw.h" +#include "spi.h" +#include "autopilot.h" + +#include "imu.h" +#include "kalman.h" +#include "control.h" + + + +#define TRANS_RAW_IMU() { \ + static uint8_t foo; foo++; \ + if (!(foo%10)) { \ + DOWNLINK_SEND_RAW_IMU( &adc_samples[ADC_CHANNEL_GYRO_X], &adc_samples[ADC_CHANNEL_GYRO_Y], \ + &adc_samples[ADC_CHANNEL_GYRO_Z], &adc_samples[ADC_CHANNEL_ACCEL_X],\ + &adc_samples[ADC_CHANNEL_ACCEL_Y], &adc_samples[ADC_CHANNEL_ACCEL_Z]); \ + } \ + }\ + + +#define TRANS_IMU() { \ + static uint8_t foo; foo++; \ + if (!(foo%10)) { \ + DOWNLINK_SEND_IMU( &imu_sample.gyro_x, &imu_sample.gyro_y, &imu_sample.gyro_z, \ + &imu_sample.accel_x, &imu_sample.accel_y, &imu_sample.accel_z); \ + } \ + } \ + +#define TRANS_KALMAN() { \ + static uint8_t foo; foo++; \ + if (!(foo%10)) { \ + DOWNLINK_SEND_KALMAN( &kalman_public.phi, &kalman_public.phi_dot, \ + &kalman_public.gyro_phi_bias, &kalman_public.theta, \ + &kalman_public.theta_dot, &kalman_public.gyro_theta_bias); \ + } \ + } \ + + +#define PERIODIC_SEND_BAT() DOWNLINK_SEND_BAT(&vsupply, &estimator_flight_time, &low_battery) +#define PERIODIC_SEND_ATTITUDE() DOWNLINK_SEND_ATTITUDE( &(kalman_public.phi), &psi, &(kalman_public.theta)) +#define PERIODIC_SEND_ADC() {} +#define PERIODIC_SEND_SETTINGS() {} +#define PERIODIC_SEND_DESIRED() {} +#define PERIODIC_SEND_CLIMB_PID() {} +#define PERIODIC_SEND_PPRZ_MODE() DOWNLINK_SEND_ATTITUDE( &(kalman_public.phi), &psi, &(kalman_public.theta)) +#define PERIODIC_SEND_DEBUG() {} +#define PERIODIC_SEND_NAVIGATION_REF() {} + + +uint8_t fatal_error_nb = 0; +uint16_t cputime = 0; +float psi = 0.; + +uint8_t mcu1_status; +uint8_t pprz_mode; +uint8_t vertical_mode; +uint8_t inflight_calib_mode; +uint8_t ir_estim_mode; + +uint8_t vsupply; + +uint8_t low_battery = FALSE; +uint16_t estimator_flight_time = 0; + +inline void copy_from_to_fbw ( void ) { + to_fbw.channels[RADIO_THROTTLE] = from_fbw.channels[RADIO_THROTTLE]; + // to_fbw.channels[RADIO_ROLL] = from_fbw.channels[RADIO_ROLL]; + // to_fbw.channels[RADIO_PITCH] = from_fbw.channels[RADIO_PITCH]; + to_fbw.channels[RADIO_YAW] = from_fbw.channels[RADIO_YAW]; + to_fbw.status = 0; +} + +inline uint8_t pprz_mode_update( void ) { + ModeUpdate(pprz_mode, PPRZ_MODE_OF_PULSE(from_fbw.channels[RADIO_MODE], from_fbw.status)); +} + +inline uint8_t mcu1_status_update( void ) { + uint8_t new_mode = from_fbw.status; + if (mcu1_status != new_mode) { + bool_t changed = ((mcu1_status&MASK_FBW_CHANGED) != (new_mode&MASK_FBW_CHANGED)); + mcu1_status = new_mode; + return changed; + } + return FALSE; +} + +inline void radio_control_task( void ) { + if (link_fbw_receive_valid) { + uint8_t mode_changed = FALSE; + copy_from_to_fbw(); + if (bit_is_set(from_fbw.status, AVERAGED_CHANNELS_SENT)) { + bool_t pprz_mode_changed = pprz_mode_update(); + mode_changed |= pprz_mode_changed; + } + mode_changed |= mcu1_status_update(); + if ( mode_changed ) + DOWNLINK_SEND_PPRZ_MODE(&pprz_mode, &vertical_mode, &inflight_calib_mode, &mcu1_status, &ir_estim_mode); + + if (pprz_mode == PPRZ_MODE_AUTO1) { + control_desired_phi_dot = FLOAT_OF_PPRZ(from_fbw.channels[RADIO_ROLL], 0., -1.25); + control_desired_theta_dot = FLOAT_OF_PPRZ(from_fbw.channels[RADIO_PITCH], 0., 1.25); + } + else if (pprz_mode == PPRZ_MODE_AUTO2) { + control_desired_phi = FLOAT_OF_PPRZ(from_fbw.channels[RADIO_ROLL], 0., -0.6); + control_desired_theta = FLOAT_OF_PPRZ(from_fbw.channels[RADIO_PITCH], 0., 0.6); + } + vsupply = from_fbw.vsupply; + } +} + +inline void periodic_task ( void ) { + static uint8_t _20Hz = 0; + + _20Hz++; + if (_20Hz>=3) _20Hz=0; + + imu_update( ); + TRANS_RAW_IMU(); + TRANS_IMU(); + kalman_state_update(imu_sample.gyro_x, imu_sample.gyro_y); + kalman_kalman_update(imu_sample.accel_x, imu_sample.accel_y, imu_sample.accel_z); + TRANS_KALMAN(); + if (pprz_mode == PPRZ_MODE_AUTO2) { + // control_run_attitude_loop(); + control_run_raw_rotational_speed_loop(); + to_fbw.channels[RADIO_ROLL] = control_command_roll; + to_fbw.channels[RADIO_PITCH] = control_command_pitch; + } + if (pprz_mode == PPRZ_MODE_AUTO1 || pprz_mode == PPRZ_MODE_AUTO2) { + control_run_rotational_speed_loop(); + to_fbw.channels[RADIO_ROLL] = control_command_roll; + to_fbw.channels[RADIO_PITCH] = control_command_pitch; + } + link_fbw_send(); + if (!_20Hz) + PeriodicSend(); +} + +int main(void) { + + timer_init(); + modem_init(); + adc_init(); + spi_init(); + link_fbw_init(); + // uart0_init(); + // imu_init(); + sei(); + + while (1) { + if (timer_periodic()) + periodic_task(); + if (link_fbw_receive_complete) { + radio_control_task(); + link_fbw_receive_complete = FALSE; + } + } + return 0; +} + + + diff --git a/sw/configurator/Makefile b/sw/configurator/Makefile new file mode 100644 index 00000000000..3ae1af8a38b --- /dev/null +++ b/sw/configurator/Makefile @@ -0,0 +1,65 @@ +# +# $Id$ +# Copyright (C) 2004 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +OCAMLC=ocamlc -g -I +labltk -I +lablgtk2 -I ../lib/ocaml + + +SRC = env.ml tty.ml varXml.ml console.ml tkXml.ml flasher.ml notebook.ml welcome.ml hardware.ml radio.ml servos.ml adc.ml infrared.ml attitude.ml autopilot.ml airframe.ml flightplan.ml upload.ml simulator.ml monitor.ml logalizer.ml main.ml +CMO = $(SRC:.ml=.cmo) + +all : configurator medit.out + +configurator : $(CMO) + $(OCAMLC) -custom -o $@ unix.cma str.cma xml-light.cma labltk.cma jpflib.cma lib.cma $^ + +medit.out : medit.cmo + $(OCAMLC) -o $@ str.cma unix.cma xml-light.cma glibivy-ocaml.cma -I +lablgtk2 lablgtk.cma lablgnomecanvas.cma gtkInit.cmo lib.cma $^ + +%.cmo : %.ml + $(OCAMLC) -c $< + +%.cmi : %.mli + $(OCAMLC) $< + +%.cmi : %.ml + $(OCAMLC) $< + + +%.i : %.ml + $(OCAMLC) -c -i $< + +clean : + rm -f *~ *.cm* *.out configurator medit .depend + + +# +# Dependencies +# + +.depend: + ocamldep *.ml* > .depend + +ifneq ($(MAKECMDGOALS),clean) +-include .depend +endif + +include ../../conf/Makefile.local diff --git a/sw/configurator/adc.ml b/sw/configurator/adc.ml new file mode 100644 index 00000000000..1bef10452bb --- /dev/null +++ b/sw/configurator/adc.ml @@ -0,0 +1,36 @@ +(* + * $Id$ + * + * Analog to Digital Converters configuration + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sheet = fun sheets xml -> + let airframe = Frame.create sheets in + Notebook.create_sheet sheets "Adc" airframe; + + let name_is_adc = fun attributes -> + try Textvariable.get (List.assoc "name" attributes) = "adc" with Not_found -> false in + + let adc_section = VarXml.child xml ~select:name_is_adc "section" in + + TkXml.create_section airframe adc_section diff --git a/sw/configurator/airframe.ml b/sw/configurator/airframe.ml new file mode 100644 index 00000000000..6f816927ad7 --- /dev/null +++ b/sw/configurator/airframe.ml @@ -0,0 +1,38 @@ +(* + * $Id$ + * + * Airframe parameters configuration + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sub_sheets = fun sf xml -> + Servos.create_sheet sf xml; + Adc.create_sheet sf xml; + Infrared.create_sheet sf xml; + Attitude.create_sheet sf xml; + Autopilot.create_sheet sf xml + +let create_sheet = fun sheets -> + let airframe = Frame.create sheets in + Notebook.create_sheet sheets "Airframe" airframe; + + TkXml.create airframe create_sub_sheets diff --git a/sw/configurator/attitude.ml b/sw/configurator/attitude.ml new file mode 100644 index 00000000000..642ba45a683 --- /dev/null +++ b/sw/configurator/attitude.ml @@ -0,0 +1,46 @@ +(* + * $Id$ + * + * Attitude control configuration + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sheet = fun sheets xml -> + let f = Frame.create sheets in + Notebook.create_sheet sheets "Attitude" f; + + + let label = fun l -> Widget.forget_type (Label.create ~text:l f) in + + let empty = Widget.dummy in + + let entry _ = Widget.forget_type (Label.create f) in + + let array = + [| [| entry (); label "Min"; label "Max"; label "Gain"|]; + [| label "Roll"; entry (); entry (); entry ()|]; + [| label "Pitch"; entry (); entry (); entry ()|]; + [| label "Throttle"; entry (); entry (); entry ()|] |] in + + Array.iteri + (fun i -> Array.iteri (fun j w -> Tk.grid ~row:i ~column:j [w])) + array diff --git a/sw/configurator/autopilot.ml b/sw/configurator/autopilot.ml new file mode 100644 index 00000000000..58714ad3843 --- /dev/null +++ b/sw/configurator/autopilot.ml @@ -0,0 +1,29 @@ +(* + * $Id$ + * + * Autopilot configuration + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sheet = fun sheets xml -> + let airframe = Frame.create sheets in + Notebook.create_sheet sheets "Autopilot" airframe; diff --git a/sw/configurator/console.ml b/sw/configurator/console.ml new file mode 100644 index 00000000000..71ce330ee6c --- /dev/null +++ b/sw/configurator/console.ml @@ -0,0 +1,76 @@ +(* + * $Id$ + * + * Console (text widget) handling + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let console = ref None + +let create = fun parent -> + let c = Text.create parent in + console := Some c; + Text.tag_configure ~tag:"red" ~foreground:`Red c; + Text.tag_configure ~tag:"blue" ~foreground:`Blue c; + c + +let write = fun ?(tags=[]) s -> + match !console with + None -> failwith "Console.write" + | Some c -> + Text.insert ~index:(`End,[]) ~tags ~text:s c; + Text.see c (`End,[]) + + +let buffer_len = 256 + + +let copy_from = + let buffer = String.create buffer_len in + fun c kill color -> + let n = Unix.read c buffer 0 buffer_len in + if n = 0 then begin + Fileevent.remove_fileinput c; + kill () + end else + write ~tags:[color] (String.sub buffer 0 n) + + +let exec = fun command -> + write command; write "\n"; + let (proc_out, _, proc_err) as triple = Unix.open_process_full command [||] in + let proc_out = Unix.descr_of_in_channel proc_out + and proc_err = Unix.descr_of_in_channel proc_err in + let kill = + let twice = ref false in + fun () -> + if !twice then begin + write + (match Unix.close_process_full triple with + Unix.WEXITED c -> Printf.sprintf "--------- terminated(%d)\n\n" c + | Unix.WSIGNALED c -> Printf.sprintf "--------- killed(%d)\n\n" c + | Unix.WSTOPPED c -> Printf.sprintf "--------- stopped(%d)\n\n" c) + end else + twice := true + in + Fileevent.add_fileinput proc_out (fun () -> copy_from proc_out kill "blue"); + Fileevent.add_fileinput proc_err (fun () -> copy_from proc_err kill "red") diff --git a/sw/configurator/console.mli b/sw/configurator/console.mli new file mode 100644 index 00000000000..c3b5f6f1f91 --- /dev/null +++ b/sw/configurator/console.mli @@ -0,0 +1,34 @@ +(* + * $Id$ + * + * Console (text widget) handling + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +val create : 'a Widget.widget -> Widget.text Widget.widget +(** [create widget] creates the (text) console widget *) + +val write : ?tags:Tk.textTag list -> string -> unit +(** Writes to the text console. Usefull tags are "red" and "blue" *) + +val exec : string -> unit +(** Executes a command while logging stdout and stderr in the console *) diff --git a/sw/configurator/env.ml b/sw/configurator/env.ml new file mode 100644 index 00000000000..944e39d9fdf --- /dev/null +++ b/sw/configurator/env.ml @@ -0,0 +1,57 @@ +(* + * $Id$ + * + * Global and default settings + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let pprz_dir = + try + Sys.getenv "PAPARAZZI_DIR" + with + Not_found -> + Filename.concat (Filename.dirname Sys.argv.(0)) "../.." + +let abs = fun x -> pprz_dir ^ "/" ^ x + +let configurator_dir = abs "sw/configurator" + +let fbw_dir = abs "sw/airborne/fly_by_wire" +let ap_dir = abs "sw/airborne/autopilot" +let modem_dir = abs "sw/ground_segment/modem" + +let fbw_tty = "/dev/ttyS1" +let ap_tty = "/dev/ttyS0" +let modem_tty = "/dev/ttyUSB0" + +let tty_rate = Serial.B38400 + + +(* Initialization very early for creation of Textvariables *) +let _ = Tk.openTk () + +let select_one_file = fun ?(filter="*.xml") use -> + let action = function + [] -> () + | [f] -> use f + | _ -> failwith "Env.select_one_file: unepected several files" in + Fileselect.f ~title:"File Selection" ~action ~filter ~file:"" ~multi:false ~sync:false diff --git a/sw/configurator/env.mli b/sw/configurator/env.mli new file mode 100644 index 00000000000..cf56bedcfd2 --- /dev/null +++ b/sw/configurator/env.mli @@ -0,0 +1,41 @@ +(* + * $Id$ + * + * Global and default settings + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +val pprz_dir : string + +val configurator_dir : string +val fbw_dir : string +val ap_dir : string +val modem_dir : string + +val fbw_tty : string +val ap_tty : string +val modem_tty : string + +val tty_rate : Serial.speed + +val select_one_file : ?filter:string -> (string -> unit) -> unit +(** File selector. Default [filter] is "*.xml" *) diff --git a/sw/configurator/flasher.ml b/sw/configurator/flasher.ml new file mode 100644 index 00000000000..a68b66b98e7 --- /dev/null +++ b/sw/configurator/flasher.ml @@ -0,0 +1,74 @@ +(* + * $Id$ + * + * Micro-controllers uploading + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +type mcu = Fbw | Ap | Modem + +let string_of = function + Fbw -> "Fly by wire" + | Ap -> "Autopilot" + | Modem -> "Modem" + + +let switch = fun mcu -> + Dialog.create Widget.default_toplevel "Paparazzi upload" (Printf.sprintf "Please connect programmer board to %s\n" (string_of mcu)) ["OK"; "Cancel"] () + +let warn = + let dont_bother_me = Textvariable.create () in + fun continue -> + if Textvariable.get dont_bother_me = "1" then + continue () + else + let t = Toplevel.create Widget.default_toplevel in + Wm.title_set t "Paparazzi warning"; + + Grab.set t; + + let destroy = fun () -> Tk.destroy t; continue () in + + let erase_AP = fun () -> + if switch Ap = 0 then + let command = Printf.sprintf "cd %s; make erase" Env.ap_dir in + Console.exec command; + destroy () in + + let l = Label.create ~text:"Warning: You are about to program the fbw microcontroller. It is possible only if the AP microcontroller is erased." t + and b = Button.create ~text:"Erase AP first" ~command:erase_AP t + and b' = Button.create ~text:"Ok (AP erased)" ~command:destroy t + and b'' = Button.create ~text:"Cancel" ~command:(fun () -> Tk.destroy t) t + and x = Checkbutton.create ~text:"Stop warning me about that" ~variable:dont_bother_me t in + + Tk.pack [l]; + Tk.pack [b;b';b''] ~side:`Left; + Tk.pack [x] + +let make = fun mcu path target -> + let do_it = fun () -> + if switch mcu = 0 then + let command = Printf.sprintf "cd %s; make %s" path target in + Console.exec command in + + if mcu = Fbw && target <> "erase" then warn do_it else do_it () + diff --git a/sw/configurator/flasher.mli b/sw/configurator/flasher.mli new file mode 100644 index 00000000000..fc7cfcccbdf --- /dev/null +++ b/sw/configurator/flasher.mli @@ -0,0 +1,30 @@ +(* + * $Id$ + * + * Micro-controllers uploading + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +type mcu = Fbw | Ap | Modem +val make : mcu -> string -> string -> unit +(** [make mcu path target] Calls the "make" command in the given [path] with +the given [target]. [mcu] helps to control the hardware connectivity. *) diff --git a/sw/configurator/flightplan.ml b/sw/configurator/flightplan.ml new file mode 100644 index 00000000000..bb08e8db48e --- /dev/null +++ b/sw/configurator/flightplan.ml @@ -0,0 +1,78 @@ +(* + * $Id$ + * + * Flight plans edition + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Latlong + +let calibrate_xy = fun x0 y0 -> + let scale = 2.5 in + + let print x y = + let xl = x0 + truncate (float x *. scale) + and yl = y0 + truncate (float y *. scale) in + let wgs84 = Latlong.wgs84_of_lambertIIe xl yl in + Console.write (Printf.sprintf "%d %d %f %f\n" x y ((Rad>>Deg)wgs84.Latlong.posn_lat) ((Rad>>Deg)wgs84.Latlong.posn_long)) in + + Console.write (Printf.sprintf "Calibration for x0=%d y0=%d:\n--8<----------------------\n" x0 y0); + + print 0 0; + print 1000 0; + print 0 1000; + Console.write (Printf.sprintf "--8<-----------------------\n") + +let calibrate_ign_tile = fun filename -> + Scanf.sscanf (Filename.basename filename) "F%3d_%3d" (fun x y -> + let x0 = x * 10000 + and y0 = (267 - y) * 10000 in + + calibrate_xy x0 y0) + +let int_of_tv = fun tv -> int_of_string (Textvariable.get tv) + + +let create_sheet = fun sheets -> + let f = Frame.create sheets in + Notebook.create_sheet sheets "Flight Plan" f; + + let b = Button.create ~text:"Calibrate IGN tile" ~command:(fun () -> Env.select_one_file ~filter:"*.png" calibrate_ign_tile) f in + + Tk.pack [b]; + + let tvx = Textvariable.create () + and tvy = Textvariable.create () in + + let lx = Label.create ~text:"LambertIIe: x:" f + and ex = Entry.create ~textvariable:tvx f + and ly = Label.create ~text:"y:" f + and ey = Entry.create ~textvariable:tvy f + and c = Button.create ~text:"Calibrate" ~command:(fun () -> calibrate_xy (int_of_tv tvx) (int_of_tv tvy)) f in + + Tk.pack ~side:`Left [lx]; + Tk.pack ~side:`Left [ex]; + Tk.pack ~side:`Left [ly]; + Tk.pack ~side:`Left [ey]; + Tk.pack ~side:`Left [c] + + diff --git a/sw/configurator/hardware.ml b/sw/configurator/hardware.ml new file mode 100644 index 00000000000..9adf8c7e675 --- /dev/null +++ b/sw/configurator/hardware.ml @@ -0,0 +1,260 @@ +(* + * $Id$ + * + * Microcotrollers connection checking + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +module FbwMcu = struct + let name = "Fly by wire" + let sort = Flasher.Fbw + let path = Env.fbw_dir + let default_tty = Env.fbw_tty +end + +module ApMcu = struct + let name = "Autopilot" + let sort = Flasher.Ap + let path = Env.ap_dir + let default_tty = Env.ap_tty +end + +module ModemMcu = struct + let name = "Ground Modem" + let sort = Flasher.Modem + let path = Env.modem_dir + let default_tty = Env.modem_tty +end + +module type MCU = sig + val name : string + val sort : Flasher.mcu + val path : string + val default_tty : string +end + +let nb_adc = 8 + +let get_2bytes = fun buf i -> + (Char.code buf.[i] lsl 8) lor (Char.code buf.[i+1]) + +let (+.=) r v = r := !r +. v + +let lls = fun values l ()-> + let sum_x = ref 0. + and sum_y = ref 0. + and sum_xy = ref 0. + and sum_x2 = ref 0. + and n = ref 0 in + Hashtbl.iter + (fun x y -> + let x = float_of_string x + and y = float_of_string y in + incr n; + sum_x +.= x; + sum_x2 +.= x*.x; + sum_y +.= y; + sum_xy +.= x*.y) + values; + let n = float !n in + let mx = !sum_x /. n + and my = !sum_y /. n in + let c_xy = mx *. my +. (!sum_xy -. mx *. !sum_y -. my *. !sum_x) /. n + and s2_x = mx *. mx +. (!sum_x2 -. 2.*. mx *. !sum_x) /. n in + let a = c_xy /. s2_x in + let b = my -. a *. mx in + + Label.configure ~text:(Printf.sprintf "a=%f b=%f" a b) l + +let rec check_adcs = fun button w tty make () -> + let text = Tk.cget button `Text in + make "TARGET=tx_adcs load" (); + + let f = Frame.create w in + let destroy = fun () -> + Tk.destroy f; + Button.configure ~text button; + Button.configure ~command:(check_adcs button w tty make) button in + + Button.configure ~text:"Close" button; + Button.configure ~command:destroy button; + + let create_channel = fun i -> + let fc = Frame.create f in + + let value = Textvariable.create () in + + let l = Label.create ~text:(string_of_int i) fc + and v = Label.create ~textvariable:value ~width:4 fc in + + let e = Entry.create ~width:4fc in + let lb = Menubutton.create ~text:"Registered" fc in + let lb_menu = Menu.create lb in + Menubutton.configure lb ~menu:lb_menu; + + let values = Hashtbl.create 97 in (* I would prefer to find the values in the Menu but I do not know how to do it ! *) + let register_values = fun ev -> + let adc_value = Textvariable.get value + and entry_value = Entry.get e in + Hashtbl.add values adc_value entry_value; + Menu.add_command lb_menu + ~label:(adc_value^":"^entry_value) + ~command:(fun () -> Hashtbl.remove values adc_value; Menu.delete ~first:`Active ~last:`Active lb_menu) + in + Tk.bind ~events:[`KeyPressDetail "Return"] ~action:register_values e; + + let lb' = Menubutton.create ~text:"Fit" fc in + let lb_menu' = Menu.create lb' in + Menubutton.configure lb' ~menu:lb_menu'; + let result = Label.create ~width:20 fc in + Menu.add_command lb_menu' ~label:"Linear" ~command:(lls values result); + + + + Tk.pack [l; v] ~side:`Left; + Tk.pack [e] ~side:`Left; + Tk.pack [lb;lb'] ~side:`Left; + Tk.pack [result] ~side:`Left; + (fc, value) + in + + let channels = Array.init nb_adc create_channel in + + (* Listen to tty input *) + Tty.connect tty; + Tty.add_formatted_input tty "\000\000" (2*(nb_adc+1)+1) + (fun input -> + for i = 0 to nb_adc - 1 do + let vi = get_2bytes input (2+2*i) in + Textvariable.set (snd channels.(i)) (string_of_int vi) + done + ); + Tk.bind ~events:[`Destroy] ~action:(fun _ -> Tty.deconnect tty) f; + + (***) + let x = Button.create ~text:"Random" + ~command:(fun () -> + for i = 0 to nb_adc - 1 do + Textvariable.set (snd channels.(i)) (string_of_int (Random.int 1024)) + done + ) f in + (***) + + + Tk.pack [x]; + Tk.pack (List.map fst (Array.to_list channels)) ~side:`Top; + + Tk.pack [f] + + +module Make(Mcu : MCU) = struct + let make = fun target () -> Flasher.make Mcu.sort Mcu.path target + let make_test = fun target () -> Flasher.make Mcu.sort (Mcu.path^"/test") target + + let tty = Textvariable.create () + + let connected = Textvariable.create () + + let get_tty = fun () -> Textvariable.get tty + + let uart = ref Unix.stdin + + let connect_tty = fun () -> + let tty = get_tty ()in + if Textvariable.get connected = "1" then + try + Tty.connect tty + with _ -> + Console.write ~tags:["red"] (Printf.sprintf "Cannot open '%s'\n" tty) + else + try + Tty.deconnect tty + with + Not_found -> + Console.write ~tags:["red"] (Printf.sprintf "Device '%s' not opened\n" tty) + + let log_tty = fun () -> Tty.add_ttyinput (get_tty ()) Console.write + + let check_adcs_b = ref (Button.create Widget.default_toplevel) + + let create = fun parent -> + let f = Frame.create ~borderwidth:4 ~relief:`Sunken parent in + let f' = Frame.create f in + + let l = Label.create ~text:Mcu.name f' + and b0 = Button.create ~text:"Erase" ~command:(make "erase") f' + and b1 = Button.create ~text:"Check link" ~command:(make "check_arch") f' + and b2 = Button.create ~text:"Write Fuses" ~command:(make "wr_fuses") f' + and b3 = Button.create ~text:"Check UART" ~command:(fun () -> make_test (Printf.sprintf "TARGET=check_uart TTY=\"%s\" load" (Textvariable.get tty)) ()) f' + and e = Entry.create ~textvariable:tty ~width:10 f' + and c = Checkbutton.create ~text:"Connect tty" ~variable:connected ~command:connect_tty f' + and c' = Checkbutton.create ~text:"Log tty" ~command:log_tty f' in + + Button.configure b3; + + Entry.insert e ~index:`End ~text:Mcu.default_tty; + + check_adcs_b := Button.create ~text:"Check ADCS" f; + Button.configure !check_adcs_b ~command:(fun () -> check_adcs !check_adcs_b f (get_tty ()) make_test ()); + + + Tk.pack [l]; + Tk.pack [b0;b1;b2] ~side:`Left; + Tk.pack [e] ~side:`Left; + Tk.pack [c;c'] ~side:`Left; + Tk.pack [b3] ~side:`Left; + Tk.pack [f']; + Tk.pack [!check_adcs_b] ~side:`Left; + f +end + +module Fbw = Make(FbwMcu) +module Ap = Make(ApMcu) +module Modem = struct + include Make(ModemMcu) + + let bat_a = Textvariable.create () + let bat_b = Textvariable.create () +end + +let create_sheet = fun sheets -> + let f = Frame.create sheets in + Notebook.create_sheet sheets "Hardware" f; + + let fbw = Fbw.create f + and ap = Ap.create f + and modem = Modem.create f in + + Button.configure ~state:`Disabled !Modem.check_adcs_b; + + let check_spi = Button.create ~text:"Check SPI" ~state:`Disabled ap in + + + let check_downlink = Button.create ~text:"Check Downlink" ~state:`Disabled modem in + + let label = Label.create ~text:"Check the connectivity of your micro-controllers" f in + + Tk.pack ~anchor:`W [check_spi] ~side:`Left; + Tk.pack ~anchor:`W [check_downlink]; + Tk.pack ~anchor:`W [label]; + Tk.pack ~anchor:`W [fbw; ap; modem] + diff --git a/sw/configurator/infrared.ml b/sw/configurator/infrared.ml new file mode 100644 index 00000000000..79d07ed9ddc --- /dev/null +++ b/sw/configurator/infrared.ml @@ -0,0 +1,3 @@ +let create_sheet = fun sheets xml -> + let airframe = Frame.create sheets in + Notebook.create_sheet sheets "Infrared" airframe diff --git a/sw/configurator/logalizer.ml b/sw/configurator/logalizer.ml new file mode 100644 index 00000000000..b9b60f5c27e --- /dev/null +++ b/sw/configurator/logalizer.ml @@ -0,0 +1,29 @@ +(* + * $Id$ + * + * Logs handling + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sheet = fun sheets -> + let airframe = Frame.create sheets in + Notebook.create_sheet sheets "Logalizer" airframe diff --git a/sw/configurator/main.ml b/sw/configurator/main.ml new file mode 100644 index 00000000000..f4060894baa --- /dev/null +++ b/sw/configurator/main.ml @@ -0,0 +1,56 @@ +(* + * $Id$ + * + * Configuration graphic interface + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let exit = fun () -> + if Dialog.create Widget.default_toplevel "Configurator quit" "Really quit ?\n" ["OK"; "Cancel"] () = 0 then + exit 0 + +let _ = + let top = Widget.default_toplevel in + Wm.title_set top "The Paparazzi Configurator"; + + let sheets = Frame.create top in + + Welcome.create_sheet sheets; + Hardware.create_sheet sheets; + Radio.create_sheet sheets; + Airframe.create_sheet sheets; + Flightplan.create_sheet sheets; + Upload.create_sheet sheets; + Simulator.create_sheet sheets; + Monitor.create_sheet sheets; + Logalizer.create_sheet sheets; + + let quit_button = Button.create ~relief:`Sunken ~text:"Quit" ~command:exit sheets + and (n, _) = Grid.size sheets in + Tk.grid ~column:n ~row:0 [quit_button]; + + let console = Console.create top in + + Tk.pack [sheets]; + Tk.pack [console]; + + Tk.mainLoop() diff --git a/sw/configurator/medit.ml b/sw/configurator/medit.ml new file mode 100644 index 00000000000..f0305607eab --- /dev/null +++ b/sw/configurator/medit.ml @@ -0,0 +1,537 @@ +(* ocamlc -I +lablgtk2 lablgtk.cma lablgnomecanvas.cma gtkInit.cmo medit.ml *) + +open Printf +open Latlong + +let sof = string_of_float +let fos = float_of_string +let sof1 = fun x -> Printf.sprintf "%.1f" x + +(* World (0., 0.) seems to be at the center of the canvas *) + +type meter = float +type en = { east : meter; north : meter } + +let flight_plan = ref (Xml.PCData "") + +module Ref = struct + let world_unit = 2.5 (* 1 pixel = 2.5m *) + let ref0 = ref {posn_lat = 43.237535; posn_long = 1.327747 } + let utm0 () = utm_of WGS84 !ref0 + + let set = fun lat lon -> + ref0 := { posn_lat = (Deg>>Rad) lat; posn_long = (Deg>>Rad) lon } + + + let world_of_en = fun en -> en.east /. world_unit, -. en.north /. world_unit + let en_of_world = fun wx wy -> { east = wx *. world_unit; north = -. wy *. world_unit } + let world_of_utm = fun utm -> + let utm0 = utm0 () in + let x = utm.utm_x -. utm0.utm_x + and y = -. (utm.utm_y -. utm0.utm_y) in + (x /. world_unit, y /. world_unit) + + + let geo_string = fun en -> + let u = utm_of WGS84 !ref0 in + let u' = {utm_x = u.utm_x +. en.east; + utm_y = u.utm_y +. en.north; + utm_zone = u.utm_zone } in + let w = of_utm WGS84 u' in + sprintf "%.4f %.4f" ((Rad>>Deg)w.posn_lat) ((Rad>>Deg)w.posn_long) + + let direction = fun w1 w2 -> + let u1 = utm_of WGS84 w1 + and u2 = utm_of WGS84 w2 in + assert (u1.utm_zone = u2.utm_zone); + (u2.utm_x -. u1.utm_x, u2.utm_y -. u1.utm_y) + + let world_of_wgs84 = fun wgs84 -> + let (dxm, dym) = direction !ref0 wgs84 in + (dxm/. world_unit, -. dym/.world_unit) +end + +let gensym = let n = ref 0 in fun prefix -> incr n; prefix ^ string_of_int !n + + +class type w = object + method alt : float + method name : string + method en : en + method zoom : float -> unit + method delete : unit +end + + +module Waypoints = struct + let waypoints = Hashtbl.create 13 + let add = fun w -> Hashtbl.add waypoints (w:>w) (w:>w) + let remove = fun x -> Hashtbl.remove waypoints x + let iter = fun f -> Hashtbl.iter f waypoints + let clear = fun () -> Hashtbl.iter (fun w _ -> w#delete) waypoints +end + +module RecentFiles = struct + let conf_file = Filename.concat (Sys.getenv "HOME") ".meditrc" + type f = Map of string | FP of string + let l = ref [] + let add = fun f -> + if not (List.mem f !l) then + l := f :: !l + let f_of_xml = fun x -> + let f = Xml.attrib x "file" in + match Xml.tag x with + "map" -> Map f + | "fp" -> FP f + | _ -> failwith "RecentFile.f_of_xml" + let xml_of_f = function + Map f -> Xml.Element ("map", ["file",f], []) + | FP f -> Xml.Element ("fp", ["file",f], []) + let load = fun () -> + let xml = try Xml.parse_file conf_file with _ -> Xml.Element ("",[],[]) in + l := List.map f_of_xml (Xml.children xml) + let save = fun () -> + let f = open_out conf_file in + let xml = Xml.Element ("files", [], List.map xml_of_f !l) in + output_string f (Xml.to_string_fmt xml); + close_out f + let iter = fun f -> List.iter f !l +end + +let s = 5. +let losange = [|s;0.; 0.;s; -.s;0.; 0.;-.s|] + +let georef = fun () -> + let dialog = GWindow.window ~border_width:10 ~title:"Geo ref" () in + let dvbx = GPack.box `VERTICAL ~packing:dialog#add () in + let lat = GEdit.entry ~text:"43.210" ~packing:dvbx#add () in + let lon = GEdit.entry ~text:"1.234" ~packing:dvbx#add () in + let cancel = GButton.button ~label:"Cancel" ~packing: dvbx#add () in + let ok = GButton.button ~label:"OK" ~packing: dvbx#add () in + ignore(cancel#connect#clicked ~callback:dialog#destroy); + ignore(ok#connect#clicked ~callback: + begin fun _ -> + let lat = float_of_string lat#text in + let lon = float_of_string lon#text in + Ref.set lat lon; + dialog#destroy () + end); + dialog#show () + +let current_zoom = ref 1. (* Would be better not to be global ??? *) + + +class waypoint = fun root (name :string) ?(alt=0.) en -> + let xw, yw = Ref.world_of_en en in + object (self) + val mutable x0 = 0. + val mutable y0 = 0. + val item = + GnoCanvas.polygon root ~points:losange + ~props:[`FILL_COLOR "red" ; `OUTLINE_COLOR "midnightblue" ; `WIDTH_UNITS 1.; `FILL_STIPPLE (Gdk.Bitmap.create_from_data ~width:2 ~height:2 "\002\001")] + + val label = GnoCanvas.text root ~props:[`TEXT name; `X s; `Y 0.; `ANCHOR `SW] + val mutable name = name + val mutable alt = alt + initializer self#move xw yw + method name = name + method set_name n = + if n <> name then + name <- n + method alt = alt + method label = label + method xy = let a = item#i2w_affine in (a.(4), a.(5)) (*** item#i2w 0. 0. causes Seg Fault !***) + method move dx dy = item#move dx dy; label#move dx dy + method edit = + let dialog = GWindow.window ~border_width:10 ~title:"Waypoint Edit" () in + let dvbx = GPack.box `VERTICAL ~packing:dialog#add () in + let en = self#en in + let ename = GEdit.entry ~text:name ~packing:dvbx#add () in + let ex = GEdit.entry ~text:(string_of_float en.east) ~packing:dvbx#add () in + let ey = GEdit.entry ~text:(string_of_float en.north) ~packing:dvbx#add () in + let ea = GEdit.entry ~text:(string_of_float alt) ~packing:dvbx#add () in + let cancel = GButton.button ~label:"Cancel" ~packing: dvbx#add () in + let ok = GButton.button ~label:"OK" ~packing: dvbx#add () in + ignore(cancel#connect#clicked ~callback:dialog#destroy); + ignore(ok#connect#clicked ~callback: + begin fun _ -> + self#set_name ename#text; + alt <- float_of_string ea#text; + label#set [`TEXT name]; + self#set {east = float_of_string ex#text; + north = float_of_string ey#text}; + dialog#destroy () + end); + dialog#show () + + + + method event (ev : GnoCanvas.item_event) = + begin + match ev with + | `BUTTON_PRESS ev -> + begin + match GdkEvent.Button.button ev with + | 1 -> self#edit + | 3 -> self#delete + | 2 -> + let x = GdkEvent.Button.x ev + and y = GdkEvent.Button.y ev in + x0 <- x; y0 <- y; + let curs = Gdk.Cursor.create `FLEUR in + item#grab [`POINTER_MOTION; `BUTTON_RELEASE] curs + (GdkEvent.Button.time ev) + | x -> printf "%d\n" x; flush stdout; + end + | `MOTION_NOTIFY ev -> + let state = GdkEvent.Motion.state ev in + if Gdk.Convert.test_modifier `BUTTON2 state then begin + let x = GdkEvent.Motion.x ev + and y = GdkEvent.Motion.y ev in + let dx = !current_zoom *. (x-. x0) + and dy = !current_zoom *. (y -. y0) in + self#move dx dy ; + x0 <- x; y0 <- y + end + | `BUTTON_RELEASE ev -> + if GdkEvent.Button.button ev = 2 then + item#ungrab (GdkEvent.Button.time ev) + | _ -> () + end; + true + initializer ignore(item#connect#event self#event) + method item = item + method en = + let (dx, dy) = self#xy in + Ref.en_of_world dx dy + method set en = + let (xw, yw) = Ref.world_of_en en + and (xw0, yw0) = self#xy in + self#move (xw-.xw0) (yw-.yw0) + method delete = + item#destroy (); + label#destroy (); + Waypoints.remove (self:>w) + method zoom (z:float) = + let a = item#i2w_affine in + a.(0) <- 1./.z; a.(3) <- 1./.z; + item#affine_absolute a; + label#affine_absolute a + end + +let xml_of_wp = fun utm0 w -> + let wgs84 = of_utm WGS84 { utm_x = utm0.utm_x +. w#en.east; utm_y = utm0.utm_y +. w#en.north ; utm_zone = utm0.utm_zone } in + let alt = if w#alt = 0. then [] else ["alt", string_of_float w#alt] in + Xml.Element ("waypoint", ["name",w#name; + "x",sof1 w#en.east; + "y", sof1 w#en.north; + "lat",string_of_float ((Rad>>Deg) wgs84.posn_lat); + "lon",string_of_float ((Rad>>Deg) wgs84.posn_long)]@alt + ,[]) + +let subst_waypoints = fun xml wpts -> + match xml with + Xml.Element (tag, attrs, children) -> + Xml.Element (tag, attrs, List.map (fun c -> if Xml.tag c = "waypoints" then wpts else c) children) + | _ -> failwith "subst_waypoints" + + + +let file_dialog ~title ~callback () = + let sel = GWindow.file_selection ~title ~filename:"*.xml" ~modal:true () in + ignore (sel#cancel_button#connect#clicked ~callback:sel#destroy); + ignore + (sel#ok_button#connect#clicked + ~callback:(fun () -> + let name = sel#filename in + sel#destroy (); + callback name)); + sel#show () + + +let write_mission = fun () -> + let l = ref [] in + Waypoints.iter (fun _ w -> l := w :: !l); + let utm0 = (Ref.utm0 ()) in + let children = List.map (xml_of_wp utm0) !l in + let waypoints = Xml.Element ("waypoints", ["utm_x0", sof1 utm0.utm_x;"utm_y0", sof1 utm0.utm_y], children) in + let fp = subst_waypoints !flight_plan waypoints in + + ignore (file_dialog ~title:"Save Flight Plan" ~callback:(fun name -> let f = open_out name in + fprintf f "%s\n" (Xml.to_string_fmt fp); + fprintf f "\n"; + close_out f ) ()) + +let float_attrib = fun x a -> float_of_string (Xml.attrib x a) +let int_attrib = fun x a -> truncate (float_attrib x a) + +let load_mission = fun root file -> + let xml = Xml.parse_file file in + + let default_alt = float_attrib xml "alt" in + + Ref.set (float_attrib xml "lat0") (float_attrib xml "lon0"); + let utm0 = Ref.utm0 () in + let wps = ExtXml.child xml "waypoints" in + + let wp_of_xml = fun xml -> + let a = try float_attrib xml "alt" with _ -> default_alt in + let en = + try + let lat = float_attrib xml "lat" + and lon = float_attrib xml "lon" in + let utm = utm_of WGS84 {posn_lat=(Deg>>Rad)lat; posn_long=(Deg>>Rad)lon} in + {east = (utm.utm_x -. utm0.utm_x); north = (utm.utm_y -. utm0.utm_y) } + with + Xml.No_attribute _ -> + { east= float_attrib xml "x"; north= float_attrib xml "y" } in + let w = new waypoint root (Xml.attrib xml "name") ~alt:a en in + w#zoom !current_zoom; + w in + + List.iter (fun w -> Waypoints.add (w:>w)) (List.map wp_of_xml (Xml.children wps)); + + flight_plan := xml + + +let open_mission = fun cont root ?file () -> + match file with + None -> + ignore (file_dialog ~title:"Open Flight Plan" ~callback:(fun name -> load_mission root name; cont (RecentFiles.FP name)) ()) + | Some name -> load_mission root name; cont (RecentFiles.FP name) + + +let display_map = fun ?(scale = 1.) x y wgs84 map_name root -> + let image = GdkPixbuf.from_file map_name in + let p = GnoCanvas.pixbuf ~pixbuf:image ~props:[`ANCHOR `NW] root in + p#lower_to_bottom (); + let wx, wy = Ref.world_of_wgs84 wgs84 in + p#move (wx -. x*.scale) (wy -. y*.scale); + let a = p#i2w_affine in + a.(0) <- scale; a.(3) <- scale; + p#affine_absolute a; + p + +let load_map = fun root (maps_menu_fact:GMenu.menu GMenu.factory) ref0 filename -> + let register = fun pixbuf -> + let mi = maps_menu_fact#add_item filename in + ignore (mi#connect#activate (fun () -> pixbuf#destroy (); maps_menu_fact#menu#remove mi)) in + if Filename.check_suffix filename ".xml" then + let xml = Xml.parse_file filename in + let map_name = Filename.concat (Filename.dirname filename) (Xml.attrib xml "file") in + + match Xml.attrib xml "projection" with + "UTM" -> + let utm_zone = try int_of_string (Xml.attrib xml "utm_zone") with _ -> fprintf stderr "Warning: utm_zone attribute not specified in '%s'; default is 31\n" filename; flush stderr; 31 in + begin + match Xml.children xml with + p::_ -> + let utm_x = float_attrib p "utm_x" + and utm_y = float_attrib p "utm_y" + and x = float_attrib p "x" + and y = float_attrib p "y" + and scale = float_attrib xml "scale" /. Ref.world_unit in + let wgs84 = of_utm WGS84 {utm_x = utm_x; utm_y = utm_y; utm_zone = utm_zone} in + register (display_map ~scale x y wgs84 map_name root) + | _ -> failwith "Exactly one ref point please" + end + | _ -> failwith "Unknwown projection" + else + try + Scanf.sscanf (Filename.basename filename) "F%3d_%3d" (fun x y -> + let lbt0 = lambert_of lambertIIe ((NTF< failwith "XML or IGN tile expected" + + + + +let open_map = fun cont root maps_menu ?file () -> + match file with + None -> + file_dialog ~title:"Open Map" ~callback:(fun name -> ignore (load_map root maps_menu !Ref.ref0 name); cont (RecentFiles.Map name)) () + | Some name -> + ignore (load_map root maps_menu !Ref.ref0 name); + cont (RecentFiles.Map name) + + + +let create_wp = fun canvas xw yw () -> + let en = Ref.en_of_world xw yw in + let name = gensym "wp" in + let x = new waypoint canvas#root name en in + x#zoom !current_zoom; + Waypoints.add x + +let dragging = ref None + + +let canvas_button_release = fun (canvas:GnoCanvas.canvas) ev -> + let state = GdkEvent.Button.state ev in + if GdkEvent.Button.button ev = 2 then begin + dragging := None; + true + end else + false + +let canvas_button_press = fun (canvas:GnoCanvas.canvas) ev -> + let state = GdkEvent.Button.state ev in + if GdkEvent.Button.button ev = 1 && Gdk.Convert.test_modifier `CONTROL state then + let xc = GdkEvent.Button.x ev in + let yc = GdkEvent.Button.y ev in + let (xw, yw) = canvas#window_to_world xc yc in + (* Effective creation delayed to avoid a bug in lablgtk *) + ignore (GMain.Timeout.add 10 (fun () -> create_wp canvas xw yw (); false)); + true + else if GdkEvent.Button.button ev = 2 then + let xc = GdkEvent.Button.x ev in + let yc = GdkEvent.Button.y ev in + dragging := Some (xc, yc); + true + else begin + false + end + +let canvas_key_press = fun (canvas:GnoCanvas.canvas) ev -> + let (x, y) = canvas#get_scroll_offsets in + match GdkEvent.Key.keyval ev with + | k when k = GdkKeysyms._Up -> canvas#scroll_to x (y-20) ; true + | k when k = GdkKeysyms._Down -> canvas#scroll_to x (y+20) ; true + | k when k = GdkKeysyms._Left -> canvas#scroll_to (x-10) y ; true + | k when k = GdkKeysyms._Right -> canvas#scroll_to (x+10) y ; true + | _ -> false + +let display_coord = fun (canvas:GnoCanvas.canvas) lbl_xy lbl_geo ev -> + let xc = GdkEvent.Motion.x ev + and yc = GdkEvent.Motion.y ev in + let (xw, yw) = canvas#window_to_world xc yc in + let en = Ref.en_of_world xw yw in + let d = sqrt (en.east*.en.east +. en.north*.en.north) in + lbl_xy#set_text (sprintf "%.0fm %.0fm (d=%.0fm)\t" en.east en.north d); + lbl_geo#set_text (Ref.geo_string en); + begin + match !dragging with + Some (x0, y0 ) -> + let xc = GdkEvent.Motion.x ev in + let yc = GdkEvent.Motion.y ev in + let (x, y) = canvas#get_scroll_offsets in + canvas#scroll_to (x+truncate (x0-.xc)) (y+truncate (y0-.yc)) + | None -> () + end; + false + + +let main () = + let window = GWindow.dialog ~title: "Paparazzi" + ~border_width: 1 ~width:800 () in + let quit = fun () -> + RecentFiles.save (); + GMain.Main.quit (); exit 0 in + ignore (window#connect#destroy ~callback:quit); + + let menubar = GMenu.menu_bar ~packing:window#vbox#pack () in + + let adj = GData.adjustment + ~value:1. ~lower:0.05 ~upper:10. + ~step_incr:0.5 ~page_incr:1.0 ~page_size:1.0 () in + + let w = GEdit.spin_button ~adjustment:adj ~rate:0. ~digits:2 ~width:50 ~packing:window#vbox#add () in + + let frame = GBin.frame ~shadow_type:`IN ~height:500 ~width:700 ~packing:window#vbox#add () in + let canvas = GnoCanvas.canvas ~packing:frame#add () in + + canvas#set_center_scroll_region false ; + canvas#set_scroll_region (-2500.) (-2500.) 2500. 2500.; + + let zoom = fun value -> + canvas#set_pixels_per_unit value; + Waypoints.iter (fun _ w -> w#zoom value); + current_zoom := value + in + + ignore (adj#connect#value_changed (fun () -> zoom adj#value)); + + let root = canvas#root in + +(*** ignore (canvas#event#connect#button_press (canvas_button_press canvas)); ***) + + let factory = new GMenu.factory menubar in + let accel_group = factory#accel_group in + let file_menu = factory#add_submenu "File" + and insert_menu = factory#add_submenu "Insert" + and maps_menu = factory#add_submenu "Maps" in + let file_menu_fact = new GMenu.factory file_menu ~accel_group + and maps_menu_fact = new GMenu.factory maps_menu ~accel_group + and insert_menu_fact = new GMenu.factory insert_menu ~accel_group in + + let register_recent_file = fun f -> + RecentFiles.add f; + match f with + RecentFiles.Map f -> + ignore (file_menu_fact#add_item f ~callback:(open_map (fun _ -> ()) root maps_menu_fact ~file:f)) + | RecentFiles.FP f -> + ignore (file_menu_fact#add_item f ~callback:(open_mission (fun _ -> ()) root ~file:f)) in + + ignore (file_menu_fact#add_item "Open Flight Plan" ~key:GdkKeysyms._O ~callback:(open_mission register_recent_file root)); + ignore (file_menu_fact#add_item "Open Map" ~key:GdkKeysyms._M ~callback:(open_map register_recent_file root maps_menu_fact)); + ignore (file_menu_fact#add_item "Write Flight Plan" ~key:GdkKeysyms._S ~callback:write_mission); + ignore (file_menu_fact#add_item "Manual Ref" ~callback:georef); + ignore (file_menu_fact#add_item "Clear Waypoints" ~key:GdkKeysyms._C ~callback:Waypoints.clear); + ignore (file_menu_fact#add_item "Quit" ~key:GdkKeysyms._Q ~callback:quit); + ignore (file_menu_fact#add_separator ()); + + RecentFiles.load (); + + RecentFiles.iter register_recent_file; + + ignore (insert_menu_fact#add_item "Waypoint" ~key:GdkKeysyms._W ~callback:(create_wp canvas 0. 0.)); + + + let bottom = GPack.hbox ~packing:window#vbox#add () in + let lbl_xy = GMisc.label ~packing:bottom#pack () in + let lbl_geo = GMisc.label ~packing:bottom#pack () in + + ignore (canvas#event#connect#motion_notify (display_coord canvas lbl_xy lbl_geo)); + ignore (canvas#event#connect#button_press (canvas_button_press canvas)); + ignore (canvas#event#connect#button_release (canvas_button_release canvas)); + ignore (canvas#event#connect#after#key_press (canvas_key_press canvas)) ; + ignore (canvas#event#connect#enter_notify (fun _ -> canvas#misc#grab_focus () ; false)); + + window#add_accel_group accel_group; + window#show (); + + let port = ref 2010 + and domain = ref "127.255.255.255" in + Arg.parse + [ "-b", Arg.Int (fun x -> port := x), "\tDefault is 2010, unused if IVYBUS is set"; + "-domain", Arg.String (fun x -> domain := x), "\tDefault is 127.255.255.255, unused if IVYBUS is set"] + (fun x -> prerr_endline ("Don't do anything with "^x)) + "Usage: "; + + let bus = + try Sys.getenv "IVYBUS" with + Not_found -> Printf.sprintf "%s:%d" !domain !port in + Ivy.init "medit" "READY" (fun _ _ -> ()); + Ivy.start bus; + + let plot_utm = + let last_plot = ref None in + fun (utm_x:float) (utm_y:float) -> + let utm_zone = (Ref.utm0 ()).utm_zone in + let (x, y) = Ref.world_of_utm {utm_x = utm_x; utm_y = utm_y; utm_zone = utm_zone } in + (match !last_plot with + None -> () + | Some (x', y') -> + ignore (GnoCanvas.line ~points:[|x;y;x';y'|] root)); + last_plot := Some (x,y) + in + + ignore (Ivy.bind (fun _ args -> plot_utm (fos args.(0)/.100.) (fos args.(1)/.100.)) "GPS +[0-9]* +([0-9]*) +([0-9]*)"); + + GMain.Main.main () + +let _ = main () + diff --git a/sw/configurator/monitor.ml b/sw/configurator/monitor.ml new file mode 100644 index 00000000000..e5c3811ccd9 --- /dev/null +++ b/sw/configurator/monitor.ml @@ -0,0 +1,29 @@ +(* + * $Id$ + * + * Real-time flight monitoring + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sheet = fun sheets -> + let airframe = Frame.create sheets in + Notebook.create_sheet sheets "Monitor" airframe diff --git a/sw/configurator/notebook.ml b/sw/configurator/notebook.ml new file mode 100644 index 00000000000..d26a8aaf695 --- /dev/null +++ b/sw/configurator/notebook.ml @@ -0,0 +1,62 @@ +(* + * $Id$ + * + * Facility to create tabs (thanx to JB) + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + + +let eval l = + let token x = Protocol.TkToken x in + Protocol.tkEval (Array.map token (Array.of_list l)) + +(* Float weights bug for grid *) +let set_weight orient w index weight = + ignore (eval ["grid"; orient ^ "configure"; Widget.name w; + string_of_int index; "-weight"; string_of_int weight]) + +let set_column_weight w = set_weight "column" w +let set_row_weight w = set_weight "row" w + +let conf_relief = fun b value -> + eval [Widget.name b; "config"; "-relief"; value] + +let create_sheet w name child = + let lower b = conf_relief b "sunken" + and upper b = conf_relief b "flat" in + let b = Button.create ~relief:`Sunken ~text:name w in + let cmd () = + let (n, _) = Grid.size w in + let slaves = Grid.slaves ~row:1 w in + if slaves <> [] then Grid.forget slaves; + List.iter (fun w -> ignore (lower w)) (Grid.slaves ~row:0 w); + ignore (upper b); + Tk.grid ~column:0 ~row:1 ~columnspan:n ~padx:20 (*** ~pady:20 ***) ~sticky:"news" [child] in + Button.configure ~borderwidth:1 ~command:cmd b; + let (n, _) = Grid.size w in + Tk.grid ~column:n ~row:0 ~sticky:"news" [b]; + set_column_weight w n 1; + if n = 0 then (cmd (); set_row_weight w 1 1) + else Grid.configure ~columnspan:(succ n) (Grid.slaves ~row:1 w) + + + diff --git a/sw/configurator/notebook.mli b/sw/configurator/notebook.mli new file mode 100644 index 00000000000..59e0d545869 --- /dev/null +++ b/sw/configurator/notebook.mli @@ -0,0 +1,28 @@ +(* + * $Id$ + * + * Facility to create tabs (thanx to JB) + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +val create_sheet : 'a Widget.widget -> string -> 'b Widget.widget -> unit +(** [create_sheet parent name frame] adds the new [frame] tab *) diff --git a/sw/configurator/radio.ml b/sw/configurator/radio.ml new file mode 100644 index 00000000000..4e1ee08303c --- /dev/null +++ b/sw/configurator/radio.ml @@ -0,0 +1,188 @@ +(* + * $Id$ + * + * Radio Control transmitter calibration + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf + +let xml_radio = ref VarXml.empty + + +let zero = '\000' +let get_2bytes = fun buf i -> + (Char.code buf.[i] lsl 8) lor (Char.code buf.[i+1]) + +type channel = { + frame : Widget.frame Widget.widget; + update : int -> unit + } + +let soi = string_of_int +let ios = int_of_string + +let nb_neutral = 10 + +let ctl_values = [|"A";"B";"C";"D";"E";"F";"G";"H";"I"|] +let functions = [|"POWER";"ROLL";"PITCH";"DIRECTION";"MODE";"GAIN1";"GAIN2";"LLS";"CALIB"|] +let ctl_values = [|"A";"B";"C";"D";"E";"F";"G"|] +let functions = [|"POWER";"ROLL";"PITCH";"DIRECTION";"MODE";"GAIN1";"GAIN2"|] + +let nb_channels = Array.length ctl_values + + +let popup_entry = fun parent set -> + let t = Toplevel.create parent in + let e = Entry.create ~takefocus:true t in + let action = fun _ -> set (Entry.get e); Tk.destroy t in + Tk.bind e ~events:[`KeyPressDetail "Return"] ~action:action; + let b = Button.create ~text:"ok" ~command:action t in + Tk.pack [e]; + Tk.pack [b] + +let list_button = fun parent values tv -> + let lb = Menubutton.create parent (* ~indicatoron:true *) ~textvariable:tv in + let lb_menu = Menu.create lb in + Menubutton.configure lb ~menu:lb_menu; + Array.iter + (fun v -> + Menu.add_command lb_menu ~label:v ~command:(fun _ -> Textvariable.set tv v)) + values; + Menu.add_command lb_menu ~label:"..." ~command:(fun _ -> popup_entry lb (fun x -> Textvariable.set tv x)); + lb + + + +let one_channel = fun parent i xml_channel -> + let frame = Frame.create ~relief:`Ridge ~borderwidth:1 ~width:5 parent in + let n = Label.create frame ~text:(string_of_int i) ~background:`Green in + + let xml_get = VarXml.attrib xml_channel in + let maximum = xml_get "max" + and neutral = xml_get "neutral" + and minimum = xml_get "min" in + + let rev = Textvariable.create () in + + let ctl = list_button frame ctl_values (xml_get "ctl") + and function_name = list_button frame functions (xml_get "function") in + let current = Label.create frame ~text:"1234" ~relief:`Sunken ~borderwidth:2; + and maxi = Entry.create ~width:4 frame ~textvariable:maximum; + and neutr = Entry.create ~width:4 frame ~textvariable:neutral; + and mini = Entry.create ~width:4 frame ~textvariable:minimum + and reverse = Checkbutton.create ~variable:rev frame + and average = Entry.create ~width:2 ~textvariable:(xml_get "average") frame in + + let max_or_min = fun a b -> + if Textvariable.get rev = "1" then min a b else max a b + and min_or_max = fun a b -> + if Textvariable.get rev = "1" then max a b else min a b in + + + Tk.pack [n] ~side:`Top; + Tk.pack [ctl; function_name] ~side:`Top; + Tk.pack [current] ~side:`Top; + Tk.pack [maxi; neutr; mini] ~side:`Top; + Tk.pack [reverse] ~side:`Top; + Tk.pack [average] ~side:`Top; + + let get_ma = fun () -> ios (Textvariable.get maximum) + and get_mi = fun () -> ios (Textvariable.get minimum) in + + let update_max = fun v -> + let ma = max_or_min (get_ma ()) v in + Textvariable.set maximum (soi ma) + and update_min = fun v -> + let mi = min_or_max (get_mi ()) v in + Textvariable.set minimum (soi mi) in + + let sum_neutral = ref 0 and cpt_neutral = ref 1 in + + Tk.bind ~events:[`ButtonPress] ~action:(fun _ -> Textvariable.set maximum "00000") maxi; + Tk.bind ~events:[`ButtonPress] ~action:(fun _ -> Textvariable.set minimum "99999") mini; + Tk.bind ~events:[`ButtonPress] ~action:(fun _ -> cpt_neutral := 0; sum_neutral := 0) neutr; + + + let update = fun value -> + Label.configure current ~text:(string_of_int value); + update_max value; + update_min value; + if !cpt_neutral < nb_neutral then begin + incr cpt_neutral; + sum_neutral := !sum_neutral + value; + Textvariable.set neutral (soi (!sum_neutral/ !cpt_neutral)); + end + in + + { frame = frame; update = update};; + + + +let program_board = fun w cs () -> + Flasher.make Flasher.Fbw (Printf.sprintf "%s/test" Env.fbw_dir) "TARGET=rc_transmitter load"; + + let tty = Hardware.Fbw.get_tty () in + Tty.connect tty; + Tty.add_formatted_input tty "\000\000" (2*(nb_channels+1)+1) + (fun input -> + for i = 0 to String.length input -1 do + printf "0x%X " (Char.code input.[i]); + done; + print_newline (); + let channel_values = Array.init nb_channels (fun s -> get_2bytes input (2+2*s)) in + for i = 0 to nb_channels - 1 do + cs.(i).update channel_values.(i) + done); + Tk.bind ~events:[`Destroy] ~action:(fun _ -> Tty.deconnect tty) w + + + +let create_display_frame = fun nf xml_radio -> + let xml_channels = Array.of_list (VarXml.children xml_radio) in + let cs = Array.mapi (fun i xml -> one_channel nf i xml) xml_channels in + + let p = Button.create ~text:"Get Radio-Control Values" ~command:(program_board nf cs) nf in + + Tk.pack [p]; + + let legend = Frame.create nf in + let label = fun s -> Label.create legend ~text:s in + Tk.pack ~anchor:`E [label "Channel: "; + label "Control name: "; + label "Function name: "; + label "Current value: "; + label "Max (click to reset): "; + label "Neutral (click to reset): "; + label "Min (click to reset): "; + label "Reverse"; + label "Averaged"] ~side:`Top; + + + Tk.pack (legend::List.map (fun c -> c.frame) (Array.to_list cs)) ~side:`Left + + +let create_sheet = fun sheets -> + let top = Frame.create sheets in + Notebook.create_sheet sheets "Radio" top; + + TkXml.create top create_display_frame diff --git a/sw/configurator/servos.ml b/sw/configurator/servos.ml new file mode 100644 index 00000000000..17838cf4445 --- /dev/null +++ b/sw/configurator/servos.ml @@ -0,0 +1,136 @@ +(* + * $Id$ + * + * Servos calibration + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let servo_update_period = 500 (* ms *) + +let default_max = 2000 +let default_min = 1000 +let max_max = 2500. +let min_min = 500. +let default_neutral = 1500 + +let soi = string_of_int +let ios = int_of_string +let fos = float_of_string + +let output_2bytes = fun tty x -> + prerr_endline (string_of_int x); + Tty.write_byte tty ((x land 0xff00) asr 8); + Tty.write_byte tty (x land 0xff) + +type selected = { widget : Widget.label Widget.widget; channel : int } + +type channel = { frame : Widget.frame Widget.widget; print : out_channel -> unit } + +let one_channel = fun _i parent slider selected servo_xml -> + let frame = Frame.create parent in + let no = Textvariable.get (VarXml.attrib servo_xml "no") in + let n = Label.create frame ~text:no ~background:`Green in + + let create_label = fun id -> + let v = VarXml.attrib servo_xml id in + let l = Label.create frame ~textvariable:v in + + let button_action = fun _event -> + Label.configure l ~relief:`Sunken; + begin + match !selected with + None -> () + | Some s -> Label.configure s.widget ~relief:`Flat + end; + selected := Some { widget=l; channel=int_of_string no }; + + Scale.set slider (fos (Textvariable.get v)); + Scale.configure ~command:(fun value -> Textvariable.set v (string_of_int (truncate value))) slider in + + Tk.bind ~events:[`ButtonPress] ~action:button_action l; + l in + + let maximum = create_label "max" + and neutral = create_label "neutral" + and minimum = create_label "min" in + + Tk.pack [n; maximum; neutral; minimum] ~side:`Top; + frame + + +let rec send_selected_value = fun selected sending_state tty -> + if Textvariable.get sending_state = "1" then begin + Timer.set servo_update_period (fun () -> send_selected_value selected sending_state tty); + match !selected with + Some s -> + Tty.write_byte tty 0; + Tty.write_byte tty s.channel; + output_2bytes tty (ios (Tk.cget s.widget `Text)); + Tty.write tty "\n"; + Tty.flush tty + | None -> () + end + +let create_sheet = fun sheets airframe_xml -> + let top = Frame.create sheets in + Notebook.create_sheet sheets "Servos" top; + + let tf = Frame.create ~relief:`Ridge ~borderwidth:1 top in + + let channels = Frame.create top in + + let sending_state = Textvariable.create () + and selected = ref None in + + let sending = fun () -> + if Textvariable.get sending_state = "1" then begin + let fbw_tty = Textvariable.get Hardware.Fbw.tty in + Console.write (Printf.sprintf "Sending to %s\n" fbw_tty); + Tty.connect fbw_tty; + send_selected_value selected sending_state fbw_tty + end else + Tty.deconnect (Textvariable.get Hardware.Fbw.tty) + in + + let program = fun () -> + Flasher.make Flasher.Fbw (Printf.sprintf "%s/test" Env.fbw_dir) "TARGET=setup_servos load" in + + let legend = Frame.create channels + and slider = Scale.create tf ~orient:`Horizontal ~min:min_min ~max:max_max ~digits:4 ~label:"micro-seconds" ~resolution:1. ~length:300 + and pb = Button.create ~text:"Program board to send" ~command:program tf + and rb = Checkbutton.create ~text:"Sending" ~variable:sending_state ~command:sending tf in + let label = fun s -> Label.create legend ~text:s in + Tk.pack [label "Channel: "; + label "Max (click to select): "; + label "Neutral (click to select): "; + label "Min (click to select): "] ~side:`Top; + + + let servos_xml = Array.of_list (VarXml.children (VarXml.child airframe_xml "servos")) in + + let cs = Array.mapi (fun i s -> one_channel i channels slider selected s) servos_xml in + + Tk.pack (legend::Array.to_list cs) ~side:`Left; + Tk.pack [tf]; + Tk.pack [pb]; Tk.pack [rb]; + Tk.pack [slider]; + Tk.pack [channels] diff --git a/sw/configurator/simulator.ml b/sw/configurator/simulator.ml new file mode 100644 index 00000000000..6e5ee783777 --- /dev/null +++ b/sw/configurator/simulator.ml @@ -0,0 +1,29 @@ +(* + * $Id$ + * + * Hardware in the loop simulator + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sheet = fun sheets -> + let airframe = Frame.create sheets in + Notebook.create_sheet sheets "Simulator" airframe diff --git a/sw/configurator/tkXml.ml b/sw/configurator/tkXml.ml new file mode 100644 index 00000000000..da91e939609 --- /dev/null +++ b/sw/configurator/tkXml.ml @@ -0,0 +1,86 @@ +(* + * $Id$ + * + * Binding of a widget to an XML file + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_display_frame = fun f xml appli -> + let tf = Frame.create f + and af = Frame.create f in + + let l = Label.create ~text:"Name:" tf + and name = Entry.create ~textvariable:(VarXml.attrib xml "name") tf in + + let destroy = fun () -> + Tk.destroy tf; + Tk.destroy af; + Grab.release f in + + let save = fun file -> + Console.write (Printf.sprintf "Saving to \"%s\"\n" file); + let s = Xml.to_string_fmt (VarXml.to_xml xml) in + let f = open_out file in + output_string f s; + close_out f; + destroy () in + + let save_button = Button.create ~text:"Save Config" ~command:(fun _ -> Env.select_one_file save) tf + and cancel_button = Button.create ~text:"Cancel" ~command:destroy tf in + + Tk.pack [l] ~side:`Left; + Tk.pack [name] ~side:`Left; + Tk.pack [save_button; cancel_button] ~side:`Left; + Tk.pack ~anchor:`N [tf]; + + Grab.set f; + + Tk.pack [af]; + + appli af xml + + + +let create = fun top appli -> + let f = Frame.create top in + let load = fun file -> + Console.write (Printf.sprintf "Reading from \"%s\"\n" file); + + create_display_frame f (VarXml.of_xml (Xml.parse_file file)) appli in + let load_button = Button.create ~text:"Load Config" ~command:(fun _ -> Env.select_one_file load) top in + + Tk.pack [load_button]; + Tk.pack [f] + + +let create_section = fun f xml -> + Tk.grid ~column:0 ~row:0 [Label.create ~text:"Name" f]; + Tk.grid ~column:1 ~row:0 [Label.create ~text:"Value" f]; + + let r = ref 0 in + List.iter + (fun def -> + incr r; + Tk.grid ~column:0 ~row:!r [Label.create ~textvariable:(VarXml.attrib def "name") f]; + Tk.grid ~column:1 ~row:!r [Entry.create ~width:4 ~textvariable:(VarXml.attrib def "value") f]) + (VarXml.children xml) + diff --git a/sw/configurator/tkXml.mli b/sw/configurator/tkXml.mli new file mode 100644 index 00000000000..fda27bafdbc --- /dev/null +++ b/sw/configurator/tkXml.mli @@ -0,0 +1,35 @@ +(* + * $Id$ + * + * Binding of a widget to an XML file + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +val create : + 'a Widget.widget -> + (Widget.frame Widget.widget -> VarXml.xml -> unit) -> unit +(** [create parent action] Wraps the given [action] into an XML file handler. +The arguments of [action] are a graphic place and the opened XML object. +[action] may then modify the contents of the XML object and save the +modifications. Note that the interface is grabbed on the created subframe. *) + +val create_section : 'a Widget.widget -> VarXml.xml -> unit diff --git a/sw/configurator/tty.ml b/sw/configurator/tty.ml new file mode 100644 index 00000000000..8b7048651cf --- /dev/null +++ b/sw/configurator/tty.ml @@ -0,0 +1,101 @@ +(* + * $Id$ + * + * Serial device handling + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let buffer_len = 256 + +let ttys = Hashtbl.create 7 +let registered = Hashtbl.create 7 + +let deconnect = fun tty -> + try + let fd = Hashtbl.find ttys tty in + Fileevent.remove_fileinput fd; + Unix.close fd; + Hashtbl.remove ttys tty + with + Not_found -> () + +let connect = fun tty -> + if Hashtbl.mem ttys tty then + deconnect tty; + let buffer = String.create buffer_len in + let fd = Serial.opendev tty Env.tty_rate in + let log = fun () -> + let n = Unix.read fd buffer 0 buffer_len in + let s = String.sub buffer 0 n in + List.iter (fun f -> f s) (Hashtbl.find_all registered tty) in + Fileevent.add_fileinput fd log; + Hashtbl.add ttys tty fd + + +let add_ttyinput = Hashtbl.add registered + +let add_formatted_input = fun tty prefix size f -> + if String.length prefix > size then raise (Invalid_argument "add_formatted_input"); + let buffer = String.create size + and idx = ref 0 in + let rec f' = fun s -> + let n = String.length s + and expected = size - !idx in + Printf.printf "%d " n; flush stdout; + let blitted = min expected n in + String.blit s 0 buffer !idx blitted; + idx := !idx + blitted; + if !idx = size then begin + if String.sub buffer 0 (String.length prefix) = prefix then begin + f buffer; + idx := 0 + end else begin (* Look for the first character of the prefix *) + try + let discarded = String.index_from buffer 1 prefix.[0] in + let kept = size - discarded in + String.blit buffer discarded buffer 0 kept; + idx := kept + with + Not_found -> (* prefix.[0] not found *) + idx := 0 + end + end; + let rest = n - blitted in + if rest > 0 then begin Printf.printf "r=%d\n" rest; flush stdout; f' (String.sub s blitted rest) end in + add_ttyinput tty f' + +let write = fun tty s -> + let fd = Hashtbl.find ttys tty in + let oc = Unix.out_channel_of_descr fd in + Printf.fprintf oc "%s" s; + flush oc + +let write_byte = fun tty b -> + let fd = Hashtbl.find ttys tty in + let oc = Unix.out_channel_of_descr fd in + output_byte oc b; + flush oc + +let flush = fun tty -> + let fd = Hashtbl.find ttys tty in + flush (Unix.out_channel_of_descr fd) + diff --git a/sw/configurator/tty.mli b/sw/configurator/tty.mli new file mode 100644 index 00000000000..90ae020fb3d --- /dev/null +++ b/sw/configurator/tty.mli @@ -0,0 +1,42 @@ +(* + * $Id$ + * + * Serial device handling + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +val connect : string -> unit +val deconnect : string -> unit +(** Opens and closes the given device *) + +val add_ttyinput : string -> (string -> unit) -> unit +(** [add_ttyinput device cb] Attaches the callback [cb] to input events *) + +val add_formatted_input : string -> string -> int -> (string -> unit) -> unit +(** [add_formatted_input device prefix size cb] Same as [add_ttyinput] +but [cb] is called only with an input of length [size] starting with +[prefix]. Characters are discarded until [prefix] is found. *) + +val write : string -> string -> unit +val write_byte : string -> int -> unit +val flush : string -> unit +(** Output on the given device *) diff --git a/sw/configurator/upload.ml b/sw/configurator/upload.ml new file mode 100644 index 00000000000..4389e6564c7 --- /dev/null +++ b/sw/configurator/upload.ml @@ -0,0 +1,34 @@ +(* + * $Id$ + * + * Uploading flight control to micro-controllers + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sheet = fun sheets -> + let f = Frame.create sheets in + Notebook.create_sheet sheets "Upload" f; + + let b = Button.create ~text:"Program Fly by Wire MCU" ~command:(Hardware.Fbw.make "load") f + and b' = Button.create ~text:"Program Autopilot MCU" ~command:(Hardware.Ap.make "load") f in + + Tk.pack [b; b'] diff --git a/sw/configurator/varXml.ml b/sw/configurator/varXml.ml new file mode 100644 index 00000000000..cd41dc18b54 --- /dev/null +++ b/sw/configurator/varXml.ml @@ -0,0 +1,91 @@ +(* + * $Id$ + * + * Mutable XML representation based on TK Textvariable + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +type xml = + | Element of (string * (string * Textvariable.textVariable) list * xml list) + | PCData of string + +let empty = PCData "Empty" + +let variable = fun s -> + let t = Textvariable.create () in + Textvariable.set t s; + t + +let rec of_xml = function + | Xml.Element (tag, attributes, children) -> + let varattrs = List.map (fun (a, v) -> (a, variable v)) attributes in + Element (tag, varattrs, List.map of_xml children) + | Xml.PCData string -> PCData string + +let rec to_xml = function + | Element (tag, attributes, children) -> + let varattrs = List.map (fun (a, v) -> (a, Textvariable.get v)) attributes in + Xml.Element (tag, varattrs, List.map to_xml children) + | PCData string -> Xml.PCData string + +let attrib xml a = + match xml with + | Element (_tag, attributes, _children) -> + List.assoc a attributes + | _ -> failwith "VarXml.attrib" + +let children xml = + match xml with + | Element (_tag, _attributes, children) -> children + | _ -> failwith "VarXml.children" + +let child xml ?select c = + let rec find = function + Element (tag, attributes, _children) as elt :: elts -> + if tag = c then + match select with + None -> elt + | Some p -> + if p attributes then elt else find elts + else + find elts + | _ :: elts -> find elts + | [] -> raise Not_found in + find (children xml) + + + +let slash = Str.regexp "/" + +let get = fun path attr root -> + let path = Str.split slash path in + let rec find = fun path attributes xmls -> + match path, xmls with + [], _ -> List.assoc attr attributes + | tag::tags, Element (tag', attributes', children)::elements -> + if tag = tag' then + find tags attributes' children + else + find path attributes elements + | tag::_, _ -> failwith ("VarXml.get "^tag) in + find path [] [root] + diff --git a/sw/configurator/varXml.mli b/sw/configurator/varXml.mli new file mode 100644 index 00000000000..bccae29ff21 --- /dev/null +++ b/sw/configurator/varXml.mli @@ -0,0 +1,37 @@ +(* + * $Id$ + * + * Mutable XML representation based on TK Textvariable + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + + +type xml +val empty : xml +val of_xml : Xml.xml -> xml +val to_xml : xml -> Xml.xml +val attrib : xml -> string -> Textvariable.textVariable +val children : xml -> xml list +val child : + xml -> + ?select:((string * Textvariable.textVariable) list -> bool) -> + string -> xml diff --git a/sw/configurator/welcome.ml b/sw/configurator/welcome.ml new file mode 100644 index 00000000000..79794926651 --- /dev/null +++ b/sw/configurator/welcome.ml @@ -0,0 +1,38 @@ +(* + * $Id$ + * + * Paparazzi welcome logo + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let create_sheet = fun sheets -> + let welcome = Frame.create sheets in + Notebook.create_sheet sheets "Welcome" welcome; + + let l = Label.create ~text:"WELCOME TO THE PAPARAZZI CONFIGURATOR" welcome + in let img = Imagephoto.create() in + Imagephoto.configure img ~file:(Env.configurator_dir^"/penguin.gif") ~format:"gif"; + let l1 = Label.create ~image:img welcome + in Tk.pack [l; l1]; + + + diff --git a/sw/ground_segment/cockpit/Makefile b/sw/ground_segment/cockpit/Makefile new file mode 100644 index 00000000000..41eeaa713b2 --- /dev/null +++ b/sw/ground_segment/cockpit/Makefile @@ -0,0 +1,35 @@ +OCAMLC=ocamlc -g +OCAMLOPT=ocamlopt +INCLUDES=-I +lablgtk2 -I +camlimages -I ../../lib/ocaml +LIBS=glibivy-ocaml.cma lablgtk.cma ci_core.cma ci_png.cma ci_gif.cma ci_jpeg.cma ci_tiff.cma ci_bmp.cma ci_ppm.cma ci_ps.cma lib.cma lablgnomecanvas.cma xlib.cma +CMXA=$(LIBS:.cma=.cmxa) + +SRC = map2d.ml +CMO = $(SRC:.ml=.cmo) +CMX = $(SRC:.ml=.cmx) + +all : map2d.opt + + +map2d.out : $(CMO) + $(OCAMLC) $(INCLUDES) $(LIBS) gtkInit.cmo $(CMO) -o $@ + + +map2d.opt : $(CMX) + $(OCAMLOPT) str.cmxa unix.cmxa xml-light.cmxa $(INCLUDES) $(CMXA) gtkInit.cmx $(CMX) -o $@ + +map2d.run: + lablgtk2 str.cma unix.cma xml-light.cma -I +camlimages -I ../../lib/ocaml glibivy-ocaml.cma ci_core.cma ci_png.cma ci_gif.cma ci_jpeg.cma ci_tiff.cma ci_bmp.cma ci_ppm.cma ci_ps.cma lib.cma xlib.cma map2d.ml + + +.SUFFIXES: .ml .mli .cmo .cmi .cmx + +.ml.cmo: + $(OCAMLC) $(INCLUDES) -labels -w s -c $< +.mli.cmi: + $(OCAMLC) $(INCLUDES) -labels -w s -c $< +.ml.cmx: + $(OCAMLOPT) $(INCLUDES) -labels -w s -c $< + +clean: + rm -f *~* *.cm* *.o *.out *.opt diff --git a/sw/ground_segment/cockpit/Paparazzi/APPage.pm b/sw/ground_segment/cockpit/Paparazzi/APPage.pm new file mode 100644 index 00000000000..7f89b08ecc1 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/APPage.pm @@ -0,0 +1,46 @@ +package Paparazzi::APPage; +use Paparazzi::NDPage; +@ISA = ("Paparazzi::NDPage"); +use strict; +use Subject; +use Data::Dumper; + +use constant TITLE => "Autopilot"; + +sub populate { + my ($self, $args) = @_; + $args->{-title} = TITLE; + $self->SUPER::populate($args); + $self->configspec( + -ap_status => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, {}], + ); +} + +sub ap_status { + my ($self, $old_val, $new_val) = @_; + return unless defined $new_val; +# print "in APPage ap_status\n".Dumper($new_val); + my $zinc = $self->get('-zinc'); + foreach my $field (keys %{$new_val}) { + $zinc->itemconfigure ($self->{'text_'.$field}, + -text => sprintf("%s : %.1f", $field, $new_val->{$field})) if defined $self->{'text_'.$field}; + } +} + +sub build_gui { + my ($self) = @_; + $self->SUPER::build_gui(); + my $zinc = $self->get('-zinc'); + my $dy = $self->get('-height')/10; + my $y=10; + my $x=10; + foreach my $field ('mode', 'h_mode', 'v_mode', 'target_climb', 'target_alt', 'target_heading') { + $self->{'text_'.$field} = $zinc->add('text', $self->{main_group}, + -position => [$x, $y+$self->{vmargin}], + -color => 'white', + -anchor => 'w', + -text => $field); + $y+=$dy; + } +} + diff --git a/sw/ground_segment/cockpit/Paparazzi/EnginePage.pm b/sw/ground_segment/cockpit/Paparazzi/EnginePage.pm new file mode 100644 index 00000000000..4d73b4aba46 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/EnginePage.pm @@ -0,0 +1,78 @@ +package Paparazzi::EnginePage; +use Paparazzi::NDPage; +@ISA = ("Paparazzi::NDPage"); +use strict; +use Subject; + +use Tk; +use Tk::Zinc; +use Math::Trig; +use Data::Dumper; + +use Paparazzi::Utils; +use Paparazzi::RotaryGauge; + +use constant TITLE => "Engine"; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $args->{-title} = TITLE; + $self->configspec( + -engine_status => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, {}], + ); +} + +sub engine_status { + my ($self, $old_val, $new_val) = @_; + my $zinc = $self->get('-zinc'); + foreach my $field (keys %{$new_val}) { + $self->{'gauge_'.$field}->configure( -value => $new_val->{$field}) if defined $self->{'gauge_'.$field}; + } +} + +sub build_gui { + my ($self) = @_; + $self->SUPER::build_gui(); + my $zinc = $self->get('-zinc'); + my $width = $self->get('-width'); + my $height = $self->get('-height'); + use constant GAUGES_PER_ROW => 3; + my @gauges = ( { name => 'throttle', format => "%.1f %%", extends => [0 , 101, 10, 20]}, + { name => 'rpm', format => "%.0f rpm", extends => [0 , 16, 1, 2]}, + { name => 'temp', format => "%.1f °C", extends => [-20, 80, 10, 20]}, + { name => 'bat', format => "%.1f V", extends => [6., 14., 0.5, 1.]}, + { name => 'amp', format => "%.1f A", extends => [0, 16, 1, 2] }, + { name => 'energy', format => "%.1f Wh", extends => [0, 50, 5, 10] }, + ); + my $margin = 5; + my $vmargin = 50; + my $gauge_spacing = ($width - 2 * $margin) / GAUGES_PER_ROW; + my $gauge_radius = $gauge_spacing / 2. * 0.8; + my $gauge_row = 0; + my $gauge_col = 0; + foreach my $gauge (@gauges) { + my $extends = $gauge->{extends}; + my $pos = [ $margin + ($gauge_col+0.1) * $gauge_spacing, $vmargin + $gauge_row * ($gauge_spacing + 30)]; + $self->{'gauge_'.$gauge->{name}} = Paparazzi::RotaryGauge->new(-zinc => $zinc, + -radius => $gauge_radius, + -origin => $pos, + -parent_grp => $self->{main_group}, + -min_val => $extends->[0], + -max_val => $extends->[1], + -tick_spacing => $extends->[2], + -legend_spacing => $extends->[3], + -min_val_angle => -90., + -dead_sector => 30., + -text => $gauge->{name}, + -format => $gauge->{format} + ); + $gauge_col++; + unless ($gauge_col lt GAUGES_PER_ROW) { + $gauge_row++; + $gauge_col = 0; + } + } +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/Geometry.pm b/sw/ground_segment/cockpit/Paparazzi/Geometry.pm new file mode 100644 index 00000000000..c27ab01858d --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/Geometry.pm @@ -0,0 +1,44 @@ +package Paparazzi::Geometry; + +use Math::Trig; + +use constant PI_TWO => (pi / 2); +use constant TWO_PI => (2 * pi); + +sub angle_of_heading_rad { + my ($angle)=@_; + return norm_angle_rad( 5 * PI_TWO - $angle ); +} + +sub heading_of_angle_rad { + +} + + +sub norm_heading_rad { + my ($val) = @_; + while ($val > TWO_PI ) {$val -= TWO_PI} + while ($val < 0) {$val += TWO_PI } + return $val; +} + +sub norm_angle_rad { + my ($val) = @_; + while ($val > pi) {$val -= TWO_PI} + while ($val < - pi) {$val += TWO_PI} + return $val; +} + + +sub cart_of_polar { + my ($r, $theta) = @_; + return ($r * cos $theta, $r * sin $theta); +} + +sub polar_of_cart { + my ($x, $y) = @_; + return (sqrt($x*$x+$y*$y), atan2($y, $x)); +} + + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/HistoryView.pm b/sw/ground_segment/cockpit/Paparazzi/HistoryView.pm new file mode 100644 index 00000000000..de9d4348f33 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/HistoryView.pm @@ -0,0 +1,80 @@ +package Paparazzi::HistoryView; + +use strict; +use Carp; +use vars qw(@ISA); + +use Subject; +@ISA = qw(Subject); + +use POSIX; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec( + -zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -parent_grp => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -nb_bars => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -initial_range => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 1.], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; + $self->{x_scrolling_val} = 0; + $self->{bar_width} = POSIX::floor($self->get('-width') / $self->get('-nb_bars')); + $self->{bars} = []; + $self->{nb_visible_bars} = 0; + $self->{scale} = $self->get('-height') / $self->get('-initial_range'); + $self->build_gui(); +} + +sub put_value { + my ($self, $val) = @_; + return unless defined $val and defined $self->{moving_group}; + if ($self->{nb_visible_bars} >= $self->get('-nb_bars')) { + my $rect = shift @{$self->{bars}}; + $self->get('-zinc')->remove($rect); +# print "removing $rect\n"; + } + else { + $self->{nb_visible_bars}++; + } + my $height = $self->get('-height'); + my $h = $val * $self->{scale}; + my $rect = [$self->{x_scrolling_val}, $height-$h, + $self->{x_scrolling_val}+$self->{bar_width}, $height]; + my $bar = $self->get('-zinc')->add('rectangle', $self->{moving_group}, $rect, + -filled => 1, + -fillcolor => 'gray60', + -visible => 1); + push @{$self->{bars}}, $bar; + $self->{x_scrolling_val}+=$self->{bar_width}; + if ($self->{x_scrolling_val} > $self->get('-width')) { + $self->get('-zinc')->translate($self->{moving_group}, -$self->{bar_width}, 0); + } +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $origin = $self->get('-origin'); + my $parent_grp = $self->get('-parent_grp'); + my $height = $self->get('-height'); + my $width = $self->get('-width'); + $self->{main_group} = $zinc->add('group', $parent_grp, -visible => 1); + $zinc->coords($self->{main_group}, $origin); + + $self->{clipping_group} = $zinc->add('group',$self->{main_group}, -visible => 1); + $self->{itemclip} = $zinc->add('rectangle', $self->{clipping_group}, [0, 0, $width, $height], + -visible => 0); + $zinc->itemconfigure($self->{clipping_group}, -clip => $self->{itemclip}); + $self->{moving_group} = $zinc->add('group',$self->{clipping_group}, -visible => 1); + $zinc->add('rectangle', $self->{main_group}, [0, 0, $width, $height], + -visible => 1, -linecolor => "green"); +} diff --git a/sw/ground_segment/cockpit/Paparazzi/Horizon.pm b/sw/ground_segment/cockpit/Paparazzi/Horizon.pm new file mode 100644 index 00000000000..cffb52a68f8 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/Horizon.pm @@ -0,0 +1,199 @@ +#============================================================================= +# Horizon Class +#============================================================================= +package Paparazzi::Horizon; +use Subject; +@ISA = ("Subject"); +use strict; + +use Paparazzi::Utils; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -parent_grp => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -radius => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -roll => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -pitch => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; + $self->build_gui(); +} + +sub roll { + my ($self, $old_angle, $new_angle) = @_; + return unless defined $old_angle; + my $dangle = Utils::rad_of_deg($old_angle - $new_angle); + $self->get('-zinc')->rotate($self->{horizon_rotate_group}, $dangle, 0, 0); +} + +sub pitch { + my ($self, $old_angle, $new_angle) = @_; + return unless defined $old_angle; + my $dy = $self->dy_of_angle($new_angle - $old_angle); + $self->get('-zinc')->translate($self->{horizon_translate_group}, 0, $dy); +} + +sub dy_of_angle { + my ($self, $angle) = @_; + return $angle * $self->{y_per_deg}; +} + +sub build_gui { + my ($self) = @_; + my $parent_grp = $self->get('-parent_grp'); + my $zinc = $self->get('-zinc'); + my $radius = $self->get('-radius'); + + $self->{horizon_group} = $zinc->add('group', $parent_grp, -visible => 1); + my $origin = $self->get('-origin'); + $zinc->coords($self->{horizon_group}, $origin); + + + $self->{horizon_rotate_group} = $zinc->add('group', $self->{horizon_group}, -visible => 1); + $self->{horizon_translate_group} = $zinc->add('group', $self->{horizon_rotate_group}, -visible => 1); + + + $self->{fixed_group} = $zinc->add('group', $self->{horizon_group}, -visible => 1); + + $self->{horizon_itemclip} = $zinc->add('arc', $self->{horizon_rotate_group}, + [-$radius, -$radius, $radius, $radius], + -visible => 0); + $zinc->itemconfigure($self->{horizon_rotate_group}, -clip => $self->{horizon_itemclip}); + + # horizon earth + $zinc->add('rectangle', $self->{horizon_translate_group} , + [-$radius, 0, $radius, 3 * $radius], + -linewidth => 0, -filled => 1,-fillcolor => '#986701', #'orange', + ); + # horizon sky + $zinc->add('rectangle', $self->{horizon_translate_group} , + [-$radius, -3 * $radius, $radius, 0], + -linewidth => 0, -filled => 1, -fillcolor => '#0099cb', # 'blue' + ); + # center line + $zinc->add('curve', $self->{horizon_translate_group}, + [-$radius, 0, $radius, 0], + -linewidth => 2, -linecolor => 'white', -filled => 0); + + # pitch scale + $self->{y_per_deg} = $radius / 30; + my $v_tick_font = '-adobe-helvetica-bold-o-normal--12-240-100-100-p-182-iso8859-1'; + my $i; + for ($i=-16; $i <= 16; $i++) { + my $angle = $i*2.5; + my $y = $self->dy_of_angle($angle); + my $x = $radius / 16; + if (!($i%4)) {$x = $radius / 4;} + elsif (!($i%2)) {$x = $radius / 8}; + $zinc->add('curve', $self->{horizon_translate_group}, + [-$x, $y, $x, $y], + -linewidth => 1, + -linecolor => 'white', + -filled => 0); + + if (!($i%4) & ($i != 0)) { + my $text_lab = sprintf("%d", $angle); + my @text_attr = ( -color => 'white', + -font => $v_tick_font, + -text => $text_lab ); + $zinc->add('text', $self->{horizon_translate_group}, + -position => [-$x-10, -$y], + -anchor => 'e', + @text_attr); + $zinc->add('text', $self->{horizon_translate_group}, + -position => [$x+10, -$y], + -anchor => 'w', + @text_attr); + } + } + + # arrow + my $arrow_len = 10; + $zinc->add('curve', $self->{horizon_rotate_group}, + [0, -$radius+1, + -$arrow_len+1, -$radius+$arrow_len, + $arrow_len-1, -$radius+$arrow_len], + -linewidth => 2, + -linecolor => 'yellow', + -filled => 0, + -closed => 1); + + # roll scale + $zinc->add('arc', $self->{fixed_group}, + [-$radius + 1, -$radius + 1, $radius - 1, $radius - 1], + -startangle => -120, + -extent => 60, + -linewidth => 2, + -linecolor => 'white', + -filled => 0); + + for ($i=-4; $i <= 4; $i++) { + my $angle = Utils::rad_of_deg($i * 15); + my $x1 = 0 * cos($angle) - $radius * sin($angle); + my $y1 = 0 * sin($angle) - $radius * cos($angle); + my $x2 = 0 * cos($angle) - ($radius+10) * sin($angle); + my $y2 = 0 * sin($angle) - ($radius+10) * cos($angle); + $zinc->add('curve', $self->{fixed_group}, + [$x1, $y1, $x2, $y2], + -linewidth => 1, + -linecolor => 'white', + -filled => 0); + } + + +# fixed indicator + my @center_sign = [ -3, -3, + -3, 3, + 3, 3, + 3, -3]; + + my @left_sign = [-50, -3, + -80, -3, + -80, 1, + -50, 1, + -50, 15, + -46, 15, + -46, -3]; + my @right_sign = [50, -3, + 80, -3, + 80, 1, + 50, 1, + 50, 15, + 46, 15, + 46, -3]; + my @fixed_indic_attr = ( -linewidth => 1, + -linecolor => 'yellow', + -filled => 1, + -fillcolor => 'black', + -closed => 1 + ); + foreach my $sign_section (\@center_sign, \@left_sign, \@right_sign) { + $zinc->add('curve', $self->{fixed_group}, + @{$sign_section}, + @fixed_indic_attr); + } + + # side black masks + my $pc_black = 0.18; + my @side_masks_attr = ( -linewidth => 0, + -filled => 1, + -fillcolor => 'black' ); + $zinc->add('rectangle', $self->{fixed_group} , + [(1-$pc_black)*$radius , -$radius, + $radius, $radius], + @side_masks_attr); + $zinc->add('rectangle', $self->{fixed_group} , + [-$radius , -$radius, + -(1-$pc_black)*$radius, $radius], + @side_masks_attr); +} + + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/IRPage.pm b/sw/ground_segment/cockpit/Paparazzi/IRPage.pm new file mode 100644 index 00000000000..290a87a4179 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/IRPage.pm @@ -0,0 +1,152 @@ +package Paparazzi::IRPage; +use Paparazzi::NDPage; +@ISA = ("Paparazzi::NDPage"); +use strict; +use Subject; +use DigiKit::Button; + +use Paparazzi::HistoryView; + + +use constant TITLE => "Infrared"; +use constant UPDATE_REPEAT => 2000; + +sub populate { + my ($self, $args) = @_; + $args->{-title} = TITLE; + $self->SUPER::populate($args); + $self->configspec( + -wind => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, {}], + -lls => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.0015], + ); +} + +sub completeinit { + my $self = shift; + $self->{wind_running} = 0; + $self->SUPER::completeinit(); +# $self->build_gui(); + $self->configure('-pubevts' => 'WIND_COMMAND'); + $self->{timer_id} = $self->get('-zinc')->repeat(UPDATE_REPEAT, [\&onTimer, $self]); +} + +sub onTimer { + my ( $self) = @_; + $self->{history}->put_value(scalar $self->get('-lls')); +} + +sub wind { + my ($self, $old_val, $new_val) = @_; + foreach my $field (keys %{$new_val}) { + $self->get('-zinc')->itemconfigure ($self->{'text_'.$field}, + -text => sprintf("%s : %.4f", $field, $new_val->{$field})) if defined $self->{'text_'.$field}; + } +} + +sub lls { + my ($self, $old_val, $new_val) = @_; + return unless defined $new_val and defined $self->{history}; +# $self->{history}->put_value($new_val); + $self->get('-zinc')->itemconfigure ($self->{'text_auto gain'}, + -text => sprintf("auto gain : %.5f", $new_val)); +} + +#sub put_lls { +# my ($self, $value) = @_; +## $self->{history}->put_value($value); +#} + +sub build_gui { + my ($self) = @_; + $self->SUPER::build_gui(); + my $zinc = $self->get('-zinc'); + my $parent_grp = $self->get('-parent_grp'); + my $main_group = $self->{main_group}; + + my $fields = ["contrast", "gain","auto gain"]; + my ($y, $dy) = ( 35, 20); + foreach my $field (@{$fields}) { + $self->{'text_'.$field} = $zinc->add('text', $main_group, + -position => [20, $y], + -color => 'white', + -anchor => 'w', + -text => "$field"); + $y+=$dy; + } + + my $nb_bars = 125; + $self->{history} = + Paparazzi::HistoryView->new(-zinc => $zinc, + -width => 250, + -height => 50, + -origin => [20, 90], + -parent_grp => $self->{main_group}, + -nb_bars => $nb_bars, + -initial_range => 0.003, + ); + $zinc->add('text', $main_group, + -position => [20, 145], + -color => 'white', + -anchor => 'w', + -text => sprintf("%d seconds", UPDATE_REPEAT * $nb_bars / 1000), + ); + + $zinc->add('text', $main_group, + -position => [10, 170], + -color => 'white', + -anchor => 'w', + -text => "Wind"); + + $fields = ['dir', 'speed','mean_aspeed', 'stddev']; + ($y, $dy) = ( 190, 20); + foreach my $field (@{$fields}) { + $self->{'text_'.$field} = $zinc->add('text', $main_group, + -position => [20, $y], + -color => 'white', + -anchor => 'w', + -text => "$field"); + $y+=$dy; + } + + $self->{button_clear_wind} = DigiKit::Button->new(-widget => $zinc, + -parentgroup => $main_group, + -style => ['Aqualike', + -width => 60, + -height => 20, + -color => 'green', + -text => 'clear', + -trunc => 'right', + ], + -position => [10, 262], + ); + $self->{button_toggle_wind} = DigiKit::Button->new(-widget => $zinc, + -parentgroup => $main_group, + -style => ['Aqualike', + -width => 60, + -height => 20, + -color => 'green', + -text => 'start', + -trunc => 'left', + ], + -position => [70, 262], + ); + $self->{button_clear_wind}->configure(-releasecommand => sub { + $self->notify('WIND_COMMAND', 'clear'); + } + ); + $self->{button_toggle_wind}->configure(-releasecommand => sub { + if ($self->{wind_running}) { + $self->{button_toggle_wind}->value('start'); + $self->notify('WIND_COMMAND', 'stop'); + $self->{wind_running} = 0; + } + else { + $self->{button_toggle_wind}->value('stop'); + $self->notify('WIND_COMMAND', 'start'); + $self->{wind_running} = 1; + } + } + ); + +} + diff --git a/sw/ground_segment/cockpit/Paparazzi/LensScale.pm b/sw/ground_segment/cockpit/Paparazzi/LensScale.pm new file mode 100644 index 00000000000..e3ad9317b5b --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/LensScale.pm @@ -0,0 +1,136 @@ +package Paparazzi::LensScale; +use Paparazzi::Scale; +@ISA = ("Paparazzi::Scale"); +use strict; + +use POSIX; +use Subject; + +use constant DECIMAL_SPACING => 18; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec( + -vz => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + ); +} + +#sub completeinit { +# my $self = shift; +# $self->SUPER::completeinit; +## $self->build_gui(); +#} + +use constant VZ_WIDTH => 8; +use constant MAX_VZ => 2. ; + +sub vz { + my ($self, $old_vz, $new_vz) = @_; + return unless defined $old_vz; + my $zinc = $self->get('-zinc'); + my $h = $self->get('-height'); + my $y = $new_vz * $h / 2 / MAX_VZ; + $zinc->coords($self->{vz_itemclip}, [0, 0, VZ_WIDTH, -$y]); + +} + +sub value() { + my ($self, $previous, $new) = @_; + $self->SUPER::value($previous, $new); + my $zinc = $self->get('-zinc'); + my $int_part = POSIX::floor($new); + + $zinc->itemconfigure ($self->{fixed_text}, + -text => sprintf("%.0f.", $int_part)); + my $decimal = POSIX::floor(($new*10))%10; + my $new_y = - $decimal * DECIMAL_SPACING; + $zinc->treset($self->{decimal_group}); + $zinc->translate($self->{decimal_group}, 0, $new_y); +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + $self->SUPER::build_gui; + my $parent_grp = $self->get('-parent_grp'); + $self->{lens_group} = $zinc->add('group', $parent_grp, -visible => 1); + + my ($xorg, $yorg) = $self->get('-origin'); + my $w = $self->get('-width'); + my $h = $self->get('-height'); + my $rc_pc = $self->get('-fig_clm_pc'); + my $h1 = 30; + my $h2 = 50; + my $x2 = $rc_pc * $w; + my $xt = $x2 + 3; + my $y1 = ($h - $h2)/2; + my $y2 = ($h - $h1)/2; + my $y3 = ($h + $h1)/2; + my $y4 = ($h + $h2)/2; + +# print "foo $xorg $yorg $w $h\n"; + + $zinc->coords($self->{lens_group}, [$xorg, $yorg]); + + $zinc->add('rectangle', $self->{lens_group} , + [0, $y2, $w, $y3], + -linewidth => 0, + -filled => 1, + -fillcolor => 'black', + ); + + $zinc->add('curve', $self->{lens_group}, + [0, $y2, + $x2, $y2, + $x2, $y1, + $w - VZ_WIDTH, $y1, + $w - VZ_WIDTH, $y4, + $x2, $y4, + $x2, $y3, + 0, $y3], + -linewidth => 2, + -linecolor => 'yellow', + -filled => 0); + + my $font = '-adobe-helvetica-bold-o-normal--16-240-100-100-p-182-iso8859-1'; + + $self->{fixed_text} = $zinc->add('text', $self->{lens_group}, + -position => [$xt, $h/2], + -color => 'white', + -font => $font, + -anchor => 'e', + -text => "00."); + + $self->{decimal_clipping_group} = $zinc->add('group', $self->{lens_group}, -visible => 1); + $self->{itemclip} = $zinc->add('rectangle', $self->{decimal_clipping_group}, [0, $y1, $w, $y4], + -visible => 0); + $zinc->itemconfigure($self->{decimal_clipping_group}, -clip => $self->{itemclip}); + $self->{decimal_group} = $zinc->add('group', $self->{decimal_clipping_group}, -visible => 1); + for (my $i=-1; $i< 11; $i++) { + $zinc->add('text', $self->{decimal_group}, + -position => [$xt, $h/2 + $i * DECIMAL_SPACING], + -color => 'white', + -font => $font, + -anchor => 'w', + -text => sprintf("%1d", $i%10) + ); + } + + $self->{vz_clipping_group} = $zinc->add('group', $self->{lens_group}, -visible => 1); + $zinc->coords($self->{vz_clipping_group}, [$w - VZ_WIDTH + 1, $h/2]); + $self->{vz_itemclip} = $zinc->add('rectangle', $self->{vz_clipping_group}, [0, -$h/2, VZ_WIDTH, $h/2], + -visible => 0); + $zinc->itemconfigure($self->{vz_clipping_group}, -clip => $self->{vz_itemclip}); + $self->{vz_group} = $zinc->add('group', $self->{vz_clipping_group}, -visible => 1); + + $zinc->add('rectangle', $self->{vz_group} , + [0, -$h/2, VZ_WIDTH, $h/2], + -linewidth => 0, + -filled => 1, + -fillcolor => "=axial 90 |red;150 50|green;150 50", -filled => 1, +# -fillcolor => 'red', + ); + + # print "in LensScale::build_gui\n"; +} diff --git a/sw/ground_segment/cockpit/Paparazzi/MapView.pm b/sw/ground_segment/cockpit/Paparazzi/MapView.pm new file mode 100644 index 00000000000..4c331f4f8a3 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/MapView.pm @@ -0,0 +1,479 @@ +package Paparazzi::MapView; + +use Tk; +#use Tk::widgets qw/PNG/; +#use Tk::JPEG; +use Tk::Zinc; +use XML::DOM; +use Math::Trig; +require File::Basename; + +use base "Tk::Frame"; +use strict; + +Construct Tk::Widget 'MapView'; + +sub ClassInit { + my ($class, $mw) = @_; + $class->SUPER::ClassInit($mw); +} + +sub Populate { + my ($self, $args) = @_; + my $render = $args->{-render}; + delete $args->{-render}; + $self->SUPER::Populate($args); + my $zinc = $self->Zinc(-backcolor => 'black', + -borderwidth => 3, + -relief => 'sunken', + -render => $render, + -trackmanagedhistorysize => 500, + -trackvisiblehistorysize => 500 + ); + $zinc->pack(-fill => 'both', -expand => "1"); + $self->Advertise('zinc' => $zinc); + $self->build_gui(); + $self->{tracks} = {}; + $self->set_bindings(); +} + +use constant SCALE_LEN => 200; +use constant SCALE_HEIGHT => 5; +use constant SCALE_X => 50; +use constant SCALE_Y => 1000; + +sub build_gui { + my ($self) = @_; + my $zinc = $self->Subwidget('zinc'); + $self->{main_group} = $zinc->add('group', 1, -visible => 1); + + $self->{pan_group} = $zinc->add('group', $self->{main_group}, -visible => 1); + $self->{zoom_group} = $zinc->add('group', $self->{pan_group}, -visible => 1); + # map + $self->{map_picture_group} = $zinc->add('group', $self->{zoom_group}, -visible => 1); + # waypoints + $self->{map_wp_group} = $zinc->add('group', $self->{zoom_group}, -visible => 1 ); + # track + $self->{map_trajectory_group} = $zinc->add('group', $self->{zoom_group}, -visible => 1); + $self->{map_track_group} = $zinc->add('group', $self->{zoom_group}, -visible => 1); + + # scale + $self->{scale_group} = $zinc->add('group', $self->{main_group}, -visible => 1); + $self->{scale_item} = $zinc->add('rectangle', $self->{scale_group}, + [SCALE_X, SCALE_Y, SCALE_X+SCALE_LEN, SCALE_Y + SCALE_HEIGHT], + -visible => 1, + -filled => 1, + -fillcolor => 'black'); + $self->{scale_text_item} = $zinc->add('text', $self->{main_group}, + -position => [SCALE_X, SCALE_Y - SCALE_HEIGHT], + -anchor => 'w', + -text => "unknown"); + $zinc->configure(-overlapmanager => $self->{map_track_group} ); + +} + +sub set_bindings { + my ($self) = @_; + my $zinc = $self->Subwidget('zinc'); + my $map_grp = $self->{pan_group}; + $zinc->Tk::bind('', [\&mouse_zoom, $self, 1.15]); + $zinc->Tk::bind('', [\&mouse_zoom, $self, 0.75]); + $zinc->Tk::bind('', [\&drag_start, $map_grp, \&drag_motion]); + $zinc->Tk::bind('', [\&drag_stop]); + $zinc->Tk::bind('', [\&show_pos_cbk, $self]); + $self->parent()->Tk::bind('', [\&dec_zoom, $self]); + $self->parent()->Tk::bind('', [\&inc_zoom, $self]); + $self->parent()->Tk::bind('', [\&clear_track, $self]); + $self->parent()->Tk::bind('', [\&clear_track, $self]); + $self->parent()->Tk::bind('', [\&scroll, $self]); + $self->parent()->Tk::bind('', [\&scroll, $self]); + $self->parent()->Tk::bind('', [\&scroll, $self]); + $self->parent()->Tk::bind('', [\&scroll, $self]); +} + +sub load_flight_plan { + my ($self, $xmldata) = @_; + my $parser = XML::DOM::Parser->new(); + my $doc = $parser->parse($xmldata); + + my $flight_plan = $doc->getElementsByTagName('flight_plan')->[0]; + + my $waypoints = $doc->getElementsByTagName('waypoints')->[0]; + $self->{NAV_UTM_EAST0} = $waypoints->getAttribute('utm_x0'); + $self->{NAV_UTM_NORTH0} = $waypoints->getAttribute('utm_y0'); + + foreach my $wp ($doc->getElementsByTagName('waypoint')) { + my ($wp_name, $wp_x_mission, $wp_y_mission, $wp_alt) = + ( $wp->getAttribute('name'), + $wp->getAttribute('x'), + $wp->getAttribute('y'), + $wp->getAttribute('alt')); + my @wp_map = $self->map_of_mission([$wp_x_mission, $wp_y_mission]); + + my $item = $self->Subwidget('zinc')->add( 'waypoint', $self->{map_wp_group}, 1, + -position => \@wp_map, + -labelformat => 'x20x18+0+0', + ); + $self->Subwidget('zinc')->itemconfigure($item, 0, + -text => "$wp_name", + ); + } +} + +sub load_map { + my ($self, $xml_map) = @_; + my $parser = XML::DOM::Parser->new(); + my $doc = $parser->parsefile($xml_map); + my $map_node = $doc->getElementsByTagName('map')->[0]; + my $projection = $map_node->getAttribute('projection'); + my $points = $doc->getElementsByTagName('point'); + my @refpoints; + foreach my $i (0..2) { + my $p = $points->[$i]; + $refpoints[$i]->{map} = [$p->getAttribute('x'), $p->getAttribute('y')]; + $refpoints[$i]->{geo} = [$p->getAttribute('utm_x'), $p->getAttribute('utm_y')]; + } + foreach my $i ('map', 'geo') { + $self->{cal_0}->{$i} = $refpoints[0]->{$i}; + $self->{cal_0X}->{$i} = [subst_c2d($refpoints[1]->{$i}, $refpoints[0]->{$i})]; + $self->{cal_0Y}->{$i} = [subst_c2d($refpoints[2]->{$i}, $refpoints[0]->{$i})]; + $self->{cal_det_OX_0Y}->{$i} = vect_prod_c2d($self->{cal_0X}->{$i}, $self->{cal_0Y}->{$i}); + } + + my $data_dir = File::Basename::dirname($xml_map); + $data_dir =~ /.*\/[^\/]$/; + my $map_filename = $data_dir."/".$map_node->getAttribute('file'); + my $image = $self->Subwidget('zinc')->Photo("bg_picture", -file => $map_filename); + my $img_item = $self->Subwidget('zinc')->add('icon', $self->{map_picture_group}, + -image => $image); +# $self->Subwidget('zinc')->coords($self->{pan_group}, [0, -$image->height()]); + $self->Subwidget('zinc')->treset($self->{pan_group}); +# $self->Subwidget('zinc')->coords($self->{pan_group}, [0 , $image->height()]); +} + +sub geo_of_map { + my ($self, $p_map) = @_; + my @OP = subst_c2d($p_map, $self->{cal_0}->{map}); +# print "OP [@OP]\n"; + my $cx = vect_prod_c2d(\@OP, $self->{cal_0Y}->{map}) / $self->{cal_det_OX_0Y}->{map}; + my $cy = -vect_prod_c2d(\@OP, $self->{cal_0X}->{map}) / $self->{cal_det_OX_0Y}->{map}; +# print "cx cy $cx $cy\n"; + my @result = add_c2d($self->{cal_0}->{geo}, + [add_c2d([scale_c2d($self->{cal_0X}->{geo}, $cx)], + [scale_c2d($self->{cal_0Y}->{geo}, $cy)])]); +# print "result [@result]\n"; + return @result; +} + +sub map_of_geo { + my ($self, $p_geo) = @_; + my @OP = subst_c2d($p_geo, $self->{cal_0}->{geo}); + my $cx = vect_prod_c2d(\@OP, $self->{cal_0Y}->{geo}) / $self->{cal_det_OX_0Y}->{geo}; + my $cy = -vect_prod_c2d(\@OP, $self->{cal_0X}->{geo}) / $self->{cal_det_OX_0Y}->{geo}; +# print "cx cy $cx $cy\n"; + + my @result = add_c2d( $self->{cal_0}->{map}, + [add_c2d([scale_c2d($self->{cal_0X}->{map}, $cx)], + [scale_c2d($self->{cal_0Y}->{map}, $cy)])]); +# print "result [@{$p_geo}] [@result]\n"; + return @result; +} + +sub map_of_mission { + my ($self, $p_mission) = @_; + my $geo_pos = [$p_mission->[0] + $self->{NAV_UTM_EAST0}, + $p_mission->[1] + $self->{NAV_UTM_NORTH0}]; +# print "geo @$geo_pos\n"; + my @result = $self->map_of_geo($geo_pos); +# print "result [@result}\n"; + return @result; +} + +sub set_pos_geo { + my ($self, $pos_utm) = @_; + my $item = $self->Subwidget('zinc')->add('text', $self->{map_trajectory_group}, + -position => [$self->map_of_geo($pos_utm)], + -color => 'blue', + -anchor => 'c', + -text => "."); +} + +sub set_track_geo { + my ($self, $name, $pos_utm) = @_; + return $self->set_track_map($name, [$self->map_of_geo($pos_utm)]); +} + +sub set_track_mission { + my ($self, $name, $pos_xy) = @_; + return $self->set_track_map($name, [$self->map_of_mission($pos_xy)]); +} + + +sub set_track_map { + my ($self, $name, $pos_xy) = @_; + my $zinc = $self->Subwidget('zinc'); + my $track_item = $self->{tracks}->{$name}; + if (not defined $track_item) { + $track_item = $zinc->add( 'track', $self->{map_track_group}, 2, + -position => $pos_xy, + -labelformat => 'x80x18+0+0', + ); + $zinc->itemconfigure($track_item, 0, + -text => "$name", + ); + $zinc->itemconfigure($track_item, +# -filledhistory => 1, +# -circlehistory => 1, +# -mixedhistory => 1, + -symbolcolor => 'green', + -leadercolor => 'green', + -markersize => '10', + -markercolor => 'green', +# -historyvisible => 100, +# -trackvisiblehistorysize => 100, +# -trackmanagedhistorysize => 100, + -historycolor =>'black', + ); + $zinc->itemconfigure($track_item, 0, + -text => "$name", + ); + $self->{tracks}->{$name} = $track_item; + } + else { + $zinc->coords($track_item, $pos_xy); + } + return $track_item; +} + +sub set_picture_mission { + my ($self, $name, $pos_xy, $heading, $scale) = @_; + return $self->set_picture_map($name, [$self->map_of_mission($pos_xy)], $heading, $scale); +} + + + +sub set_picture_map { + my ($self, $name, $pos_xy, $heading, $scale) = @_; + my $zinc = $self->Subwidget('zinc'); + my $track_item = $self->{tracks}->{$name}; + if (not defined $track_item) { + my $_w = 720; my $_h = 570; + my $w = $scale*$_w; + my $h = $scale*$_h; + + + $track_item = $zinc->add( 'rectangle', $self->{map_track_group}, + [$pos_xy->[0] - $w/2, $pos_xy->[1] - $h/2, $pos_xy->[0]+$w/2, $pos_xy->[1]+$h/2] + ); + $zinc->rotate($track_item, $heading, $pos_xy->[0], $pos_xy->[1] ); + + + $self->{tracks}->{$name} = $track_item; + + my $image = $zinc->Photo("$name", -file => $name.".gif"); + my $img_item = $zinc->add('icon', $self->{map_track_group}, + -image => $image); + $zinc->scale($img_item, $scale, $scale); + $zinc->translate($img_item ,$pos_xy->[0] - $w/2 , $pos_xy->[1] -$h/2); + $zinc->rotate($img_item ,$heading, $pos_xy->[0] , $pos_xy->[1]); + + } + else { + $zinc->coords($track_item, $pos_xy); + } + return $track_item; +} + +sub scroll_to_map { + my ($self, $p1_map, $p2_dev) = @_; + my $zinc = $self->Subwidget('zinc'); + + print "p1_map @$p1_map p2_dev @$p2_dev\n"; + + my ($x1_dev, $y1_dev) = $zinc->transform($self->{zoom_group}, 'device',[$p1_map]); + print "$p1_map $x1_dev $y1_dev \n"; + my ($xt, $yt) = subst_c2d($p2_dev, [$x1_dev, $y1_dev]); + $zinc->translate($self->{pan_group}, $xt, $yt); +} + + +sub map_of_dev { + my ($self, $p_dev) = @_; + my $zinc = $self->Subwidget('zinc'); + my ($x_m, $y_m) = $zinc->transform('device', $self->{zoom_group}, [$p_dev]); + print "in map_of_dev @$p_dev -> $x_m, $y_m \n"; + return ($x_m, $y_m); +} + +##### +# +# Callbacks +# +##### + +sub mouse_zoom { + my ($_zinc, $self, $ratio) = @_; + my $zinc = $self->Subwidget('zinc'); + my $ev = $zinc->XEvent(); + my $pointed_on_map = $self->map_of_dev([$ev->x, $ev->y]); + $zinc->scale($self->{pan_group}, $ratio, $ratio); +# $self->scroll_to_map([$pointed_on_map], [$ev->x, $ev->y]); +} + +sub inc_zoom { + my ($zinc, $self) = @_; + my $zzz = $self->Subwidget('zinc'); +# my $ev = $zzz->XEvent(); + $zzz->scale($self->{pan_group}, 1.25, 1.25); +} + +sub adjust_zoom { + my ($binded, $self, $ratio) = @_; + + print ("$binded, $self, $ratio\n"); + + my $zinc = $self->Subwidget('zinc'); # had to bind on frame for keyboard events ??? + $zinc->scale($self->{pan_group}, $ratio, $ratio); + + my $map_tgroup = $self->{zoom_group}; + my $p0 = $zinc->transform('device', $map_tgroup, [0, 0]); + my $p1 = $zinc->transform('device', $map_tgroup, [100, 0]); + + print ("$p0 $p1\n"); + +# my $v = subst_c2d([$p0], [$p1]); +# my $mod = module_c2d([$v]); +# my $r = $mod / SCALE_LEN; +# print "SCALE_LEN -> $mod : $r\n" +} + +sub clear_track { + my ($zinc, $self) = @_; + my $zzz = $self->Subwidget('zinc'); + $zzz->remove($self->{map_trajectory_group}); + $self->{map_trajectory_group} = $zzz->add('group', $self->{zoom_group}, -visible => 1); +# $self->scroll_to_map([1000,200], [250,250]); + + printf ("clear\n"); +} + +my ($x_orig, $y_orig); +sub drag_start { + print "drag start\n"; + my ($zinc, $item, $action) = @_; + my $ev = $zinc->XEvent(); + $x_orig = $ev->x; + $y_orig = $ev->y; + $zinc->Tk::bind('', [$action, $item]); +} + +sub drag_motion { +# print "motion\n"; + my ($zinc, $item) = @_; + my $ev = $zinc->XEvent(); + my $x = $ev->x; + my $y = $ev->y; + $zinc->translate($item, $x-$x_orig, $y-$y_orig); + $x_orig = $x; + $y_orig = $y; + +} + +sub drag_stop { + print "stop\n"; + my ($zinc) = @_; + $zinc->Tk::bind('', ''); +} + +sub show_pos_cbk { + my ($zinc, $self) = @_; + my $ev = $zinc->XEvent(); + my $x = $ev->x; + my $y = $ev->y; + + my $map_tgroup = $self->{zoom_group}; + + my ($x_m, $y_m) = $zinc->transform('device', $map_tgroup, [$x, $y]); + + print "in show_pos_cbk $x $y -> $x_m, $y_m \n"; +} + + +sub scroll { + + +} + + + +### +# +# Geometry.pm +# +### + +sub subst_c2d { + my ($a, $b) = @_; + my @result = map ( {$a->[$_] - $b->[$_]} 0,1); + return @result; +} + +sub add_c2d { + my ($a, $b) = @_; + my @result = map ( {$a->[$_] + $b->[$_]} 0,1); + return @result; +} + +sub module_c2d { + my ($a) = @_; + return sqrt($a->[0]*$a->[0] + $a->[1]*$a->[1]); +} + +sub scale_c2d { + my ($a, $k) = @_; + my @result = map ( {$a->[$_] * $k} 0,1); +} + +sub vect_prod_c2d { + my ($a, $b) = @_; + return $a->[0] * $b->[1] - $b->[0] * $a->[1]; +} + +### +# +# Projections +# +### + + + +#sub utm_of_geo { +# my ($pos_lon, $pos_lat) = @_; + +# let ellipsoid = ellipsoid_of geo in +# my $k0 = 0.9996; +# my $xs = 500000; +# my $ys = (phi > 0.) ? 0 : 10000000; +# let lambda_deg = truncate (floor ((Rad>>Deg)lambda)) in +# let zone = (lambda_deg + 180) / 6 + 1 in +# let lambda_c = (Deg>>Rad) (float (lambda_deg - lambda_deg mod 6 + 3)) in +# let e = ellipsoid.e +# and n = k0 *. ellipsoid.a in +# let ll = latitude_isometrique phi e +# and dl = lambda -. lambda_c in +# let phi' = asin (sin dl /. cosh ll) in +# let ll' = latitude_isometrique phi' 0. in +# let lambda' = atan (sinh ll /. cos dl) in +# let z = C.make lambda' ll' +# and c = serie5 coeff_proj_mercator e in +# let z' = ref (C.scal c.(0) z) in +# for k = 1 to Array.length c - 1 do +# z' := C.add !z' (C.scal c.(k) (C.sin (C.scal (float (2*k)) z))) +# done; +# z' := C.scal n !z'; +# { utm_zone = zone; utm_x = xs + truncate (C.im !z'); utm_y = ys + truncate (C.re !z') };; + + + + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/MissionD.pm b/sw/ground_segment/cockpit/Paparazzi/MissionD.pm new file mode 100644 index 00000000000..143c4098334 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/MissionD.pm @@ -0,0 +1,108 @@ +package Paparazzi::MissionD; + +use Tk::ROText; +use XML::DOM; + +use base qw/Tk::Frame/; +use strict; + +Construct Tk::Widget 'MissionD'; + +sub ClassInit { + my ($class, $mw) = @_; + $class->SUPER::ClassInit($mw); +} + +sub Populate { + my ($self, $args) = @_; + $self->SUPER::Populate($args); + my $text = $self->Scrolled('ROText', + -scrollbars => 'osoe', + ); + $text->pack(-fill => 'both', -expand => "1"); + $self->Advertise('text' => $text); + $self->{cur_block} = -1; + $self->{cur_stage} = -1; +# $self->ConfigSpecs(); +# $self->Delegates(); +} + +use Data::Dumper; + +sub get_block_id { + my ($no_block) = @_; + return "block_".$no_block; +} + +sub get_stage_id { + my ($no_block, $no_stage) = @_; + return "stage_".$no_block."_".$no_stage; +} + +sub set_block_and_stage { + my ($self, $new_block, $new_stage) = @_; + my $text = $self->Subwidget('text'); + if ($self->{cur_block} != $new_block) { + $text->tagConfigure(get_block_id($self->{cur_block}), -background => undef); + $text->tagConfigure(get_block_id($new_block), -background => 'green3'); + $text->tagConfigure(get_stage_id($self->{cur_block}, $self->{cur_stage}), -background => undef); + $text->tagConfigure(get_stage_id($new_block, $new_stage), -background => 'green1'); + $self->{cur_block} = $new_block; + $self->{cur_stage} = $new_stage; + } + else { + if ($self->{cur_stage} != $new_stage) { + $text->tagConfigure(get_stage_id($self->{cur_block}, $self->{cur_stage}), -background => undef); + $text->tagConfigure(get_stage_id($self->{cur_block}, $new_stage), -background => 'green1'); + $self->{cur_stage} = $new_stage; + } + } +} + +sub load_flight_plan { + my ($self, $xmldata) = @_; + + my $text = $self->Subwidget('text'); + if (Tk::Exists($text)) { + $text->delete('0.0', 'end'); + } + + my $parser = XML::DOM::Parser->new(); + my $doc = $parser->parse($xmldata); + + my ($blocks, $blocks_stages); + + foreach my $stage ($doc->getElementsByTagName('stage')) { + my $block_name = $stage->getAttribute('block_name'); + my $block_no = $stage->getAttribute('block'); + my $stage_no = $stage->getAttribute('stage'); + my $stage_text = ""; + my $stage_kids = $stage->getChildNodes(); + foreach my $kid (@{$stage_kids}) { + $stage_text = $stage_text.$kid->toString() if $kid->getNodeType() != TEXT_NODE; + } + $blocks_stages->{$block_name}->{$stage_text} = get_stage_id($block_no, $stage_no); + $blocks->{$block_name} = get_block_id($block_no) unless defined $blocks->{block_name}; + } + + # print Dumper(\$blocks); + # print Dumper(\$blocks_stages); + + foreach my $block ($doc->getElementsByTagName('block')){ + my $block_name = $block->getAttribute('name'); + foreach my $line (split (/(\n)/, $block->toString())) { + my $key = $line; + $key =~ s/^\s*//; # remove any leading whitespace + $key =~ s/\s*$//; # remove any trailing whitespace + if ($key ne "") { + my $block_id = $blocks->{$block_name}; + my $stage_id = $blocks_stages->{$block_name}->{$key}; + my $tags = [$block_id]; + push(@{$tags}, ($stage_id)) if defined $stage_id; + $self->Subwidget('text')->insert('end', $line."\n", $tags); + } + } + } +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/ND.pm b/sw/ground_segment/cockpit/Paparazzi/ND.pm new file mode 100644 index 00000000000..cb1a34512a0 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/ND.pm @@ -0,0 +1,132 @@ +package Paparazzi::ND; +use Subject; +@ISA = ("Subject"); + +use strict; + +use Math::Trig; +use Tk; +use Tk::Zinc; +use Paparazzi::SatPage; +use Paparazzi::EnginePage; +use Paparazzi::APPage; +use Paparazzi::IRPage; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -page => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, "gps"], + -engine_status => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, undef], + -ap_status => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, undef], + -wind => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, undef], + -lls => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, undef], + -sats => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, undef], + -fix => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, undef], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit(); + $self->build_gui(); + $self->configure('-pubevts' => 'WIND_COMMAND'); +} + +sub page { + my ($self, $old_val, $new_val) = @_; + print "in ND::page [$new_val]\n"; + return unless defined $new_val and defined $self->{sat_view}; +# $self->{sat_view}->configure('-visible' => $i%2); +# $i++; +} + +sub put_lls { + my ($self, $value) = @_; +# $self->{IR}->put_lls($value); +} + +sub build_gui() { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $width = $self->get('-width'); + my $height = $self->get('-height'); + my $origin = $self->get('-origin'); + + $self->{main_group} = $zinc->add('group', 1, -visible => 1); + $zinc->coords($self->{main_group}, $origin); + $zinc->add('rectangle', $self->{main_group}, + [1, 1, $width-2, $height-2], + -visible => 1, + -filled => 0, + -linecolor => 'red'); + my ($margin, $page_width) = (5, 300); + my $real_width = $page_width - 2*$margin; + my ($page_per_row, $row, $col) = (2, 0, 0); + + my $pages = ['Sat', 'Engine','AP', 'IR']; + foreach my $page (@{$pages}) { + $self->{$page} = $self->component('Paparazzi::'.$page.'Page', + -zinc => $zinc, + -parent_grp => $self->{main_group}, + -origin => [ $margin+$col*$page_width, $margin+$row*$page_width], + -width => $real_width, + -height => $real_width, + -visible => 1, + ); + # $self->{$page} = ('Paparazzi::'.$page.'Page')->new( +# -zinc => $zinc, +# -parent_grp => $self->{main_group}, +# -origin => [ $margin+$col*$page_width, $margin+$row*$page_width], +# -width => $real_width, +# -height => $real_width, +# -visible => 1, +# ); + + if ($page eq "IR") { + $self->{$page}->attach($self, 'WIND_COMMAND', [sub { my ($self, $component, $signal, $arg) = @_; + $self->notify('WIND_COMMAND', $arg)}]); + } + $col++; + unless ($col lt $page_per_row) { $col=0; $row++ }; + } + + + my $sat_h = + { + -itow => 12345, + -nch => 7, + -sats => [ { -chn => 0 , -svid => 3, -flags => 0x00, -qi => 0, -cno => 0, -elev => 45, -azim => 315, -prres => 0.}, + { -chn => 1 , -svid => 22, -flags => 0x01, -qi => 0, -cno => 45.2, -elev => 35, -azim => 300, -prres => 0.}, + { -chn => 2 , -svid => 1, -flags => 0x00, -qi => 0, -cno => 36.6, -elev => 6, -azim => 315, -prres => 0.}, + { -chn => 3 , -svid => 25, -flags => 0x01, -qi => 0, -cno => 45.2, -elev => 45, -azim => 237, -prres => 0.}, + { -chn => 4 , -svid => 6, -flags => 0x01, -qi => 0, -cno => 45.8, -elev => 58, -azim => 61, -prres => 0.}, + { -chn => 5 , -svid => 17, -flags => 0x01, -qi => 0, -cno => 43.8, -elev => 31, -azim => 123, -prres => 0.}, + { -chn => 6 , -svid => 30, -flags => 0x01, -qi => 0, -cno => 42.2, -elev => 53, -azim => 161, -prres => 0.}, + ] + }; + $self->{Sat}->configure( -sats => $sat_h); + $self->{Sat}->configure( -fix => 30.); + +# my $engine_h = { -nb_engine => 2, +# -engine => [{throttle => 50, -rpm => 3500, -temp => 39}, +# {throttle => 50, -rpm => 3400, -temp => 37}], +# -bat => 11.5, +# -energy => 25.2 +# }; + +# my $ap_h = { +# -mode => 1, +# -h_mode => 2, +# -v_mode => 0, +# -target_climb => 1., +# -target_alt => 200., +# -target_heading => 36., +# }; +# $self->{AP}->configure( -ap_status => $ap_h); +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/NDPage.pm b/sw/ground_segment/cockpit/Paparazzi/NDPage.pm new file mode 100644 index 00000000000..2ca48c5699f --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/NDPage.pm @@ -0,0 +1,62 @@ +package Paparazzi::NDPage; + +use Subject; +@ISA = ("Subject"); +use strict; + + +use Tk; +use Tk::Zinc; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -parent_grp => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -title => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -visible => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, "1"], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; + $self->build_gui(); +} + +sub visible { + my ($self, $old_val, $new_val) = @_; + print "in EngineView::visible $new_val\n"; + return unless defined $new_val and defined $self->{main_group}; + my $zinc = $self->get('-zinc'); + $zinc->itemconfigure ($self->{main_group}, + -visible => $new_val, + ); +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $width = $self->get('-width'); + my $height = $self->get('-height'); + my $origin = $self->get('-origin'); + my $parent_grp = $self->get('-parent_grp'); + $self->{main_group} = $zinc->add('group', $parent_grp, -visible => 1); + $zinc->coords($self->{main_group}, $origin); + $zinc->add('rectangle', $self->{main_group}, [0, 0, $width, $height], + -visible => 1, -linecolor => "green"); + my $vmargin = $self->get('-height')*0.1; + $self->{vmargin} = $vmargin; + my $hmargin = 10; + $zinc->add('text', $self->{main_group}, + -position => [$hmargin, $vmargin/2], + -anchor => 'w', + -text => scalar $self->get('-title'), + -color => 'white', + ); +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/PFD.pm b/sw/ground_segment/cockpit/Paparazzi/PFD.pm new file mode 100644 index 00000000000..dc1125dcc91 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/PFD.pm @@ -0,0 +1,199 @@ + +package Paparazzi::PFD; +use Subject; +@ISA = ("Subject"); + +use strict; + +use Math::Trig; +use Tk; +use Tk::Zinc; + +use Paparazzi::Scale; +use Paparazzi::LensScale; +use Paparazzi::Horizon; +use Paparazzi::PFD_Panel; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec + (-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + + -roll => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + -pitch => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + + -speed => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -target_speed => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -alt => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -target_alt => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -heading => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -target_heading => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -vz => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + + -ap_mode => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + -gps_mode => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + -lls_mode => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + -lls_value => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + -ctrst_mode => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + -ctrst_value => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + -rc_mode => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + -if_mode => [S_NOINIT, S_PRPGONLY, S_RDWR, S_OVRWRT, S_CHILDREN, 0], + + -nav_dist_wp => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -wind => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit(); + + $self->build_gui(); +# $self->configure( -speed => 9); +# $self->configure( -target_speed => 10); +# $self->configure( -alt => 140); +# $self->configure( -target_alt => 150); +# $self->configure( -roll => 20); +# $self->configure( -pitch => 10); + $self->configure('-pubevts' => 'SHOW_PAGE'); +} + +sub nav_dist_wp { + my ($self, $previous_d, $new_d) = @_; + my $str2 = sprintf("dtwp %.0f m", sqrt($new_d)); + $self->get('-zinc')->itemconfigure( $self->{nav_tab}, 2, + -text => $str2, + -color => 'white', + ); +} + +sub wind { + my ($self, $previous_w, $new_w) = @_; +# my ($dir, $speed, $mean_as, $stddev) = $new_w; + + if ($new_w != 0) { + my $dir_deg = (rad2deg($new_w->{-dir}) + 180)% 360; + my $wind1_str = sprintf ("%.0fdeg %.1f m/s", $dir_deg, $new_w->{-speed}); + my $wind2_str = sprintf ("mas %.1f m/s (%.2f)", $new_w->{-mean_as}, $new_w->{-stddev}); + $self->get('-zinc')->itemconfigure( $self->{wind_tab}, 1, + -text => $wind1_str, + -color => 'white', + ); + $self->get('-zinc')->itemconfigure( $self->{wind_tab}, 2, + -text => $wind2_str, + -color => 'white', + ); + } +} + +sub onPanelCLicked { + print "in PFD::onPanelClicked\n"; + my ($self, $component, $signal, $page) = @_; + print "$signal, $page\n"; + $self->notify('SHOW_PAGE', $page); +} + +sub min { + my ($a, $b) = @_; + return $a le $b ? $a : $b; +} +sub build_gui() { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $width = $self->get('-width'); + my $height = $self->get('-height'); + my $origin = $self->get('-origin'); + + $self->{main_group} = $zinc->add('group', 1, -visible => 1); + $zinc->coords($self->{main_group}, $origin); + my ($p_x, $p_y, $p_w, $p_h) = (0, 0.02*$width, $width, 0.12*$height); + my $component = $self->component('Paparazzi::PFD_Panel', + -zinc => $zinc, + -parent_grp => $self->{main_group}, + -origin => [$p_x, $p_y], + -width => $p_w, + -height => $p_h, + ); + $component->attach($self, 'CLICKED', ['onPanelCLicked']); + + + my ($c_x, $c_y, $radius) = ($width/2, $height/2, 0.3*min($width, $height)); + $self->component('Paparazzi::Horizon', + -zinc => $zinc, + -parent_grp => $self->{main_group}, + -origin => [$c_x, $c_y], + -radius => $radius, + ); + + $self->{speed_scale} = Paparazzi::Scale->new( -zinc => $zinc, + -parent_grp => $self->{main_group}, + -origin => [ 0.1*$width, 0.25*$height], + -width => 0.15*$width, + -height => 0.56*$height, + -min_val => 0, + -max_val => 40, + -tick_scale => 1, + -repeat_legend => 2, + ); + $self->connectoptions(-speed, S_TO, [$self->{speed_scale}, -value]); + $self->connectoptions(-target_speed, S_TO, [$self->{speed_scale}, -target_value]); + $self->{heading_scale} = Paparazzi::Scale->new( -zinc => $zinc, + -parent_grp => $self->{main_group}, + -origin => [ 0.25*$width, 0.96*$height], + -width => 0.5*$width, + -height => 0.08*$height, + -direction => -1, + -periodic => 1, + -min_val => 0, + -max_val => 360, + -tick_scale => 5, + -repeat_legend => 3, + ); + $self->connectoptions(-heading, S_TO, [$self->{heading_scale}, -value]); + $self->connectoptions(-target_heading, S_TO, [$self->{heading_scale}, -target_value]); + $self->{alt_scale} = Paparazzi::LensScale->new( -zinc => $zinc, + -parent_grp => $self->{main_group}, + -origin => [0.8*$width, 0.25*$height], + -width => 0.15*$width, + -height => 0.56*$height, + -min_val => 0, + -max_val => 3000, + -tick_scale => 5, + -repeat_legend => 2, + -fig_clm_pc => 0.6, + ); + $self->connectoptions(-alt, S_TO, [$self->{alt_scale}, -value]); + $self->connectoptions(-target_alt, S_TO, [$self->{alt_scale}, -target_value]); + $self->connectoptions(-vz, S_TO, [$self->{alt_scale}, -vz]); + # wind informations + my $labelformat = '150x250 x140x20+20+10 x140x20^0>0 x140x20^0>1 x140x20^0>2 x140x20^0>3'; + my $f = '-adobe-helvetica-bold-o-normal--16-240-100-100-p-182-iso8859-1'; + $self->{wind_tab} = $zinc->add('tabular',$self->{main_group}, 5, + -position => [0, + 600], + -labelformat => $labelformat, + ); + $zinc->itemconfigure ( $self->{wind_tab}, 0, + -font => $f, + -color => 'white', + -text => 'Wind', + ); + # nav informations + $self->{nav_tab} = $zinc->add('tabular',$self->{main_group}, 5, + -position => [000, + 600], + -labelformat => $labelformat, + ); + $zinc->itemconfigure ( $self->{nav_tab}, 0, + -font => $f, + -color => 'white', + -text => 'NAV', + ); +} + + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/PFD_Panel.pm b/sw/ground_segment/cockpit/Paparazzi/PFD_Panel.pm new file mode 100644 index 00000000000..2cefa59d2f5 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/PFD_Panel.pm @@ -0,0 +1,237 @@ +# The zone above the artificial horizon which gives informations on ap mode, GPS etc.. +#============================================================================= +package Paparazzi::PFD_Panel; +use Subject; +@ISA = ("Subject"); + +use Tk; +use Tk::Zinc; +use Data::Dumper; + +use strict; +sub populate { + my ($self, $args) = @_; + + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -parent_grp => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -gps_mode => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -ap_mode => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -rc_mode => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -ctrst_mode => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -ctrst_value => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -lls_mode => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -lls_value => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -if_mode => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], +# -pubevts => [S_NEEDINIT, S_PASSIVE, S_RDWR, S_APPEND, S_NOPRPG,[]] + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit(); + $self->{modes} = [ { name => 'rc', + str => ["lost","ok", "really lost", "not possible"], + color => ["orange", "green", "red", "red"] + }, + { name => 'cal', + str => ["unkwn", "wait", "ok"], + color =>["red", "orange", "green"] + }, + { name => 'ap', + str => ["manual", "auto1", "auto2", "home"], + color =>["green", "green", "green", "orange"] + }, + { name => 'gps', + str => [ "No fix", + "dead reckoning only", + "2D-fix", + "3D-fix", + "GPS + dead reckoning combined"], + color => ["red", "red", "orange", "green", "orange"] + }, + { name => 'lls', + str => ["OFF" , "ON"], + color =>["orange", "green"] + }, + { name => 'if', + str => ["none", "down", "up"], + color =>["green", "orange", "orange"] + } + ]; + $self->{modes_by_name} = {}; + foreach my $mode (@{$self->{modes}}) { + $self->{modes_by_name}->{$mode->{name}} = $mode; + } + $self->build_gui(); + $self->configure('-pubevts' => 'CLICKED'); +} + + +sub build_gui { + my ($self) = @_; + + my $zinc = $self->get('-zinc'); + my $parent_grp = $self->get('-parent_grp'); + $self->{main_group} = $zinc->add('group', $parent_grp, -visible => 1); + my @origin = $self->get('-origin'); + $zinc->coords($self->{main_group}, \@origin); + + + my $modes = $self->{modes}; + + my $nb_col = $#$modes+1; + +# print "nb_col $nb_col\n"; + + my $w = $self->get('-width'); + my $h = $self->get('-height'); + my $i; + for ($i=1; $i<$nb_col; $i++) { + my $x = $i / $nb_col * $w; + $zinc->add('curve', $self->{main_group}, + [$x, 10, $x, $h +10], + -linewidth => 2, + -linecolor => 'white', + -filled => 0); + } + + my $f = '-adobe-helvetica-bold-o-normal--16-240-100-100-p-182-iso8859-1'; + my $labelformat = "100x200 x80x20+20+10 x80x20^0>0 x80x20^0>1"; + my @tab_args = ('tabular',$self->{main_group}, 3, + -labelformat => $labelformat,); + my @tab_style = ( -font => $f, + -color => 'green'); + my $x=-10; + my $dx = $self->get('-width') / $nb_col; + foreach my $mode (@{$self->{modes}}) { + $mode->{tabular} = $zinc->add(@tab_args, -position => [$x, -5] ); + $zinc->itemconfigure ($mode->{tabular}, 0, @tab_style, -text => uc $mode->{name}); + $zinc->bind($mode->{tabular},''=> [\&onRectClicked, $self, $mode->{name}]); + $x += $dx; + } + + + $self->{rect_group} = $zinc->add('group', $self->{main_group}, -visible => 1); + my $rect = $zinc->add('rectangle', $self->{main_group}, [0, 0, $w, $h], + -visible => 0, + -linecolor => 'white', + -filled => '1', + ); +# $zinc->bind($rect,''=> [\&onRectClicked, $self, "coucou"]); + +} + + + +sub onRectClicked { + my ($zinc, $self, $name) = @_; + print "onRectClicked : $name\n"; + $self->notify('CLICKED', $name); +} + + + +sub set_mode { + my ($self, $name, $previous_val, $new_val) = @_; + my $mode = $self->{modes_by_name}->{$name}; + if (defined $mode) { + if (!defined $previous_val || $previous_val != $new_val) { + my $zinc = $self->get('-zinc'); + $zinc->itemconfigure( $mode->{tabular}, 1, + -text => $mode->{str}[$new_val], + -color =>$mode->{color}[$new_val], + ); + } + } +} + +sub gps_mode() { + my ($self, $previous_mode, $new_mode) = @_; + $self->set_mode("gps", $previous_mode, $new_mode); + +} + +sub ap_mode() { + my ($self, $previous_mode, $new_mode) = @_; + $self->set_mode("ap", $previous_mode, $new_mode); +} + +sub rc_mode { + my ($self, $previous_mode, $new_mode) = @_; + $self->set_mode("rc", $previous_mode, $new_mode); +} + +sub lls_mode { + my ($self, $previous_mode, $new_mode) = @_; + $self->set_mode("lls", $previous_mode, $new_mode); +} + +sub if_mode { + my ($self, $previous_mode, $new_mode) = @_; + $self->set_mode("if", $previous_mode, $new_mode); +} + +sub lls_value { + my ($self, $previous_val, $new_val) = @_; + my $mode = $self->{modes_by_name}->{lls}; + if (defined $mode) { + if (!defined $previous_val || $previous_val != $new_val) { + my $zinc = $self->get('-zinc'); + my $str_val = sprintf ("%.4f", $new_val); + $zinc->itemconfigure( $mode->{tabular}, 2, + -text => $str_val, + -color => "green", + ); + } + } +} + + + +sub ctrst_mode { + my ($self, $previous_mode, $new_mode) = @_; + $self->set_mode("ctrst", $previous_mode, $new_mode); +} + +sub ctrst_value { + my ($self, $previous_val, $new_val) = @_; + my $mode = $self->{modes_by_name}->{ctrst}; + if (defined $mode) { + if (!defined $previous_val || $previous_val != $new_val) { + my $zinc = $self->get('-zinc'); + my $str_val = sprintf ("%.4f", $new_val); + $zinc->itemconfigure( $mode->{tabular}, 2, + -text => $str_val, + -color => "green", + ); + } + } +} + + +sub setLabelContent { + my ($self, $item, $labelformat) = @_; + + my @fieldsSpec = split (/ / , $labelformat); + shift @fieldsSpec; + + my $i=0; + foreach my $fieldSpec (@fieldsSpec) { + my ($posSpec) = $fieldSpec =~ /^.\d+.\d+(.*)/ ; + print "$fieldSpec\t$i\t$posSpec\n"; + $self->{zinc}->itemconfigure ($item,$i, + -text => "$i: $posSpec", + -border => "contour", + -color => 'green', + -backcolor => 'white', + -filled => 1 + ); + $i++; + } +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/RCSlider.pm b/sw/ground_segment/cockpit/Paparazzi/RCSlider.pm new file mode 100644 index 00000000000..978082cd720 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/RCSlider.pm @@ -0,0 +1,138 @@ +package Paparazzi::RCSlider; +use Subject; +@ISA = ("Subject"); + +use Tk; +use Tk::Zinc; +use Math::Trig; + +use strict; + +use constant CURSOR_WIDTH => 2; +use constant VERTICAL_CONTROL => 0; +use constant HORIZONTAL_CONTROL => 1; + +sub populate { + my ($self, $args) = @_; + + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -len => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -direction => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -name => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -value => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; + $self->build_gui(); +} + +sub value() { + my ($self, $previous_value, $new_value) = @_; + my $zinc = $self->get('-zinc'); + my $len = $self->get('-len'); + my $new_c = $new_value * $len/2; + my $cursor_item = $self->{'cursor_item'}; + $zinc->treset($cursor_item); + if ($self->get('-direction') == VERTICAL_CONTROL) { + $zinc->translate($cursor_item, 0, $new_c); + } + else { + $zinc->translate($cursor_item, $new_c, 0); + } +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $main_group = $zinc->add('group', 1, -visible => 1); + $self->{main_group} = $main_group; + my @origin = $self->get('-origin'); + $zinc->coords($main_group, \@origin); + my $w = $self->get('-width'); + my $l = $self->get('-len'); + my $d = $self->get('-direction'); + my $name = $self->get('-name'); + my $rectangle_coor = ($d == VERTICAL_CONTROL) ? + [-$w/2, 0, $w/2, -$l] : [0, -$w/2, $l, $w/2]; + my $cursor_coor = ($d == VERTICAL_CONTROL) ? + [-$w/2, -$l/2 - CURSOR_WIDTH, $w/2, -$l/2+CURSOR_WIDTH] : + [$l/2 - CURSOR_WIDTH, -$w/2, $l/2+CURSOR_WIDTH, $w/2]; + + $zinc->add('text', $main_group, + -position =>[0, 0], + -color => 'white', + -anchor => ($d == VERTICAL_CONTROL) ? 'n':'e', + -text => $name + ); + $zinc->add('rectangle', $main_group , + $rectangle_coor, + -linewidth => 1, + -linecolor => 'black', + -filled => 1, + -fillcolor => 'white', + ); + my $cursor_item = $zinc->add('rectangle', $main_group , + $cursor_coor, + -linewidth => 1, + -linecolor => 'black', + -filled => 1, + -fillcolor => 'yellow', + ); + $self->{'cursor_item'} = $cursor_item; + $zinc->bind($cursor_item, '' => [\&press, $self, \&motion]); + $zinc->bind($cursor_item, '' => [\&release, $self]); +} + +my ($x_orig, $y_orig); + +sub press { + my ($zinc, $self, $action) = @_; + my $ev = $zinc->XEvent(); + my @origin=$zinc->coords($self->{main_group}); + $x_orig = $ev->x - $origin[0]; + $y_orig = $ev->y - $origin[1]; + $zinc->Tk::bind('', [$action, $self]); + } + +sub motion { + my ($zinc, $self) = @_; + my $ev = $zinc->XEvent(); + my @origin=$zinc->coords($self->{main_group}); + my $x = $ev->x - $origin[0]; + my $y = $ev->y - $origin[1]; + + if ($self->get('-direction') == VERTICAL_CONTROL) { + if ($y > -$self->get('-len') and $y < 0) { + $zinc->translate($self->{cursor_item}, 0, $y-$y_orig); + $y_orig = $y; + } + } + else { + if ($x < $self->get('-len') and $x > 0) { + $zinc->translate($self->{cursor_item}, $x-$x_orig, 0); + $x_orig = $x; + } + } +} + +sub release { + my ($zinc, $self) = @_; + $zinc->Tk::bind('', ''); + my $ev = $zinc->XEvent(); + my @origin=$zinc->coords($self->{main_group}); + my $x = $ev->x - $origin[0]; + my $y = $ev->y - $origin[1]; + my $len = $self->get('-len'); + my $value = Utils::trim($self->get('-direction') == VERTICAL_CONTROL ? + -2 * $y/$len - 1. : 2 * $x/$len - 1, -1., 1.); + print "slider release ( $value )\n"; + +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/RCStick.pm b/sw/ground_segment/cockpit/Paparazzi/RCStick.pm new file mode 100644 index 00000000000..0c084eecfec --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/RCStick.pm @@ -0,0 +1,131 @@ +package Paparazzi::RCStick; +use Subject; +@ISA = ("Subject"); + +use Paparazzi::RCSlider; +use Paparazzi::Utils; + +use Tk; +use Tk::Zinc; +use Math::Trig; + +use strict; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -radius => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -name => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -v_name => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -h_name => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -v_value => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -h_value => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; + $self->build_gui(); + $self->connectoptions('-v_value', S_TO, [$self->{v_slider}, '-value']); + $self->connectoptions('-h_value', S_TO, [$self->{h_slider}, '-value']); +} + +sub h_value() { + my ($self, $previous_value, $new_value) = @_; +# print "in h_value $new_value\n"; +} + +sub v_value() { + my ($self, $previous_value, $new_value) = @_; +} + +use constant WIDTH => 14; + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $radius = $self->get('-radius'); + my @origin = $self->get('-origin'); + my @v_origin = ($origin[0], $origin[1] + $radius); + my @h_origin = ($origin[0] - $radius, $origin[1]); + my $v_name = $self->get('-v_name'); + my $h_name = $self->get('-h_name'); + + $self->{v_slider} = Paparazzi::RCSlider->new( -zinc => $zinc, -origin => \@v_origin, + -width => WIDTH, -len => 2 * $radius, + -direction => Paparazzi::RCSlider::VERTICAL_CONTROL, + -name => $v_name + ); + $self->{h_slider} = Paparazzi::RCSlider->new( -zinc => $zinc, -origin => \@h_origin, + -width => WIDTH, -len => 2 * $radius, + -direction => Paparazzi::RCSlider::HORIZONTAL_CONTROL, + -name => $h_name + ); + + my $main_group = $zinc->add('group', 1, -visible => 1); + $zinc->coords($main_group, \@origin); + $self->{main_group} = $main_group; + + + my $cursor_coor = [ - WIDTH/2, - WIDTH/2, WIDTH/2, WIDTH/2 ]; + + my $cursor_item = $zinc->add('arc', $main_group, + $cursor_coor, + -filled => 1, + -fillcolor => "yellow", + -linewidth => 1, + -linecolor => "black", + -startangle => 0, -extent => 360, + -pieslice => 1, -closed => 1, + ); + $self->{'cursor_item'} = $cursor_item; + $zinc->bind($cursor_item, '' => [\&press, $self, \&motion]); + $zinc->bind($cursor_item, '' =>[\&release, $self]); +} + +my ($x_orig, $y_orig); + + sub press { + my ($zinc, $self, $action) = @_; + my $ev = $zinc->XEvent(); + my @origin=$zinc->coords($self->{main_group}); + $x_orig = $ev->x - $origin[0]; + $y_orig = $ev->y - $origin[1]; + $zinc->Tk::bind('', [$action, $self]); + } + +sub motion { + my ($zinc, $self) = @_; + my $ev = $zinc->XEvent(); + my @origin=$zinc->coords($self->{main_group}); + my $x = $ev->x - $origin[0]; + my $y = $ev->y - $origin[1]; + my $radius = $self->get('-radius'); + + if ($y > -$radius and $y < $radius) { + $zinc->translate($self->{cursor_item}, 0, $y-$y_orig); + $y_orig = $y; + } + if ($x > -$radius and $x < $radius) { + $zinc->translate($self->{cursor_item}, $x-$x_orig, 0); + $x_orig = $x; + } +} + +sub release { + my ($zinc, $self) = @_; + $zinc->Tk::bind('', ''); + my $ev = $zinc->XEvent(); + my @origin=$zinc->coords($self->{main_group}); + my $x = $ev->x - $origin[0]; + my $y = $ev->y - $origin[1]; + my $radius = $self->get('-radius'); + my ($v_value, $h_value) = (Utils::trim($y/$radius, -1., 1.), Utils::trim($x/$radius, -1, 1)); + print "stick release ( $v_value, $h_value ) \n"; + $self->configure('-v_value' => $v_value, '-h_value' => $h_value); +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/RCTransmitter.pm b/sw/ground_segment/cockpit/Paparazzi/RCTransmitter.pm new file mode 100644 index 00000000000..a426957accd --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/RCTransmitter.pm @@ -0,0 +1,84 @@ +package Paparazzi::RCTransmitter; + +use strict; + +use Tk; +use Tk::Zinc; +use XML::DOM; + +use base "Tk::Frame"; +use strict; + +use Paparazzi::RCSlider; +use Paparazzi::RCStick; + +Construct Tk::Widget 'Paparazzi::RCTransmitter'; + +use constant TYPE_SLIDER => 0; +use constant TYPE_STICK => 1; +use constant TYPE_SWITCH => 2; + +use constant VERTICAL_CONTROL => 0; +use constant HORIZONTAL_CONTROL => 1; + +sub ClassInit { + my ($class, $mw) = @_; + $class->SUPER::ClassInit($mw); +} + +sub Populate { + my ($self, $args) = @_; + $self->SUPER::Populate($args); + $self->ConfigSpecs( -filename => ['PASSIVE', undef, undef, undef], + -width => ['PASSIVE', undef, undef, undef], + -height => ['PASSIVE', undef, undef, undef]); + $self->{zinc} = $self->Zinc( +# -width => $args->{-width}, -height => $args->{-height}, + -backcolor => 'black', + -borderwidth => 3, + -relief => 'sunken', + -render => '1'); + $self->{main_group} = $self->{zinc}->add('group', 1, -visible => 1); + + my $filename = $args->{-filename}; + my $parser = XML::DOM::Parser->new(); + my $doc = $parser->parsefile($filename); + foreach my $radio ($doc->getElementsByTagName('radio')) { + my $name = $radio->getAttribute('name'); + print "name $name\n"; + my $img_filename = $doc->getElementsByTagName('photo')->[0]->getAttribute('filename'); + print "name $img_filename\n"; + my $image = $self->{zinc}->Photo("bg_picture", -file => $img_filename); + $args->{-width} = $image->width(); + $args->{-height} = $image->height; + $self->{zinc}->configure(-width => $image->width(), -height => $image->height()); + + my $img_item = $self->{zinc}->add('icon', $self->{main_group}, -image => $image); + foreach my $control ($doc->getElementsByTagName('control')) { + if ($control->getAttribute('type') eq 'stick') { + Paparazzi::RCStick->new( -zinc => $self->{zinc}, + -origin => [ $control->getAttribute('x'), $control->getAttribute('y')], + -radius => $control->getAttribute('size'), + -name => $control->getAttribute('name'), + -v_name => $control->getAttribute('v_axe'), + -h_name => $control->getAttribute('h_axe') + ); + } + elsif ($control->getAttribute('type') eq 'slider') { + Paparazzi::RCSlider->new( -zinc => $self->{zinc}, + -origin => [$control->getAttribute('x'), $control->getAttribute('y')], + -width => 14, -len => $control->getAttribute('size'), + -direction => $control->getAttribute('direction') eq "horizontal"? + HORIZONTAL_CONTROL : VERTICAL_CONTROL, + -name => $control->getAttribute('name') + ); + } + } + } + $self->{zinc}->pack(-fill => 'both', -expand => "1"); +} + + + + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/RotaryGauge.pm b/sw/ground_segment/cockpit/Paparazzi/RotaryGauge.pm new file mode 100644 index 00000000000..b9233bef431 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/RotaryGauge.pm @@ -0,0 +1,124 @@ +package Paparazzi::RotaryGauge; +use Subject; +@ISA = ("Subject"); +use strict; + + +use Tk; +use Tk::Zinc; +use Math::Trig; +use Data::Dumper; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + my ($min_val, $max_val, $tick_spacing, $legend_spacing, $min_val_angle, $dead_sector) = (0, 100, 10, 20, 0, 45); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -parent_grp => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -radius => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -min_val => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, $min_val], + -max_val => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, $max_val], + -tick_spacing => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, $tick_spacing], + -legend_spacing => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, $legend_spacing], + -min_val_angle => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, $min_val_angle], + -dead_sector => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, $dead_sector], + -text => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, ""], + -format => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, "%.1f"], + -value => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; + $self->{ang_by_val} = Utils::rad_of_deg((360. - $self->get('-dead_sector')) / + ( $self->get('-max_val') - $self->get('-min_val'))); + $self->build_gui(); +} + +sub angle_of_value { + my ($self, $value) = @_; + my $min_val_angle = $self->get('-min_val_angle'); + my $aov = Utils::rad_of_deg($min_val_angle) + $self->{ang_by_val} * ($value - $self->get('-min_val')); +# print "angle_of_value $value -> $aov\n"; + return $aov; +} + +sub value { + my ($self, $old_val, $new_val) = @_; + return unless defined $new_val and defined $self->{ang_by_val}; + my $zinc = $self->get('-zinc'); + my $angle = $self->angle_of_value($new_val); + $zinc->treset($self->{rotate_group}); + $zinc->rotate($self->{rotate_group}, $angle + 1.57, 0, 0); + return unless defined $self->{value_label} and defined $self->get('-format'); + my $name = $self->get('-text'); + $zinc->itemconfigure($self->{value_label}, -text => sprintf($self->get('-format'), $new_val)); +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $origin = $self->get('-origin'); + my $parent_grp = $self->get('-parent_grp'); + my $radius = $self->get('-radius'); + $self->{main_group} = $zinc->add('group', $parent_grp, -visible => 1); + $zinc->coords($self->{main_group}, [$origin->[0]+$radius, $origin->[1]+$radius] ); + my $rad = 0.7*$radius; + $zinc->add('rectangle', $self->{main_group}, [-1.15*$radius, -1.15*$radius, 1.15*$radius, 1.15*$radius+25], + -visible => 1, + -linecolor => 'white', + ); + $zinc->add('arc', $self->{main_group}, + [-$rad, -$rad, $rad, $rad], + -visible => 1, + -linecolor => 'white', + -filled => 0, + ); + $self->{rotate_group} = $zinc->add('group', $self->{main_group}, -visible => 1); + my $min_val_angle = $self->angle_of_value(scalar $self->get('-min_val')); + my ($p1x, $p1y) = ($rad*cos($min_val_angle), $rad*sin($min_val_angle)); + $zinc->add('curve', $self->{rotate_group}, + [0.1*$p1x, 0.1*$p1y, $p1x, $p1y], + -linewidth => 2, + -linecolor => 'white', + -filled => 0); + + # ticks + for (my $val = $self->get('-min_val'); $val < $self->get('-max_val'); $val+= $self->get('-tick_spacing')) { + my $angle = $self->angle_of_value($val); +# print "tick angle_of_value $val -> $angle\n" if ($self->get('-text') eq "bat"); + my ($px, $py) = ($radius * cos($angle), $radius * sin($angle)); + $zinc->add('curve', $self->{main_group}, + [0.7*$px, 0.7*$py, 0.8*$px, 0.8*$py], + -linewidth => 2, + -linecolor => 'white', + ); + } + # legend + for (my $val = $self->get('-min_val'); $val < $self->get('-max_val'); $val+= $self->get('-legend_spacing')) { + my $angle = $self->angle_of_value($val); + $self->{label} = $zinc->add('text', $self->{main_group}, + -position => [$radius * cos($angle), $radius * sin($angle)], + -text => sprintf("%.0f", $val), + -color => 'white', + -anchor => 'c', + ); + } + # title + my $text = $self->get('-text'); + $self->{text_label} = $zinc->add('text', $self->{main_group}, + -position => [0, $radius+8], + -text => $text, + -color => 'white', + -anchor => 'c', + ); + # value + $self->{value_label} = $zinc->add('text', $self->{main_group}, + -position => [0, $radius + 20], + -text => "", + -color => 'white', + -anchor => 'c', + ); +} diff --git a/sw/ground_segment/cockpit/Paparazzi/SatPage.pm b/sw/ground_segment/cockpit/Paparazzi/SatPage.pm new file mode 100644 index 00000000000..03f8296f8c7 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/SatPage.pm @@ -0,0 +1,145 @@ +package Paparazzi::SatPage; +use Paparazzi::NDPage; +@ISA = ("Paparazzi::NDPage"); +use strict; +use Subject; + +use Paparazzi::SatSigView; + +use constant TITLE => "Satellites"; +use constant MAX_CH => 16; + +sub populate { + my ($self, $args) = @_; + $args->{-title} = TITLE; + $self->SUPER::populate($args); + $self->configspec( + -sats => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, undef], + -fix => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, undef], + ); +} + + +sub sats { + my ($self, $old_val, $new_val) = @_; + return unless defined $new_val; + + my $itow = $new_val->{-itow}; + my $nb_ch = $new_val->{-nch}; + my $sats = $new_val->{-sats}; + my $zinc = $self->get('-zinc'); + foreach my $sat (@{$sats}) { + my $sat_obj = $self->{satellites}->[$sat->{-chn}]; + $zinc->coords($sat_obj->{-group}, $self->get_pos($sat->{-elev}, $sat->{-azim})); + $zinc->itemconfigure ($sat_obj->{-group}, + -visible => 1, + ); + $zinc->itemconfigure ($sat_obj->{-arc}, + -fillcolor => $sat->{-flags} & 0x01 ? "green3" : "red", + ); + $zinc->itemconfigure ($sat_obj->{-id_lab}, + -text => sprintf("%d", $sat->{-svid}), + ); + $sat_obj->{-sig_view}->configure(-sat => $sat); + } +} + + +sub fix { + my ($self, $old_val, $new_val) = @_; +# print "in fix\n"; + return unless defined $new_val and defined $self->{rg}; +# print "in fix2\n"; +} + + +sub get_pos { + my ($self, $elev, $azim) = @_; + my $sky_radius = $self->{sky_radius}; + my $azim_rad = Utils::rad_of_deg($azim); + + use constant LIN => 1; + + my $k_elev = LIN ? 1 - $elev/90 : 1 - sin(Utils::rad_of_deg($elev)); + my $x = $sky_radius * 1 * sin($azim_rad) * $k_elev; + my $y = $sky_radius * -1 * cos($azim_rad) * $k_elev; + return [$x, $y]; +} + +sub build_gui { + my ($self) = @_; + $self->SUPER::build_gui(); + my $zinc = $self->get('-zinc'); + my $width = $self->get('-width'); + my $height = $self->get('-height'); + $self->{sky_group} = $zinc->add('group', $self->{main_group}); + my $margin = 10; + my $sky_radius = Utils::min($width, $height)*0.55/2; + $zinc->coords($self->{sky_group}, [$sky_radius+$margin, $sky_radius+$margin+$self->{vmargin}]); + $self->{sky_radius} = $sky_radius; + # elevation scale + for (my $elev = 0; $elev < 90; $elev += 15) { + my $rad = $self->get_pos($elev, 0)->[1]; + $zinc->add('arc', $self->{sky_group}, + [-$rad, -$rad, $rad, $rad], + -visible => 1, + -linecolor => 'white', + -filled => 0, + ); + $zinc->add('text', $self->{sky_group}, + -position => $self->get_pos(90-$elev, 180-$elev), + -text => sprintf("%.0f", $elev), + -color => 'white', + -anchor => 'c', + ); + } + # azimut scale + my $tick_font = '-adobe-helvetica-bold-o-normal--24-240-100-100-p-182-iso8859-1'; + my $ticks = [["S", [0, $sky_radius]], ["E", [ $sky_radius, 0]], + ["N", [0, -$sky_radius]], ["W", [-$sky_radius, 0]]]; + foreach my $tick (@{$ticks}) { + my ($txt, $pos) = @{$tick}; + $zinc->add('text', $self->{sky_group}, + -position => $pos, + -text => $txt, + -color => 'white', + -font => $tick_font, + -anchor => 'c', + ); + } + my $pos_sig_x = 0.6*$width+$margin;; + my $h_sig = $height/ (MAX_CH +1); + my $satellites = []; + my $sat_r = $sky_radius/8; + for (my $chn=0; $chn < MAX_CH; $chn++) { + my $sat_group = $zinc->add('group', $self->{sky_group}, -visible => 0); + my $sat_arc = $zinc->add('arc', $sat_group, [- $sat_r, - $sat_r, $sat_r, $sat_r], + -visible => 1, + -linecolor => 'white', + -filled => 1, + ); + my $id_lab = $zinc->add('text', $sat_group, + -position => [0, 0], + -text => "$chn", + -color => 'white', + -anchor => 'c', + ); + my $sat_sig_view = Paparazzi::SatSigView->new(-zinc => $zinc, + -width => $width/3, + -height => $h_sig, + -origin => [$pos_sig_x, ($chn+0.5) * $h_sig], + -parent_grp => $self->{main_group}, + ); + + push @{$satellites}, { + -sig_view => $sat_sig_view, + -group => $sat_group, + -arc => $sat_arc, + -id_lab => $id_lab, + -elev => 0, + -azim => 0, + }; + + } + $self->{satellites} = $satellites; +} diff --git a/sw/ground_segment/cockpit/Paparazzi/SatSigView.pm b/sw/ground_segment/cockpit/Paparazzi/SatSigView.pm new file mode 100644 index 00000000000..8e412a9321f --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/SatSigView.pm @@ -0,0 +1,67 @@ +package Paparazzi::SatSigView; +use Subject; +@ISA = ("Subject"); +use strict; + +use constant MAX_CH => 16; + +use Tk; +use Tk::Zinc; +use Math::Trig; +use Data::Dumper; + +use Paparazzi::Utils; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -parent_grp => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -sat => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, undef], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; + $self->build_gui(); +} + +sub sat { + my ($self, $old_val, $new_val) = @_; + return unless defined $new_val; + $self->get('-zinc')->itemconfigure($self->{-id_lab}, -text => sprintf("%d", $new_val->{-svid})); + $self->get('-zinc')->itemconfigure($self->{-sig_lab}, -text => sprintf("%.1f db", $new_val->{-cno})); +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $width = $self->get('-width'); + my $height = $self->get('-height'); + my $origin = $self->get('-origin'); + my $parent_grp = $self->get('-parent_grp'); + $self->{main_group} = $zinc->add('group', $parent_grp, -visible => 1); + $zinc->coords($self->{main_group}, $origin); + $zinc->add('rectangle', $self->{main_group}, [0, 0, $width, $height], + -visible => 1, +# -fillcolor => 'red', -filled => 1 + -linecolor => 'white', + ); + $self->{-id_lab} = $zinc->add('text', $self->{main_group}, + -position => [2, 1], + -color => 'white', + -anchor => 'nw', + ); + $self->{-sig_lab} = $zinc->add('text', $self->{main_group}, + -position => [$width/3, 1], + -color => 'white', + -anchor => 'nw', + ); +} + + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/Scale.pm b/sw/ground_segment/cockpit/Paparazzi/Scale.pm new file mode 100644 index 00000000000..c1e649db9cc --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/Scale.pm @@ -0,0 +1,206 @@ +#============================================================================= +# Scale Class +#============================================================================= +package Paparazzi::Scale; +use Subject; +@ISA = ("Subject"); +use strict; + +use Tk; +use Tk::Zinc; +use Math::Trig; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -parent_grp => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -direction => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 1.], + -periodic => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 0], + -min_val => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 0.], + -max_val => [S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 100.], + -disp_tick =>[S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 10.], + -tick_scale =>[S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 1.], + -repeat_legend =>[S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 2.], + -fig_clm_pc =>[S_NOINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, 0.7], + -value => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + -target_value => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0.], + ); + $self->{value_y} = 0; + $self->{y_per_tick} = 1.; +} + +sub completeinit { +# print "in Scale::completeinit\n"; + my $self = shift; + $self->SUPER::completeinit; + $self->build_gui(); +} + +sub value() { + my ($self, $previous_value, $new_value) = @_; + my $zinc = $self->get('-zinc'); + + my $nb_ticks = ($new_value - $self->get('-min_val')) / $self->get('-tick_scale'); +# print "nb_ticks $nb_ticks\n"; + my $new_y = $nb_ticks * $self->{y_per_tick}; + $new_y = -$new_y if ( $self->get('-direction') < 0); + +# my $new_y = ($new_value / $self->get('-tick_scale')) * $self->{y_per_tick}; + $self->{value_y} = $new_y; + $zinc->treset($self->{moving_group}); + $zinc->translate($self->{moving_group}, 0, $new_y); +} + +sub target_value() { + my ($self, $previous_value, $new_value) = @_; + my $zinc = $self->get('-zinc'); + my $y = $self->get_y_from_value($new_value); + $zinc->treset($self->{target_marker}); + $zinc->translate($self->{target_marker}, 0, $y); + $zinc->itemconfigure ($self->{target_up_label}, + -text => sprintf("%.1f", $new_value), + ); +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + + my $h; my $w; +if ($self->get('-height') > $self->get('-width')) { + $h = $self->get('-height'); + $w = $self->get('-width'); +} + else { + $w = $self->get('-height'); + $h = $self->get('-width'); +} + $self->{y_per_tick} = $h/$self->get('-disp_tick')+1; +# printf ("y_per_tick %d\n" , $self->{y_per_tick}); + my $rc_pc = $self->get('-fig_clm_pc'); # figures column per cent width + my $rc_x = $rc_pc * $w; # figures column x coordinate + my $tick_pc = 0.1; # tick per cent widht + my $tick_x = ($rc_pc-$tick_pc)*$w;# tick x coordinate + + my $arrow_height = 10; + my $arrow_width = 10; + + my $parent_grp = $self->get('-parent_grp'); + $self->{main_group} = $zinc->add('group', $parent_grp, -visible => 1); + my @origin = $self->get('-origin'); + $zinc->coords($self->{main_group}, \@origin); +# $zinc->translate($self->{main_group}, 0, $h/2); + + $self->{fixed_group} = $zinc->add('group', $self->{main_group}, -visible => 1); + + $self->{clipping_group} = $zinc->add('group',$self->{main_group}, -visible => 1); + $self->{itemclip} = $zinc->add('rectangle', $self->{clipping_group}, [-10, 0, $w, $h], + -visible => 0); + $zinc->itemconfigure($self->{clipping_group}, -clip => $self->{itemclip}); + + $self->{moving_group} = $zinc->add('group',$self->{clipping_group}, -visible => 1); + + $zinc->add('rectangle', $self->{fixed_group} , + [0, 0, $rc_x, $h], + -linewidth => 0, + -filled => 1, + -fillcolor => 'gray60'); + $zinc->add('rectangle', $self->{fixed_group} , + [ $rc_x, 0, $w, $h], + -linewidth => 0, + -filled => 1, + -fillcolor => 'black'); + $zinc->add('curve', $self->{fixed_group}, + [0, 0, $w, 0], + -linewidth => 2, + -linecolor => 'white', + -filled => 0); + $zinc->add('curve', $self->{fixed_group}, + [0, $h, $w, $h], + -linewidth => 2, + -linecolor => 'white', + -filled => 0); + $zinc->add('curve', $self->{fixed_group}, + [$rc_x, 0, $rc_x, $h], + -linewidth => 2, + -linecolor => 'white', + -filled => 0); + $zinc->add('curve', $self->{fixed_group}, + [$rc_x, $h/2 , + $rc_x + $arrow_width, $h/2 + $arrow_height/2, + $rc_x + $arrow_width, $h/2 - $arrow_height/2], + -linewidth => 2, + -linecolor => 'yellow', + -filled => 1, + -fillcolor => 'yellow', + -closed => 1); + + $self->{target_marker} = $zinc->add('curve', $self->{moving_group}, + [$rc_x-1, 0, + $rc_x + $arrow_width+1, $arrow_height/2+1, + $rc_x + $arrow_width+1,-$arrow_height/2-1], + -linewidth => 2, + -linecolor => 'HotPink1', + -filled => 0, + -closed => 1); + + + + $zinc->add('curve', $self->{fixed_group}, + [0, $h/2, $rc_x, $h/2], + -linewidth => 2, + -linecolor => 'yellow', + ); + + my $nb_ticks = ($self->get('-max_val') - $self->get('-min_val')) / $self->get('-tick_scale'); + my $tick_font = '-adobe-helvetica-bold-o-normal--18-240-100-100-p-182-iso8859-1'; + my $first_tick = $self->get('-periodic') ? -$nb_ticks:0; + my $last_tick = $self->get('-periodic') ? 2*$nb_ticks:$nb_ticks; + for (my $tick=$first_tick; $tick<=$last_tick; $tick++) { + my $value = ($self->get('-min_val') + $tick) * $self->get('-tick_scale'); + my $y = $self->get_y_from_value($value); + $zinc->add('curve', $self->{moving_group}, + [$tick_x, $y, $rc_x, $y], + -linewidth => 1, + -linecolor => 'white'); + if (!($tick%$self->get('-repeat_legend'))) { + my $text = sprintf("%d", $value % $self->get('-max_val')); + $zinc->add('text', $self->{moving_group}, + -position => [$tick_x/2, $y], + -color => 'white', + -font => $tick_font, + -anchor => 'c', + -text => $text); + } + } + + $self->{target_up_label} = + $zinc->add('text', $self->{main_group}, + -position => [$tick_x/2, 0], + -color => 'HotPink1', + -font => $tick_font, + -anchor => 's', + -text => ""); + + + if ($self->get('-width') > $self->get('-height')) { + @origin = $self->get('-origin'); + $zinc->rotate($self->{main_group}, - Math::Trig::pip2(), $origin[0], $origin[1]); + } +} + + +sub get_y_from_value { + my ($self, $value) = @_; + my $h = ($self->get('-height') > $self->get('-width')) ? + $self->get('-height') : $self->get('-width'); + my $nb_ticks = ($value - $self->get('-min_val')) / $self->get('-tick_scale'); + return $self->get('-direction') > 0 ? $h/2 - $nb_ticks * $self->{y_per_tick} : + $h/2 + $nb_ticks * $self->{y_per_tick}; +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/Strip.pm b/sw/ground_segment/cockpit/Paparazzi/Strip.pm new file mode 100644 index 00000000000..c9c8333d488 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/Strip.pm @@ -0,0 +1,147 @@ +package Paparazzi::Strip; +use Subject; +@ISA = ("Subject"); + +use Data::Dumper; + +use strict; + +use Math::Trig; +use Tk; +use Tk::Zinc; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -parent_grp => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -name => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -selected => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 0], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit(); + $self->build_gui(); +} + +my $style = { + -linewidth => 3, + -linecolor => '#aaccff', + -fillcolor => 'back', + -relief => 'roundraised' + }; + +my $gradset = { + 'idnt' => '=axial 90 |#ffffff 0|#ffeedd 30|#e9d1ca 90|#e9a89a', + 'back' => '=axial 0 |#c1daff|#8aaaff', + 'shad' => '=path -40 -40 |#000000;50 0|#000000;50 92|#000000;0 100', + 'btn_outside' => '=axial 0 |#ffeedd|#8a9acc', + 'btn_inside' => '=axial 180 |#ffeedd|#8a9acc', + 'ch1' => '=axial 0 |#8aaaff|#5B76ED', + }; + +my @stripGradiants; + +sub init_gradiants { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + unless (@stripGradiants) { + my %gradiants = %{$gradset}; + my ($name, $gradiant); + while (($name, $gradiant) = each(%gradiants)) { + # création des gradients nommés + $zinc->gname($gradiant, $name) unless $zinc->gname($gradiant); + # the previous test is usefull only + # when this script is executed many time in the same process + # (it is typically the case in zinc-demos) + push(@stripGradiants, $name); + } + } +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $width = $self->get('-width'); + my $height = $self->get('-height'); + my $origin = $self->get('-origin'); + my $parent_grp = $self->get('-parent_grp'); + + $self->init_gradiants(); + + $self->{s_main_group} = $zinc->add('group', $parent_grp, -visible => 1); + $zinc->coords($self->{s_main_group}, $origin); + + my $ombre = $zinc->add('rectangle', $self->{s_main_group} , + [5, 5, $width+5, $height+5], + -filled => 1, + -linewidth => 0, + -fillcolor => 'shad', + -priority => 10, + ); + + $self->{-paper} = $zinc->add('rectangle', $self->{s_main_group} , + [0, 0, $width, $height], + -filled => 1, + -linewidth => $style->{'-linewidth'}, + -linecolor => $style->{'-linecolor'}, + -fillcolor => $style->{'-fillcolor'}, + -relief => $style->{'-relief'}, + -priority => 100 + ); + +# $zinc->bind ($self->{-paper},'',[\&OnButton1PressPaper,$self]); +# $zinc->bind ($self->{-paper},'',[\&OnButton1ReleasePaper,$self]); + +# my $texture = $zinc->Photo('background_texture.gif', +# -file => Tk->findINC("demos/zinc_data/background_texture.gif")); + # $zinc->itemconfigure($foo, -tile => $texture); + my $text = $self->get('-name'); + + $zinc->add('text', $self->{s_main_group}, + -position => [10, 10], + -color => 'white', +# -font => $v_tick_font, + -anchor => 'w', + -text => $text, + -priority => 110); + +} + +sub selected { + my ($self, $previous, $new) = @_; +# print ("in selected $self->get('-name') $previous, $new\n"); + $self->get('-zinc')->itemconfigure ($self->{-paper}, + -fillcolor => $new != 0 ? 'ch1': $style->{'-fillcolor'}, + ); + +} + + +sub OnButton1PressPaper { + my ($zinc, $self) = @_; + $zinc = $self->get('-zinc'); + + $zinc->itemconfigure ($self->{-paper}, + -fillcolor => 'ch1', + ); + +} + +sub OnButton1ReleasePaper { + my ($zinc, $self) = @_; + $zinc = $self->get('-zinc'); + + $zinc->itemconfigure ($self->{-paper}, + -fillcolor => $style->{'-fillcolor'} + ); + + +} + +1; diff --git a/sw/ground_segment/cockpit/Paparazzi/StripPanel.pm b/sw/ground_segment/cockpit/Paparazzi/StripPanel.pm new file mode 100644 index 00000000000..a22b03375b9 --- /dev/null +++ b/sw/ground_segment/cockpit/Paparazzi/StripPanel.pm @@ -0,0 +1,80 @@ +package Paparazzi::StripPanel; +use Subject; +@ISA = ("Subject"); + +use strict; + +use Math::Trig; +use Tk; +use Tk::Zinc; + +use Paparazzi::Strip; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-zinc => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -origin => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -width => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -height => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -selected_ac => [S_NOINIT, S_METHOD, S_RDWR, S_OVRWRT, S_NOPRPG, 'NONE'], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit(); + $self->build_gui(); +} + +sub build_gui { + my ($self) = @_; + my $zinc = $self->get('-zinc'); + my $width = $self->get('-width'); + my $height = $self->get('-height'); + my $origin = $self->get('-origin'); + + $self->{sp_main_group} = $zinc->add('group', 1, -visible => 1); + $zinc->coords($self->{sp_main_group}, $origin); + + my $board = $zinc->add('rectangle', $self->{sp_main_group} , + [0, 0, $width-5, $height-7], + -linewidth => 0, + -filled => 1, + -fillcolor => 'blue', + ); + my $texture = $zinc->Photo('background_texture.gif', + -file => Tk->findINC("demos/zinc_data/background_texture.gif")); + $zinc->itemconfigure($board, -tile => $texture); + $self->{strips} = {}; + +} + +sub add_strip { + my ($self, $name) = @_; + # add strip only once + return if (defined $self->{strips}->{$name}); + my $zinc = $self->get('-zinc'); + use constant NB_STRIP => 6; + my $step = $self->get('-height') / NB_STRIP; + my $nb_strips = keys %{$self->{strips}}; + my ($p, $w, $h) = ([15, 10 + $step * $nb_strips], 120, 45); + $self->{strips}->{$name} = Paparazzi::Strip->new( -zinc => $zinc, -parent_grp => $self->{sp_main_group}, + -origin => $p, -width => $w, -height => $h, + -name => $name); + $zinc->bind($self->{strips}->{$name}->{-paper},'',[\&OnStripPressed,$self, $name]); +} + +sub OnStripPressed { +# print ("OnStripPressed @_\n"); + my ($zinc, $self, $name) = @_; + $self->configure( -selected_ac => $name); +} + +sub selected_ac { + my ($self, $previous, $new) = @_; + $self->{strips}->{$previous}->configure( -selected => 0) if defined $previous and defined $self->{strips}->{$previous}; + $self->{strips}->{$new}->configure( -selected => 1) if defined $new and $new ne "NONE" and defined $self->{strips}->{$new}; +} + +1; diff --git a/sw/ground_segment/cockpit/cockpit.pl b/sw/ground_segment/cockpit/cockpit.pl new file mode 100755 index 00000000000..e83a7b2cc77 --- /dev/null +++ b/sw/ground_segment/cockpit/cockpit.pl @@ -0,0 +1,359 @@ +#!/usr/bin/perl -w +package Cockpit; + +my @paparazzi_lib; +BEGIN { + @paparazzi_lib = (defined $ENV{PAPARAZZI_SRC}) ? + ($ENV{PAPARAZZI_SRC}."/sw/lib/perl", $ENV{PAPARAZZI_SRC}."/sw/ground_segment/cockpit"):(); +} +use lib (@paparazzi_lib); + +use vars qw (@ISA) ; +use Subject; +@ISA = ("Subject"); + +use strict; +use Paparazzi::Environment; + +use constant COCKPIT_DEBUG => 0; +use constant APP_ID => "Paparazzi Cockpit"; +use constant MESSAGE_WHEN_READY => APP_ID.': READY'; + +use Paparazzi::IvyProtocol; +use Paparazzi::PFD; +use Paparazzi::ND; +use Paparazzi::MissionD; +use Paparazzi::StripPanel; +use Paparazzi::Geometry; + +use Tk; +#use Tk::PNG; +use Tk::Zinc; +use Ivy; +use Text::CSV; +use Data::Dumper; +use Pod::Usage; + +my $options = + { + ivy_bus => "127.255.255.255:2010", + render => 1, + }; + +my $md; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit(); + $self->{aircrafts} = []; + $self->{wind_dir} = 0.; + $self->{wind_speed} = 0.; + $self->start_ivy(); + $self->build_gui(); + +} + +sub build_gui { + my ($self) = @_; + $self->{mw} = MainWindow->new(); + my $top_frame = $self->{mw}->Frame()->pack(-side => 'top', -fill => 'both'); + my $bot_frame = $self->{mw}->Frame()->pack(-side => 'bottom', -fill => 'both'); + my ($stp_p, $stp_w, $stp_h) = ([0, 0], 150, 300); + my ($pfd_p, $pfd_w, $pfd_h) = ([$stp_w, 0] , 300, $stp_h); + my ($nd_p, $nd_w, $nd_h) = ([$pfd_p->[0]+ $pfd_w, 0], 600, 600); + my $zinc = $top_frame->Zinc(-width => $stp_w + $pfd_w + $nd_w , + -height => $nd_h, + -backcolor => 'black', + -borderwidth => 3, -relief => 'sunken', + -render => $options->{render}, + -lightangle => 130,); + $zinc->pack(-side => 'left', -anchor => "nw"); + $self->{strip_panel} = Paparazzi::StripPanel->new( -zinc => $zinc, + -origin => $stp_p, + -width => $stp_w, + -height => $stp_h + ); + $self->{strip_panel}->attach($self, '-selected_ac', ['onAircratftSelection', ()]); + + $self->{pfd} = Paparazzi::PFD->new( -zinc => $zinc, + -origin => $pfd_p, + -width => $pfd_w, + -height => $pfd_h, + ); + $self->{pfd}->attach($self, 'SHOW_PAGE', ['onShowPage']); + $self->{nd} = Paparazzi::ND->new( -zinc => $zinc, + -origin => $nd_p, + -width => $nd_w, + -height => $nd_h, + ); + $self->{nd}->attach($self, 'WIND_COMMAND', ['onWindCommand']); + $md = $bot_frame->MissionD(-bg => '#c1daff'); + $md->pack(-side => 'bottom', -anchor => "n", -fill => 'both'); + + +} + +sub onTimer { + my ( $self) = @_; +# print("in onTimer $self\n"); + $self->{ivy}->sendMsgs("WIND_REQ toto", {-id => "toto"}); + # Paparazzi::IvyProtocol::request_message("ground", "CONFIG", {id => 'ground'}, $self->{ivy}, [$self, \&ivyOnWind]); +} + +sub ivyOnWind { +# print "in ivyOnWind\n"; # if (COCKPIT_DEBUG); + my ($self, @args) = @_; + my $fields_by_name = Paparazzi::IvyProtocol::get_values_by_name("ground", "RES_WIND", \@args); + $self->{wind_dir} = $args[2]; + $self->{wind_speed} = $args[3]; + + my $h = { dir => $args[2], + speed => $args[3], + mean_aspeed => $args[4], + stddev => $args[5], + }; + +# print Dumper($h);# if (COCKPIT_DEBUG); + $self->{nd}->configure('-wind' => $h); +} + +sub onShowPage { + my ($self, $component, $signal, $page) = @_; + print "cockpit::onShowPage $page\n"; + print "$self->{nd}\n"; + $self->{nd}->configure('-page' => $page); +} + +sub onWindCommand { + my ($self, $component, $signal, $cmd) = @_; + print "cockpit::onWindCommand $cmd\n"; + $self->{ivy}->sendMsgs("WIND_COMMAND $cmd"); + if ($cmd eq "start") { + $self->{timer_id} = $self->{mw}->repeat(5000, [\&onTimer, $self]); + $self->{ivy}->sendMsgs("WIND_COMMAND clear"); + } + elsif ($cmd eq "stop") { + $self->{mw}->afterCancel($self->{timer_id}) + } +} + +sub onAircratftSelection { +# print ("onAircratftSelection @_\n"); + my ($self, $_sp, $what, $new_selected_ac ) = @_; + my $ivy = $self->{ivy}; + Paparazzi::IvyProtocol::sendMsg($ivy, "ground", "SELECTED",{ id => $new_selected_ac}); + return if ($new_selected_ac eq "NONE"); + my @ac_events = ( ['FLIGHT_PARAM', \&ivyOnFlightParam], + ['NAV_STATUS', \&ivyOnNavStatus], + ['AP_STATUS', \&ivyOnApStatus], + ['ENGINE_STATUS', \&ivyOnEngineStatus], + ['SATS', \&ivyOnSats], + ); + foreach my $event (@ac_events) { + # removes existing binding + Paparazzi::IvyProtocol::bind_message("aircraft_info", $event->[0], {id => $self->{selected_ac}}, $ivy, undef) unless !defined $self->{selected_ac}; + # add new one + Paparazzi::IvyProtocol::bind_message("aircraft_info", $event->[0], {id => $new_selected_ac}, $ivy, [$self, $event->[1]]); + } + $self->{selected_ac} = $new_selected_ac; +} + +sub start_ivy { + my ($self) = @_; + + Ivy->init (-ivyBus => $options->{ivy_bus}, + -appName => APP_ID, + -loopMode => 'TK', + -messWhenReady => MESSAGE_WHEN_READY, + ) ; + $self->{ivy} = Ivy->new (-statusFunc => \&ivyStatusCbk); + $self->{ivy}->start(); + my $paparazzi_home = Paparazzi::Environment::paparazzi_home(); + Paparazzi::IvyProtocol::read_protocol($paparazzi_home."/conf/messages.xml", "ground"); + Paparazzi::IvyProtocol::read_protocol($paparazzi_home."/conf/messages.xml", "aircraft_info"); + Paparazzi::IvyProtocol::bind_message("ground", "AIRCRAFTS", {}, $self->{ivy}, [$self, \&ivyOnAircrafts]); +# Paparazzi::IvyProtocol::bind_message("ground", "WIND_RES", {}, $self->{ivy}, [$self, \&ivyOnWind]); + $self->{ivy}->bindRegexp ("^ground WIND_RES (\\S+) (\\S+) (\\S+) (\\S+) (\\S+)", [$self, \&ivyOnWind]); + $self->{ivy}->bindRegexp ("^Thon1 RAW (\\S+) RAD_OF_IR (\\S+) (\\S+) (\\S+) (\\S+) (\\S+)", [$self, \&ivyOnIR]); +} + +sub ivyOnIR { + my ($self, @args) = @_; + my $h = { + ir => $args[2], + rad => $args[3], + rad_of_ir => $args[4], + ir_roll_ntrl => $args[5], + ir_pitch_ntrl => $args[6] + }; + $self->{nd}->configure('-lls' => $h->{rad_of_ir}); + $self->{nd}->put_lls($h->{rad_of_ir}); +} + + + + +sub ivyOnAircrafts { +# print "in ivyOnAircrafts\n" if (COCKPIT_DEBUG); + my ($self, @args) = @_; + my $fields_by_name = Paparazzi::IvyProtocol::get_values_by_name("ground", "AIRCRAFTS", \@args); + print Dumper($fields_by_name) if (COCKPIT_DEBUG); + my $ac_list = $fields_by_name->{ac_list}; + my $csv = Text::CSV->new(); + $csv->parse($ac_list); + my @new_ac = $csv->fields(); + my @added_ac = Utils::diff_array(\@new_ac, $self->{aircrafts}); + my @removed_ac = Utils::diff_array($self->{aircrafts}, \@new_ac); + foreach my $ac1 (@added_ac) { + $self->{strip_panel}->add_strip($ac1); + Paparazzi::IvyProtocol::request_message("aircraft_info", "CONFIG", {id => $ac1}, $self->{ivy}, [$self, \&onConfigRes]); + } + $self->{aircrafts} = \@new_ac; +} + +sub ivyOnNavStatus { + my ($self, @args) = @_; + my $fields_by_name = Paparazzi::IvyProtocol::get_values_by_name("aircraft_info", "NAV_STATUS", \@args); + print Dumper($fields_by_name) if (COCKPIT_DEBUG); + $md->set_block_and_stage($fields_by_name->{cur_block}, $fields_by_name->{cur_stage}); +} + +sub onWindRes { + my ($self, @args) = @_; + my $fields_by_name = Paparazzi::IvyProtocol::get_values_by_name("ground", "WIND_RES", \@args); + print Dumper ($fields_by_name) if (COCKPIT_DEBUG); +} + +sub onConfigRes { + print "in onConfigRes\n" if (COCKPIT_DEBUG); + my ($self, @args) = @_; + my $fields_by_name = Paparazzi::IvyProtocol::get_values_by_name("aircraft_info", "CONFIG", \@args); + my $fp = $fields_by_name->{flight_plan}; + # print Dumper($fields_by_name); + my $paparazzi_src = Paparazzi::Environment::paparazzi_src(); + my $gfp_bin = ((defined $paparazzi_src) ? $paparazzi_src."/sw/tools" : "/usr/share/paparazzi/bin") ."/gen_flight_plan.out"; + my $flight_plan_xml = `$gfp_bin -dump $fp`; + $md->load_flight_plan($flight_plan_xml); + $md->set_block_and_stage(0,0); +} + + +sub ivyOnFlightParam { + print "in ivyOnFlightParam\n" if (COCKPIT_DEBUG); + my ($self, @args) = @_; + my $fbn = Paparazzi::IvyProtocol::get_values_by_name("aircraft_info", "FLIGHT_PARAM", \@args); + print Dumper($fbn) if (COCKPIT_DEBUG); + my $gs_dir_rad = Utils::deg2rad( $fbn->{heading}); + my $gs_angle_rad = Paparazzi::Geometry::angle_of_heading_rad( Utils::deg2rad( $fbn->{heading})); +# print "$gs_dir_rad $gs_angle_rad\n"; + my ($xg, $yg) = Paparazzi::Geometry::cart_of_polar ($fbn->{speed}, $gs_angle_rad); + my $wind_angle_rad = Paparazzi::Geometry::angle_of_heading_rad( Utils::deg2rad( $self->{wind_dir} + Math::Trig::pi)); + my ($xw, $yw) = Paparazzi::Geometry::cart_of_polar ($self->{wind_speed}, $wind_angle_rad); + my ($xa, $ya) = ($xg+$xw, $yg+$yw); + my ($as, $ad) = Paparazzi::Geometry::polar_of_cart ($xa, $ya); + +# print "gs $xg $yg w $xw $yw as $xa $ya $as $ad\n"; + + + $self->{pfd}->configure( + -roll => $fbn->{roll}, + -pitch => $fbn->{pitch}, +# -speed => $fbn->{speed}, + -speed => $as, + -target_speed => $fbn->{speed}, +# -heading => $fbn->{heading}, + -heading => $ad, +# -target_heading => $fbn->{heading}, + -alt => $fbn->{alt}, + -vz => $fbn->{climb}, +# -gps_mode => 3, + -lls_mode => 1, +# -lls_value => 1.1 , + -ctrst_mode => 2 , + -ctrst_value => 200, + -rc_mode => 1, + -if_mode => 1, + ); +# $self->{nd}->configure(); + + +} + +sub ivyOnApStatus { + print "in ivyOnApStatus\n" if (COCKPIT_DEBUG); + my ($self, @args) = @_; + my $fbn = Paparazzi::IvyProtocol::get_values_by_name("aircraft_info", "AP_STATUS", \@args); +# print $self->{selected_ac}." ".Dumper($fbn);# if (COCKPIT_DEBUG); + $self->{pfd}->configure( -ap_mode => $fbn->{mode}, +# -h_mode => $fbn->{h_mode}, +# -v_mode => $fbn->{v_mode}, +# -target_vz => $fbn->{target_climb} + -target_alt => $fbn->{target_alt}, + -target_heading => $fbn->{target_heading}, + ); + $self->{nd}->configure( -ap_status, $fbn); +} + +sub ivyOnEngineStatus { + print "in ivyOnEngineStatus\n" if (COCKPIT_DEBUG); + my ($self, @args) = @_; + my $fbn = Paparazzi::IvyProtocol::get_values_by_name("aircraft_info", "ENGINE_STATUS", \@args); + $self->{nd}->configure( -engine_status, $fbn); +} + +sub ivyOnSats { + print "in ivyOnSats\n" if (COCKPIT_DEBUG); + my ($self, @args) = @_; + my $fbn = Paparazzi::IvyProtocol::get_values_by_name("aircraft_info", "SATS", \@args); + $self->{nd}->configure( -sats, $fbn); +} + + + +sub ivyStatusCbk { + printf("in ivyStatusCbk\n") if (COCKPIT_DEBUG); +} + +Paparazzi::Environment::parse_command_line($options) || pod2usage(-verbose => 0); +print Dumper($options); +my $cockpit = Cockpit->new(); +MainLoop(); + +__END__ + +=head1 NAME + +cockpit + +=head1 SYNOPSIS + +cockpit [options] + +Options: + -ivybus the ivy bus (eg 127.2552.55255:2010) + -render toggle opengl usage + +=head1 OPTIONS + +=over 8 + +=item B<-help> + +Print a brief help message and exits. + +=item B<-man> + +Prints the manual page and exits. + +=back + +=head1 DESCRIPTION + +B will display an aircraft cockpit. + +=cut diff --git a/sw/ground_segment/cockpit/map.pl b/sw/ground_segment/cockpit/map.pl new file mode 100755 index 00000000000..b828ad4aad5 --- /dev/null +++ b/sw/ground_segment/cockpit/map.pl @@ -0,0 +1,129 @@ +#!/usr/bin/perl -w +package Map; + +my @paparazzi_lib; +BEGIN { + @paparazzi_lib = (defined $ENV{PAPARAZZI_SRC}) ? + ($ENV{PAPARAZZI_SRC}."/sw/lib/perl", $ENV{PAPARAZZI_SRC}."/sw/ground_segment/cockpit"):(); +} +use lib (@paparazzi_lib); + +use vars qw (@ISA) ; +use Subject; +@ISA = ("Subject"); + +use strict; +use Paparazzi::Environment; + +use constant MAP_DEBUG => 0; +use constant APP_ID => "Paparazzi Map"; +use constant MESSAGE_WHEN_READY => APP_ID.': READY'; + +use Paparazzi::IvyProtocol; +use Paparazzi::MapView; +use Paparazzi::Utils; + +use Getopt::Long; +use Tk; +use Ivy; +use Text::CSV; + +my $paparazzi_home = Paparazzi::Environment::paparazzi_home(); + +my $options = { + paparazzi_home => $paparazzi_home, + ivy_bus => "127.255.255.255:2010", + data_dir => $paparazzi_home."/data", + map_file => "maps/defaultUTM.xml", + conf_dir => $paparazzi_home."/conf", + render => "1" + }; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit(); + $self->{aircrafts} = []; + $self->start_ivy(); + $self->build_gui(); +} + +sub start_ivy { + my ($self) = @_; + + Ivy->init (-ivyBus => $options->{ivy_bus}, + -appName => APP_ID, + -loopMode => 'TK', + -messWhenReady => MESSAGE_WHEN_READY, + ); + $self->{ivy} = Ivy->new(); + $self->{ivy}->start(); + Paparazzi::IvyProtocol::read_protocol($paparazzi_home."/conf/messages.xml", "aircraft_info"); + Paparazzi::IvyProtocol::read_protocol($paparazzi_home."/conf/messages.xml", "ground"); + Paparazzi::IvyProtocol::bind_message("ground", "AIRCRAFTS", {}, $self->{ivy}, [$self, \&ivyOnAircrafts]); +} + +sub build_gui { + my ($self) = @_; + my $mw = MainWindow->new(); + $mw->title("Paparazzi map : $options->{map_file}"); + $mw->geometry("600x600"); + $self->{map_view} = $mw->MapView(-render => $options->{render}); + $self->{map_view}->pack(-fill => 'both', -expand => "1"); + + $self->{map_view}->load_map($options->{data_dir}."/".$options->{map_file}); +# my $flight_plan = `$paparazzi_home/sw/tools/gen_flight_plan.out -dump $ flight_plan_name`; +# $mv->load_flight_plan($flight_plan); +} + +sub ivyOnAircrafts { + print "in ivyOnAircrafts\n"; + my ($self, @args) = @_; + my $fields_by_name = Paparazzi::IvyProtocol::get_values_by_name("ground", "AIRCRAFTS", \@args); +# print Dumper($fields_by_name); + my $ac_list = $fields_by_name->{ac_list}; + my $csv = Text::CSV->new(); + $csv->parse($ac_list); + my @new_ac = $csv->fields(); + my @added_ac = Utils::diff_array(\@new_ac, $self->{aircrafts}); + my @removed_ac = Utils::diff_array($self->{aircrafts}, \@new_ac); + foreach my $new_ac (@added_ac) { + print "added_ac $new_ac\n"; + Paparazzi::IvyProtocol::bind_message("aircraft_info", "FLIGHT_PARAM", {id => $new_ac}, $self->{ivy}, [$self, \&ivyOnFlightParam]); + my $track_item = $self->{map_view}->set_track_geo($new_ac, [0, 0]); + + } + foreach my $ac2 (@removed_ac) { + print "removed_ac $ac2\n"; + } + $self->{aircrafts} = \@new_ac; +} + +sub ivyOnFlightParam { +# print "in ivyOnFlightParam\n"; + my ($self, @args) = @_; + my $fbn = Paparazzi::IvyProtocol::get_values_by_name("aircraft_info", "FLIGHT_PARAM", \@args); + my $ac = $fbn->{id}; +# print Dumper($fbn); + $self->{map_view}->set_track_geo($ac, [$fbn->{east}, $fbn->{north}]); +} + +use Data::Dumper; + +GetOptions ("b=s" => \$options->{ivy_bus}, + "t=s" => \$options->{paparazzi_home}, + "d=s" => \$options->{data_dir}, + "m=s" => \$options->{map_file}, + "c=s" => \$options->{conf_dir}, + "r=s" => \$options->{render}, + ); +print Dumper($options); +my $map = Map->new(); +MainLoop(); + +1; + diff --git a/sw/ground_segment/cockpit/map2d.ml b/sw/ground_segment/cockpit/map2d.ml new file mode 100644 index 00000000000..88f88ddd67b --- /dev/null +++ b/sw/ground_segment/cockpit/map2d.ml @@ -0,0 +1,225 @@ +open Printf +open Latlong + +type color = string + +let fos = float_of_string +let list_separator = Str.regexp "," + +module G = MapCanvas + +let home = Env.paparazzi_home +let (//) = Filename.concat +let default_path_SRTM = home // "data" // "SRTM" +let default_path_maps = home // "data" // "maps" // "" +let default_path_missions = home // "conf" + +let gen_flight_plan = + try + Sys.getenv "PAPARAZZI_SRC" // "sw/tools/gen_flight_plan.out" + with + Not_found -> "/usr/bin/paparazzi gen_flight_plan" + + +type aircraft = { + track : MapTrack.track; + color: color; + mutable fp_group : MapWaypoints.group option + } + +let live_aircrafts = Hashtbl.create 3 + +let map_ref = ref None + +let float_attr = fun xml a -> float_of_string (ExtXml.attrib xml a) + +let load_map = fun (geomap:G.widget) xml_map -> + let dir = Filename.dirname xml_map in + let xml_map = Xml.parse_file xml_map in + let image = dir // ExtXml.attrib xml_map "file" + and scale = float_attr xml_map "scale" + and utm_zone = + try int_of_string (Xml.attrib xml_map "utm_zone") with + _ -> 31 in + geomap#set_world_unit scale; + let one_ref = ExtXml.child xml_map "point" in + let x = float_attr one_ref "x" and y = float_attr one_ref "y" + and utm_x = float_attr one_ref "utm_x" and utm_y = float_attr one_ref "utm_y" in + let utm_x0 = utm_x -. x *. scale + and utm_y0 = utm_y +. y *. scale in + + let utm_ref = + match !map_ref with + None -> + let utm0 = {utm_x = utm_x0; utm_y = utm_y0; utm_zone = utm_zone } in + map_ref := Some utm0; + utm0 + | Some utm -> + assert (utm_zone = utm.utm_zone); + utm in + + let wgs84_of_en = fun en -> + of_utm WGS84 {utm_x = utm_ref.utm_x +. en.G.east; utm_y = utm_ref.utm_y +. en.G.north; utm_zone = utm_zone} in + + geomap#set_wgs84_of_en wgs84_of_en; + let en0 = {G.east=utm_x0 -. utm_ref.utm_x; north=utm_y0 -. utm_ref.utm_y} in + ignore (geomap#display_map en0 (GdkPixbuf.from_file image)); + geomap#moveto en0 + + +let file_of_url = fun url -> + if String.sub url 0 7 = "file://" then + String.sub url 7 (String.length url - 7) + else + let tmp_file = Filename.temp_file "fp" ".xml" in + Sys.command (sprintf "wget -O %s %s" tmp_file url); + tmp_file + +let load_mission = fun color geomap url -> + let file = file_of_url url in + let xml = Xml.parse_in (Unix.open_process_in (sprintf "%s -dump %s" gen_flight_plan file)) in + let xml = ExtXml.child xml "flight_plan" in + let lat0 = float_attr xml "lat0" + and lon0 = float_attr xml "lon0" in + let utm0 = utm_of WGS84 {posn_lat = (Deg>>Rad)lat0; posn_long = (Deg>>Rad)lon0 } in + let waypoints = ExtXml.child xml "waypoints" in + + let utm_ref = + match !map_ref with + None -> + map_ref := Some utm0; + utm0 + | Some utm -> + assert (utm0.utm_zone = utm.utm_zone); + utm in + let en_of_xy = fun x y -> + {G.east = x +. utm0.utm_x -. utm_ref.utm_x; + G.north = y +. utm0.utm_y -. utm_ref.utm_y } in + + let fp = new MapWaypoints.group ~color ~editable:false geomap in + List.iter + (fun wp -> + let en = en_of_xy (float_attr wp "x") (float_attr wp "y") in + let alt = try Some (float_attr wp "alt") with _ -> None in + ignore (MapWaypoints.waypoint fp ~name:(ExtXml.attrib wp "name") ?alt en) + ) + (Xml.children waypoints); + fp + + +let aircraft_pos_msg = fun track utm_x utm_y heading -> + match !map_ref with + None -> () + | Some utm0 -> + let en = {G.east = utm_x -. utm0.utm_x; north = utm_y -. utm0.utm_y } in + track#add_point en; + track#move_icon en heading + +let new_color = + let colors = ref ["red"; "blue"; "green"] in + fun () -> + match !colors with + x::xs -> + colors := xs @ [x]; + x + | [] -> failwith "new_color" + + +let ivy_request = fun s f -> + let b = ref (Obj.magic ()) in + let cb = fun response -> + Ivy.unbind !b; + f response in + let id = sprintf "%s_%d" (Filename.basename Sys.argv.(1)) (Unix.getpid ()) in + b := Ivy.bind (fun _ args -> cb args.(0)) (sprintf "response %s (.*)" id); + Ivy.send (sprintf "request %s %s" id s) + + +let ask_fp = fun geomap ac -> + let b = ref (Obj.magic ()) in + let load_fp = fun file -> + Ivy.unbind !b; + let ac = Hashtbl.find live_aircrafts ac in + ac.fp_group <- Some (load_mission ac.color geomap file) in + b := Ivy.bind (fun _ args -> load_fp args.(0)) (sprintf "ground FLIGHT_PLAN %s (.*)" ac); + Ivy.send (sprintf "ask FLIGHT_PLAN %s" ac) + + +let show_mission = fun geomap ac on_off -> + if on_off then + ask_fp geomap ac + else + let a = Hashtbl.find live_aircrafts ac in + match a.fp_group with + None -> () + | Some g -> + a.fp_group <- None; + g#group#destroy () + +let resize_track = fun ac track -> + match GToolbox.input_string ~text:(string_of_int track#size) ~title:ac "Track size" with + None -> () + | Some s -> track#resize (int_of_string s) + + + +let live_aircrafts_msg = fun (geomap:MapCanvas.widget) acs -> + List.iter + (fun ac -> + if not (Hashtbl.mem live_aircrafts ac) then begin + let ac_menu = geomap#factory#add_submenu ac in + let ac_menu_fact = new GMenu.factory ac_menu in + let fp = ac_menu_fact#add_check_item "Fligh Plan" ~active:false in + ignore (fp#connect#toggled (fun () -> show_mission geomap ac fp#active)); + let color = new_color () in + let track = new MapTrack.track ~name:ac ~color:color geomap in + ignore (ac_menu_fact#add_item "Clear Track" ~callback:(fun () -> track#clear)) ; + ignore (ac_menu_fact#add_item "Resize Track" ~callback:(fun () -> resize_track ac track)) ; + let b = + Ivy.bind + (fun _ args -> aircraft_pos_msg track (fos args.(0)) (fos args.(1))(fos args.(2))) + (sprintf "%s +FLIGHT_PARAM +[^ ]* +[^ ]* +([0-9\\.]*) +([0-9\\.]*) +[0-9\\.]* +([0-9\\.]*)" ac) in + Hashtbl.add live_aircrafts ac { track = track; color = color; fp_group = None } + end + ) + acs + + +let _ = + let ivy_bus = ref "127.255.255.255:2010" + and map_file = ref "" + and mission_file = ref "" in + let options = + [ "-b", Arg.String (fun x -> ivy_bus := x), "Bus\tDefault is 127.255.255.25:2010"; + "-m", Arg.String (fun x -> map_file := x), "Map description file"] in + Arg.parse (options) + (fun x -> Printf.fprintf stderr "Warning: Don't do anythig with %s\n" x) + "Usage: "; + (* *) + Ivy.init "Paparazzi map 2D" "READY" (fun _ _ -> ()); + Ivy.start !ivy_bus; + + Srtm.add_path default_path_SRTM; + + let window = GWindow.window ~title: "Map2d" ~border_width:1 ~width:400 () in + let vbox= GPack.vbox ~packing: window#add () in + let quit = fun () -> GMain.Main.quit (); exit 0 in + ignore (window#connect#destroy ~callback:quit); + + let geomap = new MapCanvas.widget ~height:400 () in + let accel_group = geomap#menu_fact#accel_group in + ignore (geomap#menu_fact#add_item "Quit" ~key:GdkKeysyms._Q ~callback:quit); + + vbox#pack ~expand:true geomap#frame#coerce; + + (* Loading an initial map *) + if !map_file <> "" then begin + let xml_map_file = Filename.concat default_path_maps !map_file in + load_map geomap xml_map_file + end; + + Ivy.bind (fun _ args -> live_aircrafts_msg geomap (Str.split list_separator args.(0))) "ground +AIRCRAFTS +(.*)"; + + window#add_accel_group accel_group; + window#show (); + GMain.Main.main () diff --git a/sw/ground_segment/cockpit/radio_control.pl b/sw/ground_segment/cockpit/radio_control.pl new file mode 100755 index 00000000000..2ceb1d06c55 --- /dev/null +++ b/sw/ground_segment/cockpit/radio_control.pl @@ -0,0 +1,68 @@ +#!/usr/bin/perl -w +package RadioControl; + +my @paparazzi_lib; +BEGIN { + @paparazzi_lib = (defined $ENV{PAPARAZZI_SRC}) ? + ($ENV{PAPARAZZI_SRC}."/sw/lib/perl", $ENV{PAPARAZZI_SRC}."/sw/ground_segment/cockpit"):(); +} +use lib (@paparazzi_lib); + +use strict; +use Paparazzi::Environment; +use Paparazzi::RCTransmitter; +use Paparazzi::IvyProtocol; + + +use Getopt::Long; +use Tk; +use Ivy; + +use constant APP_ID => "Paparazzi RadioControl"; +use constant MESSAGE_WHEN_READY => APP_ID.': READY'; + +my $options = { + radio_file => "fc28.xml", + ivy_bus => "127.255.255.255:2010", + }; + +GetOptions ( + "r=s" => \$options->{radio_file}, + ); + + +my $mw = MainWindow->new(); +Paparazzi::RCTransmitter->new( + $mw, + -filename => Paparazzi::Environment::paparazzi_home()."/conf/radios/".$options->{radio_file} + )->pack(); +start_ivy(); +MainLoop(); + +my $self = {}; + +sub start_ivy { +# my ($self) = @_; + + Ivy->init (-ivyBus => $options->{ivy_bus}, + -appName => APP_ID, + -loopMode => 'TK', + -messWhenReady => MESSAGE_WHEN_READY, + ) ; + $self->{ivy} = Ivy->new (-statusFunc => \&ivyStatusCbk); + $self->{ivy}->start(); + my $paparazzi_home = Paparazzi::Environment::paparazzi_home(); + Paparazzi::IvyProtocol::read_protocol($paparazzi_home."/conf/messages.xml", "telemetry_fbw"); + Paparazzi::IvyProtocol::bind_message("telemetry_fbw", "RC", {}, $self->{ivy}, [$self, \&ivyOnRc]); +} + +sub ivyStatusCbk { + +} + +sub ivyOnRc { +# my ($self) = @_; + + + +} diff --git a/sw/ground_segment/modem/Makefile b/sw/ground_segment/modem/Makefile new file mode 100644 index 00000000000..88dfbb8d456 --- /dev/null +++ b/sw/ground_segment/modem/Makefile @@ -0,0 +1,40 @@ +# +# modem $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +ARCH = atmega8 +TARGET = modem_gnd +LOW_FUSE = 3f +HIGH_FUSE = cb +EXT_FUSE= ff +LOCK_FUSE= ff +INCLUDES= -I ../../include + +$(TARGET).srcs = \ + main.c \ + uart.c \ + soft_uart.c \ + adc.c \ + +include ../../../conf/Makefile.local +include ../../../conf/Makefile.avr + +clean : avr_clean diff --git a/sw/ground_segment/modem/README b/sw/ground_segment/modem/README new file mode 100644 index 00000000000..f7f32367e2d --- /dev/null +++ b/sw/ground_segment/modem/README @@ -0,0 +1,28 @@ +# +# $Id$ +# Copyright (C) 2004 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +This directory contains code for the mega8 MCU in the +ground modem. +The mega8 drives the CMX469 modem. This is necessary because +the crystal in the transmitting CMX469 is 4Mhz instead of +specified 4.032 . +This leads to incompatibility with the laptop baud generator. diff --git a/sw/ground_segment/modem/adc.c b/sw/ground_segment/modem/adc.c new file mode 100644 index 00000000000..05846774693 --- /dev/null +++ b/sw/ground_segment/modem/adc.c @@ -0,0 +1,39 @@ + +#include +#include +#include +#include + +#include "avr/std.h" +#include "adc.h" + +#define ANALOG_PORT PORTC +#define ANALOG_PORT_DIR DDRC +#define VALIM 7 + +uint16_t adc_alim; +volatile uint8_t adc_got_val; + +void adc_init( void ) +{ + /* Ensure that our port is for input with no pull-ups */ + ANALOG_PORT &= ~_BV(VALIM); + ANALOG_PORT_DIR &= ~_BV(VALIM); + + /* Select our external voltage ref, which is tied to Vcc and channel VALIM*/ + ADMUX = VALIM; + + /* Turn off the analog comparator */ + sbi( ACSR, ACD ); + + /* turn on the ADC, clock/128, interrupts, free running mode and starts conversion */ + ADCSRA = _BV(ADEN) | _BV(ADPS0) | _BV(ADPS1) | _BV(ADPS2) | _BV(ADIE) | _BV(ADFR) | _BV(ADSC); +} + + +SIGNAL( SIG_ADC ) +{ + /* Store result */ + adc_alim = ADCW; + adc_got_val = TRUE; +} diff --git a/sw/ground_segment/modem/adc.h b/sw/ground_segment/modem/adc.h new file mode 100644 index 00000000000..c5e0d77bf41 --- /dev/null +++ b/sw/ground_segment/modem/adc.h @@ -0,0 +1,8 @@ +#ifndef ADC_H +#define ADC_H + +void adc_init( void ); +extern uint16_t adc_alim; +extern volatile uint8_t adc_got_val; + +#endif diff --git a/sw/ground_segment/modem/link_tmtc.h b/sw/ground_segment/modem/link_tmtc.h new file mode 100644 index 00000000000..e58b75063b5 --- /dev/null +++ b/sw/ground_segment/modem/link_tmtc.h @@ -0,0 +1,92 @@ +#ifndef LINK_TMTC_H +#define LINK_TMTC_H + +#define STX 0x02 +#define ETX 0x03 + +#define MSG_DATA 0 +#define MSG_ERROR 1 +#define MSG_CD 2 +#define MSG_DEBUG 3 +#define MSG_VALIM 4 + + + +#define LINK_TMTC_SEND_DATA(data, _len) { \ + uint8_t checksum = 0; \ + const uint8_t real_len = 2+_len; \ + uint8_t i; \ + uart_putc(STX); \ + uart_putc(real_len); \ + checksum^=real_len; \ + uart_putc(MSG_DATA); \ + checksum^=MSG_DATA; \ + for (i=0; i<_len; i++) { \ + uart_putc(data[i]); \ + checksum^=data[i]; \ + } \ + uart_putc(checksum); \ + uart_putc(ETX); \ +} + +#define LINK_TMTC_SEND_ERROR(error) { \ + uint8_t checksum = 0; \ + const uint8_t real_len = 2+1; \ + uart_putc(STX); \ + uart_putc(real_len); \ + checksum^=real_len; \ + uart_putc(MSG_ERROR); \ + checksum^=MSG_ERROR; \ + uart_putc(error); \ + checksum^=error; \ + uart_putc(checksum); \ + uart_putc(ETX); \ +} + + +#define LINK_TMTC_SEND_CD(cd) { \ + uint8_t checksum = 0; \ + const uint8_t real_len = 2+1; \ + uart_putc(STX); \ + uart_putc(real_len); \ + checksum^=real_len; \ + uart_putc(MSG_CD); \ + checksum^=MSG_CD; \ + uart_putc(cd); \ + checksum^=cd; \ + uart_putc(checksum); \ + uart_putc(ETX); \ +} + +#define LINK_TMTC_SEND_DEBUG() { \ + uint8_t checksum = 0; \ + const uint8_t real_len = 2+1; \ + uart_putc(STX); \ + uart_putc(real_len); \ + checksum^=real_len; \ + uart_putc(MSG_DEBUG); \ + checksum^=MSG_DEBUG; \ + uart_putc(uart_nb_ovrrun); \ + checksum^=uart_nb_ovrrun; \ + uart_putc(checksum); \ + uart_putc(ETX); \ +} + +#define LINK_TMTC_SEND_VALIM(_valim) { \ + uint8_t checksum = 0; \ + const uint8_t real_len = 2+2; \ + uart_putc(STX); \ + uart_putc(real_len); \ + checksum^=real_len; \ + uart_putc(MSG_VALIM); \ + checksum^=MSG_VALIM; \ + uart_putc(*(uint8_t*)(_valim)); \ + checksum^= *(uint8_t*)(_valim); \ + uart_putc(* ((uint8_t*)(_valim) + 1)); \ + checksum^= *((uint8_t*)(_valim) + 1); \ + uart_putc(checksum); \ + uart_putc(ETX); \ +} + + +#endif diff --git a/sw/ground_segment/modem/main.c b/sw/ground_segment/modem/main.c new file mode 100644 index 00000000000..ed75ef36845 --- /dev/null +++ b/sw/ground_segment/modem/main.c @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include + + +#include "timer.h" +#include "soft_uart.h" +#include "adc.h" +#include "uart.h" +#include "link_tmtc.h" + +#define FALSE 0 +#define TRUE (!FALSE) + +static uint16_t cputime = 0; // seconds + +#define INPUT_BUF_LEN 10 +static uint8_t input_buf[INPUT_BUF_LEN]; +static uint8_t input_buf_idx = 0; + +static uint16_t saved_valim; + +inline void periodic_task( void ) { // 15 Hz + static uint8_t _1Hz = 0; + _1Hz++; + if (_1Hz>=15) _1Hz=0; + + if (!_1Hz) { + uint8_t cd_status = bit_is_set(SOFT_UART_CD_PIN, SOFT_UART_CD); + cputime++; + LINK_TMTC_SEND_CD(cd_status); + LINK_TMTC_SEND_VALIM(&saved_valim); + LINK_TMTC_SEND_DEBUG(); + } +} + +int main( void ) { + /* init peripherals */ + timer_init(); + uart_init(); + soft_uart_init(); + adc_init(); + sei(); + + /* enter mainloop */ + while( 1 ) { + if(timer_periodic()) + periodic_task(); + if (soft_uart_error) { + LINK_TMTC_SEND_ERROR(soft_uart_error); + soft_uart_error = 0; + } + if (soft_uart_got_byte) { + input_buf[input_buf_idx] = soft_uart_byte; + input_buf_idx++; + if (input_buf_idx >= INPUT_BUF_LEN) { + LINK_TMTC_SEND_DATA(input_buf, input_buf_idx); + input_buf_idx = 0; + } + soft_uart_got_byte = FALSE; + } + if (adc_got_val) { + saved_valim = adc_alim; + adc_got_val = FALSE; + } + } + return 0; +} diff --git a/sw/ground_segment/modem/soft_uart.c b/sw/ground_segment/modem/soft_uart.c new file mode 100644 index 00000000000..5e1bd4e475e --- /dev/null +++ b/sw/ground_segment/modem/soft_uart.c @@ -0,0 +1,80 @@ +#include "soft_uart.h" + +#include +#include +#include + +#define FALSE 0 +#define TRUE (!FALSE) + + +volatile uint8_t soft_uart_got_byte = FALSE; +uint8_t soft_uart_byte; +volatile uint8_t soft_uart_error = 0; + +#define RX_CLOCKED_DATA_PORT PORTB +#define RX_CLOCKED_DATA_DDR DDRB +#define RX_CLOCKED_DATA_PIN PINB +#define RX_CLOCKED_DATA 0 + + +void soft_uart_init(void) { + + /* set CD pin as input, no pullup */ + SOFT_UART_CD_DDR &= ~_BV(SOFT_UART_CD); + SOFT_UART_CD_PORT &= ~_BV(SOFT_UART_CD); + + /* set DATA pin as input no pullup*/ + RX_CLOCKED_DATA_DDR &= ~_BV(RX_CLOCKED_DATA); + RX_CLOCKED_DATA_PORT &= ~_BV(RX_CLOCKED_DATA); + + /* setup rx interrupt on failing edge of clock */ + MCUCR = _BV(ISC11); + /* clear interrupt flag */ + sbi(GIFR, INTF1); + /* enable interrupt */ + sbi(GICR, INT1); +} + + +SIGNAL(SIG_INTERRUPT1) { + static uint8_t rx_buf_idx = 0; + static uint8_t rx_buf; + + if (bit_is_clear(SOFT_UART_CD_PIN, SOFT_UART_CD)) { + rx_buf_idx = 0; + } + else { + if (rx_buf_idx==0) { + // start bit + if (bit_is_clear(RX_CLOCKED_DATA_PIN, RX_CLOCKED_DATA)) { + rx_buf = 0; + rx_buf_idx++; + } + } + else if (rx_buf_idx < 9) { + // data bits + rx_buf >>= 1; + if (bit_is_set(RX_CLOCKED_DATA_PIN, RX_CLOCKED_DATA)) + rx_buf |= 0x80; + rx_buf_idx++; + } + else { + // stop bit + if (bit_is_set(RX_CLOCKED_DATA_PIN, RX_CLOCKED_DATA)) { + if (soft_uart_got_byte) { + soft_uart_error = RX_ERROR_OVERRUN; + } + else { + soft_uart_byte = rx_buf; + soft_uart_got_byte = TRUE; + } + } + else { + // framing error + soft_uart_error = RX_ERROR_FRAMING; + } + rx_buf_idx = 0; + } + } +} diff --git a/sw/ground_segment/modem/soft_uart.h b/sw/ground_segment/modem/soft_uart.h new file mode 100644 index 00000000000..a1a034c58a4 --- /dev/null +++ b/sw/ground_segment/modem/soft_uart.h @@ -0,0 +1,21 @@ +#ifndef SOFT_UART_H +#define SOFT_UART_H + +#include + +extern volatile uint8_t soft_uart_got_byte; +extern uint8_t soft_uart_byte; + +#define RX_ERROR_FRAMING 1 +#define RX_ERROR_OVERRUN 2 +extern volatile uint8_t soft_uart_error; + +#define SOFT_UART_CD_PORT PORTD +#define SOFT_UART_CD_DDR DDRD +#define SOFT_UART_CD_PIN PIND +#define SOFT_UART_CD 6 + +void soft_uart_init(void); + + +#endif diff --git a/sw/ground_segment/modem/timer.h b/sw/ground_segment/modem/timer.h new file mode 100644 index 00000000000..c71ac78e83a --- /dev/null +++ b/sw/ground_segment/modem/timer.h @@ -0,0 +1,91 @@ +/* + * Paparazzi mcu0 timer functions + * + * Copied from autopilot (autopilot.sf.net) thanx alot Trammell + * + * Copyright (C) 2002 Trammell Hudson + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef TIMER_H +#define TIMER_H + +#include +#include +#include + + +/* + * Enable Timer1 (16-bit) running at Clk/1 for the global system + * clock. This will be used for computing the servo pulse widths, + * PPM decoding, etc. + * + * Low frequency periodic tasks will be signaled by timer 0 + * running at Clk/1024. For 4 Mhz clock, this will be every + * 65536 microseconds, or 15 Hz. + */ +static inline void timer_init( void ) { + /* Timer0 @ Clk/64: Software UART */ +/* TCCR0 = 0x03; */ + + /* Timer1 @ Clk/1: System clock, ppm and servos */ + // TCCR1A = 0x00; + // TCCR1B = 0x01; + + /* Timer2 @ Clk/1024: Periodic clock*/ + TCCR2 = 0x07; +} + + +/* + * Retrieve the current time from the global clock in Timer1, + * disabling interrupts to avoid stomping on the TEMP register. + * If interrupts are already off, the non_atomic form can be used. + */ +static inline uint16_t +timer_now( void ) +{ + return TCNT1; +} + +static inline uint16_t +timer_now_non_atomic( void ) +{ + return TCNT1L; +} + + +/* + * Periodic tasks occur when Timer2 overflows. Check and unset + * the overflow bit. We cycle through four possible periodic states, + * so each state occurs every 30 Hz. + */ +static inline uint8_t +timer_periodic( void ) +{ + if( !bit_is_set( TIFR, TOV2 ) ) + return 0; + + TIFR = 1 << TOV2; + return 1; +} + +#endif diff --git a/sw/ground_segment/modem/uart.c b/sw/ground_segment/modem/uart.c new file mode 100644 index 00000000000..90701bef833 --- /dev/null +++ b/sw/ground_segment/modem/uart.c @@ -0,0 +1,81 @@ +#include +#include +#include +#include "uart.h" + + +uint8_t uart_nb_ovrrun = 0; + +#define TX_BUF_SIZE 100 + +static volatile uint8_t tx_head = TX_BUF_SIZE - 1; +static volatile uint8_t tx_tail = TX_BUF_SIZE - 1; +static uint8_t tx_buf[ TX_BUF_SIZE ]; + + +/* + * UART Baud rate generation settings: + * + * With 16.0 MHz clock,UBRR=25 => 38400 baud + * With 8.0 Mhz clock, UBRR=12 => 38400 baud + * + * With 4.0 MHz UBRR=12 + ub2X=1 -> 38400 baud + */ + +void uart_init( void ) { + /* Baudrate is 38.4k */ + UBRRH = 0; + UBRRL = 12; + /* double speed */ + UCSRA = _BV(U2X); + /* Enable transmitter */ + UCSRB = _BV(TXEN); + /* Set frame format: 8data, 1stop bit */ + UCSRC = _BV(URSEL) | _BV(UCSZ1) | _BV(UCSZ0); +} + + +static inline void load_next_byte( void ) { + uint8_t tmp_tail; + /* load a new byte */ + tmp_tail = tx_tail + 1; + if( tmp_tail >= TX_BUF_SIZE ) + tmp_tail = 0; + tx_tail = tmp_tail; + UDR = tx_buf[tx_tail]; +} + +void uart_putc( unsigned char c ) { + uint8_t tmp_head; + + tmp_head = tx_head + 1; + if( tmp_head >= TX_BUF_SIZE ) + tmp_head = 0; + /* if buffer is full do nothing */ + if( tmp_head == tx_tail ) { + uart_nb_ovrrun++; + return; + } + + /* copy data to buffer */ + tx_buf[ tmp_head ] = c; + /* update head */ + tx_head = tmp_head; + + /* if we were not allready transmitting */ + if (bit_is_clear(UCSRB, TXCIE)) { + /* load a byte */ + load_next_byte(); + /* enable interrupt */ + sbi(UCSRB, TXCIE); + } +} + +SIGNAL( SIG_UART_TRANS ) { + /* if we have nothing left to transmit */ + if( tx_head == tx_tail ) + /* disable data register empty interrupt */ + cbi(UCSRB, TXCIE); + else + load_next_byte(); +} diff --git a/sw/ground_segment/modem/uart.h b/sw/ground_segment/modem/uart.h new file mode 100644 index 00000000000..be7f03d11b2 --- /dev/null +++ b/sw/ground_segment/modem/uart.h @@ -0,0 +1,19 @@ +#ifndef _UART_H_ +#define _UART_H_ + +#include +#include +#include +#include + + + +/************************************************************************* + * + * UART code. + */ + +void uart_init( void ); +void uart_putc( unsigned char c ); +extern uint8_t uart_nb_ovrrun; +#endif diff --git a/sw/ground_segment/speech/README b/sw/ground_segment/speech/README new file mode 100644 index 00000000000..d2a50a2e89e --- /dev/null +++ b/sw/ground_segment/speech/README @@ -0,0 +1,25 @@ +# +# $Id$ +# Copyright (C) 2004 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +This directory contains code that use the festival speech engine to +pronounce warnings and parameters. +It uses Ivy diff --git a/sw/ground_segment/speech/paparazzi_speak.pl b/sw/ground_segment/speech/paparazzi_speak.pl new file mode 100755 index 00000000000..e26095037ca --- /dev/null +++ b/sw/ground_segment/speech/paparazzi_speak.pl @@ -0,0 +1,216 @@ +#!/usr/bin/perl -w + +package PaparazziSpeak; + +my @paparazzi_lib; +BEGIN { + @paparazzi_lib = (defined $ENV{PAPARAZZI_SRC}) ? + ($ENV{PAPARAZZI_SRC}."/sw/lib/perl"):(); +} +use lib (@paparazzi_lib); + +use strict; +use Paparazzi::Environment; + +use constant APP_ID => "Paparazzi Speaker"; +use constant MESSAGE_WHEN_READY => APP_ID." : READY"; + +use strict; + +use IO::Socket; +use POSIX; +use Getopt::Long; +use Ivy; + +use Paparazzi::IvyProtocol; + + +sub new() { + + my ($proto, $festd_host, $festd_port) = @_; + my $self = { + 'ivy' => undef, + 'festival_handle' => undef, + 'vbat' => 0, + 'cur_wp' => -1, + 'cnt_nav' => 0, + }; + $self->{options} = { +# paparazzi_home => $paparazzi_home, + ivy_bus => "127.255.255.255:2010", + }; + + bless $self; + $self->parse_args(); + $self->start_ivy(); + $self->connect_to_festival($festd_host, $festd_port); + $self->say_hello(); + + # Trap signal in order to exit cleanly + $SIG{TERM} = \&catchSigTerm ; + + return $self; +} + +sub parse_args { + my ($self) = @_; + my $options = $self->{options}; + GetOptions ("b=s" => \$options->{ivy_bus}, + "t=s" => \$options->{paparazzi_home}, + ); +} + +sub start_ivy() { + my ($self) = @_; + Ivy->init (-ivyBus => $self->{options}->{ivy_bus}, + -appName => APP_ID, + -loopMode => 'LOCAL', + -messWhenReady => MESSAGE_WHEN_READY, + ) ; + my $paparazzi_home = Paparazzi::Environment::paparazzi_home(); + Paparazzi::IvyProtocol::read_protocol($paparazzi_home."/conf/messages.xml", "ground"); + Paparazzi::IvyProtocol::read_protocol($paparazzi_home."/conf/messages.xml", "aircraft_info"); + + $self->{ivy} = Ivy->new (-statusFunc => \&ivyStatusCbk) ; +# $self->{ivy}->bindRegexp (IvyMsgs::CALIB_START_Regexp(), [$self, \&ivyOnCalibStart]); +# $self->{ivy}->bindRegexp (IvyMsgs::CALIB_CONTRAST_Regexp(), [$self, \&ivyOnCalibContrast]); +# $self->{ivy}->bindRegexp (IvyMsgs::NAVIGATION_Regexp(), [$self, \&ivyOnNavigation]); +# $self->{ivy}->bindRegexp (IvyMsgs::BAT_Regexp(), [$self, \&ivyOnBat]); +# $self->{ivy}->bindRegexp (IvyMsgs::PPRZ_MODE_Regexp(), [$self, \&ivyOnPprzMode]); +# $self->{ivy}->bindRegexp (IvyMsgs::TAKEOFF_Regexp(), [$self, \&ivyOnTakeOff]); + $self->{ivy}->start() ; +} + +sub catchSigTerm() { + print ("in catchSigTerm\n"); + +} + +sub ivyStatusCbk { + print ("in ivyStatusCbk\n"); +} + +sub say_hello() { + my ($self) = @_; + $self->speak('Hello. Welcome to Paparazzi.'); +} + +sub ivyOnNavigation() { + my ($self, $sender, $cur_wp, $pos_x, $pos_y, $desired_course, $dist2_wp, $course_pgain) = @_; + # printf("NAVIGATION wp $cur_wp, x $pos_x, y $pos_y, dc $desired_course, d2wp $dist2_wp, cpg $course_pgain\n"); + + if ($self->{cur_wp} != $cur_wp) { + $self->speak(sprintf("current waypoint : %s.", $cur_wp)); + $self->{cur_wp} = $cur_wp; + $self->{cnt_nav} = 0; + } + else { + my $rdist = floor(sqrt($dist2_wp)/10)*10; + printf "dist2wp $rdist\n"; + if (($rdist ge 100 and $self->{cnt_nav} == 16) or + ($rdist ge 20 and $rdist le 100 and ($self->{cnt_nav})%5 == 0)) { + $self->speak(sprintf("distance to waypoint : %s. meters", $rdist)); + } + } + $self->{cnt_nav}++; +} + +sub ivyOnBat() { + my ($self, $sender, $voltage, $flight_time, $low_battery) = @_; + my $vbat = $voltage/10; + + if ($voltage le $low_battery) { + if ($self->{vbat} != $vbat) { + $self->speak(sprintf("battery : Warning : battery low : %s volts.", $vbat)); + $self->{vbat} = $vbat; + } + } + else { + if (abs($self->{vbat} - $vbat) ge 0.2) { + $self->speak(sprintf("battery : %s volts.", $vbat)); + $self->{vbat} = $vbat; + } + } +} + +sub ivyOnPprzMode() { + my @autopilot_mode_name=("manual", "auto one", "auto two", "home"); + my ($self, $sender, $ap_mode, $ap_altitude, $if_calib_mode, $mcu1_status, $lls_calib) = @_; + if ($self->{ap_mode} != $ap_mode) { + my $ap_str = $autopilot_mode_name[$ap_mode]; + $self->speak(sprintf("autopilot mode : %s.", $ap_str)); + $self->{ap_mode} = $ap_mode; + } +} + +sub ivyOnCalibStart() { + my ($self, $sender) = @_; + $self->speak("contrast calibration triggered"); +} + +sub ivyOnCalibContrast() { + my ($self, $sender, $adc) = @_; + my $pc_contrast = ceil($adc / 1024 * 100); + my $txt = sprintf("contrast %sper cent", $pc_contrast); + print "txt $txt\n"; + $self->speak($txt); +} + +sub ivyOnTakeOff() { + my ($self, $sender) = @_; + $self->speak('Take Off'); +} + +sub speak() { + my ($self, $what) = @_; + my $handle = $self->{festival_handle}; + my $sable_fmt = + ' + + + + %s + + + '; + + my $cmd_fmt = sprintf("(tts_text \"%s\" \'sable)\n", $sable_fmt); + my $fest_cmd = sprintf $cmd_fmt, $what; + print $handle $fest_cmd; +} + +sub connect_to_festival() { + my ($self, $host, $port) = @_; + $self->{festival_handle} = IO::Socket::INET->new(Proto => "tcp", + PeerAddr => $host, + PeerPort => $port); + $self->{festival_handle}->autoflush(1); + Ivy->fileEvent($self->{festival_handle}, [\&FestivalOnReceive, $self]); + print STDERR "[Connected to $host:$port]\n"; +} + +sub FestivalOnReceive { + my ($self) = @_; + my $file_stuff_key = "ft_StUfF_key"; # defined in speech tools + print "FestivalOnReceive\n"; + my $handle = $self->{festival_handle}; + my $line = <$handle>; + + # print "line: [$line]\n"; +# if ($line eq "WV\n") { # we have a waveform coming +# print "Waveform\n"; +# } + +# if ($line eq "LP\n") { # we have a waveform coming +# print "Lisp\n"; +# } +# if ($line =~ s/$file_stuff_key(.*)$//s) { +# print STDOUT $line; +# } +} + + + +PaparazziSpeak->new("localhost", 1314); +Ivy->mainLoop(); + diff --git a/sw/ground_segment/tmtc/Makefile b/sw/ground_segment/tmtc/Makefile new file mode 100644 index 00000000000..0f32da2dbe0 --- /dev/null +++ b/sw/ground_segment/tmtc/Makefile @@ -0,0 +1,54 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +include ../../../conf/Makefile.local + +all: messages.cmo modem.cmo receive.cmo receive.opt messages.opt + +clean: + rm -f receive *.bak *~ core *.o .depend *.opt *.out *.cm* + +OCAMLC = ocamlc +OCAMLOPT = ocamlopt +INCLUDES= -I ../../lib/ocaml -I +lablgtk2 + +messages.opt : messages.ml + $(OCAMLOPT) $(INCLUDES) -o $@ unix.cmxa xml-light.cmxa glibivy-ocaml.cmxa -I +lablgtk2 lablgtk.cmxa gtkInit.cmx str.cmxa lib.cmxa $^ + strip $@ + +messages.run: + lablgtk2 str.cma -I ../../lib/ocaml ivy-ocaml.cma lib.cma messages.cmo + +receive.opt : modem.ml receive.ml + $(OCAMLOPT) $(INCLUDES) -o $@ str.cmxa unix.cmxa xml-light.cmxa glibivy-ocaml.cmxa -I +lablgtk2 lablgtk.cmxa lib.cmxa $^ + strip $@ + +receive.out : modem.ml receive.ml + $(OCAMLC) -g $(INCLUDES) -o $@ str.cma unix.cma xml-light.cma glibivy-ocaml.cma -I +lablgtk2 lablgtk.cma lib.cma $^ + + +receive.run: + lablgtk2 str.cma -I ../../lib/ocaml ivy-ocaml.cma lib.cma modem.cmo receive.ml + + +%.cmo : %.ml + $(OCAMLC) $(INCLUDES) -c $< diff --git a/sw/ground_segment/tmtc/bilink.ml b/sw/ground_segment/tmtc/bilink.ml new file mode 100644 index 00000000000..b2d08241501 --- /dev/null +++ b/sw/ground_segment/tmtc/bilink.ml @@ -0,0 +1,44 @@ +(* ocamlc -I ../../lib/ocaml unix.cma -I +lablgtk2 lablgtk.cma lib.cma bilink.ml *) + +(* Adresse carte sol : 01 18 04 c0 00 4f *) +(* Adresse carte embarquee : 01 18 04 c0 00 51 *) +let send = fun fd com -> + Wavecard.send fd com; + flush (Unix.out_channel_of_descr fd) + +(* Wavecard.send fd ("REQ_READ_RADIO_PARAM", "\000"); *) +(* Wavecard.send fd ("REQ_FIRMWARE_VERSION", ""); *) +(* Wavecard.send fd ("REQ_READ_RADIO_PARAM", "\005"); *) +(* Wavecard.send fd ("REQ_SEND_SERVICE", "\255\255\255\255\255\255\032"); *) +(* Wavecard.send fd ("REQ_SEND_SERVICE", "\001\024\004\192\000\079\032"); *) +(* Wavecard.send fd ("REQ_READ_REMOTE_RSSI", "\001\024\004\192\000\079"); *) +(* Wavecard.send fd ("REQ_SEND_MESSAGE", "\001\024\004\192\000\079HELLO WORLD");*) + + +let send_ack = fun delay fd -> + GMain.Timeout.add delay (fun _ -> send ("ACK", "")) + + +let print_cmd = fun (name, data) -> + Printf.fprintf stderr "%s:" name; + for i = 0 to String.length data - 1 do + Printf.fprintf stderr " %02x" (Char.code data.[i]) + done; + Printf.fprintf stderr "\n"; flush stderr + +let _ = + let dev = ref "/dev/ttyS0" in + Arg.parse + [ "-d", Arg.String (fun x -> dev := x), "Device\tDefault is /dev/ttyS0"] + (fun x -> prerr_endline ("Warning: don't know what to do with "^x)) + "Usage: "; + + let fd = if !dev = "" then Unix.stdin else Serial.opendev !dev Serial.B9600 in + + ignore (GMain.Timeout.add 2000 (fun _ -> send fd; true)); + + let cb = Wavecard.receive ~ack:(send_ack 100) print_cmd in + + ignore (GMain.Io.add_watch `IN (fun () -> cb fd; true) (GMain.Io.channel_of_descr fd)); + + GMain.Main.main () diff --git a/sw/ground_segment/tmtc/messages.ml b/sw/ground_segment/tmtc/messages.ml new file mode 100644 index 00000000000..cfa238b85e6 --- /dev/null +++ b/sw/ground_segment/tmtc/messages.ml @@ -0,0 +1,139 @@ +open Printf + +let update_delay = 1. (* Min time in second before two updates *) +let led_delay = 500 (* Time in milliseconds while the green led is displayed *) + + +let space = Str.regexp "[ \t]+" + +let (//) = Filename.concat + +let xml_file = Env.paparazzi_src // "conf" // "messages.xml" + +(* let port = ref 2010 + let domain = ref "127.255.255.255" *) + +let green = "#00e000" +let red = "#ff0000" +let black = "#000000" +let yellow = "#ffff00" + +let led2 color1 color2 = [| +"16 16 5 1"; +" c None"; +". c black"; +"X c white"; +"o c "^color1; +"O c "^color2; +" ...... "; +" ........XX "; +" ...ooooooXXX "; +" ..ooooooooooXX "; +" ..oooXXXooooXX "; +"..oooXXXooooooXX"; +"..ooXXooooooooXX"; +"..ooXXooooooooXX"; +"..ooXoooooooooXX"; +"..ooooooooooooXX"; +"..ooooooooooooXX"; +" ..ooooooooooXX "; +" ..ooooooooooXX "; +" ...ooooooXXX "; +" ..XXXXXXXX "; +" XXXXXX "|] + +let led color = led2 color "None" + +let format = fun field -> + try + match Xml.attrib field "type", Xml.attrib field "format" with + "float", f -> fun x -> Printf.sprintf (Obj.magic f) (float_of_string x) + | _ -> fun x -> x + with _ -> fun x -> x + + + +open GMain +let _ = + let bus = ref "127.255.255.255:2010" in + let classes = ref ["telemetry_ap";"ground"] in + Arg.parse + [ "-b", Arg.String (fun x -> bus := x), "Bus\tDefault is 127.255.255.25:2010"; + "-c", Arg.String (fun x -> classes := x :: !classes), "class name"] + (fun x -> prerr_endline ("WARNING: don't do anything with "^x)) + "Usage: "; + + let xml = Xml.parse_file xml_file in + + let window = GWindow.window ~title:"Paparazzi messages" () in + let quit = fun () -> GMain.Main.quit (); exit 0 in + ignore (window#connect#destroy ~callback:quit); + + let notebook = GPack.notebook ~packing:window#add ~tab_pos:`LEFT () in + + let pm = fun color -> + GDraw.pixmap_from_xpm_d ~data:(led color) ~window:window () in + let black_led = pm black + and green_led = pm green + and yellow_led = pm yellow + and red_led = pm red in + + let xml_classes = + List.filter (fun x -> List.mem (Xml.attrib x "name") !classes) (Xml.children xml) in + + let messages = List.flatten (List.map Xml.children xml_classes) in + + let pages = + List.map + (fun m -> + let id = (Xml.attrib m "name") in + let h = GPack.hbox () in + let v = GPack.vbox ~width:200 () in + let l = GMisc.label ~text:id ~packing:h#add () in + let led = GMisc.pixmap black_led ~packing:h#pack () in + let time = GMisc.label ~text:"___" ~packing:h#pack () in + notebook#append_page ~tab_label:h#coerce v#coerce; + let fields = + List.map + (fun f -> + let h = GPack.hbox ~packing:v#pack () in + let unit = try "("^Xml.attrib f "unit"^")" with _ -> "" in + let name = Printf.sprintf "%s %s %s: " (Xml.attrib f "type") (Xml.attrib f "name") unit in + let _ = GMisc.label ~text:name ~packing:h#pack () in + let l = GMisc.label ~text:"XXXX" ~packing:h#pack () in + fun x -> let fx = format f x in if l#label <> fx then l#set_text fx + ) + (Xml.children m) in + let n = List.length fields in + let last_update = ref (Unix.gettimeofday ()) in + let time_since_last = ref 0 in + ignore (GMain.Timeout.add 1000 (fun () -> incr time_since_last; time#set_text (sprintf "%2d" !time_since_last); true)); + let display = fun line -> + time_since_last := 0; + let t = Unix.gettimeofday () in + if t > !last_update +. update_delay then begin + last_update := t; + let args = Str.split space line in + try + List.iter2 (fun f x -> f x) fields args; + + led#set_pixmap green_led; + ignore (GMain.Timeout.add led_delay (fun () -> led#set_pixmap yellow_led; false)) + with + Invalid_argument "List.iter2" -> + led#set_pixmap red_led; + Printf.fprintf stderr "%s: expected %d, got %d (%s)\n" id n (List.length args) line; flush stderr + end + in + let regexp = Printf.sprintf "[\\.0-9]+ %s (.*)" id in + ignore (Ivy.bind (fun _ args -> display args.(0)) regexp); + (id, (led, fields)) + ) + messages in + + window#show (); + + Ivy.init "Paparazzi messages" "READY" (fun _ _ -> ()); + Ivy.start !bus; + + GMain.Main.main () diff --git a/sw/ground_segment/tmtc/modem.ml b/sw/ground_segment/tmtc/modem.ml new file mode 100644 index 00000000000..cde4bce982d --- /dev/null +++ b/sw/ground_segment/tmtc/modem.ml @@ -0,0 +1,112 @@ +(* + * $Id$ + * + * Ground harware modem handling + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf + +module Protocol = struct +(* Header: STX, length of (payload + checksum) *) +(* Payload: tag, data *) +(* Tailer : checksum, ETX *) + + let stx = Char.chr 0x02 + let etx = 0x03 + let index_start = fun buf -> + String.index buf stx + + let payload_length = fun buf start -> + Char.code buf.[start+1] - 1 + + let length = fun buf start -> + let len = String.length buf - start in + if len >= 2 then + Char.code buf.[start+1] + 3 + else + raise Serial.Not_enough + + let checksum = fun msg -> + let l = String.length msg in + let ck_a = ref 0 in + for i = 1 to l - 3 do + ck_a := Char.code msg.[i] lxor !ck_a + done; + !ck_a = Char.code msg.[l-2] && Char.code msg.[l-1] = etx +end + +let msg_data = 0 +let msg_error = 1 +let msg_cd = 2 +let msg_debug = 3 +let msg_valim = 4 + +type status = { + mutable last_message_date : float; + mutable valim : float; + mutable cd : int; + mutable error : int; + mutable debug : int; + mutable nb_byte : int; + mutable nb_msg : int; + mutable nb_err : int + } + +let max_stalled_time = 2. + +let status = { + last_message_date = Unix.gettimeofday () -. max_stalled_time; (* FIXME *) + valim = 0.; + cd = 0; + error = 0; + debug = 0; + nb_byte = 0; + nb_msg = 0; + nb_err = 0 +} +(* FIXME *) + let valim = fun x -> float x *. 0.0162863 -. 1.17483 +(* FIXME *) + +let parse = fun msg -> + let len = String.length msg in + let id = Char.code msg.[2] in + if id = msg_data then + Some (String.sub msg 3 (len-5)) + else begin + begin + match id with + | x when x = msg_error -> + status.error <- (Char.code msg.[3]) + | x when x = msg_cd -> + status.cd <- (Char.code msg.[3]) + | x when x = msg_debug -> + status.debug <- (Char.code msg.[3]) + | x when x = msg_valim -> + status.valim <- (valim (Char.code msg.[4] * 0x100 + Char.code msg.[3])); + printf "valim=%f\n" status.valim; flush stdout; + | _ -> (* Uncorrect id *) + status.nb_err <- status.nb_err + 1 + end; + None + end diff --git a/sw/ground_segment/tmtc/receive.ml b/sw/ground_segment/tmtc/receive.ml new file mode 100644 index 00000000000..93af00e9ef3 --- /dev/null +++ b/sw/ground_segment/tmtc/receive.ml @@ -0,0 +1,306 @@ +(* + * $Id$ + * + * Multi aircrafts receiver, logger and broadcaster + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf +module U = Unix + +module ModemTransport = Serial.Transport(Modem.Protocol) +module Tele_Class = struct let name = "telemetry_ap" end +module AcInfo = struct let name = "aircraft_info" end +module Tele_Pprz = Pprz.Protocol(Tele_Class) +module AcInfo_Pprz = Pprz.Protocol(AcInfo) +module PprzTransport = Serial.Transport(Tele_Pprz) + + +let listen_pprz_modem = fun use_pprz_message tty -> + (*** let fd = Serial.opendev tty Serial.B4800 in + ***) prerr_endline tty; + let fd = U.stdin in (***) + let use_pprz_buf = fun buf -> + Debug.call 'T' (fun f -> fprintf f "use_pprz: %s\n" (Debug.xprint buf)); + use_pprz_message (Tele_Pprz.values_of_bin buf) in + let buffer = ref "" in + let use_modem_message = fun msg -> + Debug.call 'T' (fun f -> fprintf f "use_modem: %s\n" (Debug.xprint msg)); + match Modem.parse msg with + None -> () (* Only internal modem data *) + | Some data -> + let b = !buffer ^ data in + Debug.call 'T' (fun f -> fprintf f "Pprz buffer: %s\n" (Debug.xprint b)); + let x = PprzTransport.parse use_pprz_buf b in + buffer := String.sub b x (String.length b - x) + in + let scanner = Serial.input (ModemTransport.parse use_modem_message) in + let cb = fun _ -> + begin + try + scanner fd + with + e -> fprintf stderr "%s\n" (Printexc.to_string e) + end; + true in + + ignore (Glib.Io.add_watch [`IN] cb (Glib.Io.channel_of_descr fd)) + +let fos = float_of_string +let ios = int_of_string +let space = Str.regexp "[ \t]+" + +let (//) = Filename.concat +let logs_path = Env.paparazzi_home // "var" // "logs" +let conf_xml = Xml.parse_file (Env.paparazzi_home // "conf" // "conf.xml") + +type port = Ivy of string | Modem of string +type aircraft = { + port : port; + mutable roll : float; + mutable pitch : float; + mutable east : float; + mutable north : float; + mutable gspeed : float; + mutable course : float; + mutable alt : float; + mutable climb : float; + mutable cur_block : int; + mutable cur_stage : int; +(* warning twin engines ?? *) + mutable throttle : float; + mutable rpm : float; + mutable temp : float; + mutable bat : float; + mutable amp : float; + mutable energy : float; + mutable ap_mode : int; + mutable ap_altitude : int; + mutable if_calib_mode : int; + mutable mcu1_status : int; + mutable lls_calib : int; + } + +(** The aircrafts store *) +let aircrafts = Hashtbl.create 3 + +(** Broadcast of the received aircrafts *) +let aircrafts_msg_period = 5000 (* ms *) +let aircraft_msg_period = 1000 (* ms *) +let send_aircrafts_msg = fun () -> + let t = U.gettimeofday () in + let names = String.concat "," (Hashtbl.fold (fun k v r -> k::r) aircrafts []) in + Ivy.send (sprintf "ground AIRCRAFTS %s" names) +(* Ivy.send (sprintf "YOUOPIIIII") *) + +(* Opens the log file *) +(* FIXME : shoud open also an associated config file *) +let logger = fun () -> + let d = U.localtime (U.gettimeofday ()) in + let name = sprintf "%02d_%02d_%02d__%02d_%02d_%02d.log" (d.U.tm_year mod 100) (d.U.tm_mon+1) (d.U.tm_mday) (d.U.tm_hour) (d.U.tm_min) (d.U.tm_sec) in + if not (Sys.file_exists logs_path) then begin + printf "Creating '%s'\n" logs_path; flush stdout; + ignore (Sys.command (sprintf "mkdir -p %s" logs_path)) + end; + open_out (logs_path // name) + + +let log_and_parse = fun log ac_name a msg values -> + let t = U.gettimeofday () in + let s = String.concat " " (List.map snd values) in + fprintf log "%.2f %s %s %s\n" t ac_name msg.Pprz.name s; flush log; + Ivy.send (sprintf "%s RAW %.2f %s %s" ac_name t msg.Pprz.name s); + let value = fun x -> try List.assoc x values with Not_found -> failwith (sprintf "Error: field '%s' not found\n" x) in + let fvalue = fun x -> fos (value x) in + match msg.Pprz.name with + "GPS" -> + a.east <- fvalue "east" /. 100.; + a.north <- fvalue "north" /. 100.; + a.gspeed <- fvalue "speed"; + a.course <- fvalue "course"; + a.alt <- fvalue "alt"; + a.climb <- fvalue "climb" + | "ATTITUDE" -> + a.roll <- fvalue "phi"; + a.pitch <- fvalue "theta" + | "NAVIGATION" -> + a.cur_block <- ios (value "cur_block"); + a.cur_stage <- ios (value "cur_stage") + | "CLIMB_PID" -> + a.throttle <- fvalue "gaz" /. 9600. *. 100.; + a.rpm <- a.throttle *. 100. + | "BAT" -> + a.bat <- fvalue "voltage" /. 10. + | "PPRZ_MODE" -> + a.ap_mode <- ios (value "ap_mode"); + a.ap_altitude <- ios (value "ap_altitude"); + a.if_calib_mode <- ios (value "if_calib_mode"); + a.mcu1_status <- ios (value "mcu1_status"); + a.lls_calib <- ios (value "lls_calib") + | _ -> () + + +(** Callback for a message from a soft simulator *) +let sim_msg = fun log ac_name a m -> + try + let (msg_id, values) = Tele_Pprz.values_of_string m in + let msg = Tele_Pprz.message_of_id msg_id in + log_and_parse log ac_name a msg values + with + Pprz.Unknown_msg_name x -> + fprintf stderr "Unknown message %s from %s: %s\n" x ac_name m + +let soi = string_of_int + +let send_aircraft_msg = fun ac -> + try + let sof = fun f -> sprintf "%.1f" f in + let a = Hashtbl.find aircrafts ac in + let values = ["roll", sof (Geometry_2d.rad2deg a.roll); + "pitch", sof (Geometry_2d.rad2deg a.pitch); + "east", sof a.east; + "north", sof a.north; + "speed", sof a.gspeed; + "heading", sof (Geometry_2d.rad2deg a.course); + "alt", sof a.alt; + "climb", sof a.climb] in + let _, fp_msg = AcInfo_Pprz.message_of_name "FLIGHT_PARAM" in + Ivy.send (sprintf "%s %s" ac (AcInfo_Pprz.string_of_message fp_msg values)); + + let values = ["cur_block", soi a.cur_block;"cur_stage", soi a.cur_stage] + and _, ns_msg = AcInfo_Pprz.message_of_name "NAV_STATUS" in + Ivy.send (sprintf "%s %s" ac (AcInfo_Pprz.string_of_message ns_msg values)); + + let values = ["throttle", sof a.throttle;"rpm", sof a.rpm;"temp", sof a.temp;"bat", sof a.bat;"amp", sof a.amp;"energy", sof a.energy] + and _, es_msg = AcInfo_Pprz.message_of_name "ENGINE_STATUS" in + Ivy.send (sprintf "%s %s" ac (AcInfo_Pprz.string_of_message es_msg values)); + + let values = ["mode", soi a.ap_mode; "v_mode", soi a.ap_altitude] + and _, as_msg = AcInfo_Pprz.message_of_name "AP_STATUS" in + Ivy.send (sprintf "%s %s" ac (AcInfo_Pprz.string_of_message as_msg values)) + with + Not_found -> prerr_endline ac + +let new_aircraft = fun id -> + { port = id ; roll = 0.; pitch = 0.; east = 0.; north = 0.; gspeed=0.; course = 0.; alt=0.; climb=0.; cur_block=0; cur_stage=0; throttle = 0.; rpm = 0.; temp = 0.; bat = 0.; amp = 0.; energy = 0.; ap_mode=0; ap_altitude=0; if_calib_mode=0; mcu1_status=0; lls_calib=0 } + +let register_aircraft = fun name a -> + Hashtbl.add aircrafts name a; + ignore (Glib.Timeout.add aircraft_msg_period (fun () -> send_aircraft_msg name; true)) + + +(** Callback of an identifying message from a soft simulator *) +let ident_msg = fun log id name -> + if not (Hashtbl.mem aircrafts name) then begin + let ac = new_aircraft (Ivy id) in + let b = Ivy.bind (fun _ args -> sim_msg log name ac args.(0)) (sprintf "^%s +(.*)" id) in + register_aircraft name ac + end + +(* Waits for new simulated aircrafts *) +let listen_sims = fun log -> + ignore (Ivy.bind (fun _ args -> ident_msg log args.(0) args.(1)) "^(.*) IDENT +(.*)") + +(* Server on the Ivy bus *) +let send_flight_plan = fun id -> + try + let conf = ExtXml.child conf_xml "aircraft" ~select:(fun x -> ExtXml.attrib x "name" = id) in + let f = ExtXml.attrib conf "flight_plan" in + Ivy.send (sprintf "ground FLIGHT_PLAN %s file://%s/conf/%s" id Env.paparazzi_home f) + with + Not_found -> + Ivy.send (sprintf "ground UNKNOWN %s" id) + +let send_config = fun id_ac id_req -> + try + prerr_endline (sprintf "[%s] [%s]\n" id_ac id_req); + let conf = ExtXml.child conf_xml "aircraft" ~select:(fun x -> ExtXml.attrib x "name" = id_ac) in + let fp = sprintf "%s/conf/%s" Env.paparazzi_home (ExtXml.attrib conf "flight_plan") and + af = sprintf "%s/conf/%s" Env.paparazzi_home (ExtXml.attrib conf "airframe") and + rc = sprintf "%s/conf/%s" Env.paparazzi_home (ExtXml.attrib conf "radio")in + let resp = sprintf "%s CONFIG_RES %s %s %s %s" id_ac id_req fp af rc in + Ivy.send (resp); + prerr_endline (resp) + with + Not_found -> + Ivy.send (sprintf "ground UNKNOWN %s" id_req) + +let server = fun () -> + ignore (Ivy.bind (fun _ args -> send_aircrafts_msg ()) "^ask AIRCRAFTS"); + ignore (Ivy.bind (fun _ args -> send_flight_plan args.(0)) "^ask FLIGHT_PLAN +(.*)"); + ignore (Ivy.bind (fun _ args -> send_config args.(0) args.(1)) "^(.*) CONFIG_REQ +(.*)") + +let handle_pprz_message = fun log a -> + let name = ref None (*** register_aircraft "log_twinstar" a; Some "log_twinstar" ***) in + fun (msg_id, values) -> + prerr_endline "handle_pprz_message"; + let msg = Tele_Pprz.message_of_id msg_id in + match !name with + None -> + if msg.Pprz.name = "IDENT" then + let n = List.assoc "id" values in + name := Some n; + register_aircraft n a + | Some ac_name -> + log_and_parse log ac_name a msg values + +let listen_link = fun log xml_link -> + match ExtXml.attrib xml_link "protocol" with + "pprz/modem" -> + (* Hyp: One single A/C on this channel *) + let port = ExtXml.attrib xml_link "port" in + let ac = new_aircraft (Modem port) in + listen_pprz_modem (handle_pprz_message log ac) port + | _ -> fprintf stderr "Warning: Ignoring link '%s'\n" (ExtXml.attrib xml_link "name") + + + +(* main loop *) +let _ = + let xml_ground = ExtXml.child conf_xml "ground" in + let ivy_bus = ref (ExtXml.attrib xml_ground "ivy_bus") in + let options = + [ "-b", Arg.String (fun x -> ivy_bus := x), (sprintf "Bus\tDefault is %s" !ivy_bus)] in + Arg.parse (options) + (fun x -> Printf.fprintf stderr "Warning: Don't do anythig with %s\n" x) + "Usage: "; + + Ivy.init "Paparazzi receive" "READY" (fun _ _ -> ()); + Ivy.start !ivy_bus; + + + (* Opens the log file *) + let log = logger () in + + (* Waits for new simulated aircrafts *) + listen_sims log; + + (* Listen on links *) + List.iter (listen_link log) (Xml.children xml_ground); + + (* Sends periodically alive aircrafts *) + ignore (Glib.Timeout.add aircrafts_msg_period (fun () -> send_aircrafts_msg (); true)); + + server (); + + let loop = Glib.Main.create true in + while Glib.Main.is_running loop do ignore (Glib.Main.iteration true) done diff --git a/sw/ground_segment/visu3d/Help_Keys.txt b/sw/ground_segment/visu3d/Help_Keys.txt new file mode 100644 index 00000000000..32e2f2e21b8 --- /dev/null +++ b/sw/ground_segment/visu3d/Help_Keys.txt @@ -0,0 +1,26 @@ + Bouton gauche : + --------------- + +Rotation de la vue + + Bouton milieu : + --------------- + +Zoom/Unzoom en allant en avant/en arriere + + + Molette : + --------- + - zoom/unzoom + + Touches clavier : + ----------------- + + - Espace : lance/stoppe l'animation + - + et - du pave numerique : accelere ou ralentit l'animation + - Home : vue du dessus + - Page_Up / Page_Down : zoom/unzoom + - Touches flechees : deplacement de la vue + - Fleches du pave numerique : rotation de la vue + - r : affichage de la rosace + - F12 : capture ecran \ No newline at end of file diff --git a/sw/ground_segment/visu3d/Makefile b/sw/ground_segment/visu3d/Makefile new file mode 100644 index 00000000000..1b36cf18caa --- /dev/null +++ b/sw/ground_segment/visu3d/Makefile @@ -0,0 +1,48 @@ +OCAMLOPT0 = ocamlopt +OCAMLC = ocamlc + +MLFLAGS = -I +lablgtk2 -I +lablGL -I +camlimages -I ../../lib/ocaml + +OCAMLOPT = $(OCAMLOPT0) $(OCAMLOPT_OPTIONS) + +SRC = mapGL.ml + +OBJS= $(SRC:.ml=.cmo) + +LINK= $(OCAMLC) $(MLFLAGS) +LIBS_CI = ci_core.cma ci_gif.cma ci_jpeg.cma ci_tiff.cma ci_bmp.cma ci_ppm.cma ci_png.cma \ + ci_xpm.cma ci_ps.cma ci_freetype.cma +STDLIBS = unix.cma str.cmxa xml-light.cma lablgtk.cma lablgl.cma lablgtkgl.cma $(LIBS_CI) +ADD_LIBS = lib.cma xlib.cma glibivy-ocaml.cma +CLIBS = -cclib -lpthread + +all: mapGL.opt + +clean: + \rm -f *.cm* *.o *.a *~ *.opt *.out *.top *.output *obj *exe \ + stars_lexer.ml stars_parser.mli stars_parser.ml .depend + +# Executables +mapGL.out: $(OBJS) + $(OCAMLC) $(MLFLAGS) $(STDLIBS) gtkInit.cmo $(ADD_LIBS) -o $@ $(OBJS_3D) $(OBJS) $(CLIBS) + +mapGL.opt: $(OBJS:.cmo=.cmx) + $(OCAMLOPT) $(MLFLAGS) $(STDLIBS:.cma=.cmxa) gtkInit.cmx $(ADD_LIBS:.cma=.cmxa) -o $@ $(OBJS:.cmo=.cmx) $(CLIBS) + +# Do not edit below this line + +.depend: + ocamldep *.mli *.ml *.mly *.mll > .depend + +.SUFFIXES: .ml .mli .cmo .cmi .cmx + +.ml.cmo: + $(OCAMLC) $(MLFLAGS) -labels -w s -c $< +.mli.cmi: + $(OCAMLC) $(MLFLAGS) -labels -w s -c $< +.ml.cmx: + $(OCAMLOPT) $(MLFLAGS) -labels -w s -c $< + +ifneq ($(MAKECMDGOALS),clean) +-include .depend +endif diff --git a/sw/ground_segment/visu3d/TODO b/sw/ground_segment/visu3d/TODO new file mode 100644 index 00000000000..9f4ac6883ec --- /dev/null +++ b/sw/ground_segment/visu3d/TODO @@ -0,0 +1,20 @@ + + +add waypoint icons +add aircraft icon + +add camera trace on ground + +manage aircraft track length (limit to n minutes) + +map video frame (photos) on ground surface +transform photos (translate, rotate, zoom) +save photos position and visibility for future reload. +manage photos (like layers in a cad program ) + +add fact_alti adjust + +support several aircrafts, maybe with an identification label (3D radar track ? ) +select aircraft, waypoints (would it be possible??) + +support viewpoint selection using function keys - viewpoints are defined in the flight plan ? diff --git a/sw/ground_segment/visu3d/mapGL.ml b/sw/ground_segment/visu3d/mapGL.ml new file mode 100644 index 00000000000..97dfc7bb62b --- /dev/null +++ b/sw/ground_segment/visu3d/mapGL.ml @@ -0,0 +1,493 @@ +(* + * $Id$ + * + * 3D OpenGL visualisation + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Ocaml_tools +open Geometry_3d + +open Gtk_3d +open Latlong + +let fos = fun x -> + try + float_of_string x + with + Failure("float_of_string") -> failwith ("float_of_string: "^ x) +let float_attrib = fun xml a -> fos (ExtXml.attrib xml a) + +(* Version de l'appli *) +let version = "0.1" + +(* 1 point tous les 50m *) +let dx = 50 and dy = 50 + +(* Facteur d'echelle pour les altitudes *) +let fact_alti = 3 + +(* Taille de la fenetre d'affichage *) +let width = 800 and height = 600 + +let tolerance_alti = 100. + +let default_color_trajs = ref (`NAME "red") + +let home = Env.paparazzi_home +let (//) = Filename.concat +let default_path_SRTM = home // "data" // "SRTM" +let default_path_maps = home // "data" // "" +let default_path_traj = home // "var" // "" +let default_path_missions = home // "conf" + +let color_pixmaps = Hashtbl.create 101 + +let limits = ref ((min_float,min_float), (max_float, max_float)) + +let track_filter = fun points -> + let ((minx,miny), (maxx, maxy)) = !limits in + let rec loop = function + [] -> [] + | (t,x,y,a)::ps -> + if minx < x && x < maxx && miny < y && y < maxy + then (t,x,y,a)::loop ps + else loop ps in + loop points + +(* Fichier d'aide contenant les touches clavier utilisees *) +let filename_help_keys = Env.paparazzi_src // "conf" // "Help_Keys.txt" + +(* ============================================================================= *) +(* = Passage de couleur GTK vers GL = *) +(* = = *) +(* = color = couleur GTK (`NAME ou `RGB) a transformer = *) +(* ============================================================================= *) +let gtk_to_gl_color color = + let t = GDraw.color color in + ((float_of_int (Gdk.Color.red t))/.65535.0, + (float_of_int (Gdk.Color.green t))/.65535.0, + (float_of_int (Gdk.Color.blue t))/.65535.0) + +(* ============================================================================= *) +(* = Passage de couleur GL vers GTK = *) +(* = = *) +(* = (r, g, b) = couleur GL a transformer en equivalent GTK = *) +(* ============================================================================= *) +let gl_to_gtk_color (r, g, b) = + `RGB(int_of_float (r*.65535.0), int_of_float (g*.65535.0), + int_of_float (b*.65535.0)) + +(* ============================================================================= *) +(* = Lecture d'un fichier de trajectoire avec correction des points etranges = *) +(* ============================================================================= *) +let read_traj_file filename = + let traj = ref [] and prev_alti = ref None in + let corrige_alt alt = + let x = + match !prev_alti with + None -> alt + | Some prev_alti -> + if abs_float(alt-.prev_alti) + (try + if mode = "3" then (* Else no pertinent info available *) + let t = fos time + and utm_x = (fos utm_x)/.100. + and utm_y = (fos utm_y)/.100. + and alt = (fos alt) in + (* Filtrage des altitudes incorrectes *) + let alt = corrige_alt alt in + traj:=(t, utm_x, utm_y, alt)::!traj + with _ -> error_func ()) + | _ -> () + in + do_read_file filename match_func (fun () -> ()) ; + let traj' = track_filter (List.rev !traj) in + (traj', !default_color_trajs) + +(* ============================================================================= *) +(* = Ajout de la surface = *) +(* ============================================================================= *) +let add_surface view3d texture_file (min_x, min_y) (max_x, max_y) utm_zone = + (* Creation de la texture a partir d'une image *) + Printf.printf "Lecture texture..."; flush stdout ; + let texture_id = Gtk_3d.create_texture_from_image texture_file in + Printf.printf " OK\n"; flush stdout ; + + (* Creation d'une matrice contenant les elevations *) + let nx = (max_x-min_x)/dx+1 and ny = (max_y-min_y)/dy+1 in + let tab = Array.make_matrix ny nx {x3D=0.; y3D=0.; z3D=0.} in + + let y = ref max_y and i = ref 0 in + try + for i = 0 to ny - 1 do + let x = ref min_x in + for j = 0 to nx - 1 do + let alt = (Srtm.of_utm {utm_x = float !x; utm_y = float !y; utm_zone = utm_zone})*fact_alti in + tab.(i).(j) <- {x3D = float !x; y3D= float !y; z3D = float alt} ; + x:=!x+dx + done ; + y:=!y-dy + done ; + + (* Ajout de cette matrice a la vue 3D *) + view3d#add_object_surface_with_texture tab texture_id + with + Srtm.Tile_not_found s -> + failwith (Printf.sprintf "SRTM tile '%s' not found, you can download it with %s" s (Srtm.error s)) + +(* ============================================================================= *) +(* = Ajout d'une trajectoire = *) +(* ============================================================================= *) +let point3D = fun (_, utm_x, utm_y, alt) -> {x3D=utm_x; y3D=utm_y; z3D=alt*. float fact_alti} +let add_traj view3d (points, id) = + let l = List.map point3D points in + + let color = gtk_to_gl_color id in + view3d#add_object_line l color 2 false false + +(* Adding one more point to a track *) +let last_points = Hashtbl.create 11 +let add_point (view3d:Gtk_3d.widget_3d) (point, id) = + let p = point3D point in + try + let last = Hashtbl.find last_points id in + let color = gtk_to_gl_color id in + view3d#display (view3d#add_object_line [last;p] color 2 false false); + Hashtbl.replace last_points id p + with + Not_found -> + Hashtbl.add last_points id p + +(* ============================================================================= *) +(* = Load a map. Use SRTM elevation data to produce a 3d surface = *) +(* ============================================================================= *) +let load_surface view3d id_sol xml_map_file = + let min_x = ref max_int and min_y = ref max_int + and max_x = ref min_int and max_y = ref min_int + and texture_file = ref "" in + let xml = Xml.parse_file xml_map_file in + let texture_file = Xml.attrib xml "file" in + let texture_file = Filename.concat (Filename.dirname xml_map_file) texture_file in + let (_format, header) = Images.file_format texture_file in + let int_attrib x a = int_of_string (Xml.attrib x a) in + begin + match Xml.children xml with + p::_ -> + let utm_x = float_attrib p "utm_x" + and utm_y = float_attrib p "utm_y" + and x = float_attrib p "x" + and y = float_attrib p "y" + and scale = float_attrib xml "scale" in + min_x := truncate (utm_x -. scale *. x); + min_y := truncate (utm_y -. scale *. (float header.Images.header_height -. y)); + max_x := truncate (utm_x +. scale *. (float header.Images.header_width -. x)); + max_y := truncate (utm_y +. scale *. y) + | _ -> failwith "load_surface" + end; + begin + match !id_sol with + Some x -> view3d#delete_object x + | None -> () + end; + let utm_zone = try int_of_string (Xml.attrib xml "utm_zone") with _ -> Printf.fprintf stderr "Warning: utm_zone attribute not specified in '%s'; default is 31\n" xml_map_file; flush stderr; 31 in + id_sol:= Some (add_surface view3d texture_file (!min_x, !min_y) (!max_x, !max_y) utm_zone); + limits := ((float !min_x, float !min_y), (float !max_x, float !max_y)); + view3d#display_func + + +let load_mission = fun (view3d:Gtk_3d.widget_3d) xml -> + let wps = ExtXml.child xml "waypoints" in + let utm_x0 = float_attrib wps "utm_x0" + and utm_y0 = float_attrib wps "utm_y0" in + let display_waypoint = fun wp -> + let utm_x = float_attrib wp "x" +. utm_x0 + and utm_y = float_attrib wp "y" +. utm_y0 + and alt = float_attrib wp "alt" in + let p3d = point3D (0., utm_x, utm_y, alt) + and p3d_label = point3D (0., utm_x+.10., utm_y+.10., alt) in + view3d#display (view3d#add_object_point p3d p3d_label (ExtXml.attrib wp "name") (gtk_to_gl_color (`NAME "red")) true) in + List.iter display_waypoint (Xml.children wps) + +(* ============================================================================= *) +(* = Map loading callback = *) +(* ============================================================================= *) +let on_load_surface win view3d id_sol () = + let priv_load_surf xml_map_file = + load_surface view3d id_sol xml_map_file + in + try + Gtk_tools.open_file_dlg "Map calibration file" priv_load_surf None default_path_maps false + with x -> + Gtk_tools.error_box win "Read error" (Printexc.to_string x) + + +(* ============================================================================= *) +(* = Chargement d'une trajectoire = *) +(* ============================================================================= *) +let load_trajectory view3d lst_ids_trajs () = + let read_data f = + let new_traj = read_traj_file f in + lst_ids_trajs:=(add_traj view3d new_traj)::!lst_ids_trajs ; + (* Force la mise a jour de l'affichage *) + view3d#display_func + in + + Gtk_tools.open_file_dlg "Track" read_data None default_path_traj false + +(* ============================================================================= *) +(* = Recherche/Ajout d'une pixmap de couleur dans la table = *) +(* ============================================================================= *) +let get_color_pixmap win color = + let taille_x = 20 and taille_y = 8 in + try Hashtbl.find color_pixmaps color + with Not_found -> + let pm = Gtk_tools.rectangle_pixmap win color taille_x taille_y in + Hashtbl.add color_pixmaps color pm ; + pm + +(* ============================================================================= *) +(* = Selection de trajectoires = *) +(* ============================================================================= *) +let build_lst_traj tooltips view3d lst_ids_trajs () = + let get_id idx = try List.nth !lst_ids_trajs idx with _ -> (-1) in + + let (window,boite) = Gtk_tools.create_window "Liste des trajectoires" 450 300 in + let lst = Gtk_tools.create_managed_list + [("Id", 40); ("Sel.", 40); ("Color", 60)] boite#add + in + let buts = Gtk_tools.create_buttons + [("Hide", "Hide the track"); + ("Display", "Display the track"); + ("Color", "Change the color"); + ("Delete", "Delete the track") ; + ("Close", "Close the window")] tooltips boite#pack + in + let but_masque = List.nth buts 0 and but_aff = List.nth buts 1 + and but_couleur = List.nth buts 2 and but_del = List.nth buts 3 in + Gtk_tools.set_sensitive_list + [but_couleur; but_del; but_masque; but_aff] false ; + + let current_selection = ref (-1) and current_idx = ref "" in + let callback_traj index _ selection = + if selection then begin + current_selection:=get_id (int_of_string index) ; + current_idx:=index ; + let masquable = view3d#object_get_visibility !current_selection in + Gtk_tools.set_sensitive but_masque masquable ; + Gtk_tools.set_sensitive but_aff (not masquable) ; + Gtk_tools.set_sensitive_list [but_couleur; but_del] true ; + end else begin + current_selection:=(-1); current_idx:="" ; + Gtk_tools.set_sensitive_list + [but_couleur; but_del; but_masque; but_aff] false + end + in + let fill_list_traj = Gtk_tools.connect_managed_list + lst 0 callback_traj ("track", true, true) + in + let fill_list () = + let to_select = !current_idx in + current_selection:=(-1); current_idx:="" ; + let n = ref (-1) in + let l = List.map (fun id -> + incr n ; + [string_of_int !n; + (if view3d#object_get_visibility id then " x " else ""); ""] + ) !lst_ids_trajs in + fill_list_traj to_select l ; + let row = ref 0 in + List.iter (fun id -> + let c = gl_to_gtk_color (view3d#object_get_color id) in + (fst lst)#set_cell !row 2 ~pixmap:(get_color_pixmap window c) ; + incr row) !lst_ids_trajs + in + fill_list () ; + + let func_masque_aff affiche = + if !current_selection<>(-1) then begin + view3d#object_set_visibility !current_selection affiche ; + view3d#display_func ; + fill_list () + end + in + let change_color () = + if !current_selection<>(-1) then begin + Gtk_tools.select_color (fun color -> + view3d#object_set_color !current_selection (gtk_to_gl_color color) ; + view3d#display_func ; + fill_list ()) ; + end + in + let delete_traj () = + if !current_selection<>(-1) then begin + view3d#delete_object !current_selection; view3d#display_func ; + lst_ids_trajs:= + List.filter (fun id -> id <> !current_selection) !lst_ids_trajs ; + fill_list () + end + in + + Gtk_tools.create_buttons_connect buts + [(fun () -> func_masque_aff false); (fun () -> func_masque_aff true); + change_color; delete_traj; + (fun () -> window#destroy (); view3d#display_func)] ; + window#show () + +(* ============================================================================= *) +(* = Fenetre About = *) +(* ============================================================================= *) +let build_fen_about () = + (* Creation de la liste des fichiers de l'animation *) + let l = ref [] and max_pixmaps = 15 in + for i=1 to max_pixmaps do + l:=(Printf.sprintf "Pixmaps/avion%d.xpm" i)::!l + done ; + + let message = Printf.sprintf "Visu Drone v%s\n" version in + + Gtk_tools.animated_msg_box "About" message (List.rev !l) + +(* ============================================================================= *) +(* = Creation de l'interface = *) +(* ============================================================================= *) +let build_interface = fun map_file mission_file -> + let nb_menus = ref 0 in + + (* Liste des menus disponibles *) + let liste_menus = ["Map"; "Tracks"; "Parameters"] in + + (* Mise en place des couleurs correctes *) + Gtk_tools.init_colors () ; + + (* Initialisation de l'aide contextuelle *) + let tooltips = Gtk_tools.init_tooltips () in + + (* Creation d'une fenetre *) + let (window, vbox, factory, accel_group, menus, menu_help) = + Gtk_tools.create_window_with_menubar_help ("Visu Drone v"^version) + width height liste_menus in + window#connect#destroy ~callback:GMain.Main.quit; + + (* Creation du Widget OpenGL *) + let view3d = new widget_3d vbox#add false "" in + + (* Ajout des objets a la vue *) + let id_sol = ref None in + let lst_ids_trajs = ref [] in + + (* Creation des menus : Sol *) + let factory = new GMenu.factory menus.(!nb_menus) ~accel_group in + incr nb_menus ; + factory#add_item "Load Background" + ~callback:(on_load_surface window view3d id_sol) ; + + (* Creation des menus : Trajectoires *) + let factory = new GMenu.factory menus.(!nb_menus) ~accel_group in + incr nb_menus ; + factory#add_item "Load Track" ~callback:(load_trajectory view3d lst_ids_trajs) ; + factory#add_item "Edit Tracks" + ~callback:(build_lst_traj tooltips view3d lst_ids_trajs) ; + + (* Creation des menus : Parametres *) + let factory = new GMenu.factory menus.(!nb_menus) ~accel_group in + incr nb_menus ; + factory#add_item "Edit" ~callback:(fun () -> ()) ; + + (* Aide *) + let factory = new GMenu.factory menu_help in + factory#add_item "A propos" ~callback:build_fen_about ; + factory#add_item "Help keys/mouse" + ~callback:(fun () -> Gtk_tools.display_file filename_help_keys + "Aide touches clavier" 370 500 tooltips (Some "fixed")) ; + + (* Affichage de la fenetre principale *) + window#show () ; + +(* let gps_regexp = "([a-z]*) +GPS +[0-9]* +([0-9]*) +([0-9]*) +[0-9\\.]* +([0-9\\.]*)" in + ignore (Ivy.bind (fun _ args -> add_point view3d ((0., fos args.(1)/.100.,fos args.(2)/.100., fos args.(3)), `NAME "green")) gps_regexp); *) + + + let flight_param_regexp = "([a-z0-9]*) +FLIGHT_PARAM +[0-9\\.]* +[0-9\\.]* +([0-9\\.]*) +([0-9\\.]*) +[0-9\\.]* +[0-9\\.]* +([0-9\\.]*) +[0-9\\.]*" in + ignore (Ivy.bind (fun _ args -> + let name= args.(0) and + x = fos args.(1) and + y = fos args.(2) and + z = fos args.(3) in + (* Printf.fprintf stderr "############ %f %f %f\n" x y z; *) + if (Str.string_match (Str.regexp_string "twinstar1") name 0) then + add_point view3d ((0., x, y, z), `NAME "green"); + if (Str.string_match (Str.regexp_string "twinstar2") name 0) then + add_point view3d ((0., x, y, z), `NAME "blue"); +(* Printf.fprintf stderr "############\n"; *) + ) flight_param_regexp); + + + (* Loading an initial map *) + if map_file <> "" then begin + let xml_map_file = Filename.concat (default_path_maps) map_file in + load_surface view3d id_sol xml_map_file + end; + + (* Loading an initial mission *) + if mission_file <> "" then begin + let xml_file = Filename.concat (default_path_missions) mission_file in + load_mission view3d (Xml.parse_file xml_file) + end; + + (* Lancement de la mainloop *) + Gtk_tools.main_loop () + +(* ============================================================================= *) +(* = Programme principal = *) +(* ============================================================================= *) +let _ = + let ivy_bus = ref "127.255.255.255:2010" and + map_file = ref "" and + mission_file = ref "" in + let options = + [ "-b", Arg.String (fun x -> ivy_bus := x), "Bus\tDefault is 127.255.255.25:2010"; + "-m", Arg.String (fun x -> map_file := x), "Map description file"; + "-f", Arg.String (fun x -> mission_file := x), "Mission description file"] in + Arg.parse (options) + (fun x -> Printf.fprintf stderr "Warning: Don't do anythig with %s\n" x) + "Usage: "; + (* *) + Ivy.init "Paparazzi 3d visu" "READY" (fun _ _ -> ()); + Ivy.start !ivy_bus; + + Srtm.add_path default_path_SRTM; + + (* Lancement de l'interface *) + build_interface !map_file !mission_file + + +(* =============================== FIN ========================================= *) diff --git a/sw/ground_segment/wind/Makefile b/sw/ground_segment/wind/Makefile new file mode 100644 index 00000000000..4267b10be04 --- /dev/null +++ b/sw/ground_segment/wind/Makefile @@ -0,0 +1,68 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +include ../../../conf/Makefile.local + +all: wind.opt + +INCLUDES= -I ../../lib/ocaml -I +lablgtk2 + +OCAMLC= ocamlc -g $(INCLUDES) +OCAMLMLI= ocamlc $(INCLUDES) +OCAMLOPT= ocamlopt $(INCLUDES) +OCAMLDEP= ocamldep $(INCLUDES) + + + +wind.opt : wind.cmx + $(OCAMLOPT) -o $@ xml-light.cmxa glibivy-ocaml.cmxa lablgtk.cmxa str.cmxa lib.cmxa $< + strip $@ + +.SUFFIXES: +.SUFFIXES: .ml .mli .mly .mll .cmi .cmo .cmx .out .opt .p.cmx .popt + +.ml.cmo : + $(OCAMLC) -c $< +.mli.cmi : + $(OCAMLMLI) -c $< +.ml.cmx : + $(OCAMLOPT) -c $< +# To produce profiled objects +.ml.p.cmx : + $(OCAMLOPT) -p -c $< + mv $*.cmx $@ + mv $*.o $*.p.o +.cmo.out : + $(OCAMLC) -o $@ $< +# To produce profiled binaries +.p.cmx.popt : + $(OCAMLOPT) -p -o $@ $< +.cmx.opt : + $(OCAMLOPT) -o $@ $< + +clean: + \rm -f *.cmo *.cmi *.cmx *.o *~ *.opt *.out .depend *.popt + +.depend: + $(OCAMLDEP) *.mli *.ml > $@ + +include .depend diff --git a/sw/ground_segment/wind/wind.ml b/sw/ground_segment/wind/wind.ml new file mode 100644 index 00000000000..b0bbcb9cfe3 --- /dev/null +++ b/sw/ground_segment/wind/wind.ml @@ -0,0 +1,262 @@ +(* + * $Id$ + * + * Multi aircrafts receiver, logger and broadcaster + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(* + * + * Estimate wind by analysing aircrafts trajectories + * + * Author : Nicolas Barnier - barnier@recherche.enac.fr + * + *) + +let debug = false + +open Printf + +let (//) = Filename.concat +let conf_xml = Xml.parse_file (Env.paparazzi_home // "conf" // "conf.xml") +let xml_ground = ExtXml.child conf_xml "ground" +let ivy_bus = ref (ExtXml.attrib xml_ground "ivy_bus") + +open Geometry_2d + +type point_val = {p : pt_2D; f : float} + +type triangle = {a: point_val; b: point_val; c: point_val} + +let bary t = barycenter [t.a.p; t.b.p; t.c.p] + +let init w_init step f = + let pb = vect_add w_init {x2D = step; y2D = 0.} + and pc = vect_add w_init {x2D = 0.; y2D = step} in + {a = {p = w_init; f = f w_init}; + b = {p = pb; f = f pb}; + c = {p = pc; f = f pc}} + +let shift pa fa t = {a = {p = pa; f = fa}; b = t.a; c = t.b} + +let shiftpv pf t = shift pf.p pf.f t + +let calcnew p b alpha = vect_add_mul_scal alpha b (vect_make b p) + +let triangle_sort t = + let abc = [|t.a; t.b; t.c|] in + Array.sort (fun t1 t2 -> compare t2.f t1.f) abc; + {a = abc.(0); b = abc.(1); c = abc.(2)} + +let norme2 p = p.x2D *. p.x2D +. p.y2D *. p.y2D + +let simplex p fmax step max_iter precision = + let f x = -. (fmax x) in + + let rec loop num_iter vs = + if num_iter < max_iter && norme2 (vect_make vs.a.p vs.c.p) > precision then begin + begin if debug then + let pa = cart2polar vs.a.p in + Printf.printf "%f %f %f\n" pa.theta2D pa.r2D (-. vs.a.f) end; + + let vb = bary vs in + let vr = calcnew vs.c.p vb (-1.) in + let fvr = f vr in + let new_vs = + if fvr > vs.a.f then + let ve = calcnew vs.c.p vb (-2.) in + let fve = f ve in + if fve > fvr then shift ve fve vs + else shift vr fvr vs + else + let vc = calcnew vs.c.p vb 0.5 in + let fvc = f vc in + if fvc > vs.b.f || fvr > vs.b.f then + let v = if fvr > fvc then {p = vr; f = fvr} else {p = vc; f = fvc} in + if v.f <= vs.b.f then {vs with c = v} + else if v.f > vs.a.f then shiftpv v vs + else {vs with b = v; c = vs.b} + else + let vcb = calcnew vs.b.p vs.a.p 0.5 + and vcc = calcnew vs.c.p vs.a.p 0.5 in + triangle_sort {vs with b = {p = vcb; f = f vcb}; c = {p = vcc; f = f vcc}} in + + loop (num_iter + 1) new_vs end + else vs.a in + + if debug then Printf.printf "%f %f %f\n" p.x2D p.y2D (fmax p); + let vs = init p step f in + let vs = triangle_sort vs in + loop 0 vs + + +let isotropic_mean wind speeds = + let n = Array.length speeds in + let air_speeds = Array.map (fun speed -> cart2polar (vect_sub speed wind)) speeds in + let weights = + Array.map + (fun air -> + let sum = + Array.fold_left + (fun acc airj -> + acc +. norm_angle_rad (abs_float (air.theta2D -. airj.theta2D)) /. m_pi) + 0. air_speeds in + sum /. (float (n-1))) + air_speeds in + let mean = ref 0. in + for i = 0 to n-1 do + mean := !mean +. vect_norm (vect_sub speeds.(i) wind) *. weights.(i) done; + !mean /. float n + +let isotropic_wind wind_init speeds precision = + let n = Array.length speeds in + let mean wind = + let air_speeds = Array.map (fun speed -> cart2polar (vect_sub speed wind)) speeds in + let weights = + Array.mapi + (fun i airi -> + let sum = ref 0. in + for j = 0 to n-1 do + if j <> i then + sum := !sum +. + norm_angle_rad (abs_float (airi.theta2D -. air_speeds.(j).theta2D)) /. m_pi + done; + !sum /. (float (n-1))) + air_speeds in + let sum_weights = Array.fold_left (+.) 0. weights in + + let mean = ref 0. in + for i = 0 to n-1 do + mean := !mean +. vect_norm (vect_sub speeds.(i) wind) *. weights.(i) done; + (!mean /. sum_weights, sum_weights, weights) in + + let nb_calls = ref 0 in + let cost wind = + incr nb_calls; + let (m, sum_weights, weights) = mean wind in + let sum = ref 0. in + for i = 0 to n-1 do + let err = weights.(i) *. (vect_norm (vect_sub speeds.(i) wind) -. m) in + sum := !sum +. err *. err done; + !sum /. sum_weights in + + let step = 2. and max_iter = 100 in + let wind = simplex wind_init cost step max_iter precision in + if debug then Printf.printf "nb calls: %d\n" !nb_calls; + + let (mean, _, _) = mean wind.p in + (wind.p, mean, wind.f) + + +(* val wind : Geometry_2d.pt_2D -> Geometry_2d.pt_2D array -> float + -> (Geometry_2d.pt_2Dfloat * float * float) *) +(** [wind wind_init speeds precision] returns the wind and air speed mean and std dev. *) + +let wind wind_init speeds precision = + let mean wind = + let sum = + Array.fold_left (fun acc speed -> acc +. vect_norm (vect_sub speed wind)) 0. speeds in + sum /. float (Array.length speeds) in + + let nb_calls = ref 0 in + let cost wind = + incr nb_calls; + let m = mean wind in + let sum = + Array.fold_left + (fun acc speed -> + let err = vect_norm (vect_sub speed wind) -. m in + acc +. err *. err) + 0. speeds in + sum /. float (Array.length speeds) in + + let step = 2. and max_iter = 100 in + let wind = simplex wind_init cost step max_iter precision in + if debug then Printf.printf "nb calls: %d\n" !nb_calls; + + (wind.p, mean wind.p, wind.f) + + + +let _ = + let options = + [ "-b", Arg.String (fun x -> ivy_bus := x), (sprintf "Bus\tDefault is %s" !ivy_bus)] in + Arg.parse (options) + (fun x -> Printf.fprintf stderr "Warning: Don't do anything with %s\n" x) + "Usage: "; + + let precision = 1e-3 in + let speeds = ref [] and wind_init = ref null_vector in + + let on_wind_command _ args = + if Str.string_match (Str.regexp "clear") args.(0) 0 then begin + speeds := []; + wind_init := null_vector + end + in + + let on_wind_clear _ args = speeds := []; wind_init := null_vector in + + let on_flight_param = fun _ args -> +(* Array.iter (printf "%s ") args; printf "\n%!"; *) + let r = float_of_string args.(4) + and theta = heading_of_to_angle_rad (deg2rad (float_of_string args.(5))) in + let speed = polar2cart {r2D = r; theta2D = theta} in + speeds := speed :: !speeds in + + let on_wind_req _ args = + let speeds = Array.of_list !speeds in + if Array.length speeds >= 3 then begin + let (wind, mean, stddev) = wind !wind_init speeds precision in + wind_init := wind; + let wind_polar = cart2polar wind in + let wind_cap_deg = rad2deg (wind_dir_from_angle_rad wind_polar.theta2D) in + Ivy.send + (sprintf "ground WIND_RES %s %f %f %f %f" args.(0) wind_cap_deg wind_polar.r2D mean stddev) + end in + + let on_wind_iso _ args = + let speeds = Array.of_list !speeds in + if Array.length speeds >= 3 then begin + let (wind, mean, stddev) = isotropic_wind !wind_init speeds precision in + wind_init := wind; + let wind_polar = cart2polar wind in + let wind_cap_deg = rad2deg (wind_dir_from_angle_rad wind_polar.theta2D) in + Ivy.send + (sprintf "ground WIND_RES %s %f %f %f %f" args.(0) wind_cap_deg wind_polar.r2D mean stddev) + end in + + let on_aircrafts = fun _ args -> + let aclist = args.(0) in + let first_ac = aclist (*String.sub aclist 0 (String.index aclist ',')*) in + ignore + (Ivy.bind on_flight_param + (sprintf "%s +FLIGHT_PARAM (.*) (.*) (.*) (.*) (.*) (.*) (.*) (.*)" first_ac)) in + + Ivy.init "Paparazzi Wind" "READY" (fun _ _ -> ()); + ignore (Ivy.bind on_aircrafts "ground AIRCRAFTS (.*)"); + ignore (Ivy.bind on_wind_req "WIND_REQ (.*)"); + ignore (Ivy.bind on_wind_iso "WIND_ISO (.*)"); + ignore (Ivy.bind on_wind_command "WIND_COMMAND (.*)"); + Ivy.start !ivy_bus; + + GMain.Main.main () diff --git a/sw/ground_segment/wind/wind.mli b/sw/ground_segment/wind/wind.mli new file mode 100644 index 00000000000..992137c2cbe --- /dev/null +++ b/sw/ground_segment/wind/wind.mli @@ -0,0 +1,3 @@ +val wind : Geometry_2d.pt_2D -> Geometry_2d.pt_2D array -> float -> + (Geometry_2d.pt_2D * float * float) +(** [wind wind_init speeds precision] returns the wind, air speed mean and std dev *) diff --git a/sw/include/std.h b/sw/include/std.h new file mode 100644 index 00000000000..6103fdebd2d --- /dev/null +++ b/sw/include/std.h @@ -0,0 +1,38 @@ +#ifndef STD_H +#define STD_H +/* + * $Id$ + * + * Copyright (C) 2005 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * + * a couple of fundamentals used in the avr code + * + */ + +#include + +#define FALSE 0 +#define TRUE (!FALSE) + +/* Boolean values */ +typedef uint8_t bool_t; + +#endif /* STD_H */ diff --git a/sw/lib/ocaml/Makefile b/sw/lib/ocaml/Makefile new file mode 100644 index 00000000000..b0f0a838657 --- /dev/null +++ b/sw/lib/ocaml/Makefile @@ -0,0 +1,94 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +INCLUDES= -I +lablgl -I +camlimages -I +lablgtk2 +OCAMLC=ocamlc -g $(INCLUDES) +OCAMLOPT=ocamlopt $(INCLUDES) + + +SRC = debug.ml env.ml serial.ml ocaml_tools.ml extXml.ml xml2h.ml latlong.ml srtm.ml wavecard.ml geometry_2d.ml geometry_3d.ml cserial.o convert.o ubx.ml pprz.ml +CMO = $(SRC:.ml=.cmo) +CMX = $(SRC:.ml=.cmx) + +XSRC = platform.ml gtkgl_Hack.ml ml_gtkgl_hack.o gtk_image.ml gtk_tools_icons.ml gtk_tools.ml gtk_draw.ml gtk_tools_GL.ml gtk_3d.ml mapCanvas.ml mapWaypoints.ml mapTrack.ml +XCMO = $(XSRC:.ml=.cmo) +XCMX = $(XSRC:.ml=.cmx) + + +all : lib.cma lib.cmxa xlib.cma xlib.cmxa xml_get.out + + +lib.cma : $(CMO) + ocamlmklib -custom -o lib str.cma xml-light.cma unix.cma $^ + +lib.cmxa : $(CMX) + ocamlmklib -custom -o lib $^ + +xlib.cma : $(XCMO) + ocamlmklib -custom -o xlib $^ + +xlib.cmxa : $(XCMX) + ocamlmklib -custom -o xlib $^ + +xml_get.out : lib.cma xml_get.cmo + $(OCAMLC) -o $@ str.cma xml-light.cma -I . $^ + +ignutm.opt : latlong.cmx ignutm.ml + $(OCAMLOPT) -o $@ -I +camlimages ci_core.cmxa ci_png.cmxa xml-light.cmxa $^ + +utm_of.opt : latlong.cmx utm_of.ml + $(OCAMLOPT) -o $@ $^ + +GTKCFLAGS := $(shell gtk-config --cflags) + +%.o : %.c + $(OCAMLC) -c $< + +ml_gtkgl_hack.o : ml_gtkgl_hack.c + $(OCAMLC) -c -ccopt "$(GTKCFLAGS)" $< + +%.cmo : %.ml + $(OCAMLC) -c $< + +%.cmx : %.ml + $(OCAMLOPT) -c $< + +%.cmi : %.mli + $(OCAMLC) $< + +%.cmi : %.ml + $(OCAMLC) $< + +clean : + rm -f *~ *.cm* *.out *.opt .depend *.a *.o *.so + + +# +# Dependencies +# + +.depend: + ocamldep *.ml* > .depend + +ifneq ($(MAKECMDGOALS),clean) +-include .depend +endif diff --git a/sw/lib/ocaml/convert.c b/sw/lib/ocaml/convert.c new file mode 100644 index 00000000000..27ba5115489 --- /dev/null +++ b/sw/lib/ocaml/convert.c @@ -0,0 +1,46 @@ +/* + $Id$ + + Copyright (C) 2004 Pascal Brisset, Antoine Drouin + + Ocaml low level conversions + + This file is part of paparazzi. + + paparazzi is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + paparazzi is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with paparazzi; see the file COPYING. If not, write to + the Free Software Foundation, 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +#include +#include +#include +#include +#include +#include "caml/mlvalues.h" +#include "caml/alloc.h" + +value c_float_of_indexed_bytes(value s, value index) +{ + float *x = (float*)(String_val(s) + Int_val(index)); + + return copy_double((double)(*x)); +} + +value c_int32_of_indexed_bytes(value s, value index) +{ + int32_t *x = (int32_t*)(String_val(s) + Int_val(index)); + + return copy_int32(*x); +} diff --git a/sw/lib/ocaml/cserial.c b/sw/lib/ocaml/cserial.c new file mode 100644 index 00000000000..91816f8edef --- /dev/null +++ b/sw/lib/ocaml/cserial.c @@ -0,0 +1,76 @@ +/* + $Id$ + Copyright (C) 2004 Pascal Brisset, Antoine Drouin + + Ocaml bindings for handling serial ports + + This file is part of paparazzi. + + paparazzi is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + paparazzi is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with paparazzi; see the file COPYING. If not, write to + the Free Software Foundation, 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int baudrates[] = { B0, B50, B75, B110, B134, B150, B200, B300, B600, B1200, B1800, B2400, B4800, B9600, B19200, B38400, B57600, B115200, B230400 }; + + +/****************************************************************************/ +/* Open serial device for requested protocoll */ +/****************************************************************************/ +value c_init_serial(value device, value speed) +{ + struct termios orig_termios, cur_termios; + + int br = baudrates[Int_val(speed)]; + + int fd = open(String_val(device), O_RDWR); + + if (fd == -1) failwith("opening modem serial device : fd < 0"); + + if (tcgetattr(fd, &orig_termios)) failwith("getting modem serial device attr"); + cur_termios = orig_termios; + + /* input modes */ + cur_termios.c_iflag &= ~(IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK|ISTRIP|INLCR|IGNCR + |ICRNL |IXON|IXANY|IXOFF|IMAXBEL); + /* pas IGNCR sinon il vire les 0x0D */ + cur_termios.c_iflag |= BRKINT; + + /* output_flags */ + cur_termios.c_oflag &=~(OPOST|ONLCR|OCRNL|ONOCR|ONLRET); + + /* control modes */ + cur_termios.c_cflag &= ~(CSIZE|CSTOPB|CREAD|PARENB|PARODD|HUPCL|CLOCAL|CRTSCTS); + cur_termios.c_cflag |= CREAD|CS8|CLOCAL; + + /* local modes */ + cur_termios.c_lflag &= ~(ISIG|ICANON|IEXTEN|ECHO|FLUSHO|PENDIN); + cur_termios.c_lflag |= NOFLSH; + + if (cfsetispeed(&cur_termios, br)) failwith("setting modem serial device speed"); + + if (tcsetattr(fd, TCSADRAIN, &cur_termios)) failwith("setting modem serial device attr"); + + return Val_int(fd); +} diff --git a/sw/lib/ocaml/debug.ml b/sw/lib/ocaml/debug.ml new file mode 100644 index 00000000000..8a0e1524e3a --- /dev/null +++ b/sw/lib/ocaml/debug.ml @@ -0,0 +1,46 @@ + (* + * $Id$ + * + * Debugging facilities + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let level = ref (try Sys.getenv "PPRZ_DEBUG" with Not_found -> "") + let log = ref stderr + let call lev f = + assert( (* assert permet au compilo de tout virer avec l'option -noassert *) + if (String.contains !level '*' || String.contains !level lev) + then begin + f !log; + flush !log + end; + true) + +let xprint = fun s -> + let n = String.length s in + let a = String.make (3*n) ' ' in + for i = 0 to n - 1 do + let x = Printf.sprintf "%02x" (Char.code s.[i]) in + a.[3*i] <- x.[0]; + a.[3*i+1] <- x.[1] + done; + a diff --git a/sw/lib/ocaml/env.ml b/sw/lib/ocaml/env.ml new file mode 100644 index 00000000000..3fd89a95b7e --- /dev/null +++ b/sw/lib/ocaml/env.ml @@ -0,0 +1,37 @@ +(* + * $Id$ + * + * Configuration handling + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let paparazzi_src = + try + Sys.getenv "PAPARAZZI_SRC" + with + _ -> "/usr/share/paparazzi" + +let paparazzi_home = + try + Sys.getenv "PAPARAZZI_HOME" + with + _ -> Filename.concat (Sys.getenv "HOME") "paparazzi" diff --git a/sw/lib/ocaml/extXml.ml b/sw/lib/ocaml/extXml.ml new file mode 100644 index 00000000000..f449f342192 --- /dev/null +++ b/sw/lib/ocaml/extXml.ml @@ -0,0 +1,87 @@ +(* + * $Id$ + * + * Xml-Light extension + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +exception Error of string + +let sep = Str.regexp "\\." + +let child xml ?select c = + let rec find = function + Xml.Element (tag, attributes, _children) as elt :: elts -> + if tag = c then + match select with + None -> elt + | Some p -> + if p elt then elt else find elts + else + find elts + | _ :: elts -> find elts + | [] -> raise Not_found in + + + let children = Xml.children xml in + + (* Let's try with a numeric index *) + try (Array.of_list children).(int_of_string c) with + Failure "int_of_string" -> (* Bad luck. Go through the children *) + find children + + +let get xml path = + let p = Str.split sep path in + let rec iter xml = function + [] -> failwith "ExtXml.get: empty path" + | [x] -> ( try if Xml.tag xml <> x then raise Not_found else xml with _ -> raise Not_found ) + | x::xs -> iter (child xml x) xs in + iter xml p + +let get_attrib xml path attr = + Xml.attrib (get xml path) attr + +let sprint_fields = fun () l -> + "<"^ + List.fold_right (fun (a, b) -> (^) (Printf.sprintf "%s=\"%s\" " a b)) l ">" + +let attrib = fun x a -> + try + Xml.attrib x a + with + Xml.No_attribute _ -> + raise (Error (Printf.sprintf "Error: Attribute '%s' expected in <%a>" a sprint_fields (Xml.attribs x))) + +let attrib_or_default = fun x a default -> + try Xml.attrib x a with _ -> default + + +let to_string_fmt = fun xml -> + let l = String.lowercase in + let rec lower = function + Xml.PCData _ as x -> x + | Xml.Element (t, ats, cs) -> + Xml.Element(l t, + List.map (fun (a,v) -> (l a, v)) ats, + List.map lower cs) in + Xml.to_string_fmt (lower xml) diff --git a/sw/lib/ocaml/geometry_2d.ml b/sw/lib/ocaml/geometry_2d.ml new file mode 100644 index 00000000000..9f96f18bc04 --- /dev/null +++ b/sw/lib/ocaml/geometry_2d.ml @@ -0,0 +1,920 @@ +(* + * $Id$ + * + * 2D Geometry + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(* Distance d'un point a une droite, a un segment, a un ensemble de segment *) +(* Projection d'un point sur une droite, segment, ensemble de segment *) + +(* Modules locaux *) + +let epsilon = 0.0001 + +(* Type contenant un point 2D *) +type pt_2D = {x2D : float; y2D : float} + +(* Vecteurs nuls en 2D *) +let null_vector = {x2D=0.; y2D=0.} + +(* Polygone 2D, non ferme par defaut *) +type poly_2D = pt_2D list + +(* Types d'intersection : *) +(* T_IN_SEGx : point d'intersection dans le segment x (extremites exclues) *) +(* T_ON_PTx : intersection sur le point x *) +(* T_OUT_SEG_PTx : intersection hors d'un segment. Le point d'intersection se *) +(* situe du cote du point x *) +type t_crossing = T_IN_SEG1 | T_IN_SEG2 | T_ON_PT1 | T_ON_PT2 | T_ON_PT3 +| T_ON_PT4 | T_OUT_SEG_PT1 | T_OUT_SEG_PT2 | T_OUT_SEG_PT3 | T_OUT_SEG_PT4 + +(* Type de polygone : convexe, concave ou indefini *) +type t_conv = CONVEX | CONCAVE | CONV_UNDEFINED + +(* Sens d'un polygone : sens horaire, anti-horaire et indefini *) +type t_ccw = CW | CCW | CCW_UNDEFINED + +(* Carre *) +let cc x = x*.x + +(* Type des points utilises pour la triangulation *) +type vertex = {pos : pt_2D; + num : int ; + mutable prev : int ; + mutable next : int ; + mutable ear : bool} + +(* ============================================================================= *) +(* = Manipulations d'angles = *) +(* ============================================================================= *) +let m_pi = 3.1415926535897932384626433832795 +let deg2rad d = (d*.m_pi)/.180. +let rad2deg d = (d*.180.)/.m_pi + +(* ============================================================================= *) +(* = Comparaison de points/vecteurs = *) +(* ============================================================================= *) +let point_same pt1 pt2 = (pt1.x2D = pt2.x2D) && (pt1.y2D = pt2.y2D) + +(* ============================================================================= *) +(* = Creation d'un vecteur = *) +(* ============================================================================= *) +let vect_make pt1 pt2 = {x2D=pt2.x2D -. pt1.x2D; y2D=pt2.y2D -. pt1.y2D} + +(* ============================================================================= *) +(* = Norme d'un vecteur = *) +(* ============================================================================= *) +let vect_norm v = sqrt((cc v.x2D) +. (cc v.y2D)) + +(* ============================================================================= *) +(* = Normalisation d'un vecteur = *) +(* ============================================================================= *) +let vect_normalize v = let n = vect_norm v in {x2D=v.x2D/.n; y2D=v.y2D/.n} + +(* ============================================================================= *) +(* = Force la norme d'un vecteur = *) +(* ============================================================================= *) +let vect_set_norm v norme = + let n = norme /. (vect_norm v) in {x2D=v.x2D*.n; y2D=v.y2D*.n} + +(* ============================================================================= *) +(* = Distance entre deux points = *) +(* ============================================================================= *) +let distance pt1 pt2 = vect_norm (vect_make pt1 pt2) + +(* ============================================================================= *) +(* = Rotation d'un vecteur d'un angle alpha en radians = *) +(* ============================================================================= *) +let vect_rotate_rad v alpha = + let c = cos alpha and s = sin alpha in + {x2D= v.x2D *. c -. v.y2D *. s; y2D= v.x2D *. s +. v.y2D *. c} + +(* ============================================================================= *) +(* = Rotation d'un vecteur d'un angle alpha en degres = *) +(* ============================================================================= *) +let vect_rotate v alpha = vect_rotate_rad v (deg2rad alpha) + +(* ============================================================================= *) +(* = Creation d'un vecteur normal au vecteur v (rotation de 90 degres positive)= *) +(* ============================================================================= *) +let vect_rotate_90 v = {x2D= -.v.y2D; y2D=v.x2D} + +(* ============================================================================= *) +(* = Ajoute deux vecteurs (ou d'un point et d'un vecteur) = *) +(* ============================================================================= *) +let vect_add u v = {x2D=u.x2D+.v.x2D; y2D=u.y2D+.v.y2D} + +(* ============================================================================= *) +(* = Soustraction de deux vecteurs (ou d'un point et d'un vecteur) = *) +(* ============================================================================= *) +let vect_sub u v = {x2D=u.x2D-.v.x2D; y2D=u.y2D-.v.y2D} + +(* ============================================================================= *) +(* = Multiplication d'un vecteur par un flottant = *) +(* ============================================================================= *) +let vect_mul_scal v m = {x2D=m*.v.x2D; y2D=m*.v.y2D} + +(* ============================================================================= *) +(* = Operation B=lamba.v+A = *) +(* ============================================================================= *) +let vect_add_mul_scal lambda a v = vect_add a (vect_mul_scal v lambda) + +(* ============================================================================= *) +(* = Vecteur oppose = *) +(* ============================================================================= *) +let vect_inverse v = vect_mul_scal v (-1.) + +(* ============================================================================= *) +(* = Milieu d'un segment = *) +(* ============================================================================= *) +let point_middle p1 p2 = {x2D=(p1.x2D+.p2.x2D)/.2.; y2D= (p1.y2D+.p2.y2D)/.2.} + +(* ============================================================================= *) +(* = Barycentre d'une liste de points avec ou sans coefficients = *) +(* ============================================================================= *) +let barycenter lst_pts = + let v = List.fold_left (fun p pt -> vect_add p pt) null_vector lst_pts in + vect_mul_scal v (1.0/.(float_of_int (List.length lst_pts))) + +let weighted_barycenter lst_pts lst_coeffs = + let (v, somme_coeffs) = + List.fold_left2 (fun (p, s) pt c -> (vect_add_mul_scal c p pt, s+.c)) + (null_vector, 0.0) lst_pts lst_coeffs in + vect_mul_scal v (1.0/.somme_coeffs) + +(* ============================================================================= *) +(* = Produit scalaire = *) +(* ============================================================================= *) +let dot_product u v = u.x2D*.v.x2D +. u.y2D*.v.y2D + +(* ============================================================================= *) +(* = Produit vectoriel = *) +(* ============================================================================= *) +let cross_product u v = u.x2D*.v.y2D -. u.y2D*.v.x2D + + +(* ============================================================================= *) +(* = = *) +(* = Projections = *) +(* = = *) +(* ============================================================================= *) + +(* ============================================================================= *) +(* = Projection d'un point pt sur une droite (a, u) = *) +(* ============================================================================= *) +let point_project_on_line pt a u = + let v = vect_make a pt in + let n = vect_norm u in + let lambda = ((dot_product u v)/.(cc n)) in + vect_add_mul_scal lambda a u + +(* ============================================================================= *) +(* = Projection d'un point sur un segment, si possible = *) +(* ============================================================================= *) +let point_project_on_segment pt a b = + let v = vect_make a pt and u = vect_make a b in + let n = vect_norm u in + let lambda = ((dot_product u v)/.(cc n)) in + if lambda>=0. && lambda <=1. then Some (vect_add_mul_scal lambda a u) + else None + +(* ============================================================================= *) +(* = Projection d'un point sur un ensemble de segments, si possible = *) +(* ============================================================================= *) +let point_project_on_segments_list pt lst_points = + let proj = ref None and dist = ref 0. in + let rec f l = + match l with + a::b::reste -> + (match point_project_on_segment pt a b with + None -> () + | Some p -> + (* Le point se projete sur le segment ab, on teste si la distance *) + (* de pt au segment ab est inferieure a la distance courante, si *) + (* oui alors le point p est le point recherche *) + let d = distance p pt in + (match !proj with + None -> dist:= d; proj:=Some p + | Some _ -> if d < !dist then begin dist:= d; proj:=Some p end + )) ; + f (b::reste) + | _ -> !proj + in + f lst_points + + +(* ============================================================================= *) +(* = = *) +(* = Distances = *) +(* = = *) +(* ============================================================================= *) + + +(* ============================================================================= *) +(* = Distance d'un point a une droite = *) +(* ============================================================================= *) +let distance_point_line pt a u = + let v = vect_make pt a in abs_float ((cross_product u v)/.(vect_norm u)) + +(* ============================================================================= *) +(* = Distance d'un point a un ensemble de segments = *) +(* ============================================================================= *) +let distance_point_segments_list pt lst_points = + match point_project_on_segments_list pt lst_points with + None -> None + | Some p -> Some (distance p pt) + + +(* ============================================================================= *) +(* = = *) +(* = Intersections = *) +(* = = *) +(* ============================================================================= *) + + +(* ============================================================================= *) +(* = Routine globale d'intersection = *) +(* = a = point + u = vecteur directeur de la premiere droite = *) +(* = b = point + v = vecteur directeur de la seconde droite = *) +(* ============================================================================= *) +let crossing_point a u c v = + let x = vect_make c a in + let num1 = cross_product x v and num2 = cross_product x u + and denom = -.(cross_product u v) in + + if denom = 0. then + (* Les deux vecteurs sont paralleles *) + None + else begin + let r = num1 /. denom and s = num2 /. denom in + let type_intersection_seg1 = + if abs_float r < epsilon then T_ON_PT1 + else if abs_float (r-.1.0) < epsilon then T_ON_PT2 + else if r<0.0 then T_OUT_SEG_PT1 + else if r>1.0 then T_OUT_SEG_PT2 + else T_IN_SEG1 + + and type_intersection_seg2 = + if abs_float s < epsilon then T_ON_PT3 + else if abs_float (s-.1.0) < epsilon then T_ON_PT4 + else if s<0.0 then T_OUT_SEG_PT3 + else if s>1.0 then T_OUT_SEG_PT4 + else T_IN_SEG2 + + and pt_intersection = vect_add_mul_scal r a u in + + Some (type_intersection_seg1, type_intersection_seg2, pt_intersection) + end + +(* ============================================================================= *) +(* = Test du type d'intersection = *) +(* ============================================================================= *) +let test_in_segment t = + (t=T_IN_SEG1)||(t=T_ON_PT1)||(t=T_ON_PT2)|| + (t=T_IN_SEG2)||(t=T_ON_PT3)||(t=T_ON_PT4) +let test_on_hl t = (test_in_segment t)||(t=T_OUT_SEG_PT4)||(t=T_OUT_SEG_PT2) + +(* ============================================================================= *) +(* = Teste l'intersection de deux segments (a,b) et (c,d) = *) +(* ============================================================================= *) +let crossing_seg_seg a b c d = + match crossing_point a (vect_make a b) c (vect_make c d) with + None -> false + | Some (type1, type2, pt) -> (test_in_segment type1)&&(test_in_segment type2) + +(* ============================================================================= *) +(* = Teste l'intersection d'un segment (a,b) et d'une demi-droite (c,v) = *) +(* ============================================================================= *) +let crossing_seg_hl a b c v = + match crossing_point a (vect_make a b) c v with + None -> false + | Some (type1, type2, pt) -> + (* OK si intersection sur la demi-droite *) + (test_in_segment type1) && (test_on_hl type2) + +(* ============================================================================= *) +(* = Teste l'intersection de deux demi-droites = *) +(* ============================================================================= *) +let crossing_hl_hl a u c v = + let inter = crossing_point a u c v in + match inter with + None -> false + | Some (type1, type2, pt) -> (test_on_hl type1) && (test_on_hl type2) + +(* ============================================================================= *) +(* = Teste l'intersection de deux droites et renvoie le point s'il existe = *) +(* ============================================================================= *) +let crossing_lines a u c v = + match crossing_point a u c v with + None -> (false, null_vector) + | Some (type1, type2, pt) -> (true, pt) + + +(* ============================================================================= *) +(* = = *) +(* = Polygones = *) +(* = par defaut ils sont consideres comme ouverts = *) +(* = = *) +(* ============================================================================= *) + + +(* ============================================================================= *) +(* = Teste si un polygone est ferme = *) +(* ============================================================================= *) +let poly_is_closed poly = + if poly=[] then false else point_same (List.hd poly) (List.hd (List.rev poly)) + +(* ============================================================================= *) +(* = Ferme un polygone [A; B; C; D] -> [A; B; C; D; A] = *) +(* ============================================================================= *) +let poly_close poly = + if poly = [] or poly_is_closed poly then poly else poly@[List.hd poly] + +(* ============================================================================= *) +(* = Ferme un polygone [A; B; C; D] -> [|A; B; C; D; A; B|] = *) +(* ============================================================================= *) +let poly_close2 poly = + if List.length poly < 2 then Array.of_list poly else begin + let poly = if poly_is_closed poly then poly else poly@[List.hd poly] in + Array.of_list (poly@[List.hd (List.tl poly)]) + end + +(* ============================================================================= *) +(* = Indique si un point est dans un polygone = *) +(* ============================================================================= *) +let point_in_poly pt poly = + let p = Array.of_list poly in + + let do_func {x2D=xi; y2D=yi} {x2D=xj; y2D=yj} {x2D=x; y2D=y} c = + if (((yi<=y) && (y do_func p0 p.(!j) pt is_in ; j := i) p ; + + (* Resultat *) + !is_in + +(* ============================================================================= *) +(* = Indique si un point est dans un cercle = *) +(* ============================================================================= *) +let point_in_circle pt (center, r) = distance pt center <= r + +(* ============================================================================= *) +(* = Calcul de l'enveloppe convexe d'un polygone = *) +(* ============================================================================= *) +let convex_hull poly = + let det a b = + match cross_product a b with 0.0 -> 0.0 | n when n>0.0 -> 1.0 | _ -> -1.0 + in + + let du_meme_cote a b c d = + let u = vect_make a b and v = vect_make a c and w = vect_make a d in + (det u v)*.(det u w)>0.0 + in + + let plus_proche a b c d = + (d=a) or ((not(c=a)) & + ((du_meme_cote b c d a) or ( + let u = vect_make b c and v = vect_make b d in + (det u v)=0.0 & (abs_float(u.x2D)+.abs_float(u.y2D)> + abs_float(v.x2D)+.abs_float(v.y2D))))) + in + + let extract_mini p l = + let rec aux reste vu mini = + match reste with + t::q -> + if (try(p mini t) with _ -> false) then aux q (t::vu) mini + else aux q (mini::vu) t + | [] -> mini,vu + in match l with + t::q -> aux q [] t + | [] -> raise Exit + in + + let f (x,y) = {x2D=x;y2D=y} in + let p a b = a.x2Db.y2D) in + let l2=poly in + let debut,_=extract_mini p l2 in + let rec itere a o liste sol = + let p = plus_proche a o in + let u,v=extract_mini p liste in + if (u=debut) then (List.rev((u::sol))) else (itere o u v (u::sol)) + in + itere {x2D=debut.x2D+.1.0;y2D=debut.y2D} debut l2 [debut] + +(* ============================================================================= *) +(* = Intersection d'un segment et d'un polygone (non ferme) = *) +(* ============================================================================= *) +let crossing_seg_poly a b poly = + (* Supprime les doublons dans une liste triee *) + let supprime_doublons_points l = + let (p, new_l) = List.fold_left (fun (old, lst) pt -> + match old with + None -> (Some pt, [pt]) + | Some p -> if point_same p pt then (old, lst) else (Some pt, pt :: lst) + ) (None, []) l in + List.rev new_l + in + + let u = vect_make a b and pol = Array.of_list poly + and lst_pts_inter = ref [] in + + for i = 0 to (Array.length pol-1) do + let c = pol.(i) and + (* Rappel : le polygone n'est pas ferme... *) + d = if i < (Array.length pol) -1 then pol.(i+1) else pol.(0) in + let inter = crossing_point a u c (vect_make c d) in + match inter with + None -> () (* Pas d'intersection entre le segment et l'arrete *) + | Some (type1, type2, pt) -> + if (test_in_segment type1) && (test_in_segment type2) then + (* L'intersection est bien sur les 2 segments *) + lst_pts_inter := pt :: !lst_pts_inter + done ; + + (* Suppression des doublons dans la liste des points d'intersection *) + (* Il y a des doublons si intersection sur un sommet du polygone *) + supprime_doublons_points !lst_pts_inter + +(* ============================================================================= *) +(* = Intersection sans prendre en compte les sommets = *) +(* ============================================================================= *) +let crossing_seg_poly_exclusive a b poly = + let u = vect_make a b and + pol = Array.of_list poly and + lst_pts_inter = ref [] in + + for i = 0 to (Array.length pol-1) do + let c = pol.(i) and + (* Rappel : le polygone n'est pas ferme... *) + d = if i < (Array.length pol) -1 then pol.(i+1) else pol.(0) in + let inter = crossing_point a u c (vect_make c d) in + match inter with + None -> () (* Pas d'intersection entre le segment et l'arrete *) + | Some (type1, type2, pt) -> + if (type1=T_IN_SEG1) && (type2=T_IN_SEG2) then + (* L'intersection est bien sur les 2 segments *) + lst_pts_inter := pt :: !lst_pts_inter ; + done ; + + !lst_pts_inter + +(* ============================================================================= *) +(* = Cercle circonscrit a un triangle = *) +(* ============================================================================= *) +let circumcircle {x2D=x1; y2D=y1} {x2D=x2; y2D=y2} {x2D=x3; y2D=y3} = + (* Determinants de matrices 3x3 *) + let eval_det a1 a2 a3 b1 b2 b3 c1 c2 c3 = + a1*.b2*.c3-.a1*.b3*.c2-.a2*.b1*.c3+.a2*.b3*.c1+.a3*.b1*.c2-.a3*.b2*.c1 in + let eval_det1 a1 a2 b1 b2 c1 c2 = eval_det a1 a2 1. b1 b2 1. c1 c2 1. in + + let a = eval_det1 x1 y1 x2 y2 x3 y3 in + let s1 = (cc x1)+.(cc y1) and s2 = (cc x2)+.(cc y2) and s3 = (cc x3)+.(cc y3) in + let bx = eval_det1 s1 y1 s2 y2 s3 y3 in + let by = -.(eval_det1 s1 x1 s2 x2 s3 x3) in + let c = -.(eval_det s1 x1 y1 s2 x2 y2 s3 x3 y3) in + let a = 2.*.a in + let xc = bx/.a and yc = by/.a in + let r = abs_float ((sqrt(bx*.bx+.by*.by-.2.*.a*.c))/.a) in + + (* Position du centre et rayon *) + ({x2D=xc; y2D=yc}, r) + +(* ============================================================================= *) +(* = Test sens horaire ou inverse = *) +(* ============================================================================= *) +let ccw_angle p0 p1 p2 = + let p = cross_product (vect_make p0 p1) (vect_make p1 p2) in + if p > 0. then CCW else if p < 0. then CW else CCW_UNDEFINED + +(* ============================================================================= *) +(* = Teste si un polygone est concave ou convexe = *) +(* = Il est convexe si toutes les arretes consecutives sont dans la meme sens = *) +(* ============================================================================= *) +let poly_test_convex l = + if List.length l > 2 then begin + (* l = [A; B; C; D] -> t = [|A; B; C; D; A; B|] *) + let t = poly_close2 l in + let n = Array.length t in + let sign = ccw_angle t.(0) t.(1) t.(2) and i = ref 1 in + while !i 2 then begin + let t = Array.of_list (poly_close l) in + if poly_test_convex l = CONVEX then ccw_angle t.(0) t.(1) t.(2) + else begin + let s = ref 0. in + for i = 0 to (Array.length t-2) do + s:= !s+.cross_product t.(i) t.(i+1) + done ; + if !s>0. then CCW else if !s<0. then CW else CCW_UNDEFINED + end + end else CCW_UNDEFINED + +(* ============================================================================= *) +(* = Surface d'un polygone (signee) = *) +(* ============================================================================= *) +let poly_signed_area poly = + (* On peut le faire avec des produits vectoriels mais la facon suivante est *) + (* plus efficace et plus precise *) + + if List.length poly < 2 then 0. else begin + let poly = poly_close2 poly in + let n = Array.length poly -2 and area = ref 0. in + for i = 1 to n do + area:=!area+.poly.(i).x2D*.(poly.(i+1).y2D-.poly.(i-1).y2D) + done ; + !area/.2. + end + +(* ============================================================================= *) +(* = Surface d'un polygone (non signee) = *) +(* ============================================================================= *) +let poly_area poly = abs_float (poly_signed_area poly) + +(* ============================================================================= *) +(* = Centroide d'un polygone = *) +(* ============================================================================= *) +let poly_centroid poly = + (* On peut trianguler et ponderer le centre de chaque triangle par sa *) + (* surface mais on peut faire plus efficace. Ici, on prend un point du *) + (* polygone (le premier par ex.) et on pondere l'aire (signee) des *) + (* triangles construits a partir de ce point. *) + + (* Centroide d'un triangle *) + let centroid_triangle p1 p2 p3 = + {x2D=(p1.x2D+.p2.x2D+.p3.x2D)/.3.; y2D=(p1.y2D+.p2.y2D+.p3.y2D)/.3.} in + + (* Aire signee d'un triangle, pas besoin de poly_area... *) + let area_triangle p1 p2 p3 = + (cross_product (vect_make p1 p2) (vect_make p1 p3))/.2. + in + + let rec f p0 l centroid = + match l with + p1::p2::reste -> + let new_centroid = vect_add_mul_scal (area_triangle p0 p1 p2) centroid + (centroid_triangle p0 p1 p2) in + f p0 (p2::reste) new_centroid + | _ -> + let area = poly_signed_area poly in + vect_mul_scal centroid (1./.area) + in + + match poly with + [] -> null_vector + | p::[] -> p + | p1::p2::[] -> point_middle p1 p2 + | _ -> f (List.hd poly) (List.tl poly) null_vector + +(* ============================================================================= *) +(* = = *) +(* = Triangulation de polygones = *) +(* = par defaut ils sont consideres comme ouverts = *) +(* = = *) +(* ============================================================================= *) + + +(* ============================================================================= *) +(* = Triangulation d'un polygone. Vielle version incorrecte dans certains cas = *) +(* ============================================================================= *) +let in_tesselation_old l0 = + (* Recherche des extremes et du centre *) + let {x2D=x; y2D=y} = List.hd l0 in + let xmin = ref x and xmax = ref x and ymin = ref y and ymax = ref y in + List.iter (fun {x2D=x; y2D=y} -> + if x< !xmin then xmin:=x; if x> !xmax then xmax:=x; + if y< !ymin then ymin:=y; if y> !ymax then ymax:=y) l0 ; + let dmax = max (!xmax -. !xmin) (!ymax -. !ymin) in + let pmid = point_middle {x2D= !xmin; y2D= !ymin} {x2D= !xmax; y2D= !ymax} in + + (* Recherche du triangle englobant (supertriangle) *) + let n = List.length l0 in + let t = Array.of_list (l0@[{x2D=pmid.x2D-.2.*.dmax; y2D=pmid.y2D-.dmax} ; + {x2D=pmid.x2D; y2D=pmid.y2D+.2.*.dmax} ; + {x2D=pmid.x2D+.2.*.dmax; y2D=pmid.y2D-.dmax}]) in + let triangles = ref [(n, n+1, n+2)] in + + (* Tous les points du contour sont inseres les uns apres les autres *) + Array.iteri (fun i point -> + let edges = ref [] in + + triangles := List.fold_left (fun l (p1, p2, p3) -> + (* Cercle circonscrit au triangle *) + let circle = circumcircle t.(p1) t.(p2) t.(p3) in + if point_in_circle point circle then begin + (* Ajout de 3 arretes et suppression du triangle en cours *) + edges := (p3,p1)::(p2,p3)::(p1,p2)::!edges ; l + end else (p1, p2, p3)::l) [] !triangles ; + + (* Creation de nouveaux triangles a partir du point courant pour les *) + (* arretes non multiples ou qui apparaissent un nombre impair de fois *) + let ledges = ref !edges in + List.iter (fun (n1, n2) -> + let l = List.find_all (fun (n01, n02) -> + (n01=n1&&n02=n2) or (n01=n2&&n02=n1)) !ledges in + if List.length l mod 2 <> 0 then begin + triangles:=(n1, n2, i)::!triangles; + (* Si l'arrete apparait un nombre impair de fois > 1 alors *) + (* on n'insere que ce triangle et pas les suivants, sinon *) + (* certains triangles apparaissent plusieurs fois *) + if List.length l>=3 then ledges:=!ledges@[(n1, n2)] + end) !edges) t ; + + let triangle_ok (p1, p2, p3) = + let check p1 p2 = + if p1-p2=1 or p2-p1=1 or (p1=0&&p2=n-1) or (p1=n-1&&p2=0) then true + else point_in_poly (point_middle t.(p1) t.(p2)) l0 + in + check p1 p2 && check p2 p3 && check p3 p1 + in + + (* Les triangles ayant des points du supertriangle sont elimines ainsi *) + (* que tous les triangles se trouvant a l'exterieur du contour initial *) + (* car ce cas arrive lorsque le contour original est concave... *) + let l = List.fold_left (fun l (p1, p2, p3) -> + if p1>=n or p2>=n or p3>=n or not (triangle_ok (p1, p2, p3)) then l + else (p1, p2, p3)::l) [] !triangles in + + (* Renvoie la liste des triangles CW *) + let l = List.map (fun (p1, p2, p3) -> + if ccw_angle t.(p1) t.(p2) t.(p3) = CW then (p1, p2, p3) else (p1, p3, p2)) l in + + (* Tableau des points et liste des triangles *) + (* Normalement, si n points differents au depart -> n-2 triangles en sortie *) + if List.length l0<>(List.length l)+2 then begin + Printf.printf "AAAA %d points %d triangles\n" (List.length l0) (List.length l); + flush stdout + end ; + (Array.of_list l0, l) + +(* ============================================================================= *) +(* = Triangulation d'un polygone = *) +(* ============================================================================= *) +let in_tesselation poly = + (* On teste si le polygone est bien CCW, s'il ne l'est pas on l'inverse *) + let (switched, l)= + match poly_test_ccw poly with + CW -> (true, List.rev poly) + | _ -> (false, poly) + in + + (* Creation du tableau des points sous la forme necessaire a la triangulation *) + let vertices = + let t = Array.of_list l and n = List.length l and vertices = ref [] in + Array.iteri (fun i pt -> + vertices:={pos=pt; num=i; ear=false; + prev=if i=0 then n-1 else i-1; + next=if i=n-1 then 0 else i+1}::!vertices) t ; + Array.of_list (List.rev !vertices) + in + + (* Fonction testant si les points d'indices n1 et n2 forment une diagonale *) + (* completement contenue dans le polygone *) + let is_diagonal n1 n2 = + let lefton a b c = cross_product (vect_make a b) (vect_make a c) >= 0. in + let left a b c = cross_product (vect_make a b) (vect_make a c) > 0. in + + let is_in_cone a b = + let a1=vertices.(a.next) and a0=vertices.(a.prev) in + + (* Point A convexe ? *) + if lefton a.pos a1.pos a0.pos then + (left a.pos b.pos a0.pos) && (left b.pos a.pos a1.pos) + else not ((lefton a.pos b.pos a1.pos) && (lefton b.pos a.pos a0.pos)) + in + + let a = vertices.(n1) and b = vertices.(n2) in + +(* AAA if is_in_cone a b && is_in_cone b a then begin *) + if is_in_cone a b or is_in_cone b a then begin + let rec f l = + match l with + c::reste -> + let c1 = vertices.(c.next) in + if c.num<>a.num && c1.num<>a.num && c.num<>b.num && c1.num<>b.num && + crossing_seg_seg a.pos b.pos c.pos c1.pos then false + else f reste + | [] -> true + in + f (Array.to_list vertices) + end else false + in + + (* Initialisation des oreilles *) + Array.iter (fun v1 -> v1.ear <- is_diagonal v1.prev v1.next) vertices ; + + (* Triangulation *) + let current_idx = ref 0 and earfound = ref false and lst_triangles = ref [] + and n = ref (Array.length vertices) in + while !n>=3 do + earfound:=false ; + let v2 = ref !current_idx and finished = ref false in + while not !finished && not !earfound do + if vertices.(!v2).ear then begin + (* Le point courant correspond a une oreille, on va le supprimer *) + earfound:=true ; + + (* 5 points consecutifs, v2 est au 'milieu' des 5 *) + let v1=vertices.(!v2).prev and v3=vertices.(!v2).next in + let v0=vertices.(v1).prev and v4=vertices.(v3).next in + + (* Sauvegarde du triangle. Pas sous la forme v1, v2, v3 sinon *) + (* il n'est pas CCW et donc pb de normale exterieure a l'affichage *) + lst_triangles := (v3, !v2, v1)::!lst_triangles ; + + (* Mise a jour des oreilles *) + vertices.(v1).ear <- is_diagonal v0 v3 ; + vertices.(v3).ear <- is_diagonal v1 v4 ; + + (* Suppression du point v2 *) + vertices.(v1).next <- v3 ; + vertices.(v3).prev <- v1 ; + current_idx:=v3 ; + + (* Un triangle de moins a chercher *) + decr n + end else v2:=vertices.(!v2).next ; + + (* C'est fini quand on revient sur le point initial *) + finished:= !v2 = !current_idx + done ; + done ; + + if not !earfound then begin Printf.printf "No ear !\n"; flush stdout end ; + + (* Si le polygone etait CW au depart, il a ete retourne et les numeros *) + (* des points ne sont alors pas dans le meme sens que le polygone passe *) + (* par l'utilisateur. On remet alors les numeros comme ils etaients lors *) + (* de l'appel a la fonction de triangulation *) + if switched then begin + let n = List.length poly in + lst_triangles:=List.map (fun (p1, p2, p3) -> (n-1-p1, n-1-p2, n-1-p3) + ) !lst_triangles + end ; + + !lst_triangles + +(* ============================================================================= *) +(* = Triangulation d'un polygone = *) +(* ============================================================================= *) +let tesselation l = + let t = Array.of_list l in + List.map (fun (p1, p2, p3) -> [t.(p1); t.(p2); t.(p3)]) (in_tesselation l) + +(* ============================================================================= *) +(* = Recherche des triangles fan dans une liste de triangles = *) +(* ============================================================================= *) +let in_tesselation_fans l = + let t = Array.of_list l in + let l = in_tesselation l in + let tt = Array.mapi (fun i x -> (i, 0)) t in + let add_val x = let (p, n) = tt.(x) in tt.(x) <- (p, n+1) in + List.iter (fun (p1, p2, p3) -> + add_val p1; add_val p2; add_val p3) l ; + let lst = List.fast_sort (fun (_, n1) (_, n2) -> n2-n1) (Array.to_list tt) in + + let tt2 = Array.create (Array.length tt) (0, []) in + let i = ref 0 in + List.iter (fun (x, _) -> tt2.(x) <- (!i, []); incr i) lst ; + List.iter (fun (p1, p2, p3) -> + let (t1, l1) = tt2.(p1) and (t2, l2) = tt2.(p2) and (t3, l3) = tt2.(p3) in + if t1 + let l0 = ref [] in + let add_element (a, b) = + let rec f deb fin = + match fin with + [] -> (a, b, [a; b])::!l0 + | (c, d, lst)::reste -> + if b=c then begin + (* Insertion avant *) + (List.rev ((a, d, a::lst)::deb))@reste + end else if a=d then begin + (* Insertion apres *) + (List.rev ((c, b, lst@[b])::deb))@reste + end else f ((c, d, lst)::deb) reste + in + l0:=f [] !l0 + in + let merge_lists () = + let rec in_merge (a, b, l1) ll0 ll = + match ll with + (c, d, l2)::reste -> + if b=c then + (true, ((a, d, l1@(List.tl l2))::ll0)@reste) + else if d=a then + (true, ((c, b, l2@(List.tl l1))::ll0)@reste) + else in_merge (a, b, l1) ((c, d, l2)::ll0) reste + | [] -> (false, ll0) + in + let rec f l ll = + match l with + l1::reste -> + let (merged, newl) = in_merge l1 [] reste in + if merged then f newl ll + else f reste (l1::ll) + | [] -> ll + in + l0:=f !l0 [] + in + + if l<>[] then begin + List.iter (fun x -> add_element x; merge_lists ()) l ; + List.iter (fun (_, _, l) -> + lst_fans := (i::l)::!lst_fans) !l0 + end) tt2 ; + + (t, !lst_fans) + +(* ============================================================================= *) +(* = Triangulation en triangles_fan = *) +(* ============================================================================= *) +(* effectue la triangulation du polygone en + triangle_fan OpenGL. En sortie est renvoyee une liste contenant des listes + de points. Chacune de ces listes de points contient soit 3 points (triangle) + soit plus de 3 points (pour un triangle_fan) + *) + +let tesselation_fans l = + let (t, l) = in_tesselation_fans l in + List.map (fun l -> List.map (fun x -> t.(x)) l) l + + + + + + +type pt_2D_polar = { r2D : float; theta2D : float; } + +let cart2polar p = {r2D = vect_norm p; theta2D = atan2 p.y2D p.x2D} + +let polar2cart p = {x2D = p.r2D *. cos p.theta2D; y2D = p.r2D *. sin p.theta2D} + + +(* grosses conneries d'avions *) + +let two_m_pi = 2. *. m_pi +let m_pi_two = m_pi /. 2. + +let wind_dir_from_angle_rad rad = + let w = ref (3. *. m_pi_two -. rad) in + while !w > two_m_pi do + w := !w -. two_m_pi done; + !w + +let heading_of_to_angle_rad angle = + let a = ref (5. *. m_pi_two -. angle) in + while !a >= two_m_pi do a := !a -. two_m_pi done; + !a + +let norm_angle_rad a = + let a = ref a in + while !a < -. m_pi do a := !a +. two_m_pi done; + while !a > m_pi do a := !a -. two_m_pi done; + !a + +let norm_heading_rad a = + let a = ref a in + while !a < 0. do a := !a +. two_m_pi done; + while !a > two_m_pi do a := !a -. two_m_pi done; + !a + +let oposite_heading_rad rad = + norm_heading_rad (rad +. m_pi) + + +(* =============================== FIN ========================================= *) diff --git a/sw/lib/ocaml/geometry_2d.mli b/sw/lib/ocaml/geometry_2d.mli new file mode 100644 index 00000000000..cbce16edb46 --- /dev/null +++ b/sw/lib/ocaml/geometry_2d.mli @@ -0,0 +1,303 @@ +(* + * $Id$ + * + * 2D Geometry + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(** Module de géométrie 2D + + Par défaut, les polygones sont considérés comme étant ouverts + + {e Yann Le Fablec, version 1.0, 17/04/2003} + *) + +(** {6 Types} *) + +(** Type point/vecteur 2D en coordonnees cartesiennes *) +type pt_2D = { x2D : float; y2D : float; } + +(** Type point/vecteur 2D en coordonnees polaires *) +type pt_2D_polar = { r2D : float; theta2D : float; } + +(** Vecteur nul en 2D *) +val null_vector : pt_2D + +(** Un polygone *) +type poly_2D = pt_2D list + +(** Types d'intersections pour le croisement entre deux segments + [\[P1, P2\]] et [\[P3, P4\]] *) +type t_crossing = + T_IN_SEG1 (** Dans le segment 1 *) + | T_IN_SEG2 (** Dans le segment 2 *) + | T_ON_PT1 (** Sur le premier point du segment 1 *) + | T_ON_PT2 (** Sur le second point du segment 1 *) + | T_ON_PT3 (** Sur le premier point du segment 1 *) + | T_ON_PT4 (** Sur le second point du segment 2 *) + | T_OUT_SEG_PT1 (** En dehors du segment 1, avant le premier point *) + | T_OUT_SEG_PT2 (** En dehors du segment 1, après le second point *) + | T_OUT_SEG_PT3 (** En dehors du segment 2, avant le premier point *) + | T_OUT_SEG_PT4 (** En dehors du segment 2, après le second point *) + +(** indique le type d'un polygone *) +type t_conv = CONVEX | CONCAVE | CONV_UNDEFINED + +(** indique le sens d'un polygone (horaire ou contre-horaire) *) +type t_ccw = CW | CCW | CCW_UNDEFINED + +(** {6 Conversions d'angles} *) + +(** [deg2rad angle_degres] donne l'angle correpondant en radians *) +val deg2rad : float -> float + +(** [rad2deg angle_radians] donne l'angle correpondant en degrés *) +val rad2deg : float -> float + +(** {6 Points} *) + +(** [point_same A B] teste l'égalité stricte des deux points *) +val point_same : pt_2D -> pt_2D -> bool + +(** [distance A B] évalue la distance entre les points [A] et [B] *) +val distance : pt_2D -> pt_2D -> float + +(** [point_middle A B] renvoie le milieu du segment formé par [A] et [B] *) +val point_middle : pt_2D -> pt_2D -> pt_2D + +(** [barycenter lst_pts] renvoie le barycentre des points *) +val barycenter : pt_2D list -> pt_2D + +(** [weighted_barycenter lst_pts lst_poids] renvoie le barycentre des points + pondérés par [lst_poids] *) +val weighted_barycenter : pt_2D list -> float list -> pt_2D + +(** {6 Tests d'inclusion} *) + +(** [point_in_poly pt poly] teste si le point se trouve dans le polygone *) +val point_in_poly : pt_2D -> poly_2D -> bool + +(** [point_in_circle pt (centre_cercle, rayon_cercle)] teste si le point + se trouve dans le cercle indiqué *) +val point_in_circle : pt_2D -> pt_2D * float -> bool + +(** {6 Vecteurs} *) + +(** [vect_make A B] crée le vecteur AB *) +val vect_make : pt_2D -> pt_2D -> pt_2D + +(** [vect_norm v] renvoie la norme du vecteur *) +val vect_norm : pt_2D -> float + +(** [vect_normalize v] normalise le vecteur *) +val vect_normalize : pt_2D -> pt_2D + +(** [vect_set_norm v norme] change [v] pour que sa norme soit [norme] *) +val vect_set_norm : pt_2D -> float -> pt_2D + +(** [vect_add u v] réalise la somme des deux vecteurs*) +val vect_add : pt_2D -> pt_2D -> pt_2D + +(** [vect_sub u v] renvoie la soustraction de [v] à [u] *) +val vect_sub : pt_2D -> pt_2D -> pt_2D + +(** [vect_mul_scal v scalaire] multiple le vecteur par un scalaire *) +val vect_mul_scal : pt_2D -> float -> pt_2D + +(** [vect_add_mul_scal lambda p v] renvoie le point [p] translaté du + vecteur [lambda.v] *) +val vect_add_mul_scal : float -> pt_2D -> pt_2D -> pt_2D + +(** [vect_rotate_rad v angle] tourne le vecteur de l'angle indiqué en radians *) +val vect_rotate_rad : pt_2D -> float -> pt_2D + +(** [vect_rotate v angle] tourne le vecteur de l'angle indiqué en degrés *) +val vect_rotate : pt_2D -> float -> pt_2D + +(** [vect_rotate_90 v] renvoie le vecteur normal à [v] (rotation de 90 degrés + dans le sens trigonométrique) *) +val vect_rotate_90 : pt_2D -> pt_2D + +(** [vect_inverse v] renvoie le vecteur opposé à [v] *) +val vect_inverse : pt_2D -> pt_2D + +(** {6 Produits} *) + +(** [dot_product u v] fournit le produit scalaire des deux vecteurs *) +val dot_product : pt_2D -> pt_2D -> float + +(** [cross_product u v] renvoie le produit vectoriel de [u] et [v] *) +val cross_product : pt_2D -> pt_2D -> float + +(** {6 Intersections de segments/droites} *) + +(** [crossing_point A u B v] teste l'intersection de deux droites : la première + passant par le point [A] et de vecteur directeur [u] et la seconde passant par [B] + et de vecteur directeur [v]. + + En sortie, deux possibilités : + - [None] s'il n'y a pas d'intersection + - [Some (type1, type2, point_intersection)] sinon. [type1] désigne le type + d'intersection sur la première droite et [type2] la meme information pour + la seconde droite. + *) +val crossing_point : + pt_2D -> + pt_2D -> pt_2D -> pt_2D -> (t_crossing * t_crossing * pt_2D) option + +(** [test_in_segment type_intersection] teste si l'intersection est dans le + segment 1 (extrémités incluses) *) +val test_in_segment : t_crossing -> bool + +(** [test_on_hl type_intersection] teste si l'intersection est sur la demi-droite + (extrémité incluse) *) +val test_on_hl : t_crossing -> bool + +(** [crossing_seg_seg A B C D] teste l'intersection des segments + [\[A,B\]] et [\[C,D\]] *) +val crossing_seg_seg : pt_2D -> pt_2D -> pt_2D -> pt_2D -> bool + +(** [crossing_seg_hl A B C u] teste l'intersection entre le segment [\[A,B\]] et + la droite passant par [C] et de vecteur directeur [u] *) +val crossing_seg_hl : + pt_2D -> pt_2D -> pt_2D -> pt_2D -> bool + +(** [crossing_hl_hl A u B v] teste l'intersection entre les deux demi-droites *) +val crossing_hl_hl : pt_2D -> pt_2D -> pt_2D -> pt_2D -> bool + +(** [crossing_lines A u B v] teste l'intersection entre les deux droites et renvoie le + point s'il y a effectivement intersection *) +val crossing_lines : pt_2D -> pt_2D -> pt_2D -> pt_2D -> bool * pt_2D + +(** {6 Distances} *) + +(** [distance_point_line P A u] renvoie la distance entre le point [P] et + la droite passant par [A] de vecteur directeur [u] *) +val distance_point_line : pt_2D -> pt_2D -> pt_2D -> float + +(** [distance_point_segments_list P lst_points] renvoie la distance mini entre [P] + et les segments formés par la liste de points [lst_points]. La distance peut + ne pas etre définie (auquel cas None est renvoyé) *) +val distance_point_segments_list : pt_2D -> poly_2D -> float option + +(** {6 Projections} *) + +(** [point_project_on_line P A u] renvoie la projection du point [P] sur + la droite passant par [A] de vecteur directeur [u] *) +val point_project_on_line : pt_2D -> pt_2D -> pt_2D -> pt_2D + +(** [point_project_on_segment P A B] renvoie la projection, si elle existe, + du point [P] sur le segment [\[A, B\]] *) +val point_project_on_segment : pt_2D -> pt_2D -> pt_2D -> pt_2D option + +(** [point_project_on_segments_list P lst_points] renvoie la projection, si elle existe, + de [P] sur les segments formés par la liste de points [lst_points] *) +val point_project_on_segments_list : pt_2D -> poly_2D -> pt_2D option + +(** {6 Polygones} *) + +(** [poly_is_closed poly] renvoie [TRUE] si le polygone est fermé *) +val poly_is_closed : poly_2D -> bool + +(** [poly_close poly] ferme le polygone s'il ne l'est pas deja. Ainsi + [\[A; B; C; D\]] devient [\[A; B; C; D; A\]] *) +val poly_close : poly_2D -> poly_2D + +(** [poly_close2 poly] effectue l'opération suivante : + [\[A; B; C; D\]] -> [\[|A; B; C; D; A; B|\]]. + Attention, ici le polygone retourné n'est pas une liste de points + mais un tableau de points *) +val poly_close2 : poly_2D -> pt_2D array + +(** [poly_area poly] renvoie l'aire du polygone *) +val poly_area : poly_2D -> float + +(** [poly_signed_area poly] renvoie l'aire signée du polygone *) +val poly_signed_area : poly_2D -> float + +(** [poly_centroid poly] renvoie le centroide du polygone *) +val poly_centroid : poly_2D -> pt_2D + +(** [ccw_angle A B C] indique le sens de l'angle formé par les vecteurs AB et BC *) +val ccw_angle : pt_2D -> pt_2D -> pt_2D -> t_ccw + +(** [poly_test_ccw poly] renvoie le sens horaire ou contre-horaire du polygone *) +val poly_test_ccw : poly_2D -> t_ccw + +(** [poly_test_convex poly] indique le type (convexe ou pas) du polygone *) +val poly_test_convex : poly_2D -> t_conv + +(** [convex_hull poly] fournit l'enveloppe convexe de la liste de points [poly] *) +val convex_hull : poly_2D -> pt_2D list + +(** [circumcircle A B C] renvoie le cercle circonscrit au triangle ABC sous la + forme [(C, r)] où [C] désigne le centre du cercle et [r] son rayon *) +val circumcircle : pt_2D -> pt_2D -> pt_2D -> pt_2D * float + +(** [crossing_seg_poly A B poly] renvoie la liste des intersections entre + le segment [\[AB\]] et le polygone. Les intersections peuvent se trouver + en [A], en [B] ou sur des sommets du polygone *) +val crossing_seg_poly : pt_2D -> pt_2D -> poly_2D -> pt_2D list + +(** [crossing_seg_poly_exclusive A B poly] meme chose que précédemment sauf + que [A], [B] et les sommets du polygones sont exclus *) +val crossing_seg_poly_exclusive : pt_2D -> pt_2D -> poly_2D -> pt_2D list + +(** {6 Triangulation de polygones} *) + +(** [in_tesselation poly] effectue la triangulation et renvoie une liste de + triplets indiquant les indices des points constituant les triangles *) +val in_tesselation : poly_2D -> (int * int * int) list + +(** [geom_tesselation polygone] effectue la triangulation du polygone et renvoie + la liste des triangles résultants. Ici un triangle est une liste de 3 points (!). + De plus, le polygone ne doit contenir aucun point en double. + *) +val tesselation : poly_2D -> poly_2D list + +(** [in_tesselation_fans poly] effectue la triangulation (en fans) et renvoie le tableau + contenant les points et une liste de triplets indiquant les indices des points + (dans le tableau) constituant les triangles_fan *) +val in_tesselation_fans : poly_2D -> pt_2D array * (int list) list + +(** [geom_tesselation_fans polygone] effectue la triangulation du polygone en + triangle_fan OpenGL. En sortie est renvoyée une liste contenant des listes + de points. Chacune de ces listes de points contient soit 3 points (triangle) + soit plus de 3 points (pour un triangle_fan). + De plus, le polygone ne doit contenir aucun point en double. + *) +val tesselation_fans : poly_2D -> poly_2D list + + +(** {6 Coordonnees polaires} *) + +val m_pi : float + +(** [cart2polar cart] *) +val cart2polar : pt_2D -> pt_2D_polar +val polar2cart : pt_2D_polar -> pt_2D + +val wind_dir_from_angle_rad : float -> float +val heading_of_to_angle_rad : float -> float +val norm_angle_rad : float -> float +val norm_heading_rad : float -> float +val oposite_heading_rad : float -> float diff --git a/sw/lib/ocaml/geometry_3d.ml b/sw/lib/ocaml/geometry_3d.ml new file mode 100644 index 00000000000..6867fe79e5b --- /dev/null +++ b/sw/lib/ocaml/geometry_3d.ml @@ -0,0 +1,610 @@ +(* + * $Id$ + * + * 3D Geometry + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Geometry_2d + +(* Type contenant un point 2D *) +type pt_3D = {x3D : float; y3D : float; z3D : float} + +(* Polygone 3D, non ferme par defaut *) +type poly_3D = pt_3D list + +(* Volume 3D *) +type volume_3D = poly_3D list + +(* Vecteurs nuls en 2D *) +let null_vector = {x3D=0.; y3D=0.; z3D=0.} + +(* Carre *) +let cc x = x*.x + +let epsilon = 0.0001 +type t_crossing3d = T_IN_SEG1 | T_IN_SEG2 | T_ON_PT1 | T_ON_PT2 | T_ON_PT3 +| T_ON_PT4 | T_OUT_SEG_PT1 | T_OUT_SEG_PT2 | T_OUT_SEG_PT3 | T_OUT_SEG_PT4 + +(* Type pour les differents axes *) +type t_axis3d = T_X3D | T_Y3D | T_Z3D + +(* ============================================================================= *) +(* = Determinant d'une matrice 3x3 = *) +(* ============================================================================= *) +let eval_det3 a1 a2 a3 b1 b2 b3 c1 c2 c3 = + a1*.b2*.c3-.a1*.b3*.c2-.a2*.b1*.c3+.a2*.b3*.c1+.a3*.b1*.c2-.a3*.b2*.c1 + +(* ============================================================================= *) +(* = Determinant d'une matrice 4x4 (par developpement en matrice 3x3 = *) +(* ============================================================================= *) +let eval_det4 a1 a2 a3 a4 b1 b2 b3 b4 c1 c2 c3 c4 d1 d2 d3 d4 = + let det1 = eval_det3 b2 b3 b4 c2 c3 c4 d2 d3 d4 + and det2 = eval_det3 b1 b3 b4 c1 c3 c4 d1 d3 d4 + and det3 = eval_det3 b1 b2 b4 c1 c2 c4 d1 d2 d4 + and det4 = eval_det3 b1 b2 b3 c1 c2 c3 d1 d2 d3 in + + a1*.det1-.a2*.det2+.a3*.det3-.a4*.det4 + +(* ============================================================================= *) +(* = Comparaison de points/vecteurs = *) +(* ============================================================================= *) +let point_same pt1 pt2 = + (pt1.x3D = pt2.x3D) && (pt1.y3D = pt2.y3D) && (pt1.z3D = pt2.z3D) + +(* ============================================================================= *) +(* = Creation d'un vecteur = *) +(* ============================================================================= *) +let vect_make pt1 pt2 = {x3D=pt2.x3D -. pt1.x3D; y3D=pt2.y3D -. pt1.y3D; + z3D=pt2.z3D -. pt1.z3D} + +(* ============================================================================= *) +(* = Norme d'un vecteur = *) +(* ============================================================================= *) +let vect_norm v = sqrt((cc v.x3D) +. (cc v.y3D) +. (cc v.z3D)) + +(* ============================================================================= *) +(* = Normalisation d'un vecteur = *) +(* ============================================================================= *) +let vect_normalize v = + let n = vect_norm v in {x3D=v.x3D/.n; y3D=v.y3D/.n; z3D=v.z3D/.n} + +(* ============================================================================= *) +(* = Force la norme d'un vecteur = *) +(* ============================================================================= *) +let vect_set_norm v norme = + let n = norme /. (vect_norm v) in {x3D=v.x3D*.n; y3D=v.y3D*.n; z3D=v.z3D*.n} + +(* ============================================================================= *) +(* = Distance entre deux points = *) +(* ============================================================================= *) +let distance pt1 pt2 = vect_norm (vect_make pt1 pt2) + +(* ============================================================================= *) +(* = Ajoute deux vecteurs (ou d'un point et d'un vecteur) = *) +(* ============================================================================= *) +let vect_add u v = {x3D=u.x3D+.v.x3D; y3D=u.y3D+.v.y3D; z3D=u.z3D+.v.z3D} + +(* ============================================================================= *) +(* = Soustraction de deux vecteurs (ou d'un point et d'un vecteur) = *) +(* ============================================================================= *) +let vect_sub u v = {x3D=u.x3D-.v.x3D; y3D=u.y3D-.v.y3D; z3D=u.z3D-.v.z3D} + +(* ============================================================================= *) +(* = Multiplication d'un vecteur par un flottant = *) +(* ============================================================================= *) +let vect_mul_scal v m = {x3D=m*.v.x3D; y3D=m*.v.y3D; z3D=m*.v.z3D} + +(* ============================================================================= *) +(* = Operation B=lamba.v+A = *) +(* ============================================================================= *) +let vect_add_mul_scal lambda a v = vect_add a (vect_mul_scal v lambda) + +(* ============================================================================= *) +(* = Vecteur oppose = *) +(* ============================================================================= *) +let vect_inverse v = vect_mul_scal v (-1.) + +(* ============================================================================= *) +(* = Milieu d'un segment = *) +(* ============================================================================= *) +let point_middle p1 p2 = {x3D=(p1.x3D+.p2.x3D)/.2.; y3D=(p1.y3D+.p2.y3D)/.2.; + z3D=(p1.z3D+.p2.z3D)/.2.} + +(* ============================================================================= *) +(* = Barycentre d'une liste de points avec ou sans coefficients = *) +(* ============================================================================= *) +let barycenter lst_pts = + let v = List.fold_left (fun p pt -> vect_add p pt) null_vector lst_pts in + vect_mul_scal v (1.0/.(float_of_int (List.length lst_pts))) + +let weighted_barycenter lst_pts lst_coeffs = + let (v, somme_coeffs) = + List.fold_left2 (fun (p, s) pt c -> (vect_add_mul_scal c p pt, s+.c)) + (null_vector, 0.0) lst_pts lst_coeffs in + vect_mul_scal v (1.0/.somme_coeffs) + +(* ============================================================================= *) +(* = Produit scalaire = *) +(* ============================================================================= *) +let dot_product u v = u.x3D*.v.x3D +. u.y3D*.v.y3D +. u.z3D*.v.z3D + +(* ============================================================================= *) +(* = Produit vectoriel = *) +(* ============================================================================= *) +let cross_product u v = {x3D=u.y3D*.v.z3D -. u.z3D*.v.y3D; + y3D=u.z3D*.v.x3D -. u.x3D*.v.z3D; + z3D=u.x3D*.v.y3D -. u.y3D*.v.x3D} + +(* ============================================================================= *) +(* = Normale unitaire a un deux vecteurs = *) +(* ============================================================================= *) +let normal u v = vect_normalize (cross_product u v) + +(* ============================================================================= *) +(* = Test d'un point sur un segment = *) +(* ============================================================================= *) +let point_on_segment p p1 p2 = + (* P est sur le segment P1 P2 si le produit vectoriel P1P^P1P2 *) + (* est nul (P sur la droite P1 P2) et que le produit scalaire *) + (* P1P.P1P2 est compris entre 0 et la norme de P1P2 au carre *) + let v = cross_product (vect_make p1 p) (vect_make p1 p2) in + let scal = dot_product (vect_make p1 p) (vect_make p1 p2) + and n = distance p1 p2 in + v=null_vector && scal>=0. && scal<=cc n + + +(* ============================================================================= *) +(* = = *) +(* = Intersections = *) +(* = = *) +(* ============================================================================= *) + + +(* ============================================================================= *) +(* = Intersection de deux droites 3D = *) +(* ============================================================================= *) +let crossing_point a u c v = + let w = vect_make a c in + let n = cross_product u v in + let n2 = vect_norm n in let n2 = cc n2 in + + (* Si s<>0 alors les vecteurs ne sont pas coplanaires *) + let s = dot_product n w in + + (* Si n est nul alors les vecteurs sont paralleles *) + if n2 = 0.0 or s <> 0. then None else begin + let r = (dot_product (cross_product w v) n)/.n2 + and s = (dot_product (cross_product w u) n)/.n2 in + let type_intersection_seg1 = + if abs_float r < epsilon then T_ON_PT1 + else if abs_float (r-.1.0) < epsilon then T_ON_PT2 + else if r<0.0 then T_OUT_SEG_PT1 + else if r>1.0 then T_OUT_SEG_PT2 + else T_IN_SEG1 + + and type_intersection_seg2 = + if abs_float s < epsilon then T_ON_PT3 + else if abs_float (s-.1.0) < epsilon then T_ON_PT4 + else if s<0.0 then T_OUT_SEG_PT3 + else if s>1.0 then T_OUT_SEG_PT4 + else T_IN_SEG2 + + and pt_intersection = vect_add_mul_scal r a u in + + Some (type_intersection_seg1, type_intersection_seg2, pt_intersection) + end + +(* ============================================================================= *) +(* = Test du type d'intersection = *) +(* ============================================================================= *) +let test_in_segment t = + (t=T_IN_SEG1)||(t=T_ON_PT1)||(t=T_ON_PT2)|| + (t=T_IN_SEG2)||(t=T_ON_PT3)||(t=T_ON_PT4) +let test_on_hl t = (test_in_segment t)||(t=T_OUT_SEG_PT4)||(t=T_OUT_SEG_PT2) + +(* ============================================================================= *) +(* = Teste l'intersection de deux segments (a,b) et (c,d) = *) +(* ============================================================================= *) +let crossing_seg_seg a b c d = + match crossing_point a (vect_make a b) c (vect_make c d) with + None -> false + | Some (type1, type2, pt) -> (test_in_segment type1)&&(test_in_segment type2) + +(* ============================================================================= *) +(* = Teste l'intersection d'un segment (a,b) et d'une demi-droite (c,v) = *) +(* ============================================================================= *) +let crossing_seg_hl a b c v = + match crossing_point a (vect_make a b) c v with + None -> false + | Some (type1, type2, pt) -> + (* OK si intersection sur la demi-droite *) + (test_in_segment type1) && (test_on_hl type2) + +(* ============================================================================= *) +(* = Teste l'intersection de deux demi-droites = *) +(* ============================================================================= *) +let crossing_hl_hl a u c v = + let inter = crossing_point a u c v in + match inter with + None -> false + | Some (type1, type2, pt) -> (test_on_hl type1) && (test_on_hl type2) + +(* ============================================================================= *) +(* = Teste l'intersection de deux droites et renvoie le point s'il existe = *) +(* ============================================================================= *) +let crossing_lines a u c v = + match crossing_point a u c v with + None -> (false, null_vector) + | Some (type1, type2, pt) -> (true, pt) + +(* ============================================================================= *) +(* = Intersection d'une droite (a, u) et d'un plan (c, d, e) = *) +(* ============================================================================= *) +let crossing_line_plane a u c d e = + let num = eval_det4 + 1. 1. 1. 1. + c.x3D d.x3D e.x3D a.x3D + c.y3D d.y3D e.y3D a.y3D + c.z3D d.z3D e.z3D a.z3D + and denom = eval_det4 + 1. 1. 1. 0. + c.x3D d.x3D e.x3D u.x3D + c.y3D d.y3D e.y3D u.y3D + c.z3D d.z3D e.z3D u.z3D + in + if denom=0. then None + else Some (vect_add_mul_scal (num/.denom) a u) + +(* ============================================================================= *) +(* = Intersection d'une demi-droite (a, u) et d'un plan (c, d, e) = *) +(* ============================================================================= *) +let crossing_hline_plane a u c d e = + let num = eval_det4 + 1. 1. 1. 1. + c.x3D d.x3D e.x3D a.x3D + c.y3D d.y3D e.y3D a.y3D + c.z3D d.z3D e.z3D a.z3D + and denom = eval_det4 + 1. 1. 1. 0. + c.x3D d.x3D e.x3D u.x3D + c.y3D d.y3D e.y3D u.y3D + c.z3D d.z3D e.z3D u.z3D + in + + if denom=0. then None else begin + let s = (-.num)/.denom in + if s >= 0. then Some (vect_add_mul_scal s a u) + else None + end + + +(* ============================================================================= *) +(* = = *) +(* = Polygones = *) +(* = = *) +(* = Ils sont consideres comme etant ouverts et plans = *) +(* = = *) +(* ============================================================================= *) + + +(* ============================================================================= *) +(* = Teste si un polygone est ferme = *) +(* ============================================================================= *) +let poly_is_closed poly = + if poly=[] then false else point_same (List.hd poly) (List.hd (List.rev poly)) + +(* ============================================================================= *) +(* = Ferme un polygone [A; B; C; D] -> [A; B; C; D; A] = *) +(* ============================================================================= *) +let poly_close poly = + if poly = [] or poly_is_closed poly then poly else poly@[List.hd poly] + +(* ============================================================================= *) +(* = Ferme un polygone [A; B; C; D] -> [|A; B; C; D; A; B|] = *) +(* ============================================================================= *) +let poly_close2 poly = + if List.length poly < 2 then Array.of_list poly else begin + let poly = if poly_is_closed poly then poly else poly@[List.hd poly] in + Array.of_list (poly@[List.hd (List.tl poly)]) + end + +(* ============================================================================= *) +(* = Transformation d'un point 3D en 2D en supprimant la coordonnee indiquee = *) +(* ============================================================================= *) +let pt_3d_to_pt_2d axis pt = + match axis with + T_X3D -> {x2D=pt.y3D; y2D=pt.z3D} + | T_Y3D -> {x2D=pt.x3D; y2D=pt.z3D} + | T_Z3D -> {x2D=pt.x3D; y2D=pt.y3D} + +(* ============================================================================= *) +(* = Transformation d'un polygone 3D en polygone 2D en supprimant une coord = *) +(* ============================================================================= *) +let poly_3d_to_poly_2d axis poly = List.map (pt_3d_to_pt_2d axis) poly + +(* ============================================================================= *) +(* = Test d'un point dans un plan defini par 3 points = *) +(* ============================================================================= *) +let point_in_plane pt a b c = + let det = eval_det4 + pt.x3D pt.y3D pt.z3D 1. + a.x3D a.y3D a.z3D 1. + b.x3D b.y3D b.z3D 1. + c.x3D c.y3D c.z3D 1. + in + abs_float det < epsilon + +(* ============================================================================= *) +(* = Determination des etendues du polygone sur chaque axe pour choisir le = *) +(* = de projection -> limite les pbs d'instabilite numerique = *) +(* ============================================================================= *) +let span_poly poly = + let set_min_max valeur min max = + if valeur < !min then min:=valeur else if valeur > !max then max:=valeur + in + + let p = List.hd (poly) in + let minx = ref p.x3D and maxx = ref p.y3D and miny = ref p.y3D + and maxy = ref p.y3D and minz = ref p.z3D and maxz = ref p.z3D in + List.iter (fun p -> + set_min_max p.x3D minx maxx ; + set_min_max p.y3D miny maxy ; + set_min_max p.z3D minz maxz) (List.tl poly) ; + (* Renvoie les etendues sur chaque axe *) + (!maxx-. !minx, !maxy-. !miny, !maxz-. !minz) + +(* ============================================================================= *) +(* = Projection d'un polygone 3D sur le plan ayant les plus grandes etendues = *) +(* ============================================================================= *) +let poly_3d_to_poly_2d_smallest_span poly = + let (dx, dy, dz) = span_poly poly in + let axis = + if dx= 3 then begin + (* Simplification du polygone *) + let (new_poly, axis) = poly_3d_to_poly_2d_smallest_span poly in + (* Simplification du point a tester suivant le meme axe *) + let new_pt = pt_3d_to_pt_2d axis pt in + (* Utilisation de la fonction 2D pour tester l'inclusion *) + Geometry_2d.point_in_poly new_pt new_poly + end else false + +(* ============================================================================= *) +(* = Normale unitaire au plan contenant un polygone = *) +(* ============================================================================= *) +let poly_normal poly = + if List.length poly >= 3 then begin + (* 3 points du polygone qui definissent le plan le contenant *) + let a = List.hd poly and b = List.hd (List.tl poly) + and c = List.hd (List.tl (List.tl poly)) in + (* Normale unitaire au plan contenant le polygone *) + normal (vect_make a b) (vect_make b c) + end else null_vector + +(* ============================================================================= *) +(* = Test d'un point dans un polygone 3D = *) +(* ============================================================================= *) +let point_in_poly pt poly = + if List.length poly >= 3 then begin + (* 3 points du polygone qui definissent le plan le contenant *) + let a = List.hd poly and b = List.hd (List.tl poly) + and c = List.hd (List.tl (List.tl poly)) in + + (* Le point est-il dans le plan contenant le polygone ? *) + if point_in_plane pt a b c then + (* Oui, on teste alors en projetant en 2D *) + point_in_poly_2D pt poly + else false + end else false + +(* ============================================================================= *) +(* = Aire signee d'un polygone 3D = *) +(* ============================================================================= *) +let poly_signed_area poly = + if List.length poly >= 3 then begin + let poly_closed = poly_close2 poly and vect = ref null_vector in + for i = 0 to (List.length poly)-1 do + vect := vect_add !vect (cross_product poly_closed.(i) poly_closed.(i+1)) + done ; + + (dot_product (poly_normal poly) !vect)/.2. + end else 0. + +(* ============================================================================= *) +(* = Aire d'un polygone 3D = *) +(* ============================================================================= *) +let poly_area poly = abs_float (poly_signed_area poly) + +(* ============================================================================= *) +(* = Evaluation du centroide d'un polygone 3D = *) +(* ============================================================================= *) +let poly_centroid poly = + (* On peut trianguler et ponderer le centre de chaque triangle par sa *) + (* surface mais on peut faire plus efficace. Ici, on prend un point du *) + (* polygone (le premier par ex.) et on pondere l'aire (signee) des *) + (* triangles construits a partir de ce point. *) + + (* Centroide d'un triangle *) + let centroid_triangle p1 p2 p3 = + {x3D=(p1.x3D+.p2.x3D+.p3.x3D)/.3.; + y3D=(p1.y3D+.p2.y3D+.p3.y3D)/.3.; + z3D=(p1.z3D+.p2.z3D+.p3.z3D)/.3.} in + + (* Normale au plan contenant le polygone *) + let n = poly_normal poly in + + (* Aire signee d'un triangle, pas besoin de poly_area... *) + let area_triangle p1 p2 p3 = + (dot_product n (cross_product (vect_make p1 p2) (vect_make p1 p3)))/.2. in + + let rec f p0 l centroid = + match l with + p1::p2::reste -> + let new_centroid = vect_add_mul_scal (area_triangle p0 p1 p2) centroid + (centroid_triangle p0 p1 p2) in + f p0 (p2::reste) new_centroid + | _ -> + let area = poly_signed_area poly in + vect_mul_scal centroid (1./.area) + in + + match poly with + [] -> null_vector + | p::[] -> p + | p1::p2::[] -> point_middle p1 p2 + | _ -> f (List.hd poly) (List.tl poly) null_vector + +(* ============================================================================= *) +(* = Teste si un point est contenu dans un volume = *) +(* ============================================================================= *) +let point_in_volume pt vol = + let t = Hashtbl.create 11 in + + (* Ajout des points a une hashtable pour compter les points en double/triple *) + let add_point pt = + try let nb = Hashtbl.find t pt in Hashtbl.replace t pt (nb+1) + with Not_found -> Hashtbl.add t pt 1 + in + + (* Teste si le point d'intersection est sur une des aretes du volume *) + let rec point_on_one_segment pt l = + match l with + poly::reste -> + let rec f l = + match l with + p1::p2::reste -> + if point_on_segment pt p1 p2 then true else f (p2::reste) + | _ -> false + in + if f (poly_close poly) then true else point_on_one_segment pt reste + | [] -> false + in + + (* Test supplementaire pour voir si les points d'intersections se trouvent sur *) + (* un des sommets ou une des aretes *) + let traite_pts_inter is_in = + Hashtbl.iter (fun pt n -> + (* n=2 -> arete, n=3 -> sommet *) + if (n=2 or n=3) && point_on_one_segment pt vol then is_in:=not !is_in) t + in + + let rec find_direction lst_faces = + match lst_faces with + poly::reste -> + (* On essaie avec la direction entre le point et le centroide *) + (* de la face courante *) + let centroid = poly_centroid poly in + let dir = vect_normalize (vect_make pt centroid) in + + (* Normale au plan du polygone pour avoir l'angle *) + let n = poly_normal poly in + + (* Rappel : les deux vecteurs dir et n sont normalises donc pas besoin *) + (* de diviser par le produit des normes pour avoir l'angle *) + let s = dot_product dir n in + + (* On conserve cette direction si l'angle est inferieur a ~85 degres *) + if abs_float s >=0.1 then dir + else find_direction reste + | [] -> {x3D=1.; y3D=0.; z3D=0.} + in + + (* Choix d'une 'bonne' direction *) + let dir = find_direction vol in + + (* On compte le nombre d'intersections entre la demi-droite issue du point *) + (* a tester de vecteur directeur dir avec le volume *) + + let is_in = ref false and list_inter = ref [] in + List.iter (fun poly_face -> + if List.length poly_face>=3 then begin + (* 3 points definissant le plan contenant la face *) + let a = List.hd poly_face and b = List.hd (List.tl poly_face) + and c = List.hd (List.tl (List.tl poly_face)) in + + (* Evaluation du point P' projete de P, suivant dir, sur le plan contenant *) + (* la face poly_face *) + match crossing_hline_plane pt dir a b c with + None -> () (* Pas d'intersection *) + | Some p -> + add_point p ; + + (* Le point projete est-il dans le polygone constituant la face ? *) + if point_in_poly_2D p poly_face then + (* Oui -> une intersection de plus *) + is_in:=not !is_in + end) vol ; + + (* Test supplementaires pour les sommets et les aretes *) + traite_pts_inter is_in ; + + (* Nombre impair d'intersections -> le point est dans le volume *) + !is_in + + +(* ============================================================================= *) +(* = = *) +(* = Triangulation de polygones 3D = *) +(* = par defaut ils sont consideres comme ouverts = *) +(* = = *) +(* ============================================================================= *) + + +(* ============================================================================= *) +(* = Triangulation d'un polygone = *) +(* ============================================================================= *) +let tesselation poly = + (* Projection en 2D pour utiliser la routine de tesselation 2D *) + let (l, _) = poly_3d_to_poly_2d_smallest_span poly in + + (* Tesselation 2D *) + let indices = Geometry_2d.in_tesselation l in + + (* On remet le polygone en 3D en utilisant les indices des points *) + let t = Array.of_list poly in + List.map (fun (p1, p2, p3) -> [t.(p1); t.(p2); t.(p3)]) indices + +(* ============================================================================= *) +(* = Triangulation en triangles_fan = *) +(* ============================================================================= *) +let tesselation_fans poly = + (* Projection en 2D pour utiliser la routine de tesselation 2D *) + let (l, _) = poly_3d_to_poly_2d_smallest_span poly in + + (* Tesselation 2D *) + let (_, indices) = Geometry_2d.in_tesselation_fans l in + + (* On remet le polygone en 3D en utilisant les indices des points *) + let t = Array.of_list poly in + List.map (fun l -> List.map (fun x -> t.(x)) l) indices + +(* =============================== FIN ========================================= *) diff --git a/sw/lib/ocaml/geometry_3d.mli b/sw/lib/ocaml/geometry_3d.mli new file mode 100644 index 00000000000..6e712eff4e7 --- /dev/null +++ b/sw/lib/ocaml/geometry_3d.mli @@ -0,0 +1,234 @@ +(* + * $Id$ + * + * 3D Geometry + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(** Module de géométrie 3D + + Par défaut, les polygones sont considérés comme étant ouverts + + {b Dépendences : Geometry_2d} + + {e Yann Le Fablec, version 1.0, 22/04/2003} + *) + +(** {6 Types} *) + +(** Types pour les différents axes *) +type t_axis3d = T_X3D | T_Y3D | T_Z3D + +(** Types pour les intersections *) +type t_crossing3d = T_IN_SEG1 | T_IN_SEG2 | T_ON_PT1 | T_ON_PT2 | T_ON_PT3 +| T_ON_PT4 | T_OUT_SEG_PT1 | T_OUT_SEG_PT2 | T_OUT_SEG_PT3 | T_OUT_SEG_PT4 + +(** Type point/vecteur 3D *) +type pt_3D = { x3D : float; y3D : float; z3D : float; } + +(** Vecteur nul en 3D *) +val null_vector : pt_3D + +(** Un polygone 3D *) +type poly_3D = pt_3D list + +(** Un volume 3D *) +type volume_3D = poly_3D list + +(** {6 Points} *) + +(** [point_same A B] teste l'égalité stricte des deux points *) +val point_same : pt_3D -> pt_3D -> bool + +(** [distance A B] évalue la distance entre les points [A] et [B] *) +val distance : pt_3D -> pt_3D -> float + +(** [point_middle A B] renvoie le milieu du segment formé par [A] et [B] *) +val point_middle : pt_3D -> pt_3D -> pt_3D + +(** [barycenter lst_pts] renvoie le barycentre des points *) +val barycenter : pt_3D list -> pt_3D + +(** [weighted_barycenter lst_pts lst_poids] renvoie le barycentre des points + pondérés par [lst_poids] *) +val weighted_barycenter : pt_3D list -> float list -> pt_3D + +(** [pt_3d_to_pt_2d axis pt] transforme le point 3D en point 2D en supprimant + la coordonnée indiquée *) +val pt_3d_to_pt_2d : t_axis3d -> pt_3D -> Geometry_2d.pt_2D + +(** [point_in_plane P A B C] indique si le point [P] est dans le plan défini + par les trois points [A], [B] et [C] *) +val point_in_plane : pt_3D -> pt_3D -> pt_3D -> pt_3D -> bool + +(** [point_on_segment P A B] indique si le point P se trouve sur le segment [\[A, B\]] *) +val point_on_segment : pt_3D -> pt_3D -> pt_3D -> bool + +(** {6 Vecteurs} *) + + +(** [vect_make A B] crée le vecteur AB *) +val vect_make : pt_3D -> pt_3D -> pt_3D + +(** [vect_norm v] renvoie la norme du vecteur *) +val vect_norm : pt_3D -> float + +(** [vect_normalize v] normalise le vecteur *) +val vect_normalize : pt_3D -> pt_3D + +(** [vect_set_norm v norme] change [v] pour que sa norme soit [norme] *) +val vect_set_norm : pt_3D -> float -> pt_3D + +(** [vect_add u v] réalise la somme des deux vecteurs*) +val vect_add : pt_3D -> pt_3D -> pt_3D + +(** [vect_sub u v] renvoie la soustraction de [v] à [u] *) +val vect_sub : pt_3D -> pt_3D -> pt_3D + +(** [vect_mul_scal v scalaire] multiple le vecteur par un scalaire *) +val vect_mul_scal : pt_3D -> float -> pt_3D + +(** [vect_add_mul_scal lambda p v] renvoie le point [p] translaté du + vecteur [lambda.v] *) +val vect_add_mul_scal : float -> pt_3D -> pt_3D -> pt_3D + +(** [vect_inverse v] renvoie le vecteur opposé à [v] *) +val vect_inverse : pt_3D -> pt_3D + +(** [normal u v] renvoie la normale unitaire du plan défini par + les vecteurs [u] et [v] *) +val normal : pt_3D -> pt_3D -> pt_3D + + +(** {6 Produits} *) + +(** [dot_product u v] fournit le produit scalaire des deux vecteurs *) +val dot_product : pt_3D -> pt_3D -> float + +(** [cross_product u v] renvoie le produit vectoriel de [u] et [v] *) +val cross_product : pt_3D -> pt_3D -> pt_3D + + +(** {6 Intersections} *) + + +(** [test_in_segment type_intersection] teste si l'intersection est dans le + segment (extrémités incluses) *) +val test_in_segment : t_crossing3d -> bool + +(** [test_on_hl type_intersection] teste si l'intersection est sur la demi-droite + (extrémité incluse) *) +val test_on_hl : t_crossing3d -> bool + +(** [crossing_point A u B v] teste l'intersection des droites [(A, u)] et [(B, v)] + + En sortie, deux possibilités : + - [None] s'il n'y a pas d'intersection + - [Some (type1, type2, point_intersection)] sinon. [type1] désigne le type + d'intersection sur la première droite et [type2] la meme information pour + la seconde droite. + *) +val crossing_point : pt_3D -> pt_3D -> pt_3D -> pt_3D -> + (t_crossing3d * t_crossing3d * pt_3D) option + +(** [crossing_seg_seg A B C D] teste l'intersection des segments + [\[A,B\]] et [\[C,D\]] *) +val crossing_seg_seg : pt_3D -> pt_3D -> pt_3D -> pt_3D -> bool + +(** [crossing_seg_hl A B C u] teste l'intersection entre le segment [\[A,B\]] et + la droite passant par [C] et de vecteur directeur [u] *) +val crossing_seg_hl : + pt_3D -> pt_3D -> pt_3D -> pt_3D -> bool + +(** [crossing_hl_hl A u B v] teste l'intersection entre les deux demi-droites *) +val crossing_hl_hl : pt_3D -> pt_3D -> pt_3D -> pt_3D -> bool + +(** [crossing_lines A u B v] teste l'intersection entre les deux droites et renvoie le + point s'il y a effectivement intersection *) +val crossing_lines : pt_3D -> pt_3D -> pt_3D -> pt_3D -> bool * pt_3D + +(** [crossing_line_plane A u C D E] teste l'intersection de la droite passant par [A] + et de vecteur directeur [u] avec le plan défini par les trois points [C], [D] et [E] *) +val crossing_line_plane : pt_3D -> pt_3D -> pt_3D -> pt_3D -> pt_3D -> pt_3D option + +(** [crossing_hline_plane A u C D E] teste l'intersection de la demi-droite issue de [A] + et de vecteur directeur [u] avec le plan défini par les trois points [C], [D] et [E] *) +val crossing_hline_plane : pt_3D -> pt_3D -> pt_3D -> pt_3D -> pt_3D -> pt_3D option + + +(** {6 Polygones} *) + +(** [poly_3d_to_poly_2d axis poly] projete le polygone 3D en 2D en supprimant les + coordonnées de l'axe indiqué *) +val poly_3d_to_poly_2d : t_axis3d -> poly_3D -> Geometry_2d.poly_2D + +(** [poly_3d_to_poly_2d_smallest_span poly] idem en choisissant l'axe le plus approprié + numériquement (i.e celui d'étendue la plus faible) *) +val poly_3d_to_poly_2d_smallest_span : poly_3D -> Geometry_2d.poly_2D*t_axis3d + +(** [span_poly poly] renvoie, pour chaque axe, l'étendue du polygone *) +val span_poly : poly_3D -> float*float*float + +(** [point_in_poly pt poly] teste si le point est dans le polygone 3D *) +val point_in_poly : pt_3D -> poly_3D -> bool + +(** [point_in_poly_2D pt poly] teste si le point est dans le polygone quand les deux + sont projetés en 2D (en supprimant la coordonnée la plus appropriée) *) +val point_in_poly_2D : pt_3D -> poly_3D -> bool + +(** [poly_area poly] renvoie l'aire du polygone *) +val poly_area : poly_3D -> float + +(** [poly_signed_area poly] renvoie l'aire signée du polygone *) +val poly_signed_area : poly_3D -> float + +(** [poly_centroid poly] renvoie le centroide du polygone *) +val poly_centroid : poly_3D -> pt_3D + +(** [poly_normal poly] renvoie la normale unitaire au plan contenant le polygone *) +val poly_normal : poly_3D -> pt_3D + + +(** {6 Triangulation de polygones} *) + + +(** [geom_tesselation polygone] effectue la triangulation du polygone et renvoie + la liste des triangles résultants. Ici un triangle est une liste de 3 points (!). + De plus, le polygone ne doit contenir aucun point en double. + *) +val tesselation : poly_3D -> poly_3D list + +(** [geom_tesselation_fans polygone] effectue la triangulation du polygone en + triangle_fan OpenGL. En sortie est renvoyée une liste contenant des listes + de points. Chacune de ces listes de points contient soit 3 points (triangle) + soit plus de 3 points (pour un triangle_fan). + De plus, le polygone ne doit contenir aucun point en double. + *) +val tesselation_fans : poly_3D -> poly_3D list + + +(** {6 Volumes} *) + + +(** [point_in_volume P volume] teste si le point [P] se trouve dans le volume + (ce dernier peut ne pas etre convexe) *) +val point_in_volume : pt_3D -> volume_3D -> bool diff --git a/sw/lib/ocaml/gtk_3d.ml b/sw/lib/ocaml/gtk_3d.ml new file mode 100644 index 00000000000..a65dc5324bd --- /dev/null +++ b/sw/lib/ocaml/gtk_3d.ml @@ -0,0 +1,1443 @@ +(* + * $Id$ + * + * 3D display widget + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(* Scale + lighting = pb si pas de GlNormalize *) +(* Ne pas oublier de faire un area#make_current () avant d'appeler des *) +(* commandes OpenGL car s'il y a plusieurs fenetres ouvertes, il arrive *) +(* que les commandes ne soient pas envoyees dans la bonne... *) + +(* Modules gtk/Gdk *) +open GMain +open GdkKeysyms +open GlGtk +open Gtk_tools_GL +open Platform +open Gtkgl_Hack + +(* Modules locaux *) +open Geometry_2d +open Geometry_3d + +(* Nombre de widgets 3D crees *) +let nb_objects = ref 0 + +(* Temps en millisecondes pour l'animation *) +let tps_anim = 40 + +(* Exception levee lors de la recherche d'un objet d'identifiant inconnu *) +exception NO_SUCH_3D_OBJECT of int +(* Exception levee lorsqu'un autre objet est passe alors qu'un contour + pays est attendu *) +exception NOT_A_3D_OUTLINE of int +(* Idem avec une ligne *) +exception NOT_A_3D_LINE of int +(* Idem avec un point *) +exception NOT_A_3D_POINT of int + +(* Debugging *) +let debug_3d = false +(* Les fontes ne sont disponibles que sous Linux... *) +let fonts_available = not platform_is_win32 + +(* Une couleur OpenGL *) +type glcolor = float*float*float + +let glcolor_white = (1., 1., 1.) +let glcolor_black = (0., 0., 0.) +let color_rosace = (1., 0., 0.) + +(* Point 2D *) +type glpoint2d = float*float + +(* Point 3D *) +type glpoint3d = float*float*float + +(* Point ou vecteur 3D nul *) +let pt_null = (0., 0., 0.) + +(* Point ou vecteur 3D indefini *) +let pt_undefined = (9999.99, 9999.99, 9999.99) + +(* Objet non compile *) +let not_compiled_obj = Obj.magic ~-1 + +(* Type pour designer les axes *) +type t_coord = X_AXIS | Y_AXIS | Z_AXIS + +(* Type des action possibles a la souris dans un widget 3D *) +type t_action = ACTION_NONE | ACTION_ZOOM of (int*int) | ACTION_ROTATE of (int*int) + +(* Definition des curseurs utilises *) +let cursor_standard = Gdk.Cursor.create `LEFT_PTR +let cursor_zoom_up = Gdk.Cursor.create `BASED_ARROW_UP +let cursor_zoom_down = Gdk.Cursor.create `BASED_ARROW_DOWN +let cursor_wait = Gdk.Cursor.create `WATCH +let cursor_rotate = Gdk.Cursor.create `EXCHANGE + +(* Valeurs OpenGL pour utiliser une source de lumiere *) +let lights = [`lighting; `light0; `color_material] + +(* Types de fleche *) +type t_arrow = ARROW1 | ARROW2 + +(* Type de polygone *) +type t_triangulation = + NO_TRI of glpoint3d list (* polygone convexe non triangule *) + | TRI_WITH_FANS of (glpoint3d list) list (* triangule avec des triangles fans *) + | TRI_STD of glpoint3d list (* triangule avec des triangles simples *) + +(* Volume 3D *) +type vol3d = { + vol3d_contour : glpoint3d list ; (* Faces verticales (quad_strip) *) + vol3d_up : t_triangulation ; (* Face horizontale superieure *) + vol3d_down : t_triangulation ; (* Face horizontale inferieure *) + mutable vol3d_color : glcolor ; (* Couleur du volume *) + mutable vol3d_filled : bool (* Volume plein ou fil de fer *) + } + + +(* Volume d'une enveloppe 3D *) +type env3d ={ + env3d_contour : glpoint3d list ; (* Faces laterales (quad_strip) *) + mutable env3d_color : glcolor ; (* Couleur du volume *) + mutable env3d_filled : bool (* Volume plein ou fil de fer *) + } + +(* Volume d'une enveloppe 3D *) +type env3d_double ={ + env3d_double_contour_out : glpoint3d list ; (* Faces laterales externes (quad_strip) *) + env3d_double_contour_in : glpoint3d list ; (* Faces laterales internes (quad_strip) *) + mutable env3d_double_color_out : glcolor ; (* Couleur des faces externes *) + mutable env3d_double_color_in : glcolor ; (* Couleur des faces internes *) + mutable env3d_double_filled : bool (* Volume plein ou fil de fer *) + } + + +(* Contour 3D *) +type out3d = { + out3d_contour : glpoint3d list ; (* Liste des points du contour *) + mutable out3d_color_in : glcolor ; (* Couleur interieure *) + mutable out3d_color_out : glcolor ; (* Couleur du contour *) + mutable out3d_filled : bool (* Contour plein ou fil de fer *) + } + +(* Ligne 3D *) +type line3d = { + line3d_points : glpoint3d list ; (* Points de la ligne *) + mutable line3d_width : int ; (* Epaisseur *) + mutable line3d_color : glcolor ; (* Couleur *) + mutable line3d_with_bars : bool ; (* Barres verticales *) + mutable line3d_filled : bool (* Surface jusqu'au sol *) + } + +(* Fleche 3D *) +type arr3d = { + arr3d_contour : (glpoint3d list) list ; (* Faces verticales (quad_strip) *) + arr3d_pt : glpoint3d ; (* Deplacement et rotation de la fleche car *) + arr3d_vect : glpoint3d ; (* elle est creee le long de l'axe X. Il faut*) + arr3d_angle_xy : float ; (* donc la remettre dans la bonne direction *) + arr3d_angle_z : float ; + mutable arr3d_color : glcolor ; (* Couleur de la fleche *) + mutable arr3d_filled : bool (* Fleche pleine ? *) + } + +(* Point 3D *) +type point3d ={ + p3d_pos : glpoint3d ; (* Position 3D du point *) + p3d_pos2 : glpoint3d ; (* Position du nom du point *) + p3d_name : string ; (* Nom du point *) + mutable p3d_with_name : bool ; (* Affichage du nom *) + mutable p3d_color : glcolor (* Couleur du point *) + } + +(* Surface triangulee en 3D *) +type surf3d = { + s3d_pts : (glpoint3d*glcolor) array array ; + mutable s3d_filled : bool + } + +(* Surface triangulee en 3D avec une texture *) +type surf3d_tex = { + s3d_tex_pts : glpoint3d array array ; (* Tableau des points de la surface *) + s3d_tex_texture_id : GlTex.texture_id (* Id de la texture a appliquer *) + } + +(* Type contenant tous les objets 3D possibles *) +type tobj3d = + VOLUME1_3D of vol3d | OUTLINE_3D of out3d + | LINE_3D of line3d | ARROW_3D of arr3d + | POINT_3D of point3d | ENVELOPPE_3D_DOUBLE of env3d_double + | ENVELOPPE_3D of env3d | SURFACE_3D of surf3d + | SURFACE_3D_TEX of surf3d_tex + +(* Stockage d'un objet 3D *) +type obj3d = {o_obj : tobj3d ; (* L'objet *) + o_id : int ; (* Son identifiant unique *) + mutable o_compiled : GlList.t ; (* L'objet compile *) + mutable o_show : bool (* Objet affiche ou pas *) + } + +(* module OImages = OImage *) +(* module Images = Image *) + +(* ============================================================================= *) +(* = Creation d'une texture a partir d'une image = *) +(* ============================================================================= *) +let make_image filename = + let img = + match OImages.tag (OImages.load filename []) with + OImages.Rgb24 rgb24 -> + rgb24 + | OImages.Index8 img | OImages.Index16 img -> + let rgb = img#to_rgb24 in + img#destroy; + rgb + | _ -> failwith "Gtk_3d.make_image" in + let w = img#width and h = img#height in + let image = GlPix.create `ubyte ~format:`rgb ~width:w ~height:h in + for i = 0 to h - 1 do + for j = 0 to w - 1 do + let pixel = img#get j i in (* pixel is a Color.rgb *) + let red = pixel.Images.r in + let green = pixel.Images.g in + let blue = pixel.Images.b in + Raw.sets (GlPix.to_raw image) ~pos:(3*(i*w+j)) + [| red; green; blue |] + done + done; + image + +let create_texture_from_image texture_filename = + let texture = make_image texture_filename in + let id = GlTex.gen_texture () in + GlTex.bind_texture `texture_2d id; + GluMisc.build_2d_mipmaps texture; + id + +(* ============================================================================= *) +(* = Infos OpenGL = *) +(* ============================================================================= *) +(* renvoie les informations relatives a la version d'OpenGL utilisee *) +let get_gl_infos () = + let l = [("Vendor", `vendor); ("Renderer", `renderer); + ("Version", `version); ("Extensions", `extensions)] in + let s = ref "" in + List.iter (fun (str, t) -> s:=!s^str^" : "^(GlMisc.get_string t)^"\n") l ; + !s + +(* ============================================================================= *) +(* = Affichages si mode debug = *) +(* ============================================================================= *) +(* [do_msg msg] affiche le message [msg] si {!Gtk_3d.debug_3d} est vrai *) +let do_msg msg = if debug_3d then begin Printf.printf "%s\n" msg;flush stdout end + +(* ============================================================================= *) +(* = Angle modulo 360 = *) +(* ============================================================================= *) +(* [mod_360 angle] angle modulo 360 degres *) +let mod_360 angle = mod_float angle 360. + +(* ============================================================================= *) +(* = Manipulations de coordonnees = *) +(* ============================================================================= *) +(* [get_coord (x, y, z) axis] renvoie la composante [x], [y] ou [z] suivant + l'axe indique *) +let get_coord (x, y, z) axis = match axis with X_AXIS->x | Y_AXIS->y | Z_AXIS->z + +(* [add_coord (x, y, z) axis delta] ajoute [delta] a la coordonnee definie + par [axis] *) +let add_coord (x, y, z) axis d = + match axis with + X_AXIS -> (x+.d, y, z) | Y_AXIS -> (x, y+.d, z) | Z_AXIS -> (x, y, z+.d) + +(* [add_coord_360 (x, y, z) axis delta] meme chose modulo 360 *) +let add_coord_360 (x, y, z) axis d = + match axis with + X_AXIS -> (mod_360 (x+.d), y, z) + | Y_AXIS -> (x, mod_360 (y+.d), z) + | Z_AXIS -> (x, y, mod_360 (z+.d)) + +(* ============================================================================= *) +(* = Encapsulation de fonctions 3D = *) +(* ============================================================================= *) +let glpoint3d_of_pt_3d u = (u.x3D, u.y3D, u.z3D) +let glpoint3d_to_pt_3d (x, y, z) = {x3D=x; y3D=y; z3D=z} +let glpoint3d_of_pt_2d z u = (u.x2D, u.y2D, z) +let glpoint3d_of_pt_3d_lst l = List.map glpoint3d_of_pt_3d l + +(* ============================================================================= *) +(* = Normale unitaire a un triplet de points = *) +(* ============================================================================= *) +(* [geom_normal A B C] renvoie la normale unitaire a un triplet de point *) +let geom_normal p1 p2 p3 = + let p1 = glpoint3d_to_pt_3d p1 and p2 = glpoint3d_to_pt_3d p2 + and p3 = glpoint3d_to_pt_3d p3 in + let n = Geometry_3d.normal (Geometry_3d.vect_make p1 p2) + (Geometry_3d.vect_make p1 p3) in + glpoint3d_of_pt_3d n + +let geom_scal_mult n (x, y, z) = (x*.n, y*.n, z*.n) + +(* Fermeture d'un polygone *) +let geom_close_poly l = if l=[] then [] else l@[List.hd l] + + +(* ============================================================================= *) +(* = Encapsulation de fonctions OpenGL = *) +(* ============================================================================= *) +(* polygones remplis *) +let set_gl_fillpoly () = GlDraw.polygon_mode ~face:`both `fill +(* polygones vides (contour uniquement) *) +let unset_gl_fillpoly () = GlDraw.polygon_mode ~face:`both `line + +(* [set_color color] met a jour de la couleur de dessin/remplissage *) +let set_color color = GlDraw.color color +(* [set_faded_color color pct] applique la couleur plus sombre de [pct]% *) +let set_faded_color (r, g, b) pct = + let p = (float_of_int pct)/.100. in GlDraw.color (r*.p, g*.p, b*.p) + +(* [set_3d_points type_objet_opengl lst_pts] creation d'une liste de points 3D *) +let set_3d_points typ l = + GlDraw.begins typ ; List.iter GlDraw.vertex3 l ; GlDraw.ends () + +let set_3d_points_with_color typ l = + GlDraw.begins typ ; + List.iter (fun ((p, c), n) -> + set_color c; GlDraw.normal3 n; GlDraw.vertex3 p) l ; + GlDraw.ends () + +let set_3d_points_with_texture typ l (dx, dy) (x1, y1) = + GlDraw.begins typ ; + List.iter (fun ((x,y,z), n) -> + GlDraw.normal3 n; GlTex.coord2 ((x-.x1)/.dx, (y-.y1)/.dy) ; + GlDraw.vertex3 (x,y,z)) l ; + GlDraw.ends () + +(* [set_3d_points_quad_strip_with_normal lst_pts] creation d'une liste de + points 3D [quad_strip] avec les normales *) +let set_3d_points_quad_strip_with_normal l = + let t = Array.of_list l in + GlDraw.begins `quad_strip ; + Array.iteri (fun i pt -> + (* Tous les 2 points -> normale au quad_strip courant *) + if i mod 2=0 then + if i=0 or i=List.length l-2 then GlDraw.normal3 (geom_normal pt t.(1) t.(2)) + else GlDraw.normal3 (geom_normal pt t.(i+1) t.(i+2)) ; + GlDraw.vertex3 pt) t ; + GlDraw.ends () + +(* normale vers le haut *) +let normal_up () = GlDraw.normal3 (0., 0., 1.) +(* normale vers le bas *) +let normal_down () = GlDraw.normal3 (0., 0., -.1.) + +(* [rotate axis angle] effectue une rotation suivant un axe donne *) +let rotate axis angle = + match axis with + X_AXIS -> GlMat.rotate ~angle ~x:1.0 () + | Y_AXIS -> GlMat.rotate ~angle ~y:1.0 () + | Z_AXIS -> GlMat.rotate ~angle ~z:1.0 () + +(* [rotate_some angles axis_list] rotation suivant les axes + indiques par [axis_list]. + La liste [angles] donne la valeur pour chaque axe concerne *) +let rotate_some angles axis_list = + List.iter (fun axis -> rotate axis (get_coord angles axis)) axis_list +(* [rotate_all angles] rotation suivant tous les axes *) +let rotate_all angles = rotate_some angles [X_AXIS; Y_AXIS; Z_AXIS] + + +(* ============================================================================= *) +(* = Calcul des normales pour les surfaces 3D avec ou sans texture = *) +(* ============================================================================= *) +let get_surface_normals tt = + let geom_normal_in p1 p2 p3 = + let p1 = glpoint3d_to_pt_3d p1 and p2 = glpoint3d_to_pt_3d p2 + and p3 = glpoint3d_to_pt_3d p3 in + Geometry_3d.normal (Geometry_3d.vect_make p1 p2) (Geometry_3d.vect_make p1 p3) + in + + Array.mapi (fun i t0 -> + Array.mapi (fun j p -> + if i>0 && i < Array.length tt-1 then begin + if j>0 && j < Array.length t0-1 then begin + let n1 = geom_normal_in p tt.(i-1).(j) tt.(i).(j-1) + and n2 = geom_normal_in p tt.(i-1).(j+1) tt.(i-1).(j) + and n3 = geom_normal_in p tt.(i).(j+1) tt.(i-1).(j+1) + and n4 = geom_normal_in p tt.(i+1).(j) tt.(i).(j+1) + and n5 = geom_normal_in p tt.(i+1).(j-1) tt.(i+1).(j) + and n6 = geom_normal_in p tt.(i).(j-1) tt.(i+1).(j-1) in + let n = Geometry_3d.vect_add n1 n2 in + let n = Geometry_3d.vect_add n n3 in + let n = Geometry_3d.vect_add n n4 in + let n = Geometry_3d.vect_add n n5 in + let n = Geometry_3d.vect_add n n6 in + let n = Geometry_3d.vect_normalize n in + glpoint3d_of_pt_3d n + end else (0., 0., 1.) + end else (0., 0., 1.)) t0) tt + +(* ============================================================================= *) +(* = Manipulation des objets = *) +(* ============================================================================= *) +(* [get_object_color objet] renvoie la couleur de l'objet *) +let get_object_color obj = + match obj with + OUTLINE_3D o -> o.out3d_color_out + | LINE_3D l -> l.line3d_color + | VOLUME1_3D v -> v.vol3d_color + | ENVELOPPE_3D e -> e.env3d_color + | ENVELOPPE_3D_DOUBLE e -> e.env3d_double_color_out + | ARROW_3D a -> a.arr3d_color + | POINT_3D p -> p.p3d_color + | SURFACE_3D s -> glcolor_white + | SURFACE_3D_TEX s -> glcolor_white + +(* [set_object_color objet color] met a jour la couleur de l'objet *) +let set_object_color obj color = + match obj with + OUTLINE_3D o -> o.out3d_color_out <- color + | LINE_3D l -> l.line3d_color <- color + | VOLUME1_3D v -> v.vol3d_color <- color + | ENVELOPPE_3D e -> e.env3d_color <- color + | ENVELOPPE_3D_DOUBLE e -> e.env3d_double_color_out<- color + | ARROW_3D a -> a.arr3d_color <- color + | POINT_3D p -> p.p3d_color <- color + | _ -> () + +(* [get_outline_in_color objet id] renvoie la couleur de remplissage d'un objet + de type [OUTLINE_3D]. Si l'objet n'est pas de ce type, l'exception + {!Gtk_3d.NOT_A_3D_OUTLINE} est levee *) +let get_outline_in_color obj id = + match obj with OUTLINE_3D o -> o.out3d_color_in + | _ -> raise (NOT_A_3D_OUTLINE id) +(* [set_outline_in_color objet color id] met a jour la couleur de remplissage pour + un objet de type [OUTLINE_3D]. Si l'objet n'est pas de ce type, l'exception + {!Gtk_3d.NOT_A_3D_OUTLINE} est levee *) +let set_outline_in_color obj color id = + match obj with OUTLINE_3D o -> o.out3d_color_in <- color + | _ -> raise (NOT_A_3D_OUTLINE id) + +(* [get_object_fill objet] indique si l'objet est rempli ou en fil de fer *) +let get_object_fill obj = + match obj with + OUTLINE_3D o -> o.out3d_filled + | LINE_3D l -> l.line3d_filled + | VOLUME1_3D v -> v.vol3d_filled + | ENVELOPPE_3D e -> e.env3d_filled + | ENVELOPPE_3D_DOUBLE e -> e.env3d_double_filled + | ARROW_3D a -> a.arr3d_filled + | POINT_3D p -> false + | SURFACE_3D s -> s.s3d_filled + | SURFACE_3D_TEX s -> true + +(* [set_object_filled objet filled] force l'objet en mode plein ou fil de fer *) +let set_object_fill obj filled = + match obj with + OUTLINE_3D o -> o.out3d_filled <- filled + | LINE_3D l -> l.line3d_filled <- filled + | VOLUME1_3D v -> v.vol3d_filled <- filled + | ENVELOPPE_3D e -> e.env3d_filled <- filled + | ENVELOPPE_3D_DOUBLE e -> e.env3d_double_filled <- filled + | ARROW_3D a -> a.arr3d_filled <- filled + | POINT_3D p -> () + | SURFACE_3D s -> s.s3d_filled <- filled + | SURFACE_3D_TEX s -> () + +(* [get_line_width objet id] renvoie l'epaisseur d'un objet ligne. Si l'objet + passe n'est pas du type [LINE_3D] alors l'exception {!Gtk_3d.NOT_A_3D_LINE} + est levee *) +let get_line_width obj id = + match obj with LINE_3D l -> l.line3d_width | _ -> raise (NOT_A_3D_LINE id) +(* [set_line_width objet width id] met a jour l'epaisseur d'un objet ligne. Si l'objet + passe n'est pas du type [LINE_3D] alors l'exception {!Gtk_3d.NOT_A_3D_LINE} + est levee *) +let set_line_width obj width id = + match obj with LINE_3D l -> l.line3d_width<-width|_ -> raise (NOT_A_3D_LINE id) +(* [get_line_bars objet id] indique si l'objet est affiche avec des barres + verticales si cet objet est du type [LINE_3D]. Si ca n'est pas le cas, + l'exception {!Gtk_3d.NOT_A_3D_LINE} est levee *) +let get_line_bars obj id = + match obj with LINE_3D l -> l.line3d_with_bars | _ -> raise (NOT_A_3D_LINE id) +(* [set_line_bars objet bars id] met a jour l'affichage des barres + verticales d'un objet de type [LINE_3D]. S'il n'est pas de ce type alors + l'exception {!Gtk_3d.NOT_A_3D_LINE} est levee *) +let set_line_bars obj bars id = + match obj with LINE_3D l ->l.line3d_with_bars<-bars|_ ->raise (NOT_A_3D_LINE id) +(* [get_point_name objet id] indique si le nom de l'objet [POINT_3D] est + affiche. Si l'objet passe n'est pas du type [POINT_3D], + l'exception {!Gtk_3d.NOT_A_3D_POINT} est levee*) +let get_point_name obj id = + match obj with POINT_3D p -> p.p3d_with_name | _ -> raise (NOT_A_3D_POINT id) +(* [set_point_name objet name id] met a jour l'affichage ou pas du nom. + Si l'objet passe n'est pas du type [POINT_3D], + l'exception {!Gtk_3d.NOT_A_3D_POINT} est levee*) +let set_point_name obj name id = + match obj with POINT_3D p ->p.p3d_with_name <-name|_ ->raise (NOT_A_3D_POINT id) + + + +(* ============================================================================= *) +(* = Objet d'affichage 3D = *) +(* = pack = ou mettre le widget = *) +(* = with_status_bar = creation d'une barre d'infos optionnelle = *) +(* = n = nom de la fenetre = *) +(* ============================================================================= *) +(* [widget_3d pack with_status_bar name] cree un widget d'affichage 3D + + - [pack] indique où mettre le widget + - [with_status_bar] permet la creation d'une barre d'infos optionnelle + - [name] designe le nom a donner a la zone d'affichage (eventuellement affichee + en haut a gauche de la zone) + *) +class widget_3d pack with_status_bar n = + (* Creation de la GtkGlArea avec ou sans barre d'infos *) + let (area, setstatus) = + if with_status_bar then begin + let v = Gtk_tools.create_vbox pack in + let a = GlGtk.area [`RGBA; `DOUBLEBUFFER; `DEPTH_SIZE 1] ~packing:v#add () in + let s = GMisc.statusbar ~packing:v#pack () in + let ss = s#new_context ~name:"status" in + (a, fun msg -> ignore(ss#push msg)) + end else + (GlGtk.area [`RGBA; `DOUBLEBUFFER; `DEPTH_SIZE 1] ~packing:pack (), + fun msg -> ()) + in + + object (self) + + (* Numero du widget 3D *) + val nb = !nb_objects + + (* Nom de la fenetre eventuellement affiche en haut a gauche *) + val mutable name = n + (* Indique si le nom de la fenetre doit etre affiche *) + val mutable show_name = true + + (* Contient la largeur de la fenetre apres redimensionnement *) + val mutable width = -1 + (* Contient la hauteur de la fenetre apres redimensionnement *) + val mutable height = -1 + (* Position utilisateur *) + val mutable depl = (0., 0., -.1.5) + (* Rotation utilisateur *) + val mutable rot = (290., 0., 0.) + (* Position de la source de lumiere *) + val mutable lightpos = (-.0.3, -.0.3, 0.6) + (* Rotation de la source de lumiere *) + val mutable lightrot = pt_null + + (* Couleur du fond *) + val mutable back_color = glcolor_black + + (* Point central de la scene *) + val mutable extents = pt_null + (* Rayon de la scene *) + val mutable rs = 1. + (* Extremes minis *) + val mutable extreme_min = pt_undefined + (* Extremes maxis *) + val mutable extreme_max = pt_undefined + + (* indique si l'initialisation a ete faite *) + val mutable done_init = false + (* Action souris en cours s'il y en a une *) + val mutable current_action = ACTION_NONE + (* indique si les lumieres sont utilisees *) + val mutable use_lights = true + (* indique si la source de lumiere est affichee *) + val mutable show_light = false + (* smoothing ? *) + val mutable use_smooth = true + + (* repertoire pour les captures ecran *) + val mutable screenshot_path = "Captures/" + (* nom par defaut de la capture *) + val mutable screenshot_name = "capture3d.png" + (* format par defaut des captures *) + val mutable screenshot_format = Gtk_image.PNG + + (* [triangle_fan] ou [triangle] pour afficher les surfaces triangulees *) + val mutable use_fans_for_tesselation = true + + (* increment d'eloignement lie aux touches *) + val dist_incr = -.0.1 + (* increment de rotation lie aux touches *) + val rot_incr = 2. + + (* Rotation pendant l'animation *) + val rot_anim = 0.1 + + (* Compteur des objets pour les identifiants uniques *) + val mutable cpt_obj = 0 + (* Liste des objets 3D definis dans le widget 3D *) + val mutable objects = ([]:obj3d list) + + (* indique si la rosace doit etre affichee *) + val mutable show_rosace = true + (* Objet rosace *) + val mutable rosace = None + + (* Fonte OpenGL si disponible (i.e sous Unix) *) + val mutable fontbase = Obj.magic ~-1 + + (* Timer utilise pour l'animation *) + val mutable animation_timer = None + + (* cree un nouvel identifiant *) + method private get_new_id = let n = cpt_obj in cpt_obj<-cpt_obj+1; n + + (* Indique a OpenGL que la fenetre est la fenetre courante + dans laquelle doivent etre effectuees les commandes OpenGL + A faire absolument avant d'ajouter des objets pour que la + bonne fenetre recoive les commandes OpenGL qui suivent... *) + method private make_current = area#make_current () + + (* Mise a jour de la barre d'infos *) + method private set_status = + let msg = ref "" in + let add txt = msg:=if !msg="" then txt else !msg^" "^txt in + let (x, y, z) = rot in + add (Printf.sprintf "X=%.0f Y=%.0f Z=%.0f" x y z) ; + let (_, _, z) = depl in + add (Printf.sprintf "Dist=%.1f" (-.z)) ; + add (if use_lights then "Lights on " else "Lights off") ; + add (if use_smooth then "Smooth on " else "Smooth off") ; + setstatus !msg + + (* force l'utilisation de la lumiere *) + method lights_on = use_lights <- true; self#update_lights + (* annule l'utilisation de la lumiere *) + method lights_off = use_lights <- false; self#update_lights + (* change l'etat d'utilisation de la lumiere *) + method lights_switch = use_lights <- not use_lights; self#update_lights + (* met a jour le widget pour utiliser ou pas les lumieres suivant + la valeur de [use_lights] *) + method private update_lights = + do_msg (if use_lights then "Lights on" else "Lights off") ; + List.iter (if use_lights then Gl.enable else Gl.disable) lights ; + self#setup; self#display_func + + (* force l'utilisation du lissage *) + method smooth_on = use_smooth <- true; self#update_smooth + (* annule l'utilisation du lissage *) + method smooth_off = use_smooth <- false; self#update_smooth + (* change l'etat d'utilisation du lissage *) + method smooth_switch = use_smooth <- not use_smooth; self#update_smooth + (* met a jour le widget 3D pour appliquer ou pas le lissage suivant la + valeur de [use_smooth] *) + method private update_smooth = + do_msg (if use_smooth then "Smooth on" else "Smooth off") ; + GlDraw.shade_model (if use_smooth then `smooth else `flat) ; + self#set_status ; self#display_func + + (* affiche la rosace *) + method rosace_on = show_rosace <- true; self#display_func + (* masque la rosace *) + method rosace_off = show_rosace <- false; self#display_func + (* change l'etat d'affichage de la rosace suivant la valeur de [show_rosace] *) + method rosace_switch = show_rosace <- not show_rosace; self#display_func + + (* Modification de la vue et redessin *) + method change_and_redraw = self#setup; self#display_func + + (* Rotation/deplacement de la position utilisateur *) + method rotate_view r = rot<-r; self#change_and_redraw + method move_view d = depl<-d; self#change_and_redraw + + (* Modification du curseur *) + method private set_cursor c = + Gtk_tools.set_cursor area#misc#window c + method private reset_cursor = + Gtk_tools.set_cursor area#misc#window cursor_standard + + (* [scale_point point] met a l'echelle un point *) + method private scale_point pt = geom_scal_mult (1./.rs) pt + (* [scale_points lst_points] met a l'echelle une liste de points *) + method private scale_points l = List.map self#scale_point l + + (* [draw_triangulation t] dessine une face polygonale triangulee ou + pas en effectuant en plus la mise a l'echelle *) + method private draw_triangulation t = + match t with + NO_TRI l -> set_3d_points `polygon (self#scale_points l) + | TRI_STD l -> set_3d_points `triangles (self#scale_points l) + | TRI_WITH_FANS l -> List.iter (fun l -> + set_3d_points (if List.length l>3 then `triangle_fan else `triangles) + (self#scale_points l)) l + + (* [create_pyramid (x, y, z) size] cree une pyramide de hauteur + [size] centree en [(x, y, z)] *) + method private create_pyramid (x, y, z) size = + let d = size*.sqrt(3.)/.2. in + let a=d/.3. and b = (2.*.d)/.3. in + let p0 = (x, y, z+.b) and p1 = (x, y+.b, z-.a) + and p2 = (x-.size/.2., y-.a, z-.a) and p3 = (x+.size/.2., y-.a, z-.a) in + set_3d_points `triangle_fan [p0; p1; p2; p3; p1] ; + set_3d_points `triangles [p2; p1; p3] + + (* [compile_one objet] met a l'echelle et compile l'objet *) + method private compile_one o = + GlList.delete o.o_compiled ; + let compiled = GlList.create `compile in + (match o.o_obj with + OUTLINE_3D o -> + let pts = self#scale_points o.out3d_contour in + if o.out3d_filled then begin + set_gl_fillpoly () ; set_color o.out3d_color_in ; + (* Ici il faudrait trianguler le polygone si necessaire... *) + set_3d_points `polygon pts + end ; + GlDraw.line_width 2. ; + unset_gl_fillpoly () ; set_color o.out3d_color_out ; + set_3d_points `polygon pts + | LINE_3D l -> + Gl.disable `cull_face ; + let pts = self#scale_points l.line3d_points in + if l.line3d_filled or l.line3d_with_bars then begin + let lfill = List.flatten (List.map (fun (a, b, c) -> + [(a, b, 0.); (a, b, c)]) pts) in + if l.line3d_filled then begin + set_gl_fillpoly () ; set_faded_color l.line3d_color 70 ; + set_3d_points `quad_strip lfill + end ; + if l.line3d_with_bars then begin + GlDraw.line_width 2. ; + unset_gl_fillpoly () ; set_faded_color l.line3d_color 85 ; + set_3d_points `quad_strip lfill + end + end ; + GlDraw.line_width (float_of_int l.line3d_width) ; + set_color l.line3d_color ; + set_3d_points `line_strip pts ; + Gl.enable `cull_face + | VOLUME1_3D v -> + if v.vol3d_filled then set_gl_fillpoly () else begin + GlDraw.line_width 2. ; unset_gl_fillpoly () + end ; + (* Faces verticales *) + GlLight.color_material ~face:`front `ambient_and_diffuse ; + set_color v.vol3d_color ; + set_3d_points_quad_strip_with_normal (self#scale_points v.vol3d_contour) ; + + (* Dessin des faces inferieure et superieure avec une couleur plus sombre *) + if v.vol3d_filled then begin + GlLight.color_material ~face:`front `ambient_and_diffuse ; + set_faded_color v.vol3d_color 75 ; + normal_up () ; self#draw_triangulation v.vol3d_up ; + normal_down () ; self#draw_triangulation v.vol3d_down + end + | ENVELOPPE_3D e -> + (* Faces tjs visibles: Gl.disable `cull_face ; *) + if e.env3d_filled then set_gl_fillpoly () else begin + GlDraw.line_width 2. ; unset_gl_fillpoly () + end ; + + (* Faces verticales *) + GlLight.color_material ~face:`front `ambient_and_diffuse ; + set_color e.env3d_color ; + set_3d_points_quad_strip_with_normal (self#scale_points e.env3d_contour); + (*si faces env tjs visibles: Gl.enable `cull_face *) + | ENVELOPPE_3D_DOUBLE e -> + (* Faces tjs visibles: Gl.disable `cull_face ; *) + if e.env3d_double_filled then set_gl_fillpoly () else begin + GlDraw.line_width 2. ; unset_gl_fillpoly () + end ; + + (* Faces verticales externes *) + GlLight.color_material ~face:`front `ambient_and_diffuse ; + set_color e.env3d_double_color_out ; + set_3d_points_quad_strip_with_normal + (self#scale_points e.env3d_double_contour_out); + + (* Faces verticales internes *) + GlLight.color_material ~face:`front `ambient_and_diffuse ; + set_color e.env3d_double_color_in ; + set_3d_points_quad_strip_with_normal + (self#scale_points e.env3d_double_contour_in) + + (*si faces env tjs visibles: Gl.enable `cull_face *) + | ARROW_3D a -> + if a.arr3d_filled then set_gl_fillpoly () else begin + GlDraw.line_width 2. ; unset_gl_fillpoly () + end ; + GlLight.color_material ~face:`front `ambient_and_diffuse ; + set_color a.arr3d_color ; + + GlMat.push () ; + (* Deplace la fleche sur la pointe de la fleche *) + GlMat.translate3 (self#scale_point a.arr3d_pt) ; + (* Tourne la fleche pour qu'elle soit orientee comme voulu *) + GlMat.rotate3 a.arr3d_vect ~angle:a.arr3d_angle_xy ; + (* Tourne selon Z pour remettre la fleche comme il faut *) + rotate Z_AXIS a.arr3d_angle_z ; + + (* Mise a l'echelle des points de la fleche *) + let l = List.map self#scale_points a.arr3d_contour in + (* Faces 'verticales' *) + set_3d_points_quad_strip_with_normal (List.flatten l) ; + if a.arr3d_filled then begin + GlLight.color_material ~face:`front `ambient_and_diffuse ; + set_faded_color a.arr3d_color 75 ; + let (up, down) = + (List.rev_map (fun l -> List.hd (List.tl l)) l, + List.map (fun l -> List.hd l) l) in + (* Faces 'horizontales' *) + normal_up () ; set_3d_points `polygon up ; + normal_down () ; set_3d_points `polygon down + end ; + GlMat.pop () + | POINT_3D p -> + let (x0, y0, z0) = self#scale_point p.p3d_pos + and (x, y, z) = self#scale_point p.p3d_pos2 in + set_gl_fillpoly () ; set_color p.p3d_color ; + let size = 0.02 in + self#create_pyramid (x0, y0, z0) size ; + + if p.p3d_with_name then begin + GlDraw.line_width 2.0 ; + set_3d_points `line_strip [(x, y, z); (x0, y0, z0+.size)] ; + if fonts_available then begin + GlPix.raster_pos ~x ~y ~z:(z+.0.01) () ; + Gtkgl_Hack.gl_print_string fontbase p.p3d_name + end + end ; + + | SURFACE_3D_TEX s -> + normal_up () ; + let tt = Array.map (Array.map self#scale_point) s.s3d_tex_pts in + (* On recentre les coordonnees dans la texture pour la voir en entier *) + (* sur toute la surface *) + let (x1,y1,_) = tt.(0).(0) and ttt = tt.(Array.length tt-1) in + let (x2,y2,_) = ttt.(Array.length ttt-1) in + let delta = (x2-.x1, y2-.y1) and first = (x1, y1) in + + let t_normals = get_surface_normals tt in + Gl.enable `texture_2d; GlDraw.shade_model `flat ; + (* On utilise la texture indiquee *) + GlTex.bind_texture `texture_2d s.s3d_tex_texture_id; + Array.iteri (fun i t0 -> + if i + l:=(tt.(i+1).(j), t_normals.(i+1).(j))::(p, t_normals.(i).(j))::!l) t0 ; + set_3d_points_with_texture `triangle_strip (List.rev !l) delta first + end) tt ; + Gl.disable `texture_2d ; + GlDraw.shade_model (if use_smooth then `smooth else `flat) + + | SURFACE_3D s -> + if s.s3d_filled then set_gl_fillpoly () else begin + GlDraw.line_width 2. ; unset_gl_fillpoly () + end ; + GlLight.color_material ~face:`front `ambient_and_diffuse ; + normal_up () ; + let t = Array.map (Array.map (fun (p, c) -> (self#scale_point p, c))) s.s3d_pts in + + let t_normals = get_surface_normals (Array.map (Array.map fst) t) in + Array.iteri (fun i t0 -> + if i + l:=(t.(i+1).(j), t_normals.(i+1).(j))::(p, t_normals.(i).(j))::!l) t0 ; + set_3d_points_with_color `triangle_strip (List.rev !l) + end) t + ) ; + GlList.ends () ; + o.o_compiled <- compiled + + (* recompilation de tous les objets contenus dans le widget *) + method private recompile_all_objects = List.iter self#compile_one objects + (* [make_and_compile objet] compile uniquement l'objet indique *) + method private make_and_compile o = self#make_current ; self#compile_one o + + (* [add_object objet] ajoute l'objet : compilation de cet objet et eventuelle + recompilation des autres s'il sort des extremes precedents *) + method private add_object o = + self#make_current ; + let new_o = {o_obj=o; o_id=self#get_new_id; + o_compiled=not_compiled_obj; o_show=true} in + objects <- new_o::objects ; + (* Recherche des valeurs extremes de cet objet si necessaire *) + let old_rs = rs in + let (do_it, l) = + match o with + OUTLINE_3D o -> (false, []) + | LINE_3D l -> (true, l.line3d_points) + | VOLUME1_3D v -> (true, v.vol3d_contour) + | ENVELOPPE_3D e -> (true, e.env3d_contour) + | ENVELOPPE_3D_DOUBLE e -> (true, e.env3d_double_contour_out) + | ARROW_3D a -> (false, []) + | POINT_3D p -> (true, [p.p3d_pos; p.p3d_pos2]) + | SURFACE_3D s -> + let l = Array.to_list (Array.map (fun t -> Array.to_list t) s.s3d_pts) in + (true, List.map fst (List.flatten l)) + | SURFACE_3D_TEX s -> + let l = Array.to_list (Array.map (fun t -> Array.to_list t) s.s3d_tex_pts) in + (true, List.flatten l) + in + if do_it then self#get_extremes l ; + if old_rs<>rs then + (* Il faut recompiler les autres objets car les extremes ont change... *) + self#recompile_all_objects + else + (* Compilation uniquement de cet objet *) + self#compile_one new_o ; + + (* Renvoie l'identifiant du nouvel objet cree *) + new_o.o_id + + (* [get_extremes pts_list] recherche les points extremes d'un objet, met + a jour les valeurs pour la scene et reaffiche si necessaire *) + method private get_extremes pts_list = + if pts_list <> [] then begin + let (p1, p2) = + if extreme_min=pt_undefined && extreme_max=pt_undefined then + (List.hd pts_list, List.hd pts_list) + else (extreme_min, extreme_max) + in + + let minmax coord l = + let dep = (get_coord p1 coord, get_coord p2 coord) in + List.fold_left (fun (i, a) e0 -> + let e = get_coord e0 coord in (min e i, max e a)) dep l + in + + let (minx, maxx) = minmax X_AXIS pts_list + and (miny, maxy) = minmax Y_AXIS pts_list + and (minz, maxz) = minmax Z_AXIS pts_list in + let cc i a = (a +. i) /. 2. in + let r = max (maxx -. minx) (maxy -. miny) in + rs <- max r (maxz -. minz) ; + extreme_min <- (minx, miny, minz) ; + extreme_max <- (maxx, maxy, maxz) ; + let new_extents = self#scale_point (-.(cc minx maxx), -.(cc miny maxy), + -.(cc minz maxz)) in + if new_extents<>extents then begin + extents<-new_extents ; self#setup + end + end + + (* Volume simple du type secteur ou seul le contour et les niveaux *) + (* inf et sup suffisent *) + method add_object_volume_simple contour zmin zmax color filled = + (* Le contour doit etre oriente dans le sens contre-horaire (cull_face) *) + let contour = + if poly_test_ccw contour = CW then contour else List.rev contour in + let l0 = geom_close_poly ((List.map (fun p -> [glpoint3d_of_pt_2d zmin p; + glpoint3d_of_pt_2d zmax p]) + ) contour) in + + (* Triangulation ou pas des faces superieures et inferieures *) + let (down, up) = + if poly_test_convex contour = CONVEX then + (NO_TRI (List.map (fun l -> List.hd l) l0), + NO_TRI (List.rev_map (fun l -> List.hd (List.tl l)) l0)) + else begin + if use_fans_for_tesselation then begin + let fans = Geometry_2d.tesselation_fans contour in + let l1 = List.map (List.map (glpoint3d_of_pt_2d zmin)) fans + and l2 = List.map (fun lst -> + let l = List.map (glpoint3d_of_pt_2d zmax) lst in + (* Le premier point ne doit pas se trouver en dernier *) + (* car c'est le pivot du triangle_fan *) + (List.hd l)::(List.rev (List.tl l))) fans in + (TRI_WITH_FANS l1, TRI_WITH_FANS l2) + end else begin + let triangles = List.flatten (Geometry_2d.tesselation contour) in + let l1 = List.map (glpoint3d_of_pt_2d zmin) triangles + and l2 = List.rev_map (glpoint3d_of_pt_2d zmax) triangles in + (TRI_STD l1, TRI_STD l2) + end + end + in + self#add_object (VOLUME1_3D {vol3d_contour = List.flatten l0; + vol3d_up = up; + vol3d_down = down; + vol3d_color = color; + vol3d_filled = filled}) + (* Enveloppe 3D : faces laterales liant un contour_haut et un contour_bas *) + (* 2 contours = meme nombre de points, oriente dans le sens contre-horaire *) + method add_object_enveloppe contour_haut contour_bas color filled = + let add_p p_bas p_haut res = + (glpoint3d_of_pt_3d p_bas)::(glpoint3d_of_pt_3d p_haut)::res + in + let cw_poly c = + let contour_2d = List.map (pt_3d_to_pt_2d T_Z3D) c in + if (poly_test_ccw contour_2d = CW) then c else List.rev c + in + let l_bas = geom_close_poly (cw_poly contour_bas) in + let l_haut = geom_close_poly (cw_poly contour_haut) in + let l0 = + try List.fold_right2 add_p l_bas l_haut [] + with x-> Printf.printf "\nadd_object_enveloppe.fold... : "; raise x + in + try + self#add_object (ENVELOPPE_3D {env3d_contour = l0; + env3d_color = color; + env3d_filled = filled}) + with x -> + Printf.printf "\nadd_object_enveloppe. self#add_object "; raise x + + (* Enveloppe 3D : faces laterales liant un contour_haut et un contour_bas *) + (* 2 contours = meme nombre de points, oriente dans le sens contre-horaire *) + method add_object_enveloppe_double contour_haut contour_bas + color_out color_in filled = + let add_p p_bas p_haut res = + (glpoint3d_of_pt_3d p_bas)::(glpoint3d_of_pt_3d p_haut)::res + in + let cw_poly c = + let contour_2d = List.map (pt_3d_to_pt_2d T_Z3D) c in + if (poly_test_ccw contour_2d = CW) then c else List.rev c + in + let l_bas = geom_close_poly (cw_poly contour_bas) in + let l_haut = geom_close_poly (cw_poly contour_haut) in + let l0 = + try List.fold_right2 add_p l_bas l_haut [] + with x-> Printf.printf "\nadd_object_enveloppe.fold... : "; raise x + in + let l_inside = + try List.fold_right2 add_p (List.rev l_bas) (List.rev l_haut) [] + with x-> Printf.printf "\nadd_object_enveloppe.foldinside... : "; raise x + in + try + self#add_object (ENVELOPPE_3D_DOUBLE {env3d_double_contour_out = l0; + env3d_double_contour_in = l_inside; + env3d_double_color_out = color_out; + env3d_double_color_in = color_in; + env3d_double_filled = filled}) + with x -> + Printf.printf "\nadd_object_enveloppe. self#add_object "; raise x + + (* Fleche (flux) dont la pointe est placee en pt0 si sens est vrai *) + (* Ici, inutile de tesseler les faces inf et sup *) + method add_object_arrow pt0 vdir sens ep lg color filled + arrow_type = + let pts = + if arrow_type=ARROW1 then + [(0., 0.); (2.*.ep, ep); (2.*.ep, ep/.2.); (lg, ep/.2.); + (lg, (-.ep)/.2.); (2.*.ep, (-.ep)/.2.); (2.*.ep, -.ep)] + else + [(0., 0.); (2.5*.ep, ep); (2.*.ep, ep/.2.); (lg, ep/.2.); + (lg, (-.ep)/.2.); (2.*.ep, (-.ep)/.2.); (2.5*.ep, -.ep)] + in + + let pts = if sens then pts else List.map (fun (x, y) -> (x-.lg, y)) pts in + + let pts = List.map (fun (x, y) -> {x2D=x; y2D=y}) pts in + + let pts = if poly_test_ccw pts = CW then pts else List.rev pts in + let l = List.map (fun p -> + [glpoint3d_of_pt_2d ((-.ep)/.2.) p; glpoint3d_of_pt_2d (ep/.2.) p]) pts in + let dd = + if sens then Geometry_3d.vect_make pt0 vdir + else Geometry_3d.vect_make vdir pt0 + in + let h = Geometry_3d.vect_norm dd + and d = sqrt(dd.x3D*.dd.x3D+.dd.y3D*.dd.y3D) in + let alpha = + if dd.y3D>0. then acos (dd.x3D/.d) else -. (acos (dd.x3D/.d)) + in + let beta = + if dd.z3D>0. then acos (d/.h) else -.(acos (d/.h)) + in + + self#add_object (ARROW_3D {arr3d_contour = geom_close_poly l; + arr3d_pt = glpoint3d_of_pt_3d pt0 ; + arr3d_vect = (dd.y3D, -.dd.x3D, 0.) ; + arr3d_angle_xy = rad2deg beta ; + arr3d_angle_z = rad2deg alpha ; + arr3d_color = color; + arr3d_filled = filled}) + + method add_object_outline contour cin cout filled = + let l = geom_close_poly (glpoint3d_of_pt_3d_lst contour) in + self#add_object (OUTLINE_3D {out3d_contour = l ; + out3d_color_in = cin ; + out3d_color_out = cout ; + out3d_filled = filled}) + + method add_object_line l color line_width with_bars fill = + self#add_object (LINE_3D {line3d_points = glpoint3d_of_pt_3d_lst l; + line3d_width = line_width; + line3d_color = color; + line3d_with_bars = with_bars; + line3d_filled = fill}) + + method add_object_point pos pos2 name color with_name = + self#add_object (POINT_3D {p3d_pos = glpoint3d_of_pt_3d pos; + p3d_pos2 = glpoint3d_of_pt_3d pos2; + p3d_name = name; + p3d_with_name = with_name; + p3d_color = color}) + + method add_object_surface_with_texture tab texture_id = + let t = Array.map (Array.map glpoint3d_of_pt_3d) tab in + self#add_object (SURFACE_3D_TEX {s3d_tex_pts = t; + s3d_tex_texture_id = texture_id}) + + method add_object_surface tab fill = + let t = Array.map (Array.map (fun (p, c) -> (glpoint3d_of_pt_3d p, c))) tab in + self#add_object (SURFACE_3D {s3d_pts = t; s3d_filled = fill}) + + (* cree l'objet rosace *) + method private create_rosace = + self#make_current ; + let compiled = GlList.create `compile in + GlDraw.line_width 2.0 ; + GlDraw.color color_rosace ; + GlDraw.begins `line_strip ; + List.iter GlDraw.vertex2 [(0.0, -0.05); (0.0, 0.05)] ; + GlDraw.ends () ; + GlDraw.begins `line_strip ; + List.iter GlDraw.vertex2 [(-0.05, 0.0); (0.05, 0.0)] ; + GlDraw.ends () ; + GlDraw.begins `line_strip ; + List.iter GlDraw.vertex2 [(-0.01, 0.04); (0.0, 0.05); (0.01, 0.04)] ; + GlDraw.ends () ; + if fonts_available then begin + (* Affichage du Nord sur la rosace si la fonte est disponible *) + GlPix.raster_pos ~x:(-.0.01) ~y:0.07 ~z:0.0 () ; + Gtkgl_Hack.gl_print_string fontbase "N" + end ; + GlList.ends () ; + rosace <- Some compiled ; + compiled + + (* initialisation lors de la creation du widget *) + method private init_func () = + if not done_init then begin + do_msg "Init 3D" ; + List.iter Gl.enable [`depth_test; `cull_face] ; + GlDraw.cull_face `back; + GlDraw.front_face `ccw; + + List.iter (if use_lights then Gl.enable else Gl.disable) lights ; + GlDraw.shade_model (if use_smooth then `smooth else `flat) ; + + if fonts_available then fontbase <- load_bitmap_font "8x13" ; + + GlPix.store (`unpack_alignment 1); + List.iter (GlTex.parameter ~target:`texture_2d) + [ `wrap_s `repeat; `wrap_t `repeat; `mag_filter `linear; `min_filter `linear ]; + GlTex.env (`mode `decal); + + done_init <- true + end + + method private reshape_func ~width:w ~height:h = + if not done_init then self#init_func () ; + do_msg (Printf.sprintf "Reshape 3D w=%d h=%d" w h) ; + width <- w; height <- h ; + GlDraw.viewport ~x:0 ~y:0 ~w ~h; + self#setup + + (* met a jour de la vue et de la source de lumiere *) + method private setup = + do_msg "Setup" ; + GlMat.mode `projection; + GlMat.load_identity (); + let aspect = float width /. float height and view_fovs = 1. in + GluMat.perspective ~fovy:(45. *. view_fovs) ~aspect + ~z:(0.1, (rs*.sqrt(2.)+.1.)); + + GlMat.mode `modelview; + GlMat.load_identity (); + + (* Deplace et tourne la position de l'utilisateur *) + GlMat.translate3 depl ; + rotate_all rot ; + + (* Place les objets au centre de la scene *) + GlMat.translate3 extents ; + + (* Positionnement de la lumiere *) + if use_lights then begin + GlMat.push (); + rotate_all lightrot ; + let (x, y, z)= lightpos in + List.iter (GlLight.light ~num:0) [`position (x, y, z, 1.); + `ambient (1., 1., 1., 1.); + `diffuse (1., 1., 1., 1.)] ; + GlMat.pop () + end ; + + self#set_status + + method set_name n = name <- n; self#display_func + method set_show_name set = show_name <- set; self#display_func + + method display = fun o -> + GlList.call (self#get_object o).o_compiled; Gl.flush (); area#swap_buffers () + (* force l'affichage (apres ajout d'un objet par exemple) *) + method display_func = + do_msg "Display 3D" ; + self#make_current ; + (* Efface l'ecran *) + GlClear.color back_color ~alpha:0.0; GlClear.clear [`color;`depth]; + (* Passage des objets *) + List.iter (fun o -> if o.o_show then GlList.call o.o_compiled) objects ; + + let aspect = float width /. float height in + + (* Affichage du nom si c'est demande *) + if fonts_available && name<>"" && show_name then begin + GlMat.push (); + GlMat.load_identity (); + set_color (1., 1., 1.) ; + GlPix.raster_pos ~x:(-.4.*.aspect) ~y:(4.) ~z:(-.10.0) () ; + Gtkgl_Hack.gl_print_string fontbase name ; + GlMat.pop () + end ; + + (* Affichage de la rosace si necessaire *) + if show_rosace then begin + (* Creation de la rosace si ca n'est pas deja fait... *) + let r = match rosace with None -> self#create_rosace|Some r -> r in + GlMat.push (); + GlMat.load_identity (); + GlMat.translate3 (-.0.5*.aspect, 0.5, -.1.5) ; + rotate_some rot [X_AXIS; Z_AXIS] ; + GlList.call r ; + GlMat.pop () + end ; + + (* Affichage de la sphere representant la lumiere *) + if show_light then begin + GlMat.push (); + rotate_all lightrot ; + GlMat.translate3 lightpos; + set_color glcolor_white ; + let radius = 0.03 in + GluQuadric.sphere ~radius ~stacks:5 ~slices:5 (); + GlMat.pop () + end ; + + (* Affichage *) + Gl.flush (); area#swap_buffers () + + + (* [mouse_press ev] traite un evenement correspondant a l'appui sur un bouton *) + method private mouse_press ev = + let mouse_pos = Gtk_tools.get_mouse_pos_click ev in + match (Gtk_tools.test_mouse_but ev) with + Gtk_tools.B_GAUCHE -> + self#set_cursor cursor_rotate ; + current_action <- (ACTION_ROTATE mouse_pos); true + | Gtk_tools.B_MILIEU -> + current_action <- (ACTION_ZOOM mouse_pos); true + | Gtk_tools.B_DROIT -> false + | _ -> false + + (* [mouse_move ev] traite les mouvements souris *) + method private mouse_move ev = + area#misc#grab_focus () ; + let mouse_pos = Gtk_tools.get_mouse_pos_move ev in + match current_action with + ACTION_NONE -> false + | ACTION_ZOOM old_pos -> + let dy = (snd mouse_pos)-(snd old_pos) in + self#set_cursor (if dy<0 then cursor_zoom_up else cursor_zoom_down) ; + self#incr_dist (dist_incr*.(float_of_int dy)) ; + current_action <- (ACTION_ZOOM mouse_pos) ; true + | ACTION_ROTATE old_pos -> + let dz = (fst mouse_pos)-(fst old_pos) + and dx = (snd mouse_pos)-(snd old_pos) in + let r = add_coord_360 rot X_AXIS (float_of_int dx) in + self#rotate_view (add_coord_360 r Z_AXIS (float_of_int dz)) ; + current_action <- (ACTION_ROTATE mouse_pos) ; true + + (* [mouse_release ev] traite un evenement de relachement de bouton *) + method private mouse_release ev = + (match current_action with ACTION_NONE -> () | _ -> self#reset_cursor) ; + current_action <- ACTION_NONE ; + true + + (* Mouvement de la molette de la souris sous Windows *) + method private mouse_wheel ev = + match GdkEvent.Scroll.direction ev with + `UP -> self#incr_dist (-.dist_incr) ; true + | `DOWN -> self#incr_dist dist_incr ; true + | `LEFT -> false + | `RIGHT -> false + + method private start_stop_animation = + match animation_timer with + None -> + let timeout _ = self#incr_rotz rot_anim; true in + animation_timer <- Some (Timeout.add ~ms:tps_anim ~callback:timeout) + | Some t -> Timeout.remove t ; animation_timer <- None + + (* [key_pressed key] teste la touche pressee dans la zone de dessin + et effectue l'action associee le cas echeant *) + method private key_pressed key = + let keys_list = + [([_Page_Up], fun () -> self#incr_dist (-.dist_incr)) ; + ([_Page_Down], fun () -> self#incr_dist dist_incr) ; + ([_KP_Down; _KP_2], fun () -> self#incr_rotx rot_incr) ; + ([_KP_Up; _KP_8], fun () -> self#incr_rotx (-.rot_incr)) ; + ([_KP_Left; _KP_4], fun () -> self#incr_rotz (-.rot_incr)) ; + ([_KP_Right; _KP_6], fun () -> self#incr_rotz rot_incr) ; + ([_Down], fun () -> self#move_y (dist_incr/.3.)) ; + ([_Up], fun () -> self#move_y ((-.dist_incr)/.3.)) ; + ([_Left], fun () -> self#move_x (dist_incr/.3.)) ; + ([_Right], fun () -> self#move_x ((-.dist_incr)/.3.)) ; + ([_l], fun () -> self#lights_switch) ; + ([_s], fun () -> self#smooth_switch) ; + ([_r], fun () -> self#rosace_switch) ; + ([_Home], fun () -> self#rotate_view pt_null) ; + ([_a], fun () -> self#incr_light_rot rot_incr) ; + ([_z], fun () -> self#incr_light_rot (-.rot_incr)) ; + ([_L], fun () -> show_light <- not show_light; self#change_and_redraw) ; + ([_F12], fun () -> self#screenshot screenshot_format + (screenshot_path^screenshot_name)); + ([_space], fun () -> self#start_stop_animation); + ([_Escape], fun () -> self#redo_all); + ([_i], fun () -> Printf.printf "%s" (get_gl_infos ()); flush stdout); + ([_n], fun () -> self#set_show_name (not show_name))] in + (* Recherche la fonction associee a la touche presse s'il y en a une *) + let rec check_keys lst = + match lst with + (keys, func)::reste -> + if List.mem key keys then begin func ();true end else check_keys reste + | [] -> false + in + check_keys keys_list + + (* Modification de la position de l'utilisateur et des angles de vue *) + method incr_dist d = self#move_view (add_coord depl Z_AXIS d) + method incr_rotx d = self#rotate_view (add_coord_360 rot X_AXIS d) + method incr_rotz d = self#rotate_view (add_coord_360 rot Z_AXIS d) + + method move_x d = self#move_view (add_coord depl X_AXIS d) + method move_y d = self#move_view (add_coord depl Y_AXIS d) + + (* tourne la lumiere *) + method incr_light_rot d = lightrot<-add_coord_360 lightrot Z_AXIS d; + self#change_and_redraw + + (* [set_screenshot def_format def_path def_filename] met a jour les + parametres de capture d'ecran *) + method set_screenshot def_format def_path def_filename = + screenshot_path <- def_path; screenshot_name <- def_filename; + screenshot_format <- def_format + + (* [screenshot format filename] ouvre une boite de capture ecran *) + method screenshot format filename = + Gtk_tools.screenshot_box filename format area#misc#window + None 0 0 width height + + (* Manipulations generales des objets *) + method private get_object id = + try List.find (fun o -> o.o_id=id) objects + with Not_found -> raise (NO_SUCH_3D_OBJECT id) + method delete_object id = + let found = ref None in + let new_l = List.fold_left (fun l obj -> + if obj.o_id=id then begin found:=Some obj; l end else obj::l) [] objects in + match !found with + None -> raise (NO_SUCH_3D_OBJECT id) + | Some obj -> objects <- List.rev new_l; GlList.delete obj.o_compiled + method object_set_color id new_color = + let o = self#get_object id in + set_object_color o.o_obj new_color; self#make_and_compile o + method object_get_color id = + get_object_color (self#get_object id).o_obj + method object_set_visibility id visible = + let o = self#get_object id in o.o_show <- visible; self#make_and_compile o + method object_get_visibility id = (self#get_object id).o_show + method object_set_fill id filled = + let o = self#get_object id in + set_object_fill o.o_obj filled; self#make_and_compile o + method object_get_fill id = + get_object_fill (self#get_object id).o_obj + + (* Fonctions specifiques a certains objets *) + method line_get_width id = + get_line_width (self#get_object id).o_obj id + method line_set_width id width = + let o = self#get_object id in + set_line_width o.o_obj width id; self#make_and_compile o + method line_get_with_bars id = + get_line_bars (self#get_object id).o_obj id + method line_set_with_bars id withbars = + let o = self#get_object id in + set_line_bars o.o_obj withbars id; self#make_and_compile o + method outline_set_in_color id new_color = + let o = self#get_object id in + set_outline_in_color o.o_obj new_color id; self#make_and_compile o + method outline_get_in_color id = + get_outline_in_color (self#get_object id).o_obj id + method point_get_with_name id = + get_point_name (self#get_object id).o_obj id + method point_set_with_name id withname = + let o = self#get_object id in + set_point_name o.o_obj withname id; self#make_and_compile o + + method destroy_all_objects = + List.iter (fun o -> GlList.delete o.o_compiled) objects ; + objects <- [] ; + (match rosace with None -> () | Some r -> GlList.delete r) ; + unload_bitmap_font fontbase + + method private redo_all = + self#setup; self#recompile_all_objects; + (match rosace with None -> () | Some r -> GlList.delete r) ; + if show_rosace then ignore(self#create_rosace) ; + self#display_func + + method set_back_color c = back_color <- c ; self#display_func + + initializer + ignore(area#connect#realize ~callback:self#init_func) ; + ignore(area#connect#display ~callback:(fun () -> self#display_func)) ; + ignore(area#connect#reshape ~callback:self#reshape_func) ; + area#misc#realize (); + + incr nb_objects ; + + (* Indispensable pour que lorsque l'on entre dans la fenetre, les *) + (* prochaines commandes OpenGL soient bien appliquees dans la vue *) + (* correspondante dans le cas ou plusieurs vues ont ete ouvertes *) + ignore(area#event#connect#focus_in (fun _ -> self#make_current; false)) ; + + (* Callbacks des evenements souris *) + Gtk_tools_GL.glarea_mouse_connect area + self#mouse_press self#mouse_move self#mouse_release ; + + let scroll_cb = fun ev -> + match GdkEvent.get_type ev with + | `SCROLL -> self#mouse_wheel (GdkEvent.Scroll.cast ev) + | _ -> false in + + (* Reactions aux mouvements de la molette souris *) + ignore(area#event#connect#any ~callback:scroll_cb) ; + + (* Attachement des callbacks pour les evenements clavier *) + Gtk_tools_GL.glarea_key_connect area self#key_pressed (fun k -> (); false) + end + +(* =============================== FIN ========================================= *) diff --git a/sw/lib/ocaml/gtk_3d.mli b/sw/lib/ocaml/gtk_3d.mli new file mode 100644 index 00000000000..64ebc457993 --- /dev/null +++ b/sw/lib/ocaml/gtk_3d.mli @@ -0,0 +1,252 @@ +(* + * $Id$ + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(** 3D display widget + + {b Dépendences : Platform, Gtk_tools_GL, Geometry_2d, Geometry_3d} + *) + +(** {6 Exceptions} *) + +exception NO_SUCH_3D_OBJECT of int +(** Exception levée lors de la recherche d'un objet d'identifiant inconnu *) + +exception NOT_A_3D_OUTLINE of int +(** Exception levée lorsqu'un autre objet est passé alors qu'un contour + pays est attendu *) + +exception NOT_A_3D_LINE of int +(** Idem avec une ligne *) + +exception NOT_A_3D_POINT of int +(** Idem avec un point *) + +type glcolor = float * float * float +(** Une couleur OpenGL *) + +type t_arrow = ARROW1 | ARROW2 +(** Types de fleche *) + +(** [create_texture_from_image texture_filename] crée une texture à partir d'un fichier image. L'identifiant + de la texture créée est renvoyé. *) +val create_texture_from_image : string -> GlTex.texture_id + +(** [widget_3d pack with_status_bar name] crée un widget d'affichage 3D + + - [pack] indique où mettre le widget + - [with_status_bar] permet la création d'une barre d'infos optionnelle + - [name] désigne le nom à donner à la zone d'affichage (éventuellement affichée + en haut à gauche de la zone) +*) +class widget_3d : + (GObj.widget -> unit) -> + bool -> + string -> + object + + (** {6 Gestion de l'affichage} *) + + method display_func : unit + (** force l'affichage (utile après un ajout ou une modification d'objet) *) + method change_and_redraw : unit + (** prend en compte des modifications sur la position utilisateur (rotation + de la scène, éloignement) et force l'affichage *) + method incr_dist : float -> unit + (** [incr_dist delta] effectue une modification sur l'éloignement de + l'utilisateur par rapport à la scène *) + method incr_rotx : float -> unit + (** [incr_rotx delta_angle] tourne la scène de [delta_angle] (en degrés) + suivant l'axe X *) + method incr_rotz : float -> unit + (** [incr_rotz delta_angle] tourne la scène de [delta_angle] (en degrés) + suivant l'axe Z *) + + method move_x : float -> unit + (** [move_x d] déplacement de la vue sur l'axe X *) + + method move_y : float -> unit + (** [move_y d] déplacement de la vue sur l'axe Y *) + + method move_view : Gl.point3 -> unit + (** [move_view point] place l'utilisateur à la position indiquée par [point] *) + method rotate_view : float * float * float -> unit + (** [rotate_view (angle_x, angle_y, angle_z)] effectue les rotations de la scène + indiquées pour chaque axe *) + + method incr_light_rot : float -> unit + (** [incr_light_rot delta_angle] ajoute [delta_angle] à la rotation sur l'axe Z + de la source de lumière *) + + method set_name : string -> unit + (** [set_name name] modifie le nom associé au widget 3D *) + method set_show_name : bool -> unit + (** [set_show_name show] affiche/masque le nom associé au widget 3D *) + + method set_back_color : glcolor -> unit + (** [set_back_color couleur] met à jour la couleur de fond de la zone de dessin 3D *) + + (** {6 Rosace} *) + + method rosace_off : unit + (** masque la rosace *) + method rosace_on : unit + (** affiche la rosace *) + method rosace_switch : unit + (** change l'état d'affichage de la rosace suivant la valeur de [show_rosace] *) + + (** {6 Ajout/suppression d'objets} *) + + method destroy_all_objects : unit + (** Destruction de tous les objets existants *) + + method add_object_arrow : + Geometry_3d.pt_3D -> Geometry_3d.pt_3D -> bool -> float -> float -> glcolor -> + bool -> t_arrow -> int + (** [add_object_arrow ptA ptB sens ep lg color filled arrow_type] crée une fleche : + - [ptA] indique le point où se trouve la pointe de la fleche ou la + seconde extrémité + - [ptB] second point qui donne la direction de la fleche + - [sens] est vrai si [ptA] désigne la pointe de la fleche, faut s'il désigne + l'autre extrémité de la fleche + - [ep] épaisseur de la flèche + - [lg] sa longueur + - [color] la couleur + - [filled] indique si la fleche est remplie ou en fil de fer + - [arrow_type] désigne le type de la fleche + + L'identifiant de l'objet créé est renvoyé + *) + method add_object_line : + Geometry_3d.pt_3D list -> glcolor -> int -> bool -> bool -> int + (** [add_object_line lst_points color line_width with_bars fill] crée un objet + [LINE_3D] où + + - [lst_points] indique la liste des points de la ligne + - [color] désigne sa couleur + - [line_width] épaisseur + - [with_bars] indique si la ligne est affichée avec des barres verticales + - [fill] indique si la surface définie par la ligne et sa projection au + sol est affichée + + L'identifiant de l'objet créé est renvoyé + *) + + method add_object_outline : + Geometry_3d.pt_3D list -> glcolor -> glcolor -> bool -> int + (** [add_object_outline contour color_in color_out filled] crée un objet + [OUTLINE_3D] et renvoie son identifiant *) + method add_object_point : + Geometry_3d.pt_3D -> Geometry_3d.pt_3D -> string -> glcolor -> bool -> int + (** [add_object_point pos pos2 name color with_name] crée un [POINT_3D] avec : + - [pos] indique la position 3D du point + - [pos2] indique la position 3D où doit etre affiché son nom + - [name] le nom associé au point + - [color] la couleur du point + - [with_name] indique si le nom du point doit etre affiché + + L'identifiant de l'objet créé est renvoyé + *) + method add_object_volume_simple : + Geometry_2d.pt_2D list -> float -> float -> glcolor -> bool -> int + (** [add_object_volume_simple contour zmin zmax color filled] crée un objet + [VOLUME_3D] où : + + - [contour] désigne la surface (2D) définissant les faces inférieure + et supérieure du volume + - [zmin] et [zmax] indiquent repsectivement l'altitude min et l'altitude max + du volume + - [color] désigne la couleur à lui appliquer + - [filled] permet d'afficher le volume plein ou en mode fil de fer + *) + + method add_object_enveloppe : + Geometry_3d.pt_3D list -> + Geometry_3d.pt_3D list -> glcolor -> bool -> int + (** [add_object_enveloppe contour_haut contour_bas color filled] *) + + method add_object_enveloppe_double : + Geometry_3d.pt_3D list -> + Geometry_3d.pt_3D list -> glcolor -> glcolor -> bool -> int + (** [add_object_enveloppe_double contour_haut contour_bas + color_out color_in filled] *) + + method add_object_surface : (Geometry_3d.pt_3D*glcolor) array array -> bool -> int + (** [add_object_surface points filled] ajoute une surface, pleine ou pas suivant + [filled]. Elle est définie par la matrice de point [points] qui contient les + coordonnées et la couleur de chaque point de la grille *) + + method add_object_surface_with_texture : Geometry_3d.pt_3D array array -> GlTex.texture_id -> int + (** [add_object_surface_with_texture points texture_id] effectue la même opération + que la fonction précédente sauf qu'ici, on ne précise pas de couleur pour les + différents points mais une texture à appliquer sur la surface obtenue. *) + + method delete_object : int -> unit + (** [delete_object id] supprime l'objet dont l'identifiant est [id]. Si cet + objet n'existe pas, l'exception {!Gtk_3d.NO_SUCH_3D_OBJECT} est levée *) + + (** {6 Manipulation des objets} *) + + method display : int -> unit + method object_get_color : int -> glcolor + method object_get_fill : int -> bool + method object_get_visibility : int -> bool + method object_set_color : int -> glcolor -> unit + method object_set_fill : int -> bool -> unit + method object_set_visibility : int -> bool -> unit + method outline_get_in_color : int -> glcolor + method outline_set_in_color : int -> glcolor -> unit + method point_get_with_name : int -> bool + method point_set_with_name : int -> bool -> unit + method line_get_width : int -> int + method line_get_with_bars : int -> bool + method line_set_width : int -> int -> unit + method line_set_with_bars : int -> bool -> unit + + (** {6 Lighting/Smoothing} *) + + method lights_off : unit + (** annule l'utilisation de la lumière *) + method lights_on : unit + (** force l'utilisation de la lumière *) + method lights_switch : unit + (** change l'état d'utilisation de la lumière *) + + method smooth_off : unit + (** annule l'utilisation du lissage *) + method smooth_on : unit + (** force l'utilisation du lissage *) + method smooth_switch : unit + (** change l'état d'utilisation du lissage *) + + + (** {6 Captures écran} *) + + method set_screenshot : + Gtk_image.format_capture -> string -> string -> unit + (** [set_screenshot def_format def_path def_filename] met à jour les + paramètres de capture d'écran *) + + method screenshot : Gtk_image.format_capture -> string -> unit + (** [screenshot format filename] ouvre une boite de capture écran *) + end diff --git a/sw/lib/ocaml/gtk_draw.ml b/sw/lib/ocaml/gtk_draw.ml new file mode 100644 index 00000000000..ae53980752f --- /dev/null +++ b/sw/lib/ocaml/gtk_draw.ml @@ -0,0 +1,210 @@ +(* + * $Id$ + * + * GTK drawing in a pixmap + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + + +(* ============================================================================= *) +(* = Fonction de casting, indispensable pour pouvoir utiliser n'importe quel = *) +(* = type de drawable ('window, 'pixmap) pour dessiner = *) +(* ============================================================================= *) +let gd_do_cast p = (p:GDraw.drawable) + +(* ============================================================================= *) +(* = GDraw.color -> (r, g, b) en entiers = *) +(* ============================================================================= *) +let gd_rgb_of_color color = + let t = GDraw.color color in + (Gdk.Color.red t, Gdk.Color.green t, Gdk.Color.blue t) + +(* ============================================================================= *) +(* = GDraw.color -> (r, g, b) en flottants = *) +(* ============================================================================= *) +let gd_float_rgb_of_color color = + let (r, g, b) = gd_rgb_of_color color in + (float_of_int r, float_of_int g, float_of_int b) + +(* ============================================================================= *) +(* = Creation d'une GDraw.color a partir de ces composantes (r, g, b) = *) +(* ============================================================================= *) +let gd_color_of_rgb (r, g, b) = `RGB (r, g, b) + +(* ============================================================================= *) +(* = Creation d'une GDraw.color a partir de ces composantes (r, g, b) = *) +(* ============================================================================= *) +let gd_color_of_float_rgb (r, g, b) = `RGB (int_of_float r, int_of_float g, + int_of_float b) +(* ============================================================================= *) +(* = Mise a jour de la couleur de dessin = *) +(* ============================================================================= *) +let gd_set_color p color = + (gd_do_cast p)#set_foreground color + +(* ============================================================================= *) +(* = Mise a jour de la couleur de dessin a n% de la valeur indiquee = *) +(* ============================================================================= *) +let gd_set_faded_color p color pct = + let pct = (float_of_int pct)/.100. and (r, g, b) = gd_float_rgb_of_color color in + (gd_do_cast p)#set_foreground (gd_color_of_float_rgb (r*.pct, g*.pct, b*.pct)) + +(* ============================================================================= *) +(* = Modification du style de trace = *) +(* ============================================================================= *) +(* On est oblige de preciser width, alors qu'il ne doit normalement pas servir *) +(* Si on ne le met pas, les pointilles n'apparaissent pas partout !!! *) +let gd_set_style_solid p = + (gd_do_cast p)#set_line_attributes ~style:`SOLID ~width:1 () +let gd_set_style_dash p = + (gd_do_cast p)#set_line_attributes ~style:`ON_OFF_DASH ~width:1 () +let gd_set_style_double_dash p = + (gd_do_cast p)#set_line_attributes ~style:`DOUBLE_DASH ~width:1 () + +(* ============================================================================= *) +(* = Modification du mode de trace = *) +(* ============================================================================= *) +(*let gd_set_mode_xor p = (gd_do_cast p)#set_gc_xor +let gd_set_mode_std p = (gd_do_cast p)#set_gc_copy*) + +(* ============================================================================= *) +(* = Modification de l'epaisseur du trace = *) +(* ============================================================================= *) +let gd_set_line_width p width = (gd_do_cast p)#set_line_attributes ~width:width () + +(* ============================================================================= *) +(* = Centrage d'un texte a afficher = *) +(* ============================================================================= *) +let gd_center_text (x, y) string font = + let (w, h) = Gtk_tools.string_width_height font string in + (x-w/2, y+h/2) + +(* ============================================================================= *) +(* = Dessin d'un texte non centre = *) +(* ============================================================================= *) +let gd_draw_non_centered_text p (x, y) text font = + (gd_do_cast p)#string text ~font:font ~x:x ~y:y + +(* ============================================================================= *) +(* = Dessin d'un texte centre = *) +(* ============================================================================= *) +let gd_draw_text p pos_text text font = + let (x, y) = gd_center_text pos_text text font in + (gd_do_cast p)#string text ~font:font ~x:x ~y:y + +(* ============================================================================= *) +(* = Dessin d'un segment = *) +(* ============================================================================= *) +let gd_draw_segment p (x1, y1) (x2, y2) = + (gd_do_cast p)#line ~x:x1 ~y:y1 ~x:x2 ~y:y2 + +(* ============================================================================= *) +(* = Dessin d'un pixel = *) +(* ============================================================================= *) +let gd_draw_point p (x, y) = (gd_do_cast p)#point ~x:x ~y:y + +(* ============================================================================= *) +(* = Dessin d'une ligne = *) +(* ============================================================================= *) +let gd_draw_line p pts = (gd_do_cast p)#lines pts + +(* ============================================================================= *) +(* = Dessin d'un polygone = *) +(* ============================================================================= *) +let gd_draw_polygon p pts = (gd_do_cast p)#polygon ~filled:false pts + +(* ============================================================================= *) +(* = Dessin d'un polygone plein = *) +(* ============================================================================= *) +let gd_draw_filled_polygon p pts = (gd_do_cast p)#polygon ~filled:true pts + +(* ============================================================================= *) +(* = Dessin d'un cercle = *) +(* ============================================================================= *) +let gd_draw_circle p (x, y) r = + (gd_do_cast p)#arc ~filled:false ~x:(x-r) ~y:(y-r) ~width:(2*r) ~height:(2*r) () + +(* ============================================================================= *) +(* = Dessin d'un cercle plein = *) +(* ============================================================================= *) +let gd_draw_filled_circle p (x, y) r = + (gd_do_cast p)#arc ~filled:true ~x:(x-r) ~y:(y-r) ~width:(2*r) ~height:(2*r) () + +(* ============================================================================= *) +(* = Dessin d'un rectangle = *) +(* ============================================================================= *) +let gd_draw_rect p (x1, y1, x2, y2) = + (gd_do_cast p)#rectangle ~filled:false ~x:x1 ~y:y1 + ~width:(x2-x1) ~height:(y2-y1) () + +(* ============================================================================= *) +(* = Dessin d'un rectangle plein = *) +(* ============================================================================= *) +let gd_draw_filled_rect p (x1, y1, x2, y2) = + (gd_do_cast p)#rectangle ~filled:true ~x:x1 ~y:y1 ~width:(x2-x1) ~height:(y2-y1) () + +(* ============================================================================= *) +(* = Dessin d'un triangle = *) +(* ============================================================================= *) +let gd_draw_triangle p (x, y) size = + let size0 = int_of_float ((float_of_int size) *. 1.5) and + size1 = int_of_float ((float_of_int size) *. 0.5) in + (gd_do_cast p)#polygon ~filled:false + [(x, y-size); (x-size0, y+size1); (x+size0, y+size1)] + +(* ============================================================================= *) +(* = Dessin d'un triangle plein = *) +(* ============================================================================= *) +let gd_draw_filled_triangle p (x, y) size = + let size0 = int_of_float ((float_of_int size) *. 1.5) and + size1 = int_of_float ((float_of_int size) *. 0.5) in + (gd_do_cast p)#polygon ~filled:true + [(x, y-size); (x-size0, y+size1); (x+size0, y+size1)] + +(* ============================================================================= *) +(* = Efface une pixmap = *) +(* ============================================================================= *) +let gd_clear p color width height = + gd_set_color p color ; + gd_draw_filled_rect (gd_do_cast p) (0, 0, width, height) + +(* ============================================================================= *) +(* = Place le fond statique dans la pixmap de dessin = *) +(* ============================================================================= *) +let gd_set_background_pixmap p dest = (gd_do_cast dest)#put_pixmap ~x:0 ~y:0 p + +(* ============================================================================= *) +(* = Place une pixmap transparente dans le dessin = *) +(* ============================================================================= *) +let gd_put_transp_pixmap p dest x y = + (* Indispensable d'utiliser le masque pour la transparence *) + (match p#mask with + None -> () | + Some m -> (gd_do_cast dest)#set_clip_origin ~x:x ~y:y; dest#set_clip_mask m) ; + + (* Mise en place du pixmap transparent *) + dest#put_pixmap ~x:x ~y:y p#pixmap ; + + (* On enleve le masque *) + (match p#mask with None -> () | Some m -> prerr_endline "TODO (Gtk_draw): dest#unset_clip_mask") + +(* =============================== FIN ========================================= *) diff --git a/sw/lib/ocaml/gtk_draw.mli b/sw/lib/ocaml/gtk_draw.mli new file mode 100644 index 00000000000..24e8f1af892 --- /dev/null +++ b/sw/lib/ocaml/gtk_draw.mli @@ -0,0 +1,137 @@ +(** Module de dessin GTK + + {b Dépendences : Gtk_Tools} + *) + + +(** {6 Fonctions utilitaires} *) + + +(** [gd_do_cast drawable] : fonction de casting d'un drawable ('window ou 'pixmap) + pour pouvoir y dessiner *) +val gd_do_cast : GDraw.drawable -> GDraw.drawable + + +(** {6 Divers} *) + + +(** [gd_clear drawable color width height] efface une pixmap avec la couleur + [color] en dessinant un rectangle plein de taille [width]*[height] *) +val gd_clear : GDraw.drawable -> GDraw.color -> int -> int -> unit + +(** [gd_set_background_pixmap pixmap drawable] place la [pixmap] en fond de la + zone de dessin [drawable] *) +val gd_set_background_pixmap : Gdk.pixmap -> GDraw.drawable -> unit + +(** [gd_set_background_pixmap pixmap drawable x y] place la [pixmap] dans la zone + de dessin à la position [(x, y)]. Cette pixmap est transparente *) +val gd_put_transp_pixmap : GDraw.pixmap -> GDraw.drawable -> int -> int -> unit + +(** {6 Transformations de couleurs} *) + + +(** [gd_rgb_of_color color] renvoie un triplet (r, g, b) donnant les composantes + entières rouge, verte et bleue de [color]. Les composantes RGB sont dans + l'intervalle [\[0, 65535\]] *) +val gd_rgb_of_color : GDraw.color -> int * int * int + +(** [gd_float_rgb_of_color color] renvoie un triplet (r, g, b) donnant les + composantes flottantes rouge, verte et bleue de [color] *) +val gd_float_rgb_of_color : GDraw.color -> float * float * float + +(** [gd_color_of_rgb (r, g, b)] crée une [GDraw.color] à partir des composantes + entières *) +val gd_color_of_rgb : 'a * 'b * 'c -> [> `RGB of 'a * 'b * 'c] + +(** [gd_color_of_float_rgb (r, g, b)] crée une [GDraw.color] à partir des + composantes flottantes *) +val gd_color_of_float_rgb : + float * float * float -> [> `RGB of int * int * int] + + +(** {6 Modification du tracé} *) + + +(** [gd_set_color drawable color] met à jour la couleur de dessin *) +val gd_set_color : GDraw.drawable -> GDraw.color -> unit + +(** [gd_set_faded_color drawable color pct] met à jour la couleur de dessin avec + une couleur à [pct]% de [color] *) +val gd_set_faded_color : GDraw.drawable -> GDraw.color -> int -> unit + +(** [gd_set_style_solid drawable] met le style de dessin normal *) +val gd_set_style_solid : GDraw.drawable -> unit + +(** [gd_set_style_dash drawable] met le style de dessin pointillés *) +val gd_set_style_dash : GDraw.drawable -> unit + +(** [gd_set_style_double_dash drawable] met le style de dessin en doubles + pointillés *) +val gd_set_style_double_dash : GDraw.drawable -> unit + +(** [gd_set_mode_xor drawable] fixe le mode dessin en XOR *) +(*val gd_set_mode_xor : GDraw.drawable -> unit*) + +(** [gd_set_mode_std drawable] remet le dessin en mode normal *) +(*val gd_set_mode_std : GDraw.drawable -> unit*) + +(** [gd_set_line_width drawable width] fixe l'épaisseur des lignes *) +val gd_set_line_width : GDraw.drawable -> int -> unit + + +(** {6 Texte} *) + + +(** [gd_center_text (x, y) chaine fonte] centre le texte contenu dans [chaine] + a la position [(x, y)] *) +val gd_center_text : int * int -> string -> Gdk.font -> int * int + +(** [gd_draw_non_centered_text drawable (x, y) chaine fonte] dessine le texte + contenu dans [chaine] à la position [(x, y)] sans le centrer *) +val gd_draw_non_centered_text : + GDraw.drawable -> int * int -> string -> Gdk.font -> unit + +(** [gd_draw_text drawable (x, y) chaine fonte] dessine le texte + contenu dans [chaine] centré sur la position [(x, y)] *) +val gd_draw_text : + GDraw.drawable -> int * int -> string -> Gdk.font -> unit + + +(** {6 Formes géométriques} *) + + +(** [gd_draw_point drawable (x, y)] dessine un point à la position indiquée *) +val gd_draw_point : GDraw.drawable -> int * int -> unit + +(** [gd_draw_segment drawable (x1, y1) (x2, y2)] dessine un segment entre les + points [(x1, y1)] et [(x2, y2)] *) +val gd_draw_segment : GDraw.drawable -> int * int -> int * int -> unit + +(** [gd_draw_line drawable liste_points] dessine une ligne *) +val gd_draw_line : GDraw.drawable -> (int * int) list -> unit + +(** [gd_draw_polygon drawable liste_points] dessine un polygone non rempli *) +val gd_draw_polygon : GDraw.drawable -> (int * int) list -> unit + +(** [gd_draw_filled_polygon drawable liste_points] dessine un polygone rempli *) +val gd_draw_filled_polygon : GDraw.drawable -> (int * int) list -> unit + +(** [gd_draw_circle drawable (x, y) rayon] dessine un cercle non rempli *) +val gd_draw_circle : GDraw.drawable -> int * int -> int -> unit + +(** [gd_draw_filled_circle drawable (x, y) rayon] dessine un cercle rempli *) +val gd_draw_filled_circle : GDraw.drawable -> int * int -> int -> unit + +(** [gd_draw_rect drawable (x1, y1, x2, y2)] dessine un rectangle non rempli + dont les points extrèmes sont [(x1, y1)] et [(x2, y2)] *) +val gd_draw_rect : GDraw.drawable -> int * int * int * int -> unit + +(** [gd_draw_filled_rect drawable (x1, y1, x2, y2)] dessine un rectangle rempli + dont les points extrèmes sont [(x1, y1)] et [(x2, y2)] *) +val gd_draw_filled_rect : GDraw.drawable -> int * int * int * int -> unit + +(** [gd_draw_triangle drawable (x, y) size] dessine un triangle non rempli *) +val gd_draw_triangle : GDraw.drawable -> int * int -> int -> unit + +(** [gd_draw_filled_triangle drawable (x, y) size] dessine un triangle rempli *) +val gd_draw_filled_triangle : GDraw.drawable -> int * int -> int -> unit diff --git a/sw/lib/ocaml/gtk_image.ml b/sw/lib/ocaml/gtk_image.ml new file mode 100644 index 00000000000..922fa8016fa --- /dev/null +++ b/sw/lib/ocaml/gtk_image.ml @@ -0,0 +1,350 @@ +(* + * $Id$ + * + * Images utils + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(* Modules CamlImages *) +open Images +open Png +open Gif +open Jpeg +open Xpm +open Tiff + +open Platform + +(* Formats de sauvegarde pris en compte *) +type format_capture = PNG | GIF | JPEG | TIFF | BMP | PPM | POSTSCRIPT + +(* Formats disponibles suivant qu'on est sous Unix/Windows *) +let formats_capture_dispos = + if platform_is_unix then [PNG; GIF; JPEG; TIFF; BMP; PPM; POSTSCRIPT] + else [BMP; TIFF; PPM] (* Windows *) + +(* Indique si le format typ est disponible *) +let is_format_capture_dispo typ = + try ignore(List.find (fun t -> t=typ) formats_capture_dispos) ; true + with Not_found -> false + +(* Etats lors de la sauvegarde *) +type progress_save = INIT | SAVING | FINISHED + +(* [extended_string_of_format_capture format] fournit une chaine correspondant + au format : [PNG] -> "PNG"; [POSTSCRIPT] -> "Postscript" *) +let extended_string_of_format_capture format = + match format with + PNG -> "PNG" + | GIF -> "GIF" + | JPEG -> "JPEG" + | TIFF -> "TIFF" + | BMP -> "BMP" + | PPM -> "PPM" + | POSTSCRIPT -> "Postscript" + +(* [string_of_format_capture format] fournit une chaine correspondant + au format : [PNG] -> "PNG"; [POSTSCRIPT] -> "PS" *) +let string_of_format_capture format = + match format with + PNG -> "PNG" + | GIF -> "GIF" + | JPEG -> "JPG" + | TIFF -> "TIFF" + | BMP -> "BMP" + | PPM -> "PPM" + | POSTSCRIPT -> "PS" + +(* [format_capture_of_string chaine] renvoie le type {!Capture.format_capture} + correspondant a la chaine *) +let format_capture_of_string s = + match s with + "PNG" -> PNG + | "GIF" -> GIF + | "JPG" -> JPEG + | "TIFF" -> TIFF + | "BMP" -> BMP + | "PPM" -> PPM + | "PS" -> POSTSCRIPT + | _ -> PNG + +(* [string_of_extension format] renvoie l'extension correspondant au format. + i.e [PNG] -> ".png" *) +let string_of_extension format = + match format with + PNG -> ".png" + | GIF -> ".gif" + | JPEG -> ".jpg" + | TIFF -> ".tiff" + | BMP -> ".bmp" + | PPM -> ".ppm" + | POSTSCRIPT -> ".ps" + +(* ============================================================================= *) +(* = Transforme un entier en valeur (R,G,B) = *) +(* ============================================================================= *) +(* [rgb_of_int entier] transforme un entier en valeur (r, g, b) *) +let rgb_of_int v = + let r = v/65536 in + let reste = v-(r*65536) in + let g = reste/256 and + b = reste mod 256 in + {r=r; g=g; b=b} + +(* ============================================================================= *) +(* = Transforme une Gdk.Image en Image = *) +(* ============================================================================= *) +(* [image_of_gdkimage gdkimge largeur hauteur progress_func] transforme une + [Gdk.Image] en [Image] *) +let image_of_gdkimage gdkimg width height progress_func = + let total = 1.0/.float_of_int (height) and cpt = ref 0.0 in + + let img = Rgb24.create width height in + for y = 0 to height-1 do + for x = 0 to width-1 do + Rgb24.set img x y (rgb_of_int (Gdk.Image.get_pixel gdkimg x y)) ; + done ; + cpt := !cpt +. 1.0 ; + + (* Appel a la fonction de progression si necessaire *) + match progress_func with + None -> () + | Some f -> f INIT (!cpt*.total) + done ; + Rgb24(img) + +(* ============================================================================= *) +(* = Recuperation de la fonction de sauvegarde correspondant au format voulu = *) +(* ============================================================================= *) +(* [get_save_func format] renvoie la fonction de sauvegarde correspondant au + format indique *) +let get_save_func format = + match format with + PNG -> Png.save + | GIF -> Gif.save_image + | JPEG -> Jpeg.save + | TIFF -> Tiff.save + | BMP -> Bmp.save + | PPM -> Ppm.save + | POSTSCRIPT -> Ps.save + +(* ============================================================================= *) +(* = Recuperation de la fonction de chargement correspondant au format voulu = *) +(* ============================================================================= *) +let get_load_func format = + match format with + PNG -> Png.load + | JPEG -> Jpeg.load + | TIFF -> Tiff.load + | BMP -> Bmp.load + | PPM -> Ppm.load + | POSTSCRIPT -> Ps.load + | GIF -> (* Cas particulier pour les images GIF *) + let save_gif filename opts = + let sequence = Gif.load filename opts in + let frame = List.hd sequence.frames in + Index8 frame.frame_bitmap + in + save_gif + +(* ============================================================================= *) +(* = Recuperation du nom avec extension pour le format voulu = *) +(* ============================================================================= *) +let set_filename_extension filename format = + let extension = string_of_extension format in + let lg = String.length extension in + (* Test si l'extension est deja presente. Si elle ne l'est pas, on l'ajoute... *) + if (String.length filename) > lg && + (String.sub filename ((String.length filename)-lg) lg) = extension then + filename + else + filename^extension + +(* ============================================================================= *) +(* = Fonction de remplacement d'une extension = *) +(* = = *) +(* = filename = nom du fichier courant = *) +(* = old_format = ancien format = *) +(* = new_format = nouveau format = *) +(* ============================================================================= *) +let update_extension_capture filename old_format new_format = + let old_ext = string_of_extension old_format in + let lg = String.length old_ext in + if (String.sub filename ((String.length filename)-lg) lg) = old_ext then begin + (* Il faut supprimer l'ancienne extension *) + let f = String.sub filename 0 ((String.length filename)-lg) in + set_filename_extension f new_format ; + end else + (* Ajout de la nouvelle extension *) + set_filename_extension filename new_format + +(* ============================================================================= *) +(* = Effectue la capture proprement dite = *) +(* = = *) +(* = drawable = la pixmap ou fenetre contenant l'image a sauver = *) +(* = x = coordonnee x du point en haut a gauche = *) +(* = y = coordonnee y du point en haut a gauche = *) +(* = width = largeur de l'image = *) +(* = height = hauteur de l'image = *) +(* = filename = nom du fichier = *) +(* = format = format de sauvegarde (de type format_capture) = *) +(* = progress_func = fonction appelee lors de la progression (float -> unit) = *) +(* ============================================================================= *) +let capture_part draw x y width height filename format progress_func = + (* Creation d'un Gdk.Image a partir d'un Drawable pour pouvoir utiliser *) + (* la fonction get *) + let gdk_image = Gdk.Image.get draw x y width height in + + (* Transformation en Image *) + let image = image_of_gdkimage gdk_image width height progress_func in + (* On n'a plus besoin de la Gdk.Image *) + Gdk.Image.destroy gdk_image ; + + (* Sauvegarde de l'Image dans un fichier au format voulu *) + let save_func = get_save_func format in + + (* Ajout de l'extension appropriee si necessaire au nom de fichier *) + let filename_save = set_filename_extension filename format in + + (* Appel a la fonction de progression si necessaire *) + begin + match progress_func with + None -> save_func filename_save [] image + | Some f -> + save_func filename_save [Save_Progress(f SAVING)] image ; + (* Fin de la sauvegarde *) + f FINISHED 0.0 + end + +(* ============================================================================= *) +(* = Fonction principale de capture a partir d'un bout de pixmap = *) +(* = = *) +(* = window = la fenetre mere du drawable suivant = *) +(* = drawable = la pixmap ou fenetre contenant l'image a sauver = *) +(* = x = coordonnee x du point en haut a gauche = *) +(* = y = coordonnee y du point en haut a gauche = *) +(* = width = largeur de l'image = *) +(* = height = hauteur de l'image = *) +(* = filename = nom du fichier = *) +(* = format = format de sauvegarde (de type format_capture) = *) +(* = progress_func = fonction appelee lors de la progression (float -> unit) = *) +(* = caption = None ou Some(texte legende, couleur) = *) +(* ============================================================================= *) +let capture_part_with_caption window drawable x y width height filename + format progress_func caption = + let draw = + match caption with + None -> drawable (* Pas de legende *) + | Some (caption_text, caption_color, contour_color, back_color, font) -> + (* Copie de la pixmap initiale pour pouvoir y rajouter la legende *) + let depth = window#misc#visual_depth and w = window#misc#window in + let pix = Gdk.Pixmap.create ~window:w ~width:(width+x) ~height:(height+y) + ~depth:depth () in + let pixmap = new GDraw.pixmap pix in + pixmap#put_pixmap ~x:x ~y:x drawable ; + + (* Ajout de la legende *) + let taille_texte = Gdk.Font.string_width font caption_text and + taille_texte2 = Gdk.Font.string_height font caption_text in + let x0 = x+(width/2)-taille_texte/2-10 and + y0 = y+5 and + taille_x = taille_texte+20 and + taille_y = taille_texte2+10 in + + pixmap#set_foreground back_color ; + pixmap#rectangle ~filled:true ~x:x0 ~y:y0 ~width:taille_x ~height:taille_y () ; + pixmap#set_foreground contour_color ; + pixmap#rectangle ~filled:false ~x:x0 ~y:y0 ~width:taille_x ~height:taille_y () ; + pixmap#set_foreground caption_color ; + pixmap#string caption_text ~font:font + ~x:(x0+taille_x/2-taille_texte/2) ~y:(y0+taille_y/2+taille_texte2/2) ; + + (* Renvoie le nouveau drawable qui contient la legende *) + pix + in + capture_part draw x y width height filename format progress_func + +(* ============================================================================= *) +(* = Fonction principale de capture a partir d'une pixmap complete = *) +(* = = *) +(* = window = la fenetre mere du drawable suivant = *) +(* = drawable = la pixmap ou fenetre contenant l'image a sauver = *) +(* = width = largeur de l'image = *) +(* = height = hauteur de l'image = *) +(* = filename = nom du fichier = *) +(* = format = format de sauvegarde (de type format_capture) = *) +(* = progress_func = fonction appelee lors de la progression = *) +(* = du type : (progress_save * float -> unit) option = *) +(* = caption = None ou Some(texte legende, couleur) = *) +(* ============================================================================= *) +let capture_complete window drawable width height filename format progress_func caption = + capture_part_with_caption window drawable 0 0 width height filename + format progress_func caption + +(* ============================================================================= *) +(* = Creation d'une image Rgb24 quel que soit le format d'origine = *) +(* ============================================================================= *) +let gtk_image_rgb24_of_image image = + match image with + Images.Index8 i -> Index8.to_rgb24 i + | Images.Rgb24 i -> i + | Images.Index16 i -> Index16.to_rgb24 i + | Images.Rgba32 i -> Rgb24.of_rgba32 i + | Images.Cmyk32 i -> Printf.printf "Pb : Image Cmyk32 !!!\n"; flush stdout ; exit 1 + +(* ============================================================================= *) +(* = Lecture d'une image et creation d'une pixmap = *) +(* ============================================================================= *) +let gtk_image_load filename win format = + (* Chargement de l'image *) + let load_func = get_load_func format in + let image = load_func filename [] in + + (* Recuperation de sa taille *) + let (w, h) = Images.size image in + + (* Creation d'une pixmap de meme taille *) + let create_pixmap window width height = + let depth = (window:GWindow.window)#misc#visual_depth and w = window#misc#window in + let pix = Gdk.Pixmap.create ~window:w ~width:width ~height:height ~depth:depth () in + let pixmap = new GDraw.pixmap pix in + (pix, pixmap) + in + + let (pix, pixmap) = create_pixmap win w h in + + (* Creation d'une image Rgb24 quel que soit le format d'origine *) + let rgb = gtk_image_rgb24_of_image image in + + (* Transfert de l'image Rgb24 dans la pixmap *) + for y = 0 to h-1 do + for x = 0 to w-1 do + let {Images.r=r; Images.g=g; Images.b=b} = Rgb24.get rgb x y in + pixmap#set_foreground (`RGB (r*256, g*256, b*256)) ; + pixmap#point ~x:x ~y:y + done ; + done ; + + (* On renvoie la pixmap qui contient a present l'image lue *) + pixmap + +(* =============================== FIN ========================================= *) diff --git a/sw/lib/ocaml/gtk_image.mli b/sw/lib/ocaml/gtk_image.mli new file mode 100644 index 00000000000..07274abaaaa --- /dev/null +++ b/sw/lib/ocaml/gtk_image.mli @@ -0,0 +1,117 @@ +(* + * $Id$ + * + * Images utils + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(** Module de capture d'écran/gestion d'images + + {b Dépendences : Platform} + *) + +open GDraw + +(** Formats de sauvegarde pris en compte *) +type format_capture = PNG | GIF | JPEG | TIFF | BMP | PPM | POSTSCRIPT + +(** Formats disponibles suivant qu'on est sous Unix/Windows *) +val formats_capture_dispos : format_capture list + +(** [is_format_capture_dispo format] indique si le format est disponible *) +val is_format_capture_dispo : format_capture -> bool + +(** Etats lors de la sauvegarde *) +type progress_save = INIT | SAVING | FINISHED + +(** [extended_string_of_format_capture format] fournit une chaine correspondant + au format : [PNG] -> "PNG"; [POSTSCRIPT] -> "Postscript" *) +val extended_string_of_format_capture : format_capture -> string + +(** [string_of_format_capture format] fournit une chaine correspondant + au format : [PNG] -> "PNG"; [POSTSCRIPT] -> "PS" *) +val string_of_format_capture : format_capture -> string + +(** [format_capture_of_string chaine] renvoie le type {!Gtk_image.format_capture} + correspondant à la chaine *) +val format_capture_of_string : string -> format_capture + +(** [string_of_extension format] renvoie l'extension correspondant au format. + i.e [PNG] -> ".png" *) +val string_of_extension : format_capture -> string + +(** [set_filename_extension fichier format] ajoute l'extension correspondant au + format à la chaine [fichier] si nécessaire *) +val set_filename_extension : string -> format_capture -> string + +(** [update_extension_capture fichier ancien_format nouveau_format] modifie + l'extension du fichier pour qu'elle corresponde à [nouveau_format] *) +val update_extension_capture : + string -> format_capture -> format_capture -> string + +(** [capture_part pixmap_ou_fenetre x y largeur hauteur fichier format progress_func] + effectue la capture partielle à partir de [pixmap_ou_fenetre]. [x] et [y] + designent le coin en haut à gauche et [largeur] et [hauteur] la taille de + l'image à capturer *) +val capture_part : + [> `drawable ] Gobject.obj -> int -> int -> int -> int -> + string -> format_capture -> (progress_save -> float -> unit) option -> unit + +(** [capture_part_with_caption fenetre pixmap_ou_fenetre x y largeur hauteur + fichier format progress_func legende] + effectue la capture partielle à partir de [pixmap_ou_fenetre]. [x] et [y] + designent le coin en haut à gauche et [largeur] et [hauteur] la taille de + l'image à capturer. Une legende est ajoutée sur la capture si + [legende=Some (texte_legende, couleur_texte, couleur_contour, couleur_fond, + fonte)]. + [fenetre] designe ici la fenetre mère du drawable [pixmap_ou_fenetre] + *) +val capture_part_with_caption : + < misc : < visual_depth : int; window : Gdk.window; .. >; .. > -> + Gdk.pixmap -> + int -> + int -> + int -> + int -> + string -> + format_capture -> + (progress_save -> float -> unit) option -> + (string * GDraw.color * GDraw.color * GDraw.color * Gdk.font) option -> + unit + +(** [capture_complete fenetre pixmap_ou_fenetre largeur hauteur + fichier format progress_func legende_optionnelle] sauvegarde tout le contenu + de [pixmap_ou_fenetre] *) +val capture_complete : + < misc : < visual_depth : int; window : Gdk.window; .. >; .. > -> + Gdk.pixmap -> + int -> + int -> + string -> + format_capture -> + (progress_save -> float -> unit) option -> + (string * GDraw.color * GDraw.color * GDraw.color * Gdk.font) option -> + unit + +(** [gtk_image_load filename window format] charge une image au format [format] + et renvoie une pixmap contenant l'image lue *) +val gtk_image_load : string -> GWindow.window -> format_capture -> GDraw.pixmap diff --git a/sw/lib/ocaml/gtk_tools.ml b/sw/lib/ocaml/gtk_tools.ml new file mode 100644 index 00000000000..2b5edf626af --- /dev/null +++ b/sw/lib/ocaml/gtk_tools.ml @@ -0,0 +1,3199 @@ +(* + * $Id$ + * + * Lablgtk2 utils + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(* Numero de version *) +let version = "4.10" + +(* ================================================================================== *) +(* = Version : 4.10 = *) +(* = Derniere update : 26/08/2004 = *) +(* = = *) +(* = 26/08/2004 : Fonctions de creation de fenetres on top = *) +(* = v4.10 = *) +(* = 10/08/2004 : Nouvelles fonctions de modification des couleurs de widgets = *) +(* = v4.09 = *) +(* = 09/08/2004 : Ajout de rectangle_pixmap = *) +(* = v4.08 = *) +(* = 30/07/2004 : Ajout de set_columns_sizes = *) +(* = v4.07 = *) +(* = 16/12/2003 : Scroll des adjustments avec scroll_adjustement. = *) +(* = v4.06 Utilisation de cette fonction pour scroller la fenetre d'affichage= *) +(* = d'un fichier (gtk_tools_display_file), les listes et les text_edit= *) +(* = 03/11/2003 : Ajout de la molette souris = *) +(* = v4.05 = *) +(* = 01/10/2003 : Utilisation de lablgtk-20030326 = *) +(* = v4.04 Encapsulation de GToolbox.popup dans popup car les = *) +(* = coordonnees x et y d'avant ne servent en fait a rien = *) +(* = 25/08/2003 : text_entry_select_text entry = *) +(* = v4.03 = *) +(* = 04/08/2003 : Widget de selection de fonte : select_font_dlg = *) +(* = v4.02 = *) +(* = 29/07/2003 : Modif a create_option_menu qui renvoie une seconde fct = *) +(* = v4.01 permettant de mettre a jour l'option et d'activer le menu = *) +(* = correspondant = *) +(* = Alignement du texte dans les boutons et padding = *) +(* = Padding dans un label = *) +(* = but_set_width et but_set_height = *) +(* = 24/07/2003 : but_set_label, set_widget_front_color et = *) +(* = v4.0 set_button_front_color = *) +(* = 23/07/2003 : create_stipple_pixmap_from_data = *) +(* = v3.99 = *) +(* = 22/07/2003 : calendar et calendar_window = *) +(* = v3.98 set_widget_back_color = *) +(* = create_sized_label_align* et set_label_align* = *) +(* = 21/07/2003 : calendar = *) +(* = v3.97 = *) +(* = 18/06/2003 : create_text_edit, text_edit_clear = *) +(* = text_edit_get_text et text_edit_get_lines = *) +(* = create_spaced_vframe et create_spaced_hframe = *) +(* = 16/06/2003 : animated_msg_box = *) +(* = 12/06/2003 : Ajout des fonctions de creation de menus qui peuvent etre actives = *) +(* = v3.94 ou desactives = *) +(* = 27/05/2003 : Fonctions de creation de menus = *) +(* = v3.93 = *) +(* = 07/04/2003 : get_widget_size widget = *) +(* = v3.92 = *) +(* = 03/03/2003 : Eval_string modifiee pour les noms finissant par 'x' = *) +(* = v3.91 Le callback de selection des managed lists recoit (row, col, dbl) = *) +(* = au lieu du double click = *) +(* = 26/02/2003 : connect_win_focus_change = *) +(* = v3.9 = *) +(* = 10/02/2003 : Passage des rgb<->color dans gtk_draw = *) +(* = v3.8 = *) +(* = 05/02/2003 : int_spinner_connect/gtk_tools_float_spinner_connect = *) +(* = v3.7 = *) +(* = 30/01/2003 : check_dbl_click = *) +(* = 29/01/2003 : pixmap_from_file = *) +(* = 28/01/2003 : Correction du widget latlon : le menu N/S ou E/W n'etait pas a sa = *) +(* = bonne valeur au depart si lat<0 ou lon<0 = *) +(* = Les fonctions de mise en pointilles marchent mal par defaut. = *) +(* = il faut ajouter le parametre width pour que ca marche correctement= *) +(* = 22/01/2003 : color_to_rgb et rgb_to_color, = *) +(* = create_hom_hbox et create_spaced_hbox = *) +(* = 21/01/2003 : create_infos_win = *) +(* = 15/01/2003 : create_vslider_simple, slider_connect et = *) +(* = create_vslider = *) +(* = 21/11/2002 : ajout de create_togglebutton, = *) +(* = create_managed_list, connect_managed_list, = *) +(* = create_buttons et create_buttons_connect = *) +(* = create_hpaned, create_vpaned = *) +(* = 20/11/2002 : create_notebook et notebook_add_page = *) +(* = 19/11/2002 : ajout de create_ops_compare et = *) +(* = create_time_select = *) +(* = 31/10/2002 : ajout de check_overwrite a la boite de selection de fichier = *) +(* = et ajout de question_box = *) +(* = 21/10/2002 : set_cursor = *) +(* = 17/10/2002 : list_connect_up_down_keys = *) +(* = 15/10/2002 : create_hbox/vbox = *) +(* = 11/10/2002 : create_progress_bar = *) +(* = 26/09/2002 : list_connect_check_dbl_click = *) +(* = 18/09/2002 : connect_popup_menu, connect_func_popup_menu = *) +(* = 16/09/2002 : modifs a create_list_(with_hor_scroll) = *) +(* = 13/09/2002 : create_modal_window = *) +(* = 11/09/2002 : display_file = *) +(* = 11/09/2002 : create_window_with_menubar_help = *) +(* = 05/09/2002 : show_log,gtk_tools_hide_log,gtk_tools_add_log = *) +(* = 04/09/2002 : create_optionmenu = *) +(* = 03/09/2002 : disconnect = *) +(* = 18/07/2002 : create_bbox = *) +(* = 18/07/2002 : create_button, create_(sized_)label = *) +(* = 18/07/2002 : create_hframe, create_frame -> create_vframe = *) +(* = 17/07/2002 : get_screen_size = *) +(* = 17/07/2002 : get_registered_window = *) +(* = 17/07/2002 : get_window_geometry, window_modify_connect = *) +(* = et set_window_position = *) +(* = 16/07/2002 : create_list_with_hor_scroll = *) +(* = 11/07/2002 : create_latlon_selection, latlon_selection_get = *) +(* = update_latlon_selection et = *) +(* = latlon_selection_change = *) +(* = 10/07/2002 : create_text_entry_simple, text_entry_connect = *) +(* = et text_entry_connect_modify = *) +(* = 10/07/2002 : create_list/gtk_tools_list_connect = *) +(* = 10/07/2002 : register_window/gtk_tools_show_registered_window = *) +(* = 08/07/2002 : area_key_connect et modif de area_connect en = *) +(* = area_mouse_connect = *) +(* = 08/07/2002 : create_radiobuttons = *) +(* = 16/05/2002 : screenshot_box + screenshot_box_with_caption = *) +(* = 14/05/2002 : create_color_selection_button = *) +(* = 14/05/2002 : create_scrolled_box et change_scrolled_box = *) +(* = 20/04/2002 : create_text_entry = *) +(* = 19/04/2002 : create_int_spinner et create_float_spinner = *) +(* = 19/04/2002 : create_checkbutton_simple, create_checkbutton = *) +(* = 19/04/2002 : Modif dans insert_timer = *) +(* = 23/01/2002 : string_width_height = *) +(* = 23/01/2002 : legende dans screenshot_box = *) +(* = 17/01/2002 : create_pixbutton = *) +(* = 17/01/2002 : create_frame = *) +(* = 17/01/2002 : select_colors = *) +(* = 16/01/2002 : set_sensitive & set_sensitive_list = *) +(* = 16/01/2002 : error_box : boite de message d'erreur = *) +(* = 15/01/2002 : screenshot_box : boite de capture d'ecran = *) +(* = 14/01/2002 : create_popup_menu : ajout de separateurs = *) +(* = 14/01/2002 : set_widget_font = *) +(* = 10/01/2002 : create_draw_area_simple/gtk_tools_area_connect = *) +(* = 07/01/2002 : boite de selection de couleur = *) +(* = 04/01/2002 : passage a lablgtk version 1.2.3 (anciennement 1.2.1) = *) +(* = 02/01/2002 : window_set_front = *) +(* = = *) +(* ================================================================================== *) + +(* Modules Gtk/Gdk *) +open GMain +open GdkKeysyms +open Gdk +open GToolbox (* Acces a message_box *) + +(* Modules locaux *) +open Platform +open Ocaml_tools +open Gtk_tools_icons + +(* ============================================================================= *) +(* = Force la mise a jour de l'interface = *) +(* ============================================================================= *) +let force_update_interface () = + while Glib.Main.pending () do ignore(Glib.Main.iteration false) done + +(* ============================================================================= *) +(* = Initialisation des couleurs = *) +(* ============================================================================= *) +let init_colors () = + (* Mise en place des couleurs correctes *) + Gdk.Rgb.init () ; + GtkBase.Widget.set_default_visual (Gdk.Rgb.get_visual ()) ; + GtkBase.Widget.set_default_colormap (Gdk.Rgb.get_cmap ()) + +(* ============================================================================= *) +(* = Lancement de la mainloop pour l'interface = *) +(* ============================================================================= *) +let main_loop () = Main.main () + +(* ============================================================================= *) +(* = Initialisation des tooltips = *) +(* ============================================================================= *) +let init_tooltips () = GData.tooltips () + +(* ============================================================================= *) +(* = Ajout d'un tooltip a un widget = *) +(* = = *) +(* = widget = le bouton concerne = *) +(* = texte = le texte de l'aide contextuelle = *) +(* ============================================================================= *) +let add_tooltips tooltips widget texte = + (tooltips:GData.tooltips)#set_tip widget#coerce ~text:texte + +(* ============================================================================= *) +(* = Renvoie la taille de l'ecran = *) +(* ============================================================================= *) +let get_screen_size () = (Gdk.Screen.width (), Gdk.Screen.height ()) + +(* ============================================================================= *) +(* = Deconnexion d'un signal = *) +(* = = *) +(* = wid = l'objet concerne = *) +(* = id = identifiant recu lors de la connexion du signal = *) +(* ============================================================================= *) +let disconnect wid id = wid#misc#disconnect id + +(* ============================================================================= *) +(* = Activation/Desactivation d'un widget = *) +(* = = *) +(* = widget = le widget a modifier = *) +(* = sensitive = true si active, false sinon = *) +(* ============================================================================= *) +let set_sensitive widget sensitive = + ignore(widget#misc#set_sensitive sensitive) + +(* ============================================================================= *) +(* = Activation/Desactivation d'une liste de widgets = *) +(* = = *) +(* = widget_list = la liste des widgets a modifier = *) +(* = sensitive = true si active, false sinon = *) +(* ============================================================================= *) +let set_sensitive_list widget_list sensitive = + List.iter (fun widget -> set_sensitive widget sensitive) widget_list + +(* ============================================================================= *) +(* = Modification du curseur dans une fenetre = *) +(* = = *) +(* = window = la fenetre Gdk.window concernee = *) +(* = cursor = le curseur (cree avec Gdk.Cursor.create) = *) +(* ============================================================================= *) +let set_cursor window cursor = Gdk.Window.set_cursor window cursor + +(* ============================================================================= *) +(* = Taille d'un widget = *) +(* = widget = le widget dont on desire connaitre la taille = *) +(* ============================================================================= *) +let get_widget_size widget = + let rect = widget#misc#allocation in (rect.Gtk.width, rect.Gtk.height) + +(* Boutons evenements souris *) +type bouton_souris = B_GAUCHE | B_DROIT | B_MILIEU | B_NONE + +(* ============================================================================= *) +(* = Test d'un bouton souris apres un evenement de type click = *) +(* = = *) +(* = event = l'evenement souris = *) +(* ============================================================================= *) +let test_mouse_but event = + match GdkEvent.Button.button event with + 1 -> B_GAUCHE | 2 -> B_MILIEU | 3 -> B_DROIT |_ -> B_NONE + +(* ============================================================================= *) +(* = Renvoie la position de la souris apres un evenement de type click = *) +(* = = *) +(* = event = l'evenement souris = *) +(* ============================================================================= *) +let get_mouse_pos_click event = + (int_of_float (GdkEvent.Button.x event), int_of_float (GdkEvent.Button.y event)) + +(* ============================================================================= *) +(* = Renvoie la position de la souris apres un evenement de type deplacement = *) +(* = = *) +(* = event = l'evenement souris = *) +(* ============================================================================= *) +let get_mouse_pos_move event = + (int_of_float (GdkEvent.Motion.x event), int_of_float (GdkEvent.Motion.y event)) + +(* ============================================================================= *) +(* = Fonction de test d'un double click = *) +(* = = *) +(* = event = l'evenement concerne = *) +(* ============================================================================= *) +let check_dbl_click event = + match GdkEvent.get_type event with `TWO_BUTTON_PRESS -> true | _ -> false + +(* ============================================================================= *) +(* = Renvoie la fonte fixed = *) +(* ============================================================================= *) +let get_fixed_font () = + if platform_is_unix then Gdk.Font.load_fontset "fixed" + else Gdk.Font.load_fontset + "-misc-arial-medium-r-normal--13-120-75-75-c-80-iso8859-1" + +let get_fixed_font2 () = + Gdk.Font.load_fontset "-misc-fixed-medium-r-normal--13-120-75-75-c-80-iso8859-1" + +(* ============================================================================= *) +(* = Modification de la fonte d'un widget = *) +(* = = *) +(* = w = le widget = *) +(* = font = la fonte a appliquer = *) +(* ============================================================================= *) +let set_widget_font w font = + let style = w#misc#style#copy in + style#set_font font ; + ignore(w#misc#set_style style) + +(* ============================================================================= *) +(* = Modification de la couleur d'un widget = *) +(* = = *) +(* = w = le widget = *) +(* = color = la nouvelle couleur = *) +(* ============================================================================= *) +let set_widget_back_color w color = + let style = w#misc#style#copy in + style#set_bg [`NORMAL, color]; + ignore(w#misc#set_style style) +let set_widget_front_color w color = + let style = w#misc#style#copy in + style#set_fg [`NORMAL, color]; + ignore(w#misc#set_style style) + +let set_entry_back_color w color = + let style = w#misc#style#copy in + style#set_base [`NORMAL, color] ; + ignore(w#misc#set_style style) +let set_entry_front_color w color = + let style = w#misc#style#copy in + style#set_text [`NORMAL, color] ; + ignore(w#misc#set_style style) +let set_entry_outline_color w color = + set_widget_back_color w color + +let set_button_back_color w color = + let style = w#misc#style#copy in + style#set_bg [`NORMAL, color; `PRELIGHT, color] ; + ignore(w#misc#set_style style) +let set_button_front_color w color = + set_widget_front_color (List.hd w#children) color + +(* Pour le fond d'un label il faut une event box au dessus *) + +(* ============================================================================= *) +(* = Modification de l'alignement d'un widget de type label = *) +(* = = *) +(* = label = le widget = *) +(* = pos = la valeur de l'alignement (entre 0. et 1.) = *) +(* ============================================================================= *) +let set_label_align label pos = label#set_xalign pos + +let set_label_align_left label = set_label_align label 0. +let set_label_align_right label = set_label_align label 1. +let set_label_align_center label = set_label_align label 0.5 + +let set_label_padding label xpad = label#set_xpad xpad + +(* ============================================================================= *) +(* = Taille en pixel de la chaine de caractere dans la fonte indiquee = *) +(* = = *) +(* = font = la fonte = *) +(* = string = la chaine de caracteres = *) +(* ============================================================================= *) +let string_width_height font string = + (Gdk.Font.string_width font string, Gdk.Font.string_height font string) + +(* ============================================================================= *) +(* = Creation d'une boite a boutons = *) +(* = = *) +(* = pack_method = ou mettre la boite = *) +(* ============================================================================= *) +let create_bbox pack_method = + GPack.button_box `HORIZONTAL ~border_width:5 ~packing:pack_method + ~layout:`SPREAD ~spacing:5 () + +(* ============================================================================= *) +(* = Creation d'une boite horizontale = *) +(* = = *) +(* = pack_method = ou mettre la boite = *) +(* ============================================================================= *) +let create_hbox pack_method = GPack.hbox ~packing:pack_method () +let create_hom_hbox pack_method = + GPack.hbox ~homogeneous:true ~packing:pack_method () +let create_spaced_hbox pack_method = + GPack.hbox ~spacing:5 ~border_width:5~packing:pack_method () +let create_hom_spaced_hbox pack_method = + GPack.hbox ~homogeneous:true ~spacing:5 ~border_width:5~packing:pack_method () + +(* ============================================================================= *) +(* = Creation d'une boite verticale = *) +(* = = *) +(* = pack_method = ou mettre la boite = *) +(* ============================================================================= *) +let create_vbox pack_method = GPack.vbox ~packing:pack_method () +let create_hom_vbox pack_method = + GPack.vbox ~homogeneous:true ~packing:pack_method () +let create_spaced_vbox pack_method = + GPack.vbox ~spacing:5 ~border_width:5~packing:pack_method () +let create_hom_spaced_vbox pack_method = + GPack.vbox ~homogeneous:true ~spacing:5 ~border_width:5~packing:pack_method () + +(* ============================================================================= *) +(* = Creation d'une frame contenant une vbox = *) +(* = = *) +(* = title = titre de la frame = *) +(* = pack_method = ou mettre la frame = *) +(* = Renvoie la frame et la vbox = *) +(* ============================================================================= *) +let create_vframe title pack_method = + let fr = GBin.frame ~label:title ~packing:pack_method () in + (fr, create_vbox fr#add) + +let create_spaced_vframe title pack_method = + let fr = GBin.frame ~label:title ~packing:pack_method () in + (fr, create_spaced_vbox fr#add) + +(* ============================================================================= *) +(* = Creation d'une frame contenant une hbox = *) +(* = = *) +(* = title = titre de la frame = *) +(* = pack_method = ou mettre la frame = *) +(* = Renvoie la frame et la vbox = *) +(* ============================================================================= *) +let create_hframe title pack_method = + let fr = GBin.frame ~label:title ~packing:pack_method () in + (fr, create_hbox fr#add) + +let create_spaced_hframe title pack_method = + let fr = GBin.frame ~label:title ~packing:pack_method () in + (fr, create_spaced_hbox fr#add) + +(* ============================================================================= *) +(* = Creation d'une zone scrollable contenant une boite a widgets = *) +(* = = *) +(* = pack_method = ou mettre la zone scrollable = *) +(* ============================================================================= *) +let create_scrolled_box pack_method = + let scrolled_window = GBin.scrolled_window ~border_width: 10 + ~hpolicy: `AUTOMATIC ~packing:pack_method () in + (scrolled_window, create_vbox scrolled_window#add_with_viewport) + +(* ============================================================================= *) +(* = Destruction et recreation d'une boite verticale dans une zone scrollable = *) +(* = = *) +(* = scrolled_window = zone scrollable a modifier = *) +(* = old_box = ancienne boite contenue dans cette zone = *) +(* ============================================================================= *) +let change_scrolled_box scrolled_window old_box = + old_box#destroy () ; + create_vbox scrolled_window#add_with_viewport + +(* ============================================================================= *) +(* = Creation d'un widget de type notebook = *) +(* = = *) +(* = pack_method = ou mettre le widget = *) +(* ============================================================================= *) +let create_notebook pack_method = + GPack.notebook ~scrollable:true ~packing:pack_method () + +(* ============================================================================= *) +(* = Ajout d'une page a un notebook = *) +(* = = *) +(* = notebook = le notebook ou ajouter la page = *) +(* = page_label = nom de la page = *) +(* ============================================================================= *) +let notebook_add_page notebook page_label = + let lbl = GMisc.label ~text:page_label () in + let f = GBin.frame ~packing:((notebook:GPack.notebook)#append_page + ~tab_label:lbl#coerce) () in + (f, create_vbox f#add) + +(* ============================================================================= *) +(* = Creation d'un paned (division mobile entre deux zones d'une fenetre) = *) +(* = = *) +(* = pack_method = ou mettre le widget = *) +(* ============================================================================= *) +let create_hpaned pack_method = + GPack.paned ~packing:pack_method `HORIZONTAL () + +let create_vpaned pack_method = + GPack.paned ~packing:pack_method `VERTICAL () + +(* ============================================================================= *) +(* = Creation d'un bouton = *) +(* = = *) +(* = label = texte du bouton = *) +(* = pack_method = ou mettre le bouton = *) +(* ============================================================================= *) +let create_button label pack_method = + GButton.button ~label:label ~packing:pack_method () + +(* ============================================================================= *) +(* = Creation d'un bouton avec une taille fixee = *) +(* = = *) +(* = label = texte du bouton = *) +(* = width = taille du bouton = *) +(* = pack_method = ou mettre le bouton = *) +(* ============================================================================= *) +let create_sized_button label width pack_method = + let but = GButton.button ~label:label ~packing:pack_method () in + but#misc#set_size_request ~width:width () ; + but +(* GTK2 AAA GButton.button ~label:label ~width:width ~packing:pack_method () *) + +(* ============================================================================= *) +(* = Modification du texte dans un bouton = *) +(* = = *) +(* = button = le bouton = *) +(* = label = texte du bouton = *) +(* ============================================================================= *) +let but_set_label button label = + (GMisc.label_cast (List.hd button#children))#set_text label + +let set_button_align button pos = + set_label_align (GMisc.label_cast (List.hd button#children)) pos +let set_button_align_left button =set_button_align button 0. +let set_button_align_right button =set_button_align button 1. +let set_button_align_center button=set_button_align button 0.5 + +let set_button_padding button xpad = + set_label_padding (GMisc.label_cast (List.hd button#children)) xpad + +let but_set_width button width = + (button:GButton.button)#misc#set_size_request ~width () +(* GTK2 AAA GtkBase.Container.set (GtkButton.Button.cast button#as_widget) ~width:width*) +let but_set_height button height = + (button:GButton.button)#misc#set_size_request ~height () +(* GTK2 AAA GtkBase.Container.set (GtkButton.Button.cast button#as_widget) ~height:height*) + +(* ============================================================================= *) +(* = Connexion d'une fonction a un bouton = *) +(* = = *) +(* = but = le bouton = *) +(* = func = la fonction = *) +(* ============================================================================= *) +let but_connect but func = ignore(but#connect#clicked ~callback:func) + +(* ============================================================================= *) +(* = Creation d'une rangee de boutons = *) +(* = = *) +(* = lst_buts = liste des (noms, tooltips) = *) +(* = tooltips = aide contextuelle = *) +(* = pack_method = ou mettre les boutons = *) +(* ============================================================================= *) +let create_buttons lst_buts tooltips pack_method = + let bbox = create_bbox pack_method in + List.map (fun (text, tip) -> + let b = create_button text bbox#add in + add_tooltips tooltips b tip; + b) lst_buts + +(* ============================================================================= *) +(* = Association de callbacks a une liste de boutons = *) +(* = = *) +(* = lst_buts = liste des boutons creee avec la fonction precedente = *) +(* = lst_callbacks = callbacks associes = *) +(* ============================================================================= *) +let create_buttons_connect lst_buts lst_callbacks = + List.iter2 (fun b c -> but_connect b c) lst_buts lst_callbacks + +(* ============================================================================= *) +(* = Creation d'un check button sans callback = *) +(* = = *) +(* = active = valeur active/inactif au depart = *) +(* = label = texte du label = *) +(* = pack_method = ou mettre les widgets = *) +(* = tip = texte de l'aide contextuelle = *) +(* = tooltips = aide contextuelle = *) +(* ============================================================================= *) +let create_checkbutton_simple active label pack_method tip tooltips = + let check = GButton.check_button ~label:label ~active:active + ~packing:pack_method () in + if tip <> "" then add_tooltips tooltips check tip ; + check + +(* ============================================================================= *) +(* = Creation d'un check button avec callback = *) +(* = = *) +(* = active = valeur active/inactif au depart = *) +(* = label = texte du label = *) +(* = pack_method = ou mettre les widgets = *) +(* = tip = texte de l'aide contextuelle = *) +(* = tooltips = aide contextuelle = *) +(* = callback = callback appele lors de la modification du bouton = *) +(* ============================================================================= *) +let create_checkbutton active label pack_method tip tooltips callback = + let check = create_checkbutton_simple active label pack_method + tip tooltips in + but_connect check (fun () -> callback check#active) ; + check + +(* ============================================================================= *) +(* = Creation d'une suite de radio buttons = *) +(* = = *) +(* = lst_names = liste des noms des boutons avec leurs types (nom, type) = *) +(* = func_active = fonction indiquant quel est le bouton actif par defaut = *) +(* = pack_method = ou mettre les widgets = *) +(* = = *) +(* = Renvoie la liste des boutons crees (pour ajout de tooltips par exemple) = *) +(* ============================================================================= *) +let create_radiobuttons_simple lst_names func_active pack_method = + let lst_but = List.fold_left (fun lst (name, typ) -> + let but = + if lst = [] then GButton.radio_button ~label:name ~packing:pack_method () + else GButton.radio_button ~label:name ~packing:pack_method + ~group:(List.hd lst)#group () + in + but#set_active (func_active typ) ; + lst @ [but]) [] lst_names in + + lst_but + +(* ============================================================================= *) +(* = Connexion de callbacks a des radiobuttons = *) +(* = = *) +(* = lst_names = liste des noms des boutons avec leurs types (nom, type) = *) +(* = lst_but = liste des radiobuttons = *) +(* = func_select = fonction appelee lors de la modification du bouton actif = *) +(* ============================================================================= *) +let radiobuttons_connect lst_names lst_but func_select = + List.iter2 (fun but (_, typ) -> + ignore((but:GButton.radio_button)#connect#clicked ~callback:(fun () -> + func_select typ))) lst_but lst_names + +(* ============================================================================= *) +(* = Creation d'une suite de radio buttons = *) +(* = = *) +(* = lst_names = liste des noms des boutons avec leurs types (nom, type) = *) +(* = func_active = fonction indiquant quel est le bouton actif par defaut = *) +(* = func_select = fonction appelee lors de la modification du bouton actif = *) +(* = pack_method = ou mettre les widgets = *) +(* = = *) +(* = Renvoie la liste des boutons crees (pour ajout de tooltips par exemple) = *) +(* ============================================================================= *) +let create_radiobuttons lst_names func_active func_select pack_method = + let lst_but = create_radiobuttons_simple + lst_names func_active pack_method in + + (* Connexion de la fonction de selection *) + radiobuttons_connect lst_names lst_but func_select ; + lst_but + +(* ============================================================================= *) +(* = Creation d'un toggle button = *) +(* = = *) +(* = label = texte du bouton = *) +(* = active = indique l'etat initial du bouton = *) +(* = pack_method = ou mettre le widget = *) +(* ============================================================================= *) +let create_togglebutton label active pack_method = + GButton.toggle_button ~label:label ~active:active ~packing:pack_method () + +(* ============================================================================= *) +(* = Creation d'un bouton contenant une pixmap = *) +(* = = *) +(* = pm = la pixmap a mettre dans le bouton = *) +(* = pack_method = ou mettre le bouton = *) +(* ============================================================================= *) +let create_pixbutton pm pack_method = + let but = GButton.button ~packing:pack_method () in + ignore(GMisc.pixmap (pm:GDraw.pixmap) ~packing:but#add ()) ; + but + +(* ============================================================================= *) +(* = Creation d'un label = *) +(* = = *) +(* = label = texte du label = *) +(* = pack_method = ou mettre le label = *) +(* ============================================================================= *) +let create_label label pack_method = + GMisc.label ~text:label ~packing:pack_method () + +(* ============================================================================= *) +(* = Creation d'un label ayant une taille fixee = *) +(* = = *) +(* = label = texte du label = *) +(* = width = largeur du label en pixels = *) +(* = pack_method = ou mettre le label = *) +(* ============================================================================= *) +let create_sized_label label width pack_method = + GMisc.label ~text:label ~width:width ~packing:pack_method () + +let create_sized_label_align label width pos pack_method = + GMisc.label ~text:label ~width:width ~packing:pack_method ~xalign:pos () +let create_sized_label_align_left label width pack_method = + GMisc.label ~text:label ~width:width ~packing:pack_method ~xalign:0. () +let create_sized_label_align_right label width pack_method = + GMisc.label ~text:label ~width:width ~packing:pack_method ~xalign:1. () + +(* ============================================================================= *) +(* = Boite de question = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = question_msg = message a afficher = *) +(* = default_is_cancel = indique si c'est le bouton annuler qui est le defaut = *) +(* ============================================================================= *) +let question_box window title question_msg default_is_cancel = + let pm = GDraw.pixmap_from_xpm_d ~data:question_icon ~window:window () in + let icon = (GMisc.pixmap pm ())#coerce in + (* Fonction de remplacement d'une extension *) + let reponse = question_box ~title:title ~buttons:["Oui"; "Annuler"] + ~default:(if default_is_cancel then 2 else 1) + ~icon:icon question_msg in + (* Renvoie vrai si "Oui" est selectionne *) + reponse=1 + +(* ============================================================================= *) +(* = Boite de message d'erreur = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = error_msg = message d'erreur a afficher = *) +(* ============================================================================= *) +let error_box window title error_msg = + let pm = GDraw.pixmap_from_xpm_d ~data:warning_icon ~window:window () in + let icon = (GMisc.pixmap pm ())#coerce in + message_box ~title:title ~icon:icon error_msg + +(* ============================================================================= *) +(* = Boite de message avec icone anime = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = error_msg = message d'erreur a afficher = *) +(* ============================================================================= *) +let animated_msg_box title msg lst_pixmaps = + let window = GWindow.dialog ~modal:true ~title:title () in + + let max_pixmaps = List.length lst_pixmaps in + let l = List.map (fun f -> + GDraw.pixmap_from_xpm ~file:f ~window:window ()) lst_pixmaps in + let t = Array.of_list l in + + let hb = GPack.hbox ~border_width:10 ~packing:window#vbox#add () in + ignore (create_label msg (hb#pack ~from:`END)); + let hbox = ref (create_hbox hb#pack) in + + (* Fonction de mise a jour du pixmap dans la boite de dialogue *) + let create pm = + (!hbox)#destroy () ; + hbox := create_hbox hb#pack ; + (!hbox)#pack (GMisc.pixmap pm ())#coerce ~padding:4 ; + in + create t.(0) ; + + let b = create_button "OK" + (window#action_area#pack ~expand:true ~padding:4) in + but_connect b window#destroy ; + b#grab_default () ; + window#set_position `CENTER; + window#show (); + + (* Timeout de l'animation *) + let current_idx = ref 0 in + let timeout _ = + incr current_idx ; + if !current_idx>=max_pixmaps then current_idx:=0 ; + create t.(!current_idx) ; + true + in + let timeoutid = Timeout.add ~ms:100 ~callback:timeout in + + (* On stoppe le timeout quand la boite de dialogue est fermee *) + ignore (window#connect#destroy ~callback: (fun () -> + Timeout.remove timeoutid ; GMain.Main.quit ())) ; + + GMain.Main.main () + +(* ============================================================================= *) +(* = Ouverture d'une boite de dialogue de selection de fichier. On teste = *) +(* = l'existence du fichier si c'est demande = *) +(* = = *) +(* = title : titre de la fenetre de selection = *) +(* = read_func : fonction de lecture du fichier apres selection = *) +(* = update_func : None ou Some f, fonction appelee apres read_func = *) +(* = default_filename : nom selectionne par defaut si <> "" = *) +(* = check_overwrite : indique si on teste l'existence du fichier = *) +(* ============================================================================= *) +let open_file_dlg title read_func update_func default_filename + check_overwrite = + let do_select_file filename = + (* Appel a la fonction de lecture du fichier *) + read_func filename; + (* Appel a la fonction de mise a jour si besoin *) + match update_func with None -> () | Some f -> f () + in + + (* Creation de la boite de dialogue *) + let fs = GWindow.file_selection ~modal:true ~title:title () in + + let check_file_overwrite_ok filename = + try + let stat = Unix.stat filename in + if stat.Unix.st_kind = Unix.S_REG then begin + question_box fs + "Le fichier existe" "Le fichier existe, continuer ?" false + end else true + with + Unix.Unix_error (Unix.ENOENT, _, _) -> true + | _ -> false + in + + ignore(fs#ok_button#connect#clicked ~callback:(fun () -> + (* Recuperation du nom du fichier *) + let filename = fs#filename in + if not check_overwrite or (check_file_overwrite_ok filename) then begin + fs#destroy () ; + do_select_file filename + end)) ; + + ignore(fs#cancel_button#connect#clicked ~callback:fs#destroy) ; + (* Mise a jour du nom de fichier par defaut *) + if default_filename <> "" then fs#set_filename default_filename ; + fs#show () + +type timer_type = + TIMER_TIME (* Heure uniquement *) + | TIMER_DATE (* Date uniquement *) + | TIMER_TIME_AND_DATE (* Affichage de l'heure et de la date *) + +(* ============================================================================= *) +(* = Insertion d'un timer dans un label = *) +(* = = *) +(* = label = le widget label ou afficher le timer = *) +(* = timer_type = TIMER_TIME, TIMER_DATE ou TIMER_TIME_AND_DATE = *) +(* = force_beginning = true si active lance des le depart = *) +(* ============================================================================= *) +let insert_timer label timer_type force_beginning = + (* Fonction appelee *) + let timeout () = + let tm = timer_get_time () in + let val_time = + match timer_type with + TIMER_TIME -> timer_string_of_time tm + | TIMER_DATE -> timer_string_of_date tm + | TIMER_TIME_AND_DATE -> (timer_string_of_time tm) ^ " - " ^ + (timer_string_of_date tm) + in + + (label:GMisc.label)#set_text val_time; + true in + + (* Mise en place du timer toutes les 1000 millisecondes *) + ignore(Timeout.add ~ms:1000 ~callback:timeout) ; + + (* Si c'est demande, on force l'affichage des le debut *) + if force_beginning then ignore(timeout ()) + +(* ============================================================================= *) +(* = Encapsulation de GToolbox.popup = *) +(* ============================================================================= *) +let popup menu_entries = + GToolbox.popup_menu ~button:0 ~time:Int32.zero ~entries:menu_entries + +(* ============================================================================= *) +(* = Connexion d'un popup menu a un bouton = *) +(* = = *) +(* = wid = le wiget concerne = *) +(* = button = bouton a presser pour afficher le menu = *) +(* = test_cond_func = fonction testant si le menu doit etre affiche = *) +(* = menu_entries = entrees a afficher dans le menu = *) +(* ============================================================================= *) +let connect_popup_menu wid button test_cond_func menu_entries = + ignore(wid#event#connect#button_press ~callback:(fun ev -> + if test_cond_func () && + (test_mouse_but ev) = button && + GdkEvent.get_type ev = `BUTTON_PRESS then begin + popup menu_entries ; true + end else false)) + +(* ============================================================================= *) +(* = Connexion d'un popup menu a un bouton. Le menu est construit de maniere = *) +(* = dynamique, il provient de l'appel a une fonction = *) +(* = = *) +(* = wid = le wiget concerne = *) +(* = button = bouton a presser pour afficher le menu = *) +(* = test_cond_func = fonction testant si le menu doit etre affiche = *) +(* = get_menu_entries = fonction renvoyant les entrees a afficher dans le menu = *) +(* ============================================================================= *) +let connect_func_popup_menu wid button test_cond_func get_menu_entries = + ignore(wid#event#connect#button_press ~callback:(fun ev -> + if test_cond_func () && + (test_mouse_but ev) = button && + GdkEvent.get_type ev = `BUTTON_PRESS then begin + popup (get_menu_entries ()) ; true + end else false)) + +(* ============================================================================= *) +(* = Creation d'un menu la ou se trouve la souris = *) +(* = = *) +(* = title = titre du menu ("" si pas de titre) = *) +(* = event = evenement de type click souris = *) +(* = data = liste de parametres du type (texte, Some fonction, param) ou = *) +(* = texte designe le texte correspondant au sous-menu et fonction la fonction = *) +(* = a appeler apres selection avec le parametre param = *) +(* ============================================================================= *) +let create_popup_menu title event data = + let button = GdkEvent.Button.button event and + time = GdkEvent.Button.time event and + menu = GMenu.menu ~show:true () in + + (* Fonction d'ajout de sous-menus *) + let attache_menu (texte, fonction, param, sous_menu) = + (* Ajout d'un separateur ou d'un sous-menu ? *) + if texte = "" then ignore(GMenu.menu_item ~packing:menu#append ()) + else begin + let ssmenu = GMenu.menu_item ~label:texte ~packing:menu#append () in + begin + match fonction with + None -> () + | Some f -> ignore(ssmenu#connect#activate ~callback:(fun () -> f param)) + end ; + + (* Ajout d'un sous-menu *) + if sous_menu <> [] then begin + let m = GMenu.menu () in + List.iter (fun (t, func, param) -> + let ss_m = GMenu.menu_item ~label:t ~packing:m#append () in + match func with + None -> () + | Some f -> ignore(ss_m#connect#activate ~callback:(fun () -> f param)) + ) sous_menu ; + ssmenu#set_submenu m + end + end + in + + if title <> "" then begin + (* Ajout du titre *) + attache_menu (title, None, "", []) ; + (* Ajout d'un separateur apres le titre *) + attache_menu ("", None, "", []) ; + end ; + List.iter attache_menu data ; + + (* Affichage du popup-menu *) + menu#popup ~button:button ~time:time + +(* ============================================================================= *) +(* = Creation d'un menu a options = *) +(* = = *) +(* = lst_names = liste des noms des options avec leurs types (nom, type) = *) +(* = func_active = fonction indiquant quelle est l'option active par defaut = *) +(* = func_select = fonction appelee lors de la modification de l'option = *) +(* = pack_method = ou mettre le menu = *) +(* = = *) +(* = Renvoie le menu et la fonction de selection d'une option = *) +(* ============================================================================= *) +let create_optionmenu lst_names func_active func_select pack_method = + (* Creation des options *) + let menu = GMenu.menu () in + let lst_menus = + List.map (fun (nom, typ) -> + let m = GMenu.menu_item ~label:nom ~packing: menu#append () in + ignore (m#connect#activate ~callback:(fun () -> func_select typ)) ; + m) lst_names in + + (* Creation de l'option menu *) + let optionmenu = GMenu.option_menu ~packing:pack_method () in + optionmenu#set_menu menu ; + + (* Recherche du numero du menu de l'option par defaut *) + let n = ref 0 and idx = ref 0 in + List.iter (fun (_, typ) -> if func_active typ then n:=!idx; incr idx) lst_names ; + optionmenu#set_history !n ; + + let set_option typ = + let n = ref 0 and idx = ref 0 in + List.iter (fun (_, typ0) -> if typ0=typ then n:=!idx; incr idx) lst_names ; + optionmenu#set_history !n ; + in + + let set_option_and_activate typ = + let n = ref 0 and idx = ref 0 in + List.iter2 (fun (_, typ0) m -> + if typ0=typ then begin n:=!idx; m#activate () end ; + incr idx) lst_names lst_menus ; + optionmenu#set_history !n ; + in + + (optionmenu, set_option, set_option_and_activate) + +(* ============================================================================= *) +(* = Pixmap d'ouverture d'un fichier = *) +(* ============================================================================= *) +let open_file_pixmap = + [|(* width height num_colors chars_per_pixel *) + " 20 19 5 1"; + (* colors *) + ". c None";"# c #000000";"i c #ffffff";"s c #7f7f00";"y c #ffff00"; + (* pixels *) + "...................."; "...................."; "...................."; + "...........###......"; "..........#...#.#..."; "...............##..."; + "...###........###..."; "..#yiy#######......."; "..#iyiyiyiyi#......."; + "..#yiyiyiyiy#......."; "..#iyiy###########.."; "..#yiy#sssssssss#..."; + "..#iy#sssssssss#...."; "..#y#sssssssss#....."; "..##sssssssss#......"; + "..###########......."; "...................."; "...................."; + "...................." |] + +(* ============================================================================= *) +(* = Creation d'une GDraw.pixmap a partir d'un fichier xpm = *) +(* = = *) +(* = filename = nom du fichier xpm = *) +(* = window = fenetre = *) +(* ============================================================================= *) +let pixmap_from_file filename window = + GDraw.pixmap_from_xpm ~file:filename ~window:(window:GWindow.window) () + +(* ============================================================================= *) +(* = Creation d'une pixmap = *) +(* = = *) +(* = window = fenetre mere = *) +(* = width = largeur en pixels de la pixmap = *) +(* = height = hauteur en pixels de la pixmap = *) +(* = = *) +(* = En retour, pix sert a mettre la pixmap dans une zone de dessin par ex. = *) +(* = et pixmap sert au dessin. = *) +(* ============================================================================= *) +let create_pixmap window width height = + let depth = (window:GWindow.window)#misc#visual_depth and w = window#misc#window in + let pix = Pixmap.create ~window:w ~width:width ~height:height ~depth:depth () in + let pixmap = new GDraw.pixmap pix in + (pix, pixmap) + +let create_d_pixmap window width height = + let depth = (window:GWindow.window)#misc#visual_depth and w = window#misc#window in + let pix = Pixmap.create ~window:w ~width:width ~height:height ~depth:depth () in + let pixmap = new GDraw.drawable pix in + (pix, pixmap) + +(* ============================================================================= *) +(* = Fonction de creation d'une fenetre avec une boite verticale = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = width = largeur de la fenetre = *) +(* = height = hauteur de la fenetre = *) +(* ============================================================================= *) +let create_window title width height = + let window = + if width = 0 && height = 0 then + GWindow.window ~border_width: 1 ~title () + else + GWindow.window ~width ~height ~border_width: 1 ~title () + in + (window, create_vbox window#add) + +(* ============================================================================= *) +(* = Fonction de creation d'une fenetre modale avec une boite verticale = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = width = largeur de la fenetre = *) +(* = height = hauteur de la fenetre = *) +(* ============================================================================= *) +let create_modal_window title width height = + let window = + if width = 0 && height = 0 then + GWindow.window ~modal:true ~border_width: 1 ~title () + else + GWindow.window ~modal:true ~width ~height ~border_width: 1 ~title () + in + (window, create_vbox window#add) + +(* ============================================================================= *) +(* = Fonction de creation d'une fenetre avec une boite verticale. De plus, la = *) +(* = fenetre contient une barre de menu = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = width = largeur de la fenetre = *) +(* = height = hauteur de la fenetre = *) +(* = menubar_items = liste de chaines contenant les titres des menus = *) +(* ============================================================================= *) +let create_window_with_menubar title width height menubar_items = + let (window, vbox) = create_window title width height in + let menubar = GMenu.menu_bar ~packing:vbox#pack () in + let factory = new GMenu.factory menubar in + let menus = List.map (fun str -> factory#add_submenu str) menubar_items in + + (window, vbox, factory, factory#accel_group, Array.of_list menus) + +(* ============================================================================= *) +(* = Fonction de creation d'une fenetre avec une boite verticale. De plus, la = *) +(* = fenetre contient une barre de menu et un menu a gauche pour l'aide = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = width = largeur de la fenetre = *) +(* = height = hauteur de la fenetre = *) +(* = menubar_items = liste de chaines contenant les titres des menus = *) +(* ============================================================================= *) +let create_window_with_menubar_help title width height menubar_items = + let (window, vbox) = create_window title width height in + let hbox = create_hbox vbox#pack in + let menubar = GMenu.menu_bar ~packing:hbox#add () in + let factory = new GMenu.factory menubar in + let menus = List.map (fun str -> factory#add_submenu str) menubar_items in + + let menubar_hlp = GMenu.menu_bar ~packing:(hbox#pack ~from:`END) () in + let factory_hlp = new GMenu.factory menubar_hlp in + let menu_help = factory_hlp#add_submenu "Aide" in + + (window, vbox, factory, factory#accel_group, Array.of_list menus, menu_help) + +(* ============================================================================= *) +(* = Passage en avant-plan d'une fenetre = *) +(* = = *) +(* = window = la fenetre a passer en avant-plan = *) +(* ============================================================================= *) +let window_set_front window = + (* On est oblige de masquer la fenetre avant de la montrer pour etre *) + (* sur qu'elle passe en avant plan. Show tout seul ne suffit pas... *) + GtkBase.Widget.hide (window:GWindow.window)#as_window ; + ignore(window#show ()) + +(* ============================================================================= *) +(* = Renvoie la geometrie d'une fenetre = *) +(* = = *) +(* = window = la fenetre concernee = *) +(* ============================================================================= *) +let get_window_geometry window = + let pos = Window.get_position (window:GWindow.window)#misc#window and + size = Drawable.get_size window#misc#window in + (pos, size) + +(* ============================================================================= *) +(* = Fixe la position x, y d'une fenetre (la deplace si besoin) = *) +(* = = *) +(* = window = la fenetre concernee = *) +(* = (x, y) = la nouvelle position = *) +(* ============================================================================= *) +let set_window_position window (x, y) = + GtkBase.Widget.set_uposition (window:GWindow.window)#as_window ~x:x ~y:y + +(* ============================================================================= *) +(* = Connexion d'un callback lors du deplacement ou de la modification d'une = *) +(* = fenetre = *) +(* = = *) +(* = window = la fenetre concernee = *) +(* = callback = le callback a appeler = *) +(* ============================================================================= *) +let window_modify_connect window callback = + (window:GWindow.window)#event#connect#configure ~callback:(fun ev -> + callback (get_window_geometry window) ; + true) + +(* ============================================================================= *) +(* = Connecte les callbacks lies aux evenements focus_in et focus_out = *) +(* = = *) +(* = window = la fenetre concernee = *) +(* = focus_in = fonction appelee lorsque la souris entre dans le widget = *) +(* = focus_out = fonction appelee lorsque la souris sort du widget = *) +(* ============================================================================= *) +let connect_win_focus_change window focus_in focus_out = + let win = (window:GWindow.window) in + (match focus_in with + None -> () + | Some f -> ignore(win#event#connect#focus_in (fun _ -> f (); false))) ; + (match focus_out with + None -> () + | Some f -> ignore(win#event#connect#focus_out (fun _ -> f (); false))) + +(* ============================================================================= *) +(* = Creation d'une zone de dessin simple (i.e pas de callback associe) = *) +(* = = *) +(* = width = hauteur de la zone = *) +(* = height = hauteur de la zone = *) +(* = pack_method = maniere de placer la zone (ex. : hbox#pack) = *) +(* = pix_expose = pixmap a utiliser pour redessiner apres un event expose = *) +(* ============================================================================= *) +let create_draw_area_simple width height pack_method pix_expose = + (* Creation des widgets *) + let area = GMisc.drawing_area ~width:width ~height:height ~packing:pack_method () in + let drawing = area#misc#realize (); new GDraw.drawable (area#misc#window) in + + (* Creation du event expose : remise en place de la pixmap dans la zone de dessin *) + let area_expose () = drawing#put_pixmap ~x:0 ~y:0 pix_expose in + ignore(area#event#connect#expose ~callback:(fun _ -> area_expose () ; false)) ; + + (* Renvoie la fonction de reaffichage et la zone de dessin *) + (area, area_expose) + +(* ============================================================================= *) +(* = Connexion evenements souris a une zone de dessin = *) +(* = = *) +(* = area = la zone de dessin (create_draw_area_simple) = *) +(* = mouse_press = fonction appelee lors d'un click souris = *) +(* = mouse_move = fonction appelee lors d'un deplacement = *) +(* = mouse_release = fonction appelee lors du relachement d'un bouton = *) +(* ============================================================================= *) +let area_mouse_connect area mouse_press mouse_move mouse_release = + (area:GMisc.drawing_area)#event#add + [`POINTER_MOTION; `BUTTON_PRESS; `BUTTON_RELEASE] ; + area#event#set_extensions `ALL; + ignore(area#event#connect#button_press ~callback:mouse_press) ; + ignore(area#event#connect#motion_notify ~callback:mouse_move) ; + ignore(area#event#connect#button_release ~callback:mouse_release) + +(* ============================================================================= *) +(* = Connexion evenements clavier a une zone de dessin = *) +(* = = *) +(* = area = la zone de dessin (create_draw_area_simple) = *) +(* = key_press = fonction appelee lors de l'appui sur une touche = *) +(* = key_release = fonction appelee lors du relachement d'une touche = *) +(* ============================================================================= *) +let area_key_connect area key_press key_release = + (* Par defaut l'evenement key_release n'est pas associe au widget *) + (area:GMisc.drawing_area)#event#add [`KEY_RELEASE] ; + ignore(area#event#connect#key_press + ~callback:(fun ev -> key_press (GdkEvent.Key.keyval ev))) ; + ignore(area#event#connect#key_release + ~callback:(fun ev -> key_release (GdkEvent.Key.keyval ev))) ; + area#misc#set_can_focus true ; + area#misc#grab_focus () + +(* ============================================================================= *) +(* = Creation d'une zone de dessin avec callbacks = *) +(* = = *) +(* = width : largeur de la zone = *) +(* = height : hauteur de la zone = *) +(* = pack_method : ou mettre la zone (ex : hbox#pack) = *) +(* = pix_expose : pix servant au dessin dans la zone = *) +(* = mouse_press : callback click souris (fun event -> ... ; true) = *) +(* = mouse_move : callback deplacement = *) +(* = mouse_release : callback bouton relache = *) +(* = = *) +(* = En retour, la fonction area_expose permet de forcer l'affichage dans = *) +(* = la zone de dessin = *) +(* ============================================================================= *) +let create_draw_area width height pack_method pix_expose + mouse_press mouse_move mouse_release = + let (area, area_expose) = create_draw_area_simple width height + pack_method pix_expose in + (* Connexion des eventuels signaux generes par la souris *) + area_mouse_connect area mouse_press mouse_move mouse_release ; + + (* Renvoie la fonction de reaffichage *) + area_expose + +(* ============================================================================= *) +(* = Boite de selection d'une couleur = *) +(* = = *) +(* = update_func = la fonction appelee apres selection. La couleur selectionnee= *) +(* = lui est alors passee en parametre = *) +(* ============================================================================= *) +let select_color update_func = + let csd = GWindow.color_selection_dialog ~modal:true ~title:"Selection couleur" () in + ignore(csd#cancel_button#connect#clicked ~callback:csd#destroy); + ignore(csd#ok_button#connect#clicked ~callback:(fun () -> + let color = csd#colorsel#color in + (* Destruction dela fenetre apres selection *) + csd#destroy () ; + update_func (`RGB (Color.red color, Color.green color, Color.blue color)))) ; + csd#show () + +(* ============================================================================= *) +(* = Bouton de couleur permettant la selection d'une autre couleur = *) +(* = = *) +(* = window = fenetre mere = *) +(* = taille_x = largeur du rectangle indiquant la couleur = *) +(* = taille_y = hauteur = *) +(* = color = couleur initiale = *) +(* = pack_method = ou mettre le bouton = *) +(* = callback = fonction appelee apres modification de la couleur = *) +(* ============================================================================= *) +let create_color_selection_button window taille_x taille_y color + pack_method callback = + (* Le pixmap contenant la couleur courante *) + let pm = GDraw.pixmap ~window:window ~width:taille_x ~height:taille_y () in + (* Mise a jour de la couleur initiale *) + (pm:GDraw.pixmap)#set_foreground color ; + (* Dessin avec cette couleur dans le pixmap *) + pm#rectangle ~filled:true ~x:0 ~y:0 ~width:taille_x ~height:taille_y () ; + (* Creation d'un bouton contenant ce pixmap *) + let but = create_pixbutton pm pack_method in + (* Connexion du callback *) + but_connect but (fun () -> select_color callback) ; + + but + +let create_color_selection_button2 window taille_x taille_y color + pack_method callback = + (* Le pixmap contenant la couleur courante *) + let pm = GDraw.pixmap ~window:window ~width:taille_x ~height:taille_y () in + (* Mise a jour de la couleur initiale *) + (pm:GDraw.pixmap)#set_foreground color ; + (* Dessin avec cette couleur dans le pixmap *) + pm#rectangle ~filled:true ~x:0 ~y:0 ~width:taille_x ~height:taille_y () ; + (* Creation d'un bouton contenant ce pixmap *) + let but = create_pixbutton pm pack_method in + (* Connexion du callback *) + but_connect but (fun () -> select_color + (fun c -> pm#set_foreground c ; + pm#rectangle ~filled:true ~x:0 ~y:0 ~width:taille_x ~height:taille_y () ; + callback c)) ; + + but + +(* ============================================================================= *) +(* = Routine interne de creation d'une boite de selection de couleurs = *) +(* ============================================================================= *) +let scw window colors tooltips update_func vbox destroy_func = + (* Sauvegarde des valeurs initiales des couleurs en cas d'annulation *) + let save_colors = + let f c (_, lst) = c @ (List.map (fun (_, couleur, _) -> !couleur) lst) in + Array.of_list(List.fold_left f [] colors) + in + + let taille_x = 40 and taille_y = 10 + and clicked_apply = ref false and application_auto = ref true in + + (vbox:GPack.box)#set_spacing 10 ; + let (scrolled_window, v) = create_scrolled_box vbox#add in + let vb = ref v in + + (* Fonction de creation d'une boite contenant un label de titre *) + (* et un bouton de selection de couleur dont la couleur est color *) + let create_boite v title color callback = + let hbox = create_hbox (v:GPack.box)#pack in + let lab = create_label title hbox#pack and + but = create_color_selection_button (window:GWindow.window) + taille_x taille_y color (hbox#pack ~from:`END) callback in + but + in + + (* Creation des boites de selection de couleurs, pour toutes les frames *) + let rec creation_liste () = + (* Destruction de la liste precedente et recreation dans la scrolled_window *) + vb := change_scrolled_box scrolled_window !vb ; + + List.iter (fun (nom, lst) -> + let v = snd (create_vframe nom !vb#pack) in + let do_boites (title, couleur, _) = + let b = create_boite v title !couleur + (fun color -> couleur := color; + (* Recreation de toute la liste pour mise a jour de la couleur de la boite *) + creation_liste () ; + (* Application automatique ? *) + if !application_auto then begin + clicked_apply := true ; update_func () + end) in + () + in + List.iter do_boites lst) colors + in + + (* Application (i.e redessin par update_func) apres chaque modif ? *) + let check = GButton.check_button ~label:"Appliquer automatiquement" + ~active: !application_auto ~packing:vbox#pack () in + but_connect check (fun () -> application_auto := check#active) ; + + (* Boutons OK/Annulation, ... *) + let hbox = create_hom_hbox vbox#pack in + let but_ok = create_button "Ok" hbox#pack and + but_apply = create_button "Appliquer" hbox#pack and + but_default = create_button "Defaut" hbox#pack and + but_cancel = create_button "Annuler" hbox#pack in + + (* Association de l'aide contextuelle si possible *) + begin + match tooltips with + None -> () + | Some t -> + add_tooltips t but_ok "Valide les changements" ; + add_tooltips t but_apply "Applique les changements" ; + add_tooltips t but_default "Couleurs par defaut" ; + add_tooltips t but_cancel "Annulation des changements" ; + add_tooltips t check "Application des qu'une modification est effectuee" + end ; + + (* Connexion des callbacks des boutons *) + but_connect but_ok (fun () -> + (match destroy_func with None -> () | Some f -> f ()); + if not !application_auto then update_func ()) ; + but_connect but_apply (fun () -> clicked_apply := true; update_func()) ; + but_connect but_default (fun () -> + (* Valeurs par defaut *) + let f (_, lst) = List.iter + (fun (_, couleur, couleur_def) -> couleur := couleur_def) lst in + List.iter f colors ; + clicked_apply := true ; + (* Mise a jour de la liste *) + creation_liste (); + (* Si application automatique on redessine *) + if !application_auto then update_func ()) ; + + but_connect but_cancel (fun () -> + (* Restauration des couleurs initiales *) + let idx = ref 0 in + let f (_, lst) = List.iter (fun (_, couleur, _) -> + couleur := save_colors.(!idx); incr idx) lst in + List.iter f colors ; + (match destroy_func with None -> () | Some f -> f ()); + (* Application des parametres anterieurs si necessaire *) + if !clicked_apply then update_func ()) ; + + (* Premier affichage de la liste des couleurs *) + creation_liste () + +(* ============================================================================= *) +(* = Widget de selection de plusieurs couleurs = *) +(* = = *) +(* = colors = liste de (nom_frame, [(label, couleur ref, couleur_defaut)])= *) +(* = tooltips = None ou (Some tooltips) = *) +(* = update_func = la fonction d'application des nouvelles couleurs = *) +(* = vbox = ou mettre le widget = *) +(* ============================================================================= *) +let select_colors_widget window colors tooltips update_func vbox = + scw window colors tooltips update_func vbox None + +(* ============================================================================= *) +(* = Boite de selection de plusieurs couleurs = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = colors = liste de (nom_frame, [(label, couleur ref, couleur_defaut)])= *) +(* = tooltips = None ou (Some tooltips) = *) +(* = update_func = la fonction d'application des nouvelles couleurs = *) +(* ============================================================================= *) +let select_colors title width height colors tooltips update_func = + (* Creation de la fenetre *) + let (window, vbox) = create_window title width height in + scw window colors tooltips update_func vbox + (Some (fun () -> window#destroy ())); + window#show () + +(* ============================================================================= *) +(* = Fonction interne de creation d'une boite de capture d'ecran = *) +(* ============================================================================= *) +let creation_fen_capture default_filename default_format tooltips with_caption = + let filename = ref default_filename and format = ref default_format in + filename := Gtk_image.set_filename_extension !filename default_format ; + + let lst_formats = [Gtk_image.PNG; Gtk_image.JPEG; Gtk_image.POSTSCRIPT; + Gtk_image.TIFF; Gtk_image.BMP; Gtk_image.PPM] in + (* Correction du format par defaut suivant qu'il est disponible ou pas *) + (* sur l'architecture courante *) + let default_format = + if Gtk_image.is_format_capture_dispo default_format then default_format else begin + let rec f lst = + match lst with + typ::reste -> + if Gtk_image.is_format_capture_dispo typ then typ else f reste + | [] -> default_format + in + let typ = f lst_formats in + filename := Gtk_image.update_extension_capture !filename !format typ ; + typ + end + in + format := default_format ; + + let get_caption = ref (fun () -> None) in + + (* Creation de la boite de dialogue *) + let height = if with_caption then 250 else 210 in + let window = GWindow.dialog ~title:"Capture d'ecran" + ~border_width:10 ~width:350 ~height:height () in + let vbox = window#vbox in + + (* Legende *) + let taille_x = 20 and taille_y = 10 in + + if with_caption then begin + let color_caption = ref (`RGB(65535, 65535, 65535)) and + contour_color = ref (`RGB(65535, 65535, 65535)) and + back_color = ref (`RGB(0, 0, 0)) in + + let (fr, hbox) = create_hframe "Legende" vbox#pack in + let entry_caption = GEdit.entry ~text:"" ~packing:hbox#add () and + hb = ref (create_hbox (hbox#pack ~from:`END)) in + + (* Fonction de creation d'un bouton de selection d'une couleur *) + let create_but col help_text pack_method redraw_func = + let but = create_color_selection_button window taille_x taille_y !col + pack_method (fun color -> col := color; redraw_func ()) in + match tooltips with + None -> () + | Some t -> add_tooltips t but help_text + in + + (* Creation de tous les boutons de selection des couleurs de la legende *) + let rec create_boutons () = + !hb#destroy () ; + hb := create_hbox (hbox#pack ~from:`END) ; + create_but contour_color "Couleur du texte de la legende" + !hb#pack create_boutons; + create_but back_color "Couleur du fond de la boite de legende" + !hb#pack create_boutons; + create_but color_caption "Couleur du contour de la boite de legende" + !hb#pack create_boutons; + in + + create_boutons () ; + set_sensitive !hb false ; + + (* Masquage des boutons de selection de couleur si pas de legende *) + ignore(entry_caption#connect#changed ~callback:(fun () -> + set_sensitive !hb (entry_caption#text <> ""))) ; + (match tooltips with + None -> () | Some t -> add_tooltips t entry_caption + "Texte de la legende (rien=pas de legende)") ; + + (* Recuperation de la legende *) + get_caption := fun () -> + let caption = entry_caption#text in + if caption = "" then None + else Some (caption, !color_caption, !contour_color, !back_color, + get_fixed_font2 ()) + end ; + + (* Selection du format de sauvegarde *) + let (fr, hbox) = create_hframe "Format" vbox#pack in + let lst_but = List.fold_left (fun lst typ -> + let name = Gtk_image.string_of_format_capture typ in + let but = + if lst = [] then GButton.radio_button ~label:name ~packing:hbox#add () + else GButton.radio_button ~label:name ~packing:hbox#add ~group:(List.hd lst)#group () + in + but#set_active (typ = default_format) ; + set_sensitive but (Gtk_image.is_format_capture_dispo typ) ; + begin + match tooltips with + None -> () + | Some t -> + add_tooltips t but + ("Capture au format "^(Gtk_image.extended_string_of_format_capture typ)) + end ; + lst @ [but]) [] lst_formats in + + (* Nom du fichier *) + let pm = GDraw.pixmap_from_xpm_d ~data:open_file_pixmap ~window:window () and + (fr, hbox) = create_hframe "Nom du fichier" vbox#pack in + let entry = GEdit.entry ~text: !filename ~packing:hbox#add () and + but_fic = create_pixbutton pm hbox#pack in + + (* Progression de la sauvegarde *) + let (fr, vb) = create_vframe "Sauvegarde" vbox#pack in + let hbox = create_hbox vb#pack in + let l = create_label " Etat : " hbox#pack and + lab_save = create_label "" hbox#add in + + let hbox = create_hbox vb#pack in + let l = create_label " Progression : " hbox#pack and + pbar_save = GRange.progress_bar ~packing:hbox#add () in + pbar_save#set_fraction 0. ; + + let but_ok = create_button "Capture" window#action_area#add and + but_cancel = create_button "Fermer" window#action_area#add in + but_connect but_cancel (fun () -> window#destroy ()) ; + but_ok#grab_default (); + + (* Fonction appelee lors de la sauvegarde pour afficher la progression *) + let current_progress = ref "" in + let progression_save etape compteur = + begin + match etape with + Gtk_image.INIT -> lab_save#set_text "Sauvegarde en cours..." ; + | Gtk_image.SAVING -> lab_save#set_text "Sauvegarde en cours..." ; + | Gtk_image.FINISHED -> lab_save#set_text "OK" ; + end ; + let p = Printf.sprintf "%.0f%%" (100.*.compteur) in + (* Comme ca on n'affiche que tous les 1%. Sinon c'est tres lent avec GTK2 *) + if !current_progress<>p then begin + pbar_save#set_text p ; + pbar_save#set_fraction compteur ; force_update_interface () ; + current_progress:=p + end + in + + begin + match tooltips with + None -> () + | Some t -> + add_tooltips t but_ok "Effectue la capture d'ecran" ; + add_tooltips t but_cancel "Ferme la fenetre" ; + add_tooltips t entry "Nom du fichier de capture" ; + add_tooltips t but_fic "Selection d'un nom de fichier" ; + end ; + + (* Connexion des callbacks *) + List.iter2 (fun b typ -> + ignore(b#connect#clicked ~callback:(fun () -> + filename := Gtk_image.update_extension_capture !filename !format typ ; + format := typ ; + entry#set_text !filename))) lst_but lst_formats ; + + but_connect but_fic (fun () -> + open_file_dlg "Fichier image" (fun f -> + filename := Gtk_image.set_filename_extension f !format ; + entry#set_text !filename) + None !filename true) ; + + ignore(entry#connect#changed ~callback:(fun () -> filename:=entry#text)) ; + + (* On renvoie les divers parametres et widgets *) + (filename, format, Some progression_save, window, but_ok, entry, !get_caption) + +(* ============================================================================= *) +(* = Boite de capture d'ecran sans legende = *) +(* = = *) +(* = default_filename = nom du fichier par defaut = *) +(* = default_format = format selectionne par defaut (cf capture.ml) = *) +(* = drawable = zone (fenetre ou pixmap) contenant le dessin = *) +(* = tooltips = None ou Some t = *) +(* = x = coordonnee x du point de depart dans la zone = *) +(* = y = coordonnee y de ce point = *) +(* = width = largeur de la zone capturee = *) +(* = height = hauteur de la zone capturee = *) +(* ============================================================================= *) +let screenshot_box default_filename default_format drawable + tooltips x y width height = + + let (filename, format, progression_save, window, but_ok, entry, _) = + creation_fen_capture default_filename default_format tooltips false in + + let do_capture () = + if (String.length !filename)>0 then + Gtk_image.capture_part drawable x y width height !filename !format progression_save + in + + but_connect but_ok do_capture ; + + ignore(entry#connect#activate ~callback:(fun () -> + filename:=Gtk_image.set_filename_extension !filename !format ; + entry#set_text !filename ; + do_capture ())) ; + + (* Affichage de la fenetre *) + window#show () + +(* ============================================================================= *) +(* = Boite de capture d'ecran sans legende qui lance une fonction apres la = *) +(* = capture = *) +(* = = *) +(* = default_filename = nom du fichier par defaut = *) +(* = default_format = format selectionne par defaut (cf capture.ml) = *) +(* = drawable = zone (fenetre ou pixmap) contenant le dessin = *) +(* = tooltips = None ou Some t = *) +(* = x = coordonnee x du point de depart dans la zone = *) +(* = y = coordonnee y de ce point = *) +(* = width = largeur de la zone capturee = *) +(* = height = hauteur de la zone capturee = *) +(* = after_func = fonction a lancer apres la capture = *) +(* ============================================================================= *) +let screenshot_box_with_func default_filename default_format drawable + tooltips x y width height after_func = + + let (filename, format, progression_save, window, but_ok, entry, _) = + creation_fen_capture default_filename default_format tooltips false in + + let do_capture () = + if (String.length !filename)>0 then + Gtk_image.capture_part drawable x y width height !filename !format + progression_save ; + after_func () + in + + but_connect but_ok do_capture ; + + ignore(entry#connect#activate ~callback:(fun () -> + filename:=Gtk_image.set_filename_extension !filename !format ; + entry#set_text !filename ; + do_capture ())) ; + + (* Affichage de la fenetre *) + window#show () + +(* ============================================================================= *) +(* = Boite de capture d'ecran avec legende = *) +(* = Attention, ne fonctionne qu'avec des pixmaps a cause de la legende = *) +(* = = *) +(* = default_filename = nom du fichier par defaut = *) +(* = default_format = format selectionne par defaut (cf capture.ml) = *) +(* = window = la fenetre mere de la zone suivante = *) +(* = drawable = zone (pixmap uniquement) contenant le dessin = *) +(* = tooltips = None ou Some t = *) +(* = x = coordonnee x du point de depart dans la zone = *) +(* = y = coordonnee y de ce point = *) +(* = width = largeur de la zone capturee = *) +(* = height = hauteur de la zone capturee = *) +(* ============================================================================= *) +let screenshot_box_with_caption default_filename default_format + drawable tooltips x y width height = + + let (filename, format, progression_save, window, but_ok, entry, get_caption) = + creation_fen_capture default_filename default_format tooltips true in + + let do_capture () = + if (String.length !filename)>0 then + Gtk_image.capture_part_with_caption window drawable x y width height !filename !format + progression_save (get_caption()) + in + + but_connect but_ok do_capture ; + + ignore(entry#connect#activate ~callback:(fun () -> + filename:=Gtk_image.set_filename_extension !filename !format ; + entry#set_text !filename ; + do_capture ())) ; + + (* Affichage de la fenetre *) + window#show () + +(* ============================================================================= *) +(* = Creation d'une zone de selection de valeur simple (sans callback) = *) +(* = = *) +(* = label = texte du label = *) +(* = lab_width = taille du label = *) +(* = init_value = valeur initiale du texte dans la zone d'entree (chaine) = *) +(* = min_value = valeur min admissible = *) +(* = max_value = valeur max admissible = *) +(* = value_width = taille de la zone d'entree = *) +(* = step_incr = increment lie aux fleches haut/bas = *) +(* = page_incr = increment lie a page up/page down = *) +(* = tip = texte de l'aide contextuelle = *) +(* = tooltips = aide contextuelle = *) +(* = pack_method = ou mettre les widgets = *) +(* ============================================================================= *) +let create_int_spinner_simple label lab_width init_value min_value + max_value value_width step_incr page_incr tip tooltips pack_method = + let hbox = create_hbox pack_method in + let l = create_sized_label label lab_width hbox#pack and + spinner = GEdit.spin_button + ~adjustment:(GData.adjustment ~value:(float_of_int init_value) + ~lower:(float_of_int min_value) ~upper:(float_of_int max_value) + ~step_incr:(float_of_int step_incr) + ~page_incr:(float_of_int page_incr) ~page_size:0.0 ()) + ~rate:0. ~digits:0 ~width:value_width () in + hbox#pack spinner#coerce ; + if tip <> "" then add_tooltips tooltips spinner tip ; + spinner + +(* ============================================================================= *) +(* = Connexion d'un callback a un spinner = *) +(* = = *) +(* = sp = le spinner concerne = *) +(* = callback = le callback a associer = *) +(* ============================================================================= *) +let int_spinner_connect sp callback = + ignore((sp:GEdit.spin_button)#connect#value_changed ~callback:(fun () -> + try let new_value = sp#value_as_int in callback new_value + with Failure("int_of_string") -> ())) + (* Necessaire sous GTK2 pour mettre a jour la valeur quand on la *) + (* modifie en changeant directement le texte de l'entry... *) +(* ignore(sp#connect#changed ~callback:(fun () -> sp#update))*) + +(* ============================================================================= *) +(* = Creation d'une zone de selection de valeur avec callback = *) +(* = = *) +(* = label = texte du label = *) +(* = lab_width = taille du label = *) +(* = init_value = valeur initiale du texte dans la zone d'entree (chaine) = *) +(* = min_value = valeur min admissible = *) +(* = max_value = valeur max admissible = *) +(* = value_width = taille de la zone d'entree = *) +(* = step_incr = increment lie aux fleches haut/bas = *) +(* = page_incr = increment lie a page up/page down = *) +(* = tip = texte de l'aide contextuelle = *) +(* = tooltips = aide contextuelle = *) +(* = pack_method = ou mettre les widgets = *) +(* = callback = callback appele lors de la modification de la zone = *) +(* ============================================================================= *) +let create_int_spinner label lab_width init_value min_value max_value + value_width step_incr page_incr tip tooltips pack_method callback = + let spinner = create_int_spinner_simple label lab_width init_value + min_value max_value value_width step_incr page_incr tip tooltips + pack_method in + int_spinner_connect spinner callback ; + spinner + +(* ============================================================================= *) +(* = Creation d'un slider de selection d'une valeur entiere = *) +(* = = *) +(* = init_val = valeur initiale = *) +(* = min_val = valeur mini acceptee = *) +(* = max_val = valeur maxi acceptee = *) +(* = step = valeur du pas d'incrementation = *) +(* = page = valeur du pas d'incrementation d'une page = *) +(* = draw_val = affichage ou pas de la valeur sur le slider = *) +(* = pack_method = ou mettre le widget = *) +(* ============================================================================= *) +let create_vslider_simple init_val min_val max_val step page draw_val + pack_method = + let adj = GData.adjustment ~lower:(float_of_int min_val) + ~upper:(float_of_int (max_val+10)) + ~step_incr:(float_of_int step) ~page_incr:(float_of_int page) () in + let sc = GRange.scale `VERTICAL ~adjustment:adj ~draw_value:draw_val + ~digits:0 ~packing:pack_method () in + adj#set_value (float_of_int init_val) ; + (adj, sc) + +let create_hslider_simple init_val min_val max_val step page draw_val + pack_method = + let adj = GData.adjustment ~lower:(float_of_int min_val) + ~upper:(float_of_int (max_val+10)) + ~step_incr:(float_of_int step) ~page_incr:(float_of_int page) () in + let sc = GRange.scale `HORIZONTAL ~adjustment:adj ~draw_value:draw_val + ~digits:0 ~packing:pack_method () in + adj#set_value (float_of_int init_val) ; + (adj, sc) + +(* ============================================================================= *) +(* = Connexion d'un callback a un slider = *) +(* = = *) +(* = slider = le widget concerne = *) +(* = callback = le callback a associer = *) +(* ============================================================================= *) +let slider_connect slider callback = + (slider:GData.adjustment)#connect#value_changed ~callback:(fun () -> + callback (int_of_float slider#value)) + +(* ============================================================================= *) +(* = Creation d'un slider de selection d'une valeur entiere avec callback = *) +(* = = *) +(* = init_val = valeur initiale = *) +(* = min_val = valeur mini acceptee = *) +(* = max_val = valeur maxi acceptee = *) +(* = step = valeur du pas d'incrementation = *) +(* = page = valeur du pas d'incrementation d'une page = *) +(* = draw_val = affichage ou pas de la valeur sur le slider = *) +(* = pack_method = ou mettre le widget = *) +(* = callback = le callback a associer = *) +(* ============================================================================= *) +let create_vslider init_val min_val max_val step page draw_val + pack_method callback = + let slider = create_vslider_simple init_val min_val max_val + step page draw_val pack_method in + ignore(slider_connect (fst slider) callback) ; + slider + +let create_hslider init_val min_val max_val step page draw_val + pack_method callback = + let slider = create_hslider_simple init_val min_val max_val + step page draw_val pack_method in + ignore(slider_connect (fst slider) callback) ; + slider + +let create_float_spinner_simple label lab_width init_value min_value + max_value value_width nb_digits step_incr page_incr tip tooltips pack_method = + let hbox = create_hbox pack_method in + let l = create_sized_label label lab_width hbox#pack and + spinner = GEdit.spin_button + ~adjustment:(GData.adjustment ~value:init_value + ~lower:min_value ~upper:max_value + ~step_incr:step_incr ~page_incr:page_incr + ~page_size:0.0 ()) + ~rate:0. ~digits:nb_digits ~width:value_width () in + hbox#pack spinner#coerce ; + if tip <> "" then add_tooltips tooltips spinner tip ; + spinner + +let float_spinner_connect sp callback = + ignore((sp:GEdit.spin_button)#connect#value_changed ~callback:(fun () -> + try let new_value = sp#value in callback new_value ; + with Failure("float_of_string") -> ())) + (* Necessaire sous GTK2 pour mettre a jour la valeur quand on la *) + (* modifie en changeant directement le texte de l'entry... *) +(* ignore(sp#connect#changed ~callback:(fun () -> sp#update))*) + +let create_float_spinner label lab_width init_value min_value max_value + value_width nb_digits step_incr page_incr tip tooltips pack_method callback = + let spinner = create_float_spinner_simple label lab_width init_value + min_value max_value value_width nb_digits step_incr page_incr tip tooltips + pack_method in + float_spinner_connect spinner callback ; + spinner + +(* ============================================================================= *) +(* = Creation d'une zone de selection de texte = *) +(* = = *) +(* = label = texte du label = *) +(* = lab_width = taille du label = *) +(* = init_value = valeur initiale du texte dans la zone d'entree (chaine) = *) +(* = value_width = taille de la zone d'entree = *) +(* = tip = texte de l'aide contextuelle = *) +(* = tooltips = aide contextuelle = *) +(* = pack_method = ou mettre les widgets = *) +(* ============================================================================= *) +let create_text_entry_simple label lab_width init_value value_width + tip tooltips pack_method = + let hbox = create_hbox pack_method in + let lab = create_sized_label label lab_width hbox#pack and + entry = GEdit.entry ~text:init_value ~width:value_width ~packing:hbox#pack () in + if tip <> "" then add_tooltips tooltips entry tip ; + (lab, entry) + +(* ============================================================================= *) +(* = Attachement d'un callback lorsqu'on appuie sur entree dans l'entry = *) +(* = = *) +(* = entry = le widget concerne = *) +(* = callback = le callback a appeler = *) +(* ============================================================================= *) +let text_entry_connect entry callback = + (entry:GEdit.entry)#connect#activate + ~callback:(fun () -> callback (String.uppercase entry#text)) + +(* ============================================================================= *) +(* = Attachement d'un callback lorsqu'on modifie le texte dans l'entry = *) +(* = = *) +(* = entry = le widget concerne = *) +(* = callback = le callback a appeler = *) +(* ============================================================================= *) +let text_entry_connect_modify entry callback = + (entry:GEdit.entry)#connect#changed + ~callback:(fun () -> callback (String.uppercase entry#text)) + +(* ============================================================================= *) +(* = Creation d'une zone de selection de texte = *) +(* = = *) +(* = label = texte du label = *) +(* = lab_width = taille du label = *) +(* = init_value = valeur initiale du texte dans la zone d'entree (chaine) = *) +(* = value_width = taille de la zone d'entree = *) +(* = tip = texte de l'aide contextuelle = *) +(* = tooltips = aide contextuelle = *) +(* = pack_method = ou mettre les widgets = *) +(* = callback = callback appele lors de la modification de la zone = *) +(* ============================================================================= *) +let create_text_entry label lab_width init_value value_width + tip tooltips pack_method callback = + let (lab, entry) = create_text_entry_simple label lab_width + init_value value_width tip tooltips pack_method in + ignore(text_entry_connect_modify entry callback) ; + (lab, entry) + +(* Type pour les fenetres enregistrees *) +type registered_win = {reg_win_id : int ; + mutable reg_win_handle : GWindow.window option ; + reg_win_build : unit -> GWindow.window} + +(* Compteur pour les fenetres enregistrees *) +let register_count = ref 0 +(* Tableau contenant les fenetres enregistrees *) +let registered_windows = ref ([||] : registered_win array) + +(* Exception levee lors de l'appel a un numero de fenetre non enregistree *) +exception GTK_TOOLS_UNREGISTERED_WINDOW of int + +(* ============================================================================= *) +(* = Enregistrement d'une fenetre = *) +(* = = *) +(* = build_window_func = la fonction de creation de la fenetre. Cette fonction = *) +(* = doit renvoyer un objet du type GWindow.window = *) +(* = = *) +(* = Renvoie un identifiant (entier) pour cette fenetre = *) +(* ============================================================================= *) +let register_window build_window_func = + let this_win_num = !register_count in + incr register_count ; + + registered_windows := Array.append !registered_windows + [|{reg_win_id = this_win_num; reg_win_handle = None; + reg_win_build = build_window_func}|] ; + + this_win_num + +(* ============================================================================= *) +(* = Appel d'une fenetre enregistree = *) +(* = = *) +(* = id = identifiant de la fenetre (obtenu par register_window) = *) +(* ============================================================================= *) +let show_registered_window id = + if id < 0 or id >= !register_count then + raise (GTK_TOOLS_UNREGISTERED_WINDOW id) ; + + let w = !registered_windows.(id) in + match w.reg_win_handle with + None -> (* La fenetre n'avait pas encore ete creee, on le fait *) + (* Appel de la fonction de creation de la fenetre *) + let win = w.reg_win_build () in + ignore(win#connect#destroy ~callback:(fun _ -> w.reg_win_handle <- None)) ; + w.reg_win_handle <- Some win ; + win#show () + + | Some w -> (* La fenetre etait deja creee, on la met en avant-plan *) + window_set_front w + +(* ============================================================================= *) +(* = Cache (detruit) une fenetre enregistree = *) +(* = = *) +(* = id = identifiant de la fenetre (obtenu par register_window) = *) +(* ============================================================================= *) +let hide_registered_window id = + if id < 0 or id >= !register_count then + raise (GTK_TOOLS_UNREGISTERED_WINDOW id) ; + + let w = !registered_windows.(id) in + match w.reg_win_handle with + None -> () + | Some win -> win#destroy () ; w.reg_win_handle <- None + +(* ============================================================================= *) +(* = Renvoie une fenetre enregistree = *) +(* = = *) +(* = id = identifiant de la fenetre (obtenu par register_window) = *) +(* ============================================================================= *) +let get_registered_window id = + if id < 0 or id >= !register_count then + raise (GTK_TOOLS_UNREGISTERED_WINDOW id) ; + + (!registered_windows.(id)).reg_win_handle + +(* ============================================================================= *) +(* = Fonction effectuant le scrolling d'un adjustment (scrollbar) = *) +(* ============================================================================= *) +let scroll_adjustment adj scroll_up delta = + (* Valeur extremes que peut prendre l'adjustment *) + let val_min = adj#lower and val_max = adj#upper -. adj#page_size in + let current_value = adj#value in + let new_value = + if scroll_up then max val_min (current_value-.delta) + else min val_max (current_value+.delta) + in + if new_value<>current_value then adj#set_value new_value + +(* ============================================================================= *) +(* = Connexion d'un scroll aux mouvements de la molette de la souris = *) +(* ============================================================================= *) +let connect_mouse_wheel_scroll widget sb delta = + let check_wheel ev = + match GdkEvent.Scroll.direction ev with + `UP -> scroll_adjustment sb#adjustment true delta; true + | `DOWN -> scroll_adjustment sb#adjustment false delta; true + | _ -> false + in + + (* Connexion a present identique pour Unix et Windows... *) + ignore(widget#event#connect#scroll ~callback:check_wheel) + +(* ============================================================================= *) +(* = Creation d'un objet clist = *) +(* = = *) +(* = lst_titles = liste des titres de chaque colonne = *) +(* = sortable_titles = tri possible ou pas en cliquant sur les titres ? = *) +(* = first_sort = indique quelle est la colonne triee par defaut = *) +(* = pack_method = ou mettre la liste = *) +(* ============================================================================= *) +let create_list lst_titles (sortable_titles, first_sort) pack_method = + let hb = create_hbox pack_method in + let sb = GRange.scrollbar `VERTICAL ~packing:(hb#pack ~from:`END) () in + let clist = GList.clist ~titles:lst_titles ~shadow_type:`OUT + ~packing:hb#add ~vadjustment:sb#adjustment () in + + if sortable_titles then begin + (* Sens de tri de chaque colonne *) + let sens_tri = Array.mapi (fun i _ -> i <> first_sort) + (Array.of_list lst_titles) in + let select_column column = + let dir = if sens_tri.(column) then `ASCENDING else `DESCENDING in + clist#set_sort ~column:column ~dir:dir () ; + clist#sort () ; + (* Inversion du sens de tri pour la prochaine fois *) + sens_tri.(column) <- not sens_tri.(column) + in + ignore(clist#connect#click_column ~callback:(fun col -> select_column col)) ; + end ; + + (* Autorisation du defilement avec la molette de la souris *) + prerr_endline "TODO (Gtk_tools): connect_mouse_wheel_scroll clist sb 15. ;"; + clist + +(* ============================================================================= *) +(* = Creation d'un objet clist avec scroll horizontal = *) +(* = = *) +(* = lst_titles = liste des titres de chaque colonne = *) +(* = sortable_titles = tri possible ou pas en cliquant sur les titres ? = *) +(* = first_sort = indique quelle est la colonne triee par defaut = *) +(* = pack_method = ou mettre la liste = *) +(* ============================================================================= *) +let create_list_with_hor_scroll lst_titles + (sortable_titles, first_sort) pack_method = + let vb = create_vbox pack_method in + let hb = create_hbox vb#add in + let sb = GRange.scrollbar `VERTICAL ~packing:(hb#pack ~from:`END) () in + let sb2 = GRange.scrollbar `HORIZONTAL ~packing:(vb#pack ~from:`END) () in + let clist = GList.clist ~titles:lst_titles ~shadow_type:`OUT + ~packing:hb#add ~vadjustment:sb#adjustment ~hadjustment:sb2#adjustment () in + if sortable_titles then begin + (* Sens de tri de chaque colonne *) + let sens_tri = Array.mapi (fun i _ -> i <> first_sort) + (Array.of_list lst_titles) in + let select_column column = + let dir = if sens_tri.(column) then `ASCENDING else `DESCENDING in + clist#set_sort ~column:column ~dir:dir () ; + clist#sort () ; + (* Inversion du sens de tri pour la prochaine fois *) + sens_tri.(column) <- not sens_tri.(column) + in + ignore(clist#connect#click_column ~callback:(fun col -> select_column col)) ; + end ; + + (* Autorisation du defilement avec la molette de la souris *) + prerr_endline "TODO (Gtk_tools): connect_mouse_wheel_scroll clist sb 15.;"; + clist + +(* ============================================================================= *) +(* = Ajout de callbacks a un objet clist = *) +(* = = *) +(* = clist = la liste = *) +(* = callback_select = callback de selection d'une ligne = *) +(* = callback_deselect = callback de deselection d'une ligne = *) +(* = callback_select_column = callback de selection d'un titre de colonne = *) +(* ============================================================================= *) +let list_connect clist + callback_select callback_deselect callback_select_column = + (* Selection d'un element *) + (match callback_select with + None -> () + | Some callback -> + ignore((clist:string GList.clist)#connect#select_row ~callback: + (fun ~row ~column ~event -> callback row column))) ; + + (* Deselection d'un element *) + (match callback_deselect with + None -> () + | Some callback -> + ignore(clist#connect#unselect_row ~callback: + (fun ~row ~column ~event -> callback row column))) ; + + (* Click sur un titre *) + (match callback_select_column with + None -> () + | Some callback -> + ignore(clist#connect#click_column ~callback:(fun col -> callback col))) + +(* ============================================================================= *) +(* = Ajout de callbacks a un objet clist avec test du double click = *) +(* = = *) +(* = clist = la liste = *) +(* = callback_select = callback de selection d'une ligne = *) +(* = callback_deselect = callback de deselection d'une ligne = *) +(* = callback_select_column = callback de selection d'un titre de colonne = *) +(* ============================================================================= *) +let list_connect_check_dbl_click clist + callback_select callback_deselect callback_select_column = + let test_dbl_click event = + match event with None -> false | Some ev -> check_dbl_click ev + in + + (* Selection d'un element *) + (match callback_select with + None -> () + | Some callback -> + ignore((clist:string GList.clist)#connect#select_row ~callback: + (fun ~row ~column ~event -> callback row column + (test_dbl_click event)))) ; + + (* Deselection d'un element *) + (match callback_deselect with + None -> () + | Some callback -> + ignore(clist#connect#unselect_row ~callback: + (fun ~row ~column ~event -> callback row column + (test_dbl_click event)))) ; + + (* Click sur un titre *) + (match callback_select_column with + None -> () + | Some callback -> + ignore(clist#connect#click_column ~callback:(fun col -> callback col))) + +(* ============================================================================= *) +(* = Ajout de callbacks d'appui sur une touche dans une clist = *) +(* = = *) +(* = clist = la liste = *) +(* = callback_up = callback appui fleche haut = *) +(* = callback_down = callback appui fleche bas = *) +(* ============================================================================= *) +let list_connect_up_down_keys clist callback_up callback_down = + let key_press key = + if key = _Down then (callback_down (); true) + else if key = _Up then (callback_up (); true) + else false + in + ignore((clist:string GList.clist)#event#connect#key_press + ~callback:(fun ev -> key_press (GdkEvent.Key.keyval ev))) + +(* ============================================================================= *) +(* = Creation d'une liste avec titres+largeurs colonnes+label pour nb d'elts = *) +(* = = *) +(* = title_sizes = liste des (nom, taille) des colonnes = *) +(* = pack_method = ou mettre le widget = *) +(* ============================================================================= *) +let create_managed_list titles_sizes pack_method = + let vb = create_vbox pack_method in + let lab = create_label "" vb#pack in + + (* Creation de la liste *) + let titles = List.map fst titles_sizes in + let lst = create_list titles (true, 0) vb#add in + + let column_sizes = Array.of_list (List.map snd titles_sizes) in + (* Mise a jour de la taille des colonnes *) + Array.iteri (fun i size -> lst#set_column i ~width:size) column_sizes ; + + (lst, lab) + +(* ============================================================================= *) +(* = Mise a jour des tailles des colonnes d'une liste = *) +(* ============================================================================= *) +let set_columns_sizes (lst: string GList.clist) (sizes: int list) = + let i = ref 0 in + List.iter (fun width -> lst#set_column !i ~width ; incr i) sizes + +(* ============================================================================= *) +(* = Connection de callback a la liste precedente = *) +(* = = *) +(* = (lst, lab) = la liste et le label cree par la fct precedente = *) +(* = item_column = colonne ou se trouve l'item de reference = *) +(* = selection_callback = callback appele lors de la selection d'un element = *) +(* = (name, female, cap) = regle d'affichage du label du nombre d'elements = *) +(* ============================================================================= *) +let connect_managed_list (lst, lab) item_column selection_callback + (name, female, cap) = + let eval_string base_string n female first_char_upper = + let get_upper_and_female str = + let str = if female then str^"e" else str in + if first_char_upper then String.capitalize str + else str + in + match n with + 0 -> let s = get_upper_and_female "aucun" in s ^ " " ^ base_string + | 1 -> let s = get_upper_and_female "un" in s ^ " " ^ base_string + | _ -> if String.get base_string (String.length base_string -1) = 'x' then + Printf.sprintf "%d %s" n base_string + else Printf.sprintf "%d %ss" n base_string + in + + let last_row = ref (-1) and id_selected = ref "" in + let reset_selection () = last_row := (-1) ; id_selected := "" in + + (* Selections dans la liste *) + let select_func selection_type row column dbl_click = + if selection_type then begin + id_selected := lst#cell_text row item_column ; last_row := row + end else reset_selection () ; + selection_callback !id_selected (row, column, dbl_click) selection_type + in + + (* Connexion des callbacks de la liste *) + list_connect_check_dbl_click lst + (Some (select_func true)) (Some (select_func false)) None ; + + (* Ajout de cette fonction pour GTK2. En GTK1, ca scrolle tout seul *) + (* quand necessaire, pas en GTK2... *) + let scroll_if_needed () = + if !last_row<>(-1) then + match lst#row_is_visible !last_row with + `NONE | `PARTIAL -> lst#moveto !last_row item_column + | `FULL -> () + in + + let nb_elts = ref 0 in + (* Callback permettant de naviguer dans la liste avec les fleches haut/bas *) + let key_up () = + if !last_row<> -1 && !last_row <> 0 then begin + lst#select (!last_row-1) item_column ; + scroll_if_needed () + end + in + let key_down () = + if !last_row<> -1 && !last_row <> (!nb_elts-1) then begin + lst#select (!last_row+1) item_column ; + scroll_if_needed () + end + in + list_connect_up_down_keys lst key_up key_down ; + + let fill_list select_id lst_items = + lst#clear () ; lst#freeze () ; + reset_selection () ; + let row = ref 0 and selected_row = ref (-1) in + List.iter (fun l -> + ignore(lst#append l) ; + (* Est-ce la ligne de l'element a selectionner ? *) + if List.nth l item_column = select_id then selected_row := !row ; + incr row) lst_items ; + + nb_elts := !row ; + (lab:GMisc.label)#set_text (eval_string name !nb_elts female cap) ; + lst#thaw () ; + + (* Selection de l'element selectionne si c'est demande *) + if !selected_row <> -1 then lst#select !selected_row item_column + in + + fill_list + +(* Type Lat/Lon en degres ou degres/minutes/secondes *) +type t_val = + G_LAT (* Latitude en degres flottants *) + | G_LON (* Longitude en degres flottants *) + | G_LAT_D (* Latitude : degres *) + | G_LAT_M (* Latitude : minutes *) + | G_LAT_S (* Latitude : secondes *) + | G_LAT_NS (* Latitude : orientation Nord/Sud *) + | G_LON_D (* Longitude : degres *) + | G_LON_M (* Longitude : minutes *) + | G_LON_S (* Longitude : secondes *) + | G_LON_EW (* Longitude : orientation Est/Ouest *) + +(* Type contenant un widget de selection de coordonnees lat/lon *) +type latlon = {latlon_lat_val : float ref ; + latlon_lon_val : float ref ; + latlon_update : float -> float -> unit ; + latlon_change_callback : (unit -> unit) option ref} + +(* ============================================================================= *) +(* = Creation d'un widget de selection de coordonnees lat/lon = *) +(* = = *) +(* = (lat_in, lon_in) = position initiale en degres = *) +(* = pack_method = ou mettre le widget = *) +(* = tooltips = aide contextuelle = *) +(* = = *) +(* = Renvoie un widget de type latlon = *) +(* ============================================================================= *) +let create_latlon_selection (lat_in, lon_in) pack_method tooltips = + let degminsec_of_pos d = + let (d, dd, signe) = + if d < 0.0 then (-.d, int_of_float (-.d), (-1.0)) else (d, int_of_float d, 1.0) + in + let reste = d-.(float_of_int dd) in + let mm = int_of_float (reste*.60.0) in + let ss = (reste-.(float_of_int mm)/.60.0)*.3600.0 in + (* Evite d'avoir 49.3 -> 49 17 60.00 *) + let (mm, ss) = if ss > 59.9999 then (mm+1, 0.0) else (mm, ss) in + (dd, mm, ss, signe) in + + let pos_of_degminsec (d, m, s, signe) = + ((float_of_int d)+.(float_of_int m)/.60.0+.s/.3600.0) *. signe in + + (* Fonction de creation des menus de choix N/S et E/W *) + let menu_NS_EW options signe pack_method = + let menu = GMenu.menu () in + let l = List.map (fun op -> + (GMenu.menu_item ~label:op ~packing:menu#append (), op)) options in + let optionmenu = GMenu.option_menu ~packing:pack_method () in + optionmenu#set_menu menu ; + + (* Si signe est negatif alors S ou W. On selectionne par defaut le menu *) + (* correspondant *) + if signe<0.0 then optionmenu#set_history 1 ; + + (optionmenu, l) + in + + (* Initialisation des variables *) + let lat = ref lat_in and lon = ref lon_in in + let (a, b, c, s) = degminsec_of_pos lat_in in + let lat_d = ref a and lat_m = ref b and lat_s = ref c and lat_signe = ref s in + let (a, b, c, s) = degminsec_of_pos lon_in in + let lon_d = ref a and lon_m = ref b and lon_s = ref c and lon_signe = ref s in + + (* Creation des widgets *) + let box = create_spaced_hbox pack_method in + + (* Latitudes *) + let b = snd (create_vframe "Latitude" box#pack) in + let e_lat = create_float_spinner_simple "" 0 + !lat (-90.0) 90.0 210 4 0.1 1.0 + "Latitude en degres (flottant)" tooltips b#pack and + hb = GPack.hbox ~packing:b#pack ~spacing:5 ~border_width:5 () in + let e_lat_d = create_int_spinner_simple "" 0 !lat_d 0 90 40 1 5 + "Latitude : degres" tooltips hb#pack and + e_lat_m = create_int_spinner_simple "" 0 !lat_m 0 59 40 1 5 + "Latitude : minutes" tooltips hb#pack and + e_lat_s = create_float_spinner_simple "" 0 + !lat_s 0.0 59.99 55 2 0.1 1.0 + "Latitude : secondes" tooltips hb#pack and + (o_lat, m_lat) = menu_NS_EW ["N"; "S"] !lat_signe hb#pack in + + (* Longitudes *) + let b = snd (create_vframe "Longitude" box#pack) in + let e_lon = create_float_spinner_simple "" 0 + !lon (-180.0) 180.0 210 4 0.1 1.0 + "Longitude en degres (flottant)" tooltips b#pack and + hb = GPack.hbox ~packing:b#pack ~spacing:5 ~border_width:5 () in + let e_lon_d = create_int_spinner_simple "" 0 !lon_d 0 180 40 1 5 + "Longitude : degres" tooltips hb#pack and + e_lon_m = create_int_spinner_simple "" 0 !lon_m 0 59 40 1 5 + "Longitude : minutes" tooltips hb#pack and + e_lon_s = create_float_spinner_simple "" 0 + !lon_s 0.0 59.99 55 2 0.1 1.0 + "Longitude : secondes" tooltips hb#pack and + (o_lon, m_lon) = menu_NS_EW ["E"; "W"] !lon_signe hb#pack in + + (* Callbacks de modification *) + let updating = ref false in + + (* Fonctions de coherence des differents affichages *) + let update_from_latlon () = + let (a, b, c, signe) = degminsec_of_pos !lat in + lat_d := a; lat_m := b; lat_s := c ; lat_signe := signe ; + + e_lat_d#set_value (float_of_int !lat_d) ; + e_lat_m#set_value (float_of_int !lat_m) ; + e_lat_s#set_value !lat_s ; + + let m = if signe > 0.0 then 0 else 1 in + o_lat#set_history m ; + let (a, b, c, signe) = degminsec_of_pos !lon in + lon_d := a; lon_m := b; lon_s := c ; lon_signe := signe ; + + e_lon_d#set_value (float_of_int !lon_d) ; + e_lon_m#set_value (float_of_int !lon_m) ; + e_lon_s#set_value !lon_s ; + + let m = if signe > 0.0 then 0 else 1 in + o_lon#set_history m + in + let update_from_latlon_dms () = + lat := pos_of_degminsec (!lat_d, !lat_m, !lat_s, !lat_signe) ; + lon := pos_of_degminsec (!lon_d, !lon_m, !lon_s, !lon_signe) ; + e_lat#set_value !lat ; e_lon#set_value ! lon ; + in + + let change_callback = ref None in + + let update type_valeur t = + if not !updating then begin + updating := true ; + begin + match type_valeur with + G_LAT -> lat := t ; update_from_latlon () + | G_LON -> lon := t ; update_from_latlon () + | G_LAT_D -> lat_d := int_of_float t ; update_from_latlon_dms () + | G_LAT_M -> lat_m := int_of_float t ; update_from_latlon_dms () + | G_LAT_S -> lat_s := t ; update_from_latlon_dms () + | G_LON_D -> lon_d := int_of_float t ; update_from_latlon_dms () + | G_LON_M -> lon_m := int_of_float t ; update_from_latlon_dms () + | G_LON_S -> lon_s := t ; update_from_latlon_dms () + | G_LAT_NS -> lat_signe := t ; update_from_latlon_dms () + | G_LON_EW -> lon_signe := t ; update_from_latlon_dms () + end ; + updating := false ; + match !change_callback with None -> () | Some callback -> callback () + end + in + + let connect_sp spinner t = + spinner#connect#value_changed ~callback:(fun () -> + try let new_value = spinner#value in update t new_value + with Failure("float_of_string") -> ()) + in + + ignore(connect_sp e_lat G_LAT) ; ignore(connect_sp e_lon G_LON) ; + ignore(connect_sp e_lat_d G_LAT_D) ; ignore(connect_sp e_lat_m G_LAT_M) ; + ignore(connect_sp e_lat_s G_LAT_S) ; ignore(connect_sp e_lon_d G_LON_D) ; + ignore(connect_sp e_lon_m G_LON_M) ; ignore(connect_sp e_lon_s G_LON_S) ; + + let connect_menu_NS_EW l = + List.iter (fun (menuitem, op) -> + ignore(menuitem#connect#activate ~callback:(fun () -> + if op = "N" then update G_LAT_NS 1.0 ; + if op = "S" then update G_LAT_NS (-1.0) ; + if op = "E" then update G_LON_EW 1.0 ; + if op = "W" then update G_LON_EW (-1.0)))) l + in + + connect_menu_NS_EW m_lat ; + connect_menu_NS_EW m_lon ; + + {latlon_lat_val = lat; latlon_lon_val = lon; + latlon_update = (fun lat lon -> update G_LAT lat; update G_LON lon; + update G_LAT_D (float_of_int !lat_d); update G_LON_D (float_of_int !lon_d)) ; + latlon_change_callback = change_callback} + +(* ============================================================================= *) +(* = Mise a jour des valeurs dans un widget de selection de coordonnees lat/lon= *) +(* = = *) +(* = latlon_widget = le widget de selection de coordonnees lat/lon = *) +(* = new_lat = nouvelle latitude (en degres) = *) +(* = new_lon = nouvelle longitude (en degres) = *) +(* ============================================================================= *) +let update_latlon_selection latlon_widget new_lat new_lon = + latlon_widget.latlon_update new_lat new_lon + +(* ============================================================================= *) +(* = Renvoie les coordonnees choisies dans un widget lat/lon = *) +(* = = *) +(* = latlon_widget = le widget de selection de coordonnees lat/lon = *) +(* = = *) +(* = Renvoie une paire contenant la latitude et la longitude en degres = *) +(* ============================================================================= *) +let latlon_selection_get latlon_widget = + (!(latlon_widget.latlon_lat_val), !(latlon_widget.latlon_lon_val)) + +(* ============================================================================= *) +(* = Connecte un callback appele lors d'une modif dans le widget lat/lon = *) +(* = = *) +(* = latlon_widget = le widget de selection de coordonnees lat/lon = *) +(* = callback = le callback = *) +(* ============================================================================= *) +let latlon_selection_change latlon_widget callback = + latlon_widget.latlon_change_callback := Some callback + +(* Numero de la fenetre de log enregistree *) +let id_log_win = ref (-1) + +(* Niveau de verbose dans la fenetre de log *) +let log_verbose_level = ref 100 + +(* Zone de texte de la fenetre du log quand elle existe *) +let log_wid = ref None + +(* Exception levee lors de l'ajout de texte dans la fenetre de log alors que + celle-ci n'a pas encore ete creee *) +exception GTK_TOOLS_NO_LOG_WIN + +(* ============================================================================= *) +(* = Creation d'une zone de texte = *) +(* = = *) +(* = editable = zone editable par l'utilisateur = *) +(* = with_vert_scroll = presence d'une barre de defilement verticale = *) +(* = with_hor_scroll = presence d'une barre de defilement horizontale = *) +(* = pack_method = ou mettre le widget = *) +(* ============================================================================= *) +let create_text_edit editable with_vert_scroll with_hor_scroll + pack_method = + let hpolicy = if with_hor_scroll then `ALWAYS else `NEVER + and vpolicy = if with_vert_scroll then `ALWAYS else `NEVER in + let text = GText.view ~editable () in + let sw = GBin.scrolled_window ~packing:pack_method ~hpolicy ~vpolicy () in + sw#add text#coerce; + text + +(* ============================================================================= *) +(* = Efface une zone editable = *) +(* ============================================================================= *) +let text_edit_clear edit = + let (start,stop) = edit#buffer#bounds in edit#buffer#delete ~start ~stop + +(* ============================================================================= *) +(* = Renvoie le texte d'une zone editable = *) +(* ============================================================================= *) +let text_edit_get_text edit = (edit:GText.view)#buffer#get_text () + +(* ============================================================================= *) +(* = Renvoie le texte d'une zone editable sous la forme d'une liste de chaines = *) +(* = de caracteres correspondant aux differentes lignes = *) +(* ============================================================================= *) +let text_edit_get_lines edit = + let text = text_edit_get_text edit in + let l = split2 '\n' text in + (* Supprime la derniere ligne vide au besoin *) + if l<>[] && List.hd (List.rev l) = "" then List.rev (List.tl (List.rev l)) + else l + +(* ============================================================================= *) +(* = Met a jour le texte d'une zone editable = *) +(* ============================================================================= *) +let text_edit_set_text_list edit text_lst = + List.iter (fun s -> (edit:GText.view)#buffer#insert (s^"\n")) text_lst + +let text_edit_set_text edit text = (edit:GText.view)#buffer#insert text + +(* ============================================================================= *) +(* = Fonction interne : renvoie le widget text du log = *) +(* ============================================================================= *) +let get_log_wid () = + match !log_wid with None -> raise GTK_TOOLS_NO_LOG_WIN | Some w -> w + +(* ============================================================================= *) +(* = Ajout d'un texte dans le log = *) +(* = = *) +(* = text = texte du message a afficher = *) +(* = level = niveau de priorite du message = *) +(* ============================================================================= *) +let add_log text level = + if level < !log_verbose_level then begin + let zone = get_log_wid () and + time = timer_string_of_time (timer_get_time ()) in + (zone:GText.view)#buffer#insert (time ^ " : " ^ text) + end + +(* ============================================================================= *) +(* = Ajout d'un texte avec couleur dans le log = *) +(* = = *) +(* = text = texte du message a afficher = *) +(* = level = niveau de priorite du message = *) +(* = color = la couleur a utiliser = *) +(* ============================================================================= *) +let add_log_with_color text level color = + if level < !log_verbose_level then begin + let zone = get_log_wid () and + time = timer_string_of_time (timer_get_time ()) in + ignore ((zone:GText.view)#buffer#create_tag ~name:"color" + [`FOREGROUND_GDK (GDraw.color color)]); + zone#buffer#insert ~tag_names:["color"] (time ^ " : " ^ text) + end + +(* ============================================================================= *) +(* = Effacement du contenu du log = *) +(* ============================================================================= *) +let clear_log () = text_edit_clear (get_log_wid ()) + +(* ============================================================================= *) +(* = Creation de la fenetre de log = *) +(* = = *) +(* = tooltips = systeme d'aide contextuelle = *) +(* ============================================================================= *) +let create_log tooltips = + let rec in_build_log tooltips = + let (win, box) = create_window "Log" 400 300 in + + let log = create_text_edit false true true box#add in + log_wid := Some log ; + + let bbox = create_bbox box#pack in + let but_ok = create_button "OK" bbox#add and + but_clear = create_button "Effacer" bbox#add and + but_save = create_button "Sauver" bbox#add in + add_tooltips tooltips but_ok "Fermer la fenetre de log" ; + add_tooltips tooltips but_clear + "Effacer le contenu de la fenetre de log" ; + add_tooltips tooltips but_save + "Sauver le contenu de la fenetre de log" ; + but_connect but_clear (fun () -> clear_log ()) ; + but_connect but_ok (fun () -> + hide_registered_window !id_log_win) ; + but_connect but_save (fun () -> + open_file_dlg "Sauvegarde du log" + (fun filename -> + if log#buffer#char_count>0 then begin + let data = text_edit_get_text log in + let c = open_out filename in + Printf.fprintf c "%s" data ; + close_out c + end) None "log.txt" true) ; + + (* Pour que le bouton de destruction de la fenetre ne fasse *) + (* que la cacher et non la detruire reellement *) + ignore(win#connect#destroy ~callback:(fun () -> + id_log_win := register_window (fun () -> in_build_log tooltips))) ; + + win + in + id_log_win := register_window (fun () -> in_build_log tooltips) ; + + let win = in_build_log tooltips in + let w = !registered_windows.(!id_log_win) in + ignore(win#connect#destroy ~callback:(fun _ -> w.reg_win_handle <- None)) ; + w.reg_win_handle <- Some win + +(* ============================================================================= *) +(* = Affichage de la fenetre de log = *) +(* = = *) +(* = tooltips = systeme d'aide contextuelle = *) +(* ============================================================================= *) +let show_log () = + if !id_log_win <> -1 then show_registered_window !id_log_win + +(* ============================================================================= *) +(* = Cache la fenetre de log = *) +(* ============================================================================= *) +let hide_log () = hide_registered_window !id_log_win + +(* ============================================================================= *) +(* = Mise a jour du niveau d'affichage des messages dans le log = *) +(* = = *) +(* = level = le niveau de priorite = *) +(* ============================================================================= *) +let set_log_verbose_level level = log_verbose_level := level + +(* ============================================================================= *) +(* = Affichage du contenu d'un fichier dans une fenetre = *) +(* = = *) +(* = filename = le fichier a afficher = *) +(* = title = titre de la fenetre = *) +(* = tooltips = systeme d'aide contextuelle = *) +(* ============================================================================= *) +let display_file filename title width height tooltips font = + let (win, vbox) = create_window title width height in + let text = create_text_edit false true true vbox#add in + + (* Tag pour mettre le texte en rouge *) + ignore (text#buffer#create_tag ~name:"red_foreground" [`FOREGROUND "red"]); + + (* Mise a jour de la fonte si necessaire *) + (match font with + None -> () + | Some fontname -> + let font = Pango.Font.from_string fontname in + ignore(text#misc#modify_font font)) ; + + let bbox = create_bbox vbox#pack in + let but_ok = create_button "OK" bbox#add in + add_tooltips tooltips but_ok "Fermer la fenetre" ; + but_connect but_ok (fun () -> win#destroy ()) ; + + (try + let c = open_compress filename in + (try + while true do text#buffer#insert ((input_line c)^"\n") done ; + text#buffer#insert ~tag_names:["red_foreground"] + (Printf.sprintf "Erreur de lecture de %s\n" filename) + with End_of_file -> close_compress filename c) + with _ -> + text#buffer#insert ~tag_names:["red_foreground"] + (Printf.sprintf "Erreur d'ouverture de %s\n" filename)) ; + + win#show () + +(* ============================================================================= *) +(* = Creation d'une barre de progression dans une fenetre externe = *) +(* = = *) +(* = nb_blocks = nombre de subdivisions dans la barre = *) +(* = title = titre de la fenetre = *) +(* = = *) +(* = En sortie est renvoyee la fonction de mise a jour de la barre. Cette = *) +(* = fonction prend en parametre un flottant entre 0.0 et 1.0. = *) +(* = Lorsque ce flottant vaut 1.0, la fenetre est detruite = *) +(* ============================================================================= *) +let create_progress_bar_win nb_blocks title = + let window = GWindow.window ~title:title ~border_width:10 ~width:200 () in + let pbar = GRange.progress_bar ~packing:window#add () in +(* GTK2 AAA GRange.progress_bar ~bar_style:`DISCRETE ~discrete_blocks:nb_blocks () + ~packing:window#add in*) + let update_func pct = + pbar#set_fraction pct ; + (* Destruction de la fenetre si pct = 1.0 *) + if pct = 1.0 then window#destroy () ; + + (* Force la mise a jour pour voir la barre progresser *) + force_update_interface () in + window#show (); + + (* Renvoie la fonction de mise a jour *) + update_func + +(* ============================================================================= *) +(* = Creation d'une barre de progression = *) +(* = = *) +(* = pack_method = ou mettre la barre = *) +(* = = *) +(* = Renvoie la fonction de mise a jour (valeur entre 0.0 et 1.0) = *) +(* ============================================================================= *) +let create_progress_bar pack_method = + let pbar = GRange.progress_bar ~packing:pack_method () in + let current_progress = ref "" in + let update_progress p = + let pp = Printf.sprintf "%.0f%%" (100.*.p) in + (* Comme ca on n'affiche que tous les 1%. Sinon c'est tres lent avec GTK2 *) + if !current_progress<>pp then begin + pbar#set_text pp ; + pbar#set_fraction p ; force_update_interface () ; + current_progress:=pp + end + in + update_progress + +(* Operateurs de comparaison *) +type t_ops_compare = T_EQ | T_L | T_LEQ | T_G | T_GEQ + +(* ============================================================================= *) +(* = Creation d'un widget de selection d'un operateur de comparaison = *) +(* = = *) +(* = variable = variable contenant l'operateur selectionne (ref) = *) +(* = callback_modified = callback appele lorsque la valeur est modifiee (opt) = *) +(* = pack_method = ou mettre le widget = *) +(* ============================================================================= *) +let create_ops_compare variable callback_modified pack_method = + let string_of_t_op t = + match t with + T_EQ -> "=" + | T_L -> "<" + | T_LEQ -> "<=" + | T_G -> ">" + | T_GEQ -> ">=" + in + + let lst_ops = + List.map (fun t -> (string_of_t_op t, t)) [T_EQ; T_L; T_LEQ; T_G; T_GEQ] in + let func_active typ = typ = !variable in + let func_select typ = + variable := typ ; + match callback_modified with + None -> () + | Some f -> f !variable + in + ignore (create_optionmenu lst_ops func_active func_select pack_method) + +(* ============================================================================= *) +(* = Creation d'un widget de selection d'une heure = *) +(* = = *) +(* = variable = variable contenant l'heure selectionnee (ref) = *) +(* = callback_modified = callback appele lorsque la valeur est modifiee (opt) = *) +(* = pack_method = ou mettre le widget = *) +(* ============================================================================= *) +let create_time_select variable callback_modified pack_method = + let split c s = + let i = ref (String.length s - 1) in + let j = ref !i and r = ref [] in + if !i >= 0 then + while !i >= 0 do + while !i >= 0 & String.get s !i <> c do decr i; done; + if !i < !j then r := (String.sub s (!i+1) (!j - !i)) :: !r; + while !i >= 0 & String.get s !i = c do decr i; done; + j := !i; + done; + !r + in + let string_of_time s = + Printf.sprintf "%02d:%02d:%02d" (s/3600) (s/60 mod 60) (s mod 60) in + let time_of_string t = + match split ':' t with + [h;m;s]->(int_of_string h*60 + int_of_string m)*60 + int_of_string s + |_-> (-1) + in + + let b = create_hbox pack_method in + let bmoins = create_button "-" b#pack + and entry = GEdit.entry ~text:(string_of_time !variable) + ~width:65 ~packing:b#pack () + and bplus = create_button "+" b#pack in + + let set_time () = + entry#set_text (string_of_time !variable) ; + match callback_modified with None -> () | Some f -> f !variable + in + + but_connect bmoins (fun () -> + if !variable>=1 then decr variable; set_time ()) ; + but_connect bplus (fun () -> + if !variable<=86400-1 then incr variable; set_time ()) ; + + ignore(entry#connect#changed ~callback:(fun () -> + variable:=time_of_string entry#text ; + match callback_modified with None -> () | Some f -> f !variable)) ; + + match callback_modified with + None -> () + | Some f -> + ignore(entry#connect#activate ~callback:(fun () -> f !variable)) + +(* ============================================================================= *) +(* = Fenetre pour affichage d'infos sous forme de labels = *) +(* = = *) +(* = title = titre de la fenetre = *) +(* = width = largeur de la fenetre = *) +(* = height = hauteur de la fenetre = *) +(* ============================================================================= *) +let create_infos_win title width height = + let (win, vb) = create_modal_window title width height in + let update_win text = + ignore(create_sized_label text width vb#pack) ; + force_update_interface () + in + win#show () ; + force_update_interface () ; + (update_win, win#destroy) + +(* ============================================================================= *) +(* = Separateur dans un menu = *) +(* ============================================================================= *) +let menu_separator = ("", fun () -> ()) + +(* ============================================================================= *) +(* = Creation d'un menu simple dans la barre de menus d'une fenetre = *) +(* = = *) +(* = menu = le menu ou acrocher le menu cree = *) +(* = lst_items = liste des noms+actions du menu = *) +(* ============================================================================= *) +let create_simple_menu menu lst_items = + let factory = new GMenu.factory menu in + List.iter (fun (texte, action) -> + ignore(if texte<>"" then factory#add_item texte ~callback:action + else factory#add_separator ())) lst_items + +(* ============================================================================= *) +(* = Creation de menus dans la barre de menus d'une fenetre = *) +(* = = *) +(* = menus = liste des menus standards = *) +(* = nb_menus = variable (reference) indiquant le menu courant = *) +(* = lst_items = liste des noms+actions du menu = *) +(* ============================================================================= *) +let create_menu menus nb_menus lst_items = + create_simple_menu menus.(!nb_menus) lst_items ; incr nb_menus + +(* ============================================================================= *) +(* = Creation d'un menu simple dans la barre de menus d'une fenetre = *) +(* = = *) +(* = menu = le menu ou acrocher le menu cree = *) +(* = lst_items = liste des noms+actions du menu = *) +(* ============================================================================= *) +let create_simple_menu_sens menu lst_items = + let factory = new GMenu.factory menu in + List.fold_left (fun l data -> + match data with + `I (texte, action) -> + let m = factory#add_item texte ~callback:action in (texte, m)::l + | `S -> ignore (factory#add_separator ()); l + | `M (texte, entries) -> + let sub = GMenu.menu () in let f = new GMenu.factory sub in + List.iter (fun m -> + match m with + `I (texte, action) -> ignore(f#add_item texte ~callback:action) + | _ -> ()) entries ; + let m = factory#add_item ~submenu:sub texte in (texte, m)::l + | _ -> l) [] lst_items + +(* ============================================================================= *) +(* = Creation de menus dans la barre de menus d'une fenetre = *) +(* = = *) +(* = menus = liste des menus standards = *) +(* = store_menus = table de stockage des menus pour recherche ensuite = *) +(* = tab_menus_names = noms associes aux menus = *) +(* = nb_menus = variable (reference) indiquant le menu courant = *) +(* = lst_items = liste des noms+actions du menu = *) +(* ============================================================================= *) +let create_menu_sens menus tab_menus_names store_menus nb_menus lst_items = + let l = create_simple_menu_sens menus.(!nb_menus) lst_items in + Hashtbl.add store_menus tab_menus_names.(!nb_menus) l ; + incr nb_menus + +(* ============================================================================= *) +(* = Initialisation du stockage des menus = *) +(* ============================================================================= *) +let init_menus_sens () = Hashtbl.create 13 + +(* ============================================================================= *) +(* = Mise a jour de l'etat active/desactive d'un sous-menu = *) +(* = = *) +(* = store_menus = table de stockage des menus = *) +(* = menu_name = nom du menu = *) +(* = sub_menu_name = nom du sous-menu = *) +(* = sensitive = indique l'etat a donne au sous-menu = *) +(* ============================================================================= *) +let set_sub_menu_sensitive store_menus menu_name sub_menu_name sensitive = + try + let l = Hashtbl.find store_menus menu_name in + List.iter (fun (texte, sub_menu) -> + if texte = sub_menu_name then set_sensitive sub_menu sensitive) l + with Not_found -> () + +(* ============================================================================= *) +(* = Combo box = *) +(* ============================================================================= *) +let create_combo_simple lst_items pack_method = + let combo = GEdit.combo ~packing:pack_method () in + List.iter (fun item -> + let i = GList.list_item () in + ignore(create_label item i#add) ; + combo#set_item_string i item ; + combo#list#add i) lst_items ; + (* Zone ou se trouve le texte selectionne *) + let entry = combo#entry in + (* On la desactive pour que l'utilisateur ne puisse pas la modifier a la main *) + set_sensitive entry false ; + + entry + +let combo_connect entry lst_items callback = + ignore(text_entry_connect_modify entry + (fun s -> + try let (_, t) = List.find (fun (n, _) -> n=s) lst_items in callback t + with Not_found -> ())) + +(* ============================================================================= *) +(* = Widget calendrier pour la selection d'une date. Les dates disponibles sont= *) +(* = en couleur = *) +(* ============================================================================= *) +let calendar lst_dates callback_select only_available_dates_selectable + init_with_last_available_date tooltips win pack_method = + + (* Date de depart = la plus recente dans la liste ou date actuelle *) + let current_month = ref 0 and current_year = ref 0 in + if init_with_last_available_date && lst_dates<>[] then begin + let d = List.hd (List.fast_sort (fun d1 d2 -> cmp_int d2 d1) lst_dates) in + let (j, m, a) = decompose_date d in current_month := m; current_year := a + end else begin + let tm = timer_get_time () in + current_month := (tm.Unix.tm_mon+1); current_year:= (tm.Unix.tm_year+1900) + end ; + + (* Transformation des dates 20030721 -> (21, 07, 2003) *) + let lst_dates = List.map decompose_date lst_dates in + (* Fonction indiquant si une date est dans la listes des dates autorisees *) + let date_in_list i = + try ignore(List.find (fun date -> + date = (i+1, !current_month, !current_year)) lst_dates) ; true + with Not_found -> false + in + + let styles = + let default = (Obj.magic () : GObj.style) in [|default; default; default|] + in + + (* Boite pour mettre le widget *) + let vb = create_vbox pack_method in + + (* Boutons de changement de mois et label d'affichage mois+annee courants *) + let hb = create_hbox vb#pack in + let pm = pixmap_from_file "Pixmaps/left_arrow.xpm" win in + let b_mmois = create_pixbutton pm hb#pack in + let lab_mois = create_label "" hb#add in + let pm = pixmap_from_file "Pixmaps/right_arrow.xpm" win in + let b_pmois = create_pixbutton pm hb#pack in + + (* Table contenant les boutons pour les jours *) + let calendar = + GPack.table ~homogeneous: true ~rows:7 ~columns:7 + ~border_width:10 ~row_spacings:2 ~col_spacings:2 ~packing:vb#pack () in + (* Barre de titre avec les jours de la semaine *) + Array.iteri (fun i wday -> + ignore(create_button wday + (calendar#attach ~top:0 ~left:i ~expand:`BOTH))) + [|"Dim"; "Lun"; "Mar"; "Mer"; "Jeu"; "Ven"; "Sam"|] ; + + (* Creation des boutons correspondants aux jours d'un mois *) + let buttons = Array.init 31 (fun i -> + let b = GButton.button ~label:(string_of_int (i+1)) ~show:false () in + but_connect b (fun () -> + if not only_available_dates_selectable or (date_in_list i) then begin + b#misc#set_style styles.(2) ; + callback_select (compose_date (i+1, !current_month, !current_year)) + end) ; + b) in + let buttons_shown = Array.init 31 (fun i -> false) in + + (* Mise a jour des boutons dans le calendrier *) + let update_calendar () = + let mois = get_month_of_num !current_month in + lab_mois#set_text (Printf.sprintf "%s %d" mois !current_year) ; + + (* Numero, dans la semaine, du premier jour du mois indique *) + let d = + match get_day_of_date (1, !current_month, !current_year) with + "Dimanche" -> 0 | "Lundi" -> 1 | "Mardi" -> 2 + | "Mercredi" -> 3 | "Jeudi" -> 4 | "Vendredi" -> 5 + | _ -> 6 + in + + (* Suppression des boutons precedemment affiches *) + Array.iteri (fun i button -> + if buttons_shown.(i) then begin + button#misc#hide (); + calendar#remove button#coerce ; + buttons_shown.(i) <- false + end) buttons ; + + (* Affichage du bon nombre de boutons *) + let ndays = get_nb_days_in_month !current_month !current_year in + for i = 0 to ndays - 1 do + let top = (i+d) / 7 + 1 and left = (i+d) mod 7 in + calendar#attach ~left ~top ~expand:`BOTH buttons.(i)#coerce ; + buttons.(i)#misc#show () ; + buttons_shown.(i) <- true ; + add_tooltips tooltips buttons.(i) + (Printf.sprintf "%s %d %s %d" + (get_day_of_date (i+1, !current_month, !current_year)) + (i+1) mois !current_year) ; + + if date_in_list i then buttons.(i)#misc#set_style styles.(1) + else buttons.(i)#misc#set_style styles.(0) + done + in + + (* Boutons de modification du mois courant *) + but_connect b_mmois (fun () -> + if !current_month = 1 then begin decr current_year; current_month:=12 + end else decr current_month ; + update_calendar ()) ; + but_connect b_pmois (fun () -> + if !current_month = 12 then begin incr current_year; current_month:=1 + end else incr current_month ; + update_calendar ()) ; + + (* Valeurs des styles pour le changement de couleurs des boutons *) + let style = win#misc#style#copy in styles.(0) <- style; + let style = style#copy in + style#set_bg [`NORMAL, `NAME "light green"; + `PRELIGHT, `NAME "light green"]; + styles.(1) <- style; + let style = style#copy in + style#set_bg [`ACTIVE, `NAME "blue"]; + + styles.(2) <- style; + + (* Mise a jour initiale des boutons dans le calendrier *) + update_calendar () + +(* ============================================================================= *) +(* = Fenetre calendrier pour la selection d'une date. Les dates disponibles = *) +(* = sont en couleur = *) +(* ============================================================================= *) +let calendar_window lst_dates callback_select + only_available_dates_selectable init_with_last_available_date is_modal tooltips = + + (* Creation de la fenetre modale ou pas *) + let (win, vb) = + if is_modal then create_modal_window "Choix de date" 0 0 + else create_window "Choix de date" 0 0 + in + + let new_callback_select date = callback_select date; win#destroy () in + calendar lst_dates new_callback_select only_available_dates_selectable + init_with_last_available_date tooltips win vb#add ; + + (* Bouton de fermeture de la fenetre *) + let bbox = create_bbox vb#pack in + let but_cancel = create_button "Annuler" bbox#add in + but_connect but_cancel win#destroy ; + + win#show () + +(* ============================================================================= *) +(* = Creation d'une pixmap pour faire ensuite un stipple = *) +(* ============================================================================= *) +let create_stipple_pixmap_from_data data width height = + try + let s = String.create (width*height) and i = ref 0 in + List.iter (fun v -> s.[!i] <- Char.chr v; incr i) data ; + + let c1 = GDraw.color `WHITE and c2 = GDraw.color `BLACK in + (new GDraw.pixmap (Gdk.Pixmap.create_from_data ~width:width ~height:height + ~depth:1 ~fg:c1 ~bg:c2 s))#pixmap + with _ -> Printf.printf "Erreur dans create_stipple_pixmap_from_data\n"; + flush stdout; exit 1 + +(* ============================================================================= *) +(* = Fenetre de selection d'une fonte = *) +(* ============================================================================= *) +let select_font_dlg tooltips fonte_init selection_func = + let (win, vb)= create_modal_window "Selection de fonte" 0 0 in + let fn_dlg = GMisc.font_selection ~packing:vb#add ~show:true () in + let lst_buts = [("OK", "Selection de la fonte"); + ("Annuler", "Ferme la fenetre")] + in + + if fonte_init <> "" then fn_dlg#set_font_name fonte_init ; + + let get_font () = + if fn_dlg#font_name<>"" then selection_func fn_dlg#font_name ; + win#destroy () + in + + let buts = create_buttons lst_buts tooltips (vb#pack ~from:`END) in + create_buttons_connect buts + [get_font; win#destroy] ; + + win#show () ; + fn_dlg + +(* ============================================================================= *) +(* = Selection du texte contenu dans un widget = *) +(* ============================================================================= *) +let text_entry_select_text entry = + entry#select_region ~start:0 ~stop:entry#text_length + +(* ============================================================================= *) +(* = Creation d'une pixmap rectangulaire coloree = *) +(* ============================================================================= *) +let rectangle_pixmap window color width height = + (* ================================================================== *) + (* La ligne suivante marche jusqu'a lablgtk2-20040304, a partir des *) + (* versions suivantes on obtient un bug a l'execution si on n'utilise *) + (* pas GDraw.pixmap_from_xpm_d a la place de GDraw.pixmap *) + (* ================================================================== *) +(* let pm = GDraw.pixmap ~window ~width ~height () in*) + let size = Printf.sprintf "%d %d 2 1" width height in + let data = [size ; ". c None"; "# c #000000"] in + let one_line = ref "" in + for i = 0 to width-1 do one_line:=!one_line^"#" done ; + let lines = ref [] in + for i = 0 to height-1 do lines:=!one_line::!lines done ; + + let data = Array.of_list (data @ !lines) in + let pm = GDraw.pixmap_from_xpm_d ~data:data + ~window:(window:GWindow.window) () in + (pm:GDraw.pixmap)#set_foreground color ; + pm#rectangle ~filled:true ~x:0 ~y:0 ~width ~height () ; + pm + +(* ============================================================================= *) +(* = Creation d'une fenetre on top = *) +(* ============================================================================= *) +let create_window_on_top title width height window = + let x = create_window title width height in + (fst x)#set_transient_for window#as_window; x + +let create_window_on_top2 title width height window = + let x = create_window title width height in + (match window with + None -> () | Some window -> (fst x)#set_transient_for window#as_window) ; + x + +(* ============================================================================= *) +(* = Creation d'une fenetre modale on top = *) +(* ============================================================================= *) +let create_modal_window_on_top title width height window = + let x = create_modal_window title width height in + (fst x)#set_transient_for window#as_window; x + +let create_modal_window_on_top2 title width height window = + let x = create_modal_window title width height in + (match window with + None -> () | Some window -> (fst x)#set_transient_for window#as_window) ; + x + +(* =============================== FIN ========================================= *) diff --git a/sw/lib/ocaml/gtk_tools.mli b/sw/lib/ocaml/gtk_tools.mli new file mode 100644 index 00000000000..2eb3f888ad6 --- /dev/null +++ b/sw/lib/ocaml/gtk_tools.mli @@ -0,0 +1,1354 @@ +(* + * $Id$ + * + * Lablgtk2 utils + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) +(** Module outils pour lablgtk-2.4.0 + + Nouveaux widgets et encapsulation de fonctions GTK pour faciliter + l'utilisation de la bibliotheque lablgtk + + {b Dépendences : Platform, Ocaml_Tools} + + {e Yann Le Fablec, version 4.10, 26/08/2004} + *) + +(** Chaine indiquant la version de la librairie *) +val version : string + + +(** {6 Fonctions principales} *) + + +(** force la mise à jour de l'interface *) +val force_update_interface : unit -> unit + +(** initialise les couleurs *) +val init_colors : unit -> unit + +(** lance la mainloop de l'interface *) +val main_loop : unit -> unit + +(** initialise le systeme d'aide contextuelle et renvoie l'objet correspondant *) +val init_tooltips : unit -> GData.tooltips + +(** [gtk_tools_add_tooltips tooltips widget texte] ajoute une aide contextuelle + à un widget *) +val add_tooltips : + GData.tooltips -> < coerce : GObj.widget; .. > -> string -> unit + +(** renvoie la largeur et la hauteur de l'écran en pixels *) +val get_screen_size : unit -> int * int + +(** [gtk_tools_disconnect wid id] déconnecte le signal [id] du widget [wid] *) +val disconnect : + < misc : < disconnect : 'a -> 'b; .. >; .. > -> 'a -> 'b + +(** [gtk_tools_set_sensitive widget sensitive] active/désactive un widget *) +val set_sensitive : + < misc : < set_sensitive : 'a -> 'b; .. >; .. > -> 'a -> unit + +(** [gtk_tools_set_sensitive_list widgets sensitive] active/désactive une liste + de widgets *) +val set_sensitive_list : + < misc : < set_sensitive : 'a -> 'b; .. >; .. > list -> 'a -> unit + +(** [gtk_tools_set_cursor window cursor] met à jour la forme du curseur dans la + fenetre indiquée *) +val set_cursor : Gdk.window -> Gdk.cursor -> unit + +(** [gtk_tools_get_widget_size widget] renvoie un couple donnant la largeur et + la hauteur du widget indiqué *) +val get_widget_size : + < misc : < allocation : Gtk.rectangle; .. >; .. > -> int * int + +(** [gtk_tools_set_widget_back_color widget couleur] modifie la couleur de fond + d'un widget. Marche avec les widgets créant une fenetre (les boutons + par exemple), ça ne fonctionne donc pas avec les labels pour lesquels il faut + une event_box...*) +val set_widget_back_color : + < misc : < set_style : (< set_bg : ([> `NORMAL] * GDraw.color) list -> + 'c; + .. > as 'a) -> + 'd; + style : < copy : 'a; .. >; .. >; + .. > -> GDraw.color -> unit + +(** [gtk_tools_set_widget_front_color w color] modifie la couleur du texte d'un widget + (du type label) *) +val set_widget_front_color : + < misc : < set_style : (< set_fg : ([> `NORMAL] * GDraw.color) list -> + 'b; .. > as 'a) -> 'c; style : < copy : 'a; .. >; .. >; .. > -> + GDraw.color -> unit + +(** [gtk_tools_set_button_front_color bouton color] modifie la couleur du texte d'un + bouton (la fonction précédente ne marche pas avec un bouton car le texte d'un bouton + ne se trouve pas directement dans le bouton mais dans un label fils de ce bouton *) +val set_button_front_color : GButton.button -> GDraw.color -> unit + +(** [gtk_tools_set_button_back_color bouton color] modifie la couleur de fond d'un bouton. *) +val set_button_back_color : GButton.button -> GDraw.color -> unit + +(** [gtk_tools_set_entry_front_color entry color] modifie la couleur du texte dans une entry *) +val set_entry_front_color : GEdit.entry -> GDraw.color -> unit + +(** [gtk_tools_set_entry_back_color entry color] modifie la couleur de fond d'une entry *) +val set_entry_back_color : GEdit.entry -> GDraw.color -> unit + +(** [gtk_tools_set_entry_outline_color entry color] modifie la couleur du contour d'une entry *) +val set_entry_outline_color : GEdit.entry -> GDraw.color -> unit + + +(** [gtk_tools_scroll_adjustment adj scroll_up delta] effectue le scrolling d'un + adjustment [adj] (d'une scrollbar par exemple) vers le haut si [scroll_up] est vrai + et vers le bas sinon. [delta] désigne la valeur absolue du deplacement *) +val scroll_adjustment : GData.adjustment -> bool -> float -> unit + +(** [gtk_tools_connect_mouse_wheel_scroll widget scrollbar delta] connecte + un scroll de valeur absolue [delta] de la scrollbar [scrollbar] + lors de l'utilisation de la molette souris dans le widget [widget] *) +val connect_mouse_wheel_scroll : + < event : < connect : < button_press : callback:(GdkEvent.Button.t -> + bool) -> 'a; scroll : callback:(GdkEvent.Scroll.t -> bool) -> 'a; .. >; .. >; .. > + -> GRange.range -> float -> unit + +(** {6 Evénements souris} *) + + +(** Boutons souris *) +type bouton_souris = B_GAUCHE | B_DROIT | B_MILIEU | B_NONE + +(** [gtk_tools_test_mouse_but event] renvoie un element de type + {!Gtk_tools.gtk_tools_bouton_souris} indiquant le bouton de souris pressé lors + d'un click souris *) +val test_mouse_but : GdkEvent.Button.t -> bouton_souris + +(** [gtk_tools_get_mouse_pos_click event] renvoie un couple d'entiers donnant + la position de la souris lors d'un click *) +val get_mouse_pos_click : GdkEvent.Button.t -> int * int + +(** [gtk_tools_get_mouse_pos_move event] renvoie un element de type + {!Gtk_tools.gtk_tools_bouton_souris} indiquant le bouton de souris pressé + pendant un deplacement de la souris *) +val get_mouse_pos_move : GdkEvent.Motion.t -> int * int + +(** [gtk_tools_check_dbl_click event] teste le double click souris *) +val check_dbl_click : [> `TWO_BUTTON_PRESS] Gdk.event -> bool + + +(** {6 Fontes/Texte} *) + + +(** renvoie la fonte 'fixed' 8 points *) +val get_fixed_font : unit -> Gdk.font + +(** renvoie la fonte 'fixed' 13 points *) +val get_fixed_font2 : unit -> Gdk.font + +(** [gtk_tools_set_widget_font widget font] modifie la fonte d'un widget *) +val set_widget_font : + < misc : < set_style : (< set_font : 'b -> 'c; .. > as 'a) -> 'd; + style : < copy : 'a; .. >; .. >; + .. > -> + 'b -> unit + +(** [gtk_tools_string_width_height font string] indique la largeur et la hauteur, + en pixels, du texte donné par [string] affiché dans la fonte [font] *) +val string_width_height : Gdk.font -> string -> int * int + + +(** {6 Boites} *) + + +(** {0 Boites de widgets} *) + +(** [gtk_tools_create_bbox pack_method] crée une boite de boutons *) +val create_bbox : (GObj.widget -> unit) -> GPack.button_box + +(** [gtk_tools_create_hbox pack_method] création d'une boite horizontale *) +val create_hbox : (GObj.widget -> unit) -> GPack.box + +(** [gtk_tools_create_hom_hbox pack_method] création d'une boite horizontale où + la taille des widgets est homogène *) +val create_hom_hbox : (GObj.widget -> unit) -> GPack.box + +(** [gtk_tools_create_spaced_hbox pack_method] création d'une boite horizontale + avec espacement des widgets de 5 pixels *) +val create_spaced_hbox : (GObj.widget -> unit) -> GPack.box + +(** [gtk_tools_create_hom_spaced_hbox pack_method] création d'une boite horizontale + avec espacement des widgets de 5 pixels. Les widgets ont en plus une taille + homogène *) +val create_hom_spaced_hbox : (GObj.widget -> unit) -> GPack.box + +(** [gtk_tools_create_vbox pack_method] création d'une boite verticale *) +val create_vbox : (GObj.widget -> unit) -> GPack.box + +(** [gtk_tools_create_hom_vbox pack_method] création d'une boite verticale où + la taille des widgets est homogène *) +val create_hom_vbox : (GObj.widget -> unit) -> GPack.box + +(** [gtk_tools_create_spaced_vbox pack_method] création d'une boite verticale + avec espacement des widgets de 5 pixels *) +val create_spaced_vbox : (GObj.widget -> unit) -> GPack.box + +(** [gtk_tools_create_hom_spaced_vbox pack_method] création d'une boite verticale + avec espacement des widgets de 5 pixels. Les widgets ont en plus une taille + homogène *) +val create_hom_spaced_vbox : (GObj.widget -> unit) -> GPack.box + + +(** {0 Frames} *) + + +(** [gtk_tools_create_vframe title pack_method] crée une frame contenant une vbox*) +val create_vframe : + string -> (GObj.widget -> unit) -> GBin.frame * GPack.box + +(** [gtk_tools_create_spaced_vframe title pack_method] crée une frame contenant une vbox + avec espaces *) +val create_spaced_vframe : + string -> (GObj.widget -> unit) -> GBin.frame * GPack.box + +(** [gtk_tools_create_hframe title pack_method] crée une frame contenant une hbox*) +val create_hframe : + string -> (GObj.widget -> unit) -> GBin.frame * GPack.box + +(** [gtk_tools_create_spaced_hframe title pack_method] crée une frame contenant une hbox + avec espaces *) +val create_spaced_hframe : + string -> (GObj.widget -> unit) -> GBin.frame * GPack.box + + +(** {0 Boites scrollables} *) + + +(** [gtk_tools_create_scrolled_box pack_method] crée une zone scrollable contenant + une vbox. Renvoie la zone et la vbox *) +val create_scrolled_box : + (GObj.widget -> unit) -> GBin.scrolled_window * GPack.box + +(** [gtk_tools_change_scrolled_box scrolled_window old_box] détruit puis recrée + la vbox dans une zone scrollable. Renvoie la nouvelle vbox *) +val change_scrolled_box : + < add_with_viewport : GObj.widget -> unit; .. > -> + < destroy : unit -> 'a; .. > -> GPack.box + + +(** {0 Notebooks} *) + + +(** [gtk_tools_create_notebook pack_method] crée un notebook (widget contenant + différentes pages *) +val create_notebook : (GObj.widget -> unit) -> GPack.notebook + +(** [gtk_tools_notebook_add_page notebook page_label] ajoute une page nommée + [page_label] au notebook indiqué. En retour un couple contenant une frame + et une boite verticale dans cette frame est retourné *) +val notebook_add_page : + GPack.notebook -> string -> GBin.frame * GPack.box + + +(** {0 Paned windows} *) + + +(** [gtk_tools_create_hpaned pack_method] création d'une zone avec division + mobile horizontale *) +val create_hpaned : (GObj.widget -> unit) -> GPack.paned + +(** [gtk_tools_create_vpaned pack_method] création d'une zone avec division + mobile verticale *) +val create_vpaned : (GObj.widget -> unit) -> GPack.paned + + +(** {6 Boutons} *) + + +(** [gtk_tools_create_button label pack_method] création d'un bouton *) +val create_button : + string -> (GObj.widget -> unit) -> GButton.button + +(** [gtk_tools_create_sized_button label width pack_method] création d'un bouton + ayant une taille fixée *) +val create_sized_button : + string -> int -> (GObj.widget -> unit) -> GButton.button + +(** [gtk_tools_but_connect but func] connecte le callback [func] au bouton [but] *) +val but_connect : + < connect : < clicked : callback:'a -> 'b; .. >; .. > -> 'a -> unit + +(** [gtk_tools_but_set_label but label] change le texte dans le bouton pour le + remplacer par [label] *) +val but_set_label : + < children : < as_widget : 'a Gtk.obj; .. > list; .. > -> string -> unit + +(** [gtk_tools_set_button_align but pos] change l'alignement du texte + d'un bouton *) +val set_button_align : + < children : < as_widget : 'a Gtk.obj; .. > list; .. > -> float -> unit + +(** [gtk_tools_set_button_align_left but] alignement du texte + du bouton à gauche*) +val set_button_align_left : + < children : < as_widget : 'a Gtk.obj; .. > list; .. > -> unit + +(** [gtk_tools_set_button_align_right but] idem à droite *) +val set_button_align_right : + < children : < as_widget : 'a Gtk.obj; .. > list; .. > -> unit + +(** [gtk_tools_set_button_align_center but] idem au milieu *) +val set_button_align_center : + < children : < as_widget : 'a Gtk.obj; .. > list; .. > -> unit + +(** [gtk_tools_set_button_padding but xpad] met a jour la position du texte dans le bouton *) +val set_button_padding : + < children : < as_widget : 'a Gtk.obj; .. > list; .. > -> int -> unit + +(** [gtk_tools_but_set_width but width] met a jour la largeur d'un bouton *) +val but_set_width : GButton.button -> int -> unit + +(** [gtk_tools_but_set_height but height] met a jour la hauteur d'un bouton *) +val but_set_height : GButton.button -> int -> unit + +(** [gtk_tools_create_buttons lst_buts tooltips pack_method] crée une rangée de + boutons placés dans une {!Gtk_tools.gtk_tools_create_bbox}. Le paramètre + [lst_buts] est une liste de couples du type [(nom du bouton, aide)] *) +val create_buttons : + (string * string) list -> + GData.tooltips -> (GObj.widget -> unit) -> GButton.button list + +(** [gtk_tools_create_buttons_connect lst_buts lst_callbacks] connecte une liste + de callbacks à une liste de boutons *) +val create_buttons_connect : + < connect : < clicked : callback:'a -> 'b; .. >; .. > list -> + 'a list -> unit + +(** [gtk_tools_create_checkbutton_simple active label pack_method tip tooltips] + crée un check button sans callback mais avec une aide contextuelle si [tip]<>""*) +val create_checkbutton_simple : + bool -> + string -> + (GObj.widget -> unit) -> string -> GData.tooltips -> GButton.toggle_button + +(** [gtk_tools_create_checkbutton active label pack_method tip tooltips callback] + crée un check button avec callback et une aide contextuelle (si [tip]<>"") *) +val create_checkbutton : + bool -> + string -> + (GObj.widget -> unit) -> + string -> GData.tooltips -> (bool -> unit) -> GButton.toggle_button + +(** [gtk_tools_create_radiobuttons_simple lst_names func_active pack_method] crée + une liste de radio boutons : + - [lst_names] est une liste contenant des couples [(nom_bouton, type_associe)] + - [func_active] indique quel est le bouton actif au depart + - [pack_method] indique où mettre les boutons + *) +val create_radiobuttons_simple : + (string * 'a) list -> + ('a -> bool) -> (GObj.widget -> unit) -> GButton.radio_button list + +(** [gtk_tools_radiobuttons_connect lst_names lst_but func_select] connecte + un callback lié à la modification du radiobutton selectionné. Le callback + reçoit en paramètre le type correspondant à ce bouton *) +val radiobuttons_connect : + ('a * 'b) list -> GButton.radio_button list -> ('b -> unit) -> unit + +(** [gtk_tools_create_radiobuttons lst_names func_active func_select pack_method] + crée des radiobuttons avec callback *) +val create_radiobuttons : + (string * 'a) list -> + ('a -> bool) -> + ('a -> unit) -> (GObj.widget -> unit) -> GButton.radio_button list + +(** [gtk_tools_create_togglebutton label active pack_method] crée un togglebutton*) +val create_togglebutton : + string -> bool -> (GObj.widget -> unit) -> GButton.toggle_button + +(** [gtk_tools_create_pixbutton pixmap pack_method] création d'un bouton contenant + une pixmap *) +val create_pixbutton : + GDraw.pixmap -> (GObj.widget -> unit) -> GButton.button + + +(** {6 Labels} *) + + +(** [gtk_tools_create_label label pack_method] création d'un label *) +val create_label : string -> (GObj.widget -> unit) -> GMisc.label + +(** [gtk_tools_create_sized_label label width pack_method] création d'un label + avec une taille fixée *) +val create_sized_label : + string -> int -> (GObj.widget -> unit) -> GMisc.label + +(** {0 Alignement des labels} *) + +(** [gtk_tools_create_sized_label_align label width pos pack_method] création d'un label + avec une taille fixée. Le texte est positionné suivant [pos] qui est compris entre + 0.0 et 1.0 (0.0 = à gauche et 1.0 = à droite) *) +val create_sized_label_align : + string -> int -> float -> (GObj.widget -> unit) -> GMisc.label + +(** [gtk_tools_create_sized_label_align_left label width pack_method] création d'un label + avec une taille fixée. Le texte est positionné à gauche dans le label *) +val create_sized_label_align_left : + string -> int -> (GObj.widget -> unit) -> GMisc.label + +(** [gtk_tools_create_sized_label_align_right label width pack_method] création d'un label + avec une taille fixée. Le texte est positionné à droite dans le label *) +val create_sized_label_align_right : + string -> int -> (GObj.widget -> unit) -> GMisc.label + +(** [gtk_tools_set_label_align label pos] fixe l'alignement du texte dans un label. + [pos] indique où se fait l'alignement : 0.0 = à gauche et 1.0 = à droite *) +val set_label_align : GMisc.label -> float -> unit + +(** [gtk_tools_set_label_align_left label] fixe l'alignement du texte dans un label + à gauche *) +val set_label_align_left : GMisc.label -> unit + +(** [gtk_tools_set_label_align_right label] fixe l'alignement du texte dans un label + à droite *) +val set_label_align_right : GMisc.label -> unit + +(** [gtk_tools_set_label_align_center label] fixe l'alignement du texte dans un label + au centre de ce dernier *) +val set_label_align_center : GMisc.label -> unit + +(** [gtk_tools_set_label_padding label xpad] fixe la position gauche du texte + dans un label *) +val set_label_padding : GMisc.label -> int -> unit + +(** {6 Boites de dialogue} *) + + +(** [gtk_tools_question_box window title question_msg default_is_cancel] crée une + fenetre posant une question à l'utilisateur : + - [window] désigne la fenetre mère + - [title] le titre à donner à la fenetre + - [question_msg] le message à afficher + - [defaut_is_cancel] indique si par defaut c'est le bouton Annuler qui est + selectionné + + Renvoie vrai si OK a été choisi, faux sinon + *) +val question_box : + < misc : #GDraw.misc_ops; .. > -> string -> string -> bool -> bool + +(** [gtk_tools_error_box window title error_msg] affiche une boite contenant un + message d'erreur *) +val error_box : + < misc : #GDraw.misc_ops; .. > -> string -> string -> unit + +(** [gtk_tools_animated_msg_box title msg lst_pixmaps] crée une boite de message + contenant un icone animé dont [lst_pixmaps] désigne les noms des différentes + images (pixmaps) de l'animation *) +val animated_msg_box : string -> string -> string list -> unit + +(** [gtk_tools_open_file_dlg title read_func update_func default_filename + check_overwrite] crée une boite de selection de fichier : + - [title] indique le titre à donner à la fenetre + - [read_func] fonction appelée pour traiter le fichier selectionné + - [update_func] : [None] ou [Some f] fonction optionnelle appelée après + le traitement du fichier par la fonction précédente + - [default_filename] nom de fichier/répertoire par defaut + - [check_overwrite] indique si on doit tester l'existence du fichier (pour + eviter un écrasement + *) +val open_file_dlg : + string -> (string -> 'a) -> (unit -> unit) option -> string -> bool -> unit + +(** [gtk_tools_select_font_dlg tooltips fonte_init selection_func] ouvre une fenetre + de selection de fonte. Si une fonte est choisie, [selection_func] est + appelée avec son nom. [fonte_init] désigne le nom de la fonte sélectionnée par + défaut ("" pour rien). *) +val select_font_dlg : GData.tooltips -> string -> (string -> unit) + -> GMisc.font_selection + +(** {6 Timers} *) + + +(** Type de timer *) +type timer_type = + TIMER_TIME (** Heure uniquement *) + | TIMER_DATE (** Date uniquement *) + | TIMER_TIME_AND_DATE (** Affichage de l'heure et de la date *) + +(** [gtk_tools_insert_timer label timer_type force_beginning] place un timer + de type [timer_type] dans le widget [label]. Si [force_beginning] est vrai + alors l'affichage commence dès la creation du timer *) +val insert_timer : + GMisc.label -> timer_type -> bool -> unit + + +(** {6 Menus} *) + +(** [gtk_tools_popup menus_entries] affiche un popup menu *) +val popup : GToolbox.menu_entry list -> unit + +(** [gtk_tools_connect_popup_menu wid button test_cond_func menu_entries] connecte + un popup menu à un widget : + - [wid] désigne le widget concerné + - [button] le bouton à presser pour afficher le popup menu + - [test_cond_func] fonction de test indiquant si le menu doit etre affiché + - [menu_entries] contient la liste des éléments du menu +*) +val connect_popup_menu : + < event : < connect : < button_press : callback:(GdkEvent.Button.t -> bool) -> + 'a; .. >; .. >; .. > -> + bouton_souris -> + (unit -> bool) -> GToolbox.menu_entry list -> unit + +(** [gtk_tools_connect_func_popup_menu wid button test_cond_func get_menu_entries] + connecte un popup menu construit de manière dynamique à un widget : + - [wid] désigne le widget concerné + - [button] le bouton à presser pour afficher le popup menu + - [test_cond_func] fonction de test indiquant si le menu doit etre affiché + - [get_menu_entries] désigne la fonction appelée au moment de l'appui sur + le bouton et qui renvoie la liste des éléments du menu *) +val connect_func_popup_menu : + < event : < connect : < button_press : callback:(GdkEvent.Button.t -> bool) -> + 'a; .. >; .. >; .. > -> + bouton_souris -> + (unit -> bool) -> (unit -> GToolbox.menu_entry list) -> unit + +(** [gtk_tools_create_popup_menu title event data] crée un popup menu la où se + trouve la souris. [data] est une liste d'éléments indiquant le contenu du menu + sous la forme [(texte, Some fonction, parametre fonction, sous menu)]. + [titre] désigne le titre du menu, peut etre egal à "" pour ne pas mettre + de titre *) +val create_popup_menu : + string -> + GdkEvent.Button.t -> + (string * ('a -> unit) option * 'a * + (string * ('b -> unit) option * 'b) list) + list -> unit + +(** [gtk_tools_create_optionmenu lst_names func_active func_select pack_method] + creation d'un menu à options : + - [lst_names] contient la liste des options avec leur type sous la forme + [(nom, type)] + - [func_active] indique quelle est l'option active par defaut + - [func_select] appelée lors de la modification de l'option + - [pack_method] désigne l'endroit où mettre le menu + + Renvoie le menu ainsi qu'une fonction permettant de mettre à jour l'option + courante et une seconde fonction mettant à jour l'option courante et qui + appelle la fonction [func_select] en plus : + [(option_menu, set_option, set_option_and_activate)] + *) +val create_optionmenu : + (string * 'a) list -> + ('a -> bool) -> + ('a -> unit) -> (GObj.widget -> unit) -> + GMenu.option_menu * ('a -> unit) * ('a -> unit) + +(** [separateur dans un menu] *) +val menu_separator : string * (unit -> unit) + +(** [gtk_tools_create_simple_menu menu lst_items] crée un menu attaché à [menu] + et défini par [lst_items]. [lst_items] est une liste d'éléments du type + [(texte_menu, action)] définissant les éléments du menu à créer *) +val create_simple_menu : + #GMenu.menu_shell -> (string * (unit -> unit)) list -> unit + +(** [gtk_tools_create_menu menus nb_menus lst_items] : identique à la fonction + précédente sauf que [menus] désigne un tableau de menus et [nb_menus] la référence + sur une variable indiquant le menu courant dans ce tableau *) +val create_menu : + #GMenu.menu_shell array -> + int ref -> (string * (unit -> unit)) list -> unit + +(** Initialise la variable de stockage des menus (permet de les activer ou de les + desactiver) *) +val init_menus_sens : unit -> (string, (string*GMenu.menu_item) list) Hashtbl.t + +(** [gtk_tools_create_simple_menu_sens menu lst_items] crée un menu dont les sous-menus + peuvent etre activés ou désactivés *) +val create_simple_menu_sens : #GMenu.menu_shell -> GToolbox.menu_entry list -> + (string * GMenu.menu_item) list + +(** [gtk_tools_create_menu_sens menus tab_menus_names store_menus nb_menus lst_items] créé un menu + dont les sous-menus peuvent etre rendus actifs ou inactifs. [menus] désigne le tableau des menus, + [tab_menus_names] est un tableau contenant les noms des ces menus, [store_menus] contient la + variable de stockage créée avec {!Gtk_tools.gtk_tools_init_menus_sens}, nb_menus est une référence + sur la variable contenant le menu en cours de création. *) +val create_menu_sens : #GMenu.menu_shell array -> + string array -> (string, (string*GMenu.menu_item) list) Hashtbl.t -> + int ref -> GToolbox.menu_entry list -> unit + +(** [gtk_tools_set_sub_menu_sensitive store_menus menu_name sub_menu_name sensitive] active ou + désactive (suivant la valeur de [sensitive]) le sous-menu de nom [sub_menu_name] dans le + menu de nom [menu_name] *) +val set_sub_menu_sensitive : + (string, (string*GMenu.menu_item) list) Hashtbl.t -> string -> string -> bool -> unit + + +(** {6 Pixmaps} *) + + +(** pixmap d'ouverture d'un fichier *) +val open_file_pixmap : string array + +(** [gtk_tools_pixmap_from_file filename window] création d'un [GDraw.pixmap] à + partir d'un fichier xpm *) +val pixmap_from_file : string -> GWindow.window -> GDraw.pixmap + +(** [gtk_tools_create_pixmap window width height] crée une pixmap de taille + [width]*[height] où [window] désigne la fenetre mère de la pixmap. + En retour, un couple [(pix, pixmap)] où [pix] sert à mettre la pixmap dans + une zone de dessin (par ex.) et [pixmap] sert au dessin *) +val create_pixmap : + GWindow.window -> int -> int -> Gdk.pixmap * GDraw.pixmap + +(** [gtk_tools_create_d_pixmap window width height] crée un drawable de taille + [width]*[height] où [window] désigne la fenetre mère du drawable. + En retour, un couple [(pix, pixmap)] où [pix] sert à mettre le drawable dans + une zone de dessin (par ex.) et [pixmap] sert au dessin *) +val create_d_pixmap : + GWindow.window -> int -> int -> Gdk.pixmap * GDraw.drawable + +(** [gtk_tools_create_stipple_pixmap_from_data data width height] crée une pixmap + utilisable pour faire un stipple a partir de [data] qui est une liste d'entiers *) +val create_stipple_pixmap_from_data : int list -> int -> int -> Gdk.pixmap + +(** [gtk_tools_rectangle_pixmap window color width height] crée une pixmap + rectangulaire de taille [width]*[height] et de couleur [color] *) +val rectangle_pixmap : GWindow.window -> GDraw.color -> int -> int -> + GDraw.pixmap + + +(** {6 Fenetres} *) + + +(** [gtk_tools_create_window title width height] crée une fenetre contenant une + boite verticale. Si [width] et [height] sont nuls, la fenetre n'a pas de + taille par défaut *) +val create_window : + string -> int -> int -> GWindow.window * GPack.box + +(** [gtk_tools_create_window_on_top title width height window] *) +val create_window_on_top : + string -> int -> int -> GWindow.window -> GWindow.window * GPack.box + +(** [gtk_tools_create_window_on_top2 title width height window] *) +val create_window_on_top2 : + string -> int -> int -> GWindow.window option -> GWindow.window * GPack.box + +(** [gtk_tools_create_modal_window title width height] est identique à la fonction + précédente sauf que la fenetre est modale *) +val create_modal_window : + string -> int -> int -> GWindow.window * GPack.box + +(** [gtk_tools_create_modal_window_on_top title width height window] *) +val create_modal_window_on_top : + string -> int -> int -> GWindow.window -> GWindow.window * GPack.box + +(** [gtk_tools_create_modal_window_on_top2 title width height window] *) +val create_modal_window_on_top2 : + string -> int -> int -> GWindow.window option -> GWindow.window * GPack.box + +(** [gtk_tools_create_window_with_menubar title width height menubar_items] crée + une fenetre contenant une barre de menus *) +val create_window_with_menubar : + string -> + int -> + int -> + string list -> + GWindow.window * GPack.box * GMenu.menu_shell GMenu.factory * + Gtk.accel_group * GMenu.menu array + +(** [gtk_tools_create_window_with_menubar_help title width height menubar_items] + crée une fenetre avec une barre de menus et un menu d'aide à gauche de + cette barre *) +val create_window_with_menubar_help : + string -> + int -> + int -> + string list -> + GWindow.window * GPack.box * GMenu.menu_shell GMenu.factory * + Gtk.accel_group * GMenu.menu array * GMenu.menu + +(** [gtk_tools_window_set_front window] met la fenetre [window] en avant plan *) +val window_set_front : GWindow.window -> unit + +(** [gtk_tools_get_window_geometry window] renvoie la position et la taille de + la fenetre sous la forme [((x, y), (largeur, hauteur))] *) +val get_window_geometry : + GWindow.window -> (int * int) * (int * int) + +(** set_window_position window (x, y) déplace la fenetre pour que son + coin superieur à gauche soit à la position [(x, y)] *) +val set_window_position : GWindow.window -> int * int -> unit + +(** [gtk_tools_window_modify_connect window callback] connection d'un callback + appelé lors du deplacement ou du changement de taille de la fenetre [window]. + Le callback reçoit en paramètres la position et la taille de la fenetre *) +val window_modify_connect : + GWindow.window -> ((int * int) * (int * int) -> 'a) -> GtkSignal.id + +(** [gtk_tools_connect_win_focus_change window focus_in focus_out] connecte les + callbacks correspondants aux événements focus_in et focus_out à une fenetre *) +val connect_win_focus_change : + GWindow.window -> (unit -> 'a) option -> (unit -> 'b) option -> unit + + + +(** {6 Zones de dessin (Drawing Areas)} *) + + +(** [gtk_tools_create_draw_area_simple width height pack_method pix_expose] crée + une zone de dessin simple où [pix_expose] désigne la pixmap à utiliser pour + redessiner la zone après un event expose. En retour, un couple contenant + la zone créée et la fonction de mise à jour du dessin est renvoyé *) +val create_draw_area_simple : + int -> + int -> + (GObj.widget -> unit) -> Gdk.pixmap -> GMisc.drawing_area * (unit -> unit) + +(** [gtk_tools_area_mouse_connect area mouse_press mouse_move mouse_release] + connecte les événements souris à la zone de dessin [area] *) +val area_mouse_connect : + GMisc.drawing_area -> + (GdkEvent.Button.t -> bool) -> + (GdkEvent.Motion.t -> bool) -> (GdkEvent.Button.t -> bool) -> unit + +(** [gtk_tools_area_key_connect area key_press key_release] connecte les + événements claviers à la zone de dessin [area] *) +val area_key_connect : + GMisc.drawing_area -> (Gdk.keysym -> bool) -> (Gdk.keysym -> bool) -> unit + +(** [gtk_tools_create_draw_area width height pack_method pix_expose + mouse_press mouse_move mouse_release] crée une zone de dessin et connecte + les événements souris *) +val create_draw_area : + int -> + int -> + (GObj.widget -> unit) -> + Gdk.pixmap -> + (GdkEvent.Button.t -> bool) -> + (GdkEvent.Motion.t -> bool) -> (GdkEvent.Button.t -> bool) -> unit -> unit + + +(** {6 Sélection de couleurs} *) + + +(** [gtk_tools_select_color update_func] crée une boite de selection de couleur. + La fonction [update_func] est appelée après sélection avec en paramètre la + couleur choisie *) +val select_color : ([> `RGB of int * int * int] -> unit) -> unit + +(** [gtk_tools_create_color_selection_button window taille_x taille_y color + pack_method callback] crée un bouton coloré permettant le choix d'une couleur. + - [window] désigne la fenetre mère + - [taille_x] largeur du bouton + - [taille_y] hauteur du bouton + - [color] couleur initiale du bouton + - [pack_method] indique où mettre le bouton + - [callback] fonction appelée lorsqu'une nouvelle couleur est selectionnée. + Cette fonction reçoit en paramètre la nouvelle couleur + + Lors du choix d'une nouvelle couleur, la couleur du bouton n'est pas mise à jour + *) +val create_color_selection_button : + < misc : #GDraw.misc_ops; .. > -> + int -> + int -> + GDraw.color -> + (GObj.widget -> unit) -> + ([> `RGB of int * int * int] -> unit) -> GButton.button + +(** [gtk_tools_create_color_selection_button2 window taille_x taille_y color + pack_method callback] identique à la fonction précédente sauf que la couleur + selectionnee est mise à jour dans le bouton *) +val create_color_selection_button2 : + < misc : #GDraw.misc_ops; .. > -> + int -> + int -> + GDraw.color -> + (GObj.widget -> unit) -> (GDraw.color -> unit) -> GButton.button + +(** [gtk_tools_select_colors_widget window colors tooltips update_func vbox] crée + un widget de selection de plusieurs couleurs. [colors] est une liste d'éléments + au format [(nom_frame, (label, couleur ref, couleur_defaut) list)] *) +val select_colors_widget : + GWindow.window -> + (string * (string * GDraw.color ref * GDraw.color) list) list -> + GData.tooltips option -> (unit -> unit) -> GPack.box -> unit + +(** [gtk_tools_select_colors title width height colors tooltips update_func] crée + une fenetre de sélection de plusieurs couleurs *) +val select_colors : + string -> + int -> + int -> + (string * (string * GDraw.color ref * GDraw.color) list) list -> + GData.tooltips option -> (unit -> unit) -> unit + + +(** {6 Captures d'écran} *) + + +(** [creation_fen_capture default_filename default_format tooltips with_caption] + fonction interne de création d'une boite de capture d'écran *) +val creation_fen_capture : + string -> + Gtk_image.format_capture -> + GData.tooltips option -> + bool -> + string ref * Gtk_image.format_capture ref * + (Gtk_image.progress_save -> float -> unit) option * [> `DELETE_EVENT ] GWindow.dialog * + GButton.button * GEdit.entry * + (unit -> + (string * GDraw.color * GDraw.color * GDraw.color * Gdk.font) option) + +(** [gtk_tools_screenshot_box default_filename default_format drawable + tooltips x y width height] crée une boite de capture d'écran sans légende *) +val screenshot_box : + string -> + Gtk_image.format_capture -> + [> `drawable ] Gobject.obj -> + GData.tooltips option -> int -> int -> int -> int -> unit + +(** [gtk_tools_screenshot_box_with_func default_filename default_format drawable + tooltips x y width height after_func] identique à la fonction précédente sauf + que la fonction [after_func] est appelée après la capture *) +val screenshot_box_with_func : + string -> + Gtk_image.format_capture -> + [> `drawable ] Gobject.obj -> + GData.tooltips option -> int -> int -> int -> int -> (unit -> unit) -> unit + +(** [gtk_tools_screenshot_box_with_caption default_filename default_format + drawable tooltips x y width height] boite de capture écran avec légende. Ne + fonctionne qu'avec des pixmaps à cause de la légende *) +val screenshot_box_with_caption : + string -> + Gtk_image.format_capture -> + Gdk.pixmap -> GData.tooltips option -> int -> int -> int -> int -> unit + + +(** {6 Sélection de valeurs entières} *) + + +(** [gtk_tools_create_int_spinner_simple label lab_width init_value min_value + max_value value_width step_incr page_incr tip tooltips pack_method] crée un + widget de sélection d'une valeur entière *) +val create_int_spinner_simple : + string -> + int -> + int -> + int -> + int -> + int -> + int -> + int -> + string -> GData.tooltips -> (GObj.widget -> unit) -> GEdit.spin_button + +(** [gtk_tools_int_spinner_connect sp callback] connecte le callback à un widget de + sélection de valeur entière. Le callback est appelé avec la valeur selectionnée + à chaque fois que celle-ci change *) +val int_spinner_connect : GEdit.spin_button -> (int -> unit) -> unit + +(** [gtk_tools_create_int_spinner label lab_width init_value min_value max_value + value_width step_incr page_incr tip tooltips pack_method callback] crée un + widget de sélection de valeur entière avec callback *) +val create_int_spinner : + string -> + int -> + int -> + int -> + int -> + int -> + int -> + int -> + string -> + GData.tooltips -> + (GObj.widget -> unit) -> (int -> unit) -> GEdit.spin_button + +(** [gtk_tools_create_vslider_simple init_val min_val max_val step page draw_val + pack_method] création d'un slider vertical de sélection d'une valeur entière *) +val create_vslider_simple : + int -> + int -> + int -> + int -> + int -> bool -> (GObj.widget -> unit) -> GData.adjustment * GRange.scale + +(** [gtk_tools_create_hslider_simple init_val min_val max_val step page draw_val + pack_method] création d'un slider horizontal de sélection d'une valeur entière *) +val create_hslider_simple : + int -> + int -> + int -> + int -> + int -> bool -> (GObj.widget -> unit) -> GData.adjustment * GRange.scale + +(** [gtk_tools_slider_connect slider callback] connection d'un callback de + modification d'un slider de valeur entière *) +val slider_connect : + GData.adjustment -> (int -> unit) -> GtkSignal.id + +(** [gtk_tools_create_vslider init_val min_val max_val step page draw_val + pack_method callback] création d'un slider vertical de sélection d'une valeur + entière avec callback *) +val create_vslider : + int -> + int -> + int -> + int -> + int -> + bool -> + (GObj.widget -> unit) -> (int -> unit) -> GData.adjustment * GRange.scale + +(** [gtk_tools_create_hslider init_val min_val max_val step page draw_val + pack_method callback] création d'un slider horizontal de sélection d'une valeur + entière avec callback *) +val create_hslider : + int -> + int -> + int -> + int -> + int -> + bool -> + (GObj.widget -> unit) -> (int -> unit) -> GData.adjustment * GRange.scale + + +(** {6 Sélection de valeurs flottantes} *) + + +(** [gtk_tools_create_float_spinner_simple label lab_width init_value min_value + max_value value_width nb_digits step_incr page_incr tip tooltips pack_method] + crée un widget de sélection d'une valeur flottante *) +val create_float_spinner_simple : + string -> + int -> + float -> + float -> + float -> + int -> + int -> + float -> + float -> + string -> GData.tooltips -> (GObj.widget -> unit) -> GEdit.spin_button + +(** [gtk_tools_float_spinner_connect sp callback] connecte le callback à un widget de + sélection de valeur flottante. Le callback est appelé avec la valeur selectionnée + à chaque fois que celle-ci change *) +val float_spinner_connect : GEdit.spin_button -> (float -> unit) -> unit + +(** [gtk_tools_create_float_spinner label lab_width init_value min_value max_value + value_width nb_digits step_incr page_incr tip tooltips pack_method callback] + crée un widget de sélection de valeur flottante avec callback *) +val create_float_spinner : + string -> + int -> + float -> + float -> + float -> + int -> + int -> + float -> + float -> + string -> + GData.tooltips -> + (GObj.widget -> unit) -> (float -> unit) -> GEdit.spin_button + + +(** {6 Zones de texte} *) + + +(** [gtk_tools_create_text_entry_simple label lab_width init_value value_width + tip tooltips pack_method] : zone de sélection de texte. Renvoie le label et + la zone de sélection de texte *) +val create_text_entry_simple : + string -> + int -> + string -> + int -> + string -> + GData.tooltips -> (GObj.widget -> unit) -> GMisc.label * GEdit.entry + +(** [gtk_tools_text_entry_connect entry callback] connecte un callback lié à + l'appui sur la touche entrée dans la zone [entry] *) +val text_entry_connect : + GEdit.entry -> (string -> unit) -> GtkSignal.id + +(** [gtk_tools_text_entry_connect_modify entry callback] connecte un callback + appelé à chaque fois que le texte de la zone [entry] est modifié. Le callback + reçoit la chaine contenue dans la zone en majuscules *) +val text_entry_connect_modify : + GEdit.entry -> (string -> unit) -> GtkSignal.id + +(** [gtk_tools_create_text_entry label lab_width init_value value_width + tip tooltips pack_method callback] zone de sélection de texte avec callback + lié à la modification du texte dans la zone *) +val create_text_entry : + string -> + int -> + string -> + int -> + string -> + GData.tooltips -> + (GObj.widget -> unit) -> (string -> unit) -> GMisc.label * GEdit.entry + +(** [gtk_tools_text_entry_select_text entry] selectionne le texte présent dans + la zone de texte *) +val text_entry_select_text : GEdit.entry -> unit + +(** [gtk_tools_create_text_edit editable with_vert_scroll with_hor_scroll pack_method] + crée une zone de texte multiligne (editeur) avec éventuellement des barres de + défilement ([with_vert_scroll] et [with_hor_scroll]). [editable] indique si + la zone créée est éditable par l'utilisateur *) +val create_text_edit : bool -> bool -> bool -> (GObj.widget -> unit) + -> GText.view + +(** [gtk_tools_text_edit_clear edit] efface le contenu d'une zone de texte *) +val text_edit_clear : GText.view -> unit + +(** [gtk_tools_text_edit_get_text edit] renvoie l'intégralité du texte contenu + dans [edit] *) +val text_edit_get_text : GText.view -> string + +(** [gtk_tools_text_edit_get_lines edit] identique à la fonction précédente sauf + que le texte contenu dans le widget est renvoyé sous la forme d'une liste de + chaines de caractères, chacune d'elles correspondant à une ligne dans la + zone de texte *) +val text_edit_get_lines : GText.view -> string list + +(** [gtk_tools_text_edit_set_text_list edit text_lst] insère la liste de chaines + de caractères dans la zone de texte *) +val text_edit_set_text_list : GText.view -> string list -> unit + +(** [gtk_tools_text_edit_set_text edit text] insère le texte indiqué dans la + zone de texte *) +val text_edit_set_text : GText.view -> string -> unit + + +(** {6 Fenetres enregistrées} *) + + +(** Type pour les fenetres enregistrées *) +type registered_win = { + reg_win_id : int; + mutable reg_win_handle : GWindow.window option; + reg_win_build : unit -> GWindow.window; +} + +(** Exception levée lors de l'appel à un numéro de fenetre non enregistrée *) +exception GTK_TOOLS_UNREGISTERED_WINDOW of int + +(** [gtk_tools_register_window build_window_func] enregistre une fenetre. + [build_window_func] est la fonction de création de la fenetre en question *) +val register_window : (unit -> GWindow.window) -> int + +(** [gtk_tools_show_registered_window id] crée la fenetre enregistrée designée par + [id]. Si la fenetre existe déjà elle est mise en avant plan *) +val show_registered_window : int -> unit + +(** [gtk_tools_hide_registered_window id] détruit la fenetre enregistrée si elle + existe *) +val hide_registered_window : int -> unit + +(** [gtk_tools_get_registered_window id] renvoie la fenetre enregistrée + correspondant à l'identifiant [id] *) +val get_registered_window : int -> GWindow.window option + + +(** {6 Listes} *) + + +(** [gtk_tools_create_list lst_titles (sortable_titles, first_sort) pack_method] + crée une liste : + - [lst_titles] liste des titres des colonnes + - [sortable_titles] les titres sont-ils sélectionnables pour trier la liste ? + - [first_sort] indique quelle est la colonne qui sert de tri initialement + - [pack_method] où mettre la liste *) +val create_list : + string list -> bool * int -> (GObj.widget -> unit) -> string GList.clist + +(** [gtk_tools_create_list_with_hor_scroll lst_titles + (sortable_titles, first_sort) pack_method] crée une liste avec une barre de + scroll horizontale *) +val create_list_with_hor_scroll : + string list -> bool * int -> (GObj.widget -> unit) -> string GList.clist + +(** [gtk_tools_set_columns_sizes list sizes] met à jour la taille des colonnes + de la liste [list] *) +val set_columns_sizes : string GList.clist -> int list -> unit + +(** [gtk_tools_list_connect clist + callback_select callback_deselect callback_select_column] connecte les + événements sélection/désélection et sélection d'un titre de colonne. Les + callbacks [callback_select] et [callback_deselect] reçoivent en paramètres + la ligne et la colonne (dé)sélectionnées. [callback_select_column] reçoit le + numéro de la colonne *) +val list_connect : + string GList.clist -> + (int -> int -> unit) option -> + (int -> int -> unit) option -> (int -> unit) option -> unit + +(** [gtk_tools_list_connect_check_dbl_click clist + callback_select callback_deselect callback_select_column] meme chose que la + fonction précédente sauf que l'on teste s'il y a double click. Ici, + [callback_select] et [callback_deselect] reçoivent la ligne, la colonne et + un booleen indiquant s'il y a eu double click *) +val list_connect_check_dbl_click : + string GList.clist -> + (int -> int -> bool -> unit) option -> + (int -> int -> bool -> unit) option -> (int -> unit) option -> unit + +(** [gtk_tools_list_connect_up_down_keys clist callback_up callback_down] + connecte les callbacks liés à l'appui sur les touches flechées haut et bas *) +val list_connect_up_down_keys : + string GList.clist -> (unit -> 'a) -> (unit -> 'b) -> unit + +(** [gtk_tools_create_managed_list titles_sizes pack_method] crée une liste managée. + [titles_sizes] est une liste du type [(nom, taille)] indiquant le titre + ainsi que la largeur des différentes colonnes. Cet objet contient en plus + un label indiquant le nombre d'éléments contenus dans la liste. + + En retour, la liste et le label sont renvoyes *) +val create_managed_list : + (string * int) list -> + (GObj.widget -> unit) -> string GList.clist * GMisc.label + +(** [gtk_tools_connect_managed_list (lst, lab) item_column selection_callback + (name, female, cap)] connecte le callback de (dé)sélection à une liste + managée, les callbacks de selection avec les touches flechées et indique le + format du label de titre : + - [(lst, lab)] = la liste et le label renvoyés par la fonction précédente + - [item_column] = numéro de la colonne contenant l'item de référence + - [selection_callback] = la fonction appelée lors d'une (dé)sélection. + Elle appelée avec en paramètres : l'élément sélectionné (i.e élément de la ligne + sélectionnée et dans la colonne [item_column]), un triplet [(ligne, colonne, + double_ckick)] et un booleen valant vrai si sélection et faux si désélection + - [(name, female, cap)] indique le format à utiliser pour afficher le nombre + d'éléments de la liste (voir [eval_string]) + + En retour est renvoyée la fonction permettant de mettre à jour le contenu de + la liste. Cett fonction prend en paramètres : + - l'identifiant de la ligne à sélectionner par defaut ("" si pas de + sélection initiale) + - une liste contenant des listes correspondant aux différentes lignes *) +val connect_managed_list : + string GList.clist * GMisc.label -> + int -> + (string -> int * int * bool -> bool -> unit) -> + string * bool * bool -> string -> string list list -> unit + + +(** {6 Widget Lat/Lon} *) + + +(** Type contenant un widget de sélection de coordonnées lat/lon *) +type latlon = { + latlon_lat_val : float ref; + latlon_lon_val : float ref; + latlon_update : float -> float -> unit; + latlon_change_callback : (unit -> unit) option ref; +} + +(** [gtk_tools_create_latlon_selection (lat, lon) pack_method tooltips] crée un + widget de sélection de coordonnées géographiques lat/lon *) +val create_latlon_selection : + float * float -> + (GObj.widget -> unit) -> GData.tooltips -> latlon + +(** [gtk_tools_update_latlon_selection latlon_widget new_lat new_lon] met à jour + les coordonnées dans un widget de sélection lat/lon *) +val update_latlon_selection : + latlon -> float -> float -> unit + +(** [gtk_tools_latlon_selection_get latlon_widget] recupère les coordonnées + selectionnées dans le widget *) +val latlon_selection_get : latlon -> float * float + +(** [gtk_tools_latlon_selection_change latlon_widget callback] connecte un + callback appelé lors de la modification des coordonnées. Il ne prend pas de + paramètre : pour avoir connaitre la valeur des coordonnées, il faut utiliser + {!Gtk_tools.gtk_tools_latlon_selection_get} *) +val latlon_selection_change : + latlon -> (unit -> unit) -> unit + + +(** {6 Fenetre de Log} *) + +(** Exception levée lors de l'ajout de texte dans la fenetre de log alors que + celle-ci n'a pas encore ete créée *) +exception GTK_TOOLS_NO_LOG_WIN + +(** renvoie la zone de texte de la fenetre de log si cette derniere a ete créée. + Si ce n'est pas le cas alors l'exception {!Gtk_tools.GTK_TOOLS_NO_LOG_WIN} + est levée *) +val get_log_wid : unit -> GText.view + +(** [gtk_tools_add_log text level] ajoute un texte dans la fenetre de log + si [level]<[log_verbose_level]*) +val add_log : string -> int -> unit + +(** [gtk_tools_add_log_with_color text level color] meme chose que la + fonction précédente sauf que [color] indique la couleur du texte à ajouter *) +val add_log_with_color : string -> int -> GDraw.color -> unit + +(** efface le contenu de la fenetre de log *) +val clear_log : unit -> unit + +(** [gtk_tools_create_log tooltips] crée la fenetre de log *) +val create_log : GData.tooltips -> unit + +(** force l'affichage de la fenetre de log *) +val show_log : unit -> unit + +(** détruit la fenetre de log *) +val hide_log : unit -> unit + +(** [gtk_tools_set_log_verbose_level level] met à jour le niveau de verbose + dans la fenetre de log *) +val set_log_verbose_level : int -> unit + + +(** {6 Barres de progression} *) + + +(** [gtk_tools_create_progress_bar_win nb_blocks title] crée une barre de progression + dans une fenetre externe. [nb_blocks] désigne le nombre de subdivisions de + la barre. En sortie, la fonction de mise à jour de la barre de progression + est renvoyée. Cette fonction prend en paramètre un flottant compris entre + 0.0 et 1.0, lorsqu'on lui passe 1.0, la fenetre est fermée *) +val create_progress_bar_win : int -> string -> float -> unit + +(** [gtk_tools_create_progress_bar pack_method] creation d'une barre de + progression continue et sans fenetre (donc différente de + {!Gtk_tools.gtk_tools_create_progress_bar_win}. + La fonction de mise à jour de la progression est renvoyée et prend en + paramètre un flottant compris entre 0.0 et 1.0 *) +val create_progress_bar : (GObj.widget -> unit) -> float -> unit + + +(** {6 Widget de selection d'opérateur de comparaison} *) + + +(** Opérateurs de comparaison *) +type t_ops_compare = T_EQ | T_L | T_LEQ | T_G | T_GEQ + +(** [gtk_tools_create_ops_compare variable callback_modified pack_method] crée un + widget de sélection d'opérateur de comparaison. + - [variable] est une référence sur l'opérateur en cours + - [callback_modified] est appelé lorsque l'opérateur est modifié. Cette fonction + est optionnelle. Si elle est utilisée alors elle prend en paramètre la + valeur courante de l'opérateur de comparaison + - [pack_method] indique où mettre le widget de sélection + *) +val create_ops_compare : + t_ops_compare ref -> + (t_ops_compare -> unit) option -> (GObj.widget -> unit) -> unit + + +(** {6 Widget de sélection d'une heure} *) + + +(** [gtk_tools_create_time_select variable callback_modified pack_method] crée un + widget de sélection d'heure. [variable] est une référence sur un entier + contenant l'heure en secondes *) +val create_time_select : + int ref -> (int -> unit) option -> (GObj.widget -> unit) -> unit + + +(** {6 Fenetre d'affichage d'infos} *) + + +(** [gtk_tools_create_infos_win title width height] crée une fenetre d'affichage + d'informations sous forme de labels. + + En retour est renvoyé un couple contenant la fonction d'ajout d'un nouveau texte + ainsi que la fonction de fermeture de la fenetre *) +val create_infos_win : + string -> int -> int -> (string -> unit) * (unit -> unit) + + +(** {6 Widget affichant le contenu d'un fichier texte} *) + + +(** [gtk_tools_display_file filename title width height tooltips font] affiche + le contenu d'un fichier dans une fenetre *) +val display_file : + string -> string -> int -> int -> GData.tooltips -> string option -> unit + + +(** {6 Combo box} *) + + +(** [gtk_tools_create_combo lst_items pack_method] crée une combo box *) +val create_combo_simple : + string list -> (GObj.widget -> unit) -> GEdit.entry + +(** [gtk_tools_combo_connect entry lst_items callback] connecte [callback] qui est + appelé lors de la modification de la valeur dans la combo box *) +val combo_connect : GEdit.entry -> (string*'a) list -> ('a->unit) -> unit + + +(** {6 Widget calendrier} *) + +(** [gtk_tools_calendar lst_dates callback_select only_available_dates_selectable + init_with_last_available_date tooltips win pack_method] crée un widget + calendrier permettant de choisir une date. Les paramètres sont : + + - [lst_dates] contient (éventuellement) la liste des dates autorisées sous + la forme d'un entier (ex. : 20030721) + - [callback_select] désigne la fonction appelée après sélection. La date choisie + est passée en paramètre + - [only_available_dates_selectable] indique si celles les dates autorisées sont + selectionnables (ainsi, seules ces dernières peuvent appeler [callback_select]) + - [init_with_last_available_date] sert à l'initialisation du calendrier. + Si cette valeur vaut vrai, le calendrier affiche le mois et l'année correspondant + à la date la plus tardive parmi les dates autorisées. Sinon, le mois courant + est affiché + - [tooltips] désigne le système d'aide contextuelle + - [win] correspond à la fenetre mère + - [pack_method] indique où mettre le widget + *) +val calendar : int list -> (int -> unit) -> bool -> bool -> + GData.tooltips -> GWindow.window -> (GObj.widget -> unit) -> unit + +(** [gtk_tools_calendar_window lst_dates callback_select only_available_dates_selectable + init_with_last_available_date is_modal tooltips] crée une boite de dialogue + affichant un calendrier et permettant de choisir une date. + + Les paramètres utilisés sont les suivants : + + - [lst_dates] contient (éventuellement) la liste des dates autorisées sous + la forme d'un entier (ex. : 20030721) + - [callback_select] désigne la fonction appelée après sélection. La date choisie + est passée en paramètre + - [only_available_dates_selectable] indique si celles les dates autorisées sont + selectionnables (ainsi, seules ces dernières peuvent appeler [callback_select]) + - [init_with_last_available_date] sert à l'initialisation du calendrier. + Si cette valeur vaut vrai, le calendrier affiche le mois et l'année correspondant + à la date la plus tardive parmi les dates autorisées. Sinon, le mois courant + est affiché + - [is_modal] indique si la fenetre doit etre modale + *) +val calendar_window : int list -> (int -> unit) -> bool -> bool -> bool -> + GData.tooltips -> unit diff --git a/sw/lib/ocaml/gtk_tools_GL.ml b/sw/lib/ocaml/gtk_tools_GL.ml new file mode 100644 index 00000000000..e7e4080e15c --- /dev/null +++ b/sw/lib/ocaml/gtk_tools_GL.ml @@ -0,0 +1,149 @@ +(* + * $Id$ + * + * OpenGL utils + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) +(* = YLF 28/10/2001 = *) +(* = = *) +(* = Derniere update : 28/10/2002 = *) +(* = = *) +(* = = *) +(* = 28/10/2002 : create_draw_glarea_base et = *) +(* = et connect_draw_glarea_simple = *) +(* = 15/05/2002 : gl_to_gtk_color et gtk_to_gl_color = *) +(* = 12/04/2002 : create_draw_glarea_simple = *) +(* = = *) +(* ================================================================================== *) + +open Gtk_tools + +(* ============================================================================= *) +(* = Creation d'une drawing area OpenGL = *) +(* = = *) +(* = width = hauteur de la zone = *) +(* = height = hauteur de la zone = *) +(* = pack_method = maniere de placer la zone (ex. : hbox#pack) = *) +(* ============================================================================= *) +let create_draw_glarea_base width height pack_method = + (* Creation des widgets *) + GlGtk.area [`DEPTH_SIZE 1; `RGBA; `DOUBLEBUFFER] + ~width:width ~height:height ~packing:pack_method () + +(* ============================================================================= *) +(* = Connection des fonctions de base a une drawing area OpenGL = *) +(* = = *) +(* = area = la zone = *) +(* = init_func = fonction d'initialisation (realize) = *) +(* = display_func = fonction de dessin dans la zone = *) +(* = reshape_func = appelee lors d'un changement de taille = *) +(* ============================================================================= *) +let connect_draw_glarea_simple area + init_func display_func reshape_func = + (* La nouvelle fonction de dessin appelle celle qui est passee en parametre quand *) + (* necessaire et fait le flush ensuite *) + let draw = (fun () -> if (area:GlGtk.area)#misc#visible then begin + display_func (); Gl.flush (); area#swap_buffers () + end) in + + (* Connection des fonctions *) + ignore(area#connect#realize ~callback:init_func) ; + ignore(area#connect#display ~callback:draw) ; + ignore(area#connect#reshape ~callback:reshape_func) ; + + (* Renvoie la nouvelle fonction de dessin *) + draw + +(* ============================================================================= *) +(* = Creation d'une drawing area OpenGL = *) +(* = = *) +(* = width = hauteur de la zone = *) +(* = height = hauteur de la zone = *) +(* = pack_method = maniere de placer la zone (ex. : hbox#pack) = *) +(* = init_func = fonction d'initialisation (realize) = *) +(* = display_func = fonction de dessin dans la zone = *) +(* = reshape_func = appelee lors d'un changement de taille = *) +(* ============================================================================= *) +let create_draw_glarea_simple width height pack_method + init_func display_func reshape_func = + (* Creation des widgets *) + let area = create_draw_glarea_base width height pack_method in + + let draw = connect_draw_glarea_simple area + init_func display_func reshape_func in + + (* Renvoie la zone de dessin et la nouvelle fonction de dessin *) + (area, draw) + +(* ============================================================================= *) +(* = Connexion evenements souris a une zone de dessin OpenGL = *) +(* = = *) +(* = area = la zone de dessin = *) +(* = mouse_press = fonction appelee lors d'un click souris = *) +(* = mouse_move = fonction appelee lors d'un deplacement = *) +(* = mouse_release = fonction appelee lors du relachement d'un bouton = *) +(* ============================================================================= *) +let glarea_mouse_connect area mouse_press mouse_move mouse_release = + (area:GlGtk.area)#event#add [`POINTER_MOTION; `BUTTON_PRESS; `BUTTON_RELEASE] ; + area#event#set_extensions `ALL; + ignore(area#event#connect#button_press ~callback:mouse_press) ; + ignore(area#event#connect#motion_notify ~callback:mouse_move) ; + ignore(area#event#connect#button_release ~callback:mouse_release) + +(* ============================================================================= *) +(* = Connexion evenements clavier a une zone de dessin = *) +(* = = *) +(* = area = la zone de dessin = *) +(* = key_press = fonction appelee lors de l'appui sur une touche = *) +(* = key_release = fonction appelee lors du relachement d'une touche = *) +(* ============================================================================= *) +let glarea_key_connect area key_press key_release = + (* Par defaut l'evenement key_release n'est pas associe au widget *) + (area:GlGtk.area)#event#add [`KEY_RELEASE] ; + ignore(area#event#connect#key_press + ~callback:(fun ev -> key_press (GdkEvent.Key.keyval ev))) ; + ignore(area#event#connect#key_release + ~callback:(fun ev -> key_release (GdkEvent.Key.keyval ev))) ; + area#misc#set_can_focus true ; + area#misc#grab_focus () + +(* ============================================================================= *) +(* = Passage de couleur GTK vers GL = *) +(* = = *) +(* = color = couleur GTK (`NAME ou `RGB) a transformer = *) +(* ============================================================================= *) +let gtk_to_gl_color color = + let t = GDraw.color color in + ((float_of_int (Gdk.Color.red t))/.65535.0, + (float_of_int (Gdk.Color.green t))/.65535.0, + (float_of_int (Gdk.Color.blue t))/.65535.0) + +(* ============================================================================= *) +(* = Passage de couleur GL vers GTK = *) +(* = = *) +(* = (r, g, b) = couleur GL a transformer en equivalent GTK = *) +(* ============================================================================= *) +let gl_to_gtk_color (r, g, b) = + `RGB(int_of_float (r*.65535.0), int_of_float (g*.65535.0), + int_of_float (b*.65535.0)) + +(* =============================== FIN ========================================= *) diff --git a/sw/lib/ocaml/gtk_tools_GL.mli b/sw/lib/ocaml/gtk_tools_GL.mli new file mode 100644 index 00000000000..b7448efc22c --- /dev/null +++ b/sw/lib/ocaml/gtk_tools_GL.mli @@ -0,0 +1,90 @@ +(* + * $Id$ + * + * OpenGL utils + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(** Module de gestion des zones de dessin OpenGL + + {b Dépendences : Platform} + + *) + + +(** {6 Drawing Areas OpenGL} *) + + +(** [create_draw_glarea_base width height pack_method] crée une zone de + dessin OpenGL de largeur [width] et de hauteur [height]. Cette zone est + placée comme indiqué dans [pack_method]. La zone créée est renvoyée *) +val create_draw_glarea_base : + int -> int -> (GObj.widget -> unit) -> GlGtk.area + +(** [connect_draw_glarea_simple area init_func display_func reshape_func] + connecte les signaux de base à une zone de dessin créée avec + {!Gtk_tools_GL.create_draw_glarea_base}. La fonction de redessin + est renvoyée *) +val connect_draw_glarea_simple : + GlGtk.area -> + (unit -> unit) -> + (unit -> 'a) -> (width:int -> height:int -> unit) -> unit -> unit + +(** [create_draw_glarea_simple width height pack_method + init_func display_func reshape_func] crée une zone de dessin OpenGL et y + connecte les signaux. Un couple contenant la zone et la fonction de + redessin est renvoyé *) +val create_draw_glarea_simple : + int -> + int -> + (GObj.widget -> unit) -> + (unit -> unit) -> + (unit -> 'a) -> + (width:int -> height:int -> unit) -> GlGtk.area * (unit -> unit) + + +(** {6 Signaux des Drawing Areas OpenGL} *) + + +(** [glarea_mouse_connect area mouse_press mouse_move mouse_release] + connecte les événements souris à la zone de dessin [area] *) +val glarea_mouse_connect : + GlGtk.area -> + (GdkEvent.Button.t -> bool) -> + (GdkEvent.Motion.t -> bool) -> (GdkEvent.Button.t -> bool) -> unit + +(** [glarea_key_connect area key_press key_release] connecte les + événements claviers à la zone de dessin [area] *) +val glarea_key_connect : + GlGtk.area -> (Gdk.keysym -> bool) -> (Gdk.keysym -> bool) -> unit + + +(** {6 Couleurs Gtk <-> OpenGL} *) + + +(** [gtk_to_gl_color color] crée une couleur OpenGL (r, g, b) à partir + de [color]. Les composantes RGB sont dans l'intervalle [\[0.0, 1.0\]] *) +val gtk_to_gl_color : GDraw.color -> float * float * float + +(** [gl_to_gtk_color (r, g, b)] fonction inverse de la precedente *) +val gl_to_gtk_color : + float * float * float -> [> `RGB of int * int * int] diff --git a/sw/lib/ocaml/gtk_tools_icons.ml b/sw/lib/ocaml/gtk_tools_icons.ml new file mode 100644 index 00000000000..794ee1c41da --- /dev/null +++ b/sw/lib/ocaml/gtk_tools_icons.ml @@ -0,0 +1,238 @@ +(* + * $Id$ + * + * Icons library + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let question_icon = + [|"48 48 69 1"; + " c #000000";". c #060708";"X c #06080a";"o c #0a0a0a";"O c #19150d"; + "+ c gray8";"@ c gray10";"# c #221c12";"$ c #393939";"% c #2b3d61"; + "& c #354564";"* c #654f24";"= c #6c5526";"- c #705627";"; c #715929"; + ": c #735f3b";"> c #7c622d";"; c #7f6941";"< c gray39";"1 c #727272"; + "2 c #737579";"3 c gray50";"4 c #81642e";"5 c #876a31";"6 c #8c6d31"; + "7 c #937435";"8 c #9b7b3a";"9 c #a27f3b";"0 c #927c52";"q c #a6823c"; + "w c #a9853d";"e c #a68748";"r c #a38e55";"t c #a48b5a";"y c #b38d40"; + "u c #bc9443";"i c #be9b53";"p c #bea363";"a c #bfa16a";"s c #bea272"; + "d c #c09745";"f c #c39f56";"g c #c9a45b";"h c #d2a64c";"j c #d8ab4e"; + "k c #d8ac5a";"l c #d8b15f";"z c #c3a466";"x c #c6a76a";"c c #c9ac73"; + "v c #d2b06c";"b c #d9b263";"n c #d8b56e";"m c #dbba73";"M c #d8ba7b"; + "N c #f7c35a";"B c #f7c96d";"V c #f7cf7e";"C c #aaaaaa";"Z c #d8be86"; + "A c #dcc494";"S c #f7d48c";"D c #f7d899";"F c #f7dca5";"G c #f7dfaf"; + "H c #f7e2b8";"J c #f7e5c0";"K c white";"L c None"; + "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLLLo LLLLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLLL 9uj8q> LLLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLLL jDDHDMMMiq LLLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLL NHHDSNNNVMlq= LLLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLLo NGFNe77wNNVnlq LLLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLL pDHN7 7NNBll4 +LLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLo BJV7 % fNVjlq LLLLLLLLLLLLLL"; + "LLLLLLLLLLLLL rSGj &33 tNBljq KLLLLLLLLLLLLL"; + "LLLLLLLLLLLLL hVNq &3KK tBBnj4 .CKLLLLLLLLLLLL"; + "LLLLLLLLLLLLL NSNq 2KKL tNnnj* CKLLLLLLLLLLLL"; + "LLLLLLLLLLLLL vlu4 3KL zBMjq 3CKLLLLLLLLLLLL"; + "LLLLLLLLLLLLL XX tKL 6vZlj; 3CKLLLLLLLLLLLL"; + "LLLLLLLLLLLLL X X3K uSZju +3KLLLLLLLLLLLLL"; + "LLLLLLLLLLLLLL 0 c #361210";"; c #242424";"< c #2c2c2c";"1 c #323232"; + "2 c #3b3b3b";"3 c #41130e";"4 c #401411";"5 c #4d1712";"6 c #4e1814"; + "7 c #571b15";"8 c #591710";"9 c #581b15";"0 c #5c1e1a";"q c #6e190f"; + "w c #671c16";"e c #6b1d15";"r c #791e15";"t c #66221d";"y c #6c231e"; + "u c #7d231c";"i c #742620";"p c #772822";"a c #7f2821";"s c #484848"; + "d c #545454";"f c #656565";"g c #7e7e7e";"h c #951f11";"j c #981f11"; + "k c #83241b";"l c #87281e";"z c #892116";"x c #8b261c";"c c #89281f"; + "v c #952214";"b c #932419";"n c #92291f";"m c #9c2213";"M c #9a261a"; + "N c #9b291d";"B c #832b24";"V c #8c2a23";"C c #8e2e28";"Z c #8f3832"; + "A c #942c24";"S c #9b2d23";"D c #943029";"F c #9e3026";"G c #9d3229"; + "H c #9e3b32";"J c #a52415";"K c #a42618";"L c #a3291c";"P c #a82211"; + "I c #aa2718";"U c #ab291b";"Y c #b22413";"T c #b12718";"R c #b5291a"; + "E c #bb2513";"W c #bc2b1a";"Q c #a12d22";"! c #a92f20";"~ c #a03026"; + "^ c #a03229";"/ c #a5392e";"( c #ab3224";") c #a9352a";"_ c #a83a2e"; + "` c #a43c33";"' c #af3f33";"] c #c32613";"[ c #c72816";"{ c #c62d1b"; + "} c #ce2e1c";"| c #d22813";" . c #d02e1b";".. c #db2a15";"X. c #df301c"; + "o. c #e22b15";"O. c #ec2d15";"+. c #e4311b";"@. c #ea311b";"#. c #ea3a1b"; + "$. c #f4321a";"%. c #f6381c";"&. c #fd3216";"*. c #fb3318";"=. c #fc3b1a"; + "-. c #c1782e";";. c #ff401e";":. c #d48d1d";">. c #cd8622";";. c #c18138"; + "<. c #c79d37";"1. c #c49c39";"2. c #ca9d31";"3. c #cda137";"4. c #cfa43c"; + "5. c #d5a82e";"6. c #dcac2b";"7. c #d4a534";"8. c #d1a53a";"9. c #d8a733"; + "0. c #d9aa32";"q. c #daad3c";"w. c #dfb035";"e. c #ddb039";"r. c #e1ae23"; + "t. c #e1b233";"y. c #cfa540";"u. c #d4aa43";"i. c #d2aa48";"p. c #d8ac40"; + "a. c #dfb445";"s. c #fdfdfd";"d. c None"; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.=.&.*.*.$.O.@.@.@.@.@. d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.=.=.&.O.| ] E Y Y Y P Y W { } } W d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.&.&...] Y P P P P j v J J J I U R W R d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.=.&.O.] Y P P P P P m m P I J J K K L U U U z d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.=.&...E P P P P P P v J J J J J I K K L K L L L r d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.=.&.| Y P P P P P P J v J J K I K ) ( K K L K L L N 8 d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.=.&.| P P P P P P P P J J m m J K K K ( ) L L N L L N k # d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.=.o.Y P P P P J P ! ! J I J m m L K K K ) ) ) Q L L Q N w o d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.=.&.E P P P J P ! ! P ( ( J K K K b m L L K L _ ) Q L L S x ; o d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.*.| P P P P P J h S ! K ( ( K K K K b b L L L L ) _ Q Q L S 7 d.d.d.d.d.d."; + "d.d.d.d.d.d.d.#.*.E P P J h I P m P K ( ( ( ) J K K L b b N Q L L _ _ ( S Q u # $ d.d.d.d.d.d."; + "d.d.d.d.d.d.d.%...P P P P j J K I I K K ( ( ' ) L L L L M b N Q Q S / _ S S c = o d.d.d.d.d.d."; + "d.d.d.d.d.d. %.[ I :.r.r.r.r.r.r.6.6.6.6.6.e.a.e.0.0.0.0.7.2.2.7.8.8.-.Q S A 3 o d.d.d.d.d."; + "d.d.d.d.d.d. $.E P r.r.r.r.r.r.6.r.6.6.6.6.6.q.e.q.0.9.9.9.9.7.7.8.7.8.Q S A 5 . o d.d.d.d.d."; + "d.d.d.d.d.d. $.E P r.r.r.t.r.r.6.r.6.6.6.0.9.5.3.q.p.9.9.9.7.4.8.8.8.y.~ Q A 7 . , d.d.d.d.d."; + "d.d.d.d.d.d. #.Y P r.r.r.r.t.t.6.6.6.6.6.6.6.9.5.2.7.p.p.7.4.7.<.1.1.1.~ F D 9 . % d.d.d.d.d."; + "d.d.d.d.d.d. +.T J r.r.6.r.6.0.w.6.6.6.0.0.0.9.9.9.2.7.u.u.u.8.8.8.y.y.^ S C 6 % d.d.d.d.d."; + "d.d.d.d.d.d. X.T J r.6.6.6.6.6.e.e.0.0.0.6.2.9.7.7.7.2.3.8.u.u.y.8.4.y.^ G V : % d.d.d.d.d."; + "d.d.d.d.d.d. } R J >.6.6.6.6.6.6.e.e.9.0.9.2.2.9.7.7.3.3.4.8.i.i.4.y.,.^ G a & % s.d.d.d.d."; + "d.d.d.d.d.d. R W K K K I K L N M S ) _ L L Q S S S S S Q S S ^ ^ ` ^ ^ F D w X . % s.d.d.d.d."; + "d.d.d.d.d.d. q { U I K L K L U M b L / Q Q L Q n n Q A Q Q G Q ^ ^ ` G G C 6 o , s.d.d.d.d."; + "d.d.d.d.d.d. . U U K K L L L Q L N N N ^ S Q Q S c V A A ^ S G G ^ ^ ` D i # . . o 1 d.d.d.d.d."; + "d.d.d.d.d.d. o e R U L L L L L Q L N Q ) / S Q Q S F ~ S ^ ^ G ^ ^ G H Z 6 % d d.d.d.d.d."; + "d.d.d.d.d.d.d. . z U L L L L Q L S N N S ` Q S Q ~ Q ^ A A ^ G D / G C t + o < s.d.d.d.d.d."; + "d.d.d.d.d.d.d. + M K L S L Q Q ! Q S S S / Q Q ^ ^ ^ G D ^ ^ ^ G D p * $ s s.d.d.d.d.d."; + "d.d.d.d.d.d.d.d. . - M Q L L Q Q Q Q Q Q Q ` ` V A Q ^ ^ D D ^ G D p > o , g d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d. = x S S Q Q Q Q ~ ~ ~ A ` Z G ^ ^ G G G G C y * % d s.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.1 . @ e N N S S Q Q ~ ~ ~ S H ` G ^ G G D B 0 & % s s.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d. 3 k n S S S F F F G F ` G D D B y 4 . . O 1 s.s.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d. . X ; t k V V A A A C V B p 0 > X O < g s.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d. . # * 4 7 0 9 6 : * # . $ 2 g s.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d. . . . o , f s.s.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d. , % . . . . $ % d g s.s.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d. $ . . . o o . O O , s g s.s.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d. 1 , , % % % , , 1 d s.s.s.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.s.s.s.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."; + "d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d."|] + +let warning_icon = + [|"48 48 97 2";" c #000000";". c #0b0501";"X c #0b0900";"o c #0a0a0a"; + "O c #170402";"+ c #120e00";"@ c #1c0705";"# c #151001";"$ c #1c1501"; + "% c #131313";"& c #220704";"* c #260806";"= c #2c0805";"- c #2e0a08"; + "; c #251d02";": c #3b0b07";"> c #3d0c08";"; c #2a2103";"< c #312604"; + "1 c #3a2d05";"2 c gray17";"3 c #410c06";"4 c #440d09";"5 c #4b0d07"; + "6 c #490e09";"7 c #4d100c";"8 c #520e07";"9 c #571109";"0 c #5c110b"; + "q c #413205";"w c #4c3b06";"e c #513f07";"r c #62120a";"t c #6d140b"; + "y c #73150d";"u c #78150b";"i c #731711";"p c #7a1710";"a c #791812"; + "s c #524007";"d c #675009";"f c #69520a";"g c #71570a";"h c #745a0c"; + "j c #7b610a";"k c #85170c";"l c #86180d";"z c #8a180d";"x c #851a12"; + "c c #8a1a11";"v c #901a0e";"b c #9a1b0e";"n c #931d12";"m c #9c1e14"; + "M c #a41c0e";"N c #a91d0f";"B c #a01d11";"V c #b31f0f";"C c #b21f10"; + "Z c #b9200f";"A c #b62010";"S c #bc2111";"D c #ab4216";"F c #b14010"; + "G c #ba5b12";"H c #bf6f16";"J c #924f48";"K c #c62210";"L c #ca2311"; + "P c #d32412";"I c #da2613";"U c #e32712";"Y c #e42e1a";"T c #eb2713"; + "R c #ec2813";"E c #f42913";"W c #c26c13";"Q c #c67e15";"! c #c87e14"; + "~ c #b9930e";"^ c #bd9312";"/ c #cb8614";"( c #cd8912";") c #c39b0f"; + "_ c #c59913";"` c #cb9e13";"' c #d18913";"] c #d7a816";"[ c #d7a81a"; + "{ c #d8a51c";"} c #dcaa14";"| c #d8a919";" . c #dfb112";".. c #e0ad12"; + "X. c #e0b213";"o. c #808080";"O. c None"; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.R E L c o.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.T E P N l X O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.N E P N M b 0 O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.N E U V M M B z - % O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.R T Z M M A B b r O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.N E L M M G W M B v : O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.M E I M M M . .M B B u O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.R T Z M M .... .' B B v 5 O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.N E L M M W . . . .B B B y O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.N E I N M M ....~ ..} ( M b z 5 O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.U E Z M M ' ) X q } ..M B B y O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.M E P M M M .j . + ..} ( B m z 6 O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.T U V M M X. ., $ } } } B M m y O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.N E L M M ( X. .; ; } } } ( M m v 6 O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.N R I M M M ..X. .; ; q } } } } m B b y O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.t R S M M ( X. .} w q q } } } } ( B m c 4 O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.N E P N M M . . ...$ ; q } } } } ] m m m r O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.U R A M M . . ...} X X ; } } } } } ' m m x = O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.N R P N M ' . .} } } . + X < } } | } | | m m m 0 % O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.Y R A B M } ........} + s } } } | } | ( m m p @ O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.N R P B N ' .} ....} } 1 e + h } } | } | | | m m c 6 O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.Y T Z M B .} ....} } } ^ f < _ ] | } | } | | / m n i . O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.N R P M B ( .....} } } } } } } | } } | } | | [ | m m c > O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.Y T A B M .} ..} } } } } ` ; ; _ } | } | | [ | { H m n 0 O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.M R P M B ( ..} } ..} } } } w f | } ] | | [ [ { / m m a = O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.J R A B M } } } } } } } } } 1 X h | | | | | | | [ [ m m x 7 o o O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.M T I M M / ..} } } } } } } } ^ < j ] } | | | | | | [ [ H m m 0 o o O.O.O.O.O."; + "O.O.O.O.O.O.O.O.t R K M M m M M B B B G W / / } } } | | | | | | [ [ | { / D m a @ 2 O.O.O.O.O."; + "O.O.O.O.O.O.O.O.3 L B m M M M m B B B B B B m m m m B m m m m m m m m m m m m x > O.O.O.O."; + "O.O.O.O.O.O.O.O.O u 5 t t u l z n m m B B B B m m m m m m m m m m m m m m m m m 0 O.O.O.O."; + "O.O.O.O.O.O.O.O. o . & = 3 > 0 0 r t i y i x c x c c c x c x x x x x x a i 7 o O.O.O.O."; + "O.O.O.O.O.O.O.O. o . & @ & & 4 > 4 > 4 > > > : : > > > > - @ O.O.O.O."; + "O.O.O.O.O.O.O.O. . o . o o o o O.O.O.O."; + "O.O.O.O.O.O.O.O.O. o o . . o o o O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O. o o o O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O. O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."; + "O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O."|] diff --git a/sw/lib/ocaml/gtkgl_Hack.ml b/sw/lib/ocaml/gtkgl_Hack.ml new file mode 100644 index 00000000000..6cff47cea86 --- /dev/null +++ b/sw/lib/ocaml/gtkgl_Hack.ml @@ -0,0 +1,30 @@ +(* + * $Id$ + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +external load_bitmap_font : string -> GlList.base = + "_gtkgl_hack_load_bitmap_font" +external unload_bitmap_font : GlList.base -> unit = + "_gtkgl_hack_unload_bitmap_font" + +let gl_print_string font_base s = GlList.call_lists ~base:font_base(`byte s) diff --git a/sw/lib/ocaml/gtkgl_Hack.mli b/sw/lib/ocaml/gtkgl_Hack.mli new file mode 100644 index 00000000000..9bbd618f52a --- /dev/null +++ b/sw/lib/ocaml/gtkgl_Hack.mli @@ -0,0 +1,29 @@ +(* + * $Id$ + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +external load_bitmap_font : string -> GlList.base + = "_gtkgl_hack_load_bitmap_font" +external unload_bitmap_font : GlList.base -> unit + = "_gtkgl_hack_unload_bitmap_font" +val gl_print_string : GlList.base -> string -> unit diff --git a/sw/lib/ocaml/latlong.ml b/sw/lib/ocaml/latlong.ml new file mode 100644 index 00000000000..571a6d7873a --- /dev/null +++ b/sw/lib/ocaml/latlong.ml @@ -0,0 +1,334 @@ +(* + * $Id$ + * + * Geographic conversion utilities + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +module C = struct + include Complex + let make x y = {re = x; im = y} + let im c = c.im + let re c = c.re + let scal a {re = x; im = y} = {re = x*.a; im = y*.a} + let i {re = x; im = y} = {re = -.y; im = x} + let sin z = + let iz = i z in + i (scal (-. 0.5) (sub (exp iz) (exp (scal (-1.) iz)))) +end + +type degree = float +type radian = float +type semi = float +type dms = int * int * float + +type semicircle = { lat : semi; long : semi } +type geographic = { posn_lat : radian ; posn_long : radian } + +type angle_unit = Semi | Rad | Deg | Grd + +type cartesian = {x : float; y : float; z: float } + +let pi = 3.14159265358979323846;; + +let piradian = function + Semi -> 2. ** 31. | Rad -> pi | Deg -> 180. | Grd -> 200. +let (>>) u1 u2 x = (x *. piradian u2) /. piradian u1;; + +let sprint_degree_of_radian x = + Printf.sprintf "%.4f" ((Rad>>Deg) x) + +let string_degrees_of_geographic sm = + Printf.sprintf "%s\t%s" + (sprint_degree_of_radian sm.posn_lat) (sprint_degree_of_radian sm.posn_long) + +let of_semicircle x = + { posn_lat = (Semi>>Rad) x.lat ; posn_long = (Semi>>Rad) x.long } + +let semicircle_of x = + { lat = (Rad>>Semi) x.posn_lat ; long = (Rad>>Semi) x.posn_long } + +let decimal d m s = float d +. float m /. 60. +. s /. 3600.;; +let dms x = + let d = truncate x in + let m = truncate ((x -. float d) *. 60.) in + let s = 3600. *. (x -. float d -. float m /. 60.) in + (d, m, s);; + + + +type ellipsoid = { dx : float; dy : float; dz : float; a : float; df : float; e : float } +let ntf = { dx = -168.; dy = -60.; dz = 320. ; a = 6378249.2; df = 0.0034075495234250643; e = 0.08248325676} +let wgs84 = { dx = 0.; dy = 0.; dz = 0. ; a = 6378137.0; df = 0.0033528106647474805 ; e = 0.08181919106} +let ed50 = { dx = -87.0; dy = -98.0; dz = -121.0 ; a = 6378388.0; df = 0.003367003367003367 ; e = 0.08199188998} +let nad27 = { dx = 0.0; dy = 125.0; dz = 194.0 ; a = wgs84.a-. -.69.4; df = wgs84.df-. -0.37264639 /. 1e4 ; e = 0.08181919106(*** ??? ***)} + +type geodesic = NTF | ED50 | WGS84 | NAD27 +type ntf = geographic +let ellipsoid_of = function + NTF -> ntf | ED50 -> ed50 | WGS84 -> wgs84 | NAD27 -> nad27 + + +let latitude_isometrique phi e = + log (tan (pi/.4. +. phi /. 2.0)) -. e /. 2.0 *. log ((1.0 +. e *. sin phi) /. (1.0 -. e *. sin phi)) + +let inverse_latitude_isometrique lat e epsilon = + let exp_l = exp lat in + let pi_2 = pi /. 2. in + let phi0 = 2. *. atan exp_l -. pi_2 in + let rec loop phi = + let sin_phi = e *. sin phi in + let phi' = 2. *. atan (((1. +. sin_phi) /. (1. -. sin_phi))**(e/.2.) *. exp_l) -. pi_2 in + if abs_float (phi' -. phi) < epsilon then phi' else loop phi' in + loop phi0;; + +type lambert_zone = { + ellipsoid : ellipsoid; + phi0 : radian; + lphi0 : float; + r0 : float; + lambda0 : radian; + y0 : int; + x0 : int; + ys : int; + k0 : float (* facteur d'échelle *) + } + +type meter = int +type fmeter = float +type lambert = { lbt_x : meter; lbt_y : meter } +type utm = { utm_x : fmeter; utm_y : fmeter ; utm_zone : int } + +module Ellipse = struct + let e_square d = 2.0 *. d -. d ** 2.0;; + let e_prime_square d = 1.0 /. (1.0 -. d) ** 2.0 -. 1.0;; +end + +(* From http://www.tandt.be/wis/WiS/eqntf.html et http://www.ign.fr/MP/GEOD/geodesie/coordonnees.html *) +let lambertI = { + ellipsoid = ntf; + lambda0 = (Deg>>Rad) (decimal 2 20 14.025); + phi0 = (Deg>>Rad) (decimal 49 30 0.); + x0 = 600000; + y0 = 200000; + ys = 5657617; + lphi0 = 0.991996665; + r0 = 5457616.674; + k0 = 0.99987734 +};; + +let lambertII = { + ellipsoid = ntf; + lambda0 = (Deg>>Rad) (decimal 2 20 14.025); + phi0 = (Deg>>Rad) (decimal 46 48 0.); + x0 = 600000; + y0 = 2200000; + ys = 6199696; + lphi0 = 0.921557361; + r0 = 5999695.77; + k0 = 0.99987742};; + +let lambertIIe = { lambertII with ys = 8199696 };; + +let lambertIII = { + ellipsoid = ntf; + lambda0 = (Deg>>Rad) (decimal 2 20 14.025); + phi0 = (Deg>>Rad) (decimal 44 6 0.); + x0 = 600000; + y0 = 3200000; + ys = 6791905; + lphi0 = 0.854591098; + r0 = 6591905.08; + k0 = 0.99987750};; + +let lambertIV = { + ellipsoid = ntf; + lambda0 = (Deg>>Rad) (decimal 2 20 14.025); + phi0 = (Deg>>Rad) (decimal 42 09 54.); + x0 = 234; + y0 = 4185861; + ys = 7239162; + lphi0 = 0.808475773; + r0 = 7053300.18; + k0 = 0.99994471 +};; + +let lambert_n l = sin l.phi0 + + +let lambert_c l = + let n = lambert_n l in + l.r0 *. exp (l.lphi0 *. n) + +let lambert = function + 1 -> lambertI | 2 -> lambertII | 3 -> lambertIII | 4 -> lambertIV | _ -> failwith "lambert";; + + +let of_lambert l { lbt_x = x; lbt_y = y } = + let c = lambert_c l and n = lambert_n l in + let dx = float (x - l.x0) and dy = float (y - l.ys) in + let r = sqrt (dx**2. +. dy**2.) in + let gamma = atan2 dx (-. dy) in + let lambda = l.lambda0 +. gamma /. n + and ll = -. 1. /. n *. log (abs_float (r/.c)) in + let phi = inverse_latitude_isometrique ll l.ellipsoid.e 1e-11 in + {posn_long = lambda; posn_lat = phi};; + + +let lambert_of l {posn_long = lambda; posn_lat = phi} = + let n = lambert_n l in + let e = l.ellipsoid.e in + let ll = latitude_isometrique phi e in + let r = lambert_c l *. exp (-. ll *. n) in + let gamma = (lambda -. l.lambda0) *. n in + + let x = l.x0 + truncate (r *. sin gamma) + and y = l.ys - truncate (r *. cos gamma) in + { lbt_x = x; lbt_y = y };; + + +let serie5 cc e = + let ee = Array.init (Array.length cc.(0)) (fun i -> e ** (float (2*i))) in + Array.init (Array.length cc) + (fun i -> + let cci = cc.(i) in + let x = ref 0. in + for j = 0 to Array.length cci - 1 do + x := !x +. cci.(j) *. ee.(j) + done; + !x);; + +let coeff_proj_mercator = + [|[|1.; -. 1./.4.; -. 3./.64.; -.5./.256.; -.175./.16384.|]; + [|0.;1./.8.; -.1./.96.; -.9./.1024.; -.901./.184320.|]; + [|0.;0.;13./.768.;17./.5120.;-.311./.737280.|]; + [|0.;0.;0.; 61./.15360.;899./.430080.|]; + [|0.;0.;0.;0.;49561./.41287680.|]|];; + +let coeff_proj_mercator_inverse = + [|coeff_proj_mercator.(0); + [|0.;1./.8.; 1./.48.; 7./.2048.; 1./.61440.|]; + [|0.;0.;1./.768.;3./.1280.;559./.368640.|]; + [|0.;0.;0.; 17./.30720.;283./.430080.|]; + [|0.;0.;0.;0.;4397./.41287680.|]|];; + +let utm_of geo {posn_long = lambda; posn_lat = phi} = + let ellipsoid = ellipsoid_of geo in + let k0 = 0.9996 + and xs = 500000. + and ys = if phi > 0. then 0. else 10000000. in + let lambda_deg = truncate (floor ((Rad>>Deg)lambda)) in + let zone = (lambda_deg + 180) / 6 + 1 in + let lambda_c = (Deg>>Rad) (float (lambda_deg - lambda_deg mod 6 + 3)) in + let e = ellipsoid.e + and n = k0 *. ellipsoid.a in + let ll = latitude_isometrique phi e + and dl = lambda -. lambda_c in + let phi' = asin (sin dl /. cosh ll) in + let ll' = latitude_isometrique phi' 0. in + let lambda' = atan (sinh ll /. cos dl) in + let z = C.make lambda' ll' + and c = serie5 coeff_proj_mercator e in + let z' = ref (C.scal c.(0) z) in + for k = 1 to Array.length c - 1 do + z' := C.add !z' (C.scal c.(k) (C.sin (C.scal (float (2*k)) z))) + done; + z' := C.scal n !z'; + { utm_zone = zone; utm_x = xs +. C.im !z'; utm_y = ys +. C.re !z' };; + +let of_utm geo { utm_zone = f; utm_x = x; utm_y = y } = + let ellipsoid = ellipsoid_of geo in + let k0 = 0.9996 + and xs = 500000. + and ys = 0. in + let e = ellipsoid.e + and n = k0 *. ellipsoid.a in + let c = serie5 coeff_proj_mercator_inverse e in + + let lambda_c = (Deg>>Rad) (float (6 * f - 183)) in + let z' = C.scal (1./.n/.c.(0)) (C.make (y-.ys) (x-.xs)) in + let z = ref z' in + for k = 1 to Array.length c - 1 do + z := C.sub !z (C.scal c.(k) (C.sin (C.scal (float (2*k)) z'))) + done; + let ll = C.re !z and lls = C.im !z in + let lambda = lambda_c +. atan (sinh lls /. cos ll) + and phi' = asin (sin ll /. cosh lls) in + let ll = latitude_isometrique phi' 0. in + let phi = inverse_latitude_isometrique ll e 1e-11 in + {posn_long = lambda; posn_lat = phi};; + + +let (<<) geo1 geo2 {posn_long = lambda; posn_lat = phi} = + let elps1 = ellipsoid_of geo1 + and elps2 = ellipsoid_of geo2 in + let d12 = sin phi + and d13 = cos phi + and d14 = sin lambda + and d15 = cos lambda in + + let d16 = Ellipse.e_square elps2.df + and d17 = Ellipse.e_square elps1.df in + let d18 = elps2.a /. sqrt (1.0 -. d16 *. d12 ** 2.0) in + let d20 = d18 *. d13 *. d15 in + let d21 = d18 *. d13 *. d14 in + let d22 = d18 *. (1.0 -. d16) *. d12 in + let d23 = d20 -. elps1.dx +. elps2.dx in + let d24 = d21 -. elps1.dy +. elps2.dy in + let d25 = d22 -. elps1.dz +. elps2.dz in + let d26 = sqrt (d23 ** 2.0 +. d24 ** 2.0) in + let d27 = atan2 d25 (d26 *. (1.0 -. elps1.df)) in + let d28 = elps1.a *. (1.0 -. elps1.df) in + let d29 = Ellipse.e_prime_square elps1.df in + let d3 = atan2 (d25 +. d29 *. d28 *. (sin d27) ** 3.0) (d26 -. d17 *. elps1.a *. (cos d27) ** 3.0) in + let d4 = atan2 d24 d23 in + {posn_long = d4; posn_lat = d3};; + +let cartesian_of ellips {posn_long = lambda; posn_lat = phi} h = + let geo = ellipsoid_of ellips in + let w = sqrt (1. -. geo.e**2. *. sin phi ** 2.)in + let n = geo.a /. w in + let x = (n+.h) *. cos phi *. cos lambda + and y = (n+.h) *. cos phi *. sin lambda + and z = (n*.(1.-.geo.e**2.)+.h) *. sin phi in + { x = x; y = y; z = z} + +let of_cartesian ellips {x=x;y=y;z=z} = + let geo = ellipsoid_of ellips in + let epsilon = 1e-11 in + let xy = sqrt (x**2. +. y**2.) + and r = sqrt (x**2. +. y**2. +. z**2.) + and e2 = geo.e**2. in + let z_xy = z /. xy in + let lambda = 2. *. atan (y /. (x +. xy)) + and phi0 = atan (z_xy /. sqrt (1.-.geo.a*.e2/.r)) in + let rec iter phi = + let phi' = atan (z_xy /. (1.-.geo.a*.e2*.cos phi/.xy/.sqrt (1.-.e2*. sin phi ** 2.))) in + if abs_float (phi -. phi') > epsilon then iter phi' else phi' in + let phi = iter phi0 in + let h = xy/.cos phi -. geo.a /. sqrt (1.-.e2*.sin phi ** 2.) in + ({posn_long = lambda; posn_lat = phi}, h) + +let distance = fun {lbt_x=x1; lbt_y=y1} {lbt_x=x2; lbt_y=y2} -> + truncate (sqrt ((float x1 -. float x2)**2. +. (float y1 -. float y2)**2.)) + +let wgs84_of_lambertIIe = fun x y -> (WGS84<> ) : angle_unit -> angle_unit -> float -> float +(** [(Unit1>>Unit2) a] converts angle [a] expressed in [Unit1] in [Unit2] *) + +val decimal : int -> int -> float -> float +val dms : float -> dms +(** Conversions between decimal degrees and degree, minutes and seconds *) + + +(** {b Geodesic datum} *) + +type geodesic = NTF | ED50 | WGS84 | NAD27 +(** Geodesic referential *) + +type lambert_zone +val lambertI : lambert_zone +val lambertII : lambert_zone +val lambertIIe : lambert_zone +val lambertIII : lambert_zone +val lambertIV : lambert_zone +(** French lambert zones *) + + +(** {b Positions} *) + +type semicircle = { lat : semi; long : semi; } +type geographic = { posn_lat : radian; posn_long : radian; } +type cartesian = { x : float; y : float; z : float; } +type meter = int +type fmeter = float +type lambert = { lbt_x : meter; lbt_y : meter; } +type utm = { utm_x : fmeter; utm_y : fmeter; utm_zone : int; } +(** Position units. Coordinates are in meters in the [cartesian] type. *) + +val string_degrees_of_geographic : geographic -> string +(** Pretty printing *) + + +(** {b Conversions} *) + +val ( << ) : geodesic -> geodesic -> geographic -> geographic +(** [(Geo1< geographic +val semicircle_of : geographic -> semicircle + +type ntf = geographic +(** Type alias for documentation purpose *) + +val of_lambert : lambert_zone -> lambert -> ntf +val lambert_of : lambert_zone -> ntf -> lambert +(** Conversions between geographic (in NTF) and lambert *) + +val utm_of : geodesic -> geographic -> utm +val of_utm : geodesic -> utm -> geographic +(** Conversions between geographic and UTM *) + +val cartesian_of : geodesic -> geographic -> float -> cartesian +(** [cartesian_of geode geo alt] converts position [geo] at altitude [alt] +expressed in [geode] into cartesian coordinates *) + +val of_cartesian : geodesic -> cartesian -> geographic * float +(** [of_cartesian geode xyz] converts cartesian coordinates [xyz] into +geographic coordinates and altitude expressed in geodesic referential +[geode] *) + +val distance : lambert -> lambert -> meter + +val wgs84_of_lambertIIe : meter -> meter -> geographic diff --git a/sw/lib/ocaml/mapCanvas.ml b/sw/lib/ocaml/mapCanvas.ml new file mode 100644 index 00000000000..acdc49f7cf3 --- /dev/null +++ b/sw/lib/ocaml/mapCanvas.ml @@ -0,0 +1,218 @@ +open Latlong +open Printf + +let pan_step = 50 + +type meter = float +type en = { east : meter; north : meter } + +let _ = Srtm.add_path "SRTM" + +(* world_unit: m:pixel at scale 1. *) +class widget = fun ?(height=800) ?width ?wgs84_of_en () -> + + let frame = GPack.vbox ~height ?width () in + + let menubar = GMenu.menu_bar ~packing:frame#pack () in + + let adj = GData.adjustment + ~value:1. ~lower:0.25 ~upper:10. + ~step_incr:0.25 ~page_incr:1.0 ~page_size:1.0 () in + + let canvas = GnoCanvas.canvas ~height ~packing:(frame#pack ~expand:true) () in + + let bottom = GPack.hbox ~height:30 ~packing:frame#pack () in + let w = GEdit.spin_button ~adjustment:adj ~rate:0. ~digits:2 ~width:50 ~height:20 ~packing:bottom#pack () in + + + let lbl_xy = GMisc.label ~height:50 ~packing:bottom#pack () in + let lbl_geo = GMisc.label ~height:50 ~packing:bottom#pack () in + let lbl_alt = GMisc.label ~height:50 ~packing:bottom#pack () in + let lbl_group = GMisc.label ~height:50 ~packing:bottom#pack () in + + let factory = new GMenu.factory menubar in + let file_menu = factory#add_submenu "Nav" in + let menu_fact = new GMenu.factory file_menu in + + let srtm = menu_fact#add_check_item "SRTM" ~active:false in + + object (self) + + initializer ignore (menu_fact#add_item "Goto" ~callback:self#goto) + initializer ignore (canvas#event#connect#motion_notify (self#mouse_motion)); + initializer ignore (canvas#event#connect#button_press (self#button_press)); + initializer ignore (canvas#event#connect#button_release self#button_release); + initializer ignore (canvas#event#connect#after#key_press self#key_press) ; + initializer ignore (canvas#event#connect#enter_notify (fun _ -> self#canvas#misc#grab_focus () ; false)); + initializer ignore (canvas#event#connect#any self#any_event); + + initializer ignore (adj#connect#value_changed (fun () -> self#zoom adj#value)); + initializer canvas#set_center_scroll_region false ; + initializer canvas#set_scroll_region (-2500000.) (-2500000.) 2500000. 2500000.; + + + val mutable current_zoom = 1. + val mutable dragging = None + val mutable grouping = None + val mutable rectangle = None + val mutable world_unit = 1. + val mutable wgs84_of_en = wgs84_of_en + + method set_wgs84_of_en = fun x -> wgs84_of_en <- Some x + + method set_world_unit = fun x -> world_unit <- x + + method current_zoom = current_zoom + + method canvas = canvas + method frame = frame + method menu_fact = menu_fact + method window_to_world = canvas#window_to_world + method root = canvas#root + method zoom_adj = adj + method factory = factory + + + method world_of_en = fun en -> en.east /. world_unit, -. en.north /. world_unit + method en_of_world = fun wx wy -> { east = wx *. world_unit; north = -. wy *. world_unit } + + method geo_string = fun en -> + match wgs84_of_en with + None -> "" + | Some f -> string_degrees_of_geographic (f en) + + method altitude = fun wgs84 -> + try + Srtm.of_wgs84 wgs84 + with + Srtm.Tile_not_found x -> + srtm#set_active false; + GToolbox.message_box "SRTM" (sprintf "SRTM tile %s not found: %s ?" x (Srtm.error x)); + 0 + + method moveto = fun en -> + let (xw, yw) = self#world_of_en en in + let (xc, yc) = canvas#world_to_window xw yw in + canvas#scroll_to (truncate xc) (truncate yc) + + method goto = fun () -> + let dialog = GWindow.window ~border_width:10 ~title:"Geo ref" () in + let dvbx = GPack.box `VERTICAL ~packing:dialog#add () in + let lat = GEdit.entry ~packing:dvbx#add () in + let lon = GEdit.entry ~packing:dvbx#add () in + let cancel = GButton.button ~label:"Cancel" ~packing: dvbx#add () in + let ok = GButton.button ~label:"OK" ~packing: dvbx#add () in + ignore(cancel#connect#clicked ~callback:dialog#destroy); + ignore(ok#connect#clicked ~callback: + begin fun _ -> + let x = float_of_string lat#text in + let y = float_of_string lon#text in + self#moveto {east=x; north=y}; + dialog#destroy () + end); + dialog#show () + + + method display_map = fun ?(scale = 1.) en image -> + let p = GnoCanvas.pixbuf ~pixbuf:image ~props:[`ANCHOR `NW] self#root in + p#lower_to_bottom (); + let wx, wy = self#world_of_en en in + p#move wx wy; + let a = p#i2w_affine in + a.(0) <- scale; a.(3) <- scale; + p#affine_absolute a; + p + + method zoom = fun value -> + canvas#set_pixels_per_unit value; + current_zoom <- value + + + method mouse_motion = fun ev -> + let xc = GdkEvent.Motion.x ev + and yc = GdkEvent.Motion.y ev in + let (xw, yw) = self#window_to_world xc yc in + let en = self#en_of_world xw yw in + lbl_xy#set_text (sprintf "%.0fm %.0fm\t" en.east en.north); + lbl_geo#set_text (self#geo_string en); + begin + match wgs84_of_en, srtm#active with + Some wgs84_of_en, true -> + lbl_alt#set_text (sprintf "\t%dm"(self#altitude (wgs84_of_en en))) + | _ -> () + end; + begin + match dragging with + Some (x0, y0 ) -> + let (x, y) = self#canvas#get_scroll_offsets in + self#canvas#scroll_to (x+truncate (x0-.xc)) (y+truncate (y0-.yc)) + | None -> () + end; + begin + match grouping with + Some (xw1, yw1) -> + let en1 = self#en_of_world xw1 yw1 in + lbl_group#set_text (sprintf "[%.1fkm %.1fkm]" ((en1.east -. en.east)/.1000.) ((en1.north-.en.north)/.1000.)) + | None -> () + end; + false + + method button_release = fun ev -> + let state = GdkEvent.Button.state ev in + match GdkEvent.Button.button ev, grouping with + 2, _ -> + dragging <- None; false + | 1, Some (xw1, yw1) -> + let xc = GdkEvent.Button.x ev in + let yc = GdkEvent.Button.y ev in + let (xw2, yw2) = self#window_to_world xc yc in + rectangle <- Some ((xw1, yw1), (xw2, yw2)); + lbl_group#set_text ""; + grouping <- None; + false + | _ -> false + + method button_press = fun ev -> + let state = GdkEvent.Button.state ev in + let xc = GdkEvent.Button.x ev in + let yc = GdkEvent.Button.y ev in + match GdkEvent.Button.button ev with + 1 -> + let xyw = self#window_to_world xc yc in + grouping <- Some xyw; + true + | 2 -> + dragging <- Some (xc, yc); + true + | _ -> false + + method key_press = fun ev -> + let (x, y) = canvas#get_scroll_offsets in + match GdkEvent.Key.keyval ev with + | k when k = GdkKeysyms._Up -> canvas#scroll_to x (y-pan_step) ; true + | k when k = GdkKeysyms._Down -> canvas#scroll_to x (y+pan_step) ; true + | k when k = GdkKeysyms._Left -> canvas#scroll_to (x-pan_step) y ; true + | k when k = GdkKeysyms._Right -> canvas#scroll_to (x+pan_step) y ; true + | _ -> false + + method any_event = fun ev -> + match GdkEvent.get_type ev with + | `SCROLL -> begin + match GdkEvent.Scroll.direction (GdkEvent.Scroll.cast ev) with + `UP -> + adj#set_value (adj#value+.adj#step_increment) ; + true + | `DOWN -> adj#set_value (adj#value-.adj#step_increment) ; true + | _ -> false + end + | _ -> false + + + method segment = fun ?(width=1) ?fill_color en1 en2 -> + let (x1, y1) = self#world_of_en en1 + and (x2, y2) = self#world_of_en en2 in + let l = GnoCanvas.line ?fill_color ~props:[`WIDTH_PIXELS width] ~points:[|x1;y1;x2;y2|] canvas#root in + l#show (); + l +end + diff --git a/sw/lib/ocaml/mapTrack.ml b/sw/lib/ocaml/mapTrack.ml new file mode 100644 index 00000000000..e58f4344592 --- /dev/null +++ b/sw/lib/ocaml/mapTrack.ml @@ -0,0 +1,77 @@ +open Printf + +module G = MapCanvas + +let affine_pos_and_angle z xw yw angle = + let rad_angle = angle /. 180. *. acos (-1.) in + let cos_a = cos rad_angle in + let sin_a = sin rad_angle in + [| cos_a /. z ; sin_a /. z ; ~-. sin_a /. z; cos_a /. z; xw ; yw |] + +class track = fun ?(name="coucou") ?(size = 50) ?(color="red") (geomap:MapCanvas.widget) -> + let group = GnoCanvas.group geomap#canvas#root in + let empty = ({ G.east = 0.; north = 0. }, GnoCanvas.line group) in + + let aircraft = GnoCanvas.group group in + let ac_icon = + ignore (GnoCanvas.line ~fill_color:color ~props:[`WIDTH_PIXELS 4;`CAP_STYLE `ROUND] ~points:[|0.;-6.;0.;14.|] aircraft); + ignore (GnoCanvas.line ~fill_color:color ~props:[`WIDTH_PIXELS 4;`CAP_STYLE `ROUND] ~points:[|-9.;0.;9.;0.|] aircraft); + ignore (GnoCanvas.line ~fill_color:color ~props:[`WIDTH_PIXELS 4;`CAP_STYLE `ROUND] ~points:[|-4.;10.;4.;10.|] aircraft) in + let ac_label = + GnoCanvas.text group ~props:[`TEXT name; `X 25.; `Y 25.; `ANCHOR `SW; `FILL_COLOR color] in + + object (self) + val mutable segments = Array.create size empty + val mutable top = 0 + val mutable last = None + method clear_one = fun i -> + if segments.(i) != empty then begin + (snd segments.(i))#destroy (); + segments.(i) <- empty + end + method incr = + let s = Array.length segments in + top <- (top + 1) mod s + method clear = + for i = 0 to Array.length segments - 1 do + self#clear_one i + done; + top <- 0 + method add_point = fun en -> + self#clear_one top; + begin + match last with + None -> + segments.(top) <- (en, geomap#segment ~fill_color:color en en) + | Some last -> + segments.(top) <- (en, geomap#segment ~width:2 ~fill_color:color last en) + end; + self#incr; + last <- Some en + method move_icon = fun en heading -> + let (xw,yw) = geomap#world_of_en en in + aircraft#affine_absolute (affine_pos_and_angle geomap#zoom_adj#value xw yw heading); + ac_label#affine_absolute (affine_pos_and_angle geomap#zoom_adj#value xw yw 0.); + method zoom = fun z -> + let a = aircraft#i2w_affine in + let z' = sqrt (a.(0)*.a.(0)+.a.(1)*.a.(1)) in + for i = 0 to 3 do a.(i) <- a.(i) /. z' *. 1./.z done; + aircraft#affine_absolute a + method resize = fun new_size -> + let a = Array.create new_size empty in + let size = Array.length segments in + let m = min new_size size in + let j = ref ((top - m + size) mod size) in + for i = 0 to m - 1 do + a.(i) <- segments.(!j); + j := (!j + 1) mod size + done; + for i = 1 to size - new_size do (* Never done if new_size > size *) + self#clear_one !j; + j := (!j + 1) mod size + done; + top <- m mod new_size; + segments <- a + method size = Array.length segments + initializer ignore(geomap#zoom_adj#connect#value_changed (fun () -> self#zoom geomap#zoom_adj#value)) +end diff --git a/sw/lib/ocaml/mapWaypoints.ml b/sw/lib/ocaml/mapWaypoints.ml new file mode 100644 index 00000000000..071336ef53b --- /dev/null +++ b/sw/lib/ocaml/mapWaypoints.ml @@ -0,0 +1,122 @@ +open Printf + +let s = 5. +let losange = [|s;0.; 0.;s; -.s;0.; 0.;-.s|] + +class group = fun ?(color="red") ?(editable=true) (geomap:MapCanvas.widget) -> + let g = GnoCanvas.group geomap#canvas#root in + object + method group=g + method geomap=geomap + method color=color + method editable=editable +end + +class waypoint = fun (group:group) (name :string) ?(alt=0.) en -> + let geomap=group#geomap + and color = group#color + and editable = group#editable in + let xw, yw = geomap#world_of_en en in + object (self) + val mutable x0 = 0. + val mutable y0 = 0. + val item = + GnoCanvas.polygon group#group ~points:losange + ~props:[`FILL_COLOR color; `OUTLINE_COLOR "midnightblue" ; `WIDTH_UNITS 1.; `FILL_STIPPLE (Gdk.Bitmap.create_from_data ~width:2 ~height:2 "\002\001")] + + val label = GnoCanvas.text group#group ~props:[`TEXT name; `X s; `Y 0.; `ANCHOR `SW] + val mutable name = name + val mutable alt = alt + initializer self#move xw yw + method name = name + method set_name n = + if n <> name then + name <- n + method alt = alt + method label = label + method xy = let a = item#i2w_affine in (a.(4), a.(5)) (*** item#i2w 0. 0. causes Seg Fault !***) + method move dx dy = item#move dx dy; label#move dx dy + method edit = + let dialog = GWindow.window ~border_width:10 ~title:"Waypoint Edit" () in + let dvbx = GPack.box `VERTICAL ~packing:dialog#add () in + let en = self#en in + let ename = GEdit.entry ~text:name ~packing:dvbx#add () in + let ex = GEdit.entry ~text:(string_of_float en.MapCanvas.east) ~packing:dvbx#add () in + let ey = GEdit.entry ~text:(string_of_float en.MapCanvas.north) ~packing:dvbx#add () in + let ea = GEdit.entry ~text:(string_of_float alt) ~packing:dvbx#add () in + let cancel = GButton.button ~label:"Cancel" ~packing: dvbx#add () in + let ok = GButton.button ~label:"OK" ~packing: dvbx#add () in + ignore(cancel#connect#clicked ~callback:dialog#destroy); + ignore(ok#connect#clicked ~callback: + begin fun _ -> + self#set_name ename#text; + alt <- float_of_string ea#text; + label#set [`TEXT name]; + self#set {MapCanvas.east = float_of_string ex#text; + north = float_of_string ey#text}; + dialog#destroy () + end); + dialog#show () + + + + method event (ev : GnoCanvas.item_event) = + begin + match ev with + | `BUTTON_PRESS ev -> + let state = GdkEvent.Button.state ev in + begin + match GdkEvent.Button.button ev with + | 1 -> self#edit + | 3 -> self#delete + | 2 -> + let x = GdkEvent.Button.x ev + and y = GdkEvent.Button.y ev in + x0 <- x; y0 <- y; + let curs = Gdk.Cursor.create `FLEUR in + item#grab [`POINTER_MOTION; `BUTTON_RELEASE] curs + (GdkEvent.Button.time ev) + | x -> printf "%d\n" x; flush stdout; + end + | `MOTION_NOTIFY ev -> + let state = GdkEvent.Motion.state ev in + if Gdk.Convert.test_modifier `BUTTON2 state then begin + let x = GdkEvent.Motion.x ev + and y = GdkEvent.Motion.y ev in + let dx = geomap#current_zoom *. (x-. x0) + and dy = geomap#current_zoom *. (y -. y0) in + self#move dx dy ; + x0 <- x; y0 <- y + end + | `BUTTON_RELEASE ev -> + if GdkEvent.Button.button ev = 2 then + item#ungrab (GdkEvent.Button.time ev) + | _ -> () + end; + true + initializer ignore(if editable then ignore (item#connect#event self#event)) + method item = item + method en = + let (dx, dy) = self#xy in + geomap#en_of_world dx dy + method set en = + let (xw, yw) = geomap#world_of_en en + and (xw0, yw0) = self#xy in + self#move (xw-.xw0) (yw-.yw0) + method delete = + item#destroy (); + label#destroy () + method zoom (z:float) = + let a = item#i2w_affine in + a.(0) <- 1./.z; a.(3) <- 1./.z; + item#affine_absolute a; + label#affine_absolute a + initializer self#zoom geomap#zoom_adj#value + initializer ignore(geomap#zoom_adj#connect#value_changed (fun () -> self#zoom geomap#zoom_adj#value)) + end + +let gensym = let n = ref 0 in fun prefix -> incr n; prefix ^ string_of_int !n + +let waypoint = fun group ?(name = gensym "wp") ?alt en -> + new waypoint group name ?alt en + diff --git a/sw/lib/ocaml/ml_gtkgl_hack.c b/sw/lib/ocaml/ml_gtkgl_hack.c new file mode 100644 index 00000000000..d097379166c --- /dev/null +++ b/sw/lib/ocaml/ml_gtkgl_hack.c @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include + +#include +#ifdef WIN32 +#include +#else +#include +#include +#include +#include +#endif + +#define MAX_FONTS 1000 +#ifndef WIN32 +static GLuint ListBase[MAX_FONTS]; +static GLuint ListCount[MAX_FONTS]; +#endif + +CAMLprim value _gtkgl_hack_load_bitmap_font (const char *font) { + static int FirstTime = 1; + int first, last, count; + int i ; + +#ifndef WIN32 + XFontStruct *fontinfo; + GLuint fontbase; + Display* dpy = GDK_DISPLAY(); + + if (FirstTime) { + for (i=0;imin_char_or_byte2; + last = fontinfo->max_char_or_byte2; + + /* Nombre de caracteres definis par cette fonte */ + count = last-first+1; + + /* Creation des listes OpenGL a partir de la fonte */ + fontbase = glGenLists( (GLuint) (last+1) ); + if (fontbase==0) return 0 ; + /* printf("Fontbase=%d first=%d last=%d\n", fontbase, first, last) ; fflush(stdout) ;*/ + + /* OpenGL doit l'utiliser */ + glXUseXFont(fontinfo->fid, first, count, (int) fontbase+first); + + + for (i=0;id2 then 1 else 0 + +(* ============================================================================= *) +(* = Compare deux chaines = *) +(* ============================================================================= *) +let cmp_string s1 s2 = if s1s2 then 1 else 0 + +(* ============================================================================= *) +(* = Decoupage d'une chaine de caracteres = *) +(* ============================================================================= *) +let split c s = + let i = ref (String.length s - 1) in + let j = ref !i and r = ref [] in + if !i >= 0 & String.get s 0 <> '#' (* skip lines starting with '#' *) + then + while !i >= 0 do + while !i >= 0 & String.get s !i <> c do decr i; done; + if !i < !j then r := (String.sub s (!i+1) (!j - !i)) :: !r; + while !i >= 0 & String.get s !i = c do decr i; done; + j := !i; + done; + !r + +(* ============================================================================= *) +(* = Decoupage d'une chaine de caracteres qui s'arrete apres une occurence du = *) +(* = caractere de decoupage contrairement a la fonction precedente = *) +(* ============================================================================= *) +let split2 c s = + let i = ref (String.length s - 1) in + let j = ref !i and r = ref [] in + if !i >= 0 & String.get s 0 <> '#' then (* skip lines starting with '#' *) + while !i >= 0 do + while !i >= 0 & String.get s !i <> c do decr i; done; + if !i <= !j then r := (String.sub s (!i+1) (!j - !i)) :: !r; + decr i ; + j := !i; + done; + !r + +(* ============================================================================= *) +(* = Decoupage d'une chaine de caracteres (separateurs multiples) = *) +(* ============================================================================= *) +let split_multiple lst_c s = + let match_char c = List.mem c lst_c in + + let i = ref (String.length s - 1) in + let j = ref !i and r = ref [] in + if !i >= 0 & String.get s 0 <> '#' then (* skip lines starting with '#' *) + while !i >= 0 do + while !i >= 0 & (not (match_char (String.get s !i))) do decr i; done; + if !i < !j then r := (String.sub s (!i+1) (!j - !i)) :: !r; + while !i >= 0 & (match_char (String.get s !i)) do decr i; done; + j := !i; + done; + !r + +(* ============================================================================= *) +(* = Decoupage d'une chaine de caracteres (separateurs multiples). = *) +(* = On s'arrete apres chaque occurence d'un separateur comme split2 = *) +(* ============================================================================= *) +let split_multiple2 lst_c s = + let match_char c = List.mem c lst_c in + + let i = ref (String.length s - 1) in + let j = ref !i and r = ref [] in + if !i >= 0 & String.get s 0 <> '#' then (* skip lines starting with '#' *) + while !i >= 0 do + while !i >= 0 & (not (match_char (String.get s !i))) do decr i; done; + if !i <= !j then r := (String.sub s (!i+1) (!j - !i)) :: !r; + decr i ; + j := !i; + done; + !r + +(* ============================================================================= *) +(* = Fonction d'ajout d'espaces a une chaine de caracteres = *) +(* = lg = longueur desiree = *) +(* ============================================================================= *) +let rec add_spaces lg str = + if (String.length str) >= lg then str else add_spaces lg (str ^ " ") + +(* ============================================================================= *) +(* = Fonction de suppression d'espaces en fin de chaine, pour gagner un peu en = *) +(* = memoire = *) +(* ============================================================================= *) +let delete_trailing_spaces s = + if s <> "" then begin + let i = ref ((String.length s)-1) in + while !i>=0 && (String.get s !i = ' ') do decr i done ; + if !i>=0 then String.sub s 0 (!i+1) else "" + end else s + +(* ============================================================================= *) +(* = Remplace le caractere c par c2 dans la chaine s = *) +(* ============================================================================= *) +let string_replace_char s c c2 = + let i = ref 0 in + String.iter (fun ch -> if ch=c then String.set s !i c2 ; incr i) s + +(* ============================================================================= *) +(* = Supprime les eventuels CTRL-M en fin de chaine (DOS->Unix) = *) +(* ============================================================================= *) +let string_dos2unix s = + if s<>"" && String.get s (String.length s -1) = ' +' then + String.sub s 0 (String.length s -1) + else s + +(* ============================================================================= *) +(* = Teste si la chaine matche la pattern = *) +(* ============================================================================= *) +let string_match pattern string = + let l = String.length pattern in + String.length string >= l && String.sub string 0 l = pattern +let string_exact_match pattern string = pattern = string + +(* Meme chose sans tenir compte de la case des caracteres *) +let string_match_no_case pattern string = + string_match (String.uppercase pattern) (String.uppercase string) + +let string_exact_match_no_case pattern string = + string_exact_match (String.uppercase pattern) (String.uppercase string) + +let string_match_in pattern string = + if pattern = "" then true else begin + let do_match = ref false and substr = ref string in + while not !do_match && String.length !substr>0 do + do_match := string_match pattern !substr ; + substr:=String.sub !substr 1 (String.length !substr -1) + done ; + !do_match + end + +let string_match_in_no_case pattern string = + string_match_in (String.uppercase pattern) (String.uppercase string) + +(* ============================================================================= *) +(* = Liste de chaines -> une chaine = *) +(* ============================================================================= *) +let string_of_string_list lst_string = + List.fold_left (fun str s -> if str <> "" then str^" "^s else s) "" lst_string + +(* ============================================================================= *) +(* = Creation d'une chaine en fonction de la valeur d'un entier = *) +(* = = *) +(* = XXX n -> aucun(e) XXX si n=0, un(e) XXX si n=1, XXXs sinon = *) +(* = female indique si on a un nom feminin et first_char_upper indique si le = *) +(* = premier caractere doit etre en majuscule ou pas = *) +(* ============================================================================= *) +let eval_string base_string n female first_char_upper = + let get_upper_and_female str = + let str = if female then str^"e" else str in + if first_char_upper then String.capitalize str + else str + in + + match n with + 0 -> (get_upper_and_female "aucun") ^ " " ^ base_string + | 1 -> (get_upper_and_female "un") ^ " " ^ base_string + | _ -> + if String.get base_string (String.length base_string -1) = 'x' then + Printf.sprintf "%d %s" n base_string + else Printf.sprintf "%d %ss" n base_string + +(* ============================================================================= *) +(* = Fonction de parsing d'une chaine = *) +(* ============================================================================= *) +let do_parse_string s no_ligne_parsing parser_main lexer_token end_func = + let lexbuf = Lexing.from_string s in + let fin = ref false in + no_ligne_parsing := 1 ; + while not !fin do + try parser_main lexer_token lexbuf ; + with Parsing.Parse_error -> (* Erreur de syntaxe *) + raise Parsing.Parse_error + | Failure("lexing: empty token") -> (* Est-ce la fin de la chaine ? *) + fin := lexbuf.Lexing.lex_eof_reached ; + | x -> raise x + done ; + (* Appel a la fonction de fin de lecture *) + end_func () + +(* ============================================================================= *) +(* = Supprime les doublons dans une liste triee = *) +(* ============================================================================= *) +let rec supprime_dbl_list = function + [] -> [] + | x::xs -> + match xs with + [] -> [x] + | x'::xs' -> + if x = x' then supprime_dbl_list xs' else x::supprime_dbl_list xs' + +(* ============================================================================= *) +(* = Supprime l'element d'index idx dans la liste lst = *) +(* ============================================================================= *) +let del_elt_lst lst idx = + if (idx>=0) && (idx 1 then begin + let a = Array.of_list lst in + let a1 = Array.sub a 0 idx and + a2 = Array.sub a (idx+1) ((List.length lst)-1-idx) in + Array.to_list (Array.append a1 a2) + end else [] + end else lst + +(* ============================================================================= *) +(* = Ouverture d'un fichier compresse avec gzip, bzip2, zip ou non compresse = *) +(* ============================================================================= *) +let open_compress file = + if Filename.check_suffix file "gz" or Filename.check_suffix file "Z" or + Filename.check_suffix file "zip" or Filename.check_suffix file "ZIP" then + Unix.open_process_in ("gunzip -c "^file) + else if Filename.check_suffix file "bz2" then + Unix.open_process_in ("bunzip2 -c "^file) + else Pervasives.open_in file + + +let extensions = ["";".gz";".Z";".bz2";".zip";".ZIP"] +let find_file = fun path file -> + let rec loop_path = function + [] -> raise Not_found + | p::ps -> + let rec loop_ext = function + [] -> loop_path ps + | ext::es -> + let f = Filename.concat p file ^ ext in + if Sys.file_exists f then f else loop_ext es in + loop_ext extensions in + loop_path path + +(* ============================================================================= *) +(* = Fermeture d'un fichier = *) +(* ============================================================================= *) +let close_compress file inchan = + if (Filename.check_suffix file "gz") or (Filename.check_suffix file "bz2") or + (Filename.check_suffix file "Z") or (Filename.check_suffix file "zip") or + (Filename.check_suffix file) "ZIP"then + ignore(Unix.close_process_in inchan) + else close_in inchan + +(* ============================================================================= *) +(* = Gestion des fichiers gzippes = *) +(* ============================================================================= *) +let open_gzip file = + if Filename.check_suffix file "gz" or Filename.check_suffix file "Z" then + Unix.open_process_in ("gunzip -c "^file) + else Pervasives.open_in file +let close_gzip file inchan = + if Filename.check_suffix file "gz" or Filename.check_suffix file "Z" then + ignore(Unix.close_process_in inchan) + else close_in inchan +(* ============================================================================= *) + +(* ============================================================================= *) +(* = Gestion des fichiers bzippes = *) +(* ============================================================================= *) +let open_bzip file = + if Filename.check_suffix file "bz2" then + Unix.open_process_in ("bunzip2 -c "^file) + else Pervasives.open_in file +let close_bzip file inchan = + if Filename.check_suffix file "bz2" then ignore(Unix.close_process_in inchan) + else close_in inchan +(* ============================================================================= *) + +(* ============================================================================= *) +(* = Gestion des fichiers zippes = *) +(* ============================================================================= *) +let open_zip file = + if Filename.check_suffix file "zip" or Filename.check_suffix file "ZIP" then + Unix.open_process_in ("gunzip -c "^file) + else Pervasives.open_in file +let close_zip file inchan = + if Filename.check_suffix file "zip" or Filename.check_suffix file "ZIP" then + ignore(Unix.close_process_in inchan) + else close_in inchan +(* ============================================================================= *) + +(* ============================================================================= *) +(* = Compression d'un fichier suivant son extension = *) +(* ============================================================================= *) +let do_compress_file filename ext = + ignore(match ext with + "gz" | ".gz" -> Unix.system ("gzip "^filename) + | "bz2" | ".bz2" -> Unix.system ("bzip2 "^filename) + | _ -> Unix.WEXITED(0)) + +(* ============================================================================= *) +(* = Lecture d'un fichier et copie dans une chaine de caracteres = *) +(* ============================================================================= *) +let string_of_file filename = + let c = open_compress filename and texte = ref "" in + (try + while true do + if !texte = "" then texte := input_line c + else texte := !texte ^ "\n" ^ (input_line c) + done + with End_of_file -> close_compress filename c) ; + !texte + +(* ============================================================================= *) +(* = Fonction de lecture d'un fichier avec precision du separateur = *) +(* ============================================================================= *) +let do_read_file_with_separators filename match_func end_func separators = + let no_line = ref 0 in + let error_func s = + Printf.printf("Erreur ligne %d (%s)\n") !no_line s ; flush stdout in + + try + let c = open_compress filename in + let error = ref false in + (try + while not !error do + let s = input_line c in + no_line := !no_line + 1 ; + (* Passe les commentaires et decoupe la ligne *) + let splitted = split_multiple separators s in + (* On saute les lignes vides *) + if splitted <> [] then + (try match_func splitted (fun () -> error_func s) + with | _ -> + (* S'il y a une erreur ici, c'est qu'une ligne ne contient *) + (* Pas ce qui est attendu : ex pb de int_of_string *) + close_compress filename c ; + error_func s ;error := true) + done + with End_of_file -> close_compress filename c ; end_func ()) + with _ -> Printf.printf("Erreur d'ouverture du fichier %s\n") filename; + flush stdout + +(* ============================================================================= *) +(* = Fonction de lecture d'un fichier avec precision du separateur = *) +(* ============================================================================= *) +let do_read_file_with_separator filename match_func end_func separator = + do_read_file_with_separators filename match_func end_func [separator] + +(* ============================================================================= *) +(* = Fonction de lecture d'un fichier (separateur par defaut = espace) = *) +(* ============================================================================= *) +let do_read_file filename match_func end_func = + do_read_file_with_separators filename match_func end_func [' '] + +(* ============================================================================= *) +(* = Fonction de lecture d'un fichier avec precision du separateur = *) +(* ============================================================================= *) +let do_read_file_with_separators2 filename match_func end_func separators = + let no_line = ref 0 in + let error_func s = + Printf.printf("Erreur ligne %d (%s)\n") !no_line s ; flush stdout in + + try + let c = open_compress filename in + let error = ref false in + (try + while not !error do + let s = input_line c in + no_line := !no_line + 1 ; + (* Passe les commentaires et decoupe la ligne *) + let splitted = split_multiple2 separators s in + (* On saute les lignes vides *) + if splitted <> [] then + (try match_func splitted (fun () -> error_func s) + with | _ -> + (* S'il y a une erreur ici, c'est qu'une ligne ne contient *) + (* Pas ce qui est attendu : ex pb de int_of_string *) + close_compress filename c ; + error_func s ;error := true) + done + with End_of_file -> close_compress filename c ; end_func ()) + with _ -> Printf.printf("Erreur d'ouverture du fichier %s\n") filename; + flush stdout + +(* ============================================================================= *) +(* = Fonction de lecture d'un fichier avec precision du separateur = *) +(* ============================================================================= *) +let do_read_file_with_separator2 filename match_func end_func separator = + do_read_file_with_separators2 filename match_func end_func [separator] + +(* ============================================================================= *) +(* = Fonction de lecture d'un fichier (separateur par defaut = espace) = *) +(* ============================================================================= *) +let do_read_file2 filename match_func end_func = + do_read_file_with_separators2 filename match_func end_func [' '] + +(* ============================================================================= *) +(* = Fonction de parsing d'un fichier = *) +(* ============================================================================= *) +let do_parse_file filename no_ligne_parsing parser_main lexer_token end_func = + let c = + (try Some (open_compress filename) + with _ -> Printf.printf "Erreur d'ouverture du fichier %s\n" filename; + flush stdout; None) in + + match c with + None -> () + | Some c -> + let lexbuf = Lexing.from_channel c in + let fin = ref false in + no_ligne_parsing := 1 ; + while not !fin do + try parser_main lexer_token lexbuf ; + with Parsing.Parse_error -> (* Erreur de syntaxe *) + Printf.printf "Erreur ligne %d : *%s*\n" + !no_ligne_parsing (Lexing.lexeme lexbuf); flush stdout + | Failure("lexing: empty token") -> (* Est-ce la fin du fichier ? *) + fin := lexbuf.Lexing.lex_eof_reached ; + | x -> raise x + done ; + close_compress filename c ; + (* Appel a la fonction de fin de lecture *) + end_func () + +(* ============================================================================= *) +(* = Parsing d'un fichier de configuration = *) +(* ============================================================================= *) +let parse_config_file config_file spec_list anofun usage_msg = + let c = open_compress config_file in + (try + while true do + let s = input_line c in + let l = (split ' ' s) in + if l<>[] then begin + Arg.current := 0; + Arg.parse_argv (Array.of_list ("CMDE"::l)) spec_list anofun usage_msg; + end; + done; + with End_of_file -> ()); + Arg.current := 0; close_compress config_file c + +(* ============================================================================= *) +(* = Indique si le nom indique correspond a un repertoire = *) +(* ============================================================================= *) +let is_directory filename = + let stats = Unix.stat filename in stats.Unix.st_kind = Unix.S_DIR + +(* ============================================================================= *) +(* = Renvoie la liste des fichiers et repertoires contenus dans un repertoire = *) +(* ============================================================================= *) +let get_files_from_dir dirname = + let lst = ref [] in + try + let d = Unix.opendir dirname in + try + while true do let filename = Unix.readdir d in lst := filename::!lst done ; + [] + with End_of_file -> (* Lecture terminee *) + List.fast_sort cmp_string !lst + with _ -> [] + +(* ============================================================================= *) +(* = Renvoie la liste des repertoires = *) +(* ============================================================================= *) +let get_dirs_only_from_dir dirname = + let l = get_files_from_dir dirname in + List.fold_right (fun file l -> + if (file<>"."&&file<>".."&&(is_directory (dirname^file))) + then file::l else l) l [] + +(* ============================================================================= *) +(* = Renvoie la liste des fichiers = *) +(* ============================================================================= *) +let get_files_only_from_dir dirname = + let l = get_files_from_dir dirname in + List.fold_right (fun file l -> + if not (is_directory (dirname^file)) then file::l else l) l [] + +(* ============================================================================= *) +(* = Supprime l'eventuel chemin dans un nom de fichier = *) +(* ============================================================================= *) +let del_path_in_filename filename = Filename.basename filename + + +(* ***************************************************************************** *) +(* ***************************************************************************** *) +(* Manipulations de dates *) +(* ***************************************************************************** *) +(* ***************************************************************************** *) + +(* ============================================================================= *) +(* = Date sous la forme 20020114 -> 14 01 2002 = *) +(* ============================================================================= *) +let decompose_date date = (date mod 100, (date mod 10000)/100, date/10000) + +(* ============================================================================= *) +(* = Date sous la forme 14 01 2002 -> 20020114 = *) +(* ============================================================================= *) +let compose_date (jj, mm, aa) = aa*10000+mm*100+jj + +(* ============================================================================= *) +(* = Renvoie le nom du mois en fonction de son numero = *) +(* ============================================================================= *) +let get_month_of_num num = + match num with + 1 -> "Janvier" | 2 -> "Fevrier" | 3 -> "Mars" | 4 -> "Avril" + | 5 -> "Mai" | 6 -> "Juin" | 7 -> "Juillet" | 8 -> "Aout" + | 9 -> "Septembre" | 10 -> "Octobre" | 11 -> "Novembre" | 12 -> "Decembre" + | _ -> "???" + +(* ============================================================================= *) +(* = Renvoie le numero du jour de la semaine en fonction d'une date = *) +(* ============================================================================= *) +let get_day_of_date (jj, mm, aa) = + let jour_sem = [|"Dimanche"; "Lundi"; "Mardi"; "Mercredi"; "Jeudi"; + "Vendredi"; "Samedi"|] in + + let (aa, mm) = if mm < 3 then (aa-1, mm+10) else (aa, mm-2) in + let siecle = aa/100 and an = aa mod 100 in + let js = (((26 * mm - 2) / 10) + jj + an + (an / 4) + + (siecle / 4) - (2 * siecle)) mod 7 in + if js<0 then jour_sem.(js+7) else jour_sem.(js) + +let get_day_of_date2 date = get_day_of_date (decompose_date date) + +(* ============================================================================= *) +(* = L'annee indiquee est-elle bissextile ? = *) +(* = Elle l'est si elle est divisible par 4, sauf si c'est un siecle. Cependant= *) +(* = tous les 4 siecles, elle est bissextile... = *) +(* ============================================================================= *) +let is_year_bis aa = (aa mod 400 = 0) || ((aa mod 4 = 0) && (aa mod 100 <> 0)) + +(* ============================================================================= *) +(* = Renvoie le nombre de jours d'un mois en fonction du mois et de l'annee = *) +(* ============================================================================= *) +let get_nb_days_in_month mm aa = + if mm = 2 then (* Annee bissextile ? *) if is_year_bis aa then 29 else 28 + else match mm with + 1 -> 31 | 2 -> 28 | 3 -> 31 | 4 -> 30 | 5 -> 31 | 6 -> 30 + | 7 -> 31 | 8 -> 31 | 9 -> 30 | 10 -> 31 | 11 -> 30 | 12 -> 31 + | _ -> (-1) + +(* ============================================================================= *) +(* = Renvoie la date augmentee de delta jours (delta peut etre negatif) = *) +(* ============================================================================= *) +let get_delta_date (jj, mm, aa) delta = + let d = ref delta and j = ref jj and m = ref mm and a = ref aa in + let adding = delta>=0 in + while !d <> 0 do + let lg_month = get_nb_days_in_month !m !a in + if !d+ !j<=lg_month && !d+ !j>0 then begin j:=!j+ !d; d:=0 end else begin + if adding then begin + if !m<12 then incr m else begin m:=1; incr a end ; + d:=!d-(lg_month- !j)-1 ; + j:=1 + end else begin + if !m>1 then decr m else begin m:=12; decr a end ; + d:=!d+ !j ; + j:=get_nb_days_in_month !m !a + end ; + end + done ; + (!j, !m, !a) + +let get_delta_date2 date delta = + compose_date (get_delta_date (decompose_date date) delta) + +(* ============================================================================= *) +(* = Renvoie la date precedente/suivante = *) +(* ============================================================================= *) +let get_next_date (jj, mm, aa) = get_delta_date (jj, mm, aa) 1 +let get_next_date2 date = get_delta_date2 date 1 +let get_prev_date (jj, mm, aa) = get_delta_date (jj, mm, aa) (-1) +let get_prev_date2 date = get_delta_date2 date (-1) + +(* ============================================================================= *) +(* = Fonction renvoyant le nombre de jours entre deux dates d2-d1 = *) +(* ============================================================================= *) +let get_diff_date (jj1, mm1, aa1) (jj2, mm2, aa2) = + (* On se debarrasse du cas trivial *) + if mm1=mm2 && aa1=aa2 then jj2-jj1 else begin + (* Sens dans lequel on se deplace *) + let delta = + if compose_date (jj1, mm1, aa1)(jj2, mm2, aa2) do + nb_days:= !nb_days+delta ; + current_date:=get_delta_date !current_date delta + done ; + !nb_days + end + +let get_diff_date2 d1 d2 = get_diff_date (decompose_date d1) (decompose_date d2) + +(* ============================================================================= *) +(* = Date -> Chaine JJ/MM/AAAA = *) +(* ============================================================================= *) +let string_of_date (jj, mm, aa) = Printf.sprintf "%02d/%02d/%d" jj mm aa +let string_of_date2 date = string_of_date (decompose_date date) + +(* ============================================================================= *) +(* = Temps en secondes -> Chaine = *) +(* ============================================================================= *) +let string_of_time s = + Printf.sprintf "%02d:%02d:%02d" (s/3600) (s/60 mod 60) (s mod 60) + +(* ============================================================================= *) +(* = Temps en secondes -> Chaine sans les secondes = *) +(* ============================================================================= *) +let string_of_time_without_seconds s = + Printf.sprintf "%02d:%02d" (s/3600) (s/60 mod 60) + +(* ============================================================================= *) +(* = Chaine -> Temps en secondes = *) +(* ============================================================================= *) +let time_of_string t = + match split ':' t with + [h;m;s]->(int_of_string h*60 + int_of_string m)*60 + int_of_string s |_->0 + +(* ============================================================================= *) +(* = Renvoie l'heure = *) +(* ============================================================================= *) +let timer_get_time () = Unix.localtime (Unix.time ()) + +(* ============================================================================= *) +(* = Formate l'heure dans une chaine de caracteres = *) +(* ============================================================================= *) +let timer_string_of_time tm = + Printf.sprintf "%02d:%02d:%02d" tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec + +(* ============================================================================= *) +(* = Formate la date dans une chaine de caracteres = *) +(* ============================================================================= *) +let timer_string_of_date tm = + Printf.sprintf "%02d/%02d/%04d" tm.Unix.tm_mday (tm.Unix.tm_mon+1) + (tm.Unix.tm_year+1900) + +(* ============================================================================= *) +(* = Renvoie le temps ecoule entre deux heures, en secondes = *) +(* ============================================================================= *) +let timer_sub tm1 tm2 = + let (h1, m1, s1, d1) = + (tm1.Unix.tm_hour,tm1.Unix.tm_min,tm1.Unix.tm_sec, tm1.Unix.tm_mday) + and (h2, m2, s2, d2) = + (tm2.Unix.tm_hour,tm2.Unix.tm_min,tm2.Unix.tm_sec, tm2.Unix.tm_mday) in + (d2-d1)*24*3600+(h2-h1)*3600+(m2-m1)*60+s2-s1 + +(* ============================================================================= *) +(* = Chaine en Heures, Minutes, Secondes indiquant le temps ecoule = *) +(* ============================================================================= *) +let timer_string_of_secondes sec = + let h = sec / 3600 in + let reste = sec-h*3600 in + let m = reste/60 and s = reste mod 60 in + Printf.sprintf "%d:%02d:%02d" h m s + +(* ============================================================================= *) +(* = Tirage aleatoire d'un element dans une liste = *) +(* ============================================================================= *) +let tirage_aleatoire_lst (lst:'a list) = List.nth lst (Random.int (List.length lst)) + +(* =============================== FIN ========================================= *) diff --git a/sw/lib/ocaml/ocaml_tools.mli b/sw/lib/ocaml/ocaml_tools.mli new file mode 100644 index 00000000000..daf598a080b --- /dev/null +++ b/sw/lib/ocaml/ocaml_tools.mli @@ -0,0 +1,393 @@ +(* + * $Id$ + * + * Utilities + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(** Version de la librairie Ocaml_tools sous la forme d'une chaine *) +val ocaml_tools_version : string + + +(** {6 Fonctions de comparaison} *) + + +(** [cmp_string entier1 entier2] compare les 2 entiers (pour un List.sort) *) +val cmp_int : int -> int -> int + +(** [cmp_float float1 float2] compare les 2 flottants (pour un List.sort) *) +val cmp_float : 'a -> 'a -> int + +(** [cmp_string chaine1 chaine2] compare les 2 chaines (pour un List.sort) *) +val cmp_string : 'a -> 'a -> int + + +(** {6 Manipulation de chaines de caractères} *) + + +(** [split caractere chaine] découpe [chaine] suivant [caractere] et renvoie la + liste des mots decoupés. Toutes les lignes commencant par '#' sont supprimées *) +val split : char -> string -> string list + +(** [split2 caractere chaine] découpe [chaine] suivant [caractere] et renvoie la + liste des mots decoupés. Toutes les lignes commencant par '#' sont supprimées. + Ici, on découpe aprés chaque occurence de [caractere], il peut donc y avoir + des mots vides renvoyés *) +val split2 : char -> string -> string list + +(** [split_multiple lst_caracteres chaine] découpe [chaine] suivant les + caractères précisés comme la fonction [split] sauf qu'ici on peut préciser + plusieurs caractères de découpage *) +val split_multiple : char list -> string -> string list + +(** [split_multiple2 lst_caracteres chaine] découpe [chaine] suivant les + caractères précisés comme la fonction [split] sauf qu'ici on peut préciser + plusieurs caractères de découpage. On découpe après chaque occurence d'un des + caractères indiqués comme avec {!Ocaml_tools.split2} *) +val split_multiple2 : char list -> string -> string list + +(** [add_spaces longueur chaine] ajoute des espaces à [chaine] pour qu'elle fasse + [longueur] caractères *) +val add_spaces : int -> string -> string + +(** [delete_trailing_spaces chaine] supprime les espaces éventuels à la fin de + [chaine] *) +val delete_trailing_spaces : string -> string + +(** [string_replace_char chaine caractere1 caractere2] remplace toutes les + occurences de [caractere1] par [caractere2] dans [chaine] *) +val string_replace_char : string -> char -> char -> unit + +(** [string_dos2unix chaine] supprime l'eventuel caractère CTRL-M en fin + de chaine (transformation d'un fichier DOS vers Unix). *) +val string_dos2unix : string -> string + +(** [string_match pattern chaine] indique si le début de [chaine] est identique + à [pattern] *) +val string_match : string -> string -> bool + +(** [string_exact_match pattern chaine] indique si [chaine] est identique + à [pattern] (i.e chaine=pattern) *) +val string_exact_match : 'a -> 'a -> bool + +(** [string_match_no_case pattern chaine] identique à {!Ocaml_tools.string_match} + excepté que le test ne tient pas compte de la case des caractères *) +val string_match_no_case : string -> string -> bool + +(** [string_exact_match_no_case pattern chaine] identique à + {!Ocaml_tools.string_exact_match} + except& que le test ne tient pas compte de la case des caractères *) +val string_exact_match_no_case : string -> string -> bool + +(** [string_match_in pattern chaine] teste si la chaine contient [pattern] *) +val string_match_in : string -> string -> bool + +(** [string_match_in_no_case pattern chaine] teste si la chaine contient + [pattern]. Le test ne tient pas compte de la case des caractères *) +val string_match_in_no_case : string -> string -> bool + +(** [string_of_string_list liste_de_chaines] construit une chaine à partir + d'une liste de chaines de caractères *) +val string_of_string_list : string list -> string + +(** [eval_string chaine_base n feminin premier_char_capital] construit une chaine + de caractères à partir d'un nombre : + - [chaine_base] indique le nom + - [n] indique le nombre + - [feminin] indique si le nom est féminin + - [premier_char_capital] indique si le premier caractère de la chaine doit etre + en majuscule + + ex : + - [eval_string "secteur" 0 false true] donne "Aucun secteur" + - [eval_string "secteur" 1 false true] donne "Un secteur" + - [eval_string "secteur" 2 false true] donne "2 secteurs" + *) +val eval_string : string -> int -> bool -> bool -> string + +(** [do_parse_string chaine numero_ligne parser_main lexer_token end_func] parse + une chaine de caractères au lieu d'un fichier*) +val do_parse_string : + string -> + int ref -> ('a -> Lexing.lexbuf -> unit) -> 'a -> (unit -> 'b) -> 'b + +(** [parse_config_file fichier spec_list anofun usage_msg] parse un fichier de configuration + comme si c'était des options passées sur la ligne de commandes *) +val parse_config_file : string -> + (string * Arg.spec * string) list -> (string -> unit) -> string -> unit + +(** {6 Listes} *) + + +(** [supprime_dbl_list liste] supprime les doublons dans une liste d'entiers + triée *) +val supprime_dbl_list : 'a list -> 'a list + +(** [del_elt_lst liste index] supprime l'élément d'index [index] dans la liste *) +val del_elt_lst : 'a list -> int -> 'a list + + +(** {6 Ouverture/fermeture de fichiers compressés} *) + + +(** [open_compress fichier] ouvre [fichier] qui peut etre non compressé, + compressé avec gzip (se terminant par .gz ou .Z), avec bzip2 (se terminant par .bz2) + ou avec zip (extension en .zip ou .ZIP). Dans ce dernier cas, seuls les fichiers + zippés contenant UN seul fichier sont pris en compte *) +val open_compress : string -> in_channel + +(** [close_compress fichier channel] ferme un fichier ouvert + avec {!Ocaml_tools.open_compress} *) +val close_compress : string -> in_channel -> unit + +(** [find_file path file] Search for [file] or a compressed extension of it in +[path]. Returns the first occurence found. Checked extensions are .gz, .Z, .bz2, .zip +and , ZIP *) +val find_file : string list -> string -> string + +(** [open_gzip fichier] ouvre un fichier non compressé ou compressé avec gzip + (.gz ou .Z) *) +val open_gzip : string -> in_channel + +(** [close_gzip fichier channel] ferme un fichier non compressé ou + compressé avec gzip (.gz ou .Z) *) +val close_gzip : string -> in_channel -> unit + +(** [open_bzip fichier] ouvre un fichier non compressé ou compressé avec bzip2 + (.bz2) *) +val open_bzip : string -> in_channel + +(** [close_bzip fichier channel] ferme un fichier non compressé ou + compressé avec bzip2 (.bz2) *) +val close_bzip : string -> in_channel -> unit + +(** [open_zip fichier] ouvre un fichier non compressé ou compressé avec zip + (.zip ou .ZIP) *) +val open_zip : string -> in_channel + +(** [close_zip fichier channel] ferme un fichier non compressé ou + compressé avec zip *) +val close_zip : string -> in_channel -> unit + +(** [do_compress_file fichier extension] effectue la compression du fichier avec + gzip si [extension] vaut ".gz" ou bzip2 si [extension] vaut ".bz2" *) +val do_compress_file : string -> string -> unit + + +(** {6 Lecture/parsing de fichiers} *) + + +(** [string_of_file fichier] lit le fichier (éventuellement compressé) et renvoie + une chaine de caractères correspondant à son contenu *) +val string_of_file : string -> string + +(** [do_read_file_with_separators fichier match_func end_func separateurs] lit + un fichier et decoupe chacune de ses lignes suivant les caractères [separateurs]. + Les lignes ainsi decoupées sont passées à la fonction utilisateur [match_func] + qui les traite. Cette fonction reçoit comme arguments la liste des mots de + la ligne decoupée ainsi qu'une fonction d'erreur à appeler si la ligne + n'est pas au format voulu. + En fin de lecture, la fonction [end_func] est appelée *) +val do_read_file_with_separators : + string -> + (string list -> (unit -> unit) -> unit) -> (unit -> unit) -> char list -> unit + +(** [do_read_file_with_separator fichier match_func end_func separateur] lit + un fichier et decoupe chacune de ses lignes suivant le caractère [separateur]. + Les lignes ainsi decoupées sont passées à la fonction utilisateur [match_func] + qui les traite. Cette fonction reçoit comme arguments la liste des mots de + la ligne decoupée ainsi qu'une fonction d'erreur à appeler si la ligne + n'est pas au format voulu. + En fin de lecture, la fonction [end_func] est appelée *) +val do_read_file_with_separator : + string -> + (string list -> (unit -> unit) -> unit) -> (unit -> unit) -> char -> unit + +(** [do_read_file fichier match_func end_func] : meme chose que + {!Ocaml_tools.do_read_file_with_separator} sauf que le caractère séparateur + est implicitement l'espace *) +val do_read_file : + string -> (string list -> (unit -> unit) -> unit) -> (unit -> unit) -> unit + +(** [do_read_file_with_separators2 fichier match_func end_func separateurs] : + identique à {!Ocaml_tools.do_read_file_with_separators} sauf qu'on découpe + après chaque occurence des caractères de séparation *) +val do_read_file_with_separators2 : + string -> + (string list -> (unit -> unit) -> unit) -> (unit -> unit) -> char list -> unit + +(** [do_read_file_with_separator2 fichier match_func end_func separateur] : + identique à {!Ocaml_tools.do_read_file_with_separator} sauf qu'on découpe + après chaque occurence du caractère de séparation *) +val do_read_file_with_separator2 : + string -> + (string list -> (unit -> unit) -> unit) -> (unit -> unit) -> char -> unit + +(** [do_read_file2 fichier match_func end_func] : meme chose que + {!Ocaml_tools.do_read_file} sauf qu'on découpe + après chaque occurence du caractère de séparation *) +val do_read_file2 : + string -> (string list -> (unit -> unit) -> unit) -> (unit -> unit) -> unit + +(** [do_parse_file fichier numero_ligne parser_main lexer_token end_func] lit + un fichier qui est analysé par un parser. Les arguments utilisés sont : + - [fichier] : le nom du fichier à traiter + - [numero_ligne] : référence sur un entier contenant le numéro de la ligne + en cours de lecture + - [parser_main] : la fonction [main] du parser + - [lexer_token] : la fonction [token] du lexer + - [end_func] : fonction utilisateur appelée en fin de lecture du fichier + *) +val do_parse_file : + string -> + int ref -> ('a -> Lexing.lexbuf -> unit) -> 'a -> (unit -> unit) -> unit + + +(** {6 Répertoires/Noms de fichiers} *) + + +(** [is_directory nom] test si [nom] est un répertoire *) +val is_directory : string -> bool + +(** [get_files_from_dir repertoire] renvoie une liste de chaines de caractères + contenant les répertoires et fichiers contenus dans [repertoire] *) +val get_files_from_dir : string -> string list + +(** [get_dirs_only_from_dir repertoire] renvoie une liste de chaines de caractères + contenant les répertoires contenus dans [repertoire] (sans "." et "..") *) +val get_dirs_only_from_dir : string -> string list + +(** [get_files_only_from_dir repertoire] renvoie une liste de chaines de caractères + contenant les fichiers contenus dans [repertoire] *) +val get_files_only_from_dir : string -> string list + +(** [del_path_in_filename fichier] supprime l'éventuel chemin contenu dans + la chaine [fichier] *) +val del_path_in_filename : string -> string + + +(** {6 Manipulation de dates} *) + + +(** [decompose_date date] decompose une [date] de la forme 20020114 en un triplet + contenant le jour, le mois et l'année (ici (14, 01, 2002)) *) +val decompose_date : int -> int * int * int + +(** [compose_date (jour, mois, annee)] effectue l'operation inversion de + {!Ocaml_tools.decompose_date} *) +val compose_date : int * int * int -> int + +(** [get_month_of_num numero_du_mois] renvoie une chaine de caractères contenant + le mois donné par son numéro. 1 -> "Janvier" *) +val get_month_of_num : int -> string + +(** [get_day_of_date (jour, mois, annee)] renvoie une chaine indiquant le jour + de la semaine correspondant à la date donnée *) +val get_day_of_date : int * int * int -> string + +(** [get_day_of_date2 date] renvoie une chaine indiquant le jour + de la semaine correspondant à la date donnée *) +val get_day_of_date2 : int -> string + +(** [is_year_bis annee] indique si l'année donnée est bissextile ou pas *) +val is_year_bis : int -> bool + +(** [get_nb_days_in_month mois annee] indique le nombre de jour du mois de l'année + indiquée ([mois]=1 correspond a Janvier) *) +val get_nb_days_in_month : int -> int -> int + +(** [get_delta_date (jour, mois, annee) delta_jours] renvoie un triplet contenant + la date augmentée de [delta_jours]. [delta_jours] peut etre négatif *) +val get_delta_date : int * int * int -> int -> int * int * int + +(** [get_delta_date date delta_jours] renvoie une date entière correspondant à + [date] augmentée de [delta_jours]. [delta_jours] peut etre négatif *) +val get_delta_date2 : int -> int -> int + +(** [get_next_date (jour, mois, annee)] renvoie un triplet correspondant à la date + augmentée de 1 jour *) +val get_next_date : int * int * int -> int * int * int + +(** [get_next_date2 date] renvoie une date correspondant à [date] + augmentée de 1 jour *) +val get_next_date2 : int -> int + +(** [get_prev_date (jour, mois, annee)] renvoie un triplet correspondant à la date + diminuée de 1 jour *) +val get_prev_date : int * int * int -> int * int * int + +(** [get_prev_date2 date] renvoie une date correspondant à [date] diminuée de + 1 jour *) +val get_prev_date2 : int -> int + +(** [get_diff_date (jour1, mois1, annee1) (jour2, mois2, annee2)] renvoie le nombre de jours (signé) séparant la première date de la seconde (d2-d1) *) +val get_diff_date : int * int * int -> int * int * int -> int + +(** [get_diff_date2 date1 date2] idem à la fonction précédente avec des dates en entiers sous la forme AAAAMMJJ *) +val get_diff_date2 : int -> int -> int + +(** [string_of_date (jour, mois, annee)] fournit une chaine correspondant à la date + sous la forme JJ/MM/AAAA *) +val string_of_date : int * int * int -> string + +(** [string_of_date2 date] fournit une chaine correspondant à la date + sous la forme JJ/MM/AAAA *) +val string_of_date2 : int -> string + + +(** {6 Manipulation de temps} *) + + +(** [string_of_time temps] transforme un [temps] en secondes en une chaine + au format hh:mm:ss *) +val string_of_time : int -> string + +(** [string_of_time_without_seconds temps] transforme un [temps] en + secondes en une chaine au format hh:mm *) +val string_of_time_without_seconds : int -> string + +(** [time_of_string chaine] transforme une chaine au format hh:mm:ss en secondes *) +val time_of_string : string -> int + +(** renvoie l'heure actuelle *) +val timer_get_time : unit -> Unix.tm + +(** [timer_string_of_time heure] renvoie une chaine sous la forme hh:mm:ss *) +val timer_string_of_time : Unix.tm -> string + +(** [timer_string_of_date heure] renvoie une chaine au format JJ/MM/AAAA *) +val timer_string_of_date : Unix.tm -> string + +(** [timer_sub heure1 heure2] renvoie le nombre de secondes ecoulées entre + [heure1] et [heure2] *) +val timer_sub : Unix.tm -> Unix.tm -> int + +(** [timer_string_of_secondes secondes] renvoie une chaine à partir du temps donné + en secondes. La chaine est au format h:mm:ss *) +val timer_string_of_secondes : int -> string + + +(** {6 Tirages aléatoires} *) + +(** [tirage_aleatoire_lst liste] effectue un tirage aléatoire d'un des éléments de la + liste et renvoie cet élément *) +val tirage_aleatoire_lst : 'a list -> 'a diff --git a/sw/lib/ocaml/platform.ml b/sw/lib/ocaml/platform.ml new file mode 100644 index 00000000000..1f3f03d7c45 --- /dev/null +++ b/sw/lib/ocaml/platform.ml @@ -0,0 +1,31 @@ +(* + * $Id$ + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let platform_name = + let os = Sys.os_type in + if os = "Win32" then Unix.putenv "GTK_RC_FILES" (Unix.getcwd () ^ "/wingtk.rc") ; + os + +let platform_is_unix = platform_name = "Unix" +let platform_is_win32 = platform_name = "Win32" diff --git a/sw/lib/ocaml/platform.mli b/sw/lib/ocaml/platform.mli new file mode 100644 index 00000000000..3d9cdf015a9 --- /dev/null +++ b/sw/lib/ocaml/platform.mli @@ -0,0 +1,32 @@ +(* + * $Id$ + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +(** Renvoie le nom de la plateforme : Unix ou Win32 *) +val platform_name : string + +(** Teste si la plateforme courante est Unix *) +val platform_is_unix : bool + +(** Teste si la plateforme courante est Windows (Win32) *) +val platform_is_win32 : bool diff --git a/sw/lib/ocaml/pprz.ml b/sw/lib/ocaml/pprz.ml new file mode 100644 index 00000000000..9fc404bf87a --- /dev/null +++ b/sw/lib/ocaml/pprz.ml @@ -0,0 +1,192 @@ +(* + * $Id$ + * + * Downlink protocol (handling messages.xml) + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf + + +type message_id = int +type class_name = string +type format = string +type _type = string +type value = string +type field = { + _type : _type; + fformat : format; + } + +type message = { + name : string; + fields : (string * field) list + } + +type type_descr = { + format : string ; + glib_type : string; + size : int; + value : string + } + + + + +let (//) = Filename.concat +let messages_xml = Xml.parse_file (Env.paparazzi_src // "conf" // "messages.xml") + +external float_of_bytes : string -> int -> float = "c_float_of_indexed_bytes" +external int32_of_bytes : string -> int -> int32 = "c_int32_of_indexed_bytes" +let types = [ + ("uint8", { format = "%u"; glib_type = "guint8"; size = 1; value="42" }); + ("uint16", { format = "%u"; glib_type = "guint16"; size = 2; value="42" }); + ("uint32", { format = "%lu" ; glib_type = "guint32"; size = 4; value="42" }); + ("int8", { format = "%d"; glib_type = "gint8"; size = 1; value="42" }); + ("int16", { format = "%d"; glib_type = "gint16"; size = 2; value="42" }); + ("int32", { format = "%ld" ; glib_type = "gint32"; size = 4; value="42" }); + ("float", { format = "%f" ; glib_type = "gfloat"; size = 4; value="4.2" }) +] + +let size_of_field = fun f -> (List.assoc f._type types).size +let default_format = fun x -> (List.assoc x types).format +let default_value = fun x -> (List.assoc x types).value + +let size_of_message = fun message -> + List.fold_right + (fun (_, f) s -> size_of_field f + s) + message.fields + 4 + +let field_of_xml = fun xml -> + let t = ExtXml.attrib xml "type" in + let f = try Xml.attrib xml "format" with _ -> default_format t in + (ExtXml.attrib xml "name", { _type = t; fformat = f }) + + +(** Table of msg classes indexed by name. Each class is a table of messages +indexed by ids *) +let classes = Hashtbl.create 13 +let _ = + List.iter + (fun xml_class -> + let by_id = Hashtbl.create 13 + and by_name = Hashtbl.create 13 in + List.iter + (fun xml_msg -> + try + let name = ExtXml.attrib xml_msg "name" in + let msg = { + name = name; + fields = List.map field_of_xml (Xml.children xml_msg) + } in + let id = int_of_string (ExtXml.attrib xml_msg "id") (* - 1 !!!!*) in + Hashtbl.add by_id id msg; + Hashtbl.add by_name name (id, msg) + with _ -> + fprintf stderr "Warning: Ignoring '%s'\n" (Xml.to_string xml_msg)) + (Xml.children xml_class); + Hashtbl.add classes (ExtXml.attrib xml_class "name") (by_id, by_name) + ) + (Xml.children messages_xml) + +let magic = fun x -> (Obj.magic x:('a,'b,'c) Pervasives.format) + + let format_field = fun buffer index (field:field) -> + let format = field.fformat in + match field._type with + "uint8" | "int8" -> sprintf (magic format) (Char.code buffer.[index]) + | "uint16" | "int16" -> sprintf (magic format) (Char.code buffer.[index] lsl 8 + Char.code buffer.[index+1]) + | "float" -> sprintf (magic format) (float_of_bytes buffer index) + | "int32" | "uint32" -> sprintf (magic format) (int32_of_bytes buffer index) + | _ -> failwith "format_field" + +module type CLASS = sig val name : string end + +exception Unknown_msg_name of string + +module Protocol(Class:CLASS) = struct + let stx = Char.chr 0x05 + let index_start = fun buf -> + String.index buf stx + + let messages_by_id, messages_by_name = Hashtbl.find classes Class.name + let message_of_id = fun id -> Hashtbl.find messages_by_id (id (*** +1 ***)) + let message_of_name = fun name -> Hashtbl.find messages_by_name name + + let length = fun buf start -> + let len = String.length buf - start in + if len >= 2 then + let id = Char.code buf.[start+1] in + let msg = message_of_id id in + let l = size_of_message msg in + Debug.call 'T' (fun f -> fprintf f "Pprz id=%d len=%d\n" id l); + l + else + raise Serial.Not_enough + + let (+=) = fun r x -> r := (!r + x) land 0xff + let checksum = fun msg -> + let l = String.length msg in + let ck_a = ref 0 and ck_b = ref 0 in + for i = 1 to l - 3 do + ck_a += Char.code msg.[i]; + ck_b += !ck_a + done; + Debug.call 'T' (fun f -> fprintf f "Pprz ck: %d %d\n" !ck_a (Char.code msg.[l-2])); + !ck_a = Char.code msg.[l-2] && !ck_b = Char.code msg.[l-1] + + let values_of_bin = fun buffer -> + let id = Char.code buffer.[1] in + let message = message_of_id id in + Debug.call 'T' (fun f -> fprintf f "Pprz.values id=%d\n" id); + let rec loop = fun index fields -> + match fields with + [] -> [] + | (field_name, field_descr)::fs -> + let n = size_of_field field_descr in + (field_name, format_field buffer index field_descr) :: loop (index+n) fs in + (id, loop 2 message.fields) + + let space = Str.regexp "[ \t]+" + let values_of_string = fun s -> + match Str.split space s with + msg_name::args -> + begin + try + let msg_id, msg = message_of_name msg_name in + let values = List.map2 (fun (field_name, _) v -> (field_name, v)) msg.fields args in + (msg_id, values) + with + Not_found -> raise (Unknown_msg_name msg_name) + end + | [] -> invalid_arg "Pprz.values_of_string" + + let string_of_message = fun msg values -> + String.concat " " + (msg.name:: + List.map + (fun (field_name, field) -> + try List.assoc field_name values with Not_found -> default_value field._type) + msg.fields) +end + diff --git a/sw/lib/ocaml/pprz.mli b/sw/lib/ocaml/pprz.mli new file mode 100644 index 00000000000..d2439a7f90c --- /dev/null +++ b/sw/lib/ocaml/pprz.mli @@ -0,0 +1,57 @@ +(* + * $Id$ + * + * Downlink protocol (handling messages.xml) + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +type class_name = string +type message_id = int +type format = string +type _type = string +type value = string +type field = { _type : _type; fformat : format; } +type message = { name : string; fields : (string * field) list; } +type type_descr = { format : format; glib_type : string; size:int; value:string} +val types : (string * type_descr) list +val size_of_field : field -> int +val default_format : string -> string + +exception Unknown_msg_name of string + +module type CLASS = sig val name : string end +module Protocol : functor (Class : CLASS) -> sig + include Serial.PROTOCOL + val message_of_id : message_id -> message + val message_of_name : string -> message_id * message + val values_of_bin : string -> message_id * (string * string) list +(** [values raw_message] Parses a raw message, returns the + message id and the liste of (field_name, value) *) + + val values_of_string : string -> message_id * (string * string) list + (** May raise [(Unknown_msg_name msg_name)] *) + + val string_of_message : message -> (string * string) list -> string + (** [string_of_message msg values] *) +end + + diff --git a/sw/lib/ocaml/serial.ml b/sw/lib/ocaml/serial.ml new file mode 100644 index 00000000000..577c49c5a81 --- /dev/null +++ b/sw/lib/ocaml/serial.ml @@ -0,0 +1,114 @@ +(* + * $Id$ + * + * Serial Port handling + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf + +type speed = + B0 + | B50 + | B75 + | B110 + | B134 + | B150 + | B200 + | B300 + | B600 + | B1200 + | B1800 + | B2400 + | B4800 + | B9600 + | B19200 + | B38400 + | B57600 + | B115200 + | B230400 + + +external init_serial : string -> speed -> Unix.file_descr = "c_init_serial";; + +let opendev device speed = + try + init_serial device speed + with + Failure x -> + failwith (Printf.sprintf "%s (%s)" x device) + +let close = Unix.close + + +let buffer_len = 256 +let input = fun f -> + let buffer = String.create buffer_len + and index = ref 0 in + + let wait = fun start n -> + String.blit buffer start buffer 0 n; + index := n in + + fun fd -> + let n = !index + Unix.read fd buffer !index (buffer_len - !index) in + Debug.call 'T' (fun f -> fprintf f "input: %d %d\n" !index n); + let rec parse = fun start n -> + Debug.call 'T' (fun f -> fprintf f "input parse: %d %d\n" start n); + let nb_used = f (String.sub buffer start n) in +(* Printf.fprintf stderr "n'=%d\n" nb_used; flush stderr; *) + if nb_used > 0 then + parse (start + nb_used) (n - nb_used) + else + wait start n in + parse 0 n + + +exception Not_enough + +module type PROTOCOL = sig + val index_start : string -> int (* raise Not_found *) + val length : string -> int -> int (* raise Not_enough *) + val checksum : string -> bool +end + +module Transport(Protocol:PROTOCOL) = struct + let rec parse = fun use buf -> + let start = ref 0 + and n = String.length buf in + try + start := Protocol.index_start buf; + let length = Protocol.length buf !start in + let end_ = !start + length in + if n < end_ then + raise Not_enough; + let msg = String.sub buf !start length in + if Protocol.checksum msg then begin + use msg + end else + Debug.call 'T' (fun f -> fprintf f "Transport.chk: %s\n" (Debug.xprint msg)) + ; + end_ + parse use (String.sub buf end_ (String.length buf - end_)) + with + Not_found -> String.length buf + | Not_enough -> !start +end diff --git a/sw/lib/ocaml/serial.mli b/sw/lib/ocaml/serial.mli new file mode 100644 index 00000000000..93e0f6a9f2c --- /dev/null +++ b/sw/lib/ocaml/serial.mli @@ -0,0 +1,76 @@ +(* + * $Id$ + * + * Serial Port handling + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +type speed = + B0 + | B50 + | B75 + | B110 + | B134 + | B150 + | B200 + | B300 + | B600 + | B1200 + | B1800 + | B2400 + | B4800 + | B9600 + | B19200 + | B38400 + | B57600 + | B115200 + | B230400 + +val opendev : string -> speed -> Unix.file_descr +val close : Unix.file_descr -> unit + +val input : (string -> int) -> Unix.file_descr -> unit +(** [input f fd] Calls [f] on the buffer of available characters on [fd] each +time a new character arrives. [f] must return the number of consumed +characters *) + +exception Not_enough +module type PROTOCOL = + sig + val index_start : string -> int + (** Must return the index of the first char of the the first message. + May raise Not_found or Not_enough *) + + val length : string -> int -> int + (** [length buf start] Must return the length of the message starting at + [start]. May raise Not_enough *) + val checksum : string -> bool + (** [checksum message] *) + end + +module Transport : + functor (Protocol : PROTOCOL) -> + sig + val parse : (string -> unit) -> string -> int + (** [parse f buf] Scans [buf] according to [Protocol] and applies [f] on + every recognised message. Returns the number of consumed bytes. *) + end diff --git a/sw/lib/ocaml/srtm.ml b/sw/lib/ocaml/srtm.ml new file mode 100644 index 00000000000..bae494b9033 --- /dev/null +++ b/sw/lib/ocaml/srtm.ml @@ -0,0 +1,82 @@ +(* + * $Id$ + * + * Acces functions to SRTM data (http://edcftp.cr.usgs.gov/pub/data/srtm) + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Latlong + +type error = string +exception Tile_not_found of string + +let srtm_url = "ftp://e0dps01u.ecs.nasa.gov/srtm" + +let error = fun string -> + Printf.sprintf "wget %s/Eurasia/%s.hgt.zip" srtm_url string + +let tile_size = 1201 + +(* Previously opened tiles *) +let htiles = Hashtbl.create 13 + +(* Path to data files *) +let path = ref ["."] + +let add_path = fun p -> path := p :: !path + +let open_compressed = fun f -> + Ocaml_tools.open_compress (Ocaml_tools.find_file !path f) + +let find tile = + if not (Hashtbl.mem htiles tile) then begin + try + let f = open_compressed (tile^".hgt") in + let n = tile_size*tile_size*2 in + let buf = String.create n in + really_input f buf 0 n; + Hashtbl.add htiles tile buf + with Not_found -> + raise (Tile_not_found tile) + end; + Hashtbl.find htiles tile + +let get = fun tile y x -> + let tile = find tile in + let pos0 = (2*((tile_size-y)*tile_size+x)) in + let rec skip_bad = fun pos -> + let a = (Char.code tile.[pos] lsl 8) lor Char.code tile.[pos+1] in + if a > 8848 || a < 0 then skip_bad (pos+2) else a in + skip_bad pos0 + +let of_wgs84 = fun geo -> + let lat = (Rad>>Deg)geo.posn_lat + and long = (Rad>>Deg)geo.posn_long in + let bottom = floor lat and left = floor long in + let tile = + Printf.sprintf "%c%.0f%c%03.0f" (if lat > 0. then 'N' else 'S') bottom (if long > 0. then 'E' else 'W') (abs_float left) in + + get tile (truncate ((lat-.bottom)*.1200.+.0.5)) (truncate ((long-.left)*.1200.+.0.5)) + +let of_utm = fun utm -> + of_wgs84 (Latlong.of_utm WGS84 utm) + diff --git a/sw/lib/ocaml/srtm.mli b/sw/lib/ocaml/srtm.mli new file mode 100644 index 00000000000..e7ab560649a --- /dev/null +++ b/sw/lib/ocaml/srtm.mli @@ -0,0 +1,42 @@ +(* + * $Id$ + * + * Acces functions to SRTM data (http://edcftp.cr.usgs.gov/pub/data/srtm) + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +val srtm_url : string + +val add_path : string -> unit +(** [add_path directory] Adds [directory] to the current path where, possibly +compressed, SRTM data files are searched. *) + +type error = string +exception Tile_not_found of error + +val error : error -> string + +val of_utm : Latlong.utm -> int +(** [of_utm utm_pos] Returns the altitude of the given UTM position *) + +val of_wgs84 : Latlong.geographic -> int +(** [of_utm utm_pos] Returns the altitude of the given geographic position *) diff --git a/sw/lib/ocaml/ubx.ml b/sw/lib/ocaml/ubx.ml new file mode 100644 index 00000000000..a067460c8f7 --- /dev/null +++ b/sw/lib/ocaml/ubx.ml @@ -0,0 +1,159 @@ +(* + * $Id$ + * + * UBX protocol handling + * + * Copyright (C) 2004 CENA/ENAC, Yann Le Fablec, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) +module Protocol = struct + let index_start = fun buf -> + let rec loop = fun i -> + let i' = String.index_from buf i (Char.chr 0xb5) in + if String.length buf > i'+1 && buf.[i'+1] = Char.chr 0x62 then + i' + else + loop (i'+1) in + loop 0 + + let payload_length = fun buf start -> + Char.code buf.[start+5] lsl 8 + Char.code buf.[start+4] + + let length = fun buf start -> + let len = String.length buf - start in + if len > 6 then + payload_length buf start + 8 + else + raise Serial.Not_enough + + let payload = fun buf start -> + String.sub buf (start+6) (payload_length buf start) + + let uint8_t = fun x -> x land 0xff + let (+=) = fun r x -> r := uint8_t (!r + x) + let checksum = fun buf start payload -> + let ck_a = ref 0 and ck_b = ref 0 in + let l = String.length payload in + for i = 0 to l - 1 do + ck_a += Char.code payload.[i]; + ck_b += !ck_a + done; + !ck_a = Char.code buf.[start+l+6] && !ck_b = Char.code buf.[start+l+7] +end + +let (//) = Filename.concat + +let ubx_xml = + Xml.parse_file (Env.paparazzi_src // "conf" // "ubx.xml") + +let ubx_get_class = fun name -> + ExtXml.child ubx_xml ~select:(fun x -> ExtXml.attrib x "name" = name) "class" + +let ubx_nav = ubx_get_class "NAV" +let ubx_nav_id = int_of_string (ExtXml.attrib ubx_nav "ID") +let ubx_get_msg = fun ubx_class name -> + ExtXml.child ubx_class ~select:(fun x -> ExtXml.attrib x "name" = name) "message" + +let ubx_get_nav_msg = fun name -> ubx_get_msg ubx_nav name + +let nav_posllh = ubx_nav_id, ubx_get_nav_msg "POSLLH" +let nav_posutm = ubx_nav_id, ubx_get_nav_msg "POSUTM" +let nav_status = ubx_nav_id, ubx_get_nav_msg "STATUS" +let nav_velned = ubx_nav_id, ubx_get_nav_msg "VELNED" + + +let send_start_sequence = fun gps -> + output_byte gps 0xB5; + output_byte gps 0x62 + + +let sizeof = function + "U4" | "I4" -> 4 + | "U2" | "I2" -> 2 + | "U1" | "I1" -> 1 + | x -> failwith (Printf.sprintf "Ubx.sizeof: unknown format '%s'" x) + +let assoc = fun label fields -> + let rec loop o = function + [] -> raise Not_found + | f::fs -> + let format = ExtXml.attrib f "format" in + if ExtXml.attrib f "name" = label + then (o, format) + else loop (o + sizeof format) fs in + loop 0 fields + +let byte = fun x -> Char.chr (x land 0xff) + +let make_payload = fun msg_xml values -> + let n = int_of_string (ExtXml.attrib msg_xml "length") in + let p = String.make n '#' in + let fields = Xml.children msg_xml in + List.iter + (fun (label, value) -> + let (pos, fmt) = + try + assoc label fields + with + Not_found -> failwith (Printf.sprintf "Field '%s' not found in %s" label (Xml.to_string msg_xml)) + in + match fmt with + | "U1" -> + assert(value >= 0 && value < 0x100); + p.[pos] <- byte value + | "I4" | "U4" -> + assert(fmt <> "U4" || value >= 0); + p.[pos+3] <- byte (value asr 24); + p.[pos+2] <- byte (value lsr 16); + p.[pos+1] <- byte (value lsr 8); + p.[pos+0] <- byte value + | "U2" | "I2" -> + p.[pos+1] <- byte (value lsr 8); + p.[pos+0] <- byte value + | _ -> failwith (Printf.sprintf "Ubx.make_payload: unknown format '%s'" fmt) + ) + values; + p + + + + + +let send = fun gps (msg_class, msg) values -> + let msg_id = int_of_string (Xml.attrib msg "ID") in + let payload = make_payload msg values in + let n = String.length payload in + send_start_sequence gps; + + let ck_a = ref 0 and ck_b = ref 0 in + let output_byte_ck = fun c -> + ck_a := (!ck_a+c) land 0xff; ck_b := (!ck_b+ !ck_a) land 0xff; + output_byte gps c in + + output_byte_ck msg_class; + output_byte_ck msg_id; + output_byte_ck (n land 0xff); + output_byte_ck ((n land 0xff00) lsr 8); + String.iter (fun c -> output_byte_ck (Char.code c)) payload; + output_byte gps !ck_a; + output_byte gps !ck_b; + flush gps + + diff --git a/sw/lib/ocaml/utm_of.ml b/sw/lib/ocaml/utm_of.ml new file mode 100644 index 00000000000..2a1b8ea2692 --- /dev/null +++ b/sw/lib/ocaml/utm_of.ml @@ -0,0 +1,6 @@ +open Latlong + +let _ = + let f = fun i -> (Deg>>Rad)(float_of_string Sys.argv.(i)) in + let utm = utm_of WGS84 { posn_lat = f 1 ; posn_long = f 2 } in + Printf.printf "%d %d\n" utm.utm_x utm.utm_y diff --git a/sw/lib/ocaml/wavecard.ml b/sw/lib/ocaml/wavecard.ml new file mode 100644 index 00000000000..2ea9e77a5a0 --- /dev/null +++ b/sw/lib/ocaml/wavecard.ml @@ -0,0 +1,131 @@ +open Printf + +type cmd_name = string +type data = string + +type cmd = cmd_name * string + +let cmd_names = [ + 0x06, "ACK"; + 0x15, "NAK"; + 0x00, "ERROR"; + 0x40, "REQ_WRITE_RADIO_PARAM"; + 0x41, "RES_WRITE_RADIO_PARAM"; + 0x50, "REQ_READ_RADIO_PARAM"; + 0x51, "RES_READ_RADIO_PARAM"; + 0x60, "REQ_SELECT_CHANNEL"; + 0x61, "RES_SELECT_CHANNEL"; + 0x62, "REQ_READ_CHANNEL"; + 0x63, "RES_READ_CHANNEL"; + 0x64, "REQ_SELECT_PHYCONFIG"; + 0x65, "RES_SELECT_PHYCONFIG"; + 0x66, "REQ_READ_PHYCONFIG"; + 0x67, "RES_READ_PHYCONFIG"; + 0x68, "REQ_READ_REMOTE_RSSI"; + 0x69, "RES_READ_REMOTE_RSSI"; + 0x6A, "REQ_READ_LOCAL_RSSI"; + 0x6B, "RES_READ_LOCAL_RSSI"; + 0xA0, "REQ_FIRMWARE_VERSION"; + 0xA1, "RES_FIRMWARE_VERSION"; + 0xB0, "MODE_TEST"; + 0x20, "REQ_SEND_FRAME"; + 0x21, "RES_SEND_FRAME"; + 0x22, "REQ_SEND_MESSAGE"; + 0x26, "REQ_SEND_POLLING"; + 0x28, "REQ_SEND_BROADCAST"; + 0x30, "RECEIVED_FRAME"; + 0x31, "RECEPTION_ERROR"; + 0x32, "RECEIVED_FRAME_POLLING"; + 0x34, "RECEIVED_FRAME_BROADCAST"; + 0x36, "RECEIVED_MULTIFRAME"; + 0x80, "REQ_SEND_SERVICE"; + 0x81, "RES_SEND_SERVICE"; + 0x82, "SERVICE_RESPONSE"] + +let rec cossa = fun x l -> + match l with + [] -> raise Not_found + | (v, k)::xs -> if k = x then v else cossa x xs + +let cmd_name_of = fun x -> try List.assoc x cmd_names with Not_found -> failwith (sprintf "Unknown command: %2x" x) +let of_cmd_name = fun x -> try cossa x cmd_names with Not_found -> failwith (sprintf "Unknown command: %s" x) + +let sync = Char.chr 0xff +let stx = Char.chr 0x02 +let etx = Char.chr 0x03 + +let length = fun buf -> Char.code buf.[2] +let total_length = fun buf -> length buf + 3 + +let payload = fun buf -> + let l = length buf in + let data = String.sub buf 4 (l-4) in + (cmd_name_of (Char.code buf.[3]), data) + +let (^=) r x = r := !r lxor x + +let compute_checksum = + let poly = 0x8408 in + fun buf -> + let lg = length buf - 2 + and crc = ref 0 in + for j = 0 to lg - 1 do + crc ^= Char.code buf.[j+2]; + for i = 0 to 7 do + let carry = !crc land 0x01 = 1 in + crc := !crc lsr 1; + if carry then + crc ^= poly + done + done; + !crc + +let checksum = fun buf -> + let crc = compute_checksum buf + and l = length buf in + crc land 0xff = Char.code buf.[l] && crc lsr 8 = Char.code buf.[l+1] + + + + +let parse = fun buf ?ack f -> + let n = String.length buf in + if n < 3 || n < total_length buf then + 0 (* Not enough chars to read *) + else if buf.[0] <> sync then + 1 + else if buf.[1] <> stx || not (checksum buf) then + 2 + else begin + f (payload buf); + begin + match ack with + None -> () + | Some ack -> ack () + end; + total_length buf + end + + +let receive = fun ?ack f -> + Serial.input (fun b -> parse b ?ack f) + +let send = fun fd (cmd, data) -> + let l = String.length data + 4 in + if l >= 256 then + invalid_arg "Wavecard.send"; + let buf = String.create (l+3) in + buf.[0] <- sync; + buf.[1] <- stx; + buf.[2] <- Char.chr l; + buf.[3] <- Char.chr (of_cmd_name cmd); + for i = 4 to l - 1 do + buf.[i] <- data.[i-4] + done; + let crc = compute_checksum buf in + buf.[l] <- Char.chr (crc land 0xff); + buf.[l+1] <- Char.chr (crc lsr 8); + buf.[l+2] <- etx; + let o = Unix.out_channel_of_descr fd in + Printf.fprintf o "%s" buf; + flush o diff --git a/sw/lib/ocaml/wavecard.mli b/sw/lib/ocaml/wavecard.mli new file mode 100644 index 00000000000..c2f344112a6 --- /dev/null +++ b/sw/lib/ocaml/wavecard.mli @@ -0,0 +1,7 @@ +type cmd_name = string +type data = string +type cmd = cmd_name * data + +val send : Unix.file_descr -> cmd -> unit + +val receive : ?ack:(unit -> unit) -> (cmd -> 'a) -> (Unix.file_descr -> unit) diff --git a/sw/lib/ocaml/xml2h.ml b/sw/lib/ocaml/xml2h.ml new file mode 100644 index 00000000000..8b417b66352 --- /dev/null +++ b/sw/lib/ocaml/xml2h.ml @@ -0,0 +1,69 @@ +(* + * $Id$ + * + * XML preprocessing tools + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf + +exception Error of string + +let nl = print_newline + +let define = fun n x -> + printf "#define %s %s\n" n x + +let define_string = fun n x -> + define n ("\""^x^"\"") + + +let xml_error s = failwith ("Bad XML tag: "^s^ " expected") + + +let sprint_float_array = fun l -> + let rec loop = function + [] -> "}" + | [x] -> x ^ "}" + | x::xs -> x ^","^ loop xs in + "{" ^ loop l + + +let start_and_begin = fun xml_file h_name -> + let xml = Xml.parse_file xml_file in + + printf "/* This file has been generated from %s */\n" xml_file; + printf "/* Please DO NOT EDIT */\n\n"; + + printf "#ifndef %s\n" h_name; + define h_name ""; + nl (); + xml + +let finish = fun h_name -> + printf "\n#endif // %s\n" h_name + +let warning s = + Printf.fprintf stderr "\n##################################################\n"; + Printf.fprintf stderr "\n %s\n" s; + Printf.fprintf stderr "\n##################################################\n" + diff --git a/sw/lib/ocaml/xml_get.ml b/sw/lib/ocaml/xml_get.ml new file mode 100644 index 00000000000..4b19004c743 --- /dev/null +++ b/sw/lib/ocaml/xml_get.ml @@ -0,0 +1,16 @@ +open Xml2h +let _ = + if Array.length Sys.argv <> 4 then + failwith "Usage: conf_get "; + let xml_file = Sys.argv.(1) + and path = Sys.argv.(2) + and attribute = Sys.argv.(3) in + let xml = + try + Xml.parse_file xml_file + with + Xml.Error e -> + Printf.fprintf stderr "\nError in \"%s\": %s\n\n" xml_file (Xml.error e); + exit 1 + in + Printf.printf "%s\n" (ExtXml.get_attrib xml path attribute) diff --git a/sw/lib/perl/Makefile b/sw/lib/perl/Makefile new file mode 100644 index 00000000000..a0cbee84055 --- /dev/null +++ b/sw/lib/perl/Makefile @@ -0,0 +1,21 @@ + + +#all: IvyMsgs.pm + +#OCAMLC = ocamlc -I ../../lib/ocaml +#CONF_DIR = ../../../conf + +#GEN_MESSAGES = ./gen_messages.out +#MESSAGES_XML = $(CONF_DIR)/messages.xml + + +#$(GEN_MESSAGES) : gen_messages.ml +# $(OCAMLC) -o $@ lib.cma $< + +#IvyMsgs.pm : $(MESSAGES_XML) $(GEN_MESSAGES) +# $(GEN_MESSAGES) $< > /tmp/x.pm +# mv /tmp/x.pm $@ + +clean: + rm -f *~ +#IvyMsgs.pm gen_messages.out *.cmo *.cmi diff --git a/sw/lib/perl/Paparazzi/Environment.pm b/sw/lib/perl/Paparazzi/Environment.pm new file mode 100644 index 00000000000..de4f9fc3536 --- /dev/null +++ b/sw/lib/perl/Paparazzi/Environment.pm @@ -0,0 +1,69 @@ +package Paparazzi::Environment; + +use File::NCopy; +use Getopt::Long; + +use constant INST_PREFIX => "/usr"; + +my $paparazzi_src = undef; +my $paparazzi_home = $ENV{HOME}."/paparazzi"; + +if (defined $ENV{PAPARAZZI_SRC}) { + $paparazzi_src = $ENV{PAPARAZZI_SRC}; + $paparazzi_home = $ENV{PAPARAZZI_SRC}; +} +$paparazzi_home = $ENV{PAPARAZZI_HOME} if (defined $ENV{PAPARAZZI_HOME}); +print "\nEnvironment : "; +if (defined $paparazzi_src) { + print "source directory mode\n paparazzi_src $paparazzi_src\n"; +} +else { + print "system mode\n inst_prefix INST_PREFIX"; +} +print " paparazzi_home $paparazzi_home\n\n"; + +sub parse_command_line { + my ($options) = @_; + my $getopt_h = {"b=s" => \$options->{ivy_bus}}; + foreach my $option (keys %{$options}) { + $getopt_h->{$option."=s"} = \$options->{$option}; + } + return GetOptions (%{$getopt_h}); +} + +sub check_paparazzi_home { + unless (defined $paparazzi_src) { + unless (-e $paparazzi_home) { + print "\nDirectory $paparazzi_home doesn't exist\n"; + print "This directory is needed to store user configuration and data\n"; + print "Shall I create it and populate it with examples? (Y/n)\n"; + my $ans = ; + chop($ans); + if ($ans eq "" || $ans eq "Y" || $ans eq "y") { + print "Creating directory $paparazzi_home\n"; + mkdir($paparazzi_home, 0755); + print "Copying default config and examples\n"; + my $copier = File::NCopy->new(recursive => 1); + foreach my $dir ("conf", "var", "data") { + $copier->copy(INST_PREFIX."/share/paparazzi/".$dir, $paparazzi_home); + } + print "done.\n\n"; + } + else { + print "exiting...\n"; + exit(1); + } + } + } +} + +sub paparazzi_src { + return $paparazzi_src; +} + +sub paparazzi_home { + return $paparazzi_home; +} + + +1; diff --git a/sw/lib/perl/Paparazzi/IvyProtocol.pm b/sw/lib/perl/Paparazzi/IvyProtocol.pm new file mode 100644 index 00000000000..1dd4cb0ca3f --- /dev/null +++ b/sw/lib/perl/Paparazzi/IvyProtocol.pm @@ -0,0 +1,134 @@ +package Paparazzi::IvyProtocol; + +use strict; +use XML::DOM; + +use Data::Dumper; + +my $classes_by_name = {}; + +my $req_id = int rand(65534); +# TODO: make real timeout instead of single request +my $res_regexp = undef; + +sub request_message { + my ($msg_class, $msg_name, $args, $ivy, $callback) = @_; + my $message = $classes_by_name->{$msg_class}->{$msg_name}; + return unless defined $message; + # unbind previous request + $ivy->bindRegexp($res_regexp) if defined $res_regexp; + $res_regexp = "^(".$args->{id}.") ".$msg_name."_RES ".++$req_id; + foreach my $field (@{$message}) {$res_regexp.= " (\\S+)" unless $field->{name} eq "id"}; + print "res_regexp \'$res_regexp\'\n"; + $ivy->bindRegexp ($res_regexp, $callback); + my $req_msg = $args->{id}." ".$msg_name."_REQ ".$req_id; + print "req_msg \'$req_msg\'\n"; + $ivy->sendMsgs($req_msg); + return $req_id; +} + +sub bind_request_message { + my ($msg_class, $msg_name, $args, $ivy, $callback) = @_; + + + +} + +sub bind_message { + my ($msg_class, $msg_name, $args, $ivy, $callback) = @_; + my $regexp = get_regexp($msg_class, $msg_name, $args); + print ((defined $callback ? "binding":"removing binding")." on \'$regexp\'\n"); + $ivy->bindRegexp ($regexp, $callback); +} + +sub get_regexp { + my ($msg_class, $msg_name, $args) = @_; + warn "no such class : $msg_class" unless defined $classes_by_name->{$msg_class}; + my $message = $classes_by_name->{$msg_class}->{$msg_name}; + return "" if !defined $message; + my $nb_fields = @{$message}; + my $regexp = ""; + if( $msg_class eq "ground") { + $regexp = "^".$msg_class." ".$msg_name; + foreach (@{$message}) {$regexp.= " (\\S+)"}; + } + elsif ( $msg_class eq "aircraft_info") { + $regexp = "^(".$args->{id}.") ".$msg_name; + foreach my $field (@{$message}) {$regexp.= " (\\S+)" unless $field->{name} eq "id"}; + } + else { + $regexp = "^.*".$msg_name."\\s+"; + foreach (@{$message}) {$regexp.= " (\\S+)"}; + } + return $regexp; +} + +sub get_values_by_name { + my ($msg_class, $msg_name, $ivy_args) = @_; + my $values_by_name = {}; + my $message = $classes_by_name->{$msg_class}->{$msg_name}; + return {} unless defined $message; + for (my $i=0; $i<@{$message}; $i++) { + my $field = $message->[$i]; + $values_by_name->{$field->{name}} = $ivy_args->[$i+1]; + } + return $values_by_name; +} + +sub getMsg { + my ($msg_class, $msg_name, $args) = @_; + my $message = $classes_by_name->{$msg_class}->{$msg_name}; + my $str = ""; + if ( $msg_class eq "ground") { + $str .= $msg_class." ".$msg_name; + } + else { + $str .= $msg_name; + } + foreach my $field (@{$message}) { + $str.= " ".$args->{$field->{name}}; + } + return $str; +} + +sub sendMsg { + my ($ivy, $msg_class, $msg_name, $args) = @_; + $ivy->sendMsgs(getMsg($msg_class, $msg_name, $args)); +} + +sub read_protocol { + my ($filename, $classe_name) = @_; + my $parser = XML::DOM::Parser->new(); + my $doc = $parser->parsefile($filename); + my $protocol = $doc->getElementsByTagName('protocol')->[0]; + foreach my $class ($protocol->getElementsByTagName('class')) { + if ($class->getAttribute("name") eq $classe_name) { + my $messages_by_name = {}; + foreach my $message ($class->getElementsByTagName('message')) { + my $message_name = $message->getAttribute("name"); + my @msg_a = (); + if ($classe_name eq "aircraft_info") { push @msg_a, { name =>"id", type =>"string"}}; + foreach my $field ($message->getElementsByTagName('field')) { + my $field_name = $field->getAttribute("name"); + my $field_h = { name => $field->getAttribute("name"), + type => $field->getAttribute("type"), + format => $field->getAttribute("format"), + unit => $field->getAttribute("unit"), + values => $field->getAttribute("values"), + }; + push @msg_a, $field_h; + # print "$classe_name $message_name $field_name\n"; + } + $messages_by_name->{$message_name} = \@msg_a; + } + $classes_by_name->{$classe_name} = $messages_by_name; + } + } + # use Data::Dumper; + # print Dumper($messages_by_name); +} + + + +1; + diff --git a/sw/lib/perl/Paparazzi/Utils.pm b/sw/lib/perl/Paparazzi/Utils.pm new file mode 100644 index 00000000000..7c8e4d4f5dc --- /dev/null +++ b/sw/lib/perl/Paparazzi/Utils.pm @@ -0,0 +1,40 @@ +package Utils; + +use Data::Dumper; +use Math::Trig; + +sub trim { + my ($x, $min, $max) = @_; + return $min if ($x < $min); + return $max if ($x > $max); + return $x; +} + +sub diff_array { + my ($a, $b) = @_; +# print "diff_array [ @{$a} ] - [ @{$b} ] => "; + my @aonly; + my %seen; + @seen{@{$b}} = (); + foreach my $ac (@{$a}) { + push(@aonly, $ac) unless exists $seen{$ac}; + } +# print "[ @aonly ]\n"; + return @aonly; +} + +sub rad_of_deg { + return (shift @_) * Math::Trig::pip2() /90.; +} + +sub deg_of_rad { + return (shift @_) * 90. / Math::Trig::pip2(); +} + +sub min { + my ($a, $b) = @_; + return $a if ($a lt $b); + return $b; +} + +1; diff --git a/sw/logalizer/Makefile b/sw/logalizer/Makefile new file mode 100644 index 00000000000..c3423ec6a3c --- /dev/null +++ b/sw/logalizer/Makefile @@ -0,0 +1,32 @@ +# +# $Id$ +# Copyright (C) 2004 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + +OCAMLC = ocamlc -I ../lib/ocaml +OCAMLOPT = ocamlopt -I ../lib/ocaml + +all: play.opt + +clean: + rm -f *.opt *.out *~ core *.o *.bak .depend *.cm* + +play.opt : play.ml + $(OCAMLOPT) -o $@ unix.cmxa glibivy-ocaml.cmxa -I +lablgtk2 lablgtk.cmxa gtkInit.cmx $^ diff --git a/sw/logalizer/README b/sw/logalizer/README new file mode 100644 index 00000000000..914e57b072c --- /dev/null +++ b/sw/logalizer/README @@ -0,0 +1,27 @@ +# +# $Id$ +# Copyright (C) 2003 Pascal Brisset Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# + + +Tools for manipulating flight logs + + + diff --git a/sw/logalizer/play.ml b/sw/logalizer/play.ml new file mode 100644 index 00000000000..e324262d7f0 --- /dev/null +++ b/sw/logalizer/play.ml @@ -0,0 +1,117 @@ +let log = ref [||] + +let load_log = fun window (adj:GData.adjustment) name -> + let f = + if Filename.check_suffix name ".gz" + then Unix.open_process_in (Printf.sprintf "zcat %s" name) + else open_in name in + let lines = ref [] in + try + while true do + let l = input_line f in + try + Scanf.sscanf l "%f %[^\n]" (fun t m -> lines := (t,m):: !lines) + with + _ -> () + done + with + End_of_file -> + close_in f; + log := Array.of_list (List.rev !lines); + let start = fst !log.(0) in + let end_ = fst !log.(Array.length !log - 1) -. start in + adj#set_bounds ~upper:end_ (); + window#set_title (Filename.basename name) + + +let timer = ref None +let was_running = ref false + +let stop = fun () -> + match !timer with + None -> () + | Some t -> GMain.Timeout.remove t; timer := None + + +let file_dialog ~title ~callback () = + let sel = GWindow.file_selection ~title ~filename:"*.xml" ~modal:true () in + ignore (sel#cancel_button#connect#clicked ~callback:sel#destroy); + ignore + (sel#ok_button#connect#clicked + ~callback:(fun () -> + let name = sel#filename in + sel#destroy (); + callback name)); + sel#show () + +let open_log = fun window adj () -> + stop (); + ignore (file_dialog ~title:"Open Log" ~callback:(fun name -> load_log window adj name) ()) + +let index_of_time log t = + let t = t +. fst log.(0) in + let rec loop = fun a b -> + if a >= b then a else + let c = (a+b)/ 2 in + if t <= fst log.(c) then loop a c else loop (c+1) b in + loop 0 (Array.length log - 1) + +let rec run log adj i speed = + let (t, m) = log.(i) in + Ivy.send (Printf.sprintf "%.2f %s" t m); + adj#set_value (t -. fst log.(0)); + if i + 1 < Array.length log then + let dt = fst log.(i+1) -. t in + timer := Some (GMain.Timeout.add (truncate (1000. *. dt /. speed#value)) (fun () -> run log adj (i+1) speed; false)) + +let play adj speed = + stop (); + if Array.length !log > 1 then + run !log adj (index_of_time !log adj#value) speed + + + +let _ = + let window = GWindow.dialog ~title:"Paparazzi Replay" ~width:300 () in + let quit = fun () -> GMain.Main.quit (); exit 0 in + ignore (window#connect#destroy ~callback:quit); + + let adj = GData.adjustment + ~value:0. ~lower:0. ~upper:1000. + ~step_incr:0.5 ~page_incr:1.0 ~page_size:1.0 () in + + let speed = GData.adjustment ~value:1. ~lower:0.05 ~upper:10. + ~step_incr:0.25 ~page_incr:1.0 () in + + let bus = ref "127.255.255.255:2010" in + Arg.parse + [ "-b", Arg.String (fun x -> bus := x), "Bus\tDefault is 127.255.255.25:2010"] + (fun x -> load_log window adj x) + "Usage: "; + + + let menubar = GMenu.menu_bar ~packing:window#vbox#pack () in + let factory = new GMenu.factory menubar in + let accel_group = factory#accel_group in + let file_menu = factory#add_submenu "File" in + let file_menu_fact = new GMenu.factory file_menu ~accel_group in + + ignore (file_menu_fact#add_item "Open Log" ~key:GdkKeysyms._O ~callback:(open_log window adj)); + ignore (file_menu_fact#add_item "Play" ~key:GdkKeysyms._X ~callback:(fun () -> play adj speed)); + ignore (file_menu_fact#add_item "Stop" ~key:GdkKeysyms._S ~callback:(fun () -> stop ())); + ignore (file_menu_fact#add_item "Quit" ~key:GdkKeysyms._Q ~callback:quit); + + + let timescale = GRange.scale `HORIZONTAL ~adjustment:adj ~packing:window#vbox#pack () in + let speed_button = GEdit.spin_button ~adjustment:speed ~rate:0. ~digits:2 ~width:50 ~packing:window#vbox#add () in + + (** #move_slider is not working ??? **) ignore (timescale#event#connect#button_release ~callback:(fun _ -> if !was_running then play adj speed; false)); + ignore (timescale#event#connect#button_press ~callback:(fun _ -> was_running := !timer <> None; stop (); false)); + + window#add_accel_group accel_group; + window#show (); + + Ivy.init "Paparazzi replay" "READY" (fun _ _ -> ()); + Ivy.start !bus; + + GMain.Main.main () diff --git a/sw/logalizer/plot.pl b/sw/logalizer/plot.pl new file mode 100755 index 00000000000..104c9188904 --- /dev/null +++ b/sw/logalizer/plot.pl @@ -0,0 +1,342 @@ +#!/usr/bin/perl -w +use Getopt::Long; +use Tk; + +package Ploter; + +use Tk::LabEntry; +use Tk::FileSelect; +use Tk::DialogBox; +use XML::Parser; +use Expect; + +my $paparazzi_home; +BEGIN { + $paparazzi_home = "/home/drouin/work/savannah/paparazzi2"; + $paparazzi_home = $ENV{PAPARAZZI_HOME} if defined $ENV{PAPARAZZI_HOME}; +} +use lib ($paparazzi_home.'/sw/lib/perl'); + +#use ChildrenSpawner; +@ISA = qw(Subject); +use strict; +use warnings; +use diagnostics; + +use Subject; + + +my $log_date; +my $log_duration; +my $log_filename; + +my $time_range="[]"; +my $tr_entry; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-log => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, undef]); + $self->configspec(-log_start_date => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, undef]); + $self->configspec(-protocol => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, undef]); + $self->configspec(-listbox => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, undef]); + $self->configspec(-gnuplots => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, undef]); + # print("in Ploter::populate\n"); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; + $self->configure('-protocol' => undef); + $self->build_gui(); + # print("in Ploter::completeinit\n"); +} + +# +# XML +# +sub parse_messages_xml() { + my $filename = $paparazzi_home."/conf/messages.xml"; + my $p = new XML::Parser (Style => 'Tree') ; + my @msg_xml_tree = $p->parsefile ($filename) ; + print STDOUT "successfully parsed $filename\n"; + return \@msg_xml_tree; +} + +# +# Menus +# +sub build_menu_msg() { + my ($self, $xml_top_node, $menubar) = @_; + my $plot_menu = $menubar->cascade(-label => "~Data"); + while ( defined ( my $element = shift @{ $xml_top_node } )) { + my $child = shift @{ $xml_top_node }; + if ( ref $child ) { + my %attr = %{ shift @{ $child } }; + if ($element eq "protocol") { + print "found protocol\n"; + $self->build_msg_menu($child, $plot_menu); + } + } + } +} + +sub build_msg_menu() { + my ($self, $msg_node, $data_menu) = @_; +# sort @{$msg_node}; + while ( defined ( my $element = shift @{ $msg_node } )) { + my $child = shift @{ $msg_node }; + if ( ref $child ) { + my $attr = shift @{ $child }; + if ($element eq "message") { + my $id = $attr->{id}; +# print "found message $id\n"; + my $msg_menu = $data_menu->cascade(-label => $id); + $self->build_field_commands($child, $id, $msg_menu); + } + } + } +} + +sub build_field_commands() { + my ($self, $fields_node, $msg_name, $msg_menu) = @_; + my $no_field = 0; + while ( defined ( my $element = shift @{ $fields_node } )) { + my $child = shift @{ $fields_node }; + if ( ref $child ) { + my $attr = shift @{ $child } ; + if ($element eq "field") { + my $field_name = $attr->{id}; +# print "found field $field_name\n"; + my $no_field1 = $no_field; + my $file_menu = $msg_menu->command(-label => $field_name, + -command => sub { on_plot($self, $msg_name, $field_name, $no_field1 )}); + $no_field++; + } + } + } +} + +sub build_gui() { + my ($self) = @_; + my $width = 450; + my $height = 300; + my $mw = MainWindow->new; + $mw->geometry(sprintf("%dx%d", $width, $height)); + $mw->title("Paparazzi (gnu)plotter"); + + my $mb = $mw->Menu(); + my $log_menu = $mb->command(-label => "~Log", + -command => sub { on_load($self, $mw)}); + $self->build_menu_msg(@{$self->parse_messages_xml()}, $mb); + $mw->configure(-menu => $mb); + + my $padx = 10; + + my $filename_label = $self->add_label("filename :", \$log_filename, 0, $padx, $mw); + my $date_label = $self->add_label("date :", \$log_date, 1, $padx, $mw); + my $duration_label = $self->add_label("duration :", \$log_duration, 2, $padx, $mw); + + my $time_range_label = $mw->Label( -text => "time range")->pack(-side=>'left'); + $time_range_label->grid (-column=>0, -row=>3, -ipadx=>$padx); + $tr_entry = $mw->Entry(-width => 25); + $tr_entry->grid (-column=>1, -row=>3, -ipadx=>$padx); + $tr_entry->insert(0, $time_range); + + my $button = $mw->Button (-text => "update", + -command => sub { update_time_range($self)}, + ); + $button->grid (-column=>2, -row=>3, -ipadx=>$padx); + + my $listbox = $mw->Listbox(); + $listbox->grid (-column=>0, -columnspan => 3, -row=>4, -ipadx=>$padx); + $listbox->bind('', sub {$self->on_list_clicked($listbox, $mw)}); + $self->configure('-listbox' => $listbox); +} + +sub add_label() { + my ($self, $text, $text_variable, $row, $padx, $mw) = @_; + my $label1 = $mw->Label( -text => $text); + $label1->grid (-column=>0, -row=>$row, -ipadx=>$padx, -sticky => 'e' ); + my $label2 = $mw->Label( -textvariable => $text_variable); + $label2->grid (-column=>1, -row=>$row, -ipadx=>$padx, -sticky => 'w'); + return $label2; +} + +sub on_list_clicked() { + my ($self, $listbox, $mw) = @_; + my $key = $listbox->get('active'); + my $dialog = $mw->DialogBox( -title => "Plot command", + -buttons => [ "Replot", "Cancel" ], + ); + $dialog->add("Label", -text => "Plot command")->pack(); + my $gnuplots = $self->get('-gnuplots'); + my $gnuplot = $gnuplots->{$key}; + my $plot_cmd = $gnuplot->{'plot_cmd'}; + print "plot_cmd $plot_cmd\n"; + my $entry = $dialog->add("Entry", -width => 150)->pack(); + $entry->insert(0,$plot_cmd); + print "selected key $key\n"; + my $answer = $dialog->Show(); + print "selected $answer\n"; + if ($answer eq "Replot") { + my $new_plot_cmd = $entry->get(); + print("new_plot_cmd $new_plot_cmd \n"); + $gnuplot->{'plot_cmd'} = $new_plot_cmd; + my $exp = $gnuplot->{'exp'}; + print "exp $exp\n"; + $exp->send($new_plot_cmd."\n"); + my $timeout = 1; + $exp->expect($timeout); + } +} + +sub update_time_range() { + my ($self) = @_; + my $gnuplots = $self->get('-gnuplots'); + $time_range = $tr_entry->get(); + foreach my $key (keys %{$gnuplots}) { + print "update_range_for_key $key ($gnuplots->{$key})\n"; + my $gnuplot = $gnuplots->{$key}; + my $plot_cmd = $gnuplot->{'plot_cmd'}; + my $exp = $gnuplot->{'exp'}; + print "plot_cmd $time_range [$plot_cmd]\n"; + $plot_cmd =~ s/\[.*\]/$time_range/; + print "new_plot_cmd $plot_cmd\n\n"; + $gnuplot->{'plot_cmd'} = $plot_cmd; + $exp->send($plot_cmd."\n"); + my $timeout = 1; + $exp->expect($timeout); + } +} + +sub on_load() { + my ($self, $mw) = @_; + my $fs = $mw->FileSelect(-directory => $paparazzi_home."/var"); + my $file_name = $fs->Show(); + if (defined $file_name) { + print "file_name: $file_name\n"; + $self->load_log($file_name); + } +} + +sub on_plot() { + my ($self, $msg_name, $field_name, $field_pos) = @_; + # print "in on_plot msg_name $msg_name field_name $field_name field_pos $field_pos\n"; + + my $key = $msg_name.".".$field_name.".".$field_pos; + $self->gen_data_file($msg_name); + $self->add_plot($key); +} + + + +sub load_log() { + my ($self, $filename) = @_; + $log_filename = $filename; + my $nb_lines = 0; + open(INFILE, $filename) or die print STDERR "Cant open $filename: $!"; + my $log = $self->get('-log'); + $log_date = undef; + my $line; + while ($line = ) { # assigns each line in turn to $_ + if ($line =~ /(^\d+\.\d+) (\w+) (.+)/) { + $log_date = $1 unless defined $log_date; + my $rel_date = $1 - $log_date; + push (@{$log}, { date=>$rel_date, type=>$2, args=>$3}); + $nb_lines++; + } + } + close INFILE; + $self->configure('-log' => $log); + $self->configure( '-log_start_date' => $log_date); + $log_duration = "aaa"; + + + print STDERR "read $nb_lines lines\n" +} + + + +sub add_plot() { + my ($self, $data_key) = @_; + + $data_key =~ /([^\.]+).([^\.]+).([^\.]+)/ or return; + my ($msg_name, $field_name, $field_pos) = ($1, $2, $3); + + my $gnuplots = $self->get('-gnuplots'); + + my $exp = new Expect(); + $exp->raw_pty(1); + + my $rpos = $field_pos + 3; + my $nb_plots = scalar(keys(%{$gnuplots})); + my $h = $nb_plots * 240; + my $plot_cmd = "plot $time_range \"/tmp/plot_data.$msg_name\" using 1:$rpos t \"$field_name\" w l"; + my $pid = $exp->spawn("/usr/bin/gnuplot", ("-geometry","1600x200+0+$h" )); + my $gnuplot = { 'plot_cmd' => $plot_cmd, + 'exp' => $exp + }; + $gnuplots->{$data_key} = $gnuplot; + $self->configure('-gnuplots' => $gnuplots); + + $exp->send($plot_cmd."\n"); + my $timeout = 1; + $exp->expect($timeout); + + my $listbox = $self->get('-listbox'); + $listbox->insert('end', "$data_key"); + +} + +sub remove_plot() { + my ($self, $data_key) = @_; + my $gnuplots = $self->get('-gnuplots'); + my $gnuplot = $gnuplots->{$data_key}; + my $exp = $gnuplot->{'exp'}; + $exp->soft_close(); + $gnuplots->{$data_key} = undef; + $self->configure('-gnuplots' => $gnuplots); + my $listbox = $self->get('-listbox'); +# my $idx = $listbox->index($key); +# $listbox->delete($idx); +} + + +sub gen_data_file() { + my ($self, $msg_name) = @_; + my $nb_msgs = 0; + my $tmp_file = "/tmp/plot_data.$msg_name"; + open(OUTFILE, ">".$tmp_file) or die "Can t open $tmp_file: $!"; + foreach (@{$self->get('-log')}) { +# print "$_->{type} eq $msg_name \n"; + if ($_->{type} eq $msg_name) { + print OUTFILE "$_->{date} $_->{type} $_->{args}\n"; + $nb_msgs++; + } + } + close OUTFILE; + print STDERR "$nb_msgs $msg_name msgs\n"; +} + +sub catchSigTerm() { + my ($self) = @_; + printf("in catchSigTerm\n"); + my %gnuplots = $self->get('-gnuplots'); + foreach my $key (keys %gnuplots) { + print ("killing $key (%gnuplots{$key}->{'pid'})\n"); + $self->kill_gnuplot($key); + } +} + + + +$SIG{TERM} = \&catchSigTerm ; +#$SIG{KILL} = \&catchSigTerm ; +my $ploter = Ploter->new(); +#$ploter->load_log("../../var/log_04_07_01__13_06_33"); +Tk::MainLoop(); +$ploter->catchSigTerm(); +printf STDOUT "ploter over\n"; + +1; diff --git a/sw/simulator/Makefile b/sw/simulator/Makefile new file mode 100644 index 00000000000..d60c602640b --- /dev/null +++ b/sw/simulator/Makefile @@ -0,0 +1,123 @@ +# Paparazzi simulator $Id$ +# +# Copied from autopilot (autopilot.sf.net) thanx alot Trammell +# +# Copyright (C) 2003 Trammell Hudson +# Copyright (C) 2003 Pascal Brisset, Antoine Drouin +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, write to +# the Free Software Foundation, 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +include ../../conf/Makefile.local + +ACDIR= $(PAPARAZZI_HOME)/var/$(AIRCRAFT) +OBJDIR= $(ACDIR)/sim + +SIMHML = stdlib.ml types.ml data.ml flightModel.ml sirf.ml gps.ml hitl.ml sim.ml +SIMHCMO=$(SIMHML:%.ml=%.cmo) +SIMSML = stdlib.ml data.ml flightModel.ml gps.ml sitl.ml sim.ml +SIMSCMO=$(SIMSML:%.ml=%.cmo) +SIMSCMX=$(SIMSML:%.ml=%.cmx) +SIMSC = sim_ir.c sim_gps.c sim_ap.c estimator.c pid.c nav.c main.c +SIMSO=$(SIMSC:%.c=$(OBJDIR)/%.o) + +OCAMLC = ocamlc +OCAMLOPT=ocamlopt -p +INCLUDES= -I +lablgtk2 -I $(SIMDIR)/../lib/ocaml +OCAMLCC = gcc -O2 -I /usr/include/glib-2.0 -I /usr/lib/glib-2.0/include -DUBX -DCTL_BRD_V1_2 -I $(OBJDIR) -I $(ACDIR) + +FBW = ../airborne/fly_by_wire +AP = ../airborne/autopilot +VARINCLUDE=$(PAPARAZZI_HOME)/var/include +ACINCLUDE = $(PAPARAZZI_HOME)/var/$(AIRCRAFT) + +MESSAGES = ../../conf/messages.xml +GEN_DOWNLINK = ./gen_downlink.out + +SIMDIR=$(shell echo `pwd`) + + + +#all : simhitl.out sitl.cma $(GEN_DOWNLINK) +all : sitl.cma $(GEN_DOWNLINK) + +sim_sitl : $(OBJDIR)/simsitl.out + +simhitl.out : $(SIMHCMO) simhitl.cmo + $(OCAMLC) $(INCLUDES) -o $@ str.cma xml-light.cma unix.cma lib.cma lablgtk.cma gtkInit.cmo $^ + +sitl.cma : $(SIMSCMO) + ocamlc -o $@ -a $^ + +sitl.cmxa : $(SIMSCMX) + ocamlopt -o $@ -a $^ + +$(OBJDIR)/simsitl.out : sitl.cma $(SIMSO) $(OBJDIR)/simsitl.cmo + $(OCAMLC) $(INCLUDES) -custom -o $@ glibivy-ocaml.cma xml-light.cma unix.cma lib.cma lablgtk.cma gtkInit.cmo $(SIMSO) sitl.cma $(OBJDIR)/simsitl.cmo + +$(OBJDIR)/simsitl.opt : $(SIMSO) $(OBJDIR)/simsitl.cmx + $(OCAMLOPT) $(INCLUDES) -o $@ str.cmxa glibivy-ocaml.cmxa xml-light.cmxa unix.cmxa lib.cmxa lablgtk.cmxa gtkInit.cmx $(SIMSO) sitl.cmxa $(OBJDIR)/simsitl.cmx + +$(OBJDIR)/%.o : %.c + $(OCAMLCC) -c -o $@ -I $(SIMDIR) -I $(FBW) -I $(AP) -I ../include -I $(VARINCLUDE) $< + +$(OBJDIR)/%.o : $(AP)/%.c + $(OCAMLCC) -c -o $@ -I $(SIMDIR) -I $(FBW) -I $(AP) -I ../include -I $(VARINCLUDE) $< + +$(OBJDIR)/main.o : $(OBJDIR)/main.c + $(OCAMLCC) -c -o $@ -I $(SIMDIR) -I $(FBW) -I $(AP) -I ../include -I $(VARINCLUDE) $< + +sim_gps.o nav.o main.o sim_ir.o sim_ap.o pid.o estimator.o : $(ACINCLUDE)/flight_plan.h $(ACINCLUDE)/airframe.h + +$(OBJDIR)/main.c : $(OBJDIR)/downlink.h + cp $(AP)/main.c $(@) + +$(OBJDIR)/downlink.h : $(MESSAGES) $(GEN_DOWNLINK) + $(GEN_DOWNLINK) $< > $@ + +$(GEN_DOWNLINK) : gen_downlink.ml + $(OCAMLC) $(INCLUDES) -o $@ str.cma xml-light.cma lib.cma $< + + +$(OBJDIR)/simsitl.cmo : $(OBJDIR)/simsitl.ml + $(OCAMLC) $(INCLUDES) -c -o $@ $< + +$(OBJDIR)/simsitl.cmx : $(OBJDIR)/simsitl.ml + $(OCAMLOPT) $(INCLUDES) -c -o $@ $< + +$(OBJDIR)/simsitl.ml : simsitl.ml + echo "Sim.ac_name := \"$(AIRCRAFT)\"" > $@ + cat $< >> $@ + +%.cmo : %.ml + $(OCAMLC) $(INCLUDES) -c $< + +%.cmx : %.ml + $(OCAMLOPT) $(INCLUDES) -c $< + +%.cmi : %.mli + $(OCAMLC) $(INCLUDES) -c $< + +clean : + \rm -f *.cm* *~ *.out .depend *.o + +.depend: + ocamldep *.ml* > .depend + +ifneq ($(MAKECMDGOALS),clean) +-include .depend +endif diff --git a/sw/simulator/data.ml b/sw/simulator/data.ml new file mode 100644 index 00000000000..8e4de393e8a --- /dev/null +++ b/sw/simulator/data.ml @@ -0,0 +1,46 @@ +let (//) = Filename.concat + +(* let pprz_conf_path = Env.paparazzi_src // "conf" *) +let user_conf_path = Env.paparazzi_home // "conf" + +let conf_xml = Xml.parse_file (user_conf_path // "conf.xml") +let ground = ExtXml.child conf_xml "ground" + +let messages_ap = +(* let xml = Xml.parse_file (pprz_conf_path // "messages.xml") in *) + let xml = Xml.parse_file (user_conf_path // "messages.xml") in + try + ExtXml.child xml ~select:(fun x -> Xml.attrib x "name" = "telemetry_ap") "class" + with + Not_found -> failwith "'telemetry_ap' class missing in messages.xml" + +(* let ubx_xml = Xml.parse_file (pprz_conf_path // "ubx.xml") *) +let ubx_xml = Xml.parse_file (user_conf_path // "ubx.xml") + +type aircraft = { + name : string; + id : int; + airframe : Xml.xml; + flight_plan : Xml.xml; + radio: Xml.xml + } + + +let aircraft = fun name -> + let aircraft_xml, id = + let rec loop i = function + [] -> failwith ("Aicraft not found : "^name) + | x::_ when Xml.tag x = "aircraft" && Xml.attrib x "name" = name -> + (x, i) + | x::xs -> loop (i+1) xs in + loop 0 (Xml.children conf_xml) in + + let airframe_file = user_conf_path // ExtXml.attrib aircraft_xml "airframe" in + + { id = id; name = name; + airframe = Xml.parse_file airframe_file; + flight_plan = Xml.parse_file (user_conf_path // ExtXml.attrib aircraft_xml "flight_plan"); + radio = Xml.parse_file (user_conf_path // ExtXml.attrib aircraft_xml "radio") + } + +module type MISSION = sig val ac : aircraft end diff --git a/sw/simulator/events.ml b/sw/simulator/events.ml new file mode 100644 index 00000000000..dca6d0aba82 --- /dev/null +++ b/sw/simulator/events.ml @@ -0,0 +1,105 @@ +(* + * $Id$ + * + * High-level events handling + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +type callback = unit -> unit +type time = float (* Unix time *) +type period = float (* Seconds *) + +type fd = Unix.file_descr + +type timer = { + mutable next_wake : time; + period : period; + cb : callback + } + + +type on_input_id = Unix.file_descr +let dummy_on_input_id = Unix.stdin +let on_fds = Hashtbl.create 11 +let register_on_input fd cb = + Hashtbl.add on_fds fd cb; fd +let remove_on_input fd = + Hashtbl.remove on_fds fd + +type timer_id = callback +let timers = ref [] (* Is a priority queue really necessary ? *) +let register_timer period cb = + timers := { next_wake = Unix.gettimeofday () +. period; period = period; cb = cb } :: !timers; cb +let remove_timer cb = + let rec loop = function + [] -> [] + | t::ts -> if t.cb == cb then ts else t :: loop ts in + timers := loop !timers + + +let get_input_fds () = + let l = ref [] in + Hashtbl.iter (fun fd _ -> l := fd :: !l) on_fds; + !l +let get_fd_callbacks fds = + List.map (Hashtbl.find on_fds) fds + +(** Returns next timer timeout and callback. May return a dummy callback + if no timers are set. Wrap the update of the selected timer in the + callback *) +let never = 2e9 +let get_next_timeout () = + let rec loop earlier_wake earlier_cb = function + [] -> (earlier_wake, earlier_cb) + | timer :: es -> + if timer.next_wake < earlier_wake then + let t = timer.next_wake in + loop t (fun () -> timer.next_wake <- t +. timer.period; timer.cb ()) es + else + loop earlier_wake earlier_cb es in + loop never (fun () -> ()) !timers + + + + + + + + +let mainloop () = + while true do + let (next_timeout, timeout_cb) = get_next_timeout () in + let timeout = next_timeout -. Unix.gettimeofday () in + if timeout <= 0. then + timeout_cb () + else + let input_fds = get_input_fds () in + let (ready_inputs, _, _) = Unix.select input_fds [] [] timeout in + + match ready_inputs with + [] -> timeout_cb () + | _ -> + let fd_callbacks = get_fd_callbacks ready_inputs in + List.iter (fun cb -> cb ()) fd_callbacks + done + + diff --git a/sw/simulator/flightModel.ml b/sw/simulator/flightModel.ml new file mode 100644 index 00000000000..cf07c2a85b5 --- /dev/null +++ b/sw/simulator/flightModel.ml @@ -0,0 +1,157 @@ +open Stdlib + +type meter = float +type meter_s = float +type radian = float +type radian_s = float +type state = { + start : float; + mutable t : float; + mutable x : meter; + mutable y : meter; + mutable z : meter; + mutable psi : radian; (* Trigonometric *) + mutable phi : radian; + mutable phi_dot : radian_s; + mutable delta_a : float; + mutable thrust : float; + mutable air_speed : meter_s + } + +let init route = { + start = Unix.gettimeofday (); t = 0.; x = 0.; y = 0. ; z = 0.; + psi = route; phi = 0.; phi_dot = 0.; + delta_a = 0.; thrust = 0.; air_speed = 0. +} + +let get_xyz state = (state.x, state.y, state.z) +let get_time state = state.t +let get_phi state = state.phi + +let set_air_speed state x = state.air_speed <- x + +let drag = 0.45 +let c_lp = -10. +let g = 9.81 +let weight = 1. *. 1.4 + +let max_phi = 0.7 (* rad *) +let bound = fun x mi ma -> if x > ma then ma else if x < mi then mi else x + + + +module Make(A:Data.MISSION) = struct + open Data +(* Minimum complexity *) +(* + http://controls.ae.gatech.edu/papers/johnson_dasc_01.pdf + http://controls.ae.gatech.edu/papers/johnson_mst_01.pdf + *) + + let state_update = fun state (wx, wy) -> + let now = Unix.gettimeofday () -. state.start in + let dt = now -. state.t in + if state.air_speed > 0. then begin + let phi_dot_dot = state.delta_a +. c_lp *. state.phi_dot /. state.air_speed in + state.phi_dot <- state.phi_dot +. phi_dot_dot *. dt; + state.phi <- bound (state.phi +. state.phi_dot *. dt) (-.max_phi) max_phi; + let psi_dot = -. g /. state.air_speed *. tan state.phi in + state.psi <- norm_angle (state.psi +. psi_dot *. dt); + let dx = state.air_speed *. cos state.psi *. dt +. wx *. dt + and dy = state.air_speed *. sin state.psi *. dt +. wy *. dt in + state.x <- state.x +.dx ; + state.y <- state.y +. dy; + let gamma = (state.thrust -. drag) /. weight in + let dz = sin gamma *. state.air_speed *. dt in + state.z <- state.z +. dz + end; + state.t <- now + + + let servos = + try + ExtXml.child A.ac.airframe "servos" + with + Not_found -> + failwith (Printf.sprintf "Child 'servos' expected in '%s'\n" (Xml.to_string A.ac.airframe)) + + let misc_section = + try + ExtXml.child A.ac.airframe ~select:(fun x -> ExtXml.attrib x "name" = "MISC") "section" + with + Not_found -> + failwith (Printf.sprintf "Child 'section' with 'name=MISC' expected in '%s'\n" (Xml.to_string A.ac.airframe)) + + let nominal_airspeed = + try + float_of_string (Xml.attrib (ExtXml.child misc_section ~select:(fun x -> ExtXml.attrib x "name" = "NOMINAL_AIRSPEED") "define") "value") + with + Not_found -> + failwith (Printf.sprintf "Child 'define' with 'name=NOMINAL_AIRSPEED' expected in '%s'\n" (Xml.to_string misc_section)) + + let get_servo name = + try + ExtXml.child servos ~select:(fun x -> ExtXml.attrib x "name" = name) "servo" + with + Not_found -> + failwith (Printf.sprintf "Child 'servo' with name='%s' expected in '%s'\n" name (Xml.to_string servos)) + + let us_attrib = fun x a -> int_of_string (ExtXml.attrib x a) + + let gaz = get_servo "GAZ" + let min_thrust = us_attrib gaz "min" + let max_thrust = us_attrib gaz "max" + + type servo_id = int + type ppm = int + + let no_thrust = int_of_string (ExtXml.attrib gaz "no") + + + + let some_aileron_left = try Some (get_servo "AILERON_LEFT") with _ -> None + let some_ailevon_left = try Some (get_servo "AILEVON_LEFT") with _ -> None + let some_ailevon_right = try Some (get_servo "AILEVON_RIGHT") with _ -> None + + let float_attrib = fun x a -> float_of_string (ExtXml.attrib x a) + let int_attrib = fun x a -> int_of_string (ExtXml.attrib x a) + + let sign = fun x -> + if float_attrib x "min" < float_attrib x "max" then 1 else -1 + + let do_thrust = fun state servo -> + state.thrust <- (float (servo.(no_thrust) - min_thrust) /. float (max_thrust - min_thrust)) + + let do_servos = + match some_aileron_left, some_ailevon_left, some_ailevon_right with + Some aileron_left, None, None -> + let c_lda = 16. *. 9e-5 in (* phi_dot_dot from aileron *) + + let sign_aileron_left = sign aileron_left + and n_delta_a = us_attrib aileron_left "neutral" + and no_aileron_left = int_attrib aileron_left "no" in + fun state servo -> + (** Printf.printf "left=%d\n" (servo.(no_aileron_left) - n_delta_a); flush stdout; **) + state.delta_a <- c_lda *. float (- sign_aileron_left * (servo.(no_aileron_left) - n_delta_a)); + do_thrust state servo + | None, Some ailevon_left, Some ailevon_right -> + let c_lda = 2.5e-4 in (* phi_dot_dot from aileron *) + + let sign_ailevon_left = sign ailevon_left + and sign_ailevon_right = sign ailevon_right + and left_neutral = us_attrib ailevon_left "neutral" + and right_neutral = us_attrib ailevon_right "neutral" + and left_travel = float (us_attrib ailevon_left "max" - us_attrib ailevon_left "min") /. 1200. + and right_travel = float (us_attrib ailevon_right "max" - us_attrib ailevon_right "min") /. 1200. + and no_ailevon_left = int_attrib ailevon_left "no" + and no_ailevon_right = int_attrib ailevon_right "no" in + fun state servo -> + do_thrust state servo; + let sum = (float (servo.(no_ailevon_left) - left_neutral) /. left_travel +. + float (servo.(no_ailevon_right) - right_neutral) /. right_travel) /. 2. in +(* Printf.printf "%d %f\n" (servo no_ailevon_left - left_neutral) sum; flush stdout; *) + state.delta_a <- c_lda *. (-. sum) + | _ -> failwith "Aileron or Ailevon left and right PLEASE" + + let nb_servos = 10 (* 4017 *) +end diff --git a/sw/simulator/flightModel.mli b/sw/simulator/flightModel.mli new file mode 100644 index 00000000000..ef32dc9e6bc --- /dev/null +++ b/sw/simulator/flightModel.mli @@ -0,0 +1,22 @@ +type meter = float +type meter_s = float +type radian = float +type radian_s = float +type state + +val init : radian -> state + +val get_xyz : state -> meter * meter * meter +val get_time : state -> float +val get_phi : state -> radian + +val set_air_speed : state -> meter_s -> unit + +module Make : + functor (A : Data.MISSION) -> + sig + val do_servos : state -> Stdlib.us array -> unit + val nb_servos : int + val nominal_airspeed : float (* m/s *) + val state_update : state -> float * float -> unit + end diff --git a/sw/simulator/gen_downlink.ml b/sw/simulator/gen_downlink.ml new file mode 100644 index 00000000000..796e3246103 --- /dev/null +++ b/sw/simulator/gen_downlink.ml @@ -0,0 +1,92 @@ +open Printf + +let h_name = "DOWNLINK_H" + +let id_of = fun xml -> ExtXml.attrib xml "name" + +(** No dereferencement for arrays *) +let deref = fun xml -> try let _ = Xml.attrib xml "len" in "" with _ -> "*" + +let print_params = function + [] -> () + | f::fields -> + printf "%s" (id_of f); + List.iter (fun f -> printf ", %s" (id_of f)) fields + +let types = [ + "uint8", "%hhu"; + "uint16", "%hu"; + "uint32", "%u" ; + "int8", "%hhd"; + "int16", "%hd"; + "int32", "%d" ; + "float", "%f" +] + +let sprint_format = fun f -> + try + Xml.attrib f "format" + with _ -> + List.assoc (Xml.attrib f "type") types + + +let freq = 10 +let buffer_length = 5 +let step = 1. /. float freq +let nb_steps = (256 / freq) * freq + +let is_periodic = fun m -> try let _ = Xml.attrib m "period" in true with _ -> false +let period_of = fun m -> float_of_string (Xml.attrib m "period") + +let gen_periodic = fun avr_h messages -> + let periodic_messages = List.filter is_periodic messages in + + let scheduled_messages = + List.map + (fun m -> + let p = period_of m in + let period_steps = truncate (p /. step) in + (period_steps, id_of m)) + periodic_messages in + + fprintf avr_h "#define PeriodicSend() { /* %dHz */ \\\n" freq; + fprintf avr_h " static uint8_t i;\\\n"; + fprintf avr_h " i++; if (i == %d) i = 0;\\\n" nb_steps; + List.iter + (fun (p, id) -> + fprintf avr_h " if (i %% %d == 0) PERIODIC_SEND_%s();\\\n" p id) + scheduled_messages; + fprintf avr_h "}\n" + + + +let fprint_formats = fun c fields -> + List.iter (fun f -> fprintf c " %s" (sprint_format f)) fields + +let fprint_args = fun c fields -> + List.iter (fun f -> fprintf c ", %s(%s)" (deref f) (id_of f)) fields + +let one_message = fun m -> + let id = id_of m + and fields = Xml.children m in + printf "#define DOWNLINK_SEND_%s(" id; + print_params fields; + printf "){ \\\n"; + printf " IvySendMsg(\"%%d %s %a\",ac_id%a); \\\n" id fprint_formats fields fprint_args fields; + printf "}\n\n" + +let _ = + let xml = Xml2h.start_and_begin Sys.argv.(1) h_name in + let xml = ExtXml.child xml ~select:(fun x -> Xml.attrib x "name"="telemetry_ap") "class" in + let messages = (Xml.children xml) in + + printf "#include \n"; + printf "extern uint8_t ac_id;\n"; + printf "extern uint8_t modem_nb_ovrn;\n"; + + List.iter one_message messages; + + gen_periodic stdout messages; + + Xml2h.finish h_name + diff --git a/sw/simulator/gps.ml b/sw/simulator/gps.ml new file mode 100644 index 00000000000..1764c1cda0a --- /dev/null +++ b/sw/simulator/gps.ml @@ -0,0 +1,48 @@ +open Stdlib +open Latlong + +type state = { + wgs84 : Latlong.geographic; + alt : float; + time : float; + climb : float; + gspeed : float; + course : float + } + + +let earth_radius = 6378388. + + +let state = fun lat0 lon0 alt0 -> + let last_x = ref 0. and last_y = ref 0. + and last_t = ref 0. and last_z = ref 0. in + + fun (x, y, z) t -> + let dx = x -. !last_x + and dy = y -. !last_y + and dt = t -. !last_t in + let gspeed = sqrt (dx*.dx +. dy*.dy) /. dt + and course = norm_angle (pi/.2. -. atan2 dy dx) + and climb = (z -. !last_z) /. dt in + + let lat = lat0 +. y /. earth_radius + and long = lon0 +. x /.earth_radius /. cos lat0 + and alt = alt0 +. z in + + last_x := x; + last_y := y; + last_z := z; + last_t := t; + + let course = if course < 0. then course +. 2. *. pi else course in (* ???? *) + + { + wgs84 = { posn_lat=lat;posn_long=long }; + alt = alt0 +. z; + time = t; + climb = climb; + gspeed = gspeed; + course = course + } + diff --git a/sw/simulator/gui.ml b/sw/simulator/gui.ml new file mode 100644 index 00000000000..4b51574c0b4 --- /dev/null +++ b/sw/simulator/gui.ml @@ -0,0 +1,49 @@ +(* ocamlc -w a -I +lablgtk2 lablgtk.cma lablgnomecanvas.cma gtkInit.cmo gui.ml -o gui.out *) + +let main () = + + (* window *) + let window = GWindow.window ~title: "Paparazzi simulator" + ~border_width: 5 ~width: 400 ~height: 200 () in + ignore (window#connect#destroy ~callback:GMain.quit); + + + let hb = GPack.hbox ~border_width:5 ~spacing:5 ~packing:window#add () in + + + (* wind *) + let frame_w = GBin.frame ~label:"Wind" ~shadow_type:`IN ~packing:hb#pack () in + + let vb_w = GPack.vbox ~packing:frame_w#add () in + + let adj_d = + GData.adjustment ~lower:0. ~upper:100. ~step_incr:1. ~page_incr:10. () in + let sc_d = GRange.scale `HORIZONTAL ~adjustment:adj_d ~draw_value:false + ~packing:vb_w#pack () in + + let adj_s = + GData.adjustment ~lower:0. ~upper:100. ~step_incr:1. ~page_incr:10. () in + let sc_s = GRange.scale `HORIZONTAL ~adjustment:adj_s ~draw_value:false + ~packing:vb_w#pack () in + + (* infrared *) + let frame_i = GBin.frame ~label:"Infrared" ~shadow_type:`IN ~packing:hb#pack () in + + let vb_i = GPack.vbox ~packing:frame_i#add () in + + let adj_i = + GData.adjustment ~lower:0. ~upper:100. ~step_incr:1. ~page_incr:10. () in + let sc_i = GRange.scale `HORIZONTAL ~adjustment:adj_i ~draw_value:false + ~packing:vb_i#pack () in + + let button = GButton.button ~use_mnemonic:true ~label:"_Coucou" ~packing:(hb#pack ~padding:5) () in + ignore(button#connect#clicked ~callback: + (fun () -> prerr_endline "Coucou")); + + + + window#show (); + GMain.Main.main () + +let _ = main () + diff --git a/sw/simulator/hitl.ml b/sw/simulator/hitl.ml new file mode 100644 index 00000000000..76e6cac2cf0 --- /dev/null +++ b/sw/simulator/hitl.ml @@ -0,0 +1,142 @@ +(* + * $Id$ + * + * Hardware In The Loop + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + + +open Stdlib +open Latlong + +let get_port = fun n -> + Xml.attrib (ExtXml.child ~select:(fun x -> Xml.attrib x "name"=n) Data.ground "link") "port" + +let tty0 = ref (get_port "ap") +let tty1 = ref (get_port "fbw") + +let uart_mcu0 = ref Unix.stdout +let uart_mcu1 = ref Unix.stdin + +let open_mcu tty = Serial.opendev tty Serial.B38400 + +module Make(A:Data.MISSION) = struct + + let init = fun (_:int) (_:GPack.box) -> + if !tty0 <> "" then uart_mcu0 := open_mcu !tty0; + if !tty1 <> "" then uart_mcu1 := open_mcu !tty1 + + let boot = fun () -> () + + + let scale = fun value s -> truncate (value *. s) + + open Gps + + let gps = fun gps -> + let uart = Unix.out_channel_of_descr !uart_mcu0 in + let utm = utm_of WGS84 gps.wgs84 in + Ubx.send uart Ubx.nav_posutm + ["EAST", scale utm.utm_x 1e2; + "NORTH", scale utm.utm_y 1e2; + "ALT", scale gps.alt 1e2]; + Ubx.send uart Ubx.nav_status ["GPSfix", 3]; + Ubx.send uart Ubx.nav_velned + ["ITOW",scale gps.time 1e3; + "VEL_D", -scale gps.climb 1e2; + "GSpeed", scale gps.gspeed 1e2; + "Heading", scale (deg gps.course) 1e5] + + + + let irs = + try + ExtXml.child A.ac.Data.airframe + ~select:(fun x -> try Xml.attrib x "prefix" = "IR_" with Xml.No_attribute _ -> false) + "section" + with Not_found -> + failwith "Do not find an IR section in airframe description" + let ir_roll_neutral = + try + float_of_string (ExtXml.attrib (ExtXml.child irs ~select:(fun x -> try Xml.attrib x "name" = "ROLL_NEUTRAL_DEFAULT" with Xml.No_attribute _ -> false) "define") "value") + with + Not_found -> + failwith "Do not find an ROLL_NEUTRAL_DEFAULT define in IR description" + + let ir_pitch_neutral = + try + float_of_string (ExtXml.attrib (ExtXml.child irs ~select:(fun x -> try Xml.attrib x "name" = "PITCH_NEUTRAL_DEFAULT" with Xml.No_attribute _ -> false) "define") "value") + with + Not_found -> + failwith "Do not find an PITCH_NEUTRAL_DEFAULT define in IR description" + + let infrared = fun phi ctrst -> + let uart = Unix.out_channel_of_descr !uart_mcu0 in + let ir_left = truncate (phi *. ctrst +. ir_roll_neutral) + and ir_front = truncate ir_pitch_neutral in + Ubx.send uart Ubx.usr_irsim + ["ROLL", ir_left; + "PITCH", ir_front] + + let size_servos_buf = 256 + let zero = '\000' + + let get_2bytes = fun buf i -> + (Char.code buf.[i] lsl 8) lor (Char.code buf.[i+1]) + +(* nb_servos 2 bytes values, prefixed by 00 ended by \n + Returns optionaly a function associating the read value to the index *) + let clock = 16 + let read_servos = fun servos -> + let servos_buf = String.create size_servos_buf + and buf_idx = ref 0 in + let nb_servos = Array.length servos in + let tty = Unix.in_channel_of_descr !uart_mcu1 in + + fun () -> + let n = input tty servos_buf !buf_idx (size_servos_buf- !buf_idx) in + let rec parse00 = fun i m -> + if m >= 2+2*nb_servos+1 then + (if servos_buf.[i] = zero then parse0 else parse00) (i+1) (m-1) + else (* Not enough chars : wait *) + i + + and parse0 = fun i m -> + if servos_buf.[i] = zero && servos_buf.[i+2*nb_servos+1] = '\n' then begin + for s = 0 to nb_servos - 1 do + servos.(s) <- get_2bytes servos_buf (i+1+2*s) / clock + done; + i+1+2*nb_servos+1 + end else (* 0 or \n missing *) + parse00 i m in + + let nb_available_chars = (!buf_idx + n) in + let nb_read_chars = parse00 0 nb_available_chars in + let rest = nb_available_chars - nb_read_chars in + String.blit servos_buf nb_read_chars servos_buf 0 rest; + buf_idx := rest + + + + let servos = fun servos -> + ignore (GMain.Io.add_watch [`IN] (fun _ -> read_servos servos (); true) (GMain.Io.channel_of_descr !uart_mcu1)) +end diff --git a/sw/simulator/hitl.mli b/sw/simulator/hitl.mli new file mode 100644 index 00000000000..5ac1b8d776b --- /dev/null +++ b/sw/simulator/hitl.mli @@ -0,0 +1,5 @@ +val tty0 : string ref +val tty1 : string ref + + +module Make : functor (A : Data.MISSION) -> Sim.AIRCRAFT diff --git a/sw/simulator/sim.ml b/sw/simulator/sim.ml new file mode 100644 index 00000000000..c4276eead53 --- /dev/null +++ b/sw/simulator/sim.ml @@ -0,0 +1,188 @@ +(* + * $Id$ + * + * Hardware in the loop basic simulator (handling GPS, infrared and servos) + * + * Copyright (C) 2004 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Stdlib +open Geometry_2d + +let float_attrib xml a = float_of_string (ExtXml.attrib xml a) + +let wind = (0., 0.) (* m/s in local ref *) + +(* Frequencies for perdiodic tasks are expressed in periods of 100Hz *) +let timebase = 10 (* ms *) +let ir_period = 5 +let fm_period = 4 + + +module type AIRCRAFT = + sig + val init : int -> GPack.box -> unit + val boot : unit -> unit + val servos : us array -> unit + (** Called once at init *) + + val infrared : float -> float -> unit + (** [infrared phi] Called on timer *) + + val gps : Gps.state -> unit + (** [gps state] Called on timer *) + end + + +module type AIRCRAFT_ITL = functor (A : Data.MISSION) -> AIRCRAFT + + +let ac_name = ref "" + + +let common_options = [] + +module Make(AircraftItl : AIRCRAFT_ITL) = struct + + module A = struct + let ac = Data.aircraft !ac_name + end + + module Aircraft = AircraftItl(A) + + module FM = FlightModel.Make(A) + + let flight_plan = A.ac.Data.flight_plan + + let lat0 = (float_attrib flight_plan "lat0") + let lon0 = (float_attrib flight_plan "lon0") + let qfu = (float_attrib flight_plan "qfu") + let alt0 = (float_attrib flight_plan "ground_alt") +(* + let gust_dir = 0 and + let gust_speed = 0 + + let wind = fun -> + let wind_dir = wind_dir_adj#value in + let wind_speed = wind_speed_adj#value in + let gust_max = gust_max_adj#value in + let gust_dir_fact = gust_dir_fact_adj#value in + let gust_speed_fact = gust_dir_fact_adj#value in + gust_speed = trim 0, gust_max (gust_speed + Random.float gust_dir_fact) + gust_dir = gust_dirspeed + Random.float gu) + +*) + let main () = + let window = GWindow.dialog ~title:("Aircraft "^ !ac_name) () in + let quit = fun () -> GMain.Main.quit (); exit 0 in + ignore (window#connect#destroy ~callback:quit); + + Aircraft.init A.ac.Data.id window#vbox; + + let gps_period = 25 in + + let compute_gps_state = Gps.state (rad_of_deg lat0) (rad_of_deg lon0) (alt0) in + + + let initial_state = FlightModel.init (pi/.2. -. qfu/.180.*.pi) in + + let state = ref initial_state in + + let reset = fun () -> state := initial_state in + + let servos = Array.create FM.nb_servos 0 in + + Aircraft.servos servos; + + let north_label = GMisc.label ~text:"000" () + and east_label = GMisc.label ~text:"000" () + and alt_label = GMisc.label ~text:"000" () in + let wind_dir_adj = GData.adjustment ~value:0. ~lower:(0.) ~upper:370. ~step_incr:1.0 () in + let wind_speed_adj = GData.adjustment ~value:0. ~lower:(0.) ~upper:20. ~step_incr:0.1 () in + let gust_norm_max_adj = GData.adjustment ~value:0. ~lower:(0.) ~upper:20. ~step_incr:0.1 () in + let gust_norm_ch_fact_adj = GData.adjustment ~value:0. ~lower:(0.) ~upper:20. ~step_incr:0.1 () in + let gust_dir_ch_fact_adj = GData.adjustment ~value:0. ~lower:(0.) ~upper:20. ~step_incr:0.1 () in + let infrared_contrast_adj = GData.adjustment ~value:0. ~lower:(0.) ~upper:1010. ~step_incr:10. () in + + let run = ref false in + let scheduler = + let t = ref 0 in + let f = + fun () -> + incr t; + if !t mod fm_period = 0 then begin + FM.do_servos !state servos; + let wind_dir_rad = deg2rad wind_dir_adj#value in + let wind_angle_rad = heading_of_to_angle_rad wind_dir_rad in + let wind_speed_polar = {r2D = wind_speed_adj#value; theta2D = oposite_heading_rad wind_angle_rad} in + let wind_speed_cart = polar2cart wind_speed_polar in + FM.state_update !state ( wind_speed_cart.x2D, wind_speed_cart.y2D) + end; + if !t mod ir_period = 0 then + Aircraft.infrared (FlightModel.get_phi !state) infrared_contrast_adj#value; + if !t mod gps_period = 0 then begin + let (x,y,z) = FlightModel.get_xyz !state in + east_label#set_text (Printf.sprintf "%.0f" x); + north_label#set_text (Printf.sprintf "%.0f" y); + alt_label#set_text (Printf.sprintf "%.0f" z); + Aircraft.gps (compute_gps_state (x,y,z) (FlightModel.get_time !state)) + end; + true in + fun () -> ignore (GMain.Timeout.add 10 f) in + + let boot = fun () -> + Aircraft.boot (); + scheduler () in + + let take_off = fun () -> prerr_endline "takeoff"; FlightModel.set_air_speed !state FM.nominal_airspeed in + + let hbox = GPack.hbox ~packing:window#vbox#pack () in + let s = GButton.button ~label:"Boot" ~packing:(hbox#pack ~padding:5) () in + ignore (s#connect#clicked ~callback:boot); + let t = GButton.button ~label:"Launch" ~packing:hbox#pack () in + ignore (t#connect#clicked ~callback:take_off); + + let hbox = GPack.hbox ~packing:window#vbox#pack () in + let l = fun s -> ignore(GMisc.label ~text:s ~packing:hbox#pack ()) in + l "East:"; hbox#pack east_label#coerce; + l " North:"; hbox#pack north_label#coerce; + l " Height:"; hbox#pack alt_label#coerce; + + let hbox = GPack.hbox ~packing:window#vbox#pack () in + ignore (GMisc.label ~text:"wind dir:" ~packing:hbox#pack ()); + ignore (GRange.scale `HORIZONTAL ~adjustment:wind_dir_adj ~packing:hbox#add ()); + + let hbox = GPack.hbox ~packing:window#vbox#pack () in + ignore (GMisc.label ~text:"wind speed:" ~packing:hbox#pack ()); + ignore (GRange.scale `HORIZONTAL ~adjustment:wind_speed_adj ~packing:hbox#add ()); + + let hbox = GPack.hbox ~packing:window#vbox#pack () in + ignore (GMisc.label ~text:"gust max speed:" ~packing:hbox#pack ()); + ignore (GRange.scale `HORIZONTAL ~adjustment:gust_norm_max_adj ~packing:hbox#add ()); + + + let hbox = GPack.hbox ~packing:window#vbox#pack () in + ignore (GMisc.label ~text:"infrared:" ~packing:hbox#pack ()); + ignore (GRange.scale `HORIZONTAL ~adjustment:infrared_contrast_adj ~packing:hbox#add ()); + + window#show (); + Unix.handle_unix_error GMain.Main.main () +end diff --git a/sw/simulator/sim.mli b/sw/simulator/sim.mli new file mode 100644 index 00000000000..8d323cdec5c --- /dev/null +++ b/sw/simulator/sim.mli @@ -0,0 +1,24 @@ +(** Options for HITL and SITL simulators *) +val common_options : (string * Arg.spec * string) list + +val ac_name : string ref + +(** A complete aircraft with it mission *) +module type AIRCRAFT = + sig + val init : int -> GPack.box -> unit + val boot : unit -> unit + val servos : Stdlib.us array -> unit + val infrared : float -> float -> unit + val gps : Gps.state -> unit + end + +(** A simulated aircraft, without its conf *) +module type AIRCRAFT_ITL = functor (A : Data.MISSION) -> AIRCRAFT + +(** Functor to build the simulator *) +module Make : + functor (AircraftItl : AIRCRAFT_ITL) -> + sig + val main : unit -> unit + end diff --git a/sw/simulator/sim_ap.c b/sw/simulator/sim_ap.c new file mode 100644 index 00000000000..e7a175088db --- /dev/null +++ b/sw/simulator/sim_ap.c @@ -0,0 +1,93 @@ +/* Definitions and declarations required to compile autopilot code on a + i386 architecture. Binding for OCaml. */ + +#include +#include +#include +#include "std.h" +#include "link_autopilot.h" +#include "autopilot.h" +#include "estimator.h" + +#include +#include + +uint8_t ir_estim_mode; +uint8_t vertical_mode; +uint8_t inflight_calib_mode; +bool_t rc_event_1, rc_event_2; +bool_t launch; +bool_t link_fbw_receive_valid; +uint8_t gps_nb_ovrn, modem_nb_ovrn, link_fbw_fbw_nb_err, link_fbw_nb_err; + +uint8_t ac_id; + +struct inter_mcu_msg from_fbw, to_fbw; + +static int16_t values_from_ap[RADIO_CTL_NB]; + +void inflight_calib(void) { } + +void link_fbw_send(void) { + int i; + for(i = 0; i < RADIO_CTL_NB; i++) + values_from_ap[i] = to_fbw.channels[i] / CLOCK; +} + +value sim_periodic_task(value unit) { + periodic_task(); + return Val_unit; +} + + +value sim_rc_task(value unit) { + from_fbw.status = (1 << STATUS_RADIO_OK) | (1 << AVERAGED_CHANNELS_SENT); + link_fbw_receive_valid = TRUE; + radio_control_task(); + return Val_unit; +} + + +float ftimeofday(void) { + struct timeval t; + struct timezone z; + gettimeofday(&t, &z); + return (t.tv_sec + t.tv_usec/1e6); +} + +value sim_init(value id) { + pprz_mode = PPRZ_MODE_MANUAL; + estimator_init(); + ac_id = Int_val(id); + return Val_unit; +} + +value update_bat(value bat) { + from_fbw.vsupply = Int_val(bat); + return Val_unit; +} + +value update_rc_channel(value c, value v) { + from_fbw.channels[Int_val(c)] = Double_val(v)*MAX_PPRZ; + return Val_unit; +} + +// Defined in servo.c +#define _4017_NB_CHANNELS 10 +#define SERVO_NEUTRAL_(i) SERVOS_NEUTRALS_ ## i +#define SERVO_NEUTRAL(i) (SERVO_NEUTRAL_(i)) +#define SERVO_MIN (SERVO_MIN_US) +#define SERVO_MAX (SERVO_MAX_US) +#define ChopServo(x) ((x) < SERVO_MIN ? SERVO_MIN : ((x) > SERVO_MAX ? SERVO_MAX : (x))) + +value set_servos(value servos) { + int i; + + uint16_t servo_widths[_4017_NB_CHANNELS]; + ServoSet(values_from_ap); + + for(i=0; i < _4017_NB_CHANNELS; i++) + Store_field(servos, i, Val_int(servo_widths[i])); + + return Val_int(servo_widths[SERVO_GAZ]); +} diff --git a/sw/simulator/sim_gps.c b/sw/simulator/sim_gps.c new file mode 100644 index 00000000000..61d1bcbf7e8 --- /dev/null +++ b/sw/simulator/sim_gps.c @@ -0,0 +1,42 @@ +/* OCaml binding to link the simulator to autopilot functions. */ + +#include +#include "airframe.h" +#include "flight_plan.h" +#include "autopilot.h" + +#include + +uint8_t gps_mode; +float gps_ftow; /* ms */ +float gps_falt; /* m */ +float gps_fspeed; /* m/s */ +float gps_fclimb; /* m/s */ +float gps_fcourse; /* rad */ +int32_t gps_utm_east, gps_utm_north; +float gps_east, gps_north; /* m */ + +const int32_t utm_east0 = NAV_UTM_EAST0; +const int32_t utm_north0 = NAV_UTM_NORTH0; + +value sim_use_gps_pos(value x, value y, value c, value a, value s, value cl, value t) { + gps_mode = 3; + gps_utm_east = Int_val(x); + gps_utm_north = Int_val(y); + gps_fcourse = Double_val(c); + gps_falt = Double_val(a); + gps_fspeed = Double_val(s); + gps_fclimb = Double_val(cl); + gps_ftow = Double_val(t); + + gps_east = gps_utm_east / 100 - NAV_UTM_EAST0; + gps_north = gps_utm_north / 100 - NAV_UTM_NORTH0; + + use_gps_pos(); /* From main.c */ + return Val_unit; +} + +/* Second binding required because number of args > 5 */ +value sim_use_gps_pos_bytecode(value *a, int argn) { + return sim_use_gps_pos(a[0],a[1],a[2],a[3],a[4],a[5],a[6]); +} diff --git a/sw/simulator/sim_ir.c b/sw/simulator/sim_ir.c new file mode 100644 index 00000000000..4c97e742516 --- /dev/null +++ b/sw/simulator/sim_ir.c @@ -0,0 +1,24 @@ +/* Infrared soft simulation. OCaml binding. */ + +#include +#include "airframe.h" + +#include + +int16_t ir_roll; +int16_t ir_pitch; + +int16_t ir_contrast = IR_DEFAULT_CONTRAST; +int16_t ir_roll_neutral = IR_ROLL_NEUTRAL_DEFAULT; +int16_t ir_pitch_neutral = IR_PITCH_NEUTRAL_DEFAULT; +float ir_rad_of_ir = IR_RAD_OF_IR_CONTRAST / IR_DEFAULT_CONTRAST; + +void ir_update(void) { +} +void ir_gain_calib(void) { +} + +value set_ir_roll(value roll) { + ir_roll = Int_val(roll); + return Val_unit; +} diff --git a/sw/simulator/simhitl.ml b/sw/simulator/simhitl.ml new file mode 100644 index 00000000000..e50d0538826 --- /dev/null +++ b/sw/simulator/simhitl.ml @@ -0,0 +1,14 @@ +open Stdlib + +let _ = + Arg.parse + (Sim.common_options@[set_string "-aircraft" Sim.ac_name "aircraft name"; + set_string "-fbw" Hitl.tty1 "Fly by wire MCU port"; + set_string "-ap" Hitl.tty0 "Autopilot MCU port"]) + (fun x -> Printf.fprintf stderr "Warning: Don't do anythig with %s\n" x) + "Usage: "; + +module M = Sim.Make(Hitl.Make) + +let _ = + M.main () diff --git a/sw/simulator/simsitl.ml b/sw/simulator/simsitl.ml new file mode 100644 index 00000000000..69afabd146b --- /dev/null +++ b/sw/simulator/simsitl.ml @@ -0,0 +1,12 @@ + + +let _ = + Arg.parse (Sim.common_options@Sitl.options) + (fun x -> Printf.fprintf stderr "Warning: Don't do anythig with %s\n" x) + "Usage: " + + +module M = Sim.Make(Sitl.Make) + +let _ = + M.main () diff --git a/sw/simulator/simsitl.pl b/sw/simulator/simsitl.pl new file mode 100755 index 00000000000..81b0aef25b4 --- /dev/null +++ b/sw/simulator/simsitl.pl @@ -0,0 +1,31 @@ +#!/usr/bin/perl -w + +use Getopt::Long; +my @paparazzi_lib; +BEGIN { + @paparazzi_lib = (defined $ENV{PAPARAZZI_SRC}) ? + ($ENV{PAPARAZZI_SRC}."/sw/lib/perl", $ENV{PAPARAZZI_SRC}."/sw/ground_segment/cockpit"):(); +} +use lib (@paparazzi_lib); + +use strict; +use Paparazzi::Environment; + +my $options = {}; +GetOptions ( + "b=s" => \$options->{ivy_bus}, + "a=s" => \$options->{aircraft}, + ); +my @args = (); +push @args, "-b", $options->{ivy_bus}; +my $sim_binary = Paparazzi::Environment::paparazzi_home()."/var/".$options->{aircraft}."/sim/simsitl.out"; +die "$sim_binary not found. try make AIRCRAFT=$options->{aircraft} ac\n" unless -e $sim_binary; +exec ($sim_binary, @args) + + + + + + + + diff --git a/sw/simulator/sirf.ml b/sw/simulator/sirf.ml new file mode 100644 index 00000000000..89c6a5aaac9 --- /dev/null +++ b/sw/simulator/sirf.ml @@ -0,0 +1,102 @@ +open Types + + exception BadChecksum + exception BadEndSequence + + let rec skip_until_start_sequence = fun gps -> + while input_byte gps <> 0xA0 do () done; + if input_byte gps <> 0xA2 then skip_until_start_sequence gps + + let send_start_sequence = fun gps -> + output_byte gps 0xA0; + output_byte gps 0xA2 + + let get_end_sequence = fun gps -> + if input_byte gps <> 0xB0 || input_byte gps <> 0xB3 then + raise BadEndSequence + + let send_end_sequence = fun gps -> + output_byte gps 0xB0; + output_byte gps 0xB3 + + let checksum = fun data -> + let cs = ref 0 in + String.iter (fun c -> cs := (!cs + Char.code c) land 0x7fff) data; + !cs + + let receive = fun gps -> + let gps = Unix.in_channel_of_descr gps in + skip_until_start_sequence gps; + let length_h = input_byte gps in + let length_l = input_byte gps in + let length = (length_h lsl 8) lor length_l in + let payload = String.create length in + for i = 0 to length - 1 do + payload.[i] <- input_char gps; + done; + let checksum_h = input_byte gps in + let checksum_l = input_byte gps in + get_end_sequence gps; + if checksum payload <> (checksum_h lsl 8) lor checksum_l then + raise BadChecksum; + payload + + let output_2bytes = fun gps x -> + output_byte gps ((x land 0xff00) lsr 8); + output_byte gps (x land 0xff) + + let send = fun gps payload -> + let n = String.length payload in + assert(n < 1023); + send_start_sequence gps; + output_2bytes gps n; + String.iter (output_char gps) payload; + output_2bytes gps (checksum payload); + send_end_sequence gps; + flush gps + + +let send1 = fun gps c -> + Printf.printf "send 0x%2x\n" c; flush stdout + +let send_int32 = fun gps x -> + Printf.printf "send32 0x%8x\n" x; flush stdout + + +let get = fun gps n -> + let buf = String.create n in + buf.[0] <- Char.chr 0x79; + assert(input gps buf 5 (n-5) = n-5); + buf + +let log_info = Bytes [ "MID", 1; + "S_First", 1; "S_Last", 1; + "A_First", 4; "A_Last", 4; "A_Start", 4; + "Size", 4 ] +let log_data = Bytes [ "MID", 1; "Start", 4; "Data", 256*2 ] + +let extended_nav = Bytes [ + "MID", 1; + "Latitude", 4; "Longitude", 4; "Altitude", 4; + "Speed", 4; "ClimbRate", 4; "Course", 4; + "Mode", 1; + "Year", 2; "Month", 1; "Day", 1; "Hour", 1; "Minute", 1; + "Second", 2; + "GDOP", 1; "HDOP", 1; "PDOP", 1; "TDOP", 1; "VDOP", 1;] + + +let get_message = fun gps expected -> + let s = size_of_message expected in + let data = get gps s in + (expected, data, 0) + + +let log_poll_info = fun gps -> + send1 gps 0xbb; + get_message gps log_info + +let log_read = fun gps a -> + send1 gps 0xb8; + send_int32 gps a; + get_message gps log_data + diff --git a/sw/simulator/sitl.ml b/sw/simulator/sitl.ml new file mode 100644 index 00000000000..8697921a6b8 --- /dev/null +++ b/sw/simulator/sitl.ml @@ -0,0 +1,116 @@ +(* Software in the loop *) + +open Printf + +let ivy_bus = ref "127.255.255.255:2010" + +module Make(A:Data.MISSION) = struct + + let servos_period = 25 (* ms *) + let periodic_period = 16 (* ms *) + let rc_period = 25 (* ms *) + let id_period = 10_000 (* ms *) + + let periodic = fun p f -> + ignore (GMain.Timeout.add p (fun () -> f (); true)) + + + let msg = fun name -> + ExtXml.child Data.messages_ap ~select:(fun x -> ExtXml.attrib x "name" = name) "message" + let gps_msg = msg "GPS" + + +(* Servos handling (rservos is the intermediate storage) *) + let rc_channels = Array.of_list (Xml.children A.ac.Data.radio) + let nb_channels = Array.length rc_channels + let rc_channel_no = fun x -> + List.assoc x (Array.to_list (Array.mapi (fun i c -> Xml.attrib c "function", i) rc_channels)) + + let rservos = ref [||] + let adj_bat = GData.adjustment ~value:12.5 ~lower:0. ~upper:23. ~step_incr:0.1 () + + external set_servos : Stdlib.us array -> int = "set_servos" +(** Returns gaz servo value (us) *) + + let energy = ref 0. + let update_servos = fun () -> + let gaz = set_servos !rservos in + (* 100% = 1W *) + energy := !energy +. float (gaz-1000) /. 1000. *. float servos_period /. 1000. + + let update_adj_bat = fun () -> + let b = adj_bat#value in + adj_bat#set_value (b -. !energy *. 0.00259259259259259252); (* To be improved !!! *) + energy := 0. + + +(* Radio command handling *) + external update_channel : int -> float -> unit = "update_rc_channel" + + let inverted = ["ROLL"; "PITCH"; "YAW"; "GAIN1"; "GAIN2"] + + let rc = fun () -> + let name = Xml.attrib A.ac.Data.radio "name" ^ " " ^ A.ac.Data.name in + let window = GWindow.window ~title:name ~border_width:0 ~width:400 ~height:400 () in + let quit = fun () -> GMain.Main.quit (); exit 0 in + ignore (window#connect#destroy ~callback:quit); + let vbox = GPack.vbox ~packing:window#add () in + Array.iteri + (fun i c -> + let adj = GData.adjustment ~value:0. ~lower:(-100.) ~upper:110. ~step_incr:1.0 () in + let hbox = GPack.hbox ~packing:vbox#add () in + let f = (ExtXml.attrib c "function") in + let l = GMisc.label ~width:75 ~text:f ~packing:hbox#pack () in + let inv = List.mem f inverted in + let _scale = GRange.scale `HORIZONTAL ~inverted:inv ~adjustment:adj ~packing:hbox#add () in + let update = fun () -> update_channel i (adj#value /. 100.) in + + ignore (adj#connect#value_changed update); + update ()) + rc_channels; + + window#show () + + external periodic_task : unit -> unit = "sim_periodic_task" + external rc_task : unit -> unit = "sim_rc_task" + external sim_init : int -> unit = "sim_init" + external update_bat : int -> unit = "update_bat" + + let init = fun id vbox -> + Ivy.init (sprintf "Paparazzi sim %d" id) "READY" (fun _ _ -> ()); + Ivy.start !ivy_bus; + rc (); + sim_init id; + + let hbox = GPack.hbox ~packing:vbox#add () in + let l = GMisc.label ~text:"Bat:" ~packing:hbox#pack () in + let _scale = GRange.scale `HORIZONTAL ~adjustment:adj_bat ~packing:hbox#add () in + let update = fun () -> update_bat (truncate (adj_bat#value *. 10.)) in + ignore (adj_bat#connect#value_changed update); + update () + + let boot = fun () -> + periodic servos_period update_servos; + periodic periodic_period periodic_task; + periodic rc_period rc_task; + periodic 10000 update_adj_bat + + +(* Functions called by the simulator *) + let servos = fun s -> rservos := s + + external set_ir_roll : int -> unit = "set_ir_roll" + let infrared = fun phi ctrst -> + set_ir_roll (truncate (phi *. ctrst)) + + external use_gps_pos: int -> int -> float -> float -> float -> float -> float -> unit = "sim_use_gps_pos_bytecode" "sim_use_gps_pos" + open Latlong + let gps = fun gps -> + let utm = utm_of WGS84 gps.Gps.wgs84 in + let cm = fun f -> truncate (f *. 100.) in + use_gps_pos (cm utm.utm_x) (cm utm.utm_y) gps.Gps.course gps.Gps.alt gps.Gps.gspeed gps.Gps.climb gps.Gps.time + +end +let options = + [ "-b", Arg.String (fun x -> ivy_bus := x), "Bus\tDefault is 127.255.255.25:2010"] + diff --git a/sw/simulator/sitl.mli b/sw/simulator/sitl.mli new file mode 100644 index 00000000000..30fe45954b3 --- /dev/null +++ b/sw/simulator/sitl.mli @@ -0,0 +1,6 @@ +(* Software In The Loop *) + +module Make : functor (A : Data.MISSION) -> Sim.AIRCRAFT +val options : (string * Arg.spec * string) list +(** Arg options specific to Sitl *) + diff --git a/sw/simulator/stdlib.ml b/sw/simulator/stdlib.ml new file mode 100644 index 00000000000..8b0f65bbf42 --- /dev/null +++ b/sw/simulator/stdlib.ml @@ -0,0 +1,16 @@ +type us = int + +let pi = 4. *. atan 1. +let rec norm_angle = fun x -> + if x > pi then norm_angle (x-.2.*.pi) + else if x < -.pi then norm_angle (x+.2.*.pi) + else x + +let deg = fun rad -> rad /. pi *. 180. + +let rad_of_deg = fun x -> x /. 180. *. pi + +let set_float = fun option var name -> + (option, Arg.Set_float var, Printf.sprintf "%s (%f)" name !var) +let set_string = fun option var name -> + (option, Arg.Set_string var, Printf.sprintf "%s (%s)" name !var) diff --git a/sw/simulator/timer.h b/sw/simulator/timer.h new file mode 100644 index 00000000000..fddeabaa720 --- /dev/null +++ b/sw/simulator/timer.h @@ -0,0 +1 @@ +#define bit_is_set(x, b) ((x >> b) & 0x1) diff --git a/sw/simulator/types.ml b/sw/simulator/types.ml new file mode 100644 index 00000000000..b0a2564ca95 --- /dev/null +++ b/sw/simulator/types.ml @@ -0,0 +1,116 @@ +type size = int +type label = string +type data_layout = + Bits of (label * size) list + | Bytes of (label * size) list + + + +type record = data_layout * string * int +let get_record = fun record_layout string offset -> + (record_layout, string, offset) + + +(* Little endian *) +let make_int_from_bytes = fun data pos size -> + if size < 4 then + let rec mk = fun pos s i -> + if s = 0 then i else mk (pos+1) (s-1) ((i lsl 8) lor Char.code data.[pos]) in + mk pos size 0 + else if size = 4 then begin + let c = fun i -> Int32.shift_left (Int32.of_int (Char.code data.[pos+i])) (8*(3-i)) in + let lor32 = Int32.logor in + Int32.to_int (lor32 (c 0) (lor32 (c 1) (lor32 (c 2) (c 3)))) + + end else invalid_arg "make_int_from_bytes" + + +(* Little endian *) +let make_int_from_bits = fun data offset pos size -> + assert(pos < 8); + assert(size < 31); + let nb_bits_in_first_byte = min (8-pos) size in + let i = ((Char.code data.[offset] lsl pos) land 0xff) lsr (8-nb_bits_in_first_byte) in + let rec mk = fun offset s i -> + if s = 0 + then i + else if s < 8 + then (i lsl s) lor (Char.code data.[offset] lsr (8 - s)) + else mk (offset+1) (s-8) ((i lsl 8) lor Char.code data.[offset]) in + mk (offset+1) (size-nb_bits_in_first_byte) i + +let assoc = fun label layout -> + let rec assoc pos = function + [] -> failwith ("get_int: unknown field "^label) + | (l, s)::lss -> + if l = label + then (pos, s) + else assoc (pos + s) lss in + assoc 0 layout + + +let get_int = fun signed label (record_layout, data, offset) -> + match record_layout with + Bits l -> + let (pos, size) = assoc label l in + let i = + if pos mod 8 = 0 && size mod 8 = 0 then + let pos = pos / 8 and size = size / 8 in + make_int_from_bytes data (offset+pos) size + else + make_int_from_bits data (offset+pos/8) (pos mod 8) size in + if signed then (i lsl (31-size)) asr (31-size) else i + | Bytes l -> + let (pos, size) = assoc label l in + let i = make_int_from_bytes data (offset+pos) size in + if size < 4 then + if signed then (i lsl (31-4*size)) asr (31-4*size) else i + else begin + assert(not signed); + i + end + +let get_int32 = get_int true +let get_u32 = get_int false +let get_uint = get_int false +let get_int = get_int true + +let get_raw = fun label (record_layout, data, offset) -> + match record_layout with + Bytes layout -> + let (pos, size) = assoc label layout in + String.sub data (offset+pos) size + | _ -> failwith "get_raw" + + + +let sum_sizes = List.fold_left (fun a (_, s) -> a+s) 0 +let size_of_message = function (* In bytes *) + Bytes l -> sum_sizes l + | Bits l -> sum_sizes l / 8 + +let make_payload = fun layout values -> + match layout with + (Bytes layout) -> + let p = String.create (sum_sizes layout) in + List.iter + (fun (label, value) -> + let (pos, size) = assoc label layout in + let byte = fun x -> Char.chr (x land 0xff) in + match size with + 1 -> p.[pos] <- byte value + | 2 -> + p.[pos] <- byte (value asr 8); + p.[pos+1] <- byte value + | 4 -> + p.[pos] <- byte (value asr 24); + p.[pos+1] <- byte (value lsr 16); + p.[pos+2] <- byte (value lsr 8); + p.[pos+3] <- byte value + | _ -> failwith "make_payload: unknown int size" + ) + values; + p + | _ -> failwith "make_payload" + + diff --git a/sw/supervision/Paparazzi/CpGui.pm b/sw/supervision/Paparazzi/CpGui.pm new file mode 100755 index 00000000000..c445d5e3885 --- /dev/null +++ b/sw/supervision/Paparazzi/CpGui.pm @@ -0,0 +1,219 @@ +package Paparazzi::CpGui; + +use Subject; +use Paparazzi::CpSessionMgr; +@ISA = qw(Paparazzi::CpSessionMgr); + +use strict; + +use Tk; +use Tk::MainWindow; +use Tk::NoteBook; +use Tk::HList; +use Tk::ItemStyle; +use Data::Dumper; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-logo_file => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, undef], + -variables => [S_SUPER, S_SUPER, S_SUPER, S_SUPER, S_SUPER, undef], + ); +} +sub completeinit { + my ($self) = @_; + $self->SUPER::completeinit(); + $self->build_gui(); +} + +sub onProgramSelected { + my ($self, $pgm_name) = @_; + $self->toggle_program("NONE", $pgm_name, []); +} + +sub onSessionSelected { + my ($self, $session_name) = @_; + $self->start_session($session_name); +} + +use constant LIST_WIDTH => 80; +use constant LIST_HEIGHT => 20; + +sub build_gui { + my ($self) = @_; + my $mw = MainWindow->new(); + $mw->title ($self->{cp_name}); + # menu bar + my $menubar = + $mw->Frame( -relief => 'ridge')->pack(-side => 'top', -fill => 'x'); + my $session_menu = $menubar->Menubutton(-text => 'Sessions')->pack(-side => 'left');; + my $sessions = $self->get('-sessions'); + foreach my $session_name (keys %{$sessions}) { + $session_menu->command( -label => $session_name, + -command => [\&onSessionSelected, $self, $session_name] ); + } + my $program_menu = $menubar->Menubutton(-text => 'Programs')->pack(-side => 'left');; + my $programs = $self->get(-programs); + foreach my $pgm_name (keys %{$programs}) { + $program_menu->command( -label => $pgm_name, + -command => [\&onProgramSelected, $self, $pgm_name] + ); + } + # session frame + my $session_frame = $mw->Frame( -relief => 'groove')->pack(-side => 'bottom', -fill => 'both', -expand => "yes",); + my $notebook = $session_frame->NoteBook( -ipadx => 6, -ipady => 6); + $notebook->pack(-expand => "yes", + -fill => "both", + -padx => 5, -pady => 5, + -side => "top"); + $self->build_logo_page($notebook); + $self->build_list_page($notebook, "hosts", "Hosts", ["name", "ip", "status"], \&build_hosts_page); + $self->build_list_page($notebook, "variables", "Variables", ["name", "value"], \&build_variables_page); +# $self->build_list_page($notebook, "programs", "Programs", ["name", "command", "args"], \&build_programs_page); + $self->build_programs_page($notebook); + $self->build_list_page($notebook, "sessions", "Sessions", ["name", "command", "args"], \&build_sessions_page); +# $self->build_programs_page($notebook); +# my $programs_page = $notebook->add("programs", -label => "Programs", -underline => 0); +# my $sessions_page = $notebook->add("sessions", -label => "Sessions", -underline => 0); + + $self->{session_frame} = $session_frame; +} + + +sub build_logo_page { + my ($self, $notebook) = @_; + my $logo_filename = $self->get('-logo_file'); + return unless defined $logo_filename; + my $logo_page = $notebook->add("logo", -label => "Logo", -underline => 0); + my $image = $logo_page->Photo('logogif', + -format => 'GIF', + -file => $logo_filename); + my $labelImage = $logo_page->Label('-image' => 'logogif')->pack(); + return $logo_page; +} + + +sub build_hosts_page { + my ($self, $hlist, $e, $section_h, $item) = @_; + $hlist->itemCreate ($e, 0, + -itemtype => 'text', + -text => $item + ); + my $ip = $section_h->{$item}; + $hlist->itemCreate ($e, 1, + -itemtype => 'text', + -text => $ip?$ip:"unknown" + ); + $hlist->itemCreate ($e, 2, + -itemtype => 'text', + -text => "unknown" + ); +} + +sub build_variables_page { + my ($self, $hlist, $e, $section_h, $item) = @_; + $hlist->itemCreate ($e, 0, -itemtype => 'text', + -text => $item + ); + $hlist->itemCreate ($e, 1, -itemtype => 'text', + -text => $section_h->{$item}, + ); +} + +#sub build_programs_page { +# my ($self, $hlist, $e, $section_h, $item) = @_; +# $hlist->itemCreate ($e, 0, -itemtype => 'text', +# -text => $item +# ); +# $hlist->itemCreate ($e, 1, -itemtype => 'text', +# -text => $section_h->{$item}->{command}, +# ); + +# $hlist->itemCreate ($e, 2, -itemtype => 'text', +# -text => $section_h->{$item}->{args}, +# ); +#} + +sub build_sessions_page { + my ($self, $hlist, $e, $section_h, $item) = @_; + $hlist->itemCreate ($e, 0, -itemtype => 'text', + -text => $item + ); + $hlist->itemCreate ($e, 1, -itemtype => 'text', + -text => $section_h->{$item}->{command}, + ); + + $hlist->itemCreate ($e, 2, -itemtype => 'text', + -text => $section_h->{$item}->{args}, + ); +} + + +sub build_programs_page { + my ($self, $notebook) = @_; + my $page = $notebook->add("programs", -label => "Programs", -underline => 0); + my @header = ("name", "command", "args"); + my $hlist = $page->Scrolled ('HList', +# -selectmode => 'extended', + -header => 1, +# -columns => $#header + 1, + -width => LIST_WIDTH, + -height => LIST_HEIGHT, + -itemtype => 'imagetext', + -indent => 35, + -separator => '/', + )->grid(-sticky => 'nsew'); +# for my $i (0 .. $#header) { +# $hlist->header('create', $i, -text => $header[$i]); +# } + my $section_h = $self->get('-programs'); + foreach my $program (keys %{$section_h}) { +# print Dumper($section_h->{$program})."\n"; + $hlist->add($program, -text => $program ); + + $hlist->add($program."/command", -text => "command : ".$section_h->{$program}->{command}); + $hlist->add($program."/args", -text => "args :"); + my $args = $section_h->{$program}->{args}; + foreach my $argh (@{$args}) { + $hlist->add($program."/args/".$argh->{flag}, -text => $argh->{flag}."\t". $argh->{type}."\t". $argh->{value}); + } + } + return $page +} + + +sub build_list_page { + my ($self, $notebook, $section, $label, $header, $row_fun) = @_; + my $page = $notebook->add($section, -label => $label, -underline => 0); + my @header = @{$header}; + my $hlist = $page->Scrolled ('HList', + -header => 1, + -columns => $#header + 1, + -width => LIST_WIDTH, + -height => LIST_HEIGHT, + )->grid(-sticky => 'nsew'); + for my $i (0 .. $#header) { +# print("header $header[$i]\n"); + $hlist->header('create', $i, -text => $header[$i]); + } + my $section_h = $self->get('-'.$section); + print "CpGui variables ".Dumper($section_h) if ($section eq "variables"); + foreach my $item (keys %{$section_h}) { + my $e = $hlist->addchild(""); + &$row_fun($self, $hlist, $e, $section_h, $item); + # print("$hlist, $e, $section_h, $item\n"); + } + return $page +} + +1; + + + + + + + + + + diff --git a/sw/supervision/Paparazzi/CpPgmMgr.pm b/sw/supervision/Paparazzi/CpPgmMgr.pm new file mode 100644 index 00000000000..48b9a1d5094 --- /dev/null +++ b/sw/supervision/Paparazzi/CpPgmMgr.pm @@ -0,0 +1,79 @@ +package Paparazzi::CpPgmMgr; + +use Subject; +@ISA = ("Subject"); + +use strict; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-children => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, {}], + ); +} + +sub completeinit { + my $self = shift; + $self->SUPER::completeinit; +} + +sub start_program() { + my ($self, $pgm, @options, @args, $keep_stdin) = @_; + my %children = %{$self->get('-children')}; + +# print("in ChildrenSpawner::start_programm args [$pgm @args]\n"); + my $pid = undef; + my $sleep_count = 0; + my $fh; + do { + $pid = fork(); + $SIG{PIPE} = sub { die "whoops, $pgm pipe broke" }; + unless (defined $pid) { + warn "cannot fork: $!"; + die "bailing out" if $sleep_count++ > 6; + sleep 1; + } + } until defined $pid; + + if (! $pid) { # child + $SIG{TERM} = 'IGNORE'; + exec ($pgm, @options, @args);# or die "couldnt exec foo: $pgm @args"; + # NOTREACHED + exit(1); + } + # parent + $children{$pid} = {cmd => $pgm, args => \@args};#, ktw => $fh}; + $self->configure('-children', \%children); + foreach my $key (keys %children) { +# print("in ChildrenSpawner::start_programm child: [$key $children{$key}]\n"); + } + return $pid; +} + +sub stop_program() { + my ($self, $pid) = @_; +# print "in_stop_program $pid\n"; + + my %children = %{$self->get('-children')}; + my $pgm = $children{$pid}; + + if (defined $pgm) { +# printf STDOUT "Killing Process %d [%s %s]\n", $pid, $pgm->{cmd}, $pgm->{args}; + kill 9, $pid; + $children{$pid} = undef; + $self->configure('-children', \%children); + } +} + + +sub terminate_all() { + my ($self) = @_; +# print("in ChildrenSpawner::terminate_all\n"); + my %pgms = %{$self->get('-children')}; + foreach my $pid (keys %pgms) { +# print "killing $pid ($pgms{$pid})\n"; + $self->stop_program($pid); + } +} + +1; diff --git a/sw/supervision/Paparazzi/CpSessionMgr.pm b/sw/supervision/Paparazzi/CpSessionMgr.pm new file mode 100644 index 00000000000..0579e90c6b1 --- /dev/null +++ b/sw/supervision/Paparazzi/CpSessionMgr.pm @@ -0,0 +1,192 @@ +package Paparazzi::CpSessionMgr; + +use Data::Dumper; +use XML::DOM; +use Subject; + +use Paparazzi::CpPgmMgr; +@ISA = qw(Paparazzi::CpPgmMgr); + +use strict; + +sub populate { + my ($self, $args) = @_; + $self->SUPER::populate($args); + $self->configspec(-config_file => [S_NEEDINIT, S_PASSIVE, S_RDONLY, S_OVRWRT, S_NOPRPG, undef], + -bin_base_dir => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, "/usr/bin"], + -log_dir => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, "/var/tmp"], + -variables => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, {}], + -hosts => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, {}], + -programs => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, {}], + -sessions => [S_NOINIT, S_PASSIVE, S_RDWR, S_OVRWRT, S_NOPRPG, {}], + ); +} + +sub completeinit { + my ($self) = @_; + $self->SUPER::completeinit(); + my $cfg_file = $self->get('-config_file'); +# my $variables = $self->get('-variables'); +# print "initial variables\n".Dumper($variables); + $self->read_cfg($cfg_file); +# $variables = $self->get('-variables'); +# print "configured variables\n".Dumper($variables); +} + +sub prepare_args { + my ($self, $args) = @_; + my (@options, @rargs); + my $variables = $self->get('-variables'); + print "CpSessionMgr : variables ".Dumper($variables); + foreach my $opt (@{$args}) { + my $type = $opt->{type}; + my $flag = $opt->{flag}; + my $value = $type eq 'var' ? $variables->{$opt->{value}}: $opt->{value}; + if ($flag) { + if ($flag =~ /\.*=/) { push @options, $flag.$value} + else {push @options, $flag, $value} + } + else { push @rargs, $value} + } + return (@options, @rargs); +} + +sub toggle_program { + my ($self, $session_name, $pgm_name, $pgm_session_args, $session_idx) = @_; +# shift @_; +# print Dumper(@_); + my $programs = $self->get('-programs'); +# print "Progams ".Dumper($programs); + my $program = $programs->{$pgm_name}; +# print "Toggling Progam ".Dumper($program); + my $command; + if ($program->{command} =~ /^\/.*/) { + $command = $program->{command}; + } + else { + $command = $self->get('-bin_base_dir')."/".$program->{command}; + } + if ($session_name eq "NONE") { + if (defined $program->{pid}) { + $self->SUPER::stop_program($program->{pid}); + $program->{pid} = undef; + } + else { + my (@options, @args) = $self->prepare_args($program->{args}); +# print Dumper($program->{args}); + print "starting $pgm_name [$command @options, @args]\n"; + $program->{pid} = $self->SUPER::start_program($command, @options[0..$#options], @args[0..$#args]); + } + } + else { + my @pgm_args = $self->prepare_args($program->{args}); +# print "program->{args} ".Dumper($program->{args}); +# print "pgm_args ".Dumper(@pgm_args); +# print "session ".Dumper($self->{sessions}->{$session_name}); + + my $session_pgms = $self->get('-sessions')->{$session_name}->{pgms}; +# print "session_pgms ".Dumper($session_pgms); + my $_session_args = ($session_pgms->[$session_idx])->{args}; + my @session_args = defined $_session_args ? $self->prepare_args($_session_args) : []; +# print "session_args ".Dumper($_session_args); + + push @pgm_args , @session_args; + print "session $session_name starting program $pgm_name\n[$command @pgm_args]\n"; + $self->{sessions}->{$session_name}->{pgms}[$session_idx]->{pid} = $self->SUPER::start_program($command, @pgm_args[0..$#pgm_args]); + } +} + +sub start_session { + my ($self, $session_name) = @_; +# print "starting session $session_name\n"; + my $sessions = $self->get('-sessions'); + my $session = $sessions->{$session_name}; + my @progs = @{$session->{pgms}}; +# print "progs ".Dumper(@progs); + my $session_idx = 0; + foreach my $pgm (@progs) { + my $pgm_name = $pgm->{name}; + my $pgm_session_args = $pgm->{args}; + $self->toggle_program($session_name, $pgm_name, $pgm_session_args, $session_idx) if (!defined $self->{programs}->{$pgm_name}->{pid}); + $session_idx++; + } +} + +sub xml_parse_args { + my ($args) = @_; + my @args_a; + foreach my $arg (@{$args}){ + my $var = $arg->getAttribute('variable'); + my $args_h = { + flag => $arg->getAttribute('flag'), + type => $var eq '' ? 'const' : 'var', + value => $var eq '' ? $arg->getAttribute('constant'): $var, + }; + push @args_a, $args_h; + } +# print "@args_a\n"; + return \@args_a; +} + +sub xml_parse_section { + my ($self, $section) = @_; + my $section_name = $section->getAttribute('name'); + my ($items_name) = ($section_name =~ /(.*)s$/); +# print "section $section_name items_name $items_name\n"; + my $items = $section->getElementsByTagName($items_name); + my $h_name = '-'.$section_name; + print "h_name $h_name\n"; + my $tmp = $self->get($h_name); + foreach my $item (@{$items}){ + if ($section_name eq "hosts") { + $tmp->{$item->getAttribute('name')} = $item->getAttribute('ip'); + } + elsif ($section_name eq 'variables') { + $tmp->{$item->getAttribute('name')} = $item->getAttribute('value'); + } + elsif ($section_name eq 'programs') { + my $pgm_name = $item->getAttribute('name'); + my $args = $item->getElementsByTagName("arg"); + my $args_h = xml_parse_args($args); + $tmp->{$pgm_name} = + {name => $pgm_name, + command => $item->getAttribute('command'), + args => $args_h, + }; + } + elsif ($section_name eq 'sessions') { + my $session_name = $item->getAttribute('name'); + my $xsessions_pgms = $item->getElementsByTagName("program"); + my @sessions_pgms; + foreach my $session_pgm (@{$xsessions_pgms}){ + my $pgm_name = $session_pgm->getAttribute('name'); + my $session_args = $session_pgm->getElementsByTagName("arg"); + my $args_h = xml_parse_args($session_args); + push @sessions_pgms, { + name => $pgm_name, + args => $args_h + }; + } + $tmp->{$session_name} = { + name => $session_name, + pgms => \@sessions_pgms + }; + } + } + $self->configure($h_name => $tmp); +} + +sub read_cfg { + my ($self, $filename) = @_; + my $parser = XML::DOM::Parser->new(); + my $doc = $parser->parsefile($filename); + my $cp = $doc->getElementsByTagName("control_panel")->[0]; + $self->{cp_name} = $cp->getAttribute('name'); + my $sections = $cp->getElementsByTagName("section"); + foreach my $section (@{$sections}) { + $self->xml_parse_section($section); + } +} + + +1; diff --git a/sw/supervision/paparazzi.pl b/sw/supervision/paparazzi.pl new file mode 100755 index 00000000000..8a8640ec95f --- /dev/null +++ b/sw/supervision/paparazzi.pl @@ -0,0 +1,80 @@ +#!/usr/bin/perl -w +package Paparazzi; + +my $paparazzi_lib; +BEGIN { + $paparazzi_lib = (defined $ENV{PAPARAZZI_SRC}) ? + $ENV{PAPARAZZI_SRC}."/sw/lib/perl" : "/usr/lib/paparazzi/"; +} +use lib ($paparazzi_lib); + +use Paparazzi::CpGui; +@ISA = qw(Paparazzi::CpGui); + +use Paparazzi::Environment; + +use strict; + +use Tk; +use Subject; + +use Data::Dumper; +use Getopt::Long; + +sub populate { + my ($self, $args) = @_; + my $paparazzi_src = Paparazzi::Environment::paparazzi_src(); + my $paparazzi_home = Paparazzi::Environment::paparazzi_home(); + Paparazzi::Environment::check_paparazzi_home(); + $args->{-config_file} = $paparazzi_home."/conf/control_panel.xml"; + $args->{-variables} = {paparazzi_home => $paparazzi_home}; + $args->{-bin_base_dir} = $paparazzi_src; + $args->{-logo_file} = $paparazzi_home."/data/pictures/penguin_logo.gif"; + $self->SUPER::populate($args); + $self->configspec(-variables => [S_SUPER, S_SUPER, S_SUPER, S_SUPER, S_SUPER, undef]); +} +sub completeinit { + my ($self) = @_; + $self->SUPER::completeinit(); + $self->parse_args(); +} + +sub parse_args { + my ($self) = @_; + my $options = { + ivy_bus => "127.255.255.255:2005", + map => "maps/muret_UTM.xml", + render => "1", + }; + GetOptions("b=s" => \$options->{ivy_bus}, + "m=s" => \$options->{map}, + "r=s" => \$options->{render}, + ); + my $variables = $self->get('-variables'); + foreach my $var (keys %{$options}) { + $variables->{$var} = $options->{$var}; + } + $self->configure('-variables' => $variables); +} + +sub catchSigTerm() { + my ($paparazzi) = @_; + printf("in catchSigTerm\n"); + $paparazzi->terminate_all(); +} + +my $paparazzi = Paparazzi->new(); +$SIG{TERM} = sub {$paparazzi->catchSigTerm()}; +Tk::MainLoop(); +$paparazzi->catchSigTerm(); + +1; + + + + + + + + + diff --git a/sw/tools/Makefile b/sw/tools/Makefile new file mode 100644 index 00000000000..055d7e53dae --- /dev/null +++ b/sw/tools/Makefile @@ -0,0 +1,39 @@ +OCAMLC=ocamlc -g -I ../lib/ocaml +OCAMLLEX=ocamllex +OCAMLYACC=ocamlyacc + +all: gen_aircraft.out gen_airframe.out gen_calib.out gen_messages.out gen_ubx.out gen_flight_plan.out gen_radio.out + +FP_CMO = fp_syntax.cmo fp_parser.cmo fp_lexer.cmo fp_proc.cmo gen_flight_plan.cmo + +gen_flight_plan.out : $(FP_CMO) + $(OCAMLC) lib.cma $^ -o $@ + +fp_parser.cmo : fp_parser.cmi fp_syntax.cmi +fp_parser.cmi : fp_parser.ml fp_syntax.cmi +fp_lexer.cmi : fp_syntax.cmi +fp_lexer.cmo : fp_lexer.cmi +gen_flight_plan.cmo : fp_parser.cmi fp_proc.cmi +fp_syntax.cmo : fp_syntax.cmi + + +%.out : %.ml + $(OCAMLC) lib.cma $< -o $@ + +%.cmo : %.ml + $(OCAMLC) -c $< + +%.cmi : %.mli + $(OCAMLC) -c $< + +%.ml : %.mll + $(OCAMLLEX) $< + +%.ml : %.mly + $(OCAMLYACC) $< + +%.mli : %.mly + $(OCAMLYACC) $< + +clean: + rm -f *.cm* *.out *~ fp_parser.ml fp_parser.mli diff --git a/sw/tools/fp_lexer.mll b/sw/tools/fp_lexer.mll new file mode 100644 index 00000000000..b59124cc3c1 --- /dev/null +++ b/sw/tools/fp_lexer.mll @@ -0,0 +1,30 @@ +{ +open Fp_parser +} +rule token = parse + [' ' '\t' '\n'] { token lexbuf} + | "/*"([^'*']|'*'[^'/'])*'*'*'/' { token lexbuf} + | ['0'-'9']+ { INT (int_of_string (Lexing.lexeme lexbuf)) } + | ['0'-'9']+'.'['0'-'9']* { FLOAT (float_of_string (Lexing.lexeme lexbuf)) } + | ['a'-'z' 'A'-'Z'] (['a'-'z' 'A'-'Z' '_' '.' '0'-'9']*) { IDENT (Lexing.lexeme lexbuf) } + | ',' { COMMA } + | ';' { SEMICOLON } + | ':' { COLON } + | '(' { LP } + | ')' { RP } + | '{' { LC } + | '}' { RC } + | '[' { LB } + | ']' { RB } + | "==" { EQ } + | "&&" { AND } + | ">" { GT } + | ">=" { GEQ } + | "+" { PLUS } + | "=" { ASSIGN } + | "-" { MINUS } + | "*" { MULT } + | "/" { DIV } + | "!" { NOT } + | eof { EOF } + diff --git a/sw/tools/fp_parser.mly b/sw/tools/fp_parser.mly new file mode 100644 index 00000000000..402ee6039a0 --- /dev/null +++ b/sw/tools/fp_parser.mly @@ -0,0 +1,51 @@ +/* $Id$ */ +%{ +open Fp_syntax +%} +%token INT +%token FLOAT +%token IDENT +%token EOF +%token COMMA SEMICOLON LP RP LC RC LB RB AND COLON +%token EQ GT ASSIGN GEQ NOT +%token PLUS MINUS +%token MULT DIV + + +%left EQ GT ASSIGN GEQ /* lowest precedence */ +%left PLUS MINUS +%left MULT DIV +%nonassoc NOT +%nonassoc UMINUS /* highest precedence */ + +%start expression /* the entry point */ +%type expression + +%% + +expression: + expression GT expression { Call (">",[$1;$3]) } + | expression GEQ expression { Call (">=",[$1;$3]) } + | expression EQ expression { Call ("==",[$1;$3]) } + | expression AND expression { Call ("&&",[$1;$3]) } + | expression PLUS expression { Call ("+",[$1;$3]) } + | expression MINUS expression { Call ("-",[$1;$3]) } + | expression MULT expression { Call ("*",[$1;$3]) } + | expression DIV expression { Call ("/",[$1;$3]) } + | MINUS expression %prec UMINUS { Call ("-",[$2]) } + | NOT expression { Call ("!",[$2]) } + | INT { Int $1 } + | FLOAT { Float $1 } + | IDENT { Ident $1 } + | IDENT LP Args RP { Call ($1, $3) } + | LP expression RP { $2 } + | IDENT LB expression RB { Index ($1, $3) } +; + +Args: { [] } + | expression NextArgs { $1::$2 } +; + +NextArgs: { [] } + | COMMA expression NextArgs { $2::$3 } +; diff --git a/sw/tools/fp_proc.ml b/sw/tools/fp_proc.ml new file mode 100644 index 00000000000..46db0f34329 --- /dev/null +++ b/sw/tools/fp_proc.ml @@ -0,0 +1,389 @@ +(* + * $Id$ + * + * Flight plan preprocessing (procedure including) + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Fp_syntax + + +let parse_expression = fun s -> + let lexbuf = Lexing.from_string s in + try + Fp_parser.expression Fp_lexer.token lexbuf + with + Failure("lexing: empty token") -> + Printf.fprintf stderr "Lexing error in '%s': unexpected char: '%c' \n" + s (Lexing.lexeme_char lexbuf 0); + exit 1 + | Parsing.Parse_error -> + Printf.fprintf stderr "Parsing error in '%s', token '%s' ?\n" + s (Lexing.lexeme lexbuf); + exit 1 + + +open Latlong + +let float_attrib = fun xml a -> float_of_string (ExtXml.attrib xml a) + +(* Translation and rotation *) +type affine = { dx : float; dy : float; angle : float (* Deg Clockwise *) } + +let dtd_error = fun f e -> + Printf.fprintf stderr "DTD error in '%s': %s\n" f e; + exit 1 + +(* Rotation. Would be better with a matrix multiplication ? *) +let rotate = fun angle (x, y) -> + let angle = -. (Deg>>Rad) angle in + let a = atan2 y x + and r = sqrt (x**2. +. y**2.) in + let a' = a +. angle in + (r*.cos a', r*.sin a') + +let rotate_expression = fun a expression -> + let rec rot = fun e -> + match e with + | Call("Qdr", [Float a']) -> Call("Qdr", [Float (a' +. a)]) + | Call("Qdr", [Int a']) -> Call("Qdr", [Float (float a' +. a)]) + | Call(op, [e1; e2]) when op = "And" || op ="Or" -> + Call(op, [rot e1; rot e2]) + | _ -> e in + rot expression + +let subst_expression = fun env e -> + let rec sub = fun e -> + match e with + Ident i -> Ident (try List.assoc i env with Not_found -> i) + | Int _ | Float _ -> e + | Call (i, es) -> Call (i, List.map sub es) + | Index (i,e) -> Index (i,sub e) in + sub e + + +let transform_expression = fun affine env e -> + let e = parse_expression e in + let e' = rotate_expression affine.angle e in + let e'' = subst_expression env e' in + Fp_syntax.sprint_expression e'' + + +let transform_values = fun attribs_not_modified affine env attribs -> + List.map + (fun (a, v) -> + let v' = + if List.mem (String.lowercase a) attribs_not_modified + then v + else transform_expression affine env v in + (a, v')) + attribs + +let transform_waypoint = fun prefix affine xml -> + let x = float_attrib xml "x" + and y = float_attrib xml "y" in + let (x, y) = rotate affine.angle (x, y) in + let (x, y) = (x+.affine.dx, y+.affine.dy) in + let alt = try ["alt", ExtXml.attrib xml "alt"] with ExtXml.Error _ -> [] in + Xml.Element (Xml.tag xml, + ["name", prefix (ExtXml.attrib xml "name"); + "x", string_of_float x; + "y", string_of_float y]@alt, + []) + + +let prefix_value = fun prefix name attribs -> + List.map + (fun (a, v) -> + let v' = if String.lowercase a = name then prefix v else v in + (a, v')) + attribs + +let prefix_or_deroute = fun prefix reroutes name attribs -> + List.map + (fun (a, v) -> + let v' = + if String.lowercase a = name then + try List.assoc v reroutes with + Not_found -> prefix v + else v in + (a, v')) + attribs + +let transform_attribs = fun affine attribs -> + List.map + (fun (a, v) -> + match String.lowercase a with + "wp_qdr" | "from_qdr" -> + (a, string_of_float (float_of_string v +. affine.angle)) + | _ -> (a, v) + ) + attribs + + +let transform_stage = fun prefix reroutes affine env xml -> + let rec tr = fun xml -> + match xml with + Xml.Element (tag, attribs, children) -> begin + match tag with + "exception" -> + assert (children=[]); + let attribs = prefix_or_deroute prefix reroutes "deroute" attribs in + let attribs = transform_values [] affine env attribs in + Xml.Element (tag, attribs, children) + | "while" -> + let attribs = transform_values [] affine env attribs in + Xml.Element (tag, attribs, List.map tr children) + | "heading" -> + assert (children=[]); + let attribs = transform_values ["vmode"] affine env attribs in + Xml.Element (tag, attribs, children) + | "go" -> + assert (children=[]); + let attribs = transform_values ["wp";"from";"hmode";"vmode"] affine env attribs in + let attribs = prefix_value prefix "wp" attribs in + let attribs = prefix_value prefix "from" attribs in + let attribs = transform_attribs affine attribs in + Xml.Element (tag, attribs, children) + | "xyz" -> + assert (children=[]); + let attribs = transform_values [] affine env attribs in + Xml.Element (tag, attribs, children) + | "circle" -> + assert (children=[]); + let attribs = transform_values ["wp";"hmode";"vmode"] affine env attribs in + let attribs = prefix_value prefix "wp" attribs in + let attribs = transform_attribs affine attribs in + Xml.Element (tag, attribs, children) + | "deroute" -> + assert (children=[]); + let attribs = prefix_or_deroute prefix reroutes "block" attribs in + Xml.Element (tag, attribs, children) + | "stay" -> + assert (children=[]); + let attribs = transform_values ["wp"; "vmode"] affine env attribs in + let attribs = prefix_value prefix "wp" attribs in + let attribs = transform_attribs affine attribs in + Xml.Element (tag, attribs, children) + | _ -> failwith (Printf.sprintf "Fp_proc: Unexpected tag: '%s'" tag) + end + | _ -> failwith "Fp_proc: Xml.Element expected" + in + tr xml + +let transform_block = fun prefix reroutes affine env xml -> + Xml.Element (Xml.tag xml, + ["name", prefix (ExtXml.attrib xml "name")], + List.map (transform_stage prefix reroutes affine env) (Xml.children xml)) + + +let check_params = fun params env -> + List.iter + (fun p -> + if not (List.mem_assoc p env) then begin + Printf.fprintf stderr "Parameter '%s' is missing\n" p; + exit 1 + end) + params + +let parse_include = fun dir include_xml -> + let f = Filename.concat dir (ExtXml.attrib include_xml "procedure") in + let proc_name = ExtXml.attrib include_xml "name" in + let prefix = fun x -> proc_name ^ "." ^ x in + let affine = { + dx = float_attrib include_xml "x"; + dy = float_attrib include_xml "y"; + angle = float_attrib include_xml "rotate" + } in + let reroutes = + List.filter + (fun x -> String.lowercase (Xml.tag x) = "with") + (Xml.children include_xml) in + let reroutes = List.map + (fun xml -> (ExtXml.attrib xml "from", ExtXml.attrib xml "to")) + reroutes in + let args = + List.filter + (fun x -> String.lowercase (Xml.tag x) = "arg") + (Xml.children include_xml) in + let env = List.map + (fun xml -> (ExtXml.attrib xml "name", ExtXml.attrib xml "value")) + args in + try + let proc = Xml.parse_file f in + let params = List.filter + (fun x -> String.lowercase (Xml.tag x) = "param") + (Xml.children proc) in + let value = fun xml env -> + let name = ExtXml.attrib xml "name" in + try + (name, List.assoc name env) + with + Not_found -> + try + (name, Xml.attrib xml "default_value") + with + _ -> failwith (Printf.sprintf "Value required for param '%s' in %s" name (Xml.to_string include_xml)) in + (* Complete the environment with default values *) + let env = List.map (fun xml -> value xml env) params in + + let waypoints = Xml.children (ExtXml.child proc "waypoints") + and blocks = Xml.children (ExtXml.child proc "blocks") in + + let waypoints = List.map (transform_waypoint prefix affine) waypoints in + let blocks = List.map (transform_block prefix reroutes affine env) blocks in + (waypoints, blocks) + with + Dtd.Prove_error e -> dtd_error f (Dtd.prove_error e) + | Dtd.Check_error e -> dtd_error f (Dtd.check_error e) + + +(** Adds new children to a list of XML elements *) +let insert_children = fun xmls new_children_assoc -> + List.map + (fun x -> + try + let new_children = List.assoc (Xml.tag x) new_children_assoc + and old_children = Xml.children x in + Xml.Element (Xml.tag x, Xml.attribs x, old_children @ new_children) + with + Not_found -> x + ) + xmls + +let replace_children = fun xml new_children_assoc -> + Xml.Element (Xml.tag xml, Xml.attribs xml, + List.map + (fun x -> + try + let new_children = List.assoc (Xml.tag x) new_children_assoc in + new_children + with + Not_found -> x + ) + (Xml.children xml)) + + +let process_includes = fun dir xml -> + let includes, children = + List.partition (fun x -> Xml.tag x = "include") (Xml.children xml) in + + (* List of pairs of list (waypoints, blocks) *) + let waypoints_and_blocks = List.map (parse_include dir) includes in + + let (inc_waypoints, inc_blocks) = List.split waypoints_and_blocks in + let inc_waypoints = List.flatten inc_waypoints + and inc_blocks = List.flatten inc_blocks in + + let new_children = insert_children children + ["waypoints", inc_waypoints; "blocks", inc_blocks] in + + Xml.Element (Xml.tag xml, Xml.attribs xml, new_children) + +let remove_attribs = fun xml names -> + List.filter (fun (x,_) -> not (List.mem (String.lowercase x) names)) (Xml.attribs xml) + +let xml_assoc_attrib = fun a v xmls -> + match List.filter (fun x -> ExtXml.attrib x a = v) xmls with + p::_ -> p + | _ -> failwith "xml_assoc_attrib" + +let coords_of_waypoint = fun wp -> + (float_attrib wp "x", float_attrib wp "y") + + +let new_waypoint = fun wp qdr dist waypoints -> + let wp_xml = xml_assoc_attrib "name" wp !waypoints in + let wpx, wpy = coords_of_waypoint wp_xml in + let a = (Deg>>Rad)(90. -. qdr) in + let x = string_of_float (wpx +. dist *. cos a) + and y = string_of_float (wpy +. dist *. sin a) in + let name = Printf.sprintf "%s_%.0f_%.0f" wp qdr dist in + let alt = try ["alt", Xml.attrib wp_xml "alt"] with _ -> [] in + waypoints := Xml.Element("waypoint", ["name", name; "x", x; "y", y]@alt, []) :: !waypoints; + name + + +let replace_wp = fun stage waypoints -> + try + let qdr = float_attrib stage "wp_qdr" + and dist = float_attrib stage "wp_dist" in + let wp = ExtXml.attrib stage "wp" in + + let name = new_waypoint wp qdr dist waypoints in + + let other_attribs = remove_attribs stage ["wp";"wp_qdr";"wp_dist"] in + Xml.Element (Xml.tag stage, ("wp", name)::other_attribs, []) + with + _ -> stage + + +let replace_from = fun stage waypoints -> + try + let qdr = float_attrib stage "from_qdr" + and dist = float_attrib stage "from_dist" in + let wp = ExtXml.attrib stage "from" in + + let name = new_waypoint wp qdr dist waypoints in + + let other_attribs = remove_attribs stage ["from";"from_qdr";"from_dist"] in + Xml.Element (Xml.tag stage, ("from", name)::other_attribs, []) + with + _ -> stage + + +let process_stage = fun stage waypoints -> + let rec do_it = fun stage -> + match String.lowercase (Xml.tag stage) with + "go" | "stay" | "circle" -> + replace_from (replace_wp stage waypoints) waypoints + + | "while" -> + Xml.Element("while", Xml.attribs stage, List.map do_it (Xml.children stage)) + | _ -> stage in + do_it stage + + +let process_relative_waypoints = fun xml -> + let waypoints = (ExtXml.child xml "waypoints") + and blocks = ExtXml.child xml "blocks" in + + let blocks_list = Xml.children blocks in + + let waypoints_list = ref (Xml.children waypoints) in + + let blocks_list = + List.map + (fun block -> + let new_children = + List.map + (fun stage -> process_stage stage waypoints_list) + (Xml.children block) in + Xml.Element (Xml.tag block, Xml.attribs block, new_children) + ) + blocks_list in + + let new_waypoints = Xml.Element ("waypoints", Xml.attribs waypoints, !waypoints_list) + and blocks = Xml.Element ("blocks", Xml.attribs blocks, blocks_list) in + + replace_children xml ["waypoints", new_waypoints; "blocks", blocks] + diff --git a/sw/tools/fp_syntax.ml b/sw/tools/fp_syntax.ml new file mode 100644 index 00000000000..c5c3d0b4f46 --- /dev/null +++ b/sw/tools/fp_syntax.ml @@ -0,0 +1,77 @@ +(* + $Id$ + + Syntax of flight plan expressions +*) + +open Printf + +type ident = string + +type operator = string +type expression = + | Ident of ident + | Int of int + | Float of float + | Call of ident * (expression list) + | Index of ident * expression + +(* Valid unary and binary opetarors *) +let binary_operators = ["+"; ">"; "-"] +let unary_operators = ["!"; "-"] + +let is_binary = fun op -> List.mem op binary_operators +let is_unary = fun op -> List.mem op unary_operators + +let rec sprint_expression = function + Ident i -> sprintf "%s" i + | Int i -> sprintf "%d" i + | Float i -> sprintf "%f" i + | Call (op, [e1;e2]) when is_binary op -> + sprintf "(" ^ sprint_expression e1 ^ op ^ sprint_expression e2 ^ ")" + | Call (op, [e1]) when is_unary op -> + sprintf "%s(%s)" op (sprint_expression e1) + | Call (i, es) -> + let ses = List.map sprint_expression es in + sprintf "%s(" i ^ String.concat "," ses ^ ")" + | Index (i,e) -> sprintf "%s[" i ^ sprint_expression e ^ "]" + +(* Valid functions *) +let functions = [ + "Qdr"; + "And"; + "Or"; + "RcEvent1"; + "RcEvent2"] @ binary_operators @ unary_operators + +(* Valid identifiers *) +let variables = [ + "launch"; + "estimator_z"; + "estimator_flight_time"; + "stage_time"; + "block_time"; + "SECURITY_ALT"; + "GROUND_ALT"; + "TRUE"; + "QFU" +] + +exception Unknown_ident of string +exception Unknown_operator of string +exception Unknown_function of string + +let rec check_expression = fun e -> + match e with + Ident i -> + if not (List.mem i variables) then + raise (Unknown_ident i) + | Int _ | Float _ -> () + | Call (i, es) -> + if not (List.mem i functions) then + raise (Unknown_function i); + List.iter check_expression es + | Index (i,e) -> + if not (List.mem i variables) then + raise (Unknown_ident i); + check_expression e diff --git a/sw/tools/fp_syntax.mli b/sw/tools/fp_syntax.mli new file mode 100644 index 00000000000..e56dc161d72 --- /dev/null +++ b/sw/tools/fp_syntax.mli @@ -0,0 +1,44 @@ +(* + * $Id$ + * + * Syntax of flight plan parsed expressions + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +type ident = string +type operator = string +type expression = + | Ident of ident + | Int of int + | Float of float + | Call of ident * expression list + | Index of ident * expression + +val sprint_expression : expression -> string + +exception Unknown_ident of string +exception Unknown_operator of string +exception Unknown_function of string + +val check_expression : expression -> unit +(** May raise [Unknown_ident], [Unknown_operator] or [Unknown_function] + exceptions *) diff --git a/sw/tools/gen_aircraft.ml b/sw/tools/gen_aircraft.ml new file mode 100644 index 00000000000..14dd0984929 --- /dev/null +++ b/sw/tools/gen_aircraft.ml @@ -0,0 +1,33 @@ +open Printf + +let (//) = Filename.concat + +let paparazzi_home = Env.paparazzi_home +let conf_xml = paparazzi_home // "conf" // "conf.xml" + +let mkdir = fun d -> + if not (Sys.file_exists d) then + Unix.mkdir d 0o755 + +let _ = + let aircraft = Sys.argv.(1) in + let conf = Xml.parse_file conf_xml in + let aircraft_xml = + try + ExtXml.child conf ~select:(fun x -> Xml.attrib x "name" = aircraft) "aircraft" + with + Not_found -> failwith (sprintf "Aircraft '%s' not found in '%s'" aircraft conf_xml) + + in + let value = ExtXml.attrib aircraft_xml in + + let aircraft_dir = paparazzi_home // "var" // aircraft in + + mkdir aircraft_dir; + mkdir (aircraft_dir // "fbw"); + mkdir (aircraft_dir // "autopilot"); + mkdir (aircraft_dir // "sim"); + + let c = sprintf "make -f Makefile.ac AIRCRAFT=%s AIRFRAME=%s RADIO=%s FLIGHT_PLAN=%s" aircraft (value "airframe") (value "radio") (value "flight_plan") in + prerr_endline c; + exit (Sys.command c) diff --git a/sw/tools/gen_airframe.ml b/sw/tools/gen_airframe.ml new file mode 100644 index 00000000000..9b565c39cc3 --- /dev/null +++ b/sw/tools/gen_airframe.ml @@ -0,0 +1,157 @@ +(* + * $Id$ + * + * XML preprocessing for airframe parameters + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +let command_travel = 1200. (* !!!! From link_autopilot.h !!!! *) +let nb_servo_4017 = 10 (* From servo.h *) + +open Printf +open Xml2h + + +type channel = { min : float; max : float; neutral : float } + +let fos = float_of_string +let sof = fun x -> if mod_float x 1. = 0. then Printf.sprintf "%.0f" x else string_of_float x +let soi = string_of_int + +let define_macro name n x = + let a = fun s -> ExtXml.attrib x s in + printf "#define %s(" name; + match n with (* Do we really need more ??? *) + 1 -> printf "x1) (%s*(x1))\n" (a "coeff1") + | 2 -> printf "x1,x2) (%s*(x1)+ %s*(x2))\n" (a "coeff1") (a "coeff2") + | 3 -> printf "x1,x2,x3) (%s*(x1)+ %s*(x2)+%s*(x3))\n" (a "coeff1") (a "coeff2") (a "coeff3") + | _ -> failwith "define_macro" + +let parse_element = fun prefix s -> + match Xml.tag s with + "define" -> + define (prefix^ExtXml.attrib s "name") (ExtXml.attrib s "value") + | "linear" -> + let name = ExtXml.attrib s "name" + and n = int_of_string (ExtXml.attrib s "arity") in + define_macro (prefix^name) n s + | _ -> xml_error "define|linear" + + +let parse_servo = + fun default_min default_neutral default_max servo_params c -> + let name = "SERVO_"^ExtXml.attrib c "name" in + let no_servo = int_of_string (ExtXml.attrib c "no") in + define name (string_of_int no_servo); + let min = fos (ExtXml.attrib_or_default c "min" (sof default_min)) + and neutral = fos (ExtXml.attrib_or_default c "neutral" (sof default_neutral)) + and max = fos (ExtXml.attrib_or_default c "max" (sof default_max)) in + + let travel = (max-.min) /. command_travel in + define (name^"_TRAVEL") (sof travel); + define (sprintf "SERVOS_NEUTRALS_%d" no_servo) (sof neutral); + nl (); + + servo_params.(no_servo) <- { min = min; neutral = neutral; max = max } + + +let pprz_value = Str.regexp "@\\([A-Z]+\\)" +let var_value = Str.regexp "\\$\\([_a-z0-9]+\\)" +let preprocess_command = fun s -> + let s = Str.global_replace pprz_value "values[RADIO_\\1]" s in + Str.global_replace var_value "_var_\\1" s + +let parse_command = fun command -> + let a = fun s -> ExtXml.attrib command s in + match Xml.tag command with + "set" -> + let servo = a "servo" + and value = a "value" in + let v = preprocess_command value in + printf " servo_value = SERVO_NEUTRAL(SERVO_%s) + (int16_t)((%s)*SERVO_%s_TRAVEL);\\\n" servo v servo; + printf " servo_widths[SERVO_%s] = ChopServo(servo_value);\\\n\\\n" servo + | "let" -> + let var = a "var" + and value = a "value" in + let v = preprocess_command value in + printf " int16_t _var_%s = %s;\\\n" var v + | _ -> xml_error "set|let" + +let parse_section = fun s -> + match Xml.tag s with + "section" -> + let prefix = ExtXml.attrib_or_default s "prefix" "" in + List.iter (parse_element prefix) (Xml.children s); + nl () + | "servos" -> + let get_float = fun x -> float_of_string (ExtXml.attrib s x) in + let min = get_float "min" + and neutral = get_float "neutral" + and max = get_float "max" in + + let servos = Xml.children s in + define "NB_SERVO" (string_of_int (List.length servos)); + nl (); + let servos_params = Array.create nb_servo_4017 { min = min; neutral = neutral; max = max } in + + List.iter (parse_servo min neutral max servos_params) servos; + + let servos_params = Array.to_list servos_params in + + nl (); + define "SERVOS_MINS" (sprint_float_array (List.map (fun x -> sof x.min) servos_params)); + define "SERVOS_NEUTRALS" (sprint_float_array (List.map (fun x -> sof x.neutral) servos_params)); + define "SERVOS_MAXS" (sprint_float_array (List.map (fun x -> sof x.max) servos_params)); + nl (); + + (* For compatibility *) + define "SERVO_MIN_US" (sprintf "%.0ful" min); + define "SERVO_MAX_US" (sprintf "%.0ful" max); + nl () + | "command" -> + printf "#define ServoSet(values) { \\\n"; + printf " uint16_t servo_value;\\\n"; + List.iter parse_command (Xml.children s); + printf "}\n" + | _ -> xml_error "param|servos|command" + + +let h_name = "AIRFRAME_H" + +let _ = + if Array.length Sys.argv <> 3 then + failwith (Printf.sprintf "Usage: %s A/C_ident xml_file" Sys.argv.(0)); + let xml_file = Sys.argv.(2) + and ac_name = Sys.argv.(1) in + try + let xml = start_and_begin xml_file h_name in + Xml2h.warning ("AIRFRAME MODEL: "^ ac_name); + define_string "AIRFRAME_NAME" ac_name; + nl (); + let v = ExtXml.attrib xml "ctl_board" in + define ("CTL_BRD_"^v) "1"; + nl (); + List.iter parse_section (Xml.children xml); + finish h_name + with + Xml.Error e -> prerr_endline (Xml.error e) + diff --git a/sw/tools/gen_calib.ml b/sw/tools/gen_calib.ml new file mode 100644 index 00000000000..3eaa05113d8 --- /dev/null +++ b/sw/tools/gen_calib.ml @@ -0,0 +1,101 @@ +(* + * $Id$ + * + * XML preprocessing for in flight calibration + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf +open Xml2h + +let margin = ref 0 +let step = 2 +let tab () = printf "%s" (String.make !margin ' ') +let right () = margin := !margin + step +let left () = margin := !margin - step + +let lprintf = fun f -> tab (); printf f + + +let calib_mode_of_rc = function + "gain_1_up" -> 1, "up" + | "gain_1_down" -> 1, "down" + | "gain_2_up" -> 2, "up" + | "gain_2_down" -> 2, "down" + | x -> failwith (sprintf "Unknown rc: %s" x) + +let param_macro_of_type = fun x -> "ParamVal"^String.capitalize x + +let inttype = function + "int16" -> "int16_t" + | "float" -> "float" + | x -> failwith (sprintf "Gen_calib.inttype: unknown type '%s'" x) + +let parse_setting = fun xml -> + let cursor, cm = calib_mode_of_rc (ExtXml.attrib xml "rc") + and var = ExtXml.attrib xml "var" + and range = float_of_string (ExtXml.attrib xml "range") in + let t = (ExtXml.attrib xml "type") in + let param_macro = param_macro_of_type t in + let var_init = var ^ "_init" in + + lprintf "if (inflight_calib_mode == IF_CALIB_MODE_%s) {\n" (String.uppercase cm); + right (); + lprintf "static %s %s;\n" (inttype t) var_init; + lprintf "if (mode_changed) {\n"; + right (); + lprintf "%s = %s;\n" var_init var; + lprintf "slider%d_init = from_fbw.channels[RADIO_GAIN%d];\n" cursor cursor; + left (); lprintf "}\n"; + lprintf "%s = %s(%s, %f, from_fbw.channels[RADIO_GAIN%d], slider%d_init);\n" var param_macro var_init range cursor cursor; + lprintf "slider_%d_val = (float)%s;\n" cursor var; + left (); lprintf "}\n" + + +let parse_mode = fun xml -> + lprintf "if (pprz_mode == PPRZ_MODE_%s) {\n" (ExtXml.attrib xml "name"); + right (); + List.iter parse_setting (Xml.children xml); + left (); lprintf "}\n" + +let parse_modes = fun xml -> + List.iter parse_mode (Xml.children xml) + + +let _ = + if Array.length Sys.argv < 2 then + failwith (Printf.sprintf "Usage: %s xml_file" Sys.argv.(0)); + let xml_file = Sys.argv.(1) in + let h_name = "INFLIGHT_CALIB_H" in + try + let xml = start_and_begin xml_file h_name in + + let rc_control = try ExtXml.child xml "rc_control" with Not_found -> failwith (sprintf "Error: 'rc_control' child expected in %s" (Xml.to_string xml)) in + lprintf "void inflight_calib(bool_t mode_changed) {\n"; + right (); + parse_modes rc_control; + left (); lprintf "}\n"; + + finish h_name + with + Xml.Error e -> prerr_endline (Xml.error e) + | Dtd.Prove_error e -> prerr_endline (Dtd.prove_error e); exit 1 diff --git a/sw/tools/gen_flight_plan.ml b/sw/tools/gen_flight_plan.ml new file mode 100644 index 00000000000..dc56388f090 --- /dev/null +++ b/sw/tools/gen_flight_plan.ml @@ -0,0 +1,450 @@ +(* + * $Id$ + * + * Flight plan preprocessing (from XML to C) + * + * Copyright (C) 2004 CENA/ENAC, Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf +open Latlong + +let check_expressions = ref true + +let parse_expression = Fp_proc.parse_expression + +let parse = fun s -> + if !check_expressions then + let e = parse_expression s in + let unexpected = fun kind x -> + fprintf stderr "Paring error in '%s': unexpected %s: '%s' \n" s kind x; + exit 1 in + begin + try + Fp_syntax.check_expression e + with + Fp_syntax.Unknown_operator x -> unexpected "operator" x + | Fp_syntax.Unknown_ident x -> unexpected "ident" x + | Fp_syntax.Unknown_function x -> unexpected "function" x + end; + Fp_syntax.sprint_expression e + else + s + +let parsed_attrib = fun xml a -> + parse (ExtXml.attrib xml a) + +let pi = atan 1. *. 4. + +let radE8_of_deg = fun d -> + d /. 180. *. pi *. 1e8 + +let rad_of_deg = fun d -> + d /. 180. *. pi + +let gen_label = + let x = ref 0 in + fun p -> incr x; sprintf "%s_%d" p !x + +let margin = ref 0 +let step = 2 + +let right () = margin := !margin + step +let left () = margin := !margin - step + +let lprintf = fun f -> + printf "%s" (String.make !margin ' '); + printf f + +let float_attrib = fun xml a -> float_of_string (Xml.attrib xml a) +let int_attrib = fun xml a -> int_of_string (Xml.attrib xml a) +let name_of = fun wp -> ExtXml.attrib wp "name" + + +let ground_alt = ref 0. +let security_height = ref 0. + +let check_altitude = fun a x -> + if a < !ground_alt +. !security_height then begin + fprintf stderr "\nWARNING: low altitude (%.0f<%.0f+%.0f) in %s\n\n" a !ground_alt !security_height (Xml.to_string x) + end + +let print_waypoint = fun rel_utm_of_wgs84 default_alt waypoint -> + let (x, y) = + try + rel_utm_of_wgs84 {posn_lat=(Deg>>Rad)(float_attrib waypoint "lat"); + posn_long=(Deg>>Rad)(float_attrib waypoint "lon") } + with + Xml.No_attribute "lat" | Xml.No_attribute "lon" -> + (float_attrib waypoint "x", float_attrib waypoint "y") + and alt = try Xml.attrib waypoint "alt" with _ -> default_alt in + check_altitude (float_of_string alt) waypoint; + printf " {%.1f, %.1f, %s},\\\n" x y alt + + +let blocks = Hashtbl.create 11 +let index_of_blocks = ref [] +let stages = Hashtbl.create 11 + +let get_index_block = fun x -> + try + string_of_int (List.assoc x !index_of_blocks) + with + Not_found -> failwith (sprintf "Unknown block: '%s'" x) + +let print_exception = fun x -> + let i = get_index_block (ExtXml.attrib x "deroute") in + let c = parsed_attrib x "cond" in + lprintf "if %s { GotoBlock(%s) }\n" c i + +let return_from_excpt l = Xml.Element ("return_from_excpt", ["name",l], []) +let goto l = Xml.Element ("goto", ["name",l], []) +let exit_block = Xml.Element ("exit_block", [], []) + +let stage = ref 0 + +let output_label l = lprintf "Label(%s)\n" l + +let output_vmode x wp last_wp = + let pitch = try parsed_attrib x "pitch" with _ -> "0.0" in + lprintf "nav_pitch = %s;\n" pitch; + let vmode = try ExtXml.attrib x "vmode" with _ -> "alt" in + begin + match vmode with + "climb" -> + lprintf "vertical_mode = VERTICAL_MODE_AUTO_CLIMB;\n"; + lprintf "desired_climb = %s;\n" (parsed_attrib x "climb") + | "alt" -> + lprintf "vertical_mode = VERTICAL_MODE_AUTO_ALT;\n"; + let alt = + try + let a = parsed_attrib x "alt" in + begin + try + check_altitude (float_of_string a) x + with + Failure "float_of_string" -> () + end; + a + with _ -> + if wp = "" then failwith "alt or waypoint required in alt vmode" else + sprintf "waypoints[%s].a" wp in + lprintf "desired_altitude = %s;\n" alt; + lprintf "pre_climb = 0.;\n" + | "glide" -> + lprintf "vertical_mode = VERTICAL_MODE_AUTO_ALT;\n"; + lprintf "glide_to(%s, %s);\n" last_wp wp + | "gaz" -> + lprintf "vertical_mode = VERTICAL_MODE_AUTO_GAZ;\n"; + lprintf "nav_desired_gaz = TRIM_UPPRZ(%s*MAX_PPRZ);\n" (parsed_attrib x "gaz") + | x -> failwith (sprintf "Unknown vmode '%s'" x) + end; + vmode + +let output_hmode x wp last_wp = + try + let hmode = ExtXml.attrib x "hmode" in + begin + match hmode with + "route" -> + if last_wp = "last_wp" then + fprintf stderr "WARNING: Deprecated use of 'route' using last waypoint in %s\n"(Xml.to_string x); + lprintf "route_to(%s, %s);\n" last_wp wp + | "direct" -> lprintf "fly_to(%s);\n" wp + | x -> failwith (sprintf "Unknown hmode '%s'" x) + end; + hmode + with + ExtXml.Error _ -> lprintf "fly_to(%s);\n" wp; "direct" (* Default behaviour *) + + +let get_index_waypoint = fun x l -> + try + string_of_int (List.assoc x l) + with + Not_found -> failwith (sprintf "Unknown waypoint: %s\n" x) + + +let rec compile_stage = fun block x -> + incr stage; + Hashtbl.add stages x (block, !stage); + begin + match Xml.tag x with + "while" -> + List.iter (compile_stage block) (Xml.children x); + incr stage (* To count the loop stage *) + | "return_from_excpt" | "goto" | "deroute" | "exit_block" + | "heading" | "go" | "stay" | "xyz" | "circle" -> () + | s -> failwith (sprintf "Unknown stage: %s\n" s) + end + +let rec print_stage = fun index_of_waypoints x -> + incr stage; + let stage () = lprintf "Stage(%d)\n" !stage; right () in + begin + match String.lowercase (Xml.tag x) with + "return_from_excpt" -> + stage (); + lprintf "ReturnFromException(%s)\n" (name_of x) + | "goto" -> + stage (); + lprintf "Goto(%s)\n" (name_of x) + | "deroute" -> + stage (); + lprintf "GotoBlock(%s)\n" (get_index_block (ExtXml.attrib x "block")) + | "exit_block" -> + stage (); + lprintf "NextBlock()\n" + | "while" -> + let w = gen_label "while" in + let e = gen_label "endwhile" in + output_label w; + stage (); + let c = try parsed_attrib x "cond" with _ -> "TRUE" in + lprintf "if (! (%s)) Goto(%s) else NextStage();\n" c e; + List.iter (print_stage index_of_waypoints) (Xml.children x); + print_stage index_of_waypoints (goto w); + output_label e + | "heading" -> + stage (); + let until = parsed_attrib x "until" in + lprintf "if (%s) NextStage() else {\n" until; + right (); + lprintf "desired_course = RadOfDeg(%s);\n" (parsed_attrib x "course"); + ignore (output_vmode x "" ""); + left (); lprintf "}\n"; + lprintf "return;\n" + | "go" -> + stage (); + let wp = get_index_waypoint (ExtXml.attrib x "wp") index_of_waypoints in + lprintf "if (approaching(%s)) NextStageFrom(%s) else {\n" wp wp; + right (); + let last_wp = + try + get_index_waypoint (ExtXml.attrib x "from") index_of_waypoints + with _ -> "last_wp" in + let hmode = output_hmode x wp last_wp in + let vmode = output_vmode x wp last_wp in + if vmode = "glide" && hmode <> "route" then + failwith "glide vmode requires route hmode"; + left (); lprintf "}\n"; + lprintf "return;\n" + | "stay" -> + stage (); + begin + try + let wp = get_index_waypoint (ExtXml.attrib x "wp") index_of_waypoints in + ignore(output_hmode x wp ""); + ignore (output_vmode x wp ""); + with + Xml2h.Error _ -> + lprintf "fly_to_xy(last_x, last_y);\n"; + ignore(output_vmode x "" "") + end; + lprintf "return;\n" + | "xyz" -> + stage (); + let r = try parsed_attrib x "radius" with _ -> "100" in + lprintf "Goto3D(%s)\n" r; + lprintf "return;\n" + | "circle" -> + stage (); + let wp = get_index_waypoint (ExtXml.attrib x "wp") index_of_waypoints in + let r = parsed_attrib x "radius" in + let vmode = output_vmode x wp "" in + lprintf "Circle(%s, %s);\n" wp r; + begin + try + let c = parsed_attrib x "until" in + lprintf "if (%s) NextStage();\n" c + with + ExtXml.Error _ -> () + end; + lprintf "return;\n" + | s -> failwith "Unreachable" + end; + left () + +let compile_block = fun block_num (b:Xml.xml) -> + Hashtbl.add blocks b block_num; + index_of_blocks := (name_of b, block_num) :: !index_of_blocks; + stage := (-1); + let stages = + List.filter (fun x -> Xml.tag x <> "exception") (Xml.children b) in + + List.iter (compile_stage block_num) stages; + + compile_stage block_num exit_block + +let compile_blocks = fun bs -> + let block = ref (-1) in + List.iter + (fun b -> + incr block; + compile_block !block b) + bs + + + +let print_block = fun index_of_waypoints (b:Xml.xml) block_num -> + let n = name_of b in + lprintf "Block(%d) // %s\n" block_num n; + + let excpts, stages = + List.partition (fun x -> Xml.tag x = "exception") (Xml.children b) in + + List.iter print_exception excpts; + + lprintf "switch(nav_stage) {\n"; + right (); + stage := (-1); + List.iter (print_stage index_of_waypoints) stages; + + print_stage index_of_waypoints exit_block; + + left (); lprintf "}\n\n" + + + +let print_blocks = fun index_of_waypoints bs -> + lprintf "#ifdef NAV_C\n"; + lprintf "\nstatic inline void auto_nav(void) {\n"; + right (); + lprintf "switch (nav_block) {\n"; + right (); + let block = ref (-1) in + List.iter (fun b -> incr block; print_block index_of_waypoints b !block) bs; + left (); lprintf "}\n"; + left (); lprintf "}\n"; + lprintf "#endif // NAV_C\n" + + +let define_home = fun waypoints -> + let rec loop i = function + [] -> failwith "Waypoint 'HOME' required" + | w::ws -> + if name_of w = "HOME" then begin + Xml2h.define "WP_HOME" (string_of_int i); + (float_attrib w "x", float_attrib w "y") + end else + loop (i+1) ws in + loop 0 waypoints + +let check_distance = fun (hx, hy) max_d wp -> + let x = float_attrib wp "x" + and y = float_attrib wp "y" in + let d = sqrt ((x-.hx)**2. +. (y-.hy)**2.) in + if d > max_d then + fprintf stderr "\nWARNING: Waypoint '%s' too far from HOME (%.0f>%.0f)\n\n" (name_of wp) d max_d + + + +let _ = + let xml_file = ref "fligh_plan.xml" + and dump = ref false in + Arg.parse [("-dump", Arg.Set dump, "Dump compile result"); + ("-nocheck", Arg.Clear check_expressions, "Disable expression checking")] + (fun f -> xml_file := f) + "Usage:"; + try + let xml = Xml.parse_file !xml_file in + let dir = Filename.dirname !xml_file in + let xml = Fp_proc.process_includes dir xml in + (*** prerr_endline (Xml.to_string_fmt xml); prerr_endline "\n\n\n"; ***) + let xml = Fp_proc.process_relative_waypoints xml in + let waypoints = ExtXml.child xml "waypoints" + and blocks = Xml.children (ExtXml.child xml "blocks") in + + compile_blocks blocks; + + if !dump then + let block_names = List.map (fun (x,y) -> (y, x)) !index_of_blocks in + let lstages = ref [] in + Hashtbl.iter + (fun xml (b,s) -> + lstages := + Xml.Element ("stage", [ "block", string_of_int b; + "block_name", List.assoc b block_names; + "stage", string_of_int s], [xml]) + :: !lstages) + stages; + let xml_stages = Xml.Element ("stages", [], !lstages) in + let dump_xml = Xml.Element ("dump", [], [xml; xml_stages]) in + printf "%s\n" (ExtXml.to_string_fmt dump_xml) + else begin + let h_name = "FLIGHT_PLAN_H" in + printf "/* This file has been generated from %s */\n" !xml_file; + printf "/* Please DO NOT EDIT */\n\n"; + + printf "#ifndef %s\n" h_name; + Xml2h.define h_name ""; + printf "\n"; + + + let name = ExtXml.attrib xml "name" in + Xml2h.warning ("FLIGHT PLAN: "^name); + Xml2h.define_string "FLIGHT_PLAN_NAME" name; + + let get_float = fun x -> float_attrib xml x in + let lat0_deg = get_float "lat0" + and lon0_deg = get_float "lon0" + and qfu = get_float "qfu" + and mdfh = get_float "max_dist_from_home" + and alt = ExtXml.attrib xml "alt" in + security_height := get_float "security_height"; + ground_alt := get_float "ground_alt"; + + check_altitude (float_of_string alt) xml; + + let utm0 = utm_of WGS84 {posn_lat=(Deg>>Rad)lat0_deg;posn_long=(Deg>>Rad)lon0_deg } in + let rel_utm_of_wgs84 = fun wgs84 -> + let utm = utm_of WGS84 wgs84 in + (utm.utm_x -. utm0.utm_x, utm.utm_y -. utm0.utm_y) in + + Xml2h.define "NAV_UTM_EAST0" (sprintf "%.0f" utm0.utm_x); + Xml2h.define "NAV_UTM_NORTH0" (sprintf "%.0f" utm0.utm_y); + Xml2h.define "QFU" (sprintf "%.1f" qfu); + + + let waypoints = Xml.children waypoints in + let (hx, hy) = define_home waypoints in + List.iter (check_distance (hx, hy) mdfh) waypoints; + + Xml2h.define "WAYPOINTS" "{ \\"; + List.iter (print_waypoint rel_utm_of_wgs84 alt) waypoints; + lprintf "};\n"; + Xml2h.define "NB_WAYPOINT" (string_of_int (List.length waypoints)); + + Xml2h.define "GROUND_ALT" (string_of_float !ground_alt); + Xml2h.define "SECURITY_ALT" (string_of_float (!security_height +. !ground_alt)); + Xml2h.define "MAX_DIST_FROM_HOME" (string_of_float mdfh); + + let index_of_waypoints = + let i = ref (-1) in + List.map (fun w -> incr i; (name_of w, !i)) waypoints in + + print_blocks index_of_waypoints blocks; + + Xml2h.finish h_name + end + with + Xml.Error e -> prerr_endline (Xml.error e); exit 1 + | Dtd.Prove_error e -> prerr_endline (Dtd.prove_error e); exit 1 diff --git a/sw/tools/gen_messages.ml b/sw/tools/gen_messages.ml new file mode 100644 index 00000000000..d0e61e25b93 --- /dev/null +++ b/sw/tools/gen_messages.ml @@ -0,0 +1,254 @@ +(* + * $Id$ + * + * XML preprocessing for downlink protocol + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf + + +module Syntax = struct + type format = string + + type type_name = string + + type _type = Basic of string | Array of string * int + + type field = _type * string * format option + + type fields = field list + + type message = { name : string ; id : int; period : float option; fields : fields } + + type messages = message list + + let lineno = ref 1 (* For syntax error messages *) + + let assoc_types t = + try + List.assoc t Pprz.types + with + Not_found -> fprintf stderr "Error: '%s' unknown type\n" t; exit 1 + + let rec sizeof = function + Basic t -> (assoc_types t).Pprz.size + | Array (t, i) -> i * sizeof (Basic t) + let glibof = fun t -> (assoc_types t).Pprz.glib_type + let formatof = fun t -> (assoc_types t).Pprz.format + + let print_format t = function + None -> printf "(%s)" (formatof t) + | Some f -> printf "(%s)" f + + let print_field = fun (t, s, f) -> + begin + match t with + Basic t -> printf "%s %s " t s; print_format t f + | Array(t, i) -> printf "%s %s[%d] " t s i; print_format t f + end; printf "\n" + + let print_message = fun (s, fields) -> + printf "%s {\n" s; + List.iter print_field fields; + printf "}\n" + + let print_messages = List.iter print_message + + open Xml + + let xml_error s = failwith ("Bad XML tag: "^s^ " expected") + + let fprint_fields = fun f l -> + fprintf f "<"; + List.iter (fun (a, b) -> fprintf f "%s=\"%s\" " a b) l; + fprintf f ">" + + + let assoc_or_fail x l = + let x = String.uppercase x + and l = List.map (fun (a, v) -> String.uppercase a, v) l in + try + List.assoc x l + with + Not_found -> + fprintf stderr "Error: Field '%s' expected in <%a>" x fprint_fields l; + exit 1 + + let of_xml = function + Element ("message", fields, l) -> + let name = assoc_or_fail "name" fields + and id = int_of_string (assoc_or_fail "id" fields) in + { id=id; name = name; + period = (try Some (float_of_string (List.assoc "period" fields)) with Not_found -> None); + fields=List.map (function + Element ("field", fields, []) -> + let id = assoc_or_fail "name" fields + and type_name = assoc_or_fail "type" fields + and fmt = try Some (List.assoc "format" fields) with _ -> None in + let _type = try Array(type_name, int_of_string (List.assoc "len" fields)) with Not_found -> Basic type_name in + + (_type, id, fmt) + | _ -> xml_error "field") + l} + | _ -> xml_error "message with id" + + + let read filename class_ = + let xml = + try Xml.parse_file filename with + Xml.Error (msg, pos) -> fprintf stderr "%s:%d : %s\n" filename (Xml.line pos) (Xml.error_msg msg); exit 1 + in + match List.filter (fun x -> assert(Xml.tag x="class"); Xml.attrib x "name" = class_) (Xml.children xml) with + [xml_class] -> List.map of_xml (Xml.children xml_class) + | [] -> failwith (sprintf "No class '%s' found" class_) + | _ -> failwith (sprintf "Several class '%s' found" class_) +end + +module Gen_onboard = struct + open Printf + open Syntax + + let print_avr_field = fun avr_h (t, name, (_f:format option)) -> + match t with + Basic _ -> + fprintf avr_h "\t MODEM_PUT_%d_BYTE_BY_ADDR((uint8_t*)(%s)); \\\n" (sizeof t) name + | Array (t, i) -> + let s = sizeof (Basic t) in + fprintf avr_h "\t {\\\n\t int i;\\\n\t for(i = 0; i < %d; i++) {\\\n" i; + fprintf avr_h "\t MODEM_PUT_%d_BYTE_BY_ADDR((uint8_t*)(&%s[i])); \\\n" s name; + fprintf avr_h "\t }\\\n"; + fprintf avr_h "\t }\\\n" + + let print_avr_macro_names avr_h = function + [] -> () + | (_, s, _)::fields -> + fprintf avr_h "%s" s; List.iter (fun (_, s, _) -> fprintf avr_h ", %s" s) fields + + let rec size_fields = fun fields size -> + match fields with + [] -> size + 4 + | (t, _, _)::fields -> size_fields fields (size + sizeof(t)) + + let size_of_message = fun m -> size_fields m.fields 0 + + let print_avr_macro = fun avr_h {name=s; fields = fields} -> + fprintf avr_h "#define DOWNLINK_SEND_%s(" s; + print_avr_macro_names avr_h fields; + fprintf avr_h "){ \\\n"; + fprintf avr_h "\tif (MODEM_CHECK_FREE_SPACE(%d)) {\\\n" (size_fields fields 0); + fprintf avr_h "\t ModemStartMessage(DL_%s) \\\n" s; + List.iter (print_avr_field avr_h) fields; + fprintf avr_h "\t ModemEndMessage() \\\n"; + fprintf avr_h "\t} \\\n"; + fprintf avr_h "\telse \\\n"; + fprintf avr_h "\t modem_nb_ovrn++; \\\n"; + fprintf avr_h "}\n\n" + + + + let print_enum = fun avr_h messages -> + List.iter (fun m -> fprintf avr_h "#define DL_%s %d\n" m.name m.id) messages; + fprintf avr_h "#define DL_MSG_NB %d\n\n" (List.length messages) + + let print_avr_macros = fun filename avr_h messages -> + print_enum avr_h messages; + List.iter (print_avr_macro avr_h) messages; + let md5sum = Digest.file filename in + fprintf avr_h "#define MESSAGES_MD5SUM \""; + for i = 0 to String.length md5sum - 1 do + fprintf avr_h "\\%03o" (Char.code md5sum.[i]) + done; + fprintf avr_h "\"\n" + + let freq = 10 + let buffer_length = 5 + let step = 1. /. float freq + let nb_steps = (256 / freq) * freq + + let is_periodic = fun m -> m.period <> None + let period_of = fun m -> + match m.period with Some p -> p | None -> failwith "period_of" + let morefrequent = fun m1 m2 -> compare (period_of m1) (period_of m2) + + let gen_periodic = fun avr_h messages -> + let periodic_messages = List.filter is_periodic messages in + let periodic_messages = List.sort morefrequent periodic_messages in + + let load = Array.create nb_steps 0 in + let buffer_load = Array.create nb_steps 0 in + + let scheduled_messages = + List.map + (fun m -> + let p = period_of m in + let period_steps = truncate (p /. step) in + let start_step = ref 0 in + for i = 1 to period_steps - 1 do + if (load.(i), buffer_load.(i)) < (load.(!start_step), buffer_load.(!start_step)) then start_step := i + done; + let s = size_of_message m in + for j = 0 to nb_steps/period_steps - 1 do + let i = !start_step+j*period_steps in + load.(i) <- load.(i) + s; + for k = i to i + buffer_length - 1 do + let k = (k + nb_steps) mod nb_steps in + buffer_load.(k) <- buffer_load.(k) + s + done + done; + (!start_step, period_steps, m)) + periodic_messages in + + fprintf avr_h "// Load: intant(buffer)"; + for i = 0 to nb_steps - 1 do + fprintf avr_h " %d(%d)" load.(i) buffer_load.(i) + done; + fprintf avr_h "\n"; + + fprintf avr_h "#define PeriodicSend() { /* %dHz */ \\\n" freq; + fprintf avr_h " static uint8_t i;\\\n"; + fprintf avr_h " i++; if (i == %d) i = 0;\\\n" nb_steps; + List.iter + (fun (s, p, m) -> + fprintf avr_h " if (i %% %d == %d) PERIODIC_SEND_%s();\\\n" p s m.name) + scheduled_messages; + fprintf avr_h "}\n" + + + +end + +let _ = + if Array.length Sys.argv <> 3 then begin + fprintf stderr "Usage: %s <.xml file> " Sys.argv.(0) + end; + let filename = Sys.argv.(1) in + let class_name = Sys.argv.(2) in + let base = Filename.basename (Filename.chop_extension filename) ^ class_name in + + let messages = Syntax.read filename class_name in + + let avr_h = stdout in + Printf.fprintf avr_h "/* Automatically generated from %s */\n" filename; + Printf.fprintf avr_h "/* Please DO NOT EDIT */\n"; + Gen_onboard.print_avr_macros filename avr_h messages; + Gen_onboard.gen_periodic avr_h messages diff --git a/sw/tools/gen_radio.ml b/sw/tools/gen_radio.ml new file mode 100644 index 00000000000..e296c13ffcc --- /dev/null +++ b/sw/tools/gen_radio.ml @@ -0,0 +1,88 @@ +(* + * $Id$ + * + * XML preprocessing for radio-control parameters + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf +open Xml2h + +let h_name = "RADIO_H" + +let fos = float_of_string + +type channel = { min : string; max : string; neutral : string; averaged : string } + +let default_neutral = "1600" +let default_min = "1000" +let default_max = "2200" + +let parse_channel = + let no_channel = ref 0 in + fun c -> + let ctl = "RADIO_CTL_"^ExtXml.attrib c "ctl" + and fct = "RADIO_" ^ ExtXml.attrib c "function" in + define ctl (string_of_int !no_channel); + define fct ctl; + no_channel := !no_channel + 1; + { min = ExtXml.attrib_or_default c "min" default_min; + neutral = ExtXml.attrib_or_default c "neutral" default_neutral; + max = ExtXml.attrib_or_default c "max" default_max; + averaged = ExtXml.attrib_or_default c "average" "0" + } + + +let _ = + if Array.length Sys.argv < 2 then + failwith "Usage: gen_radio xml_file"; + let xml_file = Sys.argv.(1) in + let xml = Xml.parse_file xml_file in + + printf "/* This file has been generated from %s */\n" xml_file; + printf "/* Please DO NOT EDIT */\n\n"; + printf "#ifndef %s\n" h_name; + define h_name ""; + nl (); + let channels = Xml.children xml in + let n = ExtXml.attrib xml "name" in + Xml2h.warning ("RADIO MODEL: "^n); + define_string "RADIO_NAME" n; + nl (); + define "RADIO_CTL_NB" (string_of_int (List.length channels)); + nl (); + + (* For compatibility *) + define "PPM_PULSE_NEUTRAL_US" default_neutral; + nl (); + let channels_params = List.map parse_channel channels in + nl (); + define "RADIO_MINS_US" (sprint_float_array (List.map (fun x -> x.min) channels_params)); + define "RADIO_NEUTRALS_US" (sprint_float_array (List.map (fun x -> x.neutral) channels_params)); + define "RADIO_MAXS_US" (sprint_float_array (List.map (fun x -> x.max) channels_params)); + define "RADIO_AVERAGED" (sprint_float_array (List.map (fun x -> x.averaged) channels_params)); + + nl (); + define "AveragedChannel(ch)" "(((int[])RADIO_AVERAGED)[ch])"; + + printf "\n#endif // %s\n" h_name + diff --git a/sw/tools/gen_ubx.ml b/sw/tools/gen_ubx.ml new file mode 100644 index 00000000000..4251a770070 --- /dev/null +++ b/sw/tools/gen_ubx.ml @@ -0,0 +1,126 @@ +(* + * $Id$ + * + * XML preprocessing for UBX protocol + * + * Copyright (C) 2003 Pascal Brisset, Antoine Drouin + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + *) + +open Printf + +let out = stdout + +let sizeof = function + "U4" | "I4" -> 4 + | "U2" | "I2" -> 2 + | "U1" | "I1" -> 1 + | x -> failwith (sprintf "sizeof: unknown format '%s'" x) + +let (+=) = fun r x -> r := !r + x + +let get_at = fun offset format block_size -> + let t = + match format with + "I2" -> "int16_t" + | "I4" -> "int32_t" + | "U2" -> "uint16_t" + | "U4" -> "uint32_t" + | "U1" -> "uint8_t" + | "I1" -> "int8_t" + | _ -> failwith (sprintf "get_at: unknown format '%s'" format) in + let block_offset = + if block_size = 0 then "" else sprintf "+%d*_ubx_block" block_size in + sprintf "(*((%s*)(_ubx_payload+%d%s)))" t offset block_offset + +let define = fun x y -> + fprintf out "#define %s %s\n" x y + +exception Length_error of Xml.xml*int*int + + +let parse_class = fun c -> + let class_id = int_of_string (Xml.attrib c "id") + and class_name = Xml.attrib c "name" in + + fprintf out "\n"; + define (sprintf "UBX_%s_ID" class_name) (Xml.attrib c "ID"); + + let parse_message = fun m -> + let msg_name = Xml.attrib m "name" in + + fprintf out "\n"; + define (sprintf "UBX_%s_%s_ID" class_name msg_name) (Xml.attrib m "ID"); + + let offset = ref 0 in + let rec parse_field = fun block_size f -> + match Xml.tag f with + "field" -> + let field_name = Xml.attrib f "name" + and format = Xml.attrib f "format" in + let block_no = if block_size = 0 then "" else ",_ubx_block" in + define (sprintf "UBX_%s_%s_%s(_ubx_payload%s)" class_name msg_name field_name block_no) (get_at !offset format block_size); + offset += sizeof format + | "block" -> + let s = int_of_string (Xml.attrib f "length") in + let o = !offset in + List.iter (parse_field s) (Xml.children f); + let s' = !offset - o in + if s <> s' then raise (Length_error (f, s, s')) + | x -> failwith ("Unexpected field: " ^ x) + in + + List.iter (parse_field 0) (Xml.children m); + try + let l = int_of_string (Xml.attrib m "length") in + if l <> !offset then raise (Length_error (m, l, !offset)) + with + Xml.No_attribute("length") -> () + in + + + List.iter parse_message (Xml.children c) + + +let _ = + let xml_file = Sys.argv.(1) in + try + let xml = Xml.parse_file xml_file in + fprintf out "/* Generated from %s */\n" xml_file; + fprintf out "/* Please DO NOT EDIT */\n\n"; + + define "UBX_SYNC1" "0xB5"; + define "UBX_SYNC2" "0x62"; + + List.iter parse_class (Xml.children xml) + with + Xml.Error (em, ep) -> + let l = Xml.line ep + and c1, c2 = Xml.range ep in + fprintf stderr "File \"%s\", line %d, characters %d-%d:\n" xml_file l c1 c2; + fprintf stderr "%s\n" (Xml.error_msg em); + exit 1 + | Length_error (m, l1, l2) -> + fprintf stderr "File \"%s\", inconsistent length: %d expected, %d found from fields in message:\n %s\n" xml_file l1 l2 (Xml.to_string_fmt m); + exit 1 + | Dtd.Check_error e -> + fprintf stderr "File \"%s\", DTD check error: %s\n" xml_file (Dtd.check_error e) + | Dtd.Prove_error e -> + fprintf stderr "\nFile \"%s\", DTD check error: %s\n\n" xml_file (Dtd.prove_error e)

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 literal 0 HcmV?d00001 diff --git a/data/pictures/penguin_logo.gif b/data/pictures/penguin_logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..2c655c1cb37963ae9967d45e831067058e629642 GIT binary patch literal 37842 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