pax_global_header00006660000000000000000000000064141516462640014523gustar00rootroot0000000000000052 comment=b1ef680dbf71edefbbef63125acc754df46dc499 wfview-1.2d/000077500000000000000000000000001415164626400130405ustar00rootroot00000000000000wfview-1.2d/.gitignore000066400000000000000000000000561415164626400150310ustar00rootroot00000000000000*.pro.user .vs .qmake.stash debug release ui_*wfview-1.2d/.gitmodules000066400000000000000000000000001415164626400152030ustar00rootroot00000000000000wfview-1.2d/CHANGELOG000066400000000000000000000745561415164626400142730ustar00rootroot00000000000000# CHANGELOG - 20211022 Don't block until audio buffer has space Bit of tidying - 20211020 Tidy-up server shutdown Trying to find cause of lockup when client disappears - 20211006 Send TX/Freq changes multiple times with rigctld bumped to 1.2d hopefully last testversion before 1.20 - 202109022 Remove duplicate setPriority() Fix typo Add keepalive for linux/mac pty Fix alignment of rigname in taskbar Only send RX antenna byte to rig when it has an RX antenna option in rigCaps - 20210907 Make rigctld state work for USB connected rigs - 20210831 added 25 kHz step for tuning - 20210829 Experimental support for split mode in rigctld Ignore control levels that we don't currently support Add better detection of ci-v transceive disable - 20210827 Add saving of meter2 state - 20210823 Set audio thread priority in the correct place! Now with dual meters for everyone! - 20210820 Clear Center readout as well. Fixed issue where the "none" selection didn't work quite right. Also fixed the T/R meter switching to clear out invalid readings. Set audio threads to be realtime priority - 20210817 dual meter support/WHATSNEW Current scale tic marks on both sides now! More meter work, fixed some scales and added labels. Better meter fonts - 20210815 Fix for left over CIV client on server Improve detection of unsupported codec. - 20210814 Fix for Opus TX audio txrate logging wrong More warning removal! More commenting in rigctld.h Is this going to fix 8 bit audio? Comment unused structs from rigctld All audio codecs now work in both directions! Update audiohandler.cpp more 8bit fixes! ulaw encoding test 8 bit encoding fix Another issue with 8 bit audio Try again to remove debugging! Revert "Remove extra opus debugging" This reverts commit da53f5371bbeb35b10cbb831b83b74e1bdd771f9. Remove extra opus debugging Move opus init Add some debugging for decoder Use radio samplerate for opus Remove big/little endian conversion More Opus fixes Update audiohandler.cpp Try to fix Opus 2-channel - 20210809 Add constants to make parsing (hopefully) easier - 20210808 Fake known functions Fix float warning Remove calibration debugging Add proper s-meter calibration - 20210807 Add ritctl model to rigCaps Fix to make wsjt-x work again! Add split/duplex support Update rigctld.cpp Correct lack of parentheses in conditionals Fix typo Remove some debug logging More rigctl features/fixes - 20210806 Fix for get_powerstat Update rigctld.cpp Add some levels and other functions Fix compile warnings Add frequency ranges from rigcaps - 20210806 Move rigctld settings in Ui Fixes for setting freq/mode Support for more rigctld commands More fixes to rigctld Change the way rigctl response is built More rigctld fixes - 20210804 Add rigctld config to ui and fix some bugs - 20210802 added derSuessman prefix code - 20210801 Fix broken 8bit audio added derSuessmann additions to have a linux install prefix Fedora build/install notes, see merge request eliggett/wfview!4 - 20210730 Added a little extra logic, also some cross-platform help, to the custom stylesheet loader. - 20210729 fix: set the style once added /usr/local to search path for the stylesheet - 20210726 Fixed error in IC-7410 attenuator spec. - 20210726 Fix for blank username/password in server - 20210724 small changes to INSTAll.md and addition of mint 20.2/openSUSE 15.3 - 20210720 Clear out meter values when type is switched. Allow user to turn off power-down confirmation msgbox - 20210719 wfview now uses the meter's own balistics (average and peak code). This makes it very easy to meter any parameter 0-255. Meter Type "meterNone" or other will display data in "raw" format. - 20210718 Added center tuning for IC-R8600, partially moved meter balistics (average and peak) to the meter class. Quick debug for the metering queue, just in case. 20210717 Preliminary secondary meter support. See Settings tab for selection. Some scales incomplete. Added SWR and ALC scales to the meter. Fix error in scale of power meter - 20210716 Power meter for transmit. Much work remains on this meter alone. Get antenna status on start-up and slow poll for it. Add RX antenna selection for rigs that support it - 20210714 Preferences added for Anti-Alias and Interpolate. - 20210713 Added waterfall display options: anti-alias and interpolate. Not in preferences yet. Debug button enables wf pan and zoom. - 20210711 Allow user to select whether to confirm exit or not - 20210709 Reset PTT timer for control-R keystroke. Added a fix to keep the local frequency in-sync with any recent commands sent to the radio. Added more support for the IC-7600 in rigCaps. Added time, date, and UTC offset commands. Currently initiated by the debug button. There seems to be a bug in the 7300 where the UTC offset has one hour subtracted, ie, -7 HRS becomes -8 HRS. The hex command appears to be sent correctly. - 20210708 Delete oldest entry from UDP buffer before adding new one. If pty connected s/w sends a disable transceive command, dont' send any transceive commands to it New about box! - 20210706 Local af gain now has anti-log audio pot taper. Only start audio when stream is ready - 20210705 Added transceiver adjustment window show code, for the debug button only currently. Added window title text change to show radio model. Needs to be checked cross-platform. On Linux Mint, displays: "IC-7300 -- wfview" Applying setup.volume within audio handler if the audio handled is an output. Added this to the init function. waterfall theme is now saved. Added local af gain and wf length to the preferences. - 20210702 fixed small error where the tx latency was not copied in the UI - 20210626 Merge branch 'sequence' of gitlab.com:eliggett/wfview into sequence Duplicate of existing command. Remove unnecessary escape sequence Fix for fix of missing __PRETTY_FUNCTION__ Add __PRETTY_FUNCTION__ for compilers that don't have it. - 20210625 Mode changes from the combo box now use the que. There are still other methods to change mode which will transition shortly. Faster PTT Added PTT to the queue. Added unique priority insertion methods. Changed how commands with parameter data are added. Initial queued "set" command commit. Only the frequency set command is used so far, and only for the "Frequency" tab and the tuning knob. - 20210624 Quick hack for WFM forcing FIL1 always - 20210621 Added polling button Moving to std::deque (double-ended que). - 20210620 IC-R8600 span is now received into the UI correctly. New unified outgoing command queue. Tested on IC-9700 and IC-718 (to remote wfview server). CPU usage seems higher but please check your system. Timing seems to be acceptable but could probably use some tweaks. S-meter polling is 25ms for fast radios, and slower rates for slower radios. Half-duplex serial radios receive 3x slower polling to make room for replies. For Freq, Mode, etc "regular" constant polling (new feature): IC-9700 polling is 5 per second, IC-718 is 1-2 per second. Just helps keep the UI in sync with changes taking place at the rig. The polling is slow enough that it doesn't impact anything. But quick enough that it catches discrepencies pretty quickly. - 20210619 Added a few more slider things whatsnew: improved IC-R8600 - 20210618 Additional support for the IC-R8600, including wider scope spans. Minor change to remove some old debug code that snuck in. If no rig caps, then don't mess with the window! Added full duplex comms parameter to rigCaps. We assume half-duplex until we receive a reply to rigID. Fixed accidental s-meter timing parameter change. - 20210617 Radios without spectrum do not show spectrum, and, the window properly resizes for those controls. Also, a new key command, control-shift-d has been added to run debug functions from any tab in the program. - 20210615 Additional code to hide/show spectrum and correcting an issue with the rig name not populating for non-spectrum radios. Dynamic show/hide spectrum for rigs without this feature. Additional data corruption checking. - 20210614 Changed collision detection code so that we can more easily see what message was missed. Added collision detection for serial commands. Collisions are aparently frequent for true 1-wire CI-V radios. We now calculate polling rates immediately upon receiveCommReady for serial connections. For network connections, we assume sane values and modify once we receive the baud rate from the server. Add Neon (ARM) support to resampler Revert to using resampler directory rather than opus-tools submodule - 20210612 Add tooltip showing percentage of TX power when slider is moved - 20210611 adding a second path/way for the qcustomplot link if the first fails Update udpserver.cpp Use global watchdog rather than per-connection Report when users are disconnected by the watchdog Use watchdog to cleanup lost server connections Fix crash on disconnect Make status update after disconnection More server disconnection cleanup Improve server disconnection/cleanup - 20210610 remove spaces when adding extra server users Update udpserver.cpp Allow both encoded and plain text passwords Lots of fixes to server setup table Hopefully fix the occasional 0xe1 packet from hitting the pty Add more info for server connections Make sure that user is authenticated before allowing CIV/Audio Use correct location for statusupdate! Indicate when TX is not available Show server connection status in taskbar (only for USB connected rigs) - 20210609 Allow sender or receiver to be 0xe1 in server Always forward wfview traffic to wfview clients - 20210608 Truncate wfview.log on open Detect radio baudrate in server mode Comment out rtaudio if not being used Baud rate calculations are now only happening when baud rate is received and reasonable. - 20210607 Check that we have at least 1 audio channel available. Improve audio cleanup Add extra debugging for UDP server CIV Make only MacOS use buffered audio Improve mac audio - 20210606 Fix TX Audio on Linux Various fixes to udpserver Make QTMultimedia default Fix to allow rtaudio to compile again - 20210605 Add latency check to TX audio Fix incorrect use of latency setting - 20210604 Stop silly compile warning Change udpserver to use new audiosetup struct properly. Fix audio device selection Fix for txaudio Add QtMultimedia as default audio - 20210603 Hopefully fix hang on exit when trying to close audio thread. - 20210602 Use heap based rtaudio for enumeration Catch possible exception when closing non-existent stream Fix mac audio Fix mac crash on closing audio Make sure audio is deleted in destructor Force specific rtaudio API depending on platform. Linux now uses librtuadio-dev where available Removing local rtaudio code and using library instead. revert to ALSA for now Tell rtaudio not to hog device Update audiohandler.cpp Select chunksize based on native sample rate Force 48K sample rate when 44100 is detected. change linux compiler directive to allow new-aligned Tidy up server mutexes Fix server high CPU on disconnect - 20210601 Stop deleting audio after last client disconects Change udpserver packet handling to be similar to udphandler Update udpserver.cpp Let buffer keep filling. Change to 10ms poll time for server rx audio Mutex work in udpserver Fix for crash when remote requests tone. - 20210531 IC-7700 support Open pty non-blocking Fix for crashing pty on mac Fix compile issue after merge - 20210530 Keep the theme during resize. TODO: preference for wf theme Removing my own uninformed sidenote. Waterfal length may now be adjusted. Let's see what range of length seems good and limit the control accordingly. Also there may be a memory leak in the prepareWf() function where the colormap is created when the image is resized. CIV may now be changed as-needed while running. Remove various compiler warnings and tidy up. add silence if audio has stopped - 20210529 fix for mac/linux compiler Detect number of device channels and convert accordingly Lots more changes for rtaudio compatibility Small change to show default audio devices - 20210528 More chair movements. More arranging of the chairs. Also fixed a minor bug that prevented the "Manual" serial device entry on my system. Cleaning up the main constructor for wfmain. Add some startup logging Update audiohandler.cpp Update udphandler.cpp Change toolbar display formatting Use preferred sample rate rather than force 48000 - 20210527 Allow higher latency udpserver fixes Update udpserver.cpp Fix for tx audio channels Add tx audio add asound lib to linux build fix qmake file Use ring buffer with rtaudio to eliminate mutexes - 20210525 Update INSTALL_PREBUILT_BINARY.md move ulaw to dedicated header add rtaudio as submodule add opus-tools as submodule Add mutex for audio buffer - 20210523 Fixes for linux build First working rtaudio (output only) Link can now be clicked. Added helpful text to settings tab. Allow entry to Server Setup for either radio connection type. Minor change to clarify roll of Server Setup - 20210522 Add debugging and fix silly error in audiooutput combobox reenable audio buffers Attempt to fix crash Make only first client have TX audio Stop audiohandler re-enumerating devices on connect. Stop preamps/attenuators lists growing every time we reconnect. Try increasing audio device buffer size - 20210521 Changed method for adding modes to rigs and populating the rig menu. This should be easier to maintain and better in the long run. "Hopefully" fix annoying UDP server crash on client disconnect! Fix for TX audio in udp server Fix for stuttering audio on mac Fixed missing break in switchs. Typo in message about CI-V Dynamic timing update for all baud rates and connection types. Fixed support for 9600 baud and lower speeds. - 20210521 Add baud rate detection for remote rigs Correct propCIVAddr to work if less than 0xe0 Change audiohandler to use latency for tx audio - 20210520 Added IC-756 Pro. Tested UI and back-end response with 7300 and fake RigID reply. Added additional support for the IC-756 Pro III. Essentially untested. Cleaned up warning and UI help text. Add more features to rigstate - 20210519 Model ID text format fixed. Shows IC-0x followed by the raw rig ID received. Fixed issue where unknown rigs were identified as 0xff. All rigs are now identified using rigCaps.modelID, populated with the raw response from the Rig ID query. Enumerations of known rig types continue to fall into rigCaps.model, and unknwn rigs will match to rigCaps.model=modelUnknown, as before. Serial baud rate is in the UI now. Added some enable/disable code to prevent confusion about which options can be used with which types of connections. Better about box. fixed filename in instructions Add MacOS serial port entitlement - 20210518 Remove unused variables Make windows build include git version Command line version of git must be available in the path. Various file tidying for Windows/Mac builds Flush buffers if too many lost packets. remove duplicate audioPacket metatype Fix silly error in udpserver audio Allow receive only in udpHandler Manual CIV is now read from the preferences and populates the UI accordingly. Changed UI a little, and added manual CI-V options. Seems to work well. - 20210517 Fixes for MacOS build - sandbox Fixes for mac logging/pty since adding sandbox Make audio input buffer a qMap Use qMap instead of qVector for buffers as they are auto-sorted. - 20210516 Fixed attenuator on IC-7000 register audio metatype in wfmain with all of the others Move manual serial port so linux/mac only - 20210516 Added IC-7200 support. This has not been tested. - 20210515 Change to correct bug when CI-V is user-specified and rigCaps were never populated. Making the s-meter error command easier to read. Added IC-706 to rigCaps. Support for the IC-706 is currently broken but development is in progress. Added check for if the rig has spectrum during initial state queries. BSR debug code. Small fixes for rigctld - 20210514 Make UDP port textboxes expanding. Selecting an antenna now sets that antenna for TX and RX. wfview now closes when the main window is closed. Filter selection now checks for data mode. Preliminary IC-7000 support. TODO: Input selection, modes, filters, reported bug with frequency input and s-meter. Moved the power buttons. Cyan for the tuning line. Resize UDP port text boxes (again!) ake UDP port textboxes expanding. Fixes to UDP server Hopefully improve stability of pty by filtering traffic for any other CIV id. - 20210513 Additional search path for macports compatibility (macOS only). Slower polling for older rigs using lower serial baud. Fix CI-V packet length bug in udphandler Set pty serial port to NULL by default - 20210512 Fix for crash on MacOS - 20210511 Fixes for virtual serial port in Windows. Initial commit of pty rewrite There is a new combobox in Settings which shows the file that the pty device is mapped to. You can select from ~/rig-pty[1-8] which should work if you have multiple instances of wfview. - 20210509 Enhanced accessibility for the UI. - 20210508 Data Mode now sends the currently selected filter. Removed -march=native compiler and linker flags. This should make the binary builds more compatible, and also eliminate an issue on Pop! OS, where the default optimizations clashed. Preliminary IC910H support - 20210507 Added serial port iterators for the IC-7610 and IC-R8600. Untested. removing debug info we didn't need. Adding /dev/ to discovered serial devices, also ignoring case on "auto" for serial port device. wfview's own memory system now tracks mode correctly. *however*, it needs work: It should track the selected filter, since this information is generally available and useful, and it should also be storing the frequencies in Hz. I am also not sure how well the stored memory mode specification will work across multiple rigs. Added more mode shortcuts: A = airband, $ = 4 meter band (shift-4), W = WFM, V = 2M, U = 70cm, Shift-S = 23cm (S-band) Fixed BSR mode and filter selection. The band stacking register now uses the newer integer frequency parsing. We also communicate a little slower to the serial rigs, which seems more reliable. Updater serialDeviceListCombo with current COM/tty port setting pttyHandler doesn't use pty parameter in Linux so set to Q_UNUSED to eliminate warning - 20210506 Force DTR/RTS to false to stop rigs going into TX if USB Send is enabled Convert project to 32bit default on Windows and remove beginnings of VSPE code. - 20210505 Add code to select virtual serial port fixed a typo in mint instructions and ubuntu instructions - 20210504 fixed the display instead of rigcaps so that ant sel starts with 1 instead of 0 Fix to add a blank user line in server if no users are configured. small changes where to find the release; removed the src/build target directory requirement as the release unpacks in ./dist - 20310503 Fix for Built-in audio on MacOS - 20210501 Fixed bug 007, which allowed negative frequencies to be dialed. Double-clicking the waterfall now obeys the tuning step and rounding option in the settings. Unified frequency-type MHz to use same code as Hz. Add WFM mode for IC705 and remove duplicate WFM mode for IC-R8600 make bandType start at 0 so it doesn't overflow rigCaps.bsr Fix windows release build Changed the organization and domain to wfview and wfview.org. Added more modes for the IC-R8600. - 20210430 Different timing on command polling for serial rigs. - 20210427 Minor changes to hide sat button until we have time to work on that feature. Additional bands added, Airband, WFM added 630/2200m for 705; probably like the 7300 hardly useable for TX though. Also added auomatic switching to the view pane after non-BSR bands are selected added 630/2200m to 7300/7610/785x derp: fixed main dial freq display for non-BSR 4m band; see also previous commit fixed main dial freq display for non-BSR bands 60, 630, 2200m if such a band was selected in the band select menu Minor change to set frequency, which was lacking "emit" at the start. changed the modeSelectCombo-addItem sequence a bit to have modes grouped; CW and CW-R are now grouped, as well as RTTY/RTTY-R; AM and FM are grouped now - 20210426 Well, that was fun. Color preferences now work, and colors can be modified from the preference file for custom plot colors. The preference file now stores colors as unsigned int. To convert the colors in the file to standard AARRGGBB format (AA = Alpha channel), use python's hex() function. Maybe one day we will fix the qt bug and make this save in a better format. Added dynamic band buttons. Fixed multiple bugs related to various differences in band stacking register addresses (for example, the GEN band on the 705 has a different address from the 7100 and the 7300). This code was only tested with the 9700. started rough docs for the usermanual - 20210425 More work on rigctld Faster polling. Clarified in comments around line 309 in wfmain.cpp. Added ability to read RIT status. Added RIT to initial rig query. Added Added ability to read RIT status. Added RIT to initial rig query. Added variables to handle delayed command timing values. Fixed bug in frequency parsing code that had existed for some time. Changed tic marks on RIT knob because I wanted more of them. Bumped startup initial rig state queries to 100ms. May consider faster queries on startup since we are going to need more things queried on startup. - 20210424 Receiver Incremental Tuning is in. The UI does not check the rig's initial state yet, but the functions are partially in rigCommander for that purpose. - 20210423 Found two missing defaults in switch cases inside rigcommander.cpp. Modified rig power management to stop command ques (s-meter and others). Upon rig power up, the command queue is repurposed as a 3 second delay for bootup, and then, commands are issued to restore the spectrum display (if the wf checkbox was checked). I've made new functions, powerRigOff and powerRigOn, for these purposes. work in progress on spectrum enable after power off/on. powerOff should work now. - 20210420 rigctl working (sort of) with WSJT-X - 20210419 Initial commit of rigctld (doesn't currently do anything useful!) - 20210417 Adding a default position for the frequency indicator line. Goodbye tracer - 20210416 added support info for prebuild-systems - 20210412 added airband, dPMR, lw/mw European and fast HF/70/23cm 1 MHz tuning steps - 20210411 Added ATU feature on 7610. Now grabs scope state on startup. Found bug, did not fix, in the frequency parsing code. Worked aroud it by using doubles for now. - 20210410 Preamp and attenuator are now queried on startup. Additionally, the preamp is checked when the attenuator is changed, and the attenuator is checked with the preamp is changed. This helps the user understand if the rig allows only one or the other to be active (such as the IC-9700). Added frequency line. Let's see how we like it. Add some preliminary parts of getting the attenuator, preamp, and antenna selection on startup. UI not updated yet but getting there. - 20210409 Moved ATU controls to main tab. Added waterfall theme combo box Removed buttons from attenuator preamp UI controls. Antenna selection might work, untested. Preamp code is in. Can't read the current preamp state yet but we can set it. Nicer names for preamp and attenuator menu items. - 20210408 Preamp data is now tracked (but not used yet) re-added lost tooptip for SQ slider - 20210407 Minor disconnect in the getDTCS call Attenuators are in! Please try them out! - 20210406 The repeater setup now disables elements for things your rig doesn't do. We now query the repeater-related things on startup, such that the repeater UI is populated correctly. We now have kHz as the assumed input format if there isn't a dot in the entry box. Enjoy that! Minor change so that we track the selected tone to mode changes, keeping the radio in sync with the UI. Tone, Tone Squelch, and D(T)CS seem to work as expected. Mode can be selected. - 20210405 removed 150 Hz CTCSS / NATO as it can't make that by itself added 77.0 Hz tone to CTCSS We can now read the repeater access mode and update the UI. What remains is to be able to set the mode. Working read/write of Tone, TSQL, and DTCS tones/code. Some code is also present now to change the mode being used (tone, tsql, DTCS, or some combo of the two). - 20210404 Tone, TSQL, and DTCS code added, but not complete. better tone mode names Started work on the tone mode interface. - 20210401 Added placeholders for attenuator, preamp, and antenna selection UI controls. Moved some repeater-related things out from rig commander and into a shared header. - 20210331 Adjusting signals and slots for repeater duplex. Basic duplex code copied from wfmain to the new repeatersetup window. - 20210330 Added conditional to debug on serial data write size. - 20210329 Fix crash when radio is shutdown while wfview is connected. - 20210311 Add local volume control for UDP connections. add volume control to audiohandler Small fixes to UDP server Add USB audio handling to UDP server Changed frequency parameters to (mostly) unsigned 64-bit ints. This makes all the rounding code very simple and removes many annoying lines of code designed to handle errors induced by using doubles for the frequency. - 20210310 audio resampling added / opus updates on virtual serial port fixed input combo boxes from growing fixed 4:15 rollover/disconnect issue - 20210304 Tuning steps! Still need to zero out those lower digits while scrolling if box is checked. Fix spectrum peaks as well. Fix spectrum 'flattening' with strong signal Add separate mutex for udp/buffers. supported rigs IC705, IC7300 (over USB, no sound), IC7610, IC785x - 20210227 changed the way udp audio traffic is handled on both the TX and RX side. - 20210226 Fixed minor bug where flock did not stop double-clicking on the spectrum. S-meter ballistics: Turned up the speed. Once you see fast meters, there's no going back. Tested at 5ms without any issues, comitting at 10ms for now. Note that 50ms in the first 'fixed' meter code is the same as 25ms now due to how the command queue is structured. So 10ms is only a bit faster than before. Fixed meter polling issue - 20210224 fixed 785x lockup Added scale and dampening/averaging to s-meter - 20210222 f-lock works now - 20210221 added working s-meter added working power meter added visual studio 2019 solution (windows builds) changed packet handling, WIP rigtype stays in view on status bar added audio input gain slider - 20210218 added SQ slider added TX power slider added TX button added simplex/duplex/auto shifts added debug logging added TX and RX codec selections added RX audio buffer size slider started adding server setup - 20210210 added control over LAN for rigs that support it has been tested on the 705, 7610, 785x, 9700. should work on 7700, 7800, other rigs. added different sampling rates for rx added different codecs added scope data for 7610, 785x. several cosmetic changes in the UI fixed a slew of bugs builds on MAC-OS, Linux, Windows added ToFixed implemented connect/disconnect rig improved USB serial code fixed memory leak redesigned rx audio path for better performance added round trip time for network connected devices added detection code of rigtype automatic detection of CIV control address added logfile capability works on a raspberry pi (USB and ethernet) wfview-1.2d/CONTRIBUTING.md000066400000000000000000000002531415164626400152710ustar00rootroot00000000000000the following people currently contribute to this Project: (alphabetical) - Elliot W6EL - Jim PA8E - Phil M0VSE - Roeland PA3MET wfview-1.2d/INSTALL.md000066400000000000000000000076231415164626400145000ustar00rootroot00000000000000# How to install wfview ### 1. Install prerequisites: (Note, some packages may have slightly different version numbers, this should be ok for minor differences.) ~~~ sudo apt-get install build-essential sudo apt-get install qt5-qmake sudo apt-get install qt5-default sudo apt-get install libqt5core5a sudo apt-get install qtbase5-dev sudo apt-get install libqt5serialport5 libqt5serialport5-dev sudo apt-get install libqt5multimedia5 sudo apt-get install libqt5multimedia5-plugins sudo apt-get install qtmultimedia5-dev sudo apt-get install git sudo apt-get install libopus-dev ~~~ Now you need to install qcustomplot. There are two versions that are commonly found in linux distros: 1.3 and 2.0. Either will work fine. If you are not sure which version your linux install comes with, simply run both commands. One will work and the other will fail, and that's fine! qcustomplot1.3 for older linux versions (Linux Mint 19.x, Ubuntu 18.04): ~~~ sudo apt-get install libqcustomplot1.3 libqcustomplot-doc libqcustomplot-dev ~~~ qcustomplot2 for newer linux versions (Linux Mint 20, Ubuntu 19, Rasbian V?, Debian 10): ~~~ sudo apt-get install libqcustomplot2.0 libqcustomplot-doc libqcustomplot-dev ~~~ optional for those that want to work on the code using the QT Creator IDE: ~~~ sudo apt-get install qtcreator qtcreator-doc ~~~ ### 2. Clone wfview to a local directory on your computer: ~~~ cd ~/Documents git clone https://gitlab.com/eliggett/wfview.git ~~~ ### 3. Create a build directory, compile, and install: If you want to change the default install path from `/usr/local` to a different prefix (e.g. `/opt`), you must call `qmake ../wfview/wfview.pro PREFIX=/opt` ~~~ mkdir build cd build qmake ../wfview/wfview.pro make -j sudo make install ~~~ ### 4. You can now launch wfview, either from the terminal or from your desktop environment. If you encounter issues using the serial port, run the following command: ~~~ if you are using the wireless 705 or any networked rig like the 7610, 7800, 785x, there is no need to use USB so below is not needed. sudo chown $USER /dev/ttyUSB* Note, on most linux systems, you just need to add your user to the dialout group, which is persistent and more secure: ~~~ sudo usermod -aG dialout $USER ~~~ (don't forget to log out and log in) ~~~ ### opensuse/sles/tumbleweed install --- install wfview on suse 15.x sles 15.x or tumbleweed; this was done on a clean install/updated OS. we need to add packages to be able to build the stuff. - sudo zypper in --type pattern devel_basis - sudo zypper in libQt5Widgets-devel libqt5-qtbase-common-devel libqt5-qtserialport-devel libQt5SerialPort5 qcustomplot-devel libqcustomplot2 libQt5PrintSupport-devel libqt5-qtmultimedia-devel lv2-devel libopus-devel optional (mainly for development specifics): get and install qt5: - wget http://download.qt.io/official_releases/online_installers/qt-unified-linux-x64-online.run - chmod +x qt-unified-linux-x64-online.run - kdesu ./qt-unified-linux-x64-online.run install Qt 5.15.2 for GCC for desktop application development when done, create the place where to build: in this case, use your homedir: - mkdir -p ~/src/build && cd src - git clone https://gitlab.com/eliggett/wfview.git - cd build - qmake-qt5 ../wfview/wfview.pro - make -j - sudo ./install.sh wfview is now installed in /usr/local/bin --- ### Fedora install ### --- Tested under Fedora 33/34. Install qt5 dependencies: - sudo dnf install qt5-qtbase-common qt5-qtbase qt5-qtbase-gui qt5-qtserialport qt5-qtmultimedia mingw64-qt5-qmake qt5-qtbase-devel qt5-qtserialport-devel qt5-qtmultimedia-devel libopus-dev Install qcustomplot: - sudo dnf install qcustomplot qcustomplot-devel When done, create a build area, clone the repo, build and install: - mkdir -p ~/src/build && cd src - git clone https://gitlab.com/eliggett/wfview.git - cd build - qmake-qt5 ../wfview/wfview.pro - make -j - sudo ./install.sh wfview is now installed in /usr/local/bin --- wfview-1.2d/INSTALL_PREBUILT_BINARY.md000066400000000000000000000067131415164626400170510ustar00rootroot00000000000000# How to install wfview without building yourself on selected linux versions We understand that downloading sources with git, selecting branches and building yourself may a bit daunting. In the future we may at some point start distributing packages and/or images like appimage, flatpack. snap. Instructions how to use this w/o building yourself. We are using a precompiled version that has been tested on a few different versions of linux in alphabetical order. Note that all are click-click-next-next-finish installs. not supported: centos7 -- no qt support debian 10 -- outdated devuan 3.1.1.1 -- outdated redhat7 -- no qt support ~~~ Debian 11 (Debian 10 is outdated) Fedora 33 Fedora 34 mint 20.1 (and up?) openSUSE 15.2 openSUSE 15.3 (see notes at the end) openSUSE Tumbleweed(s) SLES 15.x Ubuntu 20.04.2 mint 20.2 (see notes at the end) ~~~ ### for all, the following is applicable: ~~~ download the tar.gz file here: https://wfview.org/download/test-linux-build/ the file below will unpack in ./dist tar zxvf wfview-linux.tar.gz (change the filename accordingly) cd dist sudo ./install.sh ~~~ this will install the binary and a few other files to your system. Now for the system specifics; pick your version: ### Debian 11: ~~~ sudo apt install libqcustomplot2.0 libqt5multimedia5 libqt5serialport5 sudo ln -s /usr/lib/x86_64-linux-gnu/libqcustomplot.so.2.0.1 /usr/lib/x86_64-linux-gnu/libqcustomplot.so.2 wfview ~~~ ### Fedora 33/34: ~~~ sudo dnf install qcustomplot-qt5 qt5-qtmultimedia qt5-qtserialport sudo ln -s /usr/lib64/libqcustomplot-qt5.so.2 /usr/lib64/libqcustomplot.so.2 wfview ~~~ ### Mint 20.1 ~~~ sudo apt install libqcustomplot2.0 libqt5multimedia5 libqt5serialport5 sudo ln -s /usr/lib64/libqcustomplot-qt5.so.2 /usr/lib64/libqcustomplot.so.2 wfview note: if the above symlink fails, use the following line to fix the library link: sudo ln -s /lib/x86_64-linux-gnu/libqcustomplot.so.2.0.1 /lib/x86_64-linux-gnu/libqcustomplot.so.2 ~~~ ### Mint 20.2 ~~~ SEE THE NOTES AT THE END. You need wfview153 binary here sudo apt install libqcustomplot2.0 libqt5multimedia5 libqt5serialport5 sudo ln -s /usr/lib64/libqcustomplot-qt5.so.2 /usr/lib64/libqcustomplot.so.2 wfview note: if the above symlink fails, use the following line to fix the library link: sudo ln -s /lib/x86_64-linux-gnu/libqcustomplot.so.2.0.1 /lib/x86_64-linux-gnu/libqcustomplot.so.2 ~~~ ### openSUSE/Tumbleweed/SLES based on 15.2: ~~~ sudo zypper in libqcustomplot2 libQt5SerialPort5 wfview ~~~ ### openSUSE/Tumbleweed/SLES based on 15.3: ~~~ SEE THE NOTES AT THE END. You need wfview153 here sudo zypper in libqcustomplot2 libQt5SerialPort5 wfview ~~~ ### UBUNTU: ~~~ sudo apt install libqcustomplot2.0 libqt5multimedia5 libqt5serialport5 sudo ln -s /usr/lib/x86_64-linux-gnu/libqcustomplot.so.2.0.1 /usr/lib/x86_64-linux-gnu/libqcustomplot.so.2 wfview note: if the above symlink fails, use the following line to fix the library link: sudo ln -s /lib/x86_64-linux-gnu/libqcustomplot.so.2.0.1 /lib/x86_64-linux-gnu/libqcustomplot.so.2 ~~~ ### notes: ~~~ Some newer versions of mint. ubuntu, openSUSE have different kernels and such which cause wfview to segfault. For these cases we created two binaries: one for current systems ("wfview") and one for the new systems ("wfview153") So if you encounter a SEGFAULT at start: go in to the dist directory, rename wfview to wfvie152; rename wfview153 to wfview and re-execute the install.sh script ~~~ wfview-1.2d/LICENSE000066400000000000000000001045131415164626400140510ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. 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 them 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 prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. 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. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey 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; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU 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 that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. 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. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 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. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS 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 state 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) 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 3 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, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program 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, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU 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 Lesser General Public License instead of this License. But first, please read . wfview-1.2d/README.md000066400000000000000000000137361415164626400143310ustar00rootroot00000000000000# wfview [wfview](https://gitlab.com/eliggett/wfview) is an open-source front-end application for the - [Icom IC-705 ](https://www.icomamerica.com/en/products/amateur/hf/705/default.aspx) HF portable SDR Amateur Radio - [Icom IC-7300](https://www.icomamerica.com/en/products/amateur/hf/7300/default.aspx) HF SDR Amateur Radio - [Icom IC-7610](https://www.icomamerica.com/en/products/amateur/hf/7610/default.aspx) HF SDR Amateur Radio - [Icom IC-7850](https://www.icomamerica.com/en/products/amateur/hf/7850/default.aspx) HF Hybrid SDR Amateur Radio - [Icom IC-7851](https://www.icomamerica.com/en/products/amateur/hf/7851/default.aspx) HF Hybrid SDR Amateur Radio - [Icom IC-9700](https://www.icomamerica.com/en/products/amateur/hf/9700/default.aspx) VHF/UHF SDR Amateur Radio Other models to be tested/added (including the IC-705).. website - [WFVIEW](https://wfview.org/) wfview.org wfview supports viewing the spectrum display waterfall and most normal radio controls. Using wfview, the radio can be operated using the mouse, or just the keyboard (great for those with visual impairments), or even a touch screen display. The gorgous waterfall spectrum can be displayed on a monitor of any size, and can even projected onto a wall for a presentation. Even a VNC session can make use of wfview for interesting remote rig posibilities. wfview runs on humble hardware, ranging from the $35 Raspberry Pi, to laptops, to desktops. wfview is designed to run on GNU Linux, but can probably be adapted to run on other operating systems. In fact we do have working example in windows as well. wfview is unique in the radio control ecosystem in that it is free and open-source software and can take advantage of modern radio features (such as the waterfall). wfview also does not "eat the serial port", and can allow a second program, such as fldigi, access to the radio via a pseudo-terminal device. **For screenshots, documentation, User FAQ, Programmer FAQ, and more, please [see the project's wiki](https://gitlab.com/eliggett/wfview/-/wikis/home).** wfview is copyright 2017-2020 Elliott H. Liggett. All rights reserved. wfview source code is licensed via the GNU GPLv3. ### Features: 1. Plot bandscope and bandscope waterfall. Optionally, also plot a "peak hold". A splitter lets the user adjust the space used for the waterfall and bandscope plots. 2. Double-click anywhere on the bandscope or waterfall to tune the radio. 3. Entry of frequency is permitted under the "Frequency" tab. Buttons are provided for touch-screen control 4. Bandscope parameters (span and mode) are adjustable. 5. Full [keyboard](https://gitlab.com/eliggett/wfview/-/wikis/Keystrokes) and mouse control. Operate in whichever way you like. Most radio functions can be operated from a numberic keypad! This also enables those with visual impairments to use the IC-7300. 6. 100 user memories stored in plain text on the computer 7. Stylable GUI using CSS 8. pseudo-terminal device, which allows for secondary program to control the radio while wfview is running 9. works for radios that support the ethernet interface with compareable waterfall speeds as on the radio itself. ### Build Requirements: 1. gcc / g++ / make 2. qmake 3. qt5 (proably the package named "qt5-default") 4. libqt5serialport5-dev 5. libqcustomplot-dev ### Recommended: * Debian-based Linux system (Debian Linux, Linux Mint, Ubuntu, etc) or opensuse 15.x. Any recent Linux system will do though! * QT Creator for building, designing, and debugging w/gdb ### Build directions: See [INSTALL.md](https://gitlab.com/eliggett/wfview/-/blob/master/INSTALL.md) for directions. ### Rig setting: 1. CI-V Baud rate: Auto 2. CI-V address: 94h (default) 3. CI-V Transceive ON 4. CI-V USB-> REMOTE Transceive Address: 00h 5. CI-V Output (for ANT): OFF 6. CI-V USB Port: Unlink from REMOTE 7. CI-V USB Baud Rate: 15200 8. CI-V USB Echo Back: OFF 9. Turn on the bandscope on the rig screen * Note: The program currently assumes the radio is on a device like this: ~~~ /dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_IC-7300_02010092-if00-port0 ~~~ This is symlinked to a device like /dev/ttyUSB0 typically. Make sure the port is writable by your username. You can accomplish this using udev rules, or if you are in a hurry: ~~~ sudo chown `whoami` /dev/ttyUSB* ~~~ ### TODO (for developers and contributors): 1. Re-work pseudo term code into separate thread 2. Consider XML RPC to make flrig/fldigi interface easier 3. Add hide/show for additional controls: SWR, ALC, Power, S-Meter interface 4. Fix crash on close (order of delete operations is important) 5. Add support for other compatible CI-V radios (IC-706, IC-7100, IC-7610, etc) 6. Better settings panel (select serial port, CI-V address, more obvious exit button) 7. Add support for festival or other text-to-speech method using the computer (as apposed to the radio's speech module) see also the wiki: - [bugs](https://gitlab.com/eliggett/wfview/-/wikis/Bugs) - [feature requests](https://gitlab.com/eliggett/wfview/-/wikis/Feature-requests) - [raspberry pi server](https://gitlab.com/eliggett/wfview/-/wikis/raspi-server-functionality-for-7300,7100-etc) ### THIRD PARTY code/hardware: the following projects we would like to thank in alphabetical order: - ICOM for their well designed rigs see ICOM Japan (https://www.icomjapan.com/) - ICOM for their well written RS-BA1 software see ICOM JAPAN products page (https://www.icomjapan.com/lineup/options/RS-BA1_Version2/) - kappanhang which inspired us to enhance the original wfview project: Akos Marton ES1AKOS Elliot LiggettW6EL (passcode algorithm) Norbert Varga HA2NON nonoo@nonoo.hu see for their fine s/w here [kappanhang](https://github.com/nonoo/kappanhang) - resampling code from the opus project: [Xiph.Org Foundation] (https://xiph.org/) see [sources] (https://github.com/xiph/opus/tree/master/silk) - QCP: the marvellous qt custom plot code Emanuel Eichhammer see [QCP] (https://www.qcustomplot.com/) If you feel that we forgot ayone, just drop a mail. wfview-1.2d/USERMANUAL/000077500000000000000000000000001415164626400145145ustar00rootroot00000000000000wfview-1.2d/USERMANUAL/BAND000066400000000000000000000006301415164626400151420ustar00rootroot00000000000000The Band tab: The band tab reflects what bands the rig supports. It directly will switch the rig to that band and you will be switched back to the View tab the bands offered follow generally what the rig's capabilities are. Some bands will either be RX only for some people (4m/60m/630m/2200m) Band stacking gives you the opportunity to use the band stacking registers which is filled by the rig itself. wfview-1.2d/USERMANUAL/README000066400000000000000000000005511415164626400153750ustar00rootroot00000000000000I started to describe in text what all the controls are and what they actually are supposed to do. This is a very rough start of UI documentation and will beautify that of course. Initially want just to be sure that the definitions are right and everyone can edit/push it. So feel free. Note that the markers that certainly are incomplete are marked TODO. wfview-1.2d/USERMANUAL/wfview-1.0.txt000066400000000000000000000065001415164626400170610ustar00rootroot00000000000000 View: buttons spectrum scope/waterfall: spectrum mode depending on the rig you can select: center fixed scroll-center scroll-fixed spectrum span the spectrum span is active in center modes and you can select here the span you like, just as on the rig. spectrum edge here you can select the edge number as programmed in the rig. Most rigs will accept four edges. tofixed clear peaks here you can clear the peaks. Currently we only support persistent peaks and at some point we are changing that to be able to have it go away after 10 seconds, like the rig does. enable/disable wf on/off switch for the scope/waterfall wf theme Currently fixed selections how the scope and waterfall look like with colors. At som epoint we may add the ability to accept the RGB values like the rig does. mode here you can select the mode used for tx and rx. To enable data mode there is a separate button to select. Data mode switch the D mode on/off on the selected mode. receive (and tx) filter Select the predefined filter settings of the rig. Rigs also will change the TX width accordingly. transmit/receive this button alternates between TX and RX; Note that to enable TX, you need to do that first on the Settings tab enable/disable atu if your rig supports an internal ATU, you can enable disable it here. Note that we have not tested external ATU's yet. It may follow, it may not... enable/disable rit Enable the RIT function; currently no feedback on the rx shift. tune Button to initiate the ATU. Note that we have not tested external ATU's yet. It may follow, it may not... repeater setup (expand) TODO preamp You can select the preamp mode(s). Some rigs can only accept preamp OR att. attenuator you can select the rig attenuator here. Some rigs can only accept preamp OR att. antenna selection TODO controls main dial (there is no sub dial yet) by turning the dial, or using mouse wheel or clicking on the scope you can change the current frequency. The step size is below the main dial and you can select most known stepsizes there. An Flock button will effectively lock the freq to prevent accidental mis clicking/rotating mouse actions. rit dial the rit dial will effectively modify the offset in RX; useful on the VHF bands and up. Note that you need to switch on RIT for that You can use the mouse wheel to change the offset/shift. rf gain This slider controls the RF gain of the rig af gain (defaults to 100% This controls the AF Gain locally, defaults to 100% and does not increase/decrease the AF gain on the rig itself. (Else, a remotely controlled rig could make a lot of noise ;-)) sq The squelch control tx power Control that sets the power in % with the same accuracy as the rig so if your rig is 50 Watt at 47%, this slider will too. mic gain The mic gain slider sets the modulation level on the rig. scope reference level The scope reference level can be set here and TODO because need to check if this follows per band or not. ============================= Frequency On this tab you can insert a free frequency in kHz. examples: 7100 --> 7.1 kHz 430. --> 430 MHz e.g. the dot itself defines currently that you specify MHz. It will not accept anything beyond the dot. E.g. 430.125 will end up doing nothing. After entering, you will be switched back to the View tab. STO/RCL: TODO Settings wfview-1.2d/WHATSNEW000066400000000000000000000013121415164626400142200ustar00rootroot00000000000000 The following highlights are in this 1.x-release: many changes/mods/updates/enhancements to rigctld rigctld box added in the UI build process changed: you can add the install prefix (derSuessmann) added "do not ask again" for switching off rig and exiting wfview added opus as audio transport dual meter support rigctl basic split support rigctl prevents switching off civ transceive added 25 kHz step as a temporary measure sending multiple TX/FREQ change commands to the rig when we use rigctld. people should use "fake it" in wsjtx as the split code is not reliable. tidied up udp server function for better reliability wfview-1.2d/aboutbox.cpp000066400000000000000000000135271415164626400153770ustar00rootroot00000000000000#include "aboutbox.h" #include "ui_aboutbox.h" aboutbox::aboutbox(QWidget *parent) : QWidget(parent), ui(new Ui::aboutbox) { ui->setupUi(this); setWindowTitle("About wfview"); setWindowIcon(QIcon(":resources/wfview.png")); ui->logoBtn->setIcon(QIcon(":resources/wfview.png")); ui->logoBtn->setStyleSheet("Text-align:left"); ui->topText->setText("wfview version " + QString(WFVIEW_VERSION)); QString head = QString(""); QString copyright = QString("Copyright 2017-2021 Elliott H. Liggett, W6EL. All rights reserved. wfview source code is licensed under the GNU GPLv3."); QString nacode = QString("

Networking, audio, rigctl server, and much more written by Phil Taylor, M0VSE"); QString doctest = QString("

Testing, documentation, bug fixes, and development mentorship from
Roeland Jansen, PA3MET, and Jim Nijkamp, PA8E."); QString ssCredit = QString("

Stylesheet qdarkstyle used under MIT license, stored in /usr/share/wfview/stylesheets/."); QString website = QString("

Please visit https://wfview.org/ for the latest information."); QString docs = QString("

Be sure to check the User Manual and the Forum if you have any questions."); QString support = QString("

For support, please visit the official wfview support forum."); QString gitcodelink = QString("").arg(GITSHORT); QString contact = QString("
email W6EL: kilocharlie8@gmail.com"); QString buildInfo = QString("

Build " + gitcodelink + QString(GITSHORT) + "
on " + QString(__DATE__) + " at " + __TIME__ + " by " + UNAME + "@" + HOST); QString end = QString(""); // Short credit strings: QString rsCredit = QString("

Speex Resample library Copyright 2003-2008 Jean-Marc Valin"); #if defined(RTAUDIO) QString rtaudiocredit = QString("

RT Audio, from Gary P. Scavone"); #endif #if defined(PORTAUDIO) QString portaudiocredit = QString("

Port Audio, from The Port Audio Community"); #endif QString qcpcredit = QString("

The waterfall and spectrum plot graphics use QCustomPlot, from Emanuel Eichhammer"); QString qtcredit = QString("

This copy of wfview was built against Qt version %1").arg(QT_VERSION_STR); // Acknowledgement: QString wfviewcommunityack = QString("

The developers of wfview wish to thank the many contributions from the wfview community at-large, including ideas, bug reports, and fixes."); QString kappanhangack = QString("

Special thanks to Norbert Varga, and the nonoo/kappanhang team for their initial work on the OEM protocol."); QString sxcreditcopyright = QString("Speex copyright notice: \ Copyright (C) 2003 Jean-Marc Valin\n\ Redistribution and use in source and binary forms, with or without\n\ modification, are permitted provided that the following conditions\n\ are met:\n\ - Redistributions of source code must retain the above copyright\n\ notice, this list of conditions and the following disclaimer.\n\ - Redistributions in binary form must reproduce the above copyright\n\ notice, this list of conditions and the following disclaimer in the\n\ documentation and/or other materials provided with the distribution.\n\ - Neither the name of the Xiph.org Foundation nor the names of its\n\ contributors may be used to endorse or promote products derived from\n\ this software without specific prior written permission.\n\ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\ ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n\ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n\ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR\n\ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n\ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n\ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n\ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n\ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n\ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n\ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."); // String it all together: QString aboutText = head + copyright + "\n" + nacode + "\n" + doctest + wfviewcommunityack; aboutText.append(website + "\n"+ docs + support + contact +"\n"); aboutText.append("\n" + ssCredit + "\n" + rsCredit + "\n"); #if defined(RTAUDIO) aboutText.append(rtaudiocredit); #endif #if defined(PORTAUDIO) aboutText.append(portaudiocredit); #endif aboutText.append(kappanhangack + qcpcredit + qtcredit); aboutText.append("

"); aboutText.append("
" + sxcreditcopyright + "
"); aboutText.append("

"); aboutText.append(end); ui->midTextBox->setText(aboutText); ui->bottomText->setText(buildInfo); ui->midTextBox->setFocus(); } aboutbox::~aboutbox() { delete ui; } void aboutbox::on_logoBtn_clicked() { QDesktopServices::openUrl(QUrl("https://www.wfview.org/")); } wfview-1.2d/aboutbox.h000066400000000000000000000005261415164626400150370ustar00rootroot00000000000000#ifndef ABOUTBOX_H #define ABOUTBOX_H #include #include namespace Ui { class aboutbox; } class aboutbox : public QWidget { Q_OBJECT public: explicit aboutbox(QWidget *parent = 0); ~aboutbox(); private slots: void on_logoBtn_clicked(); private: Ui::aboutbox *ui; }; #endif // ABOUTBOX_H wfview-1.2d/aboutbox.ui000066400000000000000000000042511415164626400152240ustar00rootroot00000000000000 aboutbox 0 0 700 567 Form 0 128 0 0 128 128 true wfview version <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Noto Sans'; font-size:9pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Detailed text here</p></body></html> true Build String wfview-1.2d/audiohandler.cpp000066400000000000000000000676231415164626400162210ustar00rootroot00000000000000/* This class handles both RX and TX audio, each is created as a seperate instance of the class but as the setup/handling if output (RX) and input (TX) devices is so similar I have combined them. */ #include "audiohandler.h" #include "logcategories.h" #include "ulaw.h" #if defined(Q_OS_WIN) && defined(PORTAUDIO) #include #endif audioHandler::audioHandler(QObject* parent) { Q_UNUSED(parent) } audioHandler::~audioHandler() { if (isInitialized) { #if defined(RTAUDIO) try { audio->abortStream(); audio->closeStream(); } catch (RtAudioError& e) { qInfo(logAudio()) << "Error closing stream:" << aParams.deviceId << ":" << QString::fromStdString(e.getMessage()); } delete audio; #elif defined(PORTAUDIO) Pa_StopStream(audio); Pa_CloseStream(audio); #else stop(); #endif } if (ringBuf != Q_NULLPTR) { delete ringBuf; } if (resampler != Q_NULLPTR) { speex_resampler_destroy(resampler); qDebug(logAudio()) << "Resampler closed"; } if (encoder != Q_NULLPTR) { qInfo(logAudio()) << "Destroying opus encoder"; opus_encoder_destroy(encoder); } if (decoder != Q_NULLPTR) { qInfo(logAudio()) << "Destroying opus decoder"; opus_decoder_destroy(decoder); } } bool audioHandler::init(audioSetup setupIn) { if (isInitialized) { return false; } /* 0x01 uLaw 1ch 8bit 0x02 PCM 1ch 8bit 0x04 PCM 1ch 16bit 0x08 PCM 2ch 8bit 0x10 PCM 2ch 16bit 0x20 uLaw 2ch 8bit */ setup = setupIn; setup.radioChan = 1; setup.bits = 8; if (setup.codec == 0x01 || setup.codec == 0x20) { setup.ulaw = true; } if (setup.codec == 0x08 || setup.codec == 0x10 || setup.codec == 0x20 || setup.codec == 0x80) { setup.radioChan = 2; } if (setup.codec == 0x04 || setup.codec == 0x10 || setup.codec == 0x40 || setup.codec == 0x80) { setup.bits = 16; } ringBuf = new wilt::Ring(setupIn.latency / 8 + 1); // Should be customizable. tempBuf.sent = 0; if(!setup.isinput) { this->setVolume(setup.localAFgain); } #if defined(RTAUDIO) #if !defined(Q_OS_MACX) options.flags = ((!RTAUDIO_HOG_DEVICE) | (RTAUDIO_MINIMIZE_LATENCY)); #endif #if defined(Q_OS_LINUX) audio = new RtAudio(RtAudio::Api::LINUX_ALSA); #elif defined(Q_OS_WIN) audio = new RtAudio(RtAudio::Api::WINDOWS_WASAPI); #elif defined(Q_OS_MACX) audio = new RtAudio(RtAudio::Api::MACOSX_CORE); #endif if (setup.port > 0) { aParams.deviceId = setup.port; } else if (setup.isinput) { aParams.deviceId = audio->getDefaultInputDevice(); } else { aParams.deviceId = audio->getDefaultOutputDevice(); } aParams.firstChannel = 0; try { info = audio->getDeviceInfo(aParams.deviceId); } catch (RtAudioError& e) { qInfo(logAudio()) << "Device error:" << aParams.deviceId << ":" << QString::fromStdString(e.getMessage()); return isInitialized; } if (info.probed) { // Always use the "preferred" sample rate // We can always resample if needed this->nativeSampleRate = info.preferredSampleRate; // Per channel chunk size. this->chunkSize = (this->nativeSampleRate / 50); qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << QString::fromStdString(info.name) << "(" << aParams.deviceId << ") successfully probed"; if (info.nativeFormats == 0) { qInfo(logAudio()) << " No natively supported data formats!"; return false; } else { qDebug(logAudio()) << " Supported formats:" << (info.nativeFormats & RTAUDIO_SINT8 ? "8-bit int," : "") << (info.nativeFormats & RTAUDIO_SINT16 ? "16-bit int," : "") << (info.nativeFormats & RTAUDIO_SINT24 ? "24-bit int," : "") << (info.nativeFormats & RTAUDIO_SINT32 ? "32-bit int," : "") << (info.nativeFormats & RTAUDIO_FLOAT32 ? "32-bit float," : "") << (info.nativeFormats & RTAUDIO_FLOAT64 ? "64-bit float," : ""); qInfo(logAudio()) << " Preferred sample rate:" << info.preferredSampleRate; if (setup.isinput) { devChannels = info.inputChannels; } else { devChannels = info.outputChannels; } qInfo(logAudio()) << " Channels:" << devChannels; if (devChannels > 2) { devChannels = 2; } aParams.nChannels = devChannels; } qInfo(logAudio()) << " chunkSize: " << chunkSize; try { if (setup.isinput) { audio->openStream(NULL, &aParams, RTAUDIO_SINT16, this->nativeSampleRate, &this->chunkSize, &staticWrite, this, &options); } else { audio->openStream(&aParams, NULL, RTAUDIO_SINT16, this->nativeSampleRate, &this->chunkSize, &staticRead, this, &options); } audio->startStream(); isInitialized = true; qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "device successfully opened"; qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "detected latency:" << audio->getStreamLatency(); } catch (RtAudioError& e) { qInfo(logAudio()) << "Error opening:" << QString::fromStdString(e.getMessage()); } } else { qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << QString::fromStdString(info.name) << "(" << aParams.deviceId << ") could not be probed, check audio configuration!"; } #elif defined(PORTAUDIO) PaError err; #ifdef Q_OS_WIN CoInitialize(0); #endif memset(&aParams, 0,sizeof(PaStreamParameters)); if (setup.port > 0) { aParams.device = setup.port; } else if (setup.isinput) { aParams.device = Pa_GetDefaultInputDevice(); } else { aParams.device = Pa_GetDefaultOutputDevice(); } info = Pa_GetDeviceInfo(aParams.device); aParams.channelCount = 2; aParams.hostApiSpecificStreamInfo = NULL; aParams.sampleFormat = paInt16; if (setup.isinput) { aParams.suggestedLatency = info->defaultLowInputLatency; } else { aParams.suggestedLatency = info->defaultLowOutputLatency; } aParams.hostApiSpecificStreamInfo = NULL; // Always use the "preferred" sample rate (unless it is 44100) // We can always resample if needed if (info->defaultSampleRate == 44100) { this->nativeSampleRate = 48000; } else { this->nativeSampleRate = info->defaultSampleRate; } // Per channel chunk size. this->chunkSize = (this->nativeSampleRate / 50); qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << info->name << "(" << aParams.device << ") successfully probed"; if (setup.isinput) { devChannels = info->maxInputChannels; } else { devChannels = info->maxOutputChannels; } if (devChannels > 2) { devChannels = 2; } aParams.channelCount = devChannels; qInfo(logAudio()) << " Channels:" << devChannels; qInfo(logAudio()) << " chunkSize: " << chunkSize; qInfo(logAudio()) << " sampleRate: " << nativeSampleRate; if (setup.isinput) { err=Pa_OpenStream(&audio, &aParams, 0, this->nativeSampleRate, this->chunkSize, paNoFlag, &audioHandler::staticWrite, (void*)this); } else { err=Pa_OpenStream(&audio, 0, &aParams, this->nativeSampleRate, this->chunkSize, paNoFlag, &audioHandler::staticRead, (void*)this); } if (err == paNoError) { err = Pa_StartStream(audio); } if (err == paNoError) { isInitialized = true; qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "device successfully opened"; } else { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "failed to open device" << Pa_GetErrorText(err); } #else format.setSampleSize(16); format.setChannelCount(2); format.setSampleRate(INTERNAL_SAMPLE_RATE); format.setCodec("audio/pcm"); format.setByteOrder(QAudioFormat::LittleEndian); format.setSampleType(QAudioFormat::SignedInt); if (setup.port.isNull()) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "No audio device was found. You probably need to install libqt5multimedia-plugins."; return false; } else if (!setup.port.isFormatSupported(format)) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Format not supported, choosing nearest supported format - which may not work!"; format=setup.port.nearestFormat(format); } if (format.channelCount() > 2) { format.setChannelCount(2); } else if (format.channelCount() < 1) { qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "No channels found, aborting setup."; return false; } devChannels = format.channelCount(); nativeSampleRate = format.sampleRate(); // chunk size is always relative to Internal Sample Rate. this->chunkSize = (nativeSampleRate / 50); qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Internal: sample rate" << format.sampleRate() << "channel count" << format.channelCount(); // We "hopefully" now have a valid format that is supported so try connecting if (setup.isinput) { audioInput = new QAudioInput(setup.port, format, this); connect(audioInput, SIGNAL(notify()), SLOT(notified())); connect(audioInput, SIGNAL(stateChanged(QAudio::State)), SLOT(stateChanged(QAudio::State))); isInitialized = true; } else { audioOutput = new QAudioOutput(setup.port, format, this); #ifdef Q_OS_MAC audioOutput->setBufferSize(chunkSize*4); #endif connect(audioOutput, SIGNAL(notify()), SLOT(notified())); connect(audioOutput, SIGNAL(stateChanged(QAudio::State)), SLOT(stateChanged(QAudio::State))); isInitialized = true; } #endif // Setup resampler and opus if they are needed. int resample_error = 0; int opus_err = 0; if (setup.isinput) { resampler = wf_resampler_init(devChannels, nativeSampleRate, setup.samplerate, setup.resampleQuality, &resample_error); if (setup.codec == 0x40 || setup.codec == 0x80) { // Opus codec encoder = opus_encoder_create(setup.samplerate, setup.radioChan, OPUS_APPLICATION_AUDIO, &opus_err); opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(16)); opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(1)); opus_encoder_ctl(encoder, OPUS_SET_DTX(1)); opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(5)); qInfo(logAudio()) << "Creating opus encoder: " << opus_strerror(opus_err); } } else { resampler = wf_resampler_init(devChannels, setup.samplerate, this->nativeSampleRate, setup.resampleQuality, &resample_error); if (setup.codec == 0x40 || setup.codec == 0x80) { // Opus codec decoder = opus_decoder_create(setup.samplerate, setup.radioChan, &opus_err); qInfo(logAudio()) << "Creating opus decoder: " << opus_strerror(opus_err); } } unsigned int ratioNum; unsigned int ratioDen; wf_resampler_get_ratio(resampler, &ratioNum, &ratioDen); resampleRatio = static_cast(ratioDen) / ratioNum; qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "wf_resampler_init() returned: " << resample_error << " resampleRatio: " << resampleRatio; qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "thread id" << QThread::currentThreadId(); #if !defined (RTAUDIO) && !defined(PORTAUDIO) if (isInitialized) { this->start(); } #endif return isInitialized; } #if !defined (RTAUDIO) && !defined(PORTAUDIO) void audioHandler::start() { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "start() running"; if ((audioOutput == Q_NULLPTR || audioOutput->state() != QAudio::StoppedState) && (audioInput == Q_NULLPTR || audioInput->state() != QAudio::StoppedState)) { return; } if (setup.isinput) { #ifndef Q_OS_WIN this->open(QIODevice::WriteOnly); #else this->open(QIODevice::WriteOnly | QIODevice::Unbuffered); #endif audioInput->start(this); } else { #ifndef Q_OS_WIN this->open(QIODevice::ReadOnly); #else this->open(QIODevice::ReadOnly | QIODevice::Unbuffered); #endif audioOutput->start(this); } } #endif void audioHandler::setVolume(unsigned char volume) { //this->volume = (qreal)volume/255.0; this->volume = audiopot[volume]; qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "setVolume: " << volume << "(" << this->volume << ")"; } /// /// This function processes the incoming audio FROM the radio and pushes it into the playback buffer *data /// /// /// /// #if defined(RTAUDIO) int audioHandler::readData(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status) { Q_UNUSED(inputBuffer); Q_UNUSED(streamTime); if (status == RTAUDIO_OUTPUT_UNDERFLOW) qDebug(logAudio()) << "Underflow detected"; int nBytes = nFrames * devChannels * 2; // This is ALWAYS 2 bytes per sample and 2 channels quint8* buffer = (quint8*)outputBuffer; #elif defined(PORTAUDIO) int audioHandler::readData(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo * streamTime, PaStreamCallbackFlags status) { Q_UNUSED(inputBuffer); Q_UNUSED(streamTime); Q_UNUSED(status); int nBytes = nFrames * devChannels * 2; // This is ALWAYS 2 bytes per sample and 2 channels quint8* buffer = (quint8*)outputBuffer; #else qint64 audioHandler::readData(char* buffer, qint64 nBytes) { #endif // Calculate output length, always full samples int sentlen = 0; if (!isReady) { isReady = true; } if (ringBuf->size()>0) { // Output buffer is ALWAYS 16 bit. //qDebug(logAudio()) << "Read: nFrames" << nFrames << "nBytes" << nBytes; while (sentlen < nBytes) { audioPacket packet; if (!ringBuf->try_read(packet)) { qDebug(logAudio()) << "No more data available but buffer is not full! sentlen:" << sentlen << " nBytes:" << nBytes ; break; } currentLatency = packet.time.msecsTo(QTime::currentTime()); // This shouldn't be required but if we did output a partial packet // This will add the remaining packet data to the output buffer. if (tempBuf.sent != tempBuf.data.length()) { int send = qMin((int)nBytes - sentlen, tempBuf.data.length() - tempBuf.sent); memcpy(buffer + sentlen, tempBuf.data.constData() + tempBuf.sent, send); tempBuf.sent = tempBuf.sent + send; sentlen = sentlen + send; if (tempBuf.sent != tempBuf.data.length()) { // We still don't have enough buffer space for this? break; } //qDebug(logAudio()) << "Adding partial:" << send; } if (currentLatency > setup.latency) { qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Packet " << hex << packet.seq << " arrived too late (increase output latency!) " << dec << packet.time.msecsTo(QTime::currentTime()) << "ms"; while (currentLatency > setup.latency/2) { if (!ringBuf->try_read(packet)) { break; } currentLatency = packet.time.msecsTo(QTime::currentTime()); } } int send = qMin((int)nBytes - sentlen, packet.data.length()); memcpy(buffer + sentlen, packet.data.constData(), send); sentlen = sentlen + send; if (send < packet.data.length()) { //qDebug(logAudio()) << "Asking for partial, sent:" << send << "packet length" << packet.data.length(); tempBuf = packet; tempBuf.sent = tempBuf.sent + send; lastSeq = packet.seq; break; } /* if (packet.seq <= lastSeq) { qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Duplicate/early audio packet: " << hex << lastSeq << " got " << hex << packet.seq; } else if (packet.seq != lastSeq + 1) { qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Missing audio packet(s) from: " << hex << lastSeq + 1 << " to " << hex << packet.seq - 1; } */ lastSeq = packet.seq; } } //qDebug(logAudio()) << "looking for: " << nBytes << " got: " << sentlen; // fill the rest of the buffer with silence if (nBytes > sentlen) { memset(buffer+sentlen,0,nBytes-sentlen); } #if defined(RTAUDIO) return 0; #elif defined(PORTAUDIO) return 0; #else return nBytes; #endif } #if defined(RTAUDIO) int audioHandler::writeData(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status) { Q_UNUSED(outputBuffer); Q_UNUSED(streamTime); Q_UNUSED(status); int nBytes = nFrames * devChannels * 2; // This is ALWAYS 2 bytes per sample and 2 channels const char* data = (const char*)inputBuffer; #elif defined(PORTAUDIO) int audioHandler::writeData(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo * streamTime, PaStreamCallbackFlags status) { Q_UNUSED(outputBuffer); Q_UNUSED(streamTime); Q_UNUSED(status); int nBytes = nFrames * devChannels * 2; // This is ALWAYS 2 bytes per sample and 2 channels const char* data = (const char*)inputBuffer; #else qint64 audioHandler::writeData(const char* data, qint64 nBytes) { #endif if (!isReady) { isReady = true; } int sentlen = 0; //qDebug(logAudio()) << "nFrames" << nFrames << "nBytes" << nBytes; int chunkBytes = chunkSize * devChannels * 2; while (sentlen < nBytes) { if (tempBuf.sent != chunkBytes) { int send = qMin((int)(nBytes - sentlen), chunkBytes - tempBuf.sent); tempBuf.data.append(QByteArray::fromRawData(data + sentlen, send)); sentlen = sentlen + send; tempBuf.seq = 0; // Not used in TX tempBuf.time = QTime::currentTime(); tempBuf.sent = tempBuf.sent + send; } else { //ringBuf->write(tempBuf); if (!ringBuf->try_write(tempBuf)) { qDebug(logAudio()) << "outgoing audio buffer full!"; break; } tempBuf.data.clear(); tempBuf.sent = 0; } } //qDebug(logAudio()) << "sentlen" << sentlen; #if defined(RTAUDIO) return 0; #elif defined(PORTAUDIO) return 0; #else return nBytes; #endif } void audioHandler::incomingAudio(audioPacket inPacket) { // No point buffering audio until stream is actually running. // Regardless of the radio stream format, the buffered audio will ALWAYS be // 16bit sample interleaved stereo 48K (or whatever the native sample rate is) if (!isInitialized && !isReady) { qDebug(logAudio()) << "Packet received when stream was not ready"; return; } if (setup.codec == 0x40 || setup.codec == 0x80) { unsigned char* in = (unsigned char*)inPacket.data.data(); /* Decode the frame. */ QByteArray outPacket((setup.samplerate / 50) * sizeof(qint16) * setup.radioChan, (char)0xff); // Preset the output buffer size. qint16* out = (qint16*)outPacket.data(); int nSamples = opus_packet_get_nb_samples(in, inPacket.data.size(),setup.samplerate); if (nSamples != setup.samplerate / 50) { qInfo(logAudio()) << "Opus nSamples=" << nSamples << " expected:" << (setup.samplerate / 50); return; } nSamples = opus_decode(decoder, in, inPacket.data.size(), out, (setup.samplerate / 50), 0); if (nSamples < 0) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Opus decode failed:" << opus_strerror(nSamples) << "packet size" << inPacket.data.length(); return; } else { if (int(nSamples * sizeof(qint16) * setup.radioChan) != outPacket.size()) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Opus decoder mismatch: nBytes:" << nSamples * sizeof(qint16) * setup.radioChan << "outPacket:" << outPacket.size(); outPacket.resize(nSamples * sizeof(qint16) * setup.radioChan); } //qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Opus decoded" << inPacket.data.size() << "bytes, into" << outPacket.length() << "bytes"; inPacket.data.clear(); inPacket.data = outPacket; // Replace incoming data with converted. } } //qDebug(logAudio()) << "Got" << setup.bits << "bits, length" << inPacket.data.length(); // Incoming data is 8bits? if (setup.bits == 8) { // Current packet is 8bit so need to create a new buffer that is 16bit QByteArray outPacket((int)inPacket.data.length() * 2 * (devChannels / setup.radioChan), (char)0xff); qint16* out = (qint16*)outPacket.data(); for (int f = 0; f < inPacket.data.length(); f++) { int samp = (quint8)inPacket.data[f]; for (int g = setup.radioChan; g <= devChannels; g++) { if (setup.ulaw) *out++ = ulaw_decode[samp] * this->volume; else *out++ = (qint16)((samp - 128) << 8) * this->volume; } } inPacket.data.clear(); inPacket.data = outPacket; // Replace incoming data with converted. } else { // This is already a 16bit stream, do we need to convert to stereo? if (setup.radioChan == 1 && devChannels > 1) { // Yes QByteArray outPacket(inPacket.data.length() * 2, (char)0xff); // Preset the output buffer size. qint16* in = (qint16*)inPacket.data.data(); qint16* out = (qint16*)outPacket.data(); for (int f = 0; f < inPacket.data.length() / 2; f++) { *out++ = (qint16)*in * this->volume; *out++ = (qint16)*in++ * this->volume; } inPacket.data.clear(); inPacket.data = outPacket; // Replace incoming data with converted. } else { // We already have the same number of channels so just update volume. qint16* in = (qint16*)inPacket.data.data(); for (int f = 0; f < inPacket.data.length() / 2; f++) { *in = *in * this->volume; in++; } } } /* We now have an array of 16bit samples in the NATIVE samplerate of the radio If the radio sample rate is below 48000, we need to resample. */ //qDebug(logAudio()) << "Now 16 bit stereo, length" << inPacket.data.length(); if (resampleRatio != 1.0) { // We need to resample // We have a stereo 16bit stream. quint32 outFrames = ((inPacket.data.length() / 2 / devChannels) * resampleRatio); quint32 inFrames = (inPacket.data.length() / 2 / devChannels); QByteArray outPacket(outFrames * 4, (char)0xff); // Preset the output buffer size. const qint16* in = (qint16*)inPacket.data.constData(); qint16* out = (qint16*)outPacket.data(); int err = 0; err = wf_resampler_process_interleaved_int(resampler, in, &inFrames, out, &outFrames); if (err) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Resampler error " << err << " inFrames:" << inFrames << " outFrames:" << outFrames; } inPacket.data.clear(); inPacket.data = outPacket; // Replace incoming data with converted. } //qDebug(logAudio()) << "Adding packet to buffer:" << inPacket.seq << ": " << inPacket.data.length(); lastSentSeq = inPacket.seq; if (!ringBuf->try_write(inPacket)) { qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Buffer full! capacity:" << ringBuf->capacity() << "length" << ringBuf->size(); } return; } void audioHandler::changeLatency(const quint16 newSize) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Changing latency to: " << newSize << " from " << setup.latency; setup.latency = newSize; delete ringBuf; ringBuf = new wilt::Ring(setup.latency / 8 + 1); // Should be customizable. } int audioHandler::getLatency() { return currentLatency; } void audioHandler::getNextAudioChunk(QByteArray& ret) { audioPacket packet; packet.sent = 0; if (isInitialized && ringBuf != Q_NULLPTR && ringBuf->try_read(packet)) { currentLatency = packet.time.msecsTo(QTime::currentTime()); if (currentLatency > setup.latency) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Packet " << hex << packet.seq << " arrived too late (increase output latency!) " << dec << packet.time.msecsTo(QTime::currentTime()) << "ms"; // if (!ringBuf->try_read(packet)) // break; // currentLatency = packet.time.msecsTo(QTime::currentTime()); } //qDebug(logAudio) << "Chunksize" << this->chunkSize << "Packet size" << packet.data.length(); // Packet will arrive as stereo interleaved 16bit 48K if (resampleRatio != 1.0) { quint32 outFrames = ((packet.data.length() / 2 / devChannels) * resampleRatio); quint32 inFrames = (packet.data.length() / 2 / devChannels); QByteArray outPacket((int)outFrames * 2 * devChannels, (char)0xff); const qint16* in = (qint16*)packet.data.constData(); qint16* out = (qint16*)outPacket.data(); int err = 0; err = wf_resampler_process_interleaved_int(resampler, in, &inFrames, out, &outFrames); if (err) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Resampler error " << err << " inFrames:" << inFrames << " outFrames:" << outFrames; } //qInfo(logAudio()) << "Resampler run " << err << " inFrames:" << inFrames << " outFrames:" << outFrames; //qInfo(logAudio()) << "Resampler run inLen:" << packet->datain.length() << " outLen:" << packet->dataout.length(); packet.data.clear(); packet.data = outPacket; // Copy output packet back to input buffer. } //qDebug(logAudio()) << "Now resampled, length" << packet.data.length(); // Do we need to convert mono to stereo? if (setup.radioChan == 1 && devChannels > 1) { // Strip out right channel? QByteArray outPacket(packet.data.length()/2, (char)0xff); const qint16* in = (qint16*)packet.data.constData(); qint16* out = (qint16*)outPacket.data(); for (int f = 0; f < outPacket.length()/2; f++) { *out++ = *in++; in++; // Skip each even channel. } packet.data.clear(); packet.data = outPacket; // Copy output packet back to input buffer. } //qDebug(logAudio()) << "Now mono, length" << packet.data.length(); if (setup.codec == 0x40 || setup.codec == 0x80) { //Are we using the opus codec? qint16* in = (qint16*)packet.data.data(); /* Encode the frame. */ QByteArray outPacket(1275, (char)0xff); // Preset the output buffer size to MAXIMUM possible Opus frame size unsigned char* out = (unsigned char*)outPacket.data(); int nbBytes = opus_encode(encoder, in, (setup.samplerate / 50), out, outPacket.length()); if (nbBytes < 0) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Opus encode failed:" << opus_strerror(nbBytes); return; } else { outPacket.resize(nbBytes); packet.data.clear(); packet.data = outPacket; // Replace incoming data with converted. } } else if (setup.bits == 8) { // Do we need to convert 16-bit to 8-bit? QByteArray outPacket((int)packet.data.length() / 2, (char)0xff); qint16* in = (qint16*)packet.data.data(); for (int f = 0; f < outPacket.length(); f++) { qint16 sample = *in++; if (setup.ulaw) { int sign = (sample >> 8) & 0x80; if (sign) sample = (short)-sample; if (sample > cClip) sample = cClip; sample = (short)(sample + cBias); int exponent = (int)MuLawCompressTable[(sample >> 7) & 0xFF]; int mantissa = (sample >> (exponent + 3)) & 0x0F; int compressedByte = ~(sign | (exponent << 4) | mantissa); outPacket[f] = (quint8)compressedByte; } else { int compressedByte = (((sample + 32768) >> 8) & 0xff); outPacket[f] = (quint8)compressedByte; } } packet.data.clear(); packet.data = outPacket; // Copy output packet back to input buffer. } ret = packet.data; //qDebug(logAudio()) << "Now radio format, length" << packet.data.length(); } return; } #if !defined (RTAUDIO) && !defined(PORTAUDIO) qint64 audioHandler::bytesAvailable() const { return 0; } bool audioHandler::isSequential() const { return true; } void audioHandler::notified() { } void audioHandler::stateChanged(QAudio::State state) { // Process the state switch (state) { case QAudio::IdleState: { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio now in idle state: " << audioBuffer.size() << " packets in buffer"; if (audioOutput != Q_NULLPTR && audioOutput->error() == QAudio::UnderrunError) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "buffer underrun"; //audioOutput->suspend(); } break; } case QAudio::ActiveState: { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio now in active state: " << audioBuffer.size() << " packets in buffer"; break; } case QAudio::SuspendedState: { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio now in suspended state: " << audioBuffer.size() << " packets in buffer"; break; } case QAudio::StoppedState: { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio now in stopped state: " << audioBuffer.size() << " packets in buffer"; break; } default: { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Unhandled audio state: " << audioBuffer.size() << " packets in buffer"; } } } void audioHandler::stop() { if (audioOutput != Q_NULLPTR && audioOutput->state() != QAudio::StoppedState) { // Stop audio output audioOutput->stop(); this->stop(); this->close(); delete audioOutput; audioOutput = Q_NULLPTR; } if (audioInput != Q_NULLPTR && audioInput->state() != QAudio::StoppedState) { // Stop audio output audioInput->stop(); this->stop(); this->close(); delete audioInput; audioInput = Q_NULLPTR; } isInitialized = false; } #endif wfview-1.2d/audiohandler.h000066400000000000000000000130071415164626400156510ustar00rootroot00000000000000#ifndef AUDIOHANDLER_H #define AUDIOHANDLER_H #include #include #include #include #include #if defined(RTAUDIO) #ifdef Q_OS_WIN #include "RtAudio.h" #else #include "rtaudio/RtAudio.h" #endif #elif defined (PORTAUDIO) #include "portaudio.h" //#error "PORTAUDIO is not currently supported" #else #include #include #include #include #include #endif typedef signed short MY_TYPE; #define FORMAT RTAUDIO_SINT16 #define SCALE 32767.0 #define LOG100 4.60517018599 #include #include #include #include #include "resampler/speex_resampler.h" #include "ring/ring.h" #ifdef Q_OS_WIN #include "opus.h" #else #include "opus/opus.h" #endif #include "audiotaper.h" #include //#define BUFFER_SIZE (32*1024) #define INTERNAL_SAMPLE_RATE 48000 #define MULAW_BIAS 33 #define MULAW_MAX 0x1fff struct audioPacket { quint32 seq; QTime time; quint16 sent; QByteArray data; }; struct audioSetup { QString name; quint8 bits; quint8 radioChan; quint16 samplerate; quint16 latency; quint8 codec; bool ulaw; bool isinput; #if defined(RTAUDIO) || defined(PORTAUDIO) int port; #else QAudioDeviceInfo port; #endif quint8 resampleQuality; unsigned char localAFgain; }; // For QtMultimedia, use a native QIODevice #if !defined(PORTAUDIO) && !defined(RTAUDIO) class audioHandler : public QIODevice #else class audioHandler : public QObject #endif { Q_OBJECT public: audioHandler(QObject* parent = 0); ~audioHandler(); int getLatency(); #if !defined (RTAUDIO) && !defined(PORTAUDIO) bool setDevice(QAudioDeviceInfo deviceInfo); void start(); void flush(); void stop(); qint64 bytesAvailable() const; bool isSequential() const; #endif void getNextAudioChunk(QByteArray &data); public slots: bool init(audioSetup setup); void changeLatency(const quint16 newSize); void setVolume(unsigned char volume); void incomingAudio(const audioPacket data); private slots: #if !defined (RTAUDIO) && !defined(PORTAUDIO) void notified(); void stateChanged(QAudio::State state); #endif signals: void audioMessage(QString message); void sendLatency(quint16 newSize); void haveAudioData(const QByteArray& data); private: #if defined(RTAUDIO) int readData(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status); static int staticRead(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void* userData) { return static_cast(userData)->readData(outputBuffer, inputBuffer, nFrames, streamTime, status); } int writeData(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status); static int staticWrite(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void* userData) { return static_cast(userData)->writeData(outputBuffer, inputBuffer, nFrames, streamTime, status); } #elif defined(PORTAUDIO) int readData(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo* streamTime, PaStreamCallbackFlags status); static int staticRead(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo* streamTime, PaStreamCallbackFlags status, void* userData) { return ((audioHandler*)userData)->readData(inputBuffer, outputBuffer, nFrames, streamTime, status); } int writeData(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo* streamTime, PaStreamCallbackFlags status); static int staticWrite(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo* streamTime, PaStreamCallbackFlags status, void* userData) { return ((audioHandler*)userData)->writeData(inputBuffer, outputBuffer, nFrames, streamTime, status); } #else qint64 readData(char* data, qint64 nBytes); qint64 writeData(const char* data, qint64 nBytes); #endif void reinit(); bool isInitialized=false; bool isReady = false; #if defined(RTAUDIO) RtAudio* audio = Q_NULLPTR; int audioDevice = 0; RtAudio::StreamParameters aParams; RtAudio::StreamOptions options; RtAudio::DeviceInfo info; #elif defined(PORTAUDIO) PaStream* audio = Q_NULLPTR; PaStreamParameters aParams; const PaDeviceInfo *info; #else QAudioOutput* audioOutput=Q_NULLPTR; QAudioInput* audioInput=Q_NULLPTR; QAudioFormat format; QAudioDeviceInfo deviceInfo; #endif SpeexResamplerState* resampler = Q_NULLPTR; quint16 audioLatency; unsigned int chunkSize; bool chunkAvailable; quint32 lastSeq; quint32 lastSentSeq=0; quint16 nativeSampleRate=0; quint8 radioSampleBits; quint8 radioChannels; QMapaudioBuffer; double resampleRatio; wilt::Ring *ringBuf=Q_NULLPTR; volatile bool ready = false; audioPacket tempBuf; quint16 currentLatency; qreal volume=1.0; int devChannels; audioSetup setup; OpusEncoder* encoder=Q_NULLPTR; OpusDecoder* decoder=Q_NULLPTR; }; #endif // AUDIOHANDLER_H wfview-1.2d/audiotaper.h000066400000000000000000000065751415164626400153630ustar00rootroot00000000000000#ifndef AUDIOTAPER_H #define AUDIOTAPER_H #include const qreal audiopot[256] = { 0.00000, 0.00101, 0.00202, 0.00305, 0.00409, 0.00513, 0.00619, 0.00725, 0.00832, 0.00941, 0.01050, 0.01160, 0.01272, 0.01384, 0.01497, 0.01612, 0.01727, 0.01843, 0.01961, 0.02080, 0.02199, 0.02320, 0.02442, 0.02565, 0.02689, 0.02814, 0.02940, 0.03068, 0.03196, 0.03326, 0.03457, 0.03589, 0.03723, 0.03857, 0.03993, 0.04130, 0.04268, 0.04408, 0.04548, 0.04690, 0.04834, 0.04978, 0.05124, 0.05272, 0.05420, 0.05570, 0.05721, 0.05874, 0.06028, 0.06184, 0.06341, 0.06499, 0.06659, 0.06820, 0.06982, 0.07146, 0.07312, 0.07479, 0.07648, 0.07818, 0.07990, 0.08163, 0.08338, 0.08514, 0.08692, 0.08872, 0.09053, 0.09236, 0.09421, 0.09607, 0.09795, 0.09984, 0.10176, 0.10369, 0.10564, 0.10760, 0.10959, 0.11159, 0.11361, 0.11565, 0.11770, 0.11978, 0.12187, 0.12399, 0.12612, 0.12827, 0.13044, 0.13263, 0.13484, 0.13707, 0.13933, 0.14160, 0.14389, 0.14620, 0.14854, 0.15089, 0.15327, 0.15567, 0.15809, 0.16053, 0.16299, 0.16548, 0.16799, 0.17052, 0.17307, 0.17565, 0.17825, 0.18088, 0.18353, 0.18620, 0.18889, 0.19162, 0.19436, 0.19713, 0.19993, 0.20275, 0.20560, 0.20847, 0.21137, 0.21429, 0.21725, 0.22022, 0.22323, 0.22626, 0.22932, 0.23241, 0.23553, 0.23867, 0.24184, 0.24504, 0.24828, 0.25153, 0.25482, 0.25814, 0.26149, 0.26487, 0.26828, 0.27172, 0.27520, 0.27870, 0.28224, 0.28580, 0.28941, 0.29304, 0.29670, 0.30040, 0.30414, 0.30790, 0.31170, 0.31554, 0.31941, 0.32331, 0.32725, 0.33123, 0.33524, 0.33929, 0.34338, 0.34750, 0.35166, 0.35586, 0.36009, 0.36437, 0.36868, 0.37303, 0.37742, 0.38185, 0.38633, 0.39084, 0.39539, 0.39999, 0.40462, 0.40930, 0.41402, 0.41878, 0.42359, 0.42844, 0.43333, 0.43827, 0.44326, 0.44828, 0.45336, 0.45848, 0.46364, 0.46886, 0.47412, 0.47943, 0.48478, 0.49019, 0.49564, 0.50115, 0.50670, 0.51230, 0.51796, 0.52366, 0.52942, 0.53523, 0.54110, 0.54701, 0.55298, 0.55900, 0.56508, 0.57122, 0.57741, 0.58365, 0.58995, 0.59631, 0.60273, 0.60920, 0.61574, 0.62233, 0.62898, 0.63570, 0.64247, 0.64931, 0.65620, 0.66316, 0.67019, 0.67727, 0.68442, 0.69164, 0.69892, 0.70627, 0.71368, 0.72116, 0.72871, 0.73633, 0.74402, 0.75178, 0.75960, 0.76750, 0.77547, 0.78351, 0.79163, 0.79981, 0.80808, 0.81641, 0.82483, 0.83332, 0.84188, 0.85053, 0.85925, 0.86805, 0.87693, 0.88590, 0.89494, 0.90407, 0.91327, 0.92257, 0.93194, 0.94140, 0.95095, 0.96058, 0.97030, 0.98011, 0.99001, 1.00000 }; #endif // AUDIOTAPER_H wfview-1.2d/calibrationwindow.cpp000066400000000000000000000063051415164626400172670ustar00rootroot00000000000000#include "calibrationwindow.h" #include "ui_calibrationwindow.h" #include "logcategories.h" calibrationWindow::calibrationWindow(QWidget *parent) : QDialog(parent), ui(new Ui::calibrationWindow) { ui->setupUi(this); ui->calCourseSlider->setDisabled(true); ui->calCourseSpinbox->setDisabled(true); ui->calFineSlider->setDisabled(true); ui->calFineSpinbox->setDisabled(true); } calibrationWindow::~calibrationWindow() { delete ui; } void calibrationWindow::handleCurrentFreq(double tunedFreq) { (void)tunedFreq; } void calibrationWindow::handleSpectrumPeak(double peakFreq) { (void)peakFreq; } void calibrationWindow::handleRefAdjustCourse(unsigned char value) { ui->calCourseSlider->setDisabled(false); ui->calCourseSpinbox->setDisabled(false); ui->calCourseSlider->blockSignals(true); ui->calCourseSpinbox->blockSignals(true); ui->calCourseSlider->setValue((int) value); ui->calCourseSpinbox->setValue((int) value); ui->calCourseSlider->blockSignals(false); ui->calCourseSpinbox->blockSignals(false); } void calibrationWindow::handleRefAdjustFine(unsigned char value) { ui->calFineSlider->setDisabled(false); ui->calFineSpinbox->setDisabled(false); ui->calFineSlider->blockSignals(true); ui->calFineSpinbox->blockSignals(true); ui->calFineSlider->setValue((int) value); ui->calFineSpinbox->setValue((int) value); ui->calFineSlider->blockSignals(false); ui->calFineSpinbox->blockSignals(false); } void calibrationWindow::on_calReadRigCalBtn_clicked() { emit requestRefAdjustCourse(); emit requestRefAdjustFine(); } void calibrationWindow::on_calCourseSlider_valueChanged(int value) { ui->calCourseSpinbox->blockSignals(true); ui->calCourseSpinbox->setValue((int) value); ui->calCourseSpinbox->blockSignals(false); emit setRefAdjustCourse((unsigned char) value); } void calibrationWindow::on_calFineSlider_valueChanged(int value) { ui->calFineSpinbox->blockSignals(true); ui->calFineSpinbox->setValue((int) value); ui->calFineSpinbox->blockSignals(false); emit setRefAdjustFine((unsigned char) value); } void calibrationWindow::on_calCourseSpinbox_valueChanged(int value) { // this one works with the up and down arrows, // however, if typing in a value, say "128", // this will get called three times with these values: // 1 // 12 // 128 //int value = ui->calFineSpinbox->value(); ui->calCourseSlider->blockSignals(true); ui->calCourseSlider->setValue(value); ui->calCourseSlider->blockSignals(false); emit setRefAdjustCourse((unsigned char) value); } void calibrationWindow::on_calFineSpinbox_valueChanged(int value) { //int value = ui->calFineSpinbox->value(); ui->calFineSlider->blockSignals(true); ui->calFineSlider->setValue(value); ui->calFineSlider->blockSignals(false); emit setRefAdjustFine((unsigned char) value); } void calibrationWindow::on_calFineSpinbox_editingFinished() { } void calibrationWindow::on_calCourseSpinbox_editingFinished() { // This function works well for typing in values // but the up and down arrows on the spinbox will not // trigger this function, until the enter key is pressed. } wfview-1.2d/calibrationwindow.h000066400000000000000000000022101415164626400167230ustar00rootroot00000000000000#ifndef CALIBRATIONWINDOW_H #define CALIBRATIONWINDOW_H #include namespace Ui { class calibrationWindow; } class calibrationWindow : public QDialog { Q_OBJECT public: explicit calibrationWindow(QWidget *parent = 0); ~calibrationWindow(); public slots: void handleSpectrumPeak(double peakFreq); void handleCurrentFreq(double tunedFreq); void handleRefAdjustCourse(unsigned char); void handleRefAdjustFine(unsigned char); signals: void requestSpectrumPeak(double peakFreq); void requestCurrentFreq(double tunedFreq); void requestRefAdjustCourse(); void requestRefAdjustFine(); void setRefAdjustCourse(unsigned char); void setRefAdjustFine(unsigned char); private slots: void on_calReadRigCalBtn_clicked(); void on_calCourseSlider_valueChanged(int value); void on_calFineSlider_valueChanged(int value); void on_calCourseSpinbox_valueChanged(int arg1); void on_calFineSpinbox_valueChanged(int arg1); void on_calFineSpinbox_editingFinished(); void on_calCourseSpinbox_editingFinished(); private: Ui::calibrationWindow *ui; }; #endif // CALIBRATIONWINDOW_H wfview-1.2d/calibrationwindow.ui000066400000000000000000000150661415164626400171260ustar00rootroot00000000000000 calibrationWindow 0 0 400 300 0 0 400 300 400 300 Reference Adjustment 9 9 381 281 IC-9700 Reference Adjustment Course Fine 10 255 Qt::Horizontal 255 Qt::Horizontal 10 10 255 255 Qt::Vertical 20 40 0 false <html><head/><body><p>Save the calibration data to the indicated slot in the preference file. </p></body></html> Save false <html><head/><body><p>Load the calibration data fromthe indicated slot in the preference file. </p></body></html> Load false Slot: false 1 2 3 4 5 Qt::Horizontal 40 20 Read Current Rig Calibration wfview-1.2d/commhandler.cpp000066400000000000000000000211011415164626400160300ustar00rootroot00000000000000#include "commhandler.h" #include "logcategories.h" #include // Copytight 2017-2020 Elliott H. Liggett commHandler::commHandler() { //constructor // grab baud rate and other comm port details // if they need to be changed later, please // destroy this and create a new one. port = new QSerialPort(); // TODO: The following should become arguments and/or functions // Add signal/slot everywhere for comm port setup. // Consider how to "re-setup" and how to save the state for next time. baudrate = 115200; stopbits = 1; portName = "/dev/ttyUSB0"; this->PTTviaRTS = false; setupComm(); // basic parameters openPort(); //qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize(); //port->setReadBufferSize(1024); // manually. 256 never saw any return from the radio. why... //qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize(); connect(port, SIGNAL(readyRead()), this, SLOT(receiveDataIn())); } commHandler::commHandler(QString portName, quint32 baudRate) { //constructor // grab baud rate and other comm port details // if they need to be changed later, please // destroy this and create a new one. port = new QSerialPort(); // TODO: The following should become arguments and/or functions // Add signal/slot everywhere for comm port setup. // Consider how to "re-setup" and how to save the state for next time. baudrate = baudRate; stopbits = 1; this->portName = portName; this->PTTviaRTS = false; setupComm(); // basic parameters openPort(); // qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize(); //port->setReadBufferSize(1024); // manually. 256 never saw any return from the radio. why... //qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize(); connect(port, SIGNAL(readyRead()), this, SLOT(receiveDataIn())); } commHandler::~commHandler() { this->closePort(); } void commHandler::setupComm() { serialError = false; port->setPortName(portName); port->setBaudRate(baudrate); port->setStopBits(QSerialPort::OneStop);// OneStop is other option } void commHandler::receiveDataFromUserToRig(const QByteArray &data) { sendDataOut(data); } void commHandler::sendDataOut(const QByteArray &writeData) { mutex.lock(); qint64 bytesWritten; if(PTTviaRTS) { // Size: 1 2 3 4 5 6 7 8 //index: 0 1 2 3 4 5 6 7 //Query: FE FE TO FROM 0x1C 0x00 0xFD //PTT On: FE FE TO FROM 0x1C 0x00 0x01 0xFD //PTT Off: FE FE TO FROM 0x1C 0x00 0x00 0xFD if(writeData.endsWith(QByteArrayLiteral("\x1C\x00\xFD"))) { // Query //qDebug(logSerial()) << "Looks like PTT Query"; bool pttOn = this->rtsStatus(); QByteArray pttreturncmd = QByteArray("\xFE\xFE"); pttreturncmd.append(writeData.at(3)); pttreturncmd.append(writeData.at(2)); pttreturncmd.append(QByteArray("\x1C\x00", 2)); pttreturncmd.append((char)pttOn); pttreturncmd.append("\xFD"); //qDebug(logSerial()) << "Sending fake PTT query result: " << (bool)pttOn; printHex(pttreturncmd, false, true); emit haveDataFromPort(pttreturncmd); mutex.unlock(); return; } else if(writeData.endsWith(QByteArrayLiteral("\x1C\x00\x01\xFD"))) { // PTT ON //qDebug(logSerial()) << "Looks like PTT ON"; setRTS(true); mutex.unlock(); return; } else if(writeData.endsWith(QByteArrayLiteral("\x1C\x00\x00\xFD"))) { // PTT OFF //qDebug(logSerial()) << "Looks like PTT OFF"; setRTS(false); mutex.unlock(); return; } } bytesWritten = port->write(writeData); if(bytesWritten != (qint64)writeData.size()) { qDebug(logSerial()) << "bytesWritten: " << bytesWritten << " length of byte array: " << writeData.length()\ << " size of byte array: " << writeData.size()\ << " Wrote all bytes? " << (bool) (bytesWritten == (qint64)writeData.size()); } mutex.unlock(); } void commHandler::receiveDataIn() { // connected to comm port data signal // Here we get a little specific to CIV radios // because we know what constitutes a valid "frame" of data. // new code: port->startTransaction(); inPortData = port->readAll(); if(inPortData.size() == 1) { // Generally for baud <= 9600 if (inPortData == "\xFE") { // This will get hit twice. // After the FE FE, we transition into // the normal .startsWith FE FE block // where the normal rollback code can handle things. port->rollbackTransaction(); rolledBack = true; return; } } if(inPortData.startsWith("\xFE\xFE")) { if(inPortData.contains("\xFC")) { //qInfo(logSerial()) << "Transaction contains collision data. Dumping."; //printHex(inPortData, false, true); port->commitTransaction(); return; } if(inPortData.endsWith("\xFD")) { // good! port->commitTransaction(); emit haveDataFromPort(inPortData); if(rolledBack) { // qInfo(logSerial()) << "Rolled back and was successfull. Length: " << inPortData.length(); //printHex(inPortData, false, true); rolledBack = false; } } else { // did not receive the entire thing so roll back: // qInfo(logSerial()) << "Rolling back transaction. End not detected. Lenth: " << inPortData.length(); //printHex(inPortData, false, true); port->rollbackTransaction(); rolledBack = true; } } else { port->commitTransaction(); // do not emit data, do not keep data. //qInfo(logSerial()) << "Warning: received data with invalid start. Dropping data."; //qInfo(logSerial()) << "THIS SHOULD ONLY HAPPEN ONCE!!"; // THIS SHOULD ONLY HAPPEN ONCE! // unrecoverable. We did not receive the start and must // have missed it earlier because we did not roll back to // preserve the beginning. //printHex(inPortData, false, true); } } void commHandler::setRTS(bool rtsOn) { bool success = port->setRequestToSend(rtsOn); if(!success) { qInfo(logSerial()) << "Error, could not set RTS on port " << portName; } } bool commHandler::rtsStatus() { return port->isRequestToSend(); } void commHandler::setUseRTSforPTT(bool PTTviaRTS) { this->PTTviaRTS = PTTviaRTS; } void commHandler::openPort() { bool success; // port->open(); success = port->open(QIODevice::ReadWrite); if(success) { port->setDataTerminalReady(false); port->setRequestToSend(false); isConnected = true; qInfo(logSerial()) << "Opened port: " << portName; return; } else { qInfo(logSerial()) << "Could not open serial port " << portName << " , please restart."; isConnected = false; serialError = true; emit haveSerialPortError(portName, "Could not open port. Please restart."); return; } } void commHandler::closePort() { if(port) { port->close(); delete port; } isConnected = false; } void commHandler::debugThis() { // Do not use, function is for debug only and subject to change. qInfo(logSerial()) << "comm debug called."; inPortData = port->readAll(); emit haveDataFromPort(inPortData); } void commHandler::printHex(const QByteArray &pdata, bool printVert, bool printHoriz) { qDebug(logSerial()) << "---- Begin hex dump -----:"; QString sdata("DATA: "); QString index("INDEX: "); QStringList strings; for(int i=0; i < pdata.length(); i++) { strings << QString("[%1]: %2").arg(i,8,10,QChar('0')).arg((unsigned char)pdata[i], 2, 16, QChar('0')); sdata.append(QString("%1 ").arg((unsigned char)pdata[i], 2, 16, QChar('0')) ); index.append(QString("%1 ").arg(i, 2, 10, QChar('0'))); } if(printVert) { for(int i=0; i < strings.length(); i++) { qDebug(logSerial()) << strings.at(i); } } if(printHoriz) { qDebug(logSerial()) << index; qDebug(logSerial()) << sdata; } qDebug(logSerial()) << "----- End hex dump -----"; } wfview-1.2d/commhandler.h000066400000000000000000000035021415164626400155020ustar00rootroot00000000000000#ifndef COMMHANDLER_H #define COMMHANDLER_H #include #include #include #include // This class abstracts the comm port in a useful way and connects to // the command creator and command parser. class commHandler : public QObject { Q_OBJECT public: commHandler(); commHandler(QString portName, quint32 baudRate); bool serialError; bool rtsStatus(); ~commHandler(); public slots: void setUseRTSforPTT(bool useRTS); void setRTS(bool rtsOn); private slots: void receiveDataIn(); // from physical port void receiveDataFromUserToRig(const QByteArray &data); void debugThis(); signals: void haveTextMessage(QString message); // status, debug only void sendDataOutToPort(const QByteArray &writeData); // not used void haveDataFromPort(QByteArray data); // emit this when we have data, connect to rigcommander void haveSerialPortError(const QString port, const QString error); void haveStatusUpdate(const QString text); private: void setupComm(); void openPort(); void closePort(); void sendDataOut(const QByteArray &writeData); // out to radio void debugMe(); void hexPrint(); //QDataStream stream; QByteArray outPortData; QByteArray inPortData; //QDataStream outStream; //QDataStream inStream; unsigned char buffer[256]; QString portName; QSerialPort *port; qint32 baudrate; unsigned char stopbits; bool rolledBack; QSerialPort *pseudoterm; int ptfd; // pseudo-terminal file desc. mutable QMutex ptMutex; bool havePt; QString ptDevSlave; bool PTTviaRTS = false; bool isConnected; // port opened mutable QMutex mutex; void printHex(const QByteArray &pdata, bool printVert, bool printHoriz); }; #endif // COMMHANDLER_H wfview-1.2d/freqmemory.cpp000066400000000000000000000027021415164626400157330ustar00rootroot00000000000000#include "freqmemory.h" #include "logcategories.h" // Copytight 2017-2020 Elliott H. Liggett freqMemory::freqMemory() { // NOTE: These are also present in the header and // the array must be changed if these are changed. numPresets = 100; maxIndex = numPresets - 1; initializePresets(); } void freqMemory::initializePresets() { // qInfo() << "Initializing " << numPresets << " memory channels"; for(unsigned int p=0; p < numPresets; p++) { presets[p].frequency = 14.1 + p/1000.0; presets[p].mode = modeUSB; presets[p].isSet = true; } } void freqMemory::setPreset(unsigned int index, double frequency, mode_kind mode) { if(index <= maxIndex) { presets[index].frequency = frequency; presets[index].mode = mode; presets[index].isSet = true; } } preset_kind freqMemory::getPreset(unsigned int index) { //validate then return if(index <= maxIndex) { return presets[index]; } // else, return something obviously wrong preset_kind temp; temp.frequency=12.345; temp.mode = modeUSB; temp.isSet = false; return temp; } unsigned int freqMemory::getNumPresets() { return numPresets; } void freqMemory::dumpMemory() { for(unsigned int p=0; p < numPresets; p++) { qInfo(logSystem()) << "Index: " << p << " freq: " << presets[p].frequency << " Mode: " << presets[p].mode << " isSet: " << presets[p].isSet; } } wfview-1.2d/freqmemory.h000066400000000000000000000030621415164626400154000ustar00rootroot00000000000000#ifndef FREQMEMORY_H #define FREQMEMORY_H #include #include // 0 1 2 3 4 //modes << "LSB" << "USB" << "AM" << "CW" << "RTTY"; // 5 6 7 8 9 // modes << "FM" << "CW-R" << "RTTY-R" << "LSB-D" << "USB-D"; enum mode_kind { modeLSB=0x00, modeUSB=0x01, modeAM=0x02, modeCW=0x03, modeRTTY=0x04, modeFM=0x05, modeCW_R=0x07, modeRTTY_R=0x08, modeLSB_D=0x80, modeUSB_D=0x81, modeDV=0x17, modeDD=0x27, modeWFM, modeS_AMD, modeS_AML, modeS_AMU, modeP25, modedPMR, modeNXDN_VN, modeNXDN_N, modeDCR, modePSK, modePSK_R }; struct mode_info { mode_kind mk; unsigned char reg; unsigned char filter; QString name; }; struct preset_kind { // QString name; // QString comment; // unsigned int index; // channel number double frequency; mode_kind mode; bool isSet; }; class freqMemory { public: freqMemory(); void setPreset(unsigned int index, double frequency, mode_kind mode); void setPreset(unsigned int index, double frequency, mode_kind mode, QString name); void setPreset(unsigned int index, double frequency, mode_kind mode, QString name, QString comment); void dumpMemory(); unsigned int getNumPresets(); preset_kind getPreset(unsigned int index); private: void initializePresets(); unsigned int numPresets; unsigned int maxIndex; //QVector presets; preset_kind presets[100]; }; #endif // FREQMEMORY_H wfview-1.2d/logcategories.cpp000066400000000000000000000005211415164626400163710ustar00rootroot00000000000000#include "logcategories.h" Q_LOGGING_CATEGORY(logSystem, "system") Q_LOGGING_CATEGORY(logSerial, "serial") Q_LOGGING_CATEGORY(logGui, "gui") Q_LOGGING_CATEGORY(logRig, "rig") Q_LOGGING_CATEGORY(logAudio, "audio") Q_LOGGING_CATEGORY(logUdp, "udp") Q_LOGGING_CATEGORY(logUdpServer, "udp.server") Q_LOGGING_CATEGORY(logRigCtlD, "rigctld") wfview-1.2d/logcategories.h000066400000000000000000000007731415164626400160470ustar00rootroot00000000000000#ifndef LOGCATEGORIES_H #define LOGCATEGORIES_H #include Q_DECLARE_LOGGING_CATEGORY(logSystem) Q_DECLARE_LOGGING_CATEGORY(logSerial) Q_DECLARE_LOGGING_CATEGORY(logGui) Q_DECLARE_LOGGING_CATEGORY(logRig) Q_DECLARE_LOGGING_CATEGORY(logAudio) Q_DECLARE_LOGGING_CATEGORY(logUdp) Q_DECLARE_LOGGING_CATEGORY(logUdpServer) Q_DECLARE_LOGGING_CATEGORY(logRigCtlD) #if defined(Q_OS_WIN) && !defined(__PRETTY_FUNCTION__) #define __PRETTY_FUNCTION__ __FUNCSIG__ #endif #endif // LOGCATEGORIES_H wfview-1.2d/main.cpp000066400000000000000000000124151415164626400144730ustar00rootroot00000000000000#include #include #include "wfmain.h" #include "logcategories.h" // Copytight 2017-2021 Elliott H. Liggett // Smart pointer to log file QScopedPointer m_logFile; QMutex logMutex; bool debugMode=false; void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); int main(int argc, char *argv[]) { QApplication a(argc, argv); //a.setStyle( "Fusion" ); a.setOrganizationName("wfview"); a.setOrganizationDomain("wfview.org"); a.setApplicationName("wfview"); #ifdef QT_DEBUG debugMode = true; #endif QString serialPortCL; QString hostCL; QString civCL; #ifdef Q_OS_MAC QString logFilename= QStandardPaths::standardLocations(QStandardPaths::DownloadLocation)[0] + "/wfview.log"; #else QString logFilename= QStandardPaths::standardLocations(QStandardPaths::TempLocation)[0] + "/wfview.log"; #endif QString settingsFile = NULL; QString currentArg; const QString helpText = QString("\nUsage: -p --port /dev/port, -h --host remotehostname, -c --civ 0xAddr, -l --logfile filename.log, -s --settings filename.ini, -d --debug, -v --version\n"); // TODO... const QString version = QString("wfview version: %1 (Git:%2 on %3 at %4 by %5@%6)\nOperating System: %7 (%8)\nBuild Qt Version %9. Current Qt Version: %10\n") .arg(QString(WFVIEW_VERSION)) .arg(GITSHORT).arg(__DATE__).arg(__TIME__).arg(UNAME).arg(HOST) .arg(QSysInfo::prettyProductName()).arg(QSysInfo::buildCpuArchitecture()) .arg(QT_VERSION_STR).arg(qVersion()); for(int c=1; c c) { serialPortCL = argv[c + 1]; c += 1; } } else if ((currentArg == "-d") || (currentArg == "--debug")) { debugMode = true; } else if ((currentArg == "-h") || (currentArg == "--host")) { if(argc > c) { hostCL = argv[c+1]; c+=1; } } else if ((currentArg == "-c") || (currentArg == "--civ")) { if (argc > c) { civCL = argv[c + 1]; c += 1; } } else if ((currentArg == "-l") || (currentArg == "--logfile")) { if (argc > c) { logFilename = argv[c + 1]; c += 1; } } else if ((currentArg == "-s") || (currentArg == "--settings")) { if (argc > c) { settingsFile = argv[c + 1]; c += 1; } } else if ((currentArg == "-?") || (currentArg == "--help")) { #ifdef Q_OS_WIN QMessageBox::information(0, "wfview help", helpText); #else std::cout << helpText.toStdString(); #endif return 0; } else if ((currentArg == "-v") || (currentArg == "--version")) { #ifdef Q_OS_WIN QMessageBox::information(0, "wfview version", version); #else std::cout << version.toStdString(); #endif return 0; } else { #ifdef Q_OS_WIN QMessageBox::information(0, "wfview unrecognised argument", helpText); #else std::cout << "Unrecognized option: " << currentArg.toStdString(); std::cout << helpText.toStdString(); #endif return -1; } } // Set the logging file before doing anything else. m_logFile.reset(new QFile(logFilename)); // Open the file logging m_logFile.data()->open(QFile::WriteOnly | QFile::Truncate | QFile::Text); // Set handler qInstallMessageHandler(messageHandler); qInfo(logSystem()) << version; qDebug(logSystem()) << QString("SerialPortCL as set by parser: %1").arg(serialPortCL); qDebug(logSystem()) << QString("remote host as set by parser: %1").arg(hostCL); qDebug(logSystem()) << QString("CIV as set by parser: %1").arg(civCL); a.setWheelScrollLines(1); // one line per wheel click wfmain w( serialPortCL, hostCL, settingsFile); w.show(); return a.exec(); } void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) { // Open stream file writes if (type == QtDebugMsg && !debugMode) { return; } QMutexLocker locker(&logMutex); QTextStream out(m_logFile.data()); // Write the date of recording out << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz "); // By type determine to what level belongs message switch (type) { case QtDebugMsg: out << "DBG "; break; case QtInfoMsg: out << "INF "; break; case QtWarningMsg: out << "WRN "; break; case QtCriticalMsg: out << "CRT "; break; case QtFatalMsg: out << "FTL "; break; } // Write to the output category of the message and the message itself out << context.category << ": " << msg << "\n"; out.flush(); // Clear the buffered data } wfview-1.2d/meter.cpp000066400000000000000000000377751415164626400147030ustar00rootroot00000000000000#include "meter.h" #include meter::meter(QWidget *parent) : QWidget(parent) { //QPainter painter(this); // Colors from qdarkstylesheet: // $COLOR_BACKGROUND_LIGHT: #505F69; // $COLOR_BACKGROUND_NORMAL: #32414B; // $COLOR_BACKGROUND_DARK: #19232D; // $COLOR_FOREGROUND_LIGHT: #F0F0F0; // grey // $COLOR_FOREGROUND_NORMAL: #AAAAAA; // grey // $COLOR_FOREGROUND_DARK: #787878; // grey // $COLOR_SELECTION_LIGHT: #148CD2; // $COLOR_SELECTION_NORMAL: #1464A0; // $COLOR_SELECTION_DARK: #14506E; // Colors I found that I liked from VFD images: // 3FB7CD // 3CA0DB // // Text in qdarkstylesheet seems to be #EFF0F1 if(drawLabels) { mXstart = 32; } else { mXstart = 0; } meterType = meterS; currentColor.setNamedColor("#148CD2"); currentColor = currentColor.darker(); peakColor.setNamedColor("#3CA0DB"); peakColor = peakColor.lighter(); averageColor.setNamedColor("#3FB7CD"); lowTextColor.setNamedColor("#eff0f1"); lowLineColor = lowTextColor; avgLevels.resize(averageBalisticLength, 0); peakLevels.resize(peakBalisticLength, 0); } void meter::clearMeterOnPTTtoggle() { // When a meter changes type, such as the fixed S -- TxPo meter, // there is automatic clearing. However, some meters do not switch on thier own, // and thus we are providing this clearing method. We are careful // not to clear meters that don't make sense to clear (such as Vd and Id) if( (meterType == meterALC) || (meterType == meterSWR) || (meterType == meterComp) || (meterType == meterTxMod) || (meterType == meterCenter )) { clearMeter(); } } void meter::clearMeter() { current = 0; average = 0; peak = 0; avgLevels.clear(); peakLevels.clear(); avgLevels.resize(averageBalisticLength, 0); peakLevels.resize(peakBalisticLength, 0); peakPosition = 0; avgPosition = 0; // re-draw scale: update(); } void meter::setMeterType(meterKind type) { if(type == meterType) return; meterType = type; // clear average and peak vectors: this->clearMeter(); } meterKind meter::getMeterType() { return meterType; } void meter::setMeterShortString(QString s) { meterShortString = s; } QString meter::getMeterShortString() { return meterShortString; } void meter::paintEvent(QPaintEvent *) { QPainter painter(this); // This next line sets up a canvis within the // space of the widget, and gives it corrdinates. // The end effect, is that the drawing functions will all // scale to the window size. painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.setFont(QFont(this->fontInfo().family(), fontSize)); widgetWindowHeight = this->height(); painter.setWindow(QRect(0, 0, 255+mXstart+15, widgetWindowHeight)); barHeight = widgetWindowHeight / 2; switch(meterType) { case meterS: label = "S"; peakRedLevel = 120; // S9+ drawScaleS(&painter); break; case meterPower: label = "PWR"; peakRedLevel = 210; // 100% drawScalePo(&painter); break; case meterALC: label = "ALC"; peakRedLevel = 100; drawScaleALC(&painter); break; case meterSWR: label = "SWR"; peakRedLevel = 100; // SWR 2.5 drawScaleSWR(&painter); break; case meterCenter: label = "CTR"; peakRedLevel = 256; // No need for red here drawScaleCenter(&painter); break; case meterVoltage: label = "Vd"; peakRedLevel = 241; drawScaleVd(&painter); break; case meterCurrent: label = "Id"; peakRedLevel = 120; drawScaleId(&painter); break; case meterComp: label = "CMP"; peakRedLevel = 100; drawScaleComp(&painter); break; case meterNone: return; break; default: label = "DN"; peakRedLevel = 241; drawScaleRaw(&painter); break; } // Current: the most-current value. // Draws a bar from start to value. painter.setPen(currentColor); painter.setBrush(currentColor); if(meterType == meterCenter) { painter.drawRect(mXstart+128,mYstart,current-128,barHeight); // Average: painter.setPen(averageColor); painter.setBrush(averageColor); painter.drawRect(mXstart+average-1,mYstart,1,barHeight); // bar is 1 pixel wide, height = meter start? // Peak: painter.setPen(peakColor); painter.setBrush(peakColor); if((peak > 191) || (peak < 63)) { painter.setBrush(Qt::red); painter.setPen(Qt::red); } painter.drawRect(mXstart+peak-1,mYstart,1,barHeight); } else { // X, Y, Width, Height painter.drawRect(mXstart,mYstart,current,barHeight); // Average: painter.setPen(averageColor); painter.setBrush(averageColor); painter.drawRect(mXstart+average-1,mYstart,1,barHeight); // bar is 1 pixel wide, height = meter start? // Peak: painter.setPen(peakColor); painter.setBrush(peakColor); if(peak > peakRedLevel) { painter.setBrush(Qt::red); painter.setPen(Qt::red); } painter.drawRect(mXstart+peak-1,mYstart,2,barHeight); } if(drawLabels) { drawLabel(&painter); } } void meter::drawLabel(QPainter *qp) { qp->setPen(lowLineColor); qp->drawText(0,scaleTextYstart, label ); } void meter::setLevel(int current) { this->current = current; avgLevels[(avgPosition++)%averageBalisticLength] = current; peakLevels[(peakPosition++)%peakBalisticLength] = current; int sum=0; for(unsigned int i=0; i < (unsigned int)std::min(avgPosition, (int)avgLevels.size()); i++) { sum += avgLevels.at(i); } this->average = sum / std::min(avgPosition, (int)avgLevels.size()); this->peak = 0; for(unsigned int i=0; i < peakLevels.size(); i++) { if( peakLevels.at(i) > this->peak) this->peak = peakLevels.at(i); } this->update(); } void meter::setLevels(int current, int peak, int average) { this->current = current; this->peak = peak; this->average = average; this->update(); } void meter::updateDrawing(int num) { fontSize = num; length = num; } // The drawScale functions draw the numbers and number unerline for each type of meter void meter::drawScaleRaw(QPainter *qp) { qp->setPen(lowTextColor); //qp->setFont(QFont("Arial", fontSize)); int i=mXstart; for(; idrawText(i,scaleTextYstart, QString("%1").arg(i) ); } // Now the lines: qp->setPen(lowLineColor); // Line: X1, Y1 -->to--> X2, Y2 qp->drawLine(mXstart,scaleLineYstart,peakRedLevel+mXstart,scaleLineYstart); qp->setPen(Qt::red); qp->drawLine(peakRedLevel+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); } void meter::drawScaleVd(QPainter *qp) { qp->setPen(lowTextColor); //qp->setFont(QFont("Arial", fontSize)); // 7300/9700 and others: int midPointDn = 13; int midPointVd = 10; // 705: //int midPointDn = 75; //int midPointVd = 5; int highPointDn = 241; int highPointVd = 16; float VdperDn = (float)(highPointVd-midPointVd) / float(highPointDn-midPointDn); int i=mXstart; for(; idrawText(i,scaleTextYstart, QString("%1").arg( (int)((i-mXstart) * (float(midPointVd) / float(midPointDn)) )) ); } for(; idrawText(i,scaleTextYstart, QString("%1").arg( (int) std::round( ((i-mXstart-midPointDn) * (VdperDn) ) + (midPointVd) ))); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); } // Now the lines: qp->setPen(lowLineColor); // Line: X1, Y1 -->to--> X2, Y2 qp->drawLine(mXstart,scaleLineYstart,peakRedLevel+mXstart,scaleLineYstart); qp->setPen(Qt::red); qp->drawLine(peakRedLevel+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); } void meter::drawScaleCenter(QPainter *qp) { // No known units qp->setPen(lowLineColor); qp->drawText(60+mXstart,scaleTextYstart, QString("-")); qp->setPen(Qt::green); // Attempt to draw the zero at the actual center qp->drawText(128-2+mXstart,scaleTextYstart, QString("0")); qp->setPen(lowLineColor); qp->drawText(195+mXstart,scaleTextYstart, QString("+")); qp->setPen(lowLineColor); qp->drawLine(mXstart,scaleLineYstart,128-32+mXstart,scaleLineYstart); qp->setPen(Qt::green); qp->drawLine(128-32+mXstart,scaleLineYstart,128+32+mXstart,scaleLineYstart); qp->setPen(lowLineColor); qp->drawLine(128+32+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); } void meter::drawScalePo(QPainter *qp) { //From the manual: "0000=0% to 0143=50% to 0213=100%" float dnPerWatt = 143.0f / 50.0f; qp->setPen(lowTextColor); //qp->setFont(QFont("Arial", fontSize)); int i=mXstart; // 13.3 DN per s-unit: int p=0; for(; idrawText(i,scaleTextYstart, QString("%1").arg(10*(p++)) ); } // Modify current scale position: // Here, P is now 60 watts: // Higher scale: i = i - (int)(10*dnPerWatt); // back one tick first. Otherwise i starts at 178. //qDebug() << "meter i: " << i; dnPerWatt = (213-143.0f) / 50.0f; // 1.4 dn per watt // P=5 here. qp->setPen(Qt::yellow); int k=0; for(i=mXstart+143; idrawText(i,scaleTextYstart, QString("%1").arg(k) ); } // Now we're out past 100: qp->setPen(Qt::red); for(i=mXstart+213; idrawText(i,scaleTextYstart, QString("%1").arg(k) ); } // Now the lines: qp->setPen(lowLineColor); // Line: X1, Y1 -->to--> X2, Y2 qp->drawLine(mXstart,scaleLineYstart,213+mXstart,scaleLineYstart); qp->setPen(Qt::red); qp->drawLine(213+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); (void)qp; } void meter::drawScaleRxdB(QPainter *qp) { (void)qp; } void meter::drawScaleALC(QPainter *qp) { // From the manual: 0000=Minimum to 0120=Maximum qp->setPen(lowTextColor); //qp->setFont(QFont("Arial", fontSize)); int i=mXstart; int alc=0; for(; idrawText(i,scaleTextYstart, QString("%1").arg(alc) ); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); alc +=20; } qp->setPen(Qt::red); for(; idrawText(i,scaleTextYstart, QString("+%1").arg(alc) ); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); alc +=10; } qp->setPen(lowLineColor); qp->drawLine(mXstart,scaleLineYstart,100+mXstart,scaleLineYstart); qp->setPen(Qt::red); qp->drawLine(100+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); (void)qp; } void meter::drawScaleComp(QPainter *qp) { // // 0000=0 dB, 0130=15 dB,0241=30 dB // qp->setPen(lowTextColor); int midPointDn = 130; int midPointdB = 15; int highPointDn = 241; int highPointdB = 30; float dBperDn = (float)(highPointdB-midPointdB) / float(highPointDn-midPointDn); int i=mXstart; for(; idrawText(i,scaleTextYstart, QString("%1").arg( (int)((i-mXstart) * (float(midPointdB) / float(midPointDn)) )) ); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); } i = midPointDn+60; for(; idrawText(i,scaleTextYstart, QString("%1").arg( (int) std::round( ((i-mXstart-midPointDn) * (dBperDn) ) + (midPointdB) ))); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); } // Now the lines: qp->setPen(lowLineColor); // Line: X1, Y1 -->to--> X2, Y2 qp->drawLine(mXstart,scaleLineYstart,peakRedLevel+mXstart,scaleLineYstart); qp->setPen(Qt::red); qp->drawLine(peakRedLevel+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); } void meter::drawScaleSWR(QPainter *qp) { // From the manual: // 0000=SWR1.0, // 0048=SWR1.5, // 0080=SWR2.0, // 0120=SWR3.0 qp->drawText(mXstart,scaleTextYstart, QString("1.0")); qp->drawText(24+mXstart,scaleTextYstart, QString("1.3")); qp->drawText(48+mXstart,scaleTextYstart, QString("1.5")); qp->drawText(80+mXstart,scaleTextYstart, QString("2.0")); qp->drawText(100+mXstart,scaleTextYstart, QString("2.5")); qp->drawText(120+mXstart,scaleTextYstart, QString("3.0")); qp->drawLine( 0+mXstart,scaleTextYstart, 0+mXstart, scaleTextYstart+5); qp->drawLine( 24+mXstart,scaleTextYstart, 24+mXstart, scaleTextYstart+5); qp->drawLine( 48+mXstart,scaleTextYstart, 48+mXstart, scaleTextYstart+5); qp->drawLine( 80+mXstart,scaleTextYstart, 80+mXstart, scaleTextYstart+5); qp->drawLine(100+mXstart,scaleTextYstart,100+mXstart, scaleTextYstart+5); // does not draw? qp->drawLine(120+mXstart,scaleTextYstart,120+mXstart, scaleTextYstart+5); qp->setPen(lowLineColor); qp->drawLine(mXstart,scaleLineYstart,100+mXstart,scaleLineYstart); qp->setPen(Qt::red); qp->drawLine(100+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); } void meter::drawScaleId(QPainter *qp) { // IC-7300: // 0000=0, 0097=10, 0146=15, 0241=25 // qp->setPen(lowTextColor); //qp->setFont(QFont("Arial", fontSize)); // 7300/9700 and others: int midPointDn = 97; int midPointId = 10; int highPointDn = 146; int highPointId = 15; float IdperDn = (float)(highPointId-midPointId) / float(highPointDn-midPointDn); int i=mXstart; for(; idrawText(i,scaleTextYstart, QString("%1").arg( (int)((i-mXstart) * (float(midPointId) / float(midPointDn)) )) ); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); } for(; idrawText(i,scaleTextYstart, QString("%1").arg( (int) std::round( ((i-mXstart-midPointDn) * (IdperDn) ) + (midPointId) ))); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); } // Now the lines: qp->setPen(lowLineColor); // Line: X1, Y1 -->to--> X2, Y2 qp->drawLine(mXstart,scaleLineYstart,peakRedLevel+mXstart,scaleLineYstart); qp->setPen(Qt::red); qp->drawLine(peakRedLevel+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); } void meter::drawScaleS(QPainter *qp) { // // 0000=S0, 0120=S9, 0241=S9+60dB // 120 / 9 = 13.333 steps per S-unit qp->setPen(lowTextColor); //qp->setFont(QFont("Arial", fontSize)); int i=mXstart; // 13.3 DN per s-unit: int s=0; for(; idrawText(i,scaleTextYstart, QString("%1").arg(s++) ); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); } // 2 DN per 1 dB now: // 20 DN per 10 dB // 40 DN per 20 dB // Modify current scale position: s = 20; i+=20; qp->setPen(Qt::red); for(; idrawText(i,scaleTextYstart, QString("+%1").arg(s) ); qp->drawLine(i,scaleTextYstart, i, scaleTextYstart+5); s = s + 20; } qp->setPen(lowLineColor); qp->drawLine(mXstart,scaleLineYstart,peakRedLevel+mXstart,scaleLineYstart); qp->setPen(Qt::red); qp->drawLine(peakRedLevel+mXstart,scaleLineYstart,255+mXstart,scaleLineYstart); } wfview-1.2d/meter.h000066400000000000000000000040601415164626400143250ustar00rootroot00000000000000#ifndef METER_H #define METER_H #include #include #include #include #include #include #include "rigcommander.h" // for meter types class meter : public QWidget { Q_OBJECT public: explicit meter(QWidget *parent = nullptr); signals: public slots: void paintEvent(QPaintEvent *); void updateDrawing(int num); void setLevels(int current, int peak, int average); void setLevel(int current); void clearMeterOnPTTtoggle(); void clearMeter(); void setMeterType(meterKind type); void setMeterShortString(QString); QString getMeterShortString(); meterKind getMeterType(); private: //QPainter painter; meterKind meterType; QString meterShortString; int fontSize = 10; int length=30; int current=0; int peak = 0; int average = 0; int averageBalisticLength = 30; int peakBalisticLength = 30; int avgPosition=0; int peakPosition=0; std::vector avgLevels; std::vector peakLevels; int peakRedLevel=0; bool drawLabels = true; int mXstart = 0; // Starting point for S=0. int mYstart = 14; // height, down from top, where the drawing starts int barHeight = 10; // Height of meter "bar" indicators int scaleLineYstart = 12; int scaleTextYstart = 10; int widgetWindowHeight = mYstart + barHeight + 0; // height of drawing canvis. void drawScaleS(QPainter *qp); void drawScaleCenter(QPainter *qp); void drawScalePo(QPainter *qp); void drawScaleRxdB(QPainter *qp); void drawScaleALC(QPainter *qp); void drawScaleSWR(QPainter *qp); void drawScaleVd(QPainter *qp); void drawScaleId(QPainter *qp); void drawScaleComp(QPainter *qp); void drawScaleRaw(QPainter *qp); void drawLabel(QPainter *qp); QString label; QColor currentColor; QColor averageColor; QColor peakColor; // S0-S9: QColor lowTextColor; QColor lowLineColor; // S9+: QColor highTextColor; QColor highLineColor; }; #endif // METER_H wfview-1.2d/packettypes.h000066400000000000000000000271251415164626400155540ustar00rootroot00000000000000#ifndef PACKETTYPES_H #define PACKETTYPES_H #include #pragma pack(push, 1) // Fixed Size Packets #define CONTROL_SIZE 0x10 #define WATCHDOG_SIZE 0x14 #define PING_SIZE 0x15 #define OPENCLOSE_SIZE 0x16 #define RETRANSMIT_RANGE_SIZE 0x18 #define TOKEN_SIZE 0x40 #define STATUS_SIZE 0x50 #define LOGIN_RESPONSE_SIZE 0x60 #define LOGIN_SIZE 0x80 #define CONNINFO_SIZE 0x90 #define CAPABILITIES_SIZE 0xA8 // Variable size packets + payload #define CIV_SIZE 0x15 #define AUDIO_SIZE 0x18 #define DATA_SIZE 0x15 // 0x10 length control packet (connect/disconnect/idle.) typedef union control_packet { struct { quint32 len; quint16 type; quint16 seq; quint32 sentid; quint32 rcvdid; }; char packet[CONTROL_SIZE]; } *control_packet_t; // 0x14 length watchdog packet typedef union watchdog_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c quint16 secondsa; // 0x10 quint16 secondsb; // 0x12 }; char packet[WATCHDOG_SIZE]; } *watchdog_packet_t; // 0x15 length ping packet // Also used for the slightly different civ header packet. typedef union ping_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c char reply; // 0x10 union { // This contains differences between the send/receive packet struct { // Ping quint32 time; // 0x11 }; struct { // Send quint16 datalen; // 0x11 quint16 sendseq; //0x13 }; }; }; char packet[PING_SIZE]; } *ping_packet_t, * data_packet_t, data_packet; // 0x16 length open/close packet typedef union openclose_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c quint16 data; // 0x10 char unused; // 0x11 quint16 sendseq; //0x13 char magic; // 0x15 }; char packet[OPENCLOSE_SIZE]; } *startstop_packet_t; // 0x18 length audio packet typedef union audio_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c quint16 ident; // 0x10 quint16 sendseq; // 0x12 quint16 unused; // 0x14 quint16 datalen; // 0x16 }; char packet[AUDIO_SIZE]; } *audio_packet_t; // 0x18 length retransmit_range packet typedef union retransmit_range_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c quint16 first; // 0x10 quint16 second; // 0x12 quint16 third; // 0x14 quint16 fourth; // 0x16 }; char packet[RETRANSMIT_RANGE_SIZE]; } *retransmit_range_packet_t; // 0x18 length txaudio packet /* tx[0] = static_cast(tx.length() & 0xff); tx[1] = static_cast(tx.length() >> 8 & 0xff); tx[18] = static_cast(sendAudioSeq >> 8 & 0xff); tx[19] = static_cast(sendAudioSeq & 0xff); tx[22] = static_cast(partial.length() >> 8 & 0xff); tx[23] = static_cast(partial.length() & 0xff);*/ // 0x40 length token packet typedef union token_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c char unuseda[3]; // 0x10 quint16 code; // 0x13 quint16 res; // 0x15 quint8 innerseq; // 0x17 char unusedb; // 0x18 char unusedc; // 0x19 quint16 tokrequest; // 0x1a quint32 token; // 0x1c char unusedd[7]; // 0x20 quint16 commoncap; // 0x27 char unuseddd[2]; // 0x29 char identa; // 0x2b quint32 identb; // 0x2c quint32 response; // 0x30 char unusede[12]; // 0x34 }; char packet[TOKEN_SIZE]; } *token_packet_t; // 0x50 length login status packet typedef union status_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c char unuseda[3]; // 0x10 quint16 code; // 0x13 quint16 res; // 0x15 quint8 innerseq; // 0x17 char unusedb; // 0x18 char unusedc; // 0x19 quint16 tokrequest; // 0x1a quint32 token; // 0x1c char unusedd[6]; // 0x20 quint16 unknown; // 0x26 char unusede; // 0x28 char unusedf[2]; // 0x29 char identa; // 0x2b quint32 identb; // 0x2c quint32 error; // 0x30 char unusedg[12]; // 0x34 char disc; // 0x40 char unusedh; // 0x41 quint16 civport; // 0x42 // Sent bigendian quint16 unusedi; // 0x44 // Sent bigendian quint16 audioport; // 0x46 // Sent bigendian char unusedj[7]; // 0x49 }; char packet[STATUS_SIZE]; } *status_packet_t; // 0x60 length login status packet typedef union login_response_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c char unuseda[3]; // 0x10 quint16 code; // 0x13 quint16 res; // 0x15 quint8 innerseq; // 0x17 char unusedb; // 0x18 char unusedc; // 0x19 quint16 tokrequest; // 0x1a quint32 token; // 0x1c quint16 authstartid; // 0x20 char unusedd[14]; // 0x22 quint32 error; // 0x30 char unusede[12]; // 0x34 char connection[16]; // 0x40 char unusedf[16]; // 0x50 }; char packet[LOGIN_RESPONSE_SIZE]; } *login_response_packet_t; // 0x80 length login packet typedef union login_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c char unuseda[3]; // 0x10 quint16 code; // 0x13 quint16 res; // 0x15 quint8 innerseq; // 0x17 char unusedaa; // 0x18; char unusedb; // 0x19 quint16 tokrequest; // 0x1a quint32 token; // 0x1c char unusedc[32]; // 0x20 char username[16]; // 0x40 char password[16]; // 0x50 char name[16]; // 0x60 char unusedf[16]; // 0x70 }; char packet[LOGIN_SIZE]; } *login_packet_t; // 0x90 length conninfo and stream request packet typedef union conninfo_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c char unuseda[3]; // 0x10 quint16 code; // 0x13 quint16 res; // 0x15 quint8 innerseq; // 0x17 char unusedaa; // 0x18 char unusedb; // 0x19 quint16 tokrequest; // 0x1a quint32 token; // 0x1c quint16 authstartid; // 0x20 char unusedd[5]; // 0x22 quint32 commoncap; // 0x27 char identa; // 0x2b quint32 identb; // 0x2c char unusedf[16]; // 0x30 char name[16]; // 0x40 char unusedg[16]; // 0x50 union { // This contains differences between the send/receive packet struct { // Receive quint32 busy; // 0x60 char computer[16]; // 0x64 char unusedi[16]; // 0x74 quint32 ipaddress; // 0x84 char unusedj[8]; // 0x78 }; struct { // Send char username[16]; // 0x60 char rxenable; // 0x70 char txenable; // 0x71 char rxcodec; // 0x72 char txcodec; // 0x73 quint32 rxsample; // 0x74 quint32 txsample; // 0x78 quint32 civport; // 0x7c quint32 audioport; // 0x80 quint32 txbuffer; // 0x84 quint8 convert; // 0x88 char unusedl[7]; // 0x89 }; }; }; char packet[CONNINFO_SIZE]; } *conninfo_packet_t; // 0xA8 length capabilities packet typedef union capabilities_packet { struct { quint32 len; // 0x00 quint16 type; // 0x04 quint16 seq; // 0x06 quint32 sentid; // 0x08 quint32 rcvdid; // 0x0c char unuseda[3]; // 0x10 quint16 code; // 0x13 quint16 res; // 0x15 quint8 innerseq; // 0x17 char unusedb; // 0x18 char unusedc; // 0x19 quint16 tokrequest; // 0x1a quint32 token; // 0x1c char unusedd[33]; // 0x20 char capa; // 0x41 char unusede[7]; // 0x42 quint16 commoncap; // 0x49 char unused; // 0x4b char macaddress[6]; // 0x4c char name[32]; // 0x52 char audio[32]; // 0x72 quint16 conntype; // 0x92 char civ; // 0x94 quint16 rxsample; // 0x95 quint16 txsample; // 0x97 quint8 enablea; // 0x99 quint8 enableb; // 0x9a quint8 enablec; // 0x9b quint32 baudrate; // 0x9c quint16 capf; // 0xa0 char unusedi; // 0xa2 quint16 capg; // 0xa3 char unusedj[3]; // 0xa5 }; char packet[CAPABILITIES_SIZE]; } *capabilities_packet_t; #pragma pack(pop) #endif // PACKETTYPES_H wfview-1.2d/pttyhandler.cpp000066400000000000000000000236101415164626400161040ustar00rootroot00000000000000#include "pttyhandler.h" #include "logcategories.h" #include #include #ifndef Q_OS_WIN #include #include #include #include #endif // Copyright 2017-2021 Elliott H. Liggett & Phil Taylor pttyHandler::pttyHandler(QString pty) { //constructor if (pty == "" || pty.toLower() == "none") { // Just return if pty is not configured. return; } portName = pty; #ifdef Q_OS_WIN // TODO: The following should become arguments and/or functions // Add signal/slot everywhere for comm port setup. // Consider how to "re-setup" and how to save the state for next time. baudRate = 115200; stopBits = 1; portName = pty; #endif openPort(); } void pttyHandler::openPort() { serialError = false; bool success=false; #ifdef Q_OS_WIN port = new QSerialPort(); port->setPortName(portName); port->setBaudRate(baudRate); port->setStopBits(QSerialPort::OneStop);// OneStop is other option success = port->open(QIODevice::ReadWrite); if (success) { connect(port, &QSerialPort::readyRead, this, std::bind(&pttyHandler::receiveDataIn, this, (int)0)); } #else // Generic method in Linux/MacOS to find a pty ptfd = ::posix_openpt(O_RDWR | O_NONBLOCK); if (ptfd >=0) { qInfo(logSerial()) << "Opened pt device: " << ptfd << ", attempting to grant pt status"; if (grantpt(ptfd)) { qInfo(logSerial()) << "Failed to grantpt"; return; } if (unlockpt(ptfd)) { qInfo(logSerial()) << "Failed to unlock pt"; return; } // we're good! qInfo(logSerial()) << "Opened pseudoterminal, slave name :" << ptsname(ptfd); // Open the slave device to keep alive. ptKeepAlive = open(ptsname(ptfd), O_RDONLY); ptReader = new QSocketNotifier(ptfd, QSocketNotifier::Read, this); connect(ptReader, &QSocketNotifier::activated, this, &pttyHandler::receiveDataIn); success=true; } #endif if (!success) { ptfd = 0; qInfo(logSerial()) << "Could not open pseudo terminal port, please restart."; isConnected = false; serialError = true; emit haveSerialPortError(portName, "Could not open pseudo terminal port. Please restart."); return; } #ifndef Q_OS_WIN ptDevSlave = QString::fromLocal8Bit(ptsname(ptfd)); if (portName != "" && portName.toLower() != "none") { if (!QFile::link(ptDevSlave, portName)) { qInfo(logSerial()) << "Error creating link to" << ptDevSlave << "from" << portName; } else { qInfo(logSerial()) << "Created link to" << ptDevSlave << "from" << portName; } } #endif isConnected = true; } pttyHandler::~pttyHandler() { this->closePort(); } void pttyHandler::receiveDataFromRigToPtty(const QByteArray& data) { int fePos=data.lastIndexOf((char)0xfe); if (fePos > 0 && data.length() > fePos+2) fePos=fePos-1; else { qDebug(logSerial()) << "Invalid command"; printHex(data,false,true); } if (disableTransceive && ((unsigned char)data[fePos + 2] == 0x00 || (unsigned char)data[fePos + 3] == 0x00)) { // Ignore data that is sent to/from transceive address as client has requested transceive disabled. qDebug(logSerial()) << "Transceive command filtered"; return; } if (isConnected && (unsigned char)data[fePos + 2] != 0xE1 && (unsigned char)data[fePos + 3] != 0xE1) { // send to the pseudo port as well // index 2 is dest, 0xE1 is wfview, 0xE0 is assumed to be the other device. // Changed to "Not 0xE1" // 0xE1 = wfview // 0xE0 = pseudo-term host // 0x00 = broadcast to all //qInfo(logSerial()) << "Sending data from radio to pseudo-terminal"; sendDataOut(data); } } void pttyHandler::sendDataOut(const QByteArray& writeData) { qint64 bytesWritten = 0; //qInfo(logSerial()) << "Data to pseudo term:"; //printHex(writeData, false, true); if (isConnected) { mutex.lock(); #ifdef Q_OS_WIN bytesWritten = port->write(writeData); #else bytesWritten = ::write(ptfd, writeData.constData(), writeData.size()); #endif if (bytesWritten != writeData.length()) { qInfo(logSerial()) << "bytesWritten: " << bytesWritten << " length of byte array: " << writeData.length()\ << " size of byte array: " << writeData.size()\ << " Wrote all bytes? " << (bool)(bytesWritten == (qint64)writeData.size()); } mutex.unlock(); } } void pttyHandler::receiveDataIn(int fd) { #ifndef Q_OS_WIN ssize_t available = 255; // Read up to 'available' bytes #else Q_UNUSED(fd); #endif // Linux will correctly return the number of available bytes with the FIONREAD ioctl // Sadly MacOS always returns zero! #ifdef Q_OS_LINUX int ret = ::ioctl(fd, FIONREAD, (char *) &available); if (ret != 0) return; #endif #ifdef Q_OS_WIN port->startTransaction(); inPortData = port->readAll(); #else inPortData.resize(available); ssize_t got = ::read(fd, inPortData.data(), available); int err = errno; if (got < 0) { qInfo(logSerial()) << tr("Read failed: %1").arg(QString::fromLatin1(strerror(err))); return; } inPortData.resize(got); #endif if (inPortData.startsWith("\xFE\xFE")) { if (inPortData.endsWith("\xFD")) { // good! #ifdef Q_OS_WIN port->commitTransaction(); #endif int lastFE = inPortData.lastIndexOf((char)0xfe); if (civId == 0 && inPortData.length() > lastFE + 2 && (quint8)inPortData[lastFE + 2] > (quint8)0xdf && (quint8)inPortData[lastFE + 2] < (quint8)0xef) { // This is (should be) the remotes CIV id. civId = (quint8)inPortData[lastFE + 2]; qInfo(logSerial()) << "pty detected remote CI-V:" << hex << civId; } else if (civId != 0 && inPortData.length() > lastFE + 2 && (quint8)inPortData[lastFE + 2] != civId) { civId = (quint8)inPortData[lastFE + 2]; qInfo(logSerial()) << "pty remote CI-V changed:" << hex << (quint8)civId; } // filter C-IV transceive command before forwarding on. if (inPortData.contains(rigCaps.transceiveCommand)) { //qInfo(logSerial()) << "Filtered transceive command"; //printHex(inPortData, false, true); QByteArray reply= QByteArrayLiteral("\xfe\xfe\x00\x00\xfb\xfd"); reply[2] = inPortData[3]; reply[3] = inPortData[2]; sendDataOut(inPortData); // Echo command back sendDataOut(reply); if (!disableTransceive) { qInfo(logSerial()) << "pty requested CI-V Transceive disable"; disableTransceive = true; } } else if (inPortData.length() > lastFE + 2 && ((quint8)inPortData[lastFE + 1] == civId || (quint8)inPortData[lastFE + 2] == civId)) { emit haveDataFromPort(inPortData); qDebug(logSerial()) << "Data from pseudo term:"; printHex(inPortData, false, true); } if (rolledBack) { // qInfo(logSerial()) << "Rolled back and was successfull. Length: " << inPortData.length(); //printHex(inPortData, false, true); rolledBack = false; } } else { // did not receive the entire thing so roll back: // qInfo(logSerial()) << "Rolling back transaction. End not detected. Lenth: " << inPortData.length(); //printHex(inPortData, false, true); rolledBack = true; #ifdef Q_OS_WIN port->rollbackTransaction(); } } else { port->commitTransaction(); // do not emit data, do not keep data. //qInfo(logSerial()) << "Warning: received data with invalid start. Dropping data."; //qInfo(logSerial()) << "THIS SHOULD ONLY HAPPEN ONCE!!"; // THIS SHOULD ONLY HAPPEN ONCE! } #else } } #endif } void pttyHandler::closePort() { #ifdef Q_OS_WIN if (port != Q_NULLPTR) { port->close(); delete port; } #else if (isConnected && portName != "" && portName.toLower() != "none") { QFile::remove(portName); } if (ptKeepAlive > 0) { close(ptKeepAlive); } #endif isConnected = false; } void pttyHandler::debugThis() { // Do not use, function is for debug only and subject to change. qInfo(logSerial()) << "comm debug called."; inPortData = port->readAll(); emit haveDataFromPort(inPortData); } void pttyHandler::printHex(const QByteArray& pdata, bool printVert, bool printHoriz) { qDebug(logSerial()) << "---- Begin hex dump -----:"; QString sdata("DATA: "); QString index("INDEX: "); QStringList strings; for (int i = 0; i < pdata.length(); i++) { strings << QString("[%1]: %2").arg(i, 8, 10, QChar('0')).arg((unsigned char)pdata[i], 2, 16, QChar('0')); sdata.append(QString("%1 ").arg((unsigned char)pdata[i], 2, 16, QChar('0'))); index.append(QString("%1 ").arg(i, 2, 10, QChar('0'))); } if (printVert) { for (int i = 0; i < strings.length(); i++) { //sdata = QString(strings.at(i)); qDebug(logSerial()) << strings.at(i); } } if (printHoriz) { qDebug(logSerial()) << index; qDebug(logSerial()) << sdata; } qDebug(logSerial()) << "----- End hex dump -----"; } void pttyHandler::receiveFoundRigID(rigCapabilities rigCaps) { this->rigCaps = rigCaps; qInfo(logSerial) << "Received rigCapabilities for" << rigCaps.modelName; } wfview-1.2d/pttyhandler.h000066400000000000000000000036631415164626400155570ustar00rootroot00000000000000#ifndef PTTYHANDLER_H #define PTTYHANDLER_H #include #include #include #include #include #include #include "rigidentities.h" // This class abstracts the comm port in a useful way and connects to // the command creator and command parser. class pttyHandler : public QObject { Q_OBJECT public: pttyHandler(QString portName); pttyHandler(QString portName, quint32 baudRate); bool serialError; ~pttyHandler(); private slots: void receiveDataIn(int fd); // from physical port void receiveDataFromRigToPtty(const QByteArray& data); void debugThis(); void receiveFoundRigID(rigCapabilities rigCaps); signals: void haveTextMessage(QString message); // status, debug only void haveDataFromPort(QByteArray data); // emit this when we have data, connect to rigcommander void haveSerialPortError(const QString port, const QString error); void haveStatusUpdate(const QString text); private: void setupPtty(); void openPort(); void closePort(); void sendDataOut(const QByteArray& writeData); // out to radio void debugMe(); void hexPrint(); //QDataStream stream; QByteArray outPortData; QByteArray inPortData; //QDataStream outStream; //QDataStream inStream; unsigned char buffer[256]; QString portName; QSerialPort* port = Q_NULLPTR; qint32 baudRate; unsigned char stopBits; bool rolledBack; int ptfd; // pseudo-terminal file desc. int ptKeepAlive=0; // Used to keep the pty alive after client disconects. bool havePt; QString ptDevSlave; bool isConnected=false; // port opened mutable QMutex mutex; void printHex(const QByteArray& pdata, bool printVert, bool printHoriz); bool disableTransceive = false; QSocketNotifier *ptReader = Q_NULLPTR; quint8 civId=0; rigCapabilities rigCaps; }; #endif // PTTYHANDLER_H wfview-1.2d/qdarkstyle/000077500000000000000000000000001415164626400152235ustar00rootroot00000000000000wfview-1.2d/qdarkstyle/rc/000077500000000000000000000000001415164626400156275ustar00rootroot00000000000000wfview-1.2d/qdarkstyle/rc/Hmovetoolbar.png000066400000000000000000000003341415164626400207760ustar00rootroot00000000000000PNG  IHDR@}bKGDyyS pHYs  tIME-JiTXtCommentCreated with GIMPd.e@IDATX1 @}[_SE2v^%"ft6A BxWDIENDB`wfview-1.2d/qdarkstyle/rc/Hsepartoolbar.png000066400000000000000000000002541415164626400211430ustar00rootroot00000000000000PNG  IHDR?,{bKGDyyS pHYs  tIME.Į9IDAT8c` 3ǣ1aSSΑ(ɂf QQQI|h*I>K9.?IENDB`wfview-1.2d/qdarkstyle/rc/Vmovetoolbar.png000066400000000000000000000003441415164626400210150ustar00rootroot00000000000000PNG  IHDR6  sRGBbKGDަ pHYs  tIME *+\dIDATHc0< 4Oͪs]9H+5yȈ$|O-5sl&}MTR M&ZjFhRIzLKl4cz0J q-nIENDB`wfview-1.2d/qdarkstyle/rc/Vsepartoolbar.png000066400000000000000000000002731415164626400211620ustar00rootroot00000000000000PNG  IHDR?vsRGBbKGD pHYs  tIME 5+URj;IDAT8c`#0}*%v;s_9Si4 M! xc~Tid kXIENDB`wfview-1.2d/qdarkstyle/rc/branch_closed-on.png000066400000000000000000000002231415164626400215320ustar00rootroot00000000000000PNG  IHDR bKGDӵW\ pHYs  tIME  +J<0t$IDATc`@XL\&dY&dp##Ȉa  +uIENDB`wfview-1.2d/qdarkstyle/rc/branch_closed.png000066400000000000000000000002401415164626400211170ustar00rootroot00000000000000PNG  IHDR sRGBbKGDS4] pHYs  tIME )G$IDATc`@s>XL\&dY&dpN"a )4IENDB`wfview-1.2d/qdarkstyle/rc/branch_open-on.png000066400000000000000000000002261415164626400212250ustar00rootroot00000000000000PNG  IHDR |NbKGDӵW\ pHYs  tIME  u1'IDATe upԐPxU!TpH 4+IENDB`wfview-1.2d/qdarkstyle/rc/branch_open.png000066400000000000000000000002461415164626400206150ustar00rootroot00000000000000PNG  IHDR |NsRGBbKGDS4] pHYs  tIME M[o*IDATc` s> 020 000BPƹ]fIENDB`wfview-1.2d/qdarkstyle/rc/checkbox_checked.png000066400000000000000000000007541415164626400215770ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<iIDATX;N@W܅DpBe-S;PGQ@yzVnIENDB`wfview-1.2d/qdarkstyle/rc/checkbox_checked_disabled.png000066400000000000000000000007531415164626400234250ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<hIDATXMN@WCvX&p11tHx xqυ ,\hLyMo P #+p'" -7&*O"Z֞Snoo(9p^.GAPֶN E\'t H=a> .eYT_f<~r0:ߥe)w#@"$@"4OH˦3y_RZn:LVQh֞10UcUu"oQhۑ{qj'DtOIENDB`wfview-1.2d/qdarkstyle/rc/checkbox_checked_focus.png000066400000000000000000000003741415164626400227740ustar00rootroot00000000000000PNG  IHDR szzbKGD pHYs B(xtIME 9AIDATXc`?Whi@1&FwQ3RR\3au!-g&=,g6@RZA?hB`uF0Qlв48+&Z4H1stL5qz]8e9DIENDB`wfview-1.2d/qdarkstyle/rc/checkbox_indeterminate.png000066400000000000000000000007551415164626400230420ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<jIDATXN@Cx}Ԅwei q#qK|7 Ľؘ!v&N9# UkLPT8vq>@ +"$s"M`yuW=@v&QXcIᏥry |8k|C5>]R$r L8w giO@|}vMsyV l%&1n'PV@!?u@!* fZ8k"[kI PMf(6OPiTUF1yqBW=$}BE֘,fzRv10@o=6zVIENDB`wfview-1.2d/qdarkstyle/rc/checkbox_indeterminate_disabled.png000066400000000000000000000007541415164626400246700ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<iIDATXJ@FMZ@}+ts]혈 &e} ץK{]hJFT4nf.9gf!=o@ XqynD"J , IGG@XڣLb"?5izI9"{& M՜oc}pvމ7w9v~$Y(Ejm@y'Y I)ph 0F~ (JR(:(~ "ΈM@4bZmT'r ,+0 %_N"_3_R$d1*r.Pu"Tn7u?j'**IENDB`wfview-1.2d/qdarkstyle/rc/checkbox_indeterminate_focus.png000066400000000000000000000003711415164626400242330ustar00rootroot00000000000000PNG  IHDR szzbKGD pHYs B(xtIME :iNwIDATXc`?Whi@1&FwQ3RR\3au!-g&=,g6@Q:|Kl8F0ASвVQ nP$d6=b|f ݺft*^8e9IENDB`wfview-1.2d/qdarkstyle/rc/checkbox_unchecked.png000066400000000000000000000007201415164626400221330ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<MIDATX;N@EQ+RBʍXpBe)h(w |B>pLx*[Zyf%G/*6ȥ)!0h)@U%[K`ȫڈ} f~Ʊ/l1s'-Itfj`}F@CXpZ`oҴD˛?( ]s0|&d`xJ^ )W::(,(L*uf@7INUfk1x>-tyPUY+ʼn]-|1)f֚~F6)`LQ;G#|u08IENDB`wfview-1.2d/qdarkstyle/rc/checkbox_unchecked_disabled.png000066400000000000000000000007201415164626400237620ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<MIDATXMN@[+ [!I>bBBiIx ׄpqׅɛy~n:#kϘkK=HDZAFgLԀ)0^- (IEcK]4w>"&6KK8K6[f*Lg gI xBdjyʿ\]_I H) R:`HIk!0(JuKjVL}gD@NEZäxlb]xuB'[6&ɨo̕4Q= V_|]׋-G >pdAiIENDB`wfview-1.2d/qdarkstyle/rc/checkbox_unchecked_focus.png000066400000000000000000000003601415164626400233320ustar00rootroot00000000000000PNG  IHDR szzbKGD pHYs B(xtIME :+ė}IDATXc`?Whi@1&FwQ3RR\3au!-g&=,g6@Q:`uF0Q0jҲe3h\f ݺf4lM_934+ IENDB`wfview-1.2d/qdarkstyle/rc/close-hover.png000066400000000000000000000011261415164626400205630ustar00rootroot00000000000000PNG  IHDR@@iqbKGD pHYs  tIMEܾiTXtCommentCreated with GIMPd.eIDATx[! EM7.H e9<{h=C+V9*S"YY2dpS)BFJ"d& 2|][ {ery帟Q,}$A8451Qz5%Xm󒰕ǺWۂ"F!<%0NMHi [F@6<jo @=%86:@){FqE;haUZ(<]@ >De0Z8ϖP5 `o36Ej&b$h9!A3GHp;Ea|CQ %gi#iș`|EWb=o =J#y]MnMdk]_,vY:Y$XM'3 )ya>\xG[:D>IENDB`wfview-1.2d/qdarkstyle/rc/close-pressed.png000066400000000000000000000011261415164626400211050ustar00rootroot00000000000000PNG  IHDR@@iqbKGD pHYs  tIME-ziTXtCommentCreated with GIMPd.eIDATx[! EMՎЍe9<{h=C+V9Qw]P )DBd((2"So [EL=d6V ~(+v{帞NQ,}$A8451Qz5%Xm󒰕ǺWۂ"F!<%0NMHi [F@6<jo @=%86:@){FqE;haUZ(<]@ >De0Z8ϖP5 `o36Ej&b$h9!A3GHp;Ea|CQ %gi#iș`|EWb=o =J#y]MnMdk]_,vY:Y$XM'3 )ya>\xQZw#IENDB`wfview-1.2d/qdarkstyle/rc/close.png000066400000000000000000000011121415164626400174350ustar00rootroot00000000000000PNG  IHDR@@iqbKGD pHYs  tIME87iTXtCommentCreated with GIMPd.eIDATxI E#]ӻT& ]2!h xر!̏l;YB@+p ͒!DȌdFpO2;Un!+H{%^ /=U V8s\EwkԻaC[C0Ơq-b%:Wۄ6 "*_Ǣx$0,-y^`D16=586<@ <BZ!,1%h6xg' h 020 000BPƹ]fIENDB`wfview-1.2d/qdarkstyle/rc/left_arrow.png000066400000000000000000000002461415164626400205030ustar00rootroot00000000000000PNG  IHDR sRGBbKGD̿ pHYs  tIME5*IDATc`g``B0 d``b``4D s@d@ uIENDB`wfview-1.2d/qdarkstyle/rc/left_arrow_disabled.png000066400000000000000000000002461415164626400223320ustar00rootroot00000000000000PNG  IHDR sRGBbKGD̿ pHYs  tIME w*IDATc`|```B0 d``b`H@ s@#dIENDB`wfview-1.2d/qdarkstyle/rc/radio_checked.png000066400000000000000000000016541415164626400211070ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<)IDATXOh\UedƽHW:(M M@fc7/%F$PId¼n S⟅uӤRJpJ%3$\7M7Vw9 =uC٘A*eZ>y[-7dVppQp`}TAзl.|kaX:{~\O8p8,(χi3ć'i֝<&zgG̥٤F f6sP`iii& 9&bR̙g=ֵ ?$NNpa&s=u81=Z'̆cIh ^p732;m{]+;սKBV/'qWf)wq'OW|,4[D:b'#5;2) ` a nvu--=ÿxNiBIENDB`wfview-1.2d/qdarkstyle/rc/radio_checked_disabled.png000066400000000000000000000017141415164626400227330ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IIDATXk\Uϝdҝ vh+4U̽T\Dm:M.DWxTƅhUSURp%06h Sk99}9v]n@<>.i H `tMPj9ɜ4)/IA(0 2i*o 뺑d"7JN^ f0{fRt)~@#Ry'oK[=0StɜDZ|T*|asc] Kf+8sdX(IATzp]o`Ϟ_oط]laGlm87ހؿYkLgIx|H)|X슙aӠ \pÉDDxP}? jmZ qVǯHZwyfrwD0&pG/Q'?3q{gbҝ7 \E̺=n`ώjt ؄[t/tJg@ ЀXw @r%1Z)5 lg^%3οc%Ӗ.p9.ԉ TI2C@LQ[@>׆e`_^Uᆕz7hFR!pBE.쨲UPoprʆ] JD eIENDB`wfview-1.2d/qdarkstyle/rc/radio_unchecked_disabled.png000066400000000000000000000013701415164626400232740ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<uIDATXNQ2xVMiX? &7ٶ3 0F4Q.HѸrCtؒʸ7,6@ Tq>De ;lI̙6mwO4pxHD@J!"{jq~~DZ ؙ̰z";q.*2V(iS/|堬eY )T/t4t@:0wB \Q`&X5FC; :fuJR2."d\.ye=i]]Yǹz򿱳ٗ@j`wC@LV^0=3^IZ:p\ XFտBuuD^-pJ-D"TMy`A vEQT5n+oUEeYVmNثVT";BU+owRdT./i ȓ|~շ@RYKp7L~;tq7 7Fc[[9+Tp`vJ\70 "u,J4 ,vm|>`]fq<^K4 cP\7H ; D -o67J7݌jIENDB`wfview-1.2d/qdarkstyle/rc/radio_unchecked_focus.png000066400000000000000000000012061415164626400226420ustar00rootroot00000000000000PNG  IHDR szzbKGD pHYs B(xtIME 7NlčIDATX햿kSQǿ>} q(C.V:I66Ѻ 򪣓67mX T3"-+tIw{s~sL2߅(~8DG(((ڧؼ!fgunn=cf(`Ni"3O8CDMAꩡG‹փ{RiJZl YY]/H ``~PtE*Jփ1lֶl}$O?/M 3ogl`. 30F^: `{; 6by $8̭}X@D; 8m6HRf.hG` d %edz0#+8EDˑζODM68%9bE ٶYJDJ{vT:+;p]`s'~,>ׯmGVVI&HW;jČhNIV'Ⱦ5 9]d2ҬGhaIENDB`wfview-1.2d/qdarkstyle/rc/right_arrow.png000066400000000000000000000002401415164626400206600ustar00rootroot00000000000000PNG  IHDR sRGBbKGD̿ pHYs  tIME$ $IDATc`@XL\&dY&dp!ha m u7#IENDB`wfview-1.2d/qdarkstyle/rc/right_arrow_disabled.png000066400000000000000000000002401415164626400225070ustar00rootroot00000000000000PNG  IHDR sRGBbKGD̿ pHYs  tIME R+$IDATc`@s>XL\&dY&dpN"a )4IENDB`wfview-1.2d/qdarkstyle/rc/sizegrip.png000066400000000000000000000002011415164626400201620ustar00rootroot00000000000000PNG  IHDR%=m"PLTEwk-tRNS@f)IDATx^ X pm(GhUqo%5 IENDB`wfview-1.2d/qdarkstyle/rc/stylesheet-branch-end.png000066400000000000000000000003401415164626400225220ustar00rootroot00000000000000PNG  IHDRQ:ȼsRGBbKGD pHYs  tIME )~V`IDATxٱ AqPcQ퍅%wHEjogEEQDDQDQDEDDEE[uOΈ("6i9U@IENDB`wfview-1.2d/qdarkstyle/rc/stylesheet-branch-more.png000066400000000000000000000002661415164626400227250ustar00rootroot00000000000000PNG  IHDRxl0sRGBbKGD pHYs  tIME , C6IDAT8c` ,ZE`Ԃ`#tMqqqhԂad1 KIENDB`wfview-1.2d/qdarkstyle/rc/stylesheet-vline.png000066400000000000000000000003571415164626400216460ustar00rootroot00000000000000PNG  IHDRQ:ȼsRGBbKGD pHYs  tIME *2 ZoIDATxб 0AȠ ǢI(/]Ys B(!BD! "D"D"DAQ!B(!BD! "D"D"DAQ!BUtVTIENDB`wfview-1.2d/qdarkstyle/rc/transparent.png000066400000000000000000000003031415164626400206720ustar00rootroot00000000000000PNG  IHDR@@iqbKGD pHYs  tIME  .7DiTXtCommentCreated with GIMPd.e'IDATx  Om7w@@zIENDB`wfview-1.2d/qdarkstyle/rc/undock.png000066400000000000000000000011021415164626400176120ustar00rootroot00000000000000PNG  IHDR@@iqbKGDyyS pHYs  tIME;_tMiTXtCommentCreated with GIMPd.eIDATx Ch˷ivIKB^7Q|p(,3f9m%DW1U,֋pQ|&Qx&]|+]x+3jBUvr]g'}/lU*!S_+mwj dqNSτն%x2UNp%W@0[L4ptgCpELWXv+< . {N*O](j9N;WIȜ̂pS$&k0ЯL'u@sj^aB7EAoqBŷ@B)-˜ êF<"Ǯ))e%2*ć !"Z%1 %O4>n2Z"ry&@Ƽ4յ ̋3FS!r6+cIENDB`wfview-1.2d/qdarkstyle/rc/up_arrow.png000066400000000000000000000002361415164626400201740ustar00rootroot00000000000000PNG  IHDR |NsRGBbKGD̿ pHYs  tIME."IDATc`  BH* *ʵvIENDB`wfview-1.2d/qdarkstyle/rc/up_arrow_disabled.png000066400000000000000000000002371415164626400220240ustar00rootroot00000000000000PNG  IHDR |NsRGBbKGD̿ pHYs  tIME# #IDATc` |RB) hNe4IENDB`wfview-1.2d/qdarkstyle/style.qrc000066400000000000000000000033151415164626400170740ustar00rootroot00000000000000 rc/up_arrow_disabled.png rc/Hmovetoolbar.png rc/stylesheet-branch-end.png rc/branch_closed-on.png rc/stylesheet-vline.png rc/branch_closed.png rc/branch_open-on.png rc/transparent.png rc/right_arrow_disabled.png rc/sizegrip.png rc/close.png rc/close-hover.png rc/close-pressed.png rc/down_arrow.png rc/Vmovetoolbar.png rc/left_arrow.png rc/stylesheet-branch-more.png rc/up_arrow.png rc/right_arrow.png rc/left_arrow_disabled.png rc/Hsepartoolbar.png rc/branch_open.png rc/Vsepartoolbar.png rc/down_arrow_disabled.png rc/undock.png rc/checkbox_checked_disabled.png rc/checkbox_checked_focus.png rc/checkbox_checked.png rc/checkbox_indeterminate.png rc/checkbox_indeterminate_focus.png rc/checkbox_unchecked_disabled.png rc/checkbox_unchecked_focus.png rc/checkbox_unchecked.png rc/radio_checked_disabled.png rc/radio_checked_focus.png rc/radio_checked.png rc/radio_unchecked_disabled.png rc/radio_unchecked_focus.png rc/radio_unchecked.png style.qss wfview-1.2d/qdarkstyle/style.qss000066400000000000000000000621531415164626400171220ustar00rootroot00000000000000QToolTip { border: 1px solid #767676; background-color: #5A7566; color: white; padding: 0px; /*remove padding, for fix combobox tooltip.*/ opacity: 200; } QWidget { color: #eff0f1; background-color: #313131; selection-background-color: #3daee9; selection-color: #eff0f1; background-clip: border; border-image: none; border: 0px transparent black; outline: 0; } QWidget:item:hover { background-color: #18465d; color: #eff0f1; } QWidget:item:selected { background-color: #18465d; } QCheckBox { spacing: 5px; outline: none; color: #eff0f1; margin-bottom: 2px; } QCheckBox:disabled { color: #767676; } QCheckBox::indicator, QGroupBox::indicator { width: 18px; height: 18px; } QGroupBox::indicator { margin-left: 2px; } QCheckBox::indicator:unchecked, QGroupBox::indicator:unchecked { image: url(:/qss_icons/rc/checkbox_unchecked.png); } QCheckBox::indicator:unchecked:hover, QCheckBox::indicator:unchecked:focus, QCheckBox::indicator:unchecked:pressed, QGroupBox::indicator:unchecked:hover, QGroupBox::indicator:unchecked:focus, QGroupBox::indicator:unchecked:pressed { border: none; image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); } QCheckBox::indicator:checked, QGroupBox::indicator:checked { image: url(:/qss_icons/rc/checkbox_checked.png); } QCheckBox::indicator:checked:hover, QCheckBox::indicator:checked:focus, QCheckBox::indicator:checked:pressed, QGroupBox::indicator:checked:hover, QGroupBox::indicator:checked:focus, QGroupBox::indicator:checked:pressed { border: none; image: url(:/qss_icons/rc/checkbox_checked_focus.png); } QCheckBox::indicator:indeterminate { image: url(:/qss_icons/rc/checkbox_indeterminate.png); } QCheckBox::indicator:indeterminate:focus, QCheckBox::indicator:indeterminate:hover, QCheckBox::indicator:indeterminate:pressed { image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); } QCheckBox::indicator:checked:disabled, QGroupBox::indicator:checked:disabled { image: url(:/qss_icons/rc/checkbox_checked_disabled.png); } QCheckBox::indicator:unchecked:disabled, QGroupBox::indicator:unchecked:disabled { image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); } QRadioButton { spacing: 5px; outline: none; color: #eff0f1; margin-bottom: 2px; } QRadioButton:disabled { color: #767676; } QRadioButton::indicator { width: 21px; height: 21px; } QRadioButton::indicator:unchecked { image: url(:/qss_icons/rc/radio_unchecked.png); } QRadioButton::indicator:unchecked:hover, QRadioButton::indicator:unchecked:focus, QRadioButton::indicator:unchecked:pressed { border: none; outline: none; image: url(:/qss_icons/rc/radio_unchecked_focus.png); } QRadioButton::indicator:checked { border: none; outline: none; image: url(:/qss_icons/rc/radio_checked.png); } QRadioButton::indicator:checked:hover, QRadioButton::indicator:checked:focus, QRadioButton::indicator:checked:pressed { border: none; outline: none; image: url(:/qss_icons/rc/radio_checked_focus.png); } QRadioButton::indicator:checked:disabled { outline: none; image: url(:/qss_icons/rc/radio_checked_disabled.png); } QRadioButton::indicator:unchecked:disabled { image: url(:/qss_icons/rc/radio_unchecked_disabled.png); } QMenuBar { background-color: #313131; color: #eff0f1; } QMenuBar::item { background: transparent; } QMenuBar::item:selected { background: transparent; border: 1px solid #767676; } QMenuBar::item:pressed { border: 1px solid #767676; background-color: #3daee9; color: #eff0f1; margin-bottom: -1px; padding-bottom: 1px; } QMenu { border: 1px solid #767676; color: #eff0f1; margin: 2px; } QMenu::icon { margin: 5px; } QMenu::item { padding: 5px 30px 5px 30px; border: 1px solid transparent; /* reserve space for selection border */ } QMenu::item:selected { color: #eff0f1; } QMenu::separator { height: 2px; background: lightblue; margin-left: 10px; margin-right: 5px; } QMenu::indicator { width: 18px; height: 18px; } /* non-exclusive indicator = check box style indicator (see QActionGroup::setExclusive) */ QMenu::indicator:non-exclusive:unchecked { image: url(:/qss_icons/rc/checkbox_unchecked.png); } QMenu::indicator:non-exclusive:unchecked:selected { image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); } QMenu::indicator:non-exclusive:checked { image: url(:/qss_icons/rc/checkbox_checked.png); } QMenu::indicator:non-exclusive:checked:selected { image: url(:/qss_icons/rc/checkbox_checked_disabled.png); } /* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ QMenu::indicator:exclusive:unchecked { image: url(:/qss_icons/rc/radio_unchecked.png); } QMenu::indicator:exclusive:unchecked:selected { image: url(:/qss_icons/rc/radio_unchecked_disabled.png); } QMenu::indicator:exclusive:checked { image: url(:/qss_icons/rc/radio_checked.png); } QMenu::indicator:exclusive:checked:selected { image: url(:/qss_icons/rc/radio_checked_disabled.png); } QMenu::right-arrow { margin: 5px; image: url(:/qss_icons/rc/right_arrow.png) } QWidget:disabled { color: #454545; background-color: #313131; } QAbstractItemView { alternate-background-color: #313131; color: #eff0f1; border: 1px solid #3A3939; border-radius: 2px; } QWidget:focus, QMenuBar:focus { border: 1px solid #3daee9; } QTabWidget:focus, QCheckBox:focus, QRadioButton:focus, QSlider:focus { border: none; } QLineEdit { background-color: #232629; padding: 5px; border-style: solid; border: 1px solid #767676; border-radius: 2px; color: #eff0f1; } QAbstractItemView QLineEdit { padding: 0; } QGroupBox { border: 1px solid #767676; border-radius: 2px; margin-top: 20px; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding-left: 10px; padding-right: 10px; padding-top: 10px; } QAbstractScrollArea { border-radius: 2px; border: 1px solid #767676; background-color: transparent; } QScrollBar:horizontal { height: 15px; margin: 3px 15px 3px 15px; border: 1px transparent #2A2929; border-radius: 4px; background-color: #2A2929; } QScrollBar::handle:horizontal { background-color: #605F5F; min-width: 5px; border-radius: 4px; } QScrollBar::add-line:horizontal { margin: 0px 3px 0px 3px; border-image: url(:/qss_icons/rc/right_arrow_disabled.png); width: 10px; height: 10px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal { margin: 0px 3px 0px 3px; border-image: url(:/qss_icons/rc/left_arrow_disabled.png); height: 10px; width: 10px; subcontrol-position: left; subcontrol-origin: margin; } QScrollBar::add-line:horizontal:hover, QScrollBar::add-line:horizontal:on { border-image: url(:/qss_icons/rc/right_arrow.png); height: 10px; width: 10px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on { border-image: url(:/qss_icons/rc/left_arrow.png); height: 10px; width: 10px; subcontrol-position: left; subcontrol-origin: margin; } QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal { background: none; } QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; } QScrollBar:vertical { background-color: #2A2929; width: 15px; margin: 15px 3px 15px 3px; border: 1px transparent #2A2929; border-radius: 4px; } QScrollBar::handle:vertical { background-color: #605F5F; min-height: 5px; border-radius: 4px; } QScrollBar::sub-line:vertical { margin: 3px 0px 3px 0px; border-image: url(:/qss_icons/rc/up_arrow_disabled.png); height: 10px; width: 10px; subcontrol-position: top; subcontrol-origin: margin; } QScrollBar::add-line:vertical { margin: 3px 0px 3px 0px; border-image: url(:/qss_icons/rc/down_arrow_disabled.png); height: 10px; width: 10px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::sub-line:vertical:hover, QScrollBar::sub-line:vertical:on { border-image: url(:/qss_icons/rc/up_arrow.png); height: 10px; width: 10px; subcontrol-position: top; subcontrol-origin: margin; } QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on { border-image: url(:/qss_icons/rc/down_arrow.png); height: 10px; width: 10px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { background: none; } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } QTextEdit { background-color: #232629; color: #eff0f1; border: 1px solid #767676; } QPlainTextEdit { background-color: #232629; ; color: #eff0f1; border-radius: 2px; border: 1px solid #767676; } QHeaderView::section { background-color: #767676; color: #eff0f1; padding: 5px; border: 1px solid #767676; } QSizeGrip { image: url(:/qss_icons/rc/sizegrip.png); width: 12px; height: 12px; } QMainWindow::separator { background-color: #313131; color: white; padding-left: 4px; spacing: 2px; border: 1px dashed #767676; } QMainWindow::separator:hover { background-color: #787876; color: white; padding-left: 4px; border: 1px solid #767676; spacing: 2px; } QMenu::separator { height: 1px; background-color: #767676; color: white; padding-left: 4px; margin-left: 10px; margin-right: 5px; } QFrame { border-radius: 2px; border: 1px solid #767676; } QFrame[frameShape="0"] { border-radius: 2px; border: 1px transparent #767676; } QStackedWidget { border: 1px transparent black; } QToolBar { border: 1px transparent #393838; background: 1px solid #313131; font-weight: bold; } QToolBar::handle:horizontal { image: url(:/qss_icons/rc/Hmovetoolbar.png); } QToolBar::handle:vertical { image: url(:/qss_icons/rc/Vmovetoolbar.png); } QToolBar::separator:horizontal { image: url(:/qss_icons/rc/Hsepartoolbar.png); } QToolBar::separator:vertical { image: url(:/qss_icons/rc/Vsepartoolbar.png); } QToolButton#qt_toolbar_ext_button { background: #585858 } QPushButton { color: #eff0f1; background-color: #313131; border-width: 1px; border-color: #767676; border-style: solid; padding: 5px; border-radius: 2px; outline: none; } QPushButton:disabled { background-color: #313131; border-width: 1px; border-color: #454545; border-style: solid; padding-top: 5px; padding-bottom: 5px; padding-left: 10px; padding-right: 10px; border-radius: 2px; color: #454545; } QPushButton:focus { background-color: #3daee9; color: white; } QPushButton:pressed { background-color: #3daee9; padding-top: -15px; padding-bottom: -17px; } QComboBox { selection-background-color: #3daee9; border-style: solid; border: 1px solid #767676; border-radius: 2px; padding: 5px; min-width: 75px; } QPushButton:checked { background-color: #767676; border-color: #6A6969; } QComboBox:hover, QPushButton:hover, QAbstractSpinBox:hover, QLineEdit:hover, QTextEdit:hover, QPlainTextEdit:hover, QAbstractView:hover, QTreeView:hover { border: 1px solid #3daee9; color: #eff0f1; } QComboBox:on { padding-top: 3px; padding-left: 4px; selection-background-color: #4a4a4a; } QComboBox QAbstractItemView { background-color: #232629; border-radius: 2px; border: 1px solid #767676; selection-background-color: #18465d; } QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 15px; border-left-width: 0px; border-left-color: darkgray; border-left-style: solid; border-top-right-radius: 3px; border-bottom-right-radius: 3px; } QComboBox::down-arrow { image: url(:/qss_icons/rc/down_arrow_disabled.png); } QComboBox::down-arrow:on, QComboBox::down-arrow:hover, QComboBox::down-arrow:focus { image: url(:/qss_icons/rc/down_arrow.png); } QAbstractSpinBox { padding: 5px; border: 1px solid #767676; background-color: #232629; color: #eff0f1; border-radius: 2px; min-width: 75px; } QAbstractSpinBox:up-button { background-color: transparent; subcontrol-origin: border; subcontrol-position: center right; } QAbstractSpinBox:down-button { background-color: transparent; subcontrol-origin: border; subcontrol-position: center left; } QAbstractSpinBox::up-arrow, QAbstractSpinBox::up-arrow:disabled, QAbstractSpinBox::up-arrow:off { image: url(:/qss_icons/rc/up_arrow_disabled.png); width: 10px; height: 10px; } QAbstractSpinBox::up-arrow:hover { image: url(:/qss_icons/rc/up_arrow.png); } QAbstractSpinBox::down-arrow, QAbstractSpinBox::down-arrow:disabled, QAbstractSpinBox::down-arrow:off { image: url(:/qss_icons/rc/down_arrow_disabled.png); width: 10px; height: 10px; } QAbstractSpinBox::down-arrow:hover { image: url(:/qss_icons/rc/down_arrow.png); } QLabel { border: 0px solid black; } QTabWidget { border: 0px transparent black; } QTabWidget::pane { border: 1px solid #767676; padding: 5px; margin: 0px; } QTabWidget::tab-bar { /* left: 5px; move to the right by 5px */ } QTabBar { qproperty-drawBase: 0; border-radius: 3px; } QTabBar:focus { border: 0px transparent black; } QTabBar::close-button { image: url(:/qss_icons/rc/close.png); background: transparent; } QTabBar::close-button:hover { image: url(:/qss_icons/rc/close-hover.png); background: transparent; } QTabBar::close-button:pressed { image: url(:/qss_icons/rc/close-pressed.png); background: transparent; } /* TOP TABS */ QTabBar::tab:top { color: #eff0f1; border: 1px solid #767676; border-bottom: 1px transparent black; background-color: #313131; padding: 5px; min-width: 50px; border-top-left-radius: 2px; border-top-right-radius: 2px; } QTabBar::tab:top:selected { color: #eff0f1; background-color: #54575B; border: 1px solid #767676; border-bottom: 2px solid #3daee9; border-top-left-radius: 2px; border-top-right-radius: 2px; } QTabBar::tab:top:!selected:hover { background-color: #3daee9; } /* BOTTOM TABS */ QTabBar::tab:bottom { color: #eff0f1; border: 1px solid #767676; border-top: 1px transparent black; background-color: #313131; padding: 5px; border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; min-width: 50px; } QTabBar::tab:bottom:selected { color: #eff0f1; background-color: #54575B; border: 1px solid #767676; border-top: 2px solid #3daee9; border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } QTabBar::tab:bottom:!selected:hover { background-color: #3daee9; } /* LEFT TABS */ QTabBar::tab:left { color: #eff0f1; border: 1px solid #767676; border-left: 1px transparent black; background-color: #313131; padding: 5px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; min-height: 50px; } QTabBar::tab:left:selected { color: #eff0f1; background-color: #54575B; border: 1px solid #767676; border-left: 2px solid #3daee9; border-top-right-radius: 2px; border-bottom-right-radius: 2px; } QTabBar::tab:left:!selected:hover { background-color: #3daee9; } /* RIGHT TABS */ QTabBar::tab:right { color: #eff0f1; border: 1px solid #767676; border-right: 1px transparent black; background-color: #313131; padding: 5px; border-top-left-radius: 2px; border-bottom-left-radius: 2px; min-height: 50px; } QTabBar::tab:right:selected { color: #eff0f1; background-color: #54575B; border: 1px solid #767676; border-right: 2px solid #3daee9; border-top-left-radius: 2px; border-bottom-left-radius: 2px; } QTabBar::tab:right:!selected:hover { background-color: #3daee9; } QTabBar QToolButton::right-arrow:enabled { image: url(:/qss_icons/rc/right_arrow.png); } QTabBar QToolButton::left-arrow:enabled { image: url(:/qss_icons/rc/left_arrow.png); } QTabBar QToolButton::right-arrow:disabled { image: url(:/qss_icons/rc/right_arrow_disabled.png); } QTabBar QToolButton::left-arrow:disabled { image: url(:/qss_icons/rc/left_arrow_disabled.png); } QDockWidget { background: #313131; border: 1px solid #403F3F; titlebar-close-icon: url(:/qss_icons/rc/close.png); titlebar-normal-icon: url(:/qss_icons/rc/undock.png); } QDockWidget::close-button, QDockWidget::float-button { border: 1px solid transparent; border-radius: 2px; background: transparent; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background: rgba(255, 255, 255, 10); } QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { padding: 1px -1px -1px 1px; background: rgba(255, 255, 255, 10); } QTreeView, QListView { border: 1px solid #767676; background-color: #232629; } QTreeView:branch:selected, QTreeView:branch:hover { background: url(:/qss_icons/rc/transparent.png); } QTreeView::branch:has-siblings:!adjoins-item { border-image: url(:/qss_icons/rc/transparent.png); } QTreeView::branch:has-siblings:adjoins-item { border-image: url(:/qss_icons/rc/transparent.png); } QTreeView::branch:!has-children:!has-siblings:adjoins-item { border-image: url(:/qss_icons/rc/transparent.png); } QTreeView::branch:has-children:!has-siblings:closed, QTreeView::branch:closed:has-children:has-siblings { image: url(:/qss_icons/rc/branch_closed.png); } QTreeView::branch:open:has-children:!has-siblings, QTreeView::branch:open:has-children:has-siblings { image: url(:/qss_icons/rc/branch_open.png); } QTreeView::branch:has-children:!has-siblings:closed:hover, QTreeView::branch:closed:has-children:has-siblings:hover { image: url(:/qss_icons/rc/branch_closed-on.png); } QTreeView::branch:open:has-children:!has-siblings:hover, QTreeView::branch:open:has-children:has-siblings:hover { image: url(:/qss_icons/rc/branch_open-on.png); } QListView::item:!selected:hover, QTreeView::item:!selected:hover { background: #18465d; outline: 0; color: #eff0f1 } QListView::item:selected:hover, QTreeView::item:selected:hover { background: #287399; color: #eff0f1; } QTreeView::indicator:checked, QListView::indicator:checked { image: url(:/qss_icons/rc/checkbox_checked.png); } QTreeView::indicator:unchecked, QListView::indicator:unchecked { image: url(:/qss_icons/rc/checkbox_unchecked.png); } QTreeView::indicator:indeterminate, QListView::indicator:indeterminate { image: url(:/qss_icons/rc/checkbox_indeterminate.png); } QTreeView::indicator:checked:hover, QTreeView::indicator:checked:focus, QTreeView::indicator:checked:pressed, QListView::indicator:checked:hover, QListView::indicator:checked:focus, QListView::indicator:checked:pressed { image: url(:/qss_icons/rc/checkbox_checked_focus.png); } QTreeView::indicator:unchecked:hover, QTreeView::indicator:unchecked:focus, QTreeView::indicator:unchecked:pressed, QListView::indicator:unchecked:hover, QListView::indicator:unchecked:focus, QListView::indicator:unchecked:pressed { image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); } QTreeView::indicator:indeterminate:hover, QTreeView::indicator:indeterminate:focus, QTreeView::indicator:indeterminate:pressed, QListView::indicator:indeterminate:hover, QListView::indicator:indeterminate:focus, QListView::indicator:indeterminate:pressed { image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); } QSlider::groove:horizontal { border: 1px solid #565a5e; height: 4px; background: #565a5e; margin: 0px; border-radius: 2px; } QSlider::handle:horizontal { background: #232629; border: 1px solid #565a5e; width: 16px; height: 16px; margin: -8px 0; border-radius: 9px; } QSlider::groove:vertical { border: 1px solid #565a5e; width: 4px; background: #565a5e; margin: 0px; border-radius: 3px; } QSlider::handle:vertical { background: #232629; border: 1px solid #565a5e; width: 16px; height: 16px; margin: 0 -8px; border-radius: 9px; } QToolButton { background-color: transparent; border: 1px transparent #767676; border-radius: 2px; margin: 3px; padding: 5px; } QToolButton[popupMode="1"] { /* only for MenuButtonPopup */ padding-right: 20px; /* make way for the popup button */ border: 1px #767676; border-radius: 5px; } QToolButton[popupMode="2"] { /* only for InstantPopup */ padding-right: 10px; /* make way for the popup button */ border: 1px #767676; } QToolButton:hover, QToolButton::menu-button:hover { background-color: transparent; border: 1px solid #3daee9; padding: 5px; } QToolButton:checked, QToolButton:pressed, QToolButton::menu-button:pressed { background-color: #3daee9; border: 1px solid #3daee9; padding: 5px; } /* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ QToolButton::menu-indicator { image: url(:/qss_icons/rc/down_arrow.png); top: -7px; left: -2px; /* shift it a bit */ } /* the subcontrols below are used only in the MenuButtonPopup mode */ QToolButton::menu-button { border: 1px transparent #767676; border-top-right-radius: 6px; border-bottom-right-radius: 6px; /* 16px width + 4px for border = 20px allocated above */ width: 16px; outline: none; } QToolButton::menu-arrow { image: url(:/qss_icons/rc/down_arrow.png); } QToolButton::menu-arrow:open { border: 1px solid #767676; } QPushButton::menu-indicator { subcontrol-origin: padding; subcontrol-position: bottom right; left: 8px; } QTableView { border: 1px solid #767676; gridline-color: #313131; background-color: #232629; } QTableView, QHeaderView { border-radius: 0px; } QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { background: #18465d; color: #eff0f1; } QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { background: #287399; color: #eff0f1; } QHeaderView { background-color: #313131; border: 1px transparent; border-radius: 0px; margin: 0px; padding: 0px; } QHeaderView::section { background-color: #313131; color: #eff0f1; padding: 5px; border: 1px solid #767676; border-radius: 0px; text-align: center; } QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one { border-top: 1px solid #767676; } QHeaderView::section::vertical { border-top: transparent; } QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one { border-left: 1px solid #767676; } QHeaderView::section::horizontal { border-left: transparent; } QHeaderView::section:checked { color: white; background-color: #334e5e; } /* style the sort indicator */ QHeaderView::down-arrow { image: url(:/qss_icons/rc/down_arrow.png); } QHeaderView::up-arrow { image: url(:/qss_icons/rc/up_arrow.png); } QTableCornerButton::section { background-color: #313131; border: 1px transparent #767676; border-radius: 0px; } QToolBox { padding: 5px; border: 1px transparent black; } QToolBox::tab { color: #eff0f1; background-color: #313131; border: 1px solid #767676; border-bottom: 1px transparent #313131; border-top-left-radius: 5px; border-top-right-radius: 5px; } QToolBox::tab:selected { /* italicize selected tabs */ font: italic; background-color: #313131; border-color: #3daee9; } QStatusBar::item { border: 0px transparent dark; } QFrame[height="3"], QFrame[width="3"] { background-color: #767676; } QSplitter::handle { border: 1px dashed #767676; } QSplitter::handle:hover { background-color: #787876; border: 1px solid #767676; } QSplitter::handle:horizontal { width: 1px; } QSplitter::handle:vertical { height: 1px; } QProgressBar { border: 1px solid #767676; border-radius: 5px; text-align: center; } QProgressBar::chunk { background-color: #05B8CC; } QDateEdit { selection-background-color: #3daee9; border-style: solid; border: 1px solid #3375A3; border-radius: 2px; padding: 1px; min-width: 75px; } QDateEdit:on { padding-top: 3px; padding-left: 4px; selection-background-color: #4a4a4a; } QDateEdit QAbstractItemView { background-color: #232629; border-radius: 2px; border: 1px solid #3375A3; selection-background-color: #3daee9; } QDateEdit::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 15px; border-left-width: 0px; border-left-color: darkgray; border-left-style: solid; border-top-right-radius: 3px; border-bottom-right-radius: 3px; } QDateEdit::down-arrow { image: url(:/qss_icons/rc/down_arrow_disabled.png); } QDateEdit::down-arrow:on, QDateEdit::down-arrow:hover, QDateEdit::down-arrow:focus { image: url(:/qss_icons/rc/down_arrow.png); } wfview-1.2d/qledlabel.cpp000066400000000000000000000032111415164626400154660ustar00rootroot00000000000000#include "qledlabel.h" #include static const int SIZE = 16; static const QString greenSS = QString("color: white;border-radius: %1;background-color: qlineargradient(spread:pad, x1:0.145, y1:0.16, x2:1, y2:1, stop:0 rgba(20, 252, 7, 255), stop:1 rgba(25, 134, 5, 255));").arg(SIZE / 2); static const QString redSS = QString("color: white;border-radius: %1;background-color: qlineargradient(spread:pad, x1:0.145, y1:0.16, x2:0.92, y2:0.988636, stop:0 rgba(255, 12, 12, 255), stop:0.869347 rgba(103, 0, 0, 255));").arg(SIZE / 2); static const QString orangeSS = QString("color: white;border-radius: %1;background-color: qlineargradient(spread:pad, x1:0.232, y1:0.272, x2:0.98, y2:0.959773, stop:0 rgba(255, 113, 4, 255), stop:1 rgba(91, 41, 7, 255))").arg(SIZE / 2); static const QString blueSS = QString("color: white;border-radius: %1;background-color: qlineargradient(spread:pad, x1:0.04, y1:0.0565909, x2:0.799, y2:0.795, stop:0 rgba(203, 220, 255, 255), stop:0.41206 rgba(0, 115, 255, 255), stop:1 rgba(0, 49, 109, 255));").arg(SIZE / 2); QLedLabel::QLedLabel(QWidget* parent) : QLabel(parent) { //Set to ok by default setState(StateOkBlue); setFixedSize(SIZE, SIZE); } void QLedLabel::setState(State state) { qInfo() << "setState" << state; switch (state) { case StateOk: setStyleSheet(greenSS); break; case StateWarning: setStyleSheet(orangeSS); break; case StateError: setStyleSheet(redSS); break; case StateOkBlue: default: setStyleSheet(blueSS); break; } } void QLedLabel::setState(bool state) { setState(state ? StateOk : StateError); }wfview-1.2d/qledlabel.h000066400000000000000000000005731415164626400151430ustar00rootroot00000000000000#ifndef QLEDLABEL_H #define QLEDLABEL_H #include class QLedLabel : public QLabel { Q_OBJECT public: explicit QLedLabel(QWidget* parent = 0); enum State { StateOk, StateOkBlue, StateWarning, StateError }; signals: public slots: void setState(State state); void setState(bool state); }; #endif // QLEDLABEL_H wfview-1.2d/repeaterattributes.h000066400000000000000000000013641415164626400171330ustar00rootroot00000000000000#ifndef REPEATERATTRIBUTES_H #define REPEATERATTRIBUTES_H #include enum duplexMode { dmSplitOff=0x00, dmSplitOn=0x01, dmSimplex=0x10, dmDupMinus=0x11, dmDupPlus=0x12, dmDupRPS=0x13, dmDupAutoOn=0x26, dmDupAutoOff=0x36 }; // Here, T=tone, D=DCS, N=none // And the naming convention order is Transmit Receive enum rptAccessTxRx { ratrNN=0x00, ratrTN=0x01, // "TONE" (T only) ratrNT=0x02, // "TSQL" (R only) ratrDD=0x03, // "DTCS" (TR) ratrDN=0x06, // "DTCS(T)" ratrTD=0x07, // "TONE(T) / TSQL(R)" ratrDT=0x08, // "DTCS(T) / TSQL(R)" ratrTT=0x09 // "TONE(T) / TSQL(R)" }; Q_DECLARE_METATYPE(enum duplexMode) Q_DECLARE_METATYPE(enum rptAccessTxRx) #endif // REPEATERATTRIBUTES_H wfview-1.2d/repeatersetup.cpp000066400000000000000000000324131415164626400164370ustar00rootroot00000000000000#include "repeatersetup.h" #include "ui_repeatersetup.h" repeaterSetup::repeaterSetup(QWidget *parent) : QMainWindow(parent), ui(new Ui::repeaterSetup) { ui->setupUi(this); // populate the CTCSS combo box: populateTones(); // populate the DCS combo box: populateDTCS(); #ifdef QT_DEBUG ui->debugBtn->setVisible(true); ui->rptReadRigBtn->setVisible(true); #else ui->debugBtn->setVisible(false); ui->rptReadRigBtn->setVisible(false); #endif } repeaterSetup::~repeaterSetup() { // Trying this for more consistant destruction rig.inputs.clear(); rig.preamps.clear(); rig.attenuators.clear(); rig.antennas.clear(); delete ui; } void repeaterSetup::setRig(rigCapabilities inRig) { this->rig = inRig; haveRig = true; if(rig.hasCTCSS) { ui->rptToneCombo->setDisabled(false); ui->toneTone->setDisabled(false); ui->toneTSQL->setDisabled(false); } else { ui->rptToneCombo->setDisabled(true); ui->toneTone->setDisabled(true); ui->toneTSQL->setDisabled(true); } if(rig.hasDTCS) { ui->rptDTCSCombo->setDisabled(false); ui->toneDTCS->setDisabled(false); ui->rptDTCSInvertRx->setDisabled(false); ui->rptDTCSInvertTx->setDisabled(false); } else { ui->rptDTCSCombo->setDisabled(true); ui->toneDTCS->setDisabled(true); ui->rptDTCSInvertRx->setDisabled(true); ui->rptDTCSInvertTx->setDisabled(true); } } void repeaterSetup::populateTones() { ui->rptToneCombo->addItem("67.0", quint16(670)); ui->rptToneCombo->addItem("69.3", quint16(693)); ui->rptToneCombo->addItem("71.9", quint16(719)); ui->rptToneCombo->addItem("74.4", quint16(744)); ui->rptToneCombo->addItem("77.0", quint16(770)); ui->rptToneCombo->addItem("79.7", quint16(797)); ui->rptToneCombo->addItem("82.5", quint16(825)); ui->rptToneCombo->addItem("85.4", quint16(854)); ui->rptToneCombo->addItem("88.5", quint16(885)); ui->rptToneCombo->addItem("91.5", quint16(915)); ui->rptToneCombo->addItem("94.8", quint16(948)); ui->rptToneCombo->addItem("97.4", quint16(974)); ui->rptToneCombo->addItem("100.0", quint16(1000)); ui->rptToneCombo->addItem("103.5", quint16(1035)); ui->rptToneCombo->addItem("107.2", quint16(1072)); ui->rptToneCombo->addItem("110.9", quint16(1109)); ui->rptToneCombo->addItem("114.8", quint16(1148)); ui->rptToneCombo->addItem("118.8", quint16(1188)); ui->rptToneCombo->addItem("123.0", quint16(1230)); ui->rptToneCombo->addItem("127.3", quint16(1273)); ui->rptToneCombo->addItem("131.8", quint16(1318)); ui->rptToneCombo->addItem("136.5", quint16(1365)); ui->rptToneCombo->addItem("141.3", quint16(1413)); ui->rptToneCombo->addItem("146.2", quint16(1462)); ui->rptToneCombo->addItem("151.4", quint16(1514)); ui->rptToneCombo->addItem("156.7", quint16(1567)); ui->rptToneCombo->addItem("159.8", quint16(1598)); ui->rptToneCombo->addItem("162.2", quint16(1622)); ui->rptToneCombo->addItem("165.5", quint16(1655)); ui->rptToneCombo->addItem("167.9", quint16(1679)); ui->rptToneCombo->addItem("171.3", quint16(1713)); ui->rptToneCombo->addItem("173.8", quint16(1738)); ui->rptToneCombo->addItem("177.3", quint16(1773)); ui->rptToneCombo->addItem("179.9", quint16(1799)); ui->rptToneCombo->addItem("183.5", quint16(1835)); ui->rptToneCombo->addItem("186.2", quint16(1862)); ui->rptToneCombo->addItem("189.9", quint16(1899)); ui->rptToneCombo->addItem("192.8", quint16(1928)); ui->rptToneCombo->addItem("196.6", quint16(1966)); ui->rptToneCombo->addItem("199.5", quint16(1995)); ui->rptToneCombo->addItem("203.5", quint16(2035)); ui->rptToneCombo->addItem("206.5", quint16(2065)); ui->rptToneCombo->addItem("210.7", quint16(2107)); ui->rptToneCombo->addItem("218.1", quint16(2181)); ui->rptToneCombo->addItem("225.7", quint16(2257)); ui->rptToneCombo->addItem("229.1", quint16(2291)); ui->rptToneCombo->addItem("233.6", quint16(2336)); ui->rptToneCombo->addItem("241.8", quint16(2418)); ui->rptToneCombo->addItem("250.3", quint16(2503)); ui->rptToneCombo->addItem("254.1", quint16(2541)); } void repeaterSetup::populateDTCS() { ui->rptDTCSCombo->addItem("023", quint16(23)); ui->rptDTCSCombo->addItem("025", quint16(25)); ui->rptDTCSCombo->addItem("026", quint16(26)); ui->rptDTCSCombo->addItem("031", quint16(31)); ui->rptDTCSCombo->addItem("032", quint16(32)); ui->rptDTCSCombo->addItem("036", quint16(36)); ui->rptDTCSCombo->addItem("043", quint16(43)); ui->rptDTCSCombo->addItem("047", quint16(47)); ui->rptDTCSCombo->addItem("051", quint16(51)); ui->rptDTCSCombo->addItem("053", quint16(53)); ui->rptDTCSCombo->addItem("054", quint16(54)); ui->rptDTCSCombo->addItem("065", quint16(65)); ui->rptDTCSCombo->addItem("071", quint16(71)); ui->rptDTCSCombo->addItem("072", quint16(72)); ui->rptDTCSCombo->addItem("073", quint16(73)); ui->rptDTCSCombo->addItem("074", quint16(74)); ui->rptDTCSCombo->addItem("114", quint16(114)); ui->rptDTCSCombo->addItem("115", quint16(115)); ui->rptDTCSCombo->addItem("116", quint16(116)); ui->rptDTCSCombo->addItem("122", quint16(122)); ui->rptDTCSCombo->addItem("125", quint16(125)); ui->rptDTCSCombo->addItem("131", quint16(131)); ui->rptDTCSCombo->addItem("132", quint16(132)); ui->rptDTCSCombo->addItem("134", quint16(134)); ui->rptDTCSCombo->addItem("143", quint16(143)); ui->rptDTCSCombo->addItem("145", quint16(145)); ui->rptDTCSCombo->addItem("152", quint16(152)); ui->rptDTCSCombo->addItem("155", quint16(155)); ui->rptDTCSCombo->addItem("156", quint16(156)); ui->rptDTCSCombo->addItem("162", quint16(162)); ui->rptDTCSCombo->addItem("165", quint16(165)); ui->rptDTCSCombo->addItem("172", quint16(172)); ui->rptDTCSCombo->addItem("174", quint16(174)); ui->rptDTCSCombo->addItem("205", quint16(205)); ui->rptDTCSCombo->addItem("212", quint16(212)); ui->rptDTCSCombo->addItem("223", quint16(223)); ui->rptDTCSCombo->addItem("225", quint16(225)); ui->rptDTCSCombo->addItem("226", quint16(226)); ui->rptDTCSCombo->addItem("243", quint16(243)); ui->rptDTCSCombo->addItem("244", quint16(244)); ui->rptDTCSCombo->addItem("245", quint16(245)); ui->rptDTCSCombo->addItem("246", quint16(246)); ui->rptDTCSCombo->addItem("251", quint16(251)); ui->rptDTCSCombo->addItem("252", quint16(252)); ui->rptDTCSCombo->addItem("255", quint16(255)); ui->rptDTCSCombo->addItem("261", quint16(261)); ui->rptDTCSCombo->addItem("263", quint16(263)); ui->rptDTCSCombo->addItem("265", quint16(265)); ui->rptDTCSCombo->addItem("266", quint16(266)); ui->rptDTCSCombo->addItem("271", quint16(271)); ui->rptDTCSCombo->addItem("274", quint16(274)); ui->rptDTCSCombo->addItem("306", quint16(306)); ui->rptDTCSCombo->addItem("311", quint16(311)); ui->rptDTCSCombo->addItem("315", quint16(315)); ui->rptDTCSCombo->addItem("325", quint16(325)); ui->rptDTCSCombo->addItem("331", quint16(331)); ui->rptDTCSCombo->addItem("332", quint16(332)); ui->rptDTCSCombo->addItem("343", quint16(343)); ui->rptDTCSCombo->addItem("346", quint16(346)); ui->rptDTCSCombo->addItem("351", quint16(351)); ui->rptDTCSCombo->addItem("356", quint16(356)); ui->rptDTCSCombo->addItem("364", quint16(364)); ui->rptDTCSCombo->addItem("365", quint16(365)); ui->rptDTCSCombo->addItem("371", quint16(371)); ui->rptDTCSCombo->addItem("411", quint16(411)); ui->rptDTCSCombo->addItem("412", quint16(412)); ui->rptDTCSCombo->addItem("413", quint16(413)); ui->rptDTCSCombo->addItem("423", quint16(423)); ui->rptDTCSCombo->addItem("431", quint16(431)); ui->rptDTCSCombo->addItem("432", quint16(432)); ui->rptDTCSCombo->addItem("445", quint16(445)); ui->rptDTCSCombo->addItem("446", quint16(446)); ui->rptDTCSCombo->addItem("452", quint16(452)); ui->rptDTCSCombo->addItem("454", quint16(454)); ui->rptDTCSCombo->addItem("455", quint16(455)); ui->rptDTCSCombo->addItem("462", quint16(462)); ui->rptDTCSCombo->addItem("464", quint16(464)); ui->rptDTCSCombo->addItem("465", quint16(465)); ui->rptDTCSCombo->addItem("466", quint16(466)); ui->rptDTCSCombo->addItem("503", quint16(503)); ui->rptDTCSCombo->addItem("506", quint16(506)); ui->rptDTCSCombo->addItem("516", quint16(516)); ui->rptDTCSCombo->addItem("523", quint16(512)); ui->rptDTCSCombo->addItem("526", quint16(526)); ui->rptDTCSCombo->addItem("532", quint16(532)); ui->rptDTCSCombo->addItem("546", quint16(546)); ui->rptDTCSCombo->addItem("565", quint16(565)); ui->rptDTCSCombo->addItem("606", quint16(606)); ui->rptDTCSCombo->addItem("612", quint16(612)); ui->rptDTCSCombo->addItem("624", quint16(624)); ui->rptDTCSCombo->addItem("627", quint16(627)); ui->rptDTCSCombo->addItem("631", quint16(631)); ui->rptDTCSCombo->addItem("632", quint16(632)); ui->rptDTCSCombo->addItem("654", quint16(654)); ui->rptDTCSCombo->addItem("662", quint16(662)); ui->rptDTCSCombo->addItem("664", quint16(664)); ui->rptDTCSCombo->addItem("703", quint16(703)); ui->rptDTCSCombo->addItem("712", quint16(712)); ui->rptDTCSCombo->addItem("723", quint16(723)); ui->rptDTCSCombo->addItem("731", quint16(731)); ui->rptDTCSCombo->addItem("732", quint16(732)); ui->rptDTCSCombo->addItem("734", quint16(734)); ui->rptDTCSCombo->addItem("746", quint16(746)); ui->rptDTCSCombo->addItem("754", quint16(754)); } void repeaterSetup::receiveDuplexMode(duplexMode dm) { currentdm = dm; switch(dm) { case dmSimplex: case dmSplitOff: ui->rptSimplexBtn->setChecked(true); break; case dmDupPlus: ui->rptDupPlusBtn->setChecked(true); break; case dmDupMinus: ui->rptDupMinusBtn->setChecked(true); break; default: break; } } void repeaterSetup::handleRptAccessMode(rptAccessTxRx tmode) { switch(tmode) { case ratrNN: ui->toneNone->setChecked(true); break; case ratrTT: ui->toneTSQL->setChecked(true); break; case ratrTN: ui->toneTone->setChecked(true); break; case ratrDD: ui->toneDTCS->setChecked(true); break; default: break; } (void)tmode; } void repeaterSetup::handleTone(quint16 tone) { int tindex = ui->rptToneCombo->findData(tone); ui->rptToneCombo->setCurrentIndex(tindex); } void repeaterSetup::handleTSQL(quint16 tsql) { // TODO: Consider a second combo box for the TSQL int tindex = ui->rptToneCombo->findData(tsql); ui->rptToneCombo->setCurrentIndex(tindex); } void repeaterSetup::handleDTCS(quint16 dcode, bool tinv, bool rinv) { int dindex = ui->rptDTCSCombo->findData(dcode); ui->rptDTCSCombo->setCurrentIndex(dindex); ui->rptDTCSInvertTx->setChecked(tinv); ui->rptDTCSInvertRx->setChecked(rinv); } void repeaterSetup::on_rptSimplexBtn_clicked() { // Simplex emit setDuplexMode(dmDupAutoOff); emit setDuplexMode(dmSimplex); } void repeaterSetup::on_rptDupPlusBtn_clicked() { // DUP+ emit setDuplexMode(dmDupAutoOff); emit setDuplexMode(dmDupPlus); } void repeaterSetup::on_rptDupMinusBtn_clicked() { // DUP- emit setDuplexMode(dmDupAutoOff); emit setDuplexMode(dmDupMinus); } void repeaterSetup::on_rptAutoBtn_clicked() { // Auto Rptr (enable this feature) // TODO: Hide an AutoOff button somewhere for non-US users emit setDuplexMode(dmDupAutoOn); } void repeaterSetup::on_rptReadRigBtn_clicked() { emit getDuplexMode(); } void repeaterSetup::on_rptToneCombo_activated(int tindex) { quint16 tone=0; tone = (quint16)ui->rptToneCombo->itemData(tindex).toUInt(); if(ui->toneTone->isChecked()) { emit setTone(tone); } else if (ui->toneTSQL->isChecked()) { emit setTSQL(tone); } } void repeaterSetup::on_rptDTCSCombo_activated(int index) { quint16 dcode=0; bool tinv = ui->rptDTCSInvertTx->isChecked(); bool rinv = ui->rptDTCSInvertRx->isChecked(); dcode = (quint16)ui->rptDTCSCombo->itemData(index).toUInt(); emit setDTCS(dcode, tinv, rinv); } void repeaterSetup::on_toneNone_clicked() { rptAccessTxRx rm; rm = ratrNN; emit setRptAccessMode(rm); } void repeaterSetup::on_toneTone_clicked() { rptAccessTxRx rm; rm = ratrTN; emit setRptAccessMode(rm); emit setTone((quint16)ui->rptToneCombo->currentData().toUInt()); } void repeaterSetup::on_toneTSQL_clicked() { rptAccessTxRx rm; rm = ratrTT; emit setRptAccessMode(rm); emit setTSQL((quint16)ui->rptToneCombo->currentData().toUInt()); } void repeaterSetup::on_toneDTCS_clicked() { rptAccessTxRx rm; quint16 dcode=0; rm = ratrDD; emit setRptAccessMode(rm); bool tinv = ui->rptDTCSInvertTx->isChecked(); bool rinv = ui->rptDTCSInvertRx->isChecked(); dcode = (quint16)ui->rptDTCSCombo->currentData().toUInt(); emit setDTCS(dcode, tinv, rinv); } void repeaterSetup::on_debugBtn_clicked() { // TODO: Move these four commands to wfview's startup command list (place at the end) //emit getTone(); //emit getTSQL(); //emit getDTCS(); emit getRptAccessMode(); } wfview-1.2d/repeatersetup.h000066400000000000000000000030571415164626400161060ustar00rootroot00000000000000#ifndef REPEATERSETUP_H #define REPEATERSETUP_H #include #include #include "repeaterattributes.h" #include "rigidentities.h" namespace Ui { class repeaterSetup; } class repeaterSetup : public QMainWindow { Q_OBJECT public: explicit repeaterSetup(QWidget *parent = 0); ~repeaterSetup(); void setRig(rigCapabilities rig); signals: void getDuplexMode(); void setDuplexMode(duplexMode dm); void setTone(quint16 tone); void setTSQL(quint16 tsql); void setDTCS(quint16 dcode, bool tinv, bool rinv); void getTone(); void getTSQL(); void getDTCS(); void setRptAccessMode(rptAccessTxRx tmode); void getRptAccessMode(); public slots: void receiveDuplexMode(duplexMode dm); void handleRptAccessMode(rptAccessTxRx tmode); void handleTone(quint16 tone); void handleTSQL(quint16 tsql); void handleDTCS(quint16 dcscode, bool tinv, bool rinv); private slots: void on_rptSimplexBtn_clicked(); void on_rptDupPlusBtn_clicked(); void on_rptDupMinusBtn_clicked(); void on_rptAutoBtn_clicked(); void on_rptReadRigBtn_clicked(); void on_rptToneCombo_activated(int index); void on_rptDTCSCombo_activated(int index); void on_debugBtn_clicked(); void on_toneNone_clicked(); void on_toneTone_clicked(); void on_toneTSQL_clicked(); void on_toneDTCS_clicked(); private: Ui::repeaterSetup *ui; void populateTones(); void populateDTCS(); rigCapabilities rig; bool haveRig = false; duplexMode currentdm; }; #endif // REPEATERSETUP_H wfview-1.2d/repeatersetup.ui000066400000000000000000000153711415164626400162760ustar00rootroot00000000000000 repeaterSetup 0 0 800 217 Repeater Setup Qt::Vertical 20 40 0 Read Current Settings Debug Qt::Horizontal 40 20 0 0 Repeater Duplex Simplex rptDuplexBtns Dup+ rptDuplexBtns Dup- rptDuplexBtns Auto rptDuplexBtns Repeater Tone Type None rptToneBtns Transmit Tone only rptToneBtns Tone Squelch rptToneBtns DTCS rptToneBtns Tone Selection Tone DTCS Invert Tx Invert Rx wfview-1.2d/resampler/000077500000000000000000000000001415164626400150325ustar00rootroot00000000000000wfview-1.2d/resampler/COPYING000066400000000000000000000404601415164626400160710ustar00rootroot00000000000000 Opus-tools, with the exception of opusinfo.[ch] is available under the following two clause BSD-style license: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Opusinfo is a fork of ogginfo from the vorbis-tools package (https://www.xiph.org). It is available under the GPL: 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 wfview-1.2d/resampler/arch.h000066400000000000000000000143571415164626400161320ustar00rootroot00000000000000/* Copyright (C) 2003 Jean-Marc Valin */ /** @file arch.h @brief Various architecture definitions Speex */ /* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Xiph.org Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCH_H #define ARCH_H /* A couple test to catch stupid option combinations */ #ifdef FIXED_POINT #if ((defined (ARM4_ASM)||defined (ARM4_ASM)) && defined(BFIN_ASM)) || (defined (ARM4_ASM)&&defined(ARM5E_ASM)) #error Make up your mind. What CPU do you have? #endif #else #if defined (ARM4_ASM) || defined(ARM5E_ASM) || defined(BFIN_ASM) #error I suppose you can have a [ARM4/ARM5E/Blackfin] that has float instructions? #endif #endif #ifndef OUTSIDE_SPEEX #include "speex/speexdsp_types.h" #endif #define ABS(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute integer value. */ #define ABS16(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 16-bit value. */ #define MIN16(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 16-bit value. */ #define MAX16(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 16-bit value. */ #define ABS32(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 32-bit value. */ #define MIN32(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 32-bit value. */ #define MAX32(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 32-bit value. */ #ifdef FIXED_POINT typedef spx_int16_t spx_word16_t; typedef spx_int32_t spx_word32_t; typedef spx_word32_t spx_mem_t; typedef spx_word16_t spx_coef_t; typedef spx_word16_t spx_lsp_t; typedef spx_word32_t spx_sig_t; #define Q15ONE 32767 #define LPC_SCALING 8192 #define SIG_SCALING 16384 #define LSP_SCALING 8192. #define GAMMA_SCALING 32768. #define GAIN_SCALING 64 #define GAIN_SCALING_1 0.015625 #define LPC_SHIFT 13 #define LSP_SHIFT 13 #define SIG_SHIFT 14 #define GAIN_SHIFT 6 #define WORD2INT(x) ((x) < -32767 ? -32768 : ((x) > 32766 ? 32767 : (x))) #define VERY_SMALL 0 #define VERY_LARGE32 ((spx_word32_t)2147483647) #define VERY_LARGE16 ((spx_word16_t)32767) #define Q15_ONE ((spx_word16_t)32767) #ifdef FIXED_DEBUG #include "fixed_debug.h" #else #include "fixed_generic.h" #ifdef ARM5E_ASM #include "fixed_arm5e.h" #elif defined (ARM4_ASM) #include "fixed_arm4.h" #elif defined (BFIN_ASM) #include "fixed_bfin.h" #endif #endif #else typedef float spx_mem_t; typedef float spx_coef_t; typedef float spx_lsp_t; typedef float spx_sig_t; typedef float spx_word16_t; typedef float spx_word32_t; #define Q15ONE 1.0f #define LPC_SCALING 1.f #define SIG_SCALING 1.f #define LSP_SCALING 1.f #define GAMMA_SCALING 1.f #define GAIN_SCALING 1.f #define GAIN_SCALING_1 1.f #define VERY_SMALL 1e-15f #define VERY_LARGE32 1e15f #define VERY_LARGE16 1e15f #define Q15_ONE ((spx_word16_t)1.f) #define QCONST16(x,bits) (x) #define QCONST32(x,bits) (x) #define NEG16(x) (-(x)) #define NEG32(x) (-(x)) #define EXTRACT16(x) (x) #define EXTEND32(x) (x) #define SHR16(a,shift) (a) #define SHL16(a,shift) (a) #define SHR32(a,shift) (a) #define SHL32(a,shift) (a) #define PSHR16(a,shift) (a) #define PSHR32(a,shift) (a) #define VSHR32(a,shift) (a) #define SATURATE16(x,a) (x) #define SATURATE32(x,a) (x) #define SATURATE32PSHR(x,shift,a) (x) #define PSHR(a,shift) (a) #define SHR(a,shift) (a) #define SHL(a,shift) (a) #define SATURATE(x,a) (x) #define ADD16(a,b) ((a)+(b)) #define SUB16(a,b) ((a)-(b)) #define ADD32(a,b) ((a)+(b)) #define SUB32(a,b) ((a)-(b)) #define MULT16_16_16(a,b) ((a)*(b)) #define MULT16_16(a,b) ((spx_word32_t)(a)*(spx_word32_t)(b)) #define MAC16_16(c,a,b) ((c)+(spx_word32_t)(a)*(spx_word32_t)(b)) #define MULT16_32_Q11(a,b) ((a)*(b)) #define MULT16_32_Q13(a,b) ((a)*(b)) #define MULT16_32_Q14(a,b) ((a)*(b)) #define MULT16_32_Q15(a,b) ((a)*(b)) #define MULT16_32_P15(a,b) ((a)*(b)) #define MAC16_32_Q11(c,a,b) ((c)+(a)*(b)) #define MAC16_32_Q15(c,a,b) ((c)+(a)*(b)) #define MAC16_16_Q11(c,a,b) ((c)+(a)*(b)) #define MAC16_16_Q13(c,a,b) ((c)+(a)*(b)) #define MAC16_16_P13(c,a,b) ((c)+(a)*(b)) #define MULT16_16_Q11_32(a,b) ((a)*(b)) #define MULT16_16_Q13(a,b) ((a)*(b)) #define MULT16_16_Q14(a,b) ((a)*(b)) #define MULT16_16_Q15(a,b) ((a)*(b)) #define MULT16_16_P15(a,b) ((a)*(b)) #define MULT16_16_P13(a,b) ((a)*(b)) #define MULT16_16_P14(a,b) ((a)*(b)) #define DIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b)) #define PDIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b)) #define DIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b)) #define PDIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b)) #define WORD2INT(x) ((x) < -32767.5f ? -32768 : \ ((x) > 32766.5f ? 32767 : (spx_int16_t)floor(.5 + (x)))) #endif #if defined (CONFIG_TI_C54X) || defined (CONFIG_TI_C55X) /* 2 on TI C5x DSP */ #define BYTES_PER_CHAR 2 #define BITS_PER_CHAR 16 #define LOG2_BITS_PER_CHAR 4 #else #define BYTES_PER_CHAR 1 #define BITS_PER_CHAR 8 #define LOG2_BITS_PER_CHAR 3 #endif #ifdef FIXED_DEBUG extern long long spx_mips; #endif #endif wfview-1.2d/resampler/resample.c000066400000000000000000001324231415164626400170130ustar00rootroot00000000000000/* Copyright (C) 2007-2008 Jean-Marc Valin Copyright (C) 2008 Thorvald Natvig File: resample.c Arbitrary resampling code Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* The design goals of this code are: - Very fast algorithm - SIMD-friendly algorithm - Low memory requirement - Good *perceptual* quality (and not best SNR) Warning: This resampler is relatively new. Although I think I got rid of all the major bugs and I don't expect the API to change anymore, there may be something I've missed. So use with caution. This algorithm is based on this original resampling algorithm: Smith, Julius O. Digital Audio Resampling Home Page Center for Computer Research in Music and Acoustics (CCRMA), Stanford University, 2007. Web published at https://ccrma.stanford.edu/~jos/resample/. There is one main difference, though. This resampler uses cubic interpolation instead of linear interpolation in the above paper. This makes the table much smaller and makes it possible to compute that table on a per-stream basis. In turn, being able to tweak the table for each stream makes it possible to both reduce complexity on simple ratios (e.g. 2/3), and get rid of the rounding operations in the inner loop. The latter both reduces CPU time and makes the algorithm more SIMD-friendly. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef OUTSIDE_SPEEX #include static void* speex_alloc(int size) { return calloc(size, 1); } static void* speex_realloc(void* ptr, int size) { return realloc(ptr, size); } static void speex_free(void* ptr) { free(ptr); } #ifndef EXPORT #define EXPORT #endif #include "speex_resampler.h" #include "arch.h" #else /* OUTSIDE_SPEEX */ #include "speex/speex_resampler.h" #include "arch.h" #include "os_support.h" #endif /* OUTSIDE_SPEEX */ #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #define IMAX(a,b) ((a) > (b) ? (a) : (b)) #define IMIN(a,b) ((a) < (b) ? (a) : (b)) #ifndef NULL #define NULL 0 #endif #ifndef UINT32_MAX #define UINT32_MAX 4294967295U #endif #ifdef USE_SSE #include "resample_sse.h" #endif #ifdef USE_NEON #include "resample_neon.h" #endif /* Numer of elements to allocate on the stack */ #ifdef VAR_ARRAYS #define FIXED_STACK_ALLOC 8192 #else #define FIXED_STACK_ALLOC 1024 #endif typedef int (*resampler_basic_func)(SpeexResamplerState*, spx_uint32_t, const spx_word16_t*, spx_uint32_t*, spx_word16_t*, spx_uint32_t*); struct SpeexResamplerState_ { spx_uint32_t in_rate; spx_uint32_t out_rate; spx_uint32_t num_rate; spx_uint32_t den_rate; int quality; spx_uint32_t nb_channels; spx_uint32_t filt_len; spx_uint32_t mem_alloc_size; spx_uint32_t buffer_size; int int_advance; int frac_advance; float cutoff; spx_uint32_t oversample; int initialised; int started; /* These are per-channel */ spx_int32_t* last_sample; spx_uint32_t* samp_frac_num; spx_uint32_t* magic_samples; spx_word16_t* mem; spx_word16_t* sinc_table; spx_uint32_t sinc_table_length; resampler_basic_func resampler_ptr; int in_stride; int out_stride; }; static const double kaiser12_table[68] = { 0.99859849, 1.00000000, 0.99859849, 0.99440475, 0.98745105, 0.97779076, 0.96549770, 0.95066529, 0.93340547, 0.91384741, 0.89213598, 0.86843014, 0.84290116, 0.81573067, 0.78710866, 0.75723148, 0.72629970, 0.69451601, 0.66208321, 0.62920216, 0.59606986, 0.56287762, 0.52980938, 0.49704014, 0.46473455, 0.43304576, 0.40211431, 0.37206735, 0.34301800, 0.31506490, 0.28829195, 0.26276832, 0.23854851, 0.21567274, 0.19416736, 0.17404546, 0.15530766, 0.13794294, 0.12192957, 0.10723616, 0.09382272, 0.08164178, 0.07063950, 0.06075685, 0.05193064, 0.04409466, 0.03718069, 0.03111947, 0.02584161, 0.02127838, 0.01736250, 0.01402878, 0.01121463, 0.00886058, 0.00691064, 0.00531256, 0.00401805, 0.00298291, 0.00216702, 0.00153438, 0.00105297, 0.00069463, 0.00043489, 0.00025272, 0.00013031, 0.0000527734, 0.00001000, 0.00000000 }; /* static const double kaiser12_table[36] = { 0.99440475, 1.00000000, 0.99440475, 0.97779076, 0.95066529, 0.91384741, 0.86843014, 0.81573067, 0.75723148, 0.69451601, 0.62920216, 0.56287762, 0.49704014, 0.43304576, 0.37206735, 0.31506490, 0.26276832, 0.21567274, 0.17404546, 0.13794294, 0.10723616, 0.08164178, 0.06075685, 0.04409466, 0.03111947, 0.02127838, 0.01402878, 0.00886058, 0.00531256, 0.00298291, 0.00153438, 0.00069463, 0.00025272, 0.0000527734, 0.00000500, 0.00000000}; */ static const double kaiser10_table[36] = { 0.99537781, 1.00000000, 0.99537781, 0.98162644, 0.95908712, 0.92831446, 0.89005583, 0.84522401, 0.79486424, 0.74011713, 0.68217934, 0.62226347, 0.56155915, 0.50119680, 0.44221549, 0.38553619, 0.33194107, 0.28205962, 0.23636152, 0.19515633, 0.15859932, 0.12670280, 0.09935205, 0.07632451, 0.05731132, 0.04193980, 0.02979584, 0.02044510, 0.01345224, 0.00839739, 0.00488951, 0.00257636, 0.00115101, 0.00035515, 0.00000000, 0.00000000 }; static const double kaiser8_table[36] = { 0.99635258, 1.00000000, 0.99635258, 0.98548012, 0.96759014, 0.94302200, 0.91223751, 0.87580811, 0.83439927, 0.78875245, 0.73966538, 0.68797126, 0.63451750, 0.58014482, 0.52566725, 0.47185369, 0.41941150, 0.36897272, 0.32108304, 0.27619388, 0.23465776, 0.19672670, 0.16255380, 0.13219758, 0.10562887, 0.08273982, 0.06335451, 0.04724088, 0.03412321, 0.02369490, 0.01563093, 0.00959968, 0.00527363, 0.00233883, 0.00050000, 0.00000000 }; static const double kaiser6_table[36] = { 0.99733006, 1.00000000, 0.99733006, 0.98935595, 0.97618418, 0.95799003, 0.93501423, 0.90755855, 0.87598009, 0.84068475, 0.80211977, 0.76076565, 0.71712752, 0.67172623, 0.62508937, 0.57774224, 0.53019925, 0.48295561, 0.43647969, 0.39120616, 0.34752997, 0.30580127, 0.26632152, 0.22934058, 0.19505503, 0.16360756, 0.13508755, 0.10953262, 0.08693120, 0.06722600, 0.05031820, 0.03607231, 0.02432151, 0.01487334, 0.00752000, 0.00000000 }; struct FuncDef { const double* table; int oversample; }; static const struct FuncDef kaiser12_funcdef = { kaiser12_table, 64 }; #define KAISER12 (&kaiser12_funcdef) static const struct FuncDef kaiser10_funcdef = { kaiser10_table, 32 }; #define KAISER10 (&kaiser10_funcdef) static const struct FuncDef kaiser8_funcdef = { kaiser8_table, 32 }; #define KAISER8 (&kaiser8_funcdef) static const struct FuncDef kaiser6_funcdef = { kaiser6_table, 32 }; #define KAISER6 (&kaiser6_funcdef) struct QualityMapping { int base_length; int oversample; float downsample_bandwidth; float upsample_bandwidth; const struct FuncDef* window_func; }; /* This table maps conversion quality to internal parameters. There are two reasons that explain why the up-sampling bandwidth is larger than the down-sampling bandwidth: 1) When up-sampling, we can assume that the spectrum is already attenuated close to the Nyquist rate (from an A/D or a previous resampling filter) 2) Any aliasing that occurs very close to the Nyquist rate will be masked by the sinusoids/noise just below the Nyquist rate (guaranteed only for up-sampling). */ static const struct QualityMapping quality_map[11] = { { 8, 4, 0.830f, 0.860f, KAISER6 }, /* Q0 */ { 16, 4, 0.850f, 0.880f, KAISER6 }, /* Q1 */ { 32, 4, 0.882f, 0.910f, KAISER6 }, /* Q2 */ /* 82.3% cutoff ( ~60 dB stop) 6 */ { 48, 8, 0.895f, 0.917f, KAISER8 }, /* Q3 */ /* 84.9% cutoff ( ~80 dB stop) 8 */ { 64, 8, 0.921f, 0.940f, KAISER8 }, /* Q4 */ /* 88.7% cutoff ( ~80 dB stop) 8 */ { 80, 16, 0.922f, 0.940f, KAISER10}, /* Q5 */ /* 89.1% cutoff (~100 dB stop) 10 */ { 96, 16, 0.940f, 0.945f, KAISER10}, /* Q6 */ /* 91.5% cutoff (~100 dB stop) 10 */ {128, 16, 0.950f, 0.950f, KAISER10}, /* Q7 */ /* 93.1% cutoff (~100 dB stop) 10 */ {160, 16, 0.960f, 0.960f, KAISER10}, /* Q8 */ /* 94.5% cutoff (~100 dB stop) 10 */ {192, 32, 0.968f, 0.968f, KAISER12}, /* Q9 */ /* 95.5% cutoff (~100 dB stop) 10 */ {256, 32, 0.975f, 0.975f, KAISER12}, /* Q10 */ /* 96.6% cutoff (~100 dB stop) 10 */ }; /*8,24,40,56,80,104,128,160,200,256,320*/ static double compute_func(float x, const struct FuncDef* func) { float y, frac; double interp[4]; int ind; y = x * func->oversample; ind = (int)floor(y); frac = (y - ind); /* CSE with handle the repeated powers */ interp[3] = -0.1666666667 * frac + 0.1666666667 * (frac * frac * frac); interp[2] = frac + 0.5 * (frac * frac) - 0.5 * (frac * frac * frac); /*interp[2] = 1.f - 0.5f*frac - frac*frac + 0.5f*frac*frac*frac;*/ interp[0] = -0.3333333333 * frac + 0.5 * (frac * frac) - 0.1666666667 * (frac * frac * frac); /* Just to make sure we don't have rounding problems */ interp[1] = 1.f - interp[3] - interp[2] - interp[0]; /*sum = frac*accum[1] + (1-frac)*accum[2];*/ return interp[0] * func->table[ind] + interp[1] * func->table[ind + 1] + interp[2] * func->table[ind + 2] + interp[3] * func->table[ind + 3]; } #if 0 #include int main(int argc, char** argv) { int i; for (i = 0; i < 256; i++) { printf("%f\n", compute_func(i / 256., KAISER12)); } return 0; } #endif #ifdef FIXED_POINT /* The slow way of computing a sinc for the table. Should improve that some day */ static spx_word16_t sinc(float cutoff, float x, int N, const struct FuncDef* window_func) { /*fprintf (stderr, "%f ", x);*/ float xx = x * cutoff; if (fabs(x) < 1e-6f) return WORD2INT(32768. * cutoff); else if (fabs(x) > .5f * N) return 0; /*FIXME: Can it really be any slower than this? */ return WORD2INT(32768. * cutoff * sin(M_PI * xx) / (M_PI * xx) * compute_func(fabs(2. * x / N), window_func)); } #else /* The slow way of computing a sinc for the table. Should improve that some day */ static spx_word16_t sinc(float cutoff, float x, int N, const struct FuncDef* window_func) { /*fprintf (stderr, "%f ", x);*/ float xx = x * cutoff; if (fabs(x) < 1e-6) return cutoff; else if (fabs(x) > .5 * N) return 0; /*FIXME: Can it really be any slower than this? */ return cutoff * sin(M_PI * xx) / (M_PI * xx) * compute_func(fabs(2. * x / N), window_func); } #endif #ifdef FIXED_POINT static void cubic_coef(spx_word16_t x, spx_word16_t interp[4]) { /* Compute interpolation coefficients. I'm not sure whether this corresponds to cubic interpolation but I know it's MMSE-optimal on a sinc */ spx_word16_t x2, x3; x2 = MULT16_16_P15(x, x); x3 = MULT16_16_P15(x, x2); interp[0] = PSHR32(MULT16_16(QCONST16(-0.16667f, 15), x) + MULT16_16(QCONST16(0.16667f, 15), x3), 15); interp[1] = EXTRACT16(EXTEND32(x) + SHR32(SUB32(EXTEND32(x2), EXTEND32(x3)), 1)); interp[3] = PSHR32(MULT16_16(QCONST16(-0.33333f, 15), x) + MULT16_16(QCONST16(.5f, 15), x2) - MULT16_16(QCONST16(0.16667f, 15), x3), 15); /* Just to make sure we don't have rounding problems */ interp[2] = Q15_ONE - interp[0] - interp[1] - interp[3]; if (interp[2] < 32767) interp[2] += 1; } #else static void cubic_coef(spx_word16_t frac, spx_word16_t interp[4]) { /* Compute interpolation coefficients. I'm not sure whether this corresponds to cubic interpolation but I know it's MMSE-optimal on a sinc */ interp[0] = -0.16667f * frac + 0.16667f * frac * frac * frac; interp[1] = frac + 0.5f * frac * frac - 0.5f * frac * frac * frac; /*interp[2] = 1.f - 0.5f*frac - frac*frac + 0.5f*frac*frac*frac;*/ interp[3] = -0.33333f * frac + 0.5f * frac * frac - 0.16667f * frac * frac * frac; /* Just to make sure we don't have rounding problems */ interp[2] = 1. - interp[0] - interp[1] - interp[3]; } #endif static int resampler_basic_direct_single(SpeexResamplerState* st, spx_uint32_t channel_index, const spx_word16_t* in, spx_uint32_t* in_len, spx_word16_t* out, spx_uint32_t* out_len) { const int N = st->filt_len; int out_sample = 0; int last_sample = st->last_sample[channel_index]; spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index]; const spx_word16_t* sinc_table = st->sinc_table; const int out_stride = st->out_stride; const int int_advance = st->int_advance; const int frac_advance = st->frac_advance; const spx_uint32_t den_rate = st->den_rate; spx_word32_t sum; while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len)) { const spx_word16_t* sinct = &sinc_table[samp_frac_num * N]; const spx_word16_t* iptr = &in[last_sample]; #ifndef OVERRIDE_INNER_PRODUCT_SINGLE int j; sum = 0; for (j = 0; j < N; j++) sum += MULT16_16(sinct[j], iptr[j]); /* This code is slower on most DSPs which have only 2 accumulators. Plus this this forces truncation to 32 bits and you lose the HW guard bits. I think we can trust the compiler and let it vectorize and/or unroll itself. spx_word32_t accum[4] = {0,0,0,0}; for(j=0;j= den_rate) { samp_frac_num -= den_rate; last_sample++; } } st->last_sample[channel_index] = last_sample; st->samp_frac_num[channel_index] = samp_frac_num; return out_sample; } #ifdef FIXED_POINT #else /* This is the same as the previous function, except with a double-precision accumulator */ static int resampler_basic_direct_double(SpeexResamplerState* st, spx_uint32_t channel_index, const spx_word16_t* in, spx_uint32_t* in_len, spx_word16_t* out, spx_uint32_t* out_len) { const int N = st->filt_len; int out_sample = 0; int last_sample = st->last_sample[channel_index]; spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index]; const spx_word16_t* sinc_table = st->sinc_table; const int out_stride = st->out_stride; const int int_advance = st->int_advance; const int frac_advance = st->frac_advance; const spx_uint32_t den_rate = st->den_rate; double sum; while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len)) { const spx_word16_t* sinct = &sinc_table[samp_frac_num * N]; const spx_word16_t* iptr = &in[last_sample]; #ifndef OVERRIDE_INNER_PRODUCT_DOUBLE int j; double accum[4] = { 0,0,0,0 }; for (j = 0; j < N; j += 4) { accum[0] += sinct[j] * iptr[j]; accum[1] += sinct[j + 1] * iptr[j + 1]; accum[2] += sinct[j + 2] * iptr[j + 2]; accum[3] += sinct[j + 3] * iptr[j + 3]; } sum = accum[0] + accum[1] + accum[2] + accum[3]; #else sum = inner_product_double(sinct, iptr, N); #endif out[out_stride * out_sample++] = PSHR32(sum, 15); last_sample += int_advance; samp_frac_num += frac_advance; if (samp_frac_num >= den_rate) { samp_frac_num -= den_rate; last_sample++; } } st->last_sample[channel_index] = last_sample; st->samp_frac_num[channel_index] = samp_frac_num; return out_sample; } #endif static int resampler_basic_interpolate_single(SpeexResamplerState* st, spx_uint32_t channel_index, const spx_word16_t* in, spx_uint32_t* in_len, spx_word16_t* out, spx_uint32_t* out_len) { const int N = st->filt_len; int out_sample = 0; int last_sample = st->last_sample[channel_index]; spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index]; const int out_stride = st->out_stride; const int int_advance = st->int_advance; const int frac_advance = st->frac_advance; const spx_uint32_t den_rate = st->den_rate; spx_word32_t sum; while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len)) { const spx_word16_t* iptr = &in[last_sample]; const int offset = samp_frac_num * st->oversample / st->den_rate; #ifdef FIXED_POINT const spx_word16_t frac = PDIV32(SHL32((samp_frac_num * st->oversample) % st->den_rate, 15), st->den_rate); #else const spx_word16_t frac = ((float)((samp_frac_num * st->oversample) % st->den_rate)) / st->den_rate; #endif spx_word16_t interp[4]; #ifndef OVERRIDE_INTERPOLATE_PRODUCT_SINGLE int j; spx_word32_t accum[4] = { 0,0,0,0 }; for (j = 0; j < N; j++) { const spx_word16_t curr_in = iptr[j]; accum[0] += MULT16_16(curr_in, st->sinc_table[4 + (j + 1) * st->oversample - offset - 2]); accum[1] += MULT16_16(curr_in, st->sinc_table[4 + (j + 1) * st->oversample - offset - 1]); accum[2] += MULT16_16(curr_in, st->sinc_table[4 + (j + 1) * st->oversample - offset]); accum[3] += MULT16_16(curr_in, st->sinc_table[4 + (j + 1) * st->oversample - offset + 1]); } cubic_coef(frac, interp); sum = MULT16_32_Q15(interp[0], accum[0]) + MULT16_32_Q15(interp[1], accum[1]) + MULT16_32_Q15(interp[2], accum[2]) + MULT16_32_Q15(interp[3], accum[3]); sum = SATURATE32PSHR(sum, 15, 32767); #else cubic_coef(frac, interp); sum = interpolate_product_single(iptr, st->sinc_table + st->oversample + 4 - offset - 2, N, st->oversample, interp); #endif out[out_stride * out_sample++] = sum; last_sample += int_advance; samp_frac_num += frac_advance; if (samp_frac_num >= den_rate) { samp_frac_num -= den_rate; last_sample++; } } st->last_sample[channel_index] = last_sample; st->samp_frac_num[channel_index] = samp_frac_num; return out_sample; } #ifdef FIXED_POINT #else /* This is the same as the previous function, except with a double-precision accumulator */ static int resampler_basic_interpolate_double(SpeexResamplerState* st, spx_uint32_t channel_index, const spx_word16_t* in, spx_uint32_t* in_len, spx_word16_t* out, spx_uint32_t* out_len) { const int N = st->filt_len; int out_sample = 0; int last_sample = st->last_sample[channel_index]; spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index]; const int out_stride = st->out_stride; const int int_advance = st->int_advance; const int frac_advance = st->frac_advance; const spx_uint32_t den_rate = st->den_rate; spx_word32_t sum; while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len)) { const spx_word16_t* iptr = &in[last_sample]; const int offset = samp_frac_num * st->oversample / st->den_rate; #ifdef FIXED_POINT const spx_word16_t frac = PDIV32(SHL32((samp_frac_num * st->oversample) % st->den_rate, 15), st->den_rate); #else const spx_word16_t frac = ((float)((samp_frac_num * st->oversample) % st->den_rate)) / st->den_rate; #endif spx_word16_t interp[4]; #ifndef OVERRIDE_INTERPOLATE_PRODUCT_DOUBLE int j; double accum[4] = { 0,0,0,0 }; for (j = 0; j < N; j++) { const double curr_in = iptr[j]; accum[0] += MULT16_16(curr_in, st->sinc_table[4 + (j + 1) * st->oversample - offset - 2]); accum[1] += MULT16_16(curr_in, st->sinc_table[4 + (j + 1) * st->oversample - offset - 1]); accum[2] += MULT16_16(curr_in, st->sinc_table[4 + (j + 1) * st->oversample - offset]); accum[3] += MULT16_16(curr_in, st->sinc_table[4 + (j + 1) * st->oversample - offset + 1]); } cubic_coef(frac, interp); sum = MULT16_32_Q15(interp[0], accum[0]) + MULT16_32_Q15(interp[1], accum[1]) + MULT16_32_Q15(interp[2], accum[2]) + MULT16_32_Q15(interp[3], accum[3]); #else cubic_coef(frac, interp); sum = interpolate_product_double(iptr, st->sinc_table + st->oversample + 4 - offset - 2, N, st->oversample, interp); #endif out[out_stride * out_sample++] = PSHR32(sum, 15); last_sample += int_advance; samp_frac_num += frac_advance; if (samp_frac_num >= den_rate) { samp_frac_num -= den_rate; last_sample++; } } st->last_sample[channel_index] = last_sample; st->samp_frac_num[channel_index] = samp_frac_num; return out_sample; } #endif /* This resampler is used to produce zero output in situations where memory for the filter could not be allocated. The expected numbers of input and output samples are still processed so that callers failing to check error codes are not surprised, possibly getting into infinite loops. */ static int resampler_basic_zero(SpeexResamplerState* st, spx_uint32_t channel_index, const spx_word16_t* in, spx_uint32_t* in_len, spx_word16_t* out, spx_uint32_t* out_len) { int out_sample = 0; int last_sample = st->last_sample[channel_index]; spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index]; const int out_stride = st->out_stride; const int int_advance = st->int_advance; const int frac_advance = st->frac_advance; const spx_uint32_t den_rate = st->den_rate; (void)in; while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len)) { out[out_stride * out_sample++] = 0; last_sample += int_advance; samp_frac_num += frac_advance; if (samp_frac_num >= den_rate) { samp_frac_num -= den_rate; last_sample++; } } st->last_sample[channel_index] = last_sample; st->samp_frac_num[channel_index] = samp_frac_num; return out_sample; } static int multiply_frac(spx_uint32_t* result, spx_uint32_t value, spx_uint32_t num, spx_uint32_t den) { spx_uint32_t major = value / den; spx_uint32_t remain = value % den; /* TODO: Could use 64 bits operation to check for overflow. But only guaranteed in C99+ */ if (remain > UINT32_MAX / num || major > UINT32_MAX / num || major * num > UINT32_MAX - remain * num / den) return RESAMPLER_ERR_OVERFLOW; *result = remain * num / den + major * num; return RESAMPLER_ERR_SUCCESS; } static int update_filter(SpeexResamplerState* st) { spx_uint32_t old_length = st->filt_len; spx_uint32_t old_alloc_size = st->mem_alloc_size; int use_direct; spx_uint32_t min_sinc_table_length; spx_uint32_t min_alloc_size; st->int_advance = st->num_rate / st->den_rate; st->frac_advance = st->num_rate % st->den_rate; st->oversample = quality_map[st->quality].oversample; st->filt_len = quality_map[st->quality].base_length; if (st->num_rate > st->den_rate) { /* down-sampling */ st->cutoff = quality_map[st->quality].downsample_bandwidth * st->den_rate / st->num_rate; if (multiply_frac(&st->filt_len, st->filt_len, st->num_rate, st->den_rate) != RESAMPLER_ERR_SUCCESS) goto fail; /* Round up to make sure we have a multiple of 8 for SSE */ st->filt_len = ((st->filt_len - 1) & (~0x7)) + 8; if (2 * st->den_rate < st->num_rate) st->oversample >>= 1; if (4 * st->den_rate < st->num_rate) st->oversample >>= 1; if (8 * st->den_rate < st->num_rate) st->oversample >>= 1; if (16 * st->den_rate < st->num_rate) st->oversample >>= 1; if (st->oversample < 1) st->oversample = 1; } else { /* up-sampling */ st->cutoff = quality_map[st->quality].upsample_bandwidth; } #ifdef RESAMPLE_FULL_SINC_TABLE use_direct = 1; if (INT_MAX / sizeof(spx_word16_t) / st->den_rate < st->filt_len) goto fail; #else /* Choose the resampling type that requires the least amount of memory */ use_direct = st->filt_len * st->den_rate <= st->filt_len * st->oversample + 8 && INT_MAX / sizeof(spx_word16_t) / st->den_rate >= st->filt_len; #endif if (use_direct) { min_sinc_table_length = st->filt_len * st->den_rate; } else { if ((INT_MAX / sizeof(spx_word16_t) - 8) / st->oversample < st->filt_len) goto fail; min_sinc_table_length = st->filt_len * st->oversample + 8; } if (st->sinc_table_length < min_sinc_table_length) { spx_word16_t* sinc_table = (spx_word16_t*)speex_realloc(st->sinc_table, min_sinc_table_length * sizeof(spx_word16_t)); if (!sinc_table) goto fail; st->sinc_table = sinc_table; st->sinc_table_length = min_sinc_table_length; } if (use_direct) { spx_uint32_t i; for (i = 0; i < st->den_rate; i++) { spx_int32_t j; for (j = 0; j < st->filt_len; j++) { st->sinc_table[i * st->filt_len + j] = sinc(st->cutoff, ((j - (spx_int32_t)st->filt_len / 2 + 1) - ((float)i) / st->den_rate), st->filt_len, quality_map[st->quality].window_func); } } #ifdef FIXED_POINT st->resampler_ptr = resampler_basic_direct_single; #else if (st->quality > 8) st->resampler_ptr = resampler_basic_direct_double; else st->resampler_ptr = resampler_basic_direct_single; #endif /*fprintf (stderr, "resampler uses direct sinc table and normalised cutoff %f\n", cutoff);*/ } else { spx_int32_t i; for (i = -4; i < (spx_int32_t)(st->oversample * st->filt_len + 4); i++) st->sinc_table[i + 4] = sinc(st->cutoff, (i / (float)st->oversample - st->filt_len / 2), st->filt_len, quality_map[st->quality].window_func); #ifdef FIXED_POINT st->resampler_ptr = resampler_basic_interpolate_single; #else if (st->quality > 8) st->resampler_ptr = resampler_basic_interpolate_double; else st->resampler_ptr = resampler_basic_interpolate_single; #endif /*fprintf (stderr, "resampler uses interpolated sinc table and normalised cutoff %f\n", cutoff);*/ } /* Here's the place where we update the filter memory to take into account the change in filter length. It's probably the messiest part of the code due to handling of lots of corner cases. */ /* Adding buffer_size to filt_len won't overflow here because filt_len could be multiplied by sizeof(spx_word16_t) above. */ min_alloc_size = st->filt_len - 1 + st->buffer_size; if (min_alloc_size > st->mem_alloc_size) { spx_word16_t* mem; if (INT_MAX / sizeof(spx_word16_t) / st->nb_channels < min_alloc_size) goto fail; else if (!(mem = (spx_word16_t*)speex_realloc(st->mem, st->nb_channels * min_alloc_size * sizeof(*mem)))) goto fail; st->mem = mem; st->mem_alloc_size = min_alloc_size; } if (!st->started) { spx_uint32_t i; for (i = 0; i < st->nb_channels * st->mem_alloc_size; i++) st->mem[i] = 0; /*speex_warning("reinit filter");*/ } else if (st->filt_len > old_length) { spx_uint32_t i; /* Increase the filter length */ /*speex_warning("increase filter size");*/ for (i = st->nb_channels; i--;) { spx_uint32_t j; spx_uint32_t olen = old_length; /*if (st->magic_samples[i])*/ { /* Try and remove the magic samples as if nothing had happened */ /* FIXME: This is wrong but for now we need it to avoid going over the array bounds */ olen = old_length + 2 * st->magic_samples[i]; for (j = old_length - 1 + st->magic_samples[i]; j--;) st->mem[i * st->mem_alloc_size + j + st->magic_samples[i]] = st->mem[i * old_alloc_size + j]; for (j = 0; j < st->magic_samples[i]; j++) st->mem[i * st->mem_alloc_size + j] = 0; st->magic_samples[i] = 0; } if (st->filt_len > olen) { /* If the new filter length is still bigger than the "augmented" length */ /* Copy data going backward */ for (j = 0; j < olen - 1; j++) st->mem[i * st->mem_alloc_size + (st->filt_len - 2 - j)] = st->mem[i * st->mem_alloc_size + (olen - 2 - j)]; /* Then put zeros for lack of anything better */ for (; j < st->filt_len - 1; j++) st->mem[i * st->mem_alloc_size + (st->filt_len - 2 - j)] = 0; /* Adjust last_sample */ st->last_sample[i] += (st->filt_len - olen) / 2; } else { /* Put back some of the magic! */ st->magic_samples[i] = (olen - st->filt_len) / 2; for (j = 0; j < st->filt_len - 1 + st->magic_samples[i]; j++) st->mem[i * st->mem_alloc_size + j] = st->mem[i * st->mem_alloc_size + j + st->magic_samples[i]]; } } } else if (st->filt_len < old_length) { spx_uint32_t i; /* Reduce filter length, this a bit tricky. We need to store some of the memory as "magic" samples so they can be used directly as input the next time(s) */ for (i = 0; i < st->nb_channels; i++) { spx_uint32_t j; spx_uint32_t old_magic = st->magic_samples[i]; st->magic_samples[i] = (old_length - st->filt_len) / 2; /* We must copy some of the memory that's no longer used */ /* Copy data going backward */ for (j = 0; j < st->filt_len - 1 + st->magic_samples[i] + old_magic; j++) st->mem[i * st->mem_alloc_size + j] = st->mem[i * st->mem_alloc_size + j + st->magic_samples[i]]; st->magic_samples[i] += old_magic; } } return RESAMPLER_ERR_SUCCESS; fail: st->resampler_ptr = resampler_basic_zero; /* st->mem may still contain consumed input samples for the filter. Restore filt_len so that filt_len - 1 still points to the position after the last of these samples. */ st->filt_len = old_length; return RESAMPLER_ERR_ALLOC_FAILED; } EXPORT SpeexResamplerState* speex_resampler_init(spx_uint32_t nb_channels, spx_uint32_t in_rate, spx_uint32_t out_rate, int quality, int* err) { return speex_resampler_init_frac(nb_channels, in_rate, out_rate, in_rate, out_rate, quality, err); } EXPORT SpeexResamplerState* speex_resampler_init_frac(spx_uint32_t nb_channels, spx_uint32_t ratio_num, spx_uint32_t ratio_den, spx_uint32_t in_rate, spx_uint32_t out_rate, int quality, int* err) { SpeexResamplerState* st; int filter_err; if (nb_channels == 0 || ratio_num == 0 || ratio_den == 0 || quality > 10 || quality < 0) { if (err) *err = RESAMPLER_ERR_INVALID_ARG; return NULL; } st = (SpeexResamplerState*)speex_alloc(sizeof(SpeexResamplerState)); if (!st) { if (err) *err = RESAMPLER_ERR_ALLOC_FAILED; return NULL; } st->initialised = 0; st->started = 0; st->in_rate = 0; st->out_rate = 0; st->num_rate = 0; st->den_rate = 0; st->quality = -1; st->sinc_table_length = 0; st->mem_alloc_size = 0; st->filt_len = 0; st->mem = 0; st->resampler_ptr = 0; st->cutoff = 1.f; st->nb_channels = nb_channels; st->in_stride = 1; st->out_stride = 1; st->buffer_size = 160; /* Per channel data */ if (!(st->last_sample = (spx_int32_t*)speex_alloc(nb_channels * sizeof(spx_int32_t)))) goto fail; if (!(st->magic_samples = (spx_uint32_t*)speex_alloc(nb_channels * sizeof(spx_uint32_t)))) goto fail; if (!(st->samp_frac_num = (spx_uint32_t*)speex_alloc(nb_channels * sizeof(spx_uint32_t)))) goto fail; speex_resampler_set_quality(st, quality); speex_resampler_set_rate_frac(st, ratio_num, ratio_den, in_rate, out_rate); filter_err = update_filter(st); if (filter_err == RESAMPLER_ERR_SUCCESS) { st->initialised = 1; } else { speex_resampler_destroy(st); st = NULL; } if (err) *err = filter_err; return st; fail: if (err) *err = RESAMPLER_ERR_ALLOC_FAILED; speex_resampler_destroy(st); return NULL; } EXPORT void speex_resampler_destroy(SpeexResamplerState* st) { speex_free(st->mem); speex_free(st->sinc_table); speex_free(st->last_sample); speex_free(st->magic_samples); speex_free(st->samp_frac_num); speex_free(st); } static int speex_resampler_process_native(SpeexResamplerState* st, spx_uint32_t channel_index, spx_uint32_t* in_len, spx_word16_t* out, spx_uint32_t* out_len) { int j = 0; const int N = st->filt_len; int out_sample = 0; spx_word16_t* mem = st->mem + channel_index * st->mem_alloc_size; spx_uint32_t ilen; st->started = 1; /* Call the right resampler through the function ptr */ out_sample = st->resampler_ptr(st, channel_index, mem, in_len, out, out_len); if (st->last_sample[channel_index] < (spx_int32_t)*in_len) *in_len = st->last_sample[channel_index]; *out_len = out_sample; st->last_sample[channel_index] -= *in_len; ilen = *in_len; for (j = 0; j < N - 1; ++j) mem[j] = mem[j + ilen]; return RESAMPLER_ERR_SUCCESS; } static int speex_resampler_magic(SpeexResamplerState* st, spx_uint32_t channel_index, spx_word16_t** out, spx_uint32_t out_len) { spx_uint32_t tmp_in_len = st->magic_samples[channel_index]; spx_word16_t* mem = st->mem + channel_index * st->mem_alloc_size; const int N = st->filt_len; speex_resampler_process_native(st, channel_index, &tmp_in_len, *out, &out_len); st->magic_samples[channel_index] -= tmp_in_len; /* If we couldn't process all "magic" input samples, save the rest for next time */ if (st->magic_samples[channel_index]) { spx_uint32_t i; for (i = 0; i < st->magic_samples[channel_index]; i++) mem[N - 1 + i] = mem[N - 1 + i + tmp_in_len]; } *out += out_len * st->out_stride; return out_len; } #ifdef FIXED_POINT EXPORT int speex_resampler_process_int(SpeexResamplerState* st, spx_uint32_t channel_index, const spx_int16_t* in, spx_uint32_t* in_len, spx_int16_t* out, spx_uint32_t* out_len) #else EXPORT int speex_resampler_process_float(SpeexResamplerState* st, spx_uint32_t channel_index, const float* in, spx_uint32_t* in_len, float* out, spx_uint32_t* out_len) #endif { int j; spx_uint32_t ilen = *in_len; spx_uint32_t olen = *out_len; spx_word16_t* x = st->mem + channel_index * st->mem_alloc_size; const int filt_offs = st->filt_len - 1; const spx_uint32_t xlen = st->mem_alloc_size - filt_offs; const int istride = st->in_stride; if (st->magic_samples[channel_index]) olen -= speex_resampler_magic(st, channel_index, &out, olen); if (!st->magic_samples[channel_index]) { while (ilen && olen) { spx_uint32_t ichunk = (ilen > xlen) ? xlen : ilen; spx_uint32_t ochunk = olen; if (in) { for (j = 0; j < ichunk; ++j) x[j + filt_offs] = in[j * istride]; } else { for (j = 0; j < ichunk; ++j) x[j + filt_offs] = 0; } speex_resampler_process_native(st, channel_index, &ichunk, out, &ochunk); ilen -= ichunk; olen -= ochunk; out += ochunk * st->out_stride; if (in) in += ichunk * istride; } } *in_len -= ilen; *out_len -= olen; return st->resampler_ptr == resampler_basic_zero ? RESAMPLER_ERR_ALLOC_FAILED : RESAMPLER_ERR_SUCCESS; } #ifdef FIXED_POINT EXPORT int speex_resampler_process_float(SpeexResamplerState* st, spx_uint32_t channel_index, const float* in, spx_uint32_t* in_len, float* out, spx_uint32_t* out_len) #else EXPORT int speex_resampler_process_int(SpeexResamplerState* st, spx_uint32_t channel_index, const spx_int16_t* in, spx_uint32_t* in_len, spx_int16_t* out, spx_uint32_t* out_len) #endif { int j; const int istride_save = st->in_stride; const int ostride_save = st->out_stride; spx_uint32_t ilen = *in_len; spx_uint32_t olen = *out_len; spx_word16_t* x = st->mem + channel_index * st->mem_alloc_size; const spx_uint32_t xlen = st->mem_alloc_size - (st->filt_len - 1); #ifdef VAR_ARRAYS const unsigned int ylen = (olen < FIXED_STACK_ALLOC) ? olen : FIXED_STACK_ALLOC; spx_word16_t ystack[ylen]; #else const unsigned int ylen = FIXED_STACK_ALLOC; spx_word16_t ystack[FIXED_STACK_ALLOC]; #endif st->out_stride = 1; while (ilen && olen) { spx_word16_t* y = ystack; spx_uint32_t ichunk = (ilen > xlen) ? xlen : ilen; spx_uint32_t ochunk = (olen > ylen) ? ylen : olen; spx_uint32_t omagic = 0; if (st->magic_samples[channel_index]) { omagic = speex_resampler_magic(st, channel_index, &y, ochunk); ochunk -= omagic; olen -= omagic; } if (!st->magic_samples[channel_index]) { if (in) { for (j = 0; j < ichunk; ++j) #ifdef FIXED_POINT x[j + st->filt_len - 1] = WORD2INT(in[j * istride_save]); #else x[j + st->filt_len - 1] = in[j * istride_save]; #endif } else { for (j = 0; j < ichunk; ++j) x[j + st->filt_len - 1] = 0; } speex_resampler_process_native(st, channel_index, &ichunk, y, &ochunk); } else { ichunk = 0; ochunk = 0; } for (j = 0; j < ochunk + omagic; ++j) #ifdef FIXED_POINT out[j * ostride_save] = ystack[j]; #else out[j * ostride_save] = WORD2INT(ystack[j]); #endif ilen -= ichunk; olen -= ochunk; out += (ochunk + omagic) * ostride_save; if (in) in += ichunk * istride_save; } st->out_stride = ostride_save; *in_len -= ilen; *out_len -= olen; return st->resampler_ptr == resampler_basic_zero ? RESAMPLER_ERR_ALLOC_FAILED : RESAMPLER_ERR_SUCCESS; } EXPORT int speex_resampler_process_interleaved_float(SpeexResamplerState* st, const float* in, spx_uint32_t* in_len, float* out, spx_uint32_t* out_len) { spx_uint32_t i; int istride_save, ostride_save; spx_uint32_t bak_out_len = *out_len; spx_uint32_t bak_in_len = *in_len; istride_save = st->in_stride; ostride_save = st->out_stride; st->in_stride = st->out_stride = st->nb_channels; for (i = 0; i < st->nb_channels; i++) { *out_len = bak_out_len; *in_len = bak_in_len; if (in != NULL) speex_resampler_process_float(st, i, in + i, in_len, out + i, out_len); else speex_resampler_process_float(st, i, NULL, in_len, out + i, out_len); } st->in_stride = istride_save; st->out_stride = ostride_save; return st->resampler_ptr == resampler_basic_zero ? RESAMPLER_ERR_ALLOC_FAILED : RESAMPLER_ERR_SUCCESS; } EXPORT int speex_resampler_process_interleaved_int(SpeexResamplerState* st, const spx_int16_t* in, spx_uint32_t* in_len, spx_int16_t* out, spx_uint32_t* out_len) { spx_uint32_t i; int istride_save, ostride_save; spx_uint32_t bak_out_len = *out_len; spx_uint32_t bak_in_len = *in_len; istride_save = st->in_stride; ostride_save = st->out_stride; st->in_stride = st->out_stride = st->nb_channels; for (i = 0; i < st->nb_channels; i++) { *out_len = bak_out_len; *in_len = bak_in_len; if (in != NULL) speex_resampler_process_int(st, i, in + i, in_len, out + i, out_len); else speex_resampler_process_int(st, i, NULL, in_len, out + i, out_len); } st->in_stride = istride_save; st->out_stride = ostride_save; return st->resampler_ptr == resampler_basic_zero ? RESAMPLER_ERR_ALLOC_FAILED : RESAMPLER_ERR_SUCCESS; } EXPORT int speex_resampler_set_rate(SpeexResamplerState* st, spx_uint32_t in_rate, spx_uint32_t out_rate) { return speex_resampler_set_rate_frac(st, in_rate, out_rate, in_rate, out_rate); } EXPORT void speex_resampler_get_rate(SpeexResamplerState* st, spx_uint32_t* in_rate, spx_uint32_t* out_rate) { *in_rate = st->in_rate; *out_rate = st->out_rate; } static inline spx_uint32_t compute_gcd(spx_uint32_t a, spx_uint32_t b) { while (b != 0) { spx_uint32_t temp = a; a = b; b = temp % b; } return a; } EXPORT int speex_resampler_set_rate_frac(SpeexResamplerState* st, spx_uint32_t ratio_num, spx_uint32_t ratio_den, spx_uint32_t in_rate, spx_uint32_t out_rate) { spx_uint32_t fact; spx_uint32_t old_den; spx_uint32_t i; if (ratio_num == 0 || ratio_den == 0) return RESAMPLER_ERR_INVALID_ARG; if (st->in_rate == in_rate && st->out_rate == out_rate && st->num_rate == ratio_num && st->den_rate == ratio_den) return RESAMPLER_ERR_SUCCESS; old_den = st->den_rate; st->in_rate = in_rate; st->out_rate = out_rate; st->num_rate = ratio_num; st->den_rate = ratio_den; fact = compute_gcd(st->num_rate, st->den_rate); st->num_rate /= fact; st->den_rate /= fact; if (old_den > 0) { for (i = 0; i < st->nb_channels; i++) { if (multiply_frac(&st->samp_frac_num[i], st->samp_frac_num[i], st->den_rate, old_den) != RESAMPLER_ERR_SUCCESS) return RESAMPLER_ERR_OVERFLOW; /* Safety net */ if (st->samp_frac_num[i] >= st->den_rate) st->samp_frac_num[i] = st->den_rate - 1; } } if (st->initialised) return update_filter(st); return RESAMPLER_ERR_SUCCESS; } EXPORT void speex_resampler_get_ratio(SpeexResamplerState* st, spx_uint32_t* ratio_num, spx_uint32_t* ratio_den) { *ratio_num = st->num_rate; *ratio_den = st->den_rate; } EXPORT int speex_resampler_set_quality(SpeexResamplerState* st, int quality) { if (quality > 10 || quality < 0) return RESAMPLER_ERR_INVALID_ARG; if (st->quality == quality) return RESAMPLER_ERR_SUCCESS; st->quality = quality; if (st->initialised) return update_filter(st); return RESAMPLER_ERR_SUCCESS; } EXPORT void speex_resampler_get_quality(SpeexResamplerState* st, int* quality) { *quality = st->quality; } EXPORT void speex_resampler_set_input_stride(SpeexResamplerState* st, spx_uint32_t stride) { st->in_stride = stride; } EXPORT void speex_resampler_get_input_stride(SpeexResamplerState* st, spx_uint32_t* stride) { *stride = st->in_stride; } EXPORT void speex_resampler_set_output_stride(SpeexResamplerState* st, spx_uint32_t stride) { st->out_stride = stride; } EXPORT void speex_resampler_get_output_stride(SpeexResamplerState* st, spx_uint32_t* stride) { *stride = st->out_stride; } EXPORT int speex_resampler_get_input_latency(SpeexResamplerState* st) { return st->filt_len / 2; } EXPORT int speex_resampler_get_output_latency(SpeexResamplerState* st) { return ((st->filt_len / 2) * st->den_rate + (st->num_rate >> 1)) / st->num_rate; } EXPORT int speex_resampler_skip_zeros(SpeexResamplerState* st) { spx_uint32_t i; for (i = 0; i < st->nb_channels; i++) st->last_sample[i] = st->filt_len / 2; return RESAMPLER_ERR_SUCCESS; } EXPORT int speex_resampler_reset_mem(SpeexResamplerState* st) { spx_uint32_t i; for (i = 0; i < st->nb_channels; i++) { st->last_sample[i] = 0; st->magic_samples[i] = 0; st->samp_frac_num[i] = 0; } for (i = 0; i < st->nb_channels * (st->filt_len - 1); i++) st->mem[i] = 0; return RESAMPLER_ERR_SUCCESS; } EXPORT const char* speex_resampler_strerror(int err) { switch (err) { case RESAMPLER_ERR_SUCCESS: return "Success."; case RESAMPLER_ERR_ALLOC_FAILED: return "Memory allocation failed."; case RESAMPLER_ERR_BAD_STATE: return "Bad resampler state."; case RESAMPLER_ERR_INVALID_ARG: return "Invalid argument."; case RESAMPLER_ERR_PTR_OVERLAP: return "Input and output buffers overlap."; default: return "Unknown error. Bad error code or strange version mismatch."; } }wfview-1.2d/resampler/resample_neon.h000066400000000000000000000242321415164626400200350ustar00rootroot00000000000000/* Copyright (C) 2007-2008 Jean-Marc Valin * Copyright (C) 2008 Thorvald Natvig * Copyright (C) 2011 Texas Instruments * author Jyri Sarha */ /** @file resample_neon.h @brief Resampler functions (NEON version) */ /* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Xiph.org Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifdef FIXED_POINT #if defined(__aarch64__) static inline int32_t saturate_32bit_to_16bit(int32_t a) { int32_t ret; asm ("fmov s0, %w[a]\n" "sqxtn h0, s0\n" "sxtl v0.4s, v0.4h\n" "fmov %w[ret], s0\n" : [ret] "=r" (ret) : [a] "r" (a) : "v0" ); return ret; } #elif defined(__thumb2__) static inline int32_t saturate_32bit_to_16bit(int32_t a) { int32_t ret; asm ("ssat %[ret], #16, %[a]" : [ret] "=r" (ret) : [a] "r" (a) : ); return ret; } #else static inline int32_t saturate_32bit_to_16bit(int32_t a) { int32_t ret; asm ("vmov.s32 d0[0], %[a]\n" "vqmovn.s32 d0, q0\n" "vmov.s16 %[ret], d0[0]\n" : [ret] "=r" (ret) : [a] "r" (a) : "q0"); return ret; } #endif #undef WORD2INT #define WORD2INT(x) (saturate_32bit_to_16bit(x)) #define OVERRIDE_INNER_PRODUCT_SINGLE /* Only works when len % 4 == 0 and len >= 4 */ #if defined(__aarch64__) static inline int32_t inner_product_single(const int16_t *a, const int16_t *b, unsigned int len) { int32_t ret; uint32_t remainder = len % 16; len = len - remainder; asm volatile (" cmp %w[len], #0\n" " b.ne 1f\n" " ld1 {v16.4h}, [%[b]], #8\n" " ld1 {v20.4h}, [%[a]], #8\n" " subs %w[remainder], %w[remainder], #4\n" " smull v0.4s, v16.4h, v20.4h\n" " b.ne 4f\n" " b 5f\n" "1:" " ld1 {v16.4h, v17.4h, v18.4h, v19.4h}, [%[b]], #32\n" " ld1 {v20.4h, v21.4h, v22.4h, v23.4h}, [%[a]], #32\n" " subs %w[len], %w[len], #16\n" " smull v0.4s, v16.4h, v20.4h\n" " smlal v0.4s, v17.4h, v21.4h\n" " smlal v0.4s, v18.4h, v22.4h\n" " smlal v0.4s, v19.4h, v23.4h\n" " b.eq 3f\n" "2:" " ld1 {v16.4h, v17.4h, v18.4h, v19.4h}, [%[b]], #32\n" " ld1 {v20.4h, v21.4h, v22.4h, v23.4h}, [%[a]], #32\n" " subs %w[len], %w[len], #16\n" " smlal v0.4s, v16.4h, v20.4h\n" " smlal v0.4s, v17.4h, v21.4h\n" " smlal v0.4s, v18.4h, v22.4h\n" " smlal v0.4s, v19.4h, v23.4h\n" " b.ne 2b\n" "3:" " cmp %w[remainder], #0\n" " b.eq 5f\n" "4:" " ld1 {v18.4h}, [%[b]], #8\n" " ld1 {v22.4h}, [%[a]], #8\n" " subs %w[remainder], %w[remainder], #4\n" " smlal v0.4s, v18.4h, v22.4h\n" " b.ne 4b\n" "5:" " saddlv d0, v0.4s\n" " sqxtn s0, d0\n" " sqrshrn h0, s0, #15\n" " sxtl v0.4s, v0.4h\n" " fmov %w[ret], s0\n" : [ret] "=r" (ret), [a] "+r" (a), [b] "+r" (b), [len] "+r" (len), [remainder] "+r" (remainder) : : "cc", "v0", "v16", "v17", "v18", "v19", "v20", "v21", "v22", "v23"); return ret; } #else static inline int32_t inner_product_single(const int16_t *a, const int16_t *b, unsigned int len) { int32_t ret; uint32_t remainder = len % 16; len = len - remainder; asm volatile (" cmp %[len], #0\n" " bne 1f\n" " vld1.16 {d16}, [%[b]]!\n" " vld1.16 {d20}, [%[a]]!\n" " subs %[remainder], %[remainder], #4\n" " vmull.s16 q0, d16, d20\n" " beq 5f\n" " b 4f\n" "1:" " vld1.16 {d16, d17, d18, d19}, [%[b]]!\n" " vld1.16 {d20, d21, d22, d23}, [%[a]]!\n" " subs %[len], %[len], #16\n" " vmull.s16 q0, d16, d20\n" " vmlal.s16 q0, d17, d21\n" " vmlal.s16 q0, d18, d22\n" " vmlal.s16 q0, d19, d23\n" " beq 3f\n" "2:" " vld1.16 {d16, d17, d18, d19}, [%[b]]!\n" " vld1.16 {d20, d21, d22, d23}, [%[a]]!\n" " subs %[len], %[len], #16\n" " vmlal.s16 q0, d16, d20\n" " vmlal.s16 q0, d17, d21\n" " vmlal.s16 q0, d18, d22\n" " vmlal.s16 q0, d19, d23\n" " bne 2b\n" "3:" " cmp %[remainder], #0\n" " beq 5f\n" "4:" " vld1.16 {d16}, [%[b]]!\n" " vld1.16 {d20}, [%[a]]!\n" " subs %[remainder], %[remainder], #4\n" " vmlal.s16 q0, d16, d20\n" " bne 4b\n" "5:" " vaddl.s32 q0, d0, d1\n" " vadd.s64 d0, d0, d1\n" " vqmovn.s64 d0, q0\n" " vqrshrn.s32 d0, q0, #15\n" " vmov.s16 %[ret], d0[0]\n" : [ret] "=r" (ret), [a] "+r" (a), [b] "+r" (b), [len] "+r" (len), [remainder] "+r" (remainder) : : "cc", "q0", "d16", "d17", "d18", "d19", "d20", "d21", "d22", "d23"); return ret; } #endif // !defined(__aarch64__) #elif defined(FLOATING_POINT) #if defined(__aarch64__) static inline int32_t saturate_float_to_16bit(float a) { int32_t ret; asm ("fcvtas s1, %s[a]\n" "sqxtn h1, s1\n" "sxtl v1.4s, v1.4h\n" "fmov %w[ret], s1\n" : [ret] "=r" (ret) : [a] "w" (a) : "v1"); return ret; } #else static inline int32_t saturate_float_to_16bit(float a) { int32_t ret; asm ("vmov.f32 d0[0], %[a]\n" "vcvt.s32.f32 d0, d0, #15\n" "vqrshrn.s32 d0, q0, #15\n" "vmov.s16 %[ret], d0[0]\n" : [ret] "=r" (ret) : [a] "r" (a) : "q0"); return ret; } #endif #undef WORD2INT #define WORD2INT(x) (saturate_float_to_16bit(x)) #define OVERRIDE_INNER_PRODUCT_SINGLE /* Only works when len % 4 == 0 and len >= 4 */ #if defined(__aarch64__) static inline float inner_product_single(const float *a, const float *b, unsigned int len) { float ret; uint32_t remainder = len % 16; len = len - remainder; asm volatile (" cmp %w[len], #0\n" " b.ne 1f\n" " ld1 {v16.4s}, [%[b]], #16\n" " ld1 {v20.4s}, [%[a]], #16\n" " subs %w[remainder], %w[remainder], #4\n" " fmul v1.4s, v16.4s, v20.4s\n" " b.ne 4f\n" " b 5f\n" "1:" " ld1 {v16.4s, v17.4s, v18.4s, v19.4s}, [%[b]], #64\n" " ld1 {v20.4s, v21.4s, v22.4s, v23.4s}, [%[a]], #64\n" " subs %w[len], %w[len], #16\n" " fmul v1.4s, v16.4s, v20.4s\n" " fmul v2.4s, v17.4s, v21.4s\n" " fmul v3.4s, v18.4s, v22.4s\n" " fmul v4.4s, v19.4s, v23.4s\n" " b.eq 3f\n" "2:" " ld1 {v16.4s, v17.4s, v18.4s, v19.4s}, [%[b]], #64\n" " ld1 {v20.4s, v21.4s, v22.4s, v23.4s}, [%[a]], #64\n" " subs %w[len], %w[len], #16\n" " fmla v1.4s, v16.4s, v20.4s\n" " fmla v2.4s, v17.4s, v21.4s\n" " fmla v3.4s, v18.4s, v22.4s\n" " fmla v4.4s, v19.4s, v23.4s\n" " b.ne 2b\n" "3:" " fadd v16.4s, v1.4s, v2.4s\n" " fadd v17.4s, v3.4s, v4.4s\n" " cmp %w[remainder], #0\n" " fadd v1.4s, v16.4s, v17.4s\n" " b.eq 5f\n" "4:" " ld1 {v18.4s}, [%[b]], #16\n" " ld1 {v22.4s}, [%[a]], #16\n" " subs %w[remainder], %w[remainder], #4\n" " fmla v1.4s, v18.4s, v22.4s\n" " b.ne 4b\n" "5:" " faddp v1.4s, v1.4s, v1.4s\n" " faddp %[ret].4s, v1.4s, v1.4s\n" : [ret] "=w" (ret), [a] "+r" (a), [b] "+r" (b), [len] "+r" (len), [remainder] "+r" (remainder) : : "cc", "v1", "v2", "v3", "v4", "v16", "v17", "v18", "v19", "v20", "v21", "v22", "v23"); return ret; } #else static inline float inner_product_single(const float *a, const float *b, unsigned int len) { float ret; uint32_t remainder = len % 16; len = len - remainder; asm volatile (" cmp %[len], #0\n" " bne 1f\n" " vld1.32 {q4}, [%[b]]!\n" " vld1.32 {q8}, [%[a]]!\n" " subs %[remainder], %[remainder], #4\n" " vmul.f32 q0, q4, q8\n" " bne 4f\n" " b 5f\n" "1:" " vld1.32 {q4, q5}, [%[b]]!\n" " vld1.32 {q8, q9}, [%[a]]!\n" " vld1.32 {q6, q7}, [%[b]]!\n" " vld1.32 {q10, q11}, [%[a]]!\n" " subs %[len], %[len], #16\n" " vmul.f32 q0, q4, q8\n" " vmul.f32 q1, q5, q9\n" " vmul.f32 q2, q6, q10\n" " vmul.f32 q3, q7, q11\n" " beq 3f\n" "2:" " vld1.32 {q4, q5}, [%[b]]!\n" " vld1.32 {q8, q9}, [%[a]]!\n" " vld1.32 {q6, q7}, [%[b]]!\n" " vld1.32 {q10, q11}, [%[a]]!\n" " subs %[len], %[len], #16\n" " vmla.f32 q0, q4, q8\n" " vmla.f32 q1, q5, q9\n" " vmla.f32 q2, q6, q10\n" " vmla.f32 q3, q7, q11\n" " bne 2b\n" "3:" " vadd.f32 q4, q0, q1\n" " vadd.f32 q5, q2, q3\n" " cmp %[remainder], #0\n" " vadd.f32 q0, q4, q5\n" " beq 5f\n" "4:" " vld1.32 {q6}, [%[b]]!\n" " vld1.32 {q10}, [%[a]]!\n" " subs %[remainder], %[remainder], #4\n" " vmla.f32 q0, q6, q10\n" " bne 4b\n" "5:" " vadd.f32 d0, d0, d1\n" " vpadd.f32 d0, d0, d0\n" " vmov.f32 %[ret], d0[0]\n" : [ret] "=r" (ret), [a] "+r" (a), [b] "+r" (b), [len] "+l" (len), [remainder] "+l" (remainder) : : "cc", "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10", "q11"); return ret; } #endif // defined(__aarch64__) #endifwfview-1.2d/resampler/resample_sse.h000066400000000000000000000114041415164626400176650ustar00rootroot00000000000000/* Copyright (C) 2007-2008 Jean-Marc Valin * Copyright (C) 2008 Thorvald Natvig */ /** @file resample_sse.h @brief Resampler functions (SSE version) */ /* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Xiph.org Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #define OVERRIDE_INNER_PRODUCT_SINGLE static inline float inner_product_single(const float* a, const float* b, unsigned int len) { int i; float ret; __m128 sum = _mm_setzero_ps(); for (i = 0; i < len; i += 8) { sum = _mm_add_ps(sum, _mm_mul_ps(_mm_loadu_ps(a + i), _mm_loadu_ps(b + i))); sum = _mm_add_ps(sum, _mm_mul_ps(_mm_loadu_ps(a + i + 4), _mm_loadu_ps(b + i + 4))); } sum = _mm_add_ps(sum, _mm_movehl_ps(sum, sum)); sum = _mm_add_ss(sum, _mm_shuffle_ps(sum, sum, 0x55)); _mm_store_ss(&ret, sum); return ret; } #define OVERRIDE_INTERPOLATE_PRODUCT_SINGLE static inline float interpolate_product_single(const float* a, const float* b, unsigned int len, const spx_uint32_t oversample, float* frac) { int i; float ret; __m128 sum = _mm_setzero_ps(); __m128 f = _mm_loadu_ps(frac); for (i = 0; i < len; i += 2) { sum = _mm_add_ps(sum, _mm_mul_ps(_mm_load1_ps(a + i), _mm_loadu_ps(b + i * oversample))); sum = _mm_add_ps(sum, _mm_mul_ps(_mm_load1_ps(a + i + 1), _mm_loadu_ps(b + (i + 1) * oversample))); } sum = _mm_mul_ps(f, sum); sum = _mm_add_ps(sum, _mm_movehl_ps(sum, sum)); sum = _mm_add_ss(sum, _mm_shuffle_ps(sum, sum, 0x55)); _mm_store_ss(&ret, sum); return ret; } #ifdef USE_SSE2 #include #define OVERRIDE_INNER_PRODUCT_DOUBLE static inline double inner_product_double(const float* a, const float* b, unsigned int len) { int i; double ret; __m128d sum = _mm_setzero_pd(); __m128 t; for (i = 0; i < len; i += 8) { t = _mm_mul_ps(_mm_loadu_ps(a + i), _mm_loadu_ps(b + i)); sum = _mm_add_pd(sum, _mm_cvtps_pd(t)); sum = _mm_add_pd(sum, _mm_cvtps_pd(_mm_movehl_ps(t, t))); t = _mm_mul_ps(_mm_loadu_ps(a + i + 4), _mm_loadu_ps(b + i + 4)); sum = _mm_add_pd(sum, _mm_cvtps_pd(t)); sum = _mm_add_pd(sum, _mm_cvtps_pd(_mm_movehl_ps(t, t))); } sum = _mm_add_sd(sum, _mm_unpackhi_pd(sum, sum)); _mm_store_sd(&ret, sum); return ret; } #define OVERRIDE_INTERPOLATE_PRODUCT_DOUBLE static inline double interpolate_product_double(const float* a, const float* b, unsigned int len, const spx_uint32_t oversample, float* frac) { int i; double ret; __m128d sum; __m128d sum1 = _mm_setzero_pd(); __m128d sum2 = _mm_setzero_pd(); __m128 f = _mm_loadu_ps(frac); __m128d f1 = _mm_cvtps_pd(f); __m128d f2 = _mm_cvtps_pd(_mm_movehl_ps(f, f)); __m128 t; for (i = 0; i < len; i += 2) { t = _mm_mul_ps(_mm_load1_ps(a + i), _mm_loadu_ps(b + i * oversample)); sum1 = _mm_add_pd(sum1, _mm_cvtps_pd(t)); sum2 = _mm_add_pd(sum2, _mm_cvtps_pd(_mm_movehl_ps(t, t))); t = _mm_mul_ps(_mm_load1_ps(a + i + 1), _mm_loadu_ps(b + (i + 1) * oversample)); sum1 = _mm_add_pd(sum1, _mm_cvtps_pd(t)); sum2 = _mm_add_pd(sum2, _mm_cvtps_pd(_mm_movehl_ps(t, t))); } sum1 = _mm_mul_pd(f1, sum1); sum2 = _mm_mul_pd(f2, sum2); sum = _mm_add_pd(sum1, sum2); sum = _mm_add_sd(sum, _mm_unpackhi_pd(sum, sum)); _mm_store_sd(&ret, sum); return ret; } #endifwfview-1.2d/resampler/speex_resampler.h000066400000000000000000000345731415164626400204150ustar00rootroot00000000000000/* Copyright (C) 2007 Jean-Marc Valin File: speex_resampler.h Resampling code The design goals of this code are: - Very fast algorithm - Low memory requirement - Good *perceptual* quality (and not best SNR) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef SPEEX_RESAMPLER_H #define SPEEX_RESAMPLER_H #ifdef OUTSIDE_SPEEX /********* WARNING: MENTAL SANITY ENDS HERE *************/ /* If the resampler is defined outside of Speex, we change the symbol names so that there won't be any clash if linking with Speex later on. */ /* #define RANDOM_PREFIX your software name here */ #ifndef RANDOM_PREFIX #error "Please define RANDOM_PREFIX (above) to something specific to your project to prevent symbol name clashes" #endif #define CAT_PREFIX2(a,b) a ## b #define CAT_PREFIX(a,b) CAT_PREFIX2(a, b) #define speex_resampler_init CAT_PREFIX(RANDOM_PREFIX,_resampler_init) #define speex_resampler_init_frac CAT_PREFIX(RANDOM_PREFIX,_resampler_init_frac) #define speex_resampler_destroy CAT_PREFIX(RANDOM_PREFIX,_resampler_destroy) #define speex_resampler_process_float CAT_PREFIX(RANDOM_PREFIX,_resampler_process_float) #define speex_resampler_process_int CAT_PREFIX(RANDOM_PREFIX,_resampler_process_int) #define speex_resampler_process_interleaved_float CAT_PREFIX(RANDOM_PREFIX,_resampler_process_interleaved_float) #define speex_resampler_process_interleaved_int CAT_PREFIX(RANDOM_PREFIX,_resampler_process_interleaved_int) #define speex_resampler_set_rate CAT_PREFIX(RANDOM_PREFIX,_resampler_set_rate) #define speex_resampler_get_rate CAT_PREFIX(RANDOM_PREFIX,_resampler_get_rate) #define speex_resampler_set_rate_frac CAT_PREFIX(RANDOM_PREFIX,_resampler_set_rate_frac) #define speex_resampler_get_ratio CAT_PREFIX(RANDOM_PREFIX,_resampler_get_ratio) #define speex_resampler_set_quality CAT_PREFIX(RANDOM_PREFIX,_resampler_set_quality) #define speex_resampler_get_quality CAT_PREFIX(RANDOM_PREFIX,_resampler_get_quality) #define speex_resampler_set_input_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_set_input_stride) #define speex_resampler_get_input_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_get_input_stride) #define speex_resampler_set_output_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_set_output_stride) #define speex_resampler_get_output_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_get_output_stride) #define speex_resampler_get_input_latency CAT_PREFIX(RANDOM_PREFIX,_resampler_get_input_latency) #define speex_resampler_get_output_latency CAT_PREFIX(RANDOM_PREFIX,_resampler_get_output_latency) #define speex_resampler_skip_zeros CAT_PREFIX(RANDOM_PREFIX,_resampler_skip_zeros) #define speex_resampler_reset_mem CAT_PREFIX(RANDOM_PREFIX,_resampler_reset_mem) #define speex_resampler_strerror CAT_PREFIX(RANDOM_PREFIX,_resampler_strerror) #define spx_int16_t short #define spx_int32_t int #define spx_uint16_t unsigned short #define spx_uint32_t unsigned int #define speex_assert(cond) #else /* OUTSIDE_SPEEX */ #include "speexdsp_types.h" #endif /* OUTSIDE_SPEEX */ #ifdef __cplusplus extern "C" { #endif #define SPEEX_RESAMPLER_QUALITY_MAX 10 #define SPEEX_RESAMPLER_QUALITY_MIN 0 #define SPEEX_RESAMPLER_QUALITY_DEFAULT 4 #define SPEEX_RESAMPLER_QUALITY_VOIP 3 #define SPEEX_RESAMPLER_QUALITY_DESKTOP 5 enum { RESAMPLER_ERR_SUCCESS = 0, RESAMPLER_ERR_ALLOC_FAILED = 1, RESAMPLER_ERR_BAD_STATE = 2, RESAMPLER_ERR_INVALID_ARG = 3, RESAMPLER_ERR_PTR_OVERLAP = 4, RESAMPLER_ERR_OVERFLOW = 5, RESAMPLER_ERR_MAX_ERROR }; struct SpeexResamplerState_; typedef struct SpeexResamplerState_ SpeexResamplerState; /** Create a new resampler with integer input and output rates. * @param nb_channels Number of channels to be processed * @param in_rate Input sampling rate (integer number of Hz). * @param out_rate Output sampling rate (integer number of Hz). * @param quality Resampling quality between 0 and 10, where 0 has poor quality * and 10 has very high quality. * @return Newly created resampler state * @retval NULL Error: not enough memory */ SpeexResamplerState *speex_resampler_init(spx_uint32_t nb_channels, spx_uint32_t in_rate, spx_uint32_t out_rate, int quality, int *err); /** Create a new resampler with fractional input/output rates. The sampling * rate ratio is an arbitrary rational number with both the numerator and * denominator being 32-bit integers. * @param nb_channels Number of channels to be processed * @param ratio_num Numerator of the sampling rate ratio * @param ratio_den Denominator of the sampling rate ratio * @param in_rate Input sampling rate rounded to the nearest integer (in Hz). * @param out_rate Output sampling rate rounded to the nearest integer (in Hz). * @param quality Resampling quality between 0 and 10, where 0 has poor quality * and 10 has very high quality. * @return Newly created resampler state * @retval NULL Error: not enough memory */ SpeexResamplerState *speex_resampler_init_frac(spx_uint32_t nb_channels, spx_uint32_t ratio_num, spx_uint32_t ratio_den, spx_uint32_t in_rate, spx_uint32_t out_rate, int quality, int *err); /** Destroy a resampler state. * @param st Resampler state */ void speex_resampler_destroy(SpeexResamplerState *st); /** Resample a float array. The input and output buffers must *not* overlap. * @param st Resampler state * @param channel_index Index of the channel to process for the multi-channel * base (0 otherwise) * @param in Input buffer * @param in_len Number of input samples in the input buffer. Returns the * number of samples processed * @param out Output buffer * @param out_len Size of the output buffer. Returns the number of samples written */ int speex_resampler_process_float(SpeexResamplerState *st, spx_uint32_t channel_index, const float *in, spx_uint32_t *in_len, float *out, spx_uint32_t *out_len); /** Resample an int array. The input and output buffers must *not* overlap. * @param st Resampler state * @param channel_index Index of the channel to process for the multi-channel * base (0 otherwise) * @param in Input buffer * @param in_len Number of input samples in the input buffer. Returns the number * of samples processed * @param out Output buffer * @param out_len Size of the output buffer. Returns the number of samples written */ int speex_resampler_process_int(SpeexResamplerState *st, spx_uint32_t channel_index, const spx_int16_t *in, spx_uint32_t *in_len, spx_int16_t *out, spx_uint32_t *out_len); /** Resample an interleaved float array. The input and output buffers must *not* overlap. * @param st Resampler state * @param in Input buffer * @param in_len Number of input samples in the input buffer. Returns the number * of samples processed. This is all per-channel. * @param out Output buffer * @param out_len Size of the output buffer. Returns the number of samples written. * This is all per-channel. */ int speex_resampler_process_interleaved_float(SpeexResamplerState *st, const float *in, spx_uint32_t *in_len, float *out, spx_uint32_t *out_len); /** Resample an interleaved int array. The input and output buffers must *not* overlap. * @param st Resampler state * @param in Input buffer * @param in_len Number of input samples in the input buffer. Returns the number * of samples processed. This is all per-channel. * @param out Output buffer * @param out_len Size of the output buffer. Returns the number of samples written. * This is all per-channel. */ int speex_resampler_process_interleaved_int(SpeexResamplerState *st, const spx_int16_t *in, spx_uint32_t *in_len, spx_int16_t *out, spx_uint32_t *out_len); /** Set (change) the input/output sampling rates (integer value). * @param st Resampler state * @param in_rate Input sampling rate (integer number of Hz). * @param out_rate Output sampling rate (integer number of Hz). */ int speex_resampler_set_rate(SpeexResamplerState *st, spx_uint32_t in_rate, spx_uint32_t out_rate); /** Get the current input/output sampling rates (integer value). * @param st Resampler state * @param in_rate Input sampling rate (integer number of Hz) copied. * @param out_rate Output sampling rate (integer number of Hz) copied. */ void speex_resampler_get_rate(SpeexResamplerState *st, spx_uint32_t *in_rate, spx_uint32_t *out_rate); /** Set (change) the input/output sampling rates and resampling ratio * (fractional values in Hz supported). * @param st Resampler state * @param ratio_num Numerator of the sampling rate ratio * @param ratio_den Denominator of the sampling rate ratio * @param in_rate Input sampling rate rounded to the nearest integer (in Hz). * @param out_rate Output sampling rate rounded to the nearest integer (in Hz). */ int speex_resampler_set_rate_frac(SpeexResamplerState *st, spx_uint32_t ratio_num, spx_uint32_t ratio_den, spx_uint32_t in_rate, spx_uint32_t out_rate); /** Get the current resampling ratio. This will be reduced to the least * common denominator. * @param st Resampler state * @param ratio_num Numerator of the sampling rate ratio copied * @param ratio_den Denominator of the sampling rate ratio copied */ void speex_resampler_get_ratio(SpeexResamplerState *st, spx_uint32_t *ratio_num, spx_uint32_t *ratio_den); /** Set (change) the conversion quality. * @param st Resampler state * @param quality Resampling quality between 0 and 10, where 0 has poor * quality and 10 has very high quality. */ int speex_resampler_set_quality(SpeexResamplerState *st, int quality); /** Get the conversion quality. * @param st Resampler state * @param quality Resampling quality between 0 and 10, where 0 has poor * quality and 10 has very high quality. */ void speex_resampler_get_quality(SpeexResamplerState *st, int *quality); /** Set (change) the input stride. * @param st Resampler state * @param stride Input stride */ void speex_resampler_set_input_stride(SpeexResamplerState *st, spx_uint32_t stride); /** Get the input stride. * @param st Resampler state * @param stride Input stride copied */ void speex_resampler_get_input_stride(SpeexResamplerState *st, spx_uint32_t *stride); /** Set (change) the output stride. * @param st Resampler state * @param stride Output stride */ void speex_resampler_set_output_stride(SpeexResamplerState *st, spx_uint32_t stride); /** Get the output stride. * @param st Resampler state copied * @param stride Output stride */ void speex_resampler_get_output_stride(SpeexResamplerState *st, spx_uint32_t *stride); /** Get the latency introduced by the resampler measured in input samples. * @param st Resampler state */ int speex_resampler_get_input_latency(SpeexResamplerState *st); /** Get the latency introduced by the resampler measured in output samples. * @param st Resampler state */ int speex_resampler_get_output_latency(SpeexResamplerState *st); /** Make sure that the first samples to go out of the resamplers don't have * leading zeros. This is only useful before starting to use a newly created * resampler. It is recommended to use that when resampling an audio file, as * it will generate a file with the same length. For real-time processing, * it is probably easier not to use this call (so that the output duration * is the same for the first frame). * @param st Resampler state */ int speex_resampler_skip_zeros(SpeexResamplerState *st); /** Reset a resampler so a new (unrelated) stream can be processed. * @param st Resampler state */ int speex_resampler_reset_mem(SpeexResamplerState *st); /** Returns the English meaning for an error code * @param err Error code * @return English string */ const char *speex_resampler_strerror(int err); #ifdef __cplusplus } #endif #endif wfview-1.2d/resources/000077500000000000000000000000001415164626400150525ustar00rootroot00000000000000wfview-1.2d/resources/Info.plist000066400000000000000000000016571415164626400170330ustar00rootroot00000000000000 CFBundleExecutable wfview CFBundleIconFile wfview.icns CFBundleIdentifier org.wfview.wfview CFBundlePackageType APPL CFBundleSignature ???? LSMinimumSystemVersion 10.13 NOTE Open Source interface for Icom transceivers NSPrincipalClass NSApplication NSSupportsAutomaticGraphicsSwitching NSMicrophoneUsageDescription Microphone required for TX audio NSDownloadsFolderUsageDescription Storing temporary files wfview-1.2d/resources/flrig/000077500000000000000000000000001415164626400161555ustar00rootroot00000000000000wfview-1.2d/resources/flrig/IC-7300.prefs000066400000000000000000000050671415164626400201100ustar00rootroot00000000000000; FLTK preferences file format 1.0 ; vendor: w1hkj.com ; application: IC-7300 ; This file goes in ~/.flrig/ ; It is recommended that you make a backup of this file as flrig can ; change the file and at times invalidate the serial port entry. ; You can also chmod 444 the file to prevent accidental changes. [.] version:1.3.26 mainx:968 mainy:1155 mainw:735 mainh:150 uisize:0 ;xcvr_serial_port:/dev/pts/1 xcvr_serial_port:/tmp/rig comm_baudrate:9 comm_stopbits:1 comm_retries:1 comm_wait:5 comm_timeout:5 serloop_timing:200 byte_interval:0 comm_echo:1 ptt_via_cat:1 ptt_via_rts:0 ptt_via_dtr:0 rts_cts_flow:0 rts_plus:0 dtr_plus:0 civadr:148 usbaudio:1 aux_serial_port:NONE aux_rts:0 aux_dtr:0 sep_serial_port:NONE sep_rtsptt:0 sep_dtrptt:0 sep_rtsplus:0 set_dtrplus:0 poll_smeter:3 poll_frequency:1 poll_mode:1 poll_bandwidth:0 poll_volume:0 poll_auto_notch:0 poll_notch:0 poll_ifshift:0 poll_power_control:0 poll_pre_att:0 poll_micgain:0 poll_squelch:0 poll_rfgain:0 poll_pout:3 poll_swr:1 poll_alc:1 poll_split:0 poll_noise:0 poll_nr:0 poll_all:4 ;bw_A:34 ;mode_A:0 ;freq_A:7070000 ;bw_B:34 ;mode_B:13 ;freq_B:7070000 use_rig_data:1 restore_rig_data:0 bool_spkr_on:1 int_volume:0 dbl_power:90 int_mic:0 bool_notch:0 int_notch:0 bool_shift:0 int_shift:0 rfgain:88 squelch:10 schema:0 rx_avg:5 rx_peak:5 pwr_avg:5 pwr_peak:5 pwr_scale:4 ft950_rg_reverse:0 line_out:0 data_port:0 vox_on_dataport:1 agc_level:1 cw_wpm:18 cw_weight:3 cw_vol:0 cw_spot:0 spot_onoff:0 cw_spot_tone:700 cw_qsk:15 enable_keyer:0 vox_onoff:0 vox_gain:10 vox_anti:10 vox_hang:100 compression:0 compON:0 noise_reduction:0 noise_red_val:0 nb_level:0 bool_noise:0 int_preamp:0 int_att:0 vfo_adj:0 bfo_freq:600 rit_freq:0 xit_freq:0 bpf_center:1500 use_bpf_center:1 fg_red:0 fg_green:0 fg_blue:0 bg_red:232 bg_green:255 bg_blue:232 smeter_red:0 smeter_green:180 smeter_blue:0 power_red:180 power_green:0 power_blue:0 swr_red:148 swr_green:0 swr_blue:148 peak_red:255 peak_green:0 peak_blue:0 fg_sys_red:0 fg_sys_green:0 fg_sys_blue:0 bg_sys_red:192 bg_sys_green:192 bg_sys_blue:192 bg2_sys_red:255 bg2_sys_green:255 bg2_sys_blue:255 slider_red:232 slider_green:255 slider_blue:232 slider_btn_red:0 slider_btn_green:0 slider_btn_blue:128 lighted_btn_red:255 lighted_btn_green:255 lighted_btn_blue:0 fontnbr:4 tooltips:0 ui_scheme:gtk+ server_port:7362 server_addr:127.0.0.1 tcpip_port:4001 tcpip_addr:127.0.0.1 tcpip_ping_delay:50 tcpip_tcpip_reconnect_after:10 tcpip_drops_allowed:10 use_tcpip:0 xcvr_auto_on:0 xcvr_auto_off:0 external_tuner:0 fldigi_is_server:0 hrd_buttons:1 bw_A:34 mode_A:0 freq_A:7070000 bw_B:34 mode_B:1 freq_B:7070000 wfview-1.2d/resources/flrig/flrig.prefs000066400000000000000000000001441415164626400203200ustar00rootroot00000000000000; FLTK preferences file format 1.0 ; vendor: w1hkj.com ; application: flrig [.] xcvr_name:IC-7300 wfview-1.2d/resources/install.sh000066400000000000000000000016121415164626400170540ustar00rootroot00000000000000#!/bin/bash echo "This script copies the following items into your system:" echo "" echo "icon: wfview.png to /usr/share/pixmaps/" echo "wfview application to /usr/local/bin/" echo "wfview.desktop to /usr/share/applications/" echo "qdarkstyle stylesheet to /usr/share/wfview/stylesheets" echo "" echo "This script MUST be run from the build directory. Do not run it from the source directory!" echo "" if ! [ $(id -u) = 0 ]; then echo "This script must be run as root." echo "example: sudo $0" exit 1 fi read -p "Do you wish to continue? (Y/N): " -n 1 -r if [[ ! $REPLY =~ ^[Yy]$ ]] then exit 1 fi # Now the actual install: echo "" echo "Copying files now." echo "" cp wfview /usr/local/bin/wfview cp wfview.desktop /usr/share/applications/ cp wfview.png /usr/share/pixmaps/ mkdir -p /usr/share/wfview/stylesheets cp -r qdarkstyle /usr/share/wfview/stylesheets/ echo "" echo "Done!" wfview-1.2d/resources/resources.qrc000066400000000000000000000001311415164626400175660ustar00rootroot00000000000000 wfview.png wfview-1.2d/resources/wfview.desktop000066400000000000000000000003051415164626400177520ustar00rootroot00000000000000[Desktop Entry] Name=wfview GenericName=Radio Controller Comment=Control and visualize data from amateur radios Exec=wfview Icon=wfview Terminal=false Type=Application Categories=Network;HamRadio; wfview-1.2d/resources/wfview.entitlements000066400000000000000000000012221415164626400210130ustar00rootroot00000000000000 com.apple.security.app-sandbox com.apple.security.device.audio-input com.apple.security.device.bluetooth com.apple.security.device.serial com.apple.security.files.user-selected.read-write com.apple.security.files.downloads.read-write com.apple.security.network.client com.apple.security.network.server wfview-1.2d/resources/wfview.icns000066400000000000000000037510501415164626400172520ustar00rootroot00000000000000icns(ic12PNG  IHDR@@iqsRGBDeXIfMM*i@@FQBIDATxYluǿ%R+Q4۲e;VYAдH>(җ>\y(6@45HM5c[,[eY )j("i.>G ܙ9ߙ£GWgeU>y'O, +a#rod^ atb1ʵx`/pE7stpQ/ { hqށW8witf ;+&|$ 0pkC'hp!&8p4pE251=l;y>u7$C=.r!s94 h: Mp[{hH)" 6@7Χν,|'@'nĉ CSP{EP˓|nWr  9 Հ\c '84~Ն2}jD}|łގkM`c؝6p)k5p1ne0r${^8Ke _0R]L bt0~Y/G oym0MP߀xk9 4Qmzu8 $_*>_!l ǐ=ϰGv= ZH8Δ7U# Bcg+C? V0L(4^s˘G>WA? +`>Zm}5̈<0pg=Fo3F'_ C+1?Ӊ1O7[C߷ׯXjv6, ,a3u\D~݉w綮Nfk/x@wp^Z\_˪U*̄(]}&I[y`s}e75u8暀fTц iǠۂ@ᥭn/y3ʢp}eLx Csja]y>é*ּՓVF7admλ[bg&;DQ_T08PÜǺW|zlY,TWĉr,~4Ȋ6p2J;bci 0)GW9Yz!5VrC!Ƨh)1_Q[9OncS<!:e_b+S+b'Pul|;\L[87Mv>&c! } VE'/kTaOMK<AB:[ $1]o~iYrZ =Y[8"6qηc,hE3KPEh3嗵0{|q`Ơd<9!Q蒻X0H!/o4 D1;|Dʙ(fufEdUq8 ȚRiYiCzͪpW3zg9)2Ypu!83ioeڒ=}q}>!4~j u YL|K WF;O!ok,1} 60-Z@:>O:@?дI_>qb0-3DlAIǘ6Z9s2؆,,XϹnwEi[̍ ~NДjp)sYzrkf'N' 鋞ůՈ=z g,U&cUsaqwsg4` :۠q; 4Y#Kҟq4H]Kkڿzj۔8$qn583u\A[`wxX͛me'"3Tg>vU-|( "{=>w;<2.2ӤW~>i2CwgUxՊq4 ,<3TP +t}+R"k{s+Ù!Ԧb^~gw@E@u]!+. Dߋװ17S`# M/"5(u9Wt\|SFc<\B5؉?C" .'!,JRm3yڸ[zPiOz얾X5(gtgf4z~ 1\0!&TVCk)گ!opnE1PAXeK4y7_qc\]؏.=jy9e5OY"lgxK861L{cw&+YH9bRIy@ 7 bҗы&m^/`5GC´Yf0ފqWX"m ]Ww;*RLʲt㳑&ڼ VVVZ[?2zוJʒf3G&L4"jԜ| ztq3#8]:S @JOM-}6UQM1f\+| (&%@4P =w7Dъ_- 8Hn6-`X6UIs0QJVN#] 3V 8KC4<}uQǙih?y}j^jZ|gU#툢)'l^7 ؖ:{},?UC K'1В]m~3\%Es #[۳s眔zA2j gh,9|=M11ڟw7cQE4 f>~@]9y6bC6;gF; G[vKioY LhxM4f(̗dؠ^-9ړ}dAc܍@6U\$ Z;SpY(cF+b,+'t67IQ)C1w.c2s%\>$ԂJ@v0#Z .tq9#IPDg"%gRz\f۽#c@-$NLZf$y)-Mm=ݥ8y|6%}M 0 4J d}Nl>VgiQ:Ɠ9򀥲LOwupvOxjr vv'Zmygw'd f-1Fqc7kJ54GQ+ȸFd|?nO:9n |u♐Xwmg]kucwŰ+bBC-5lbTK kB#j+Y(~?(pfN赴^(|#1xnmr2Y PD}Cnw鿥]~kaűA-n}`H<IH  Moh(/?Ǒsxm'\E=K.V2F wB1&æ#a{:K6)Օ$Q6;}9I:)Fl`sx>{@obM`n5I;JO]A:o.xEy=aFiF`!JEsdwa k0.æk$rgigZXesw@h,= VCЂ,\ߴ`AՎQ)(l^u`T6}?#BFҵ;B|b]NKa- z {(08A ^s]`i" ,|G Mt;z ?}[72xnt~ GK_ÊS_ `ΒBLZ^g*§ˎSh-.$xvi/!?z+O0}k ]anD=ĸ.e=u1V`XV$y~iƧ>Šqqz~Ä?6v$N6٘bg v`i6i.UfSlE\17Fg.T򗅢oV8 If?RؓK!oqPK6N41z JT3[`SP"=bjݯؠ߻)tvG[z _[li? 52qMi_ bf{b,%>ʕlᐜ.^rqS5fjzF/sH_4 ,jF.dPJ])r -/>-ymV*fY< G/n@4w_zꗚ#q3Tqe=5mf yu[_B&l"G\X6>i#%ifh~!N/_O> )gGasRGBDeXIfMM*iHw@IDATxy]}'эFcW؈UȤD#ilGGJ\q2ĩI*I*TT⚪LqR,S#mْ)YD7;/=xBt~~~ܪ-(F10Q b`(F10Q b`(F10Q F9jV_w_˕[?랙^^k\h~[?6V}Uw G }:Umi~x5J_>X:4o*HquC9sy߆RC5Vpd}é`g |:gJK2T-l!5(:2XBm@hՂ{Ưw}F#Tz+UǪ7WDw9T'mܑ_rځX3M9tT᳁RPհ#Qݝa+ss< hNlM{~q[-CuÍ PR[ߐ{3 r3Q7R\?$vZCcUlRk\ _:3O3؜|FDKJ~t FݷmDܮ믤c95 S#4M_! fgsTVU-лꞐX}&Rɖz\,ߞsÓ{ٜsRqclB|:gs/y> jk;rIS6\0V\g}˝{o?_s/ QmRz<$Sc {wz+=gsebEZ⿑V9.-P~S5Kj]΍52.u6ُuD½t@# ': 罱SFJgaܮ<8Ϟ\Gr'RΈv*Ac*:ب9/&1iEyL͕s `mzl*UwdAxl8elXo(M ̨q67y +~ KJx>q|[(]sLlsz:3=Zif@[RS0Q]뼀 jDl6PԳiT>!@?t/L%EUy/ :.77ݶۣ3{jD-NsO<5}lSsO_hEcoqDŽZzHeF-6s-U`g5n1d3Uz?ssߢ\?9 @3&i+|b N ~E|NHEB8:#h"6? ʉ_2}6p aҁO=y9sm3lC}Ē䙏ȵj U݌YV#,7Ͳ&}ggr=¸/se#ؔC8cbsq?/. {)َ?Cxz?=r>cw<_ B詬QdЁ ĖnlԿ;"z&W]A+9׺i>Q侾™(p8&q'a2jji:}gz 5? l'mvE ׳A\w#3#q(Aiv\ =2 ڃ>)c\s?9kEU_oe0|[zNiT_m ;&=]~BCᕃsUw#;z?ɽT?_Ss^!ƈÁXr8PNす73wuϬ/oN aH}_U[1O2F[3Y!فQzSmߕqL_Ϥj_`$<7>-3veftkAœTד]uWK? PyE!S7uq!8^3OרE\>mtl~ш1! GO suB#ȹ]:8;;rϬHcajvd0S{b0CU3Q_ίɕ `o4},~ 2n?1u2A R A?X TU5\ BM'?wW!">9qy\dNgOTGVo&xuoϿ!9Q\kgPg~?,ߗh'9s]~H@Rٜ3m6 ;?NhN7 Šz&Hx#MHCyh~J 79s:yϦpVAU]9'Jh]rC|11)~a0_ jε)5v-@M-u7wަU!̻3=jLz$s,Vնay9XHXwyT\{#ww^s[U}_|Y'D}#-hJ+Ai!st%\ Cܑ~hܗ;KuC"ߊ@\q)zFx}wO/E?Mo=;Iw>[T#שXki2 |{(#p4qxYwHw1J𸱡>RQ81@rxfْ/@(nj4XVUx-,?M8d,'>#́_\blJM7D_48!'Za|[=le%0xސүzf(p *Z?լܗ1쨉ǹ vCD}!BДFL| 2!:wؔPdܽ8Eyvr#r `o*RU#ki1$k! ՠ8"׃.L(DS|Kɰc?ȯ߈2xӓ;3<(8{qM`[5$|#~/5T_Z0zN4=SK}"$ʆYHص8μ\%HJ{3^Tf8lKHvCb+eܺ2z< wV.ޟE Jo7po 'Е{^soelJ) ޠPcbɚڭ댚#WŁ& Om[!郰I9#zG;[Uv+*o>>ҙ]H\ 1q"Ԟ7eН `_%* q\3"'D}̠2Bs|Z'̋/>u֟sې/srgW~#ƕ%X{}+R_[)՚?LoVC|~x7:+p%hČח9Y\z|?<']כҠx/&̼fpESA:<6RvEHʝ߂o3"11[FO I :Vd|RWH'L >v|(=(M ,f"rk? _?=^eE@sFx3@* N.-f̶#\1ѐU9I&2X,ʌ;W-\!dY? 4Q"d1#Q37P~;=vX7zn Tbpa7A`66 'py9"EfߕE\*K ;+NDXT:-=K$$Mپ p 1S{eAP26OQ(|秧]5MiJ܂ؕ{K J![shE=N̎[[Pf0l H+/-RjE`婙q{?dbL= d ??ėK.GoY+CBmME+뚑''vPp1"$6l &p"m?O=d/M/ òaKݦID:I";3$=k:W[ffpχ`WB ޕ3N}*Z Ɠm;וyn:!PR&ov` / T1H] ڽELwE|00r3^ &:tvR)_p Gs7"hI_?ʩLD4IԠOH]VA,1 k[o߉r\7մMR`6wl!>cV(k9/.!C}Ktߗ>hn ~_%c}Z9Г"6LX3DV1ňqOeB36m6~p!ƭ CY%,i*ivs_  p2_˛+^ 5B}TR+95h$p|td;sOeHa`|zq56G3Lþq1.ԍMf66V%L˭20YXGnȹ*7;MCC*o5}9mY5yeKDgGӃJFŧ2)I63* wtMN)d/'L c{9w\D&0;Ǯf5ip T=h\" /%ۇ66hiRGyjB pP})=enJ_L*U`$̾ ooO(bĭʸ"D+r`䕎G2 -`$̅]'%D]/DI {Sqo=(T1B6Z2@!O` NfcEDqLБ8QG$RgmF5r_ ٌOFʙǰmM/ʧ>S'߭)E̬wCTN uJי)$w!:y]2r5v EYUrCO 0;rPҥ("M!<J< рᔱ1#lIPQd lsS#^H\ߌ |jė{:b_ݦP5G!!O=Lh[|)D" %'5n`Bܭe'`H-:^YSUeph4ɓ^o"36wb|OoI1S˂*gco X<CryP82`J K !y#DI+vޫ:#b!;Rߟ*Zblnsܭ]Iզ)c#iWx'ⳑKܺ Fo1j]BMcbgm.6$&d }ir_Tz__I3 !-? d3Mzzl1 H;OcPY<5Ip PH0F¢Ձ9˛uW!sVmd{xgC1meHe[k8͌)؝NxIT |,NnK;~Hj:gެ9 `S^YWzWj YKL(g=-ϲ :Ւ89ܜ\*1<7zJY,٩BSAiJ,LA!fP4ny9ø(d"* `~KgI+Cd&4b|'Rh>=ę:n @aiAm3wEd[j jh~71UI2uWB bǖ6W*Di_3@?X=z "g;3Tϴ\I!ӧ7/ٺZM Z-KMc"H9|UT=N nQNffH)MD ƑGZ{raQ,8B\r T1-!Y| hI@*VP^zj$ RD/M.k~߆z_X P{/bl5uf'HE|8X]5&(|ּMx4s!AnXfb.7uR6@…<Dbȱf`ٿZ\Ɓ_?V[E4lF0l qdJbw ,K __4-֜0iWP&#FSiIŝ sl`@l:޾*Jxa$~ (L!f1˪kc DL)03\Yt^^$  5^ɚT5)k'tE2Fe~ ʝrY.QL-$LsYg$6=%Vͭņ΍$׏0D y ̗fEs6V=WEN֎<3> _l6̀.Ƭs' n ǮђeK:ì绩7i#H4@<+b Ig gd 16 r6I5o*Q[/CFݠÈS8jIw\חE%Qt|feDE֤Dw1 p$cIFT  Χs|UuX\Z?LWHk7/ 1?R b |>;MAgFЫDO忀۩2@o/cL h6_X8p vHA-qXӲn[Le}S}1XН ?H.0(O\RYđBZOg">K`x'OVn1P4{$ ~:өYN@bIG|fHXoaڰM.678NF S"63x3Ed,O5eB{X+޷#iHzA'3<k"/o2]7+VM?c}Gitq# Z6PmpO*0NPۯŋ7J9d|1 A++k%O ϮHV<7T=ynp!Z)lfړn0s푠ʤ 3i\65VnQ/Ie|r}ş l}A/Lɝg^&Yyڪ{ذ'edo̧vLbL@I>c&l5iO7Hx͟ 4JԪ!~:t^$5hd 8xmK%\q28N3&msp<eU4dlIc_oCjs؛@"(N`vAA8 v 3=$l6j++}7&y0yD`H_Z"#11@AHMH9z]NzZXA,K@[P$F)fӳ` ZpX=wlF5up8s4p"@3L$lF[rJc7tknJm{,[0  .(5d!*fQ7hbUa⸢$9-ϯj;S+SP'P"?Q\}ANV"7>uwj&M!XΡZ@FtdpV4"ƧtF *)t_ζ\J-H!^ޱgb[O[(P Adl MFTS4!Cҕ薓Aierd_cL^*O\bo{sG$e+wQV %tm{No4UxW{e+ DQxj{Zf>>0ƐˠoBߐohI{E/v N |Iy-Ϥ6E[[K 8l`WE!ڪc~sdsq%?qo! WK=ŝ%H 1l\+C*aIᒝ{^%" "a $ݏxv9a]@%0%N RZ@_BϢCOȿK1!ԏ},S푨gjNyxCpʮQH]9Zbt5g O7t߾#| Mdܭ#Ľbg0ZNʮ 9tDT6(?JgAdd7j5=*0BY D rZ FPfZbj`lRDuF |HPXݖMG*fd? ""ԐH3與 6#:>nJ8~: |_ ~@~[Ն3Q(7uo ,7euYk.ل:f|"qO`aȘ1P1gIJڱb7;D]*D턅k0hd+PXd}. ߧS+ˮ*(N|>K[!#@ Lav.}7YnP1X D@3g3EPr_dTy{ VT#fsI+]'+*F:_NBe\6=Fw. z$@\?ѻˍ\~ixE^#S|ǥW_b\E8etF&q?^ tƧ "I9ƧM8a`c5؂nj"Iခ(S] wTi M<8 A{_زPA 5&Sɴݫ@;aj˙R2bz#+%H Y&kl&x$^jj#=d"(AXlإ\:dߛӯE5(Ҽ)WT"[2&G*rzᮝ7&5 ^ -V(v~H0 [j%4:rm+neɖ8Sꨗߍȟz2R)T69cIHw;-nH2@T{LK+eq18 1:/GG06NgN IJJfO'nx+6-ynޞ4i@{r}Pa1pi$@Yl]k۶z9/5jpsYUϳ>')x{)v9!8Ǜ: RlQu7oVފˋJ ]>B8y8@d,"Ivjnww=(Ta)n}ͻEW\kdfCA2P$@V>)!@0srcay٨@Ӂw\ է?YA0ur}2 X>VO I 7-<>!B%"Vt#o& X"ڟ}s՟ToP6. en^-&(S%ЂHzSM`;kxƷr̊5aHpp@Qdo~:sUW'͖ެVgl #Bȕos GEī}Qke>[!(Jd.T 2]bgœ2yBQ4@tͫ_~#h"JW Blޑu98dO '?r.M]Vꗹ Jj!I9Ȅ6_Eί{C@;ҿum'ÍDQ4zo|r.F6(UeŐ@\ P:t0,+4_ ;ٿp4Xnl_!]>;I],$a qV*ƓPL9LB`+!E5~ u%8ߦ26(b\N H-o1↴\(ghy8ɻAXAx8s*D~t[R:Tl|9`VZd7Z 2-L\v Ƌ;hNCbe*Uz\eC1qx@BJN!i˲ΡsEZl3/ȼ{7pppP3Ct$vn,.O|ƬQ _$&<#D-Pns:CcoˊMZEv.qiY[21A1pܯ.zž!ÒK0r Ή j@j)oo 2|@?n12O񿞄MB#D'1YHNjx)M0bY{!Z\0'%Sܠ䇚ҕW^g@#Oe0OePҾ,"#`2~K&$ۇ(w0\VJ=w$>`E"&mݓ!5)`emeªUM~⭡CD"h0 Y'\Aoв^Go5( x%P<*ᔰŧ}lAܒeq}w-b \oѪc,c]P~;HǃAx:#@L 55<2 IDAT|od}܋E k(ˠdXއ<0 6fjg/Ñk-mM.o|~w\۞ 7[Hm>%b!~kf%2Vu[ZF8bwt_rDGAxH!.q H@F$VƮ*$@J#y}j4iѴt"lmB$K^pʚ"yI̥bGe'XAg;:Lp>{D>.uedZ/ĕ8l%DjQH{"=¶1_˕$aRu H(vX_]){V ݮ}BHvIqu˥+!@.'sJD#Bԅ 8"IW*^Y?9#EC[A:@ʉ2ML'\<a%7!}RI}#Y9bֳf -&>ŸBv33@왢E @ !iI" Y%Bw&xaY`oKBf[F);%cA /A!ul.nVsUδ ,/")JRٵmoOr翉yQT wx 8e<t.5ْ|+"1 mZW0G4'Iv'ZK ~P\ViOGg#-K\fPpe+*gn@zg*B߉=p1'Gk9Ռ=|F ;5TT߿HGܼ|\HYI ˷]pt \EE,uiwk֖q'b^` d`,hm@-,v}a=ҋȞ!1` ?WI& =;F#G'?lI5:zITτO  `Mu  b"hD!Oj<tY~8뎞[[0 `ǂ2 J$P!`ӼqKbvLtտS܊ĪKm+#wp?G@IY2A4(axlȌw"p A] 1~JETX+;cɌ h2%4fCTcMy# CJ>:ulpSl͊ Ơ47;,H'[o%kMm,JW/9#!E 'cIFg ;.I7Ud 2Krq]χ5-z،یa?ff#,qz~?h'*]K_'-ȕg8XQLjM,0^gEN,!kۡe[A[@he{_,b%1 Y TݲYr;iwl[\b+(8|4!OD;m[c__& ZH.1b0ȸUs *S5 iA cщd A-`EVO#  brqh~8ZD(:A>;=鑖}bhs0'x!k}-GLZmPPX[!J }?heS#{efPd޻K5/f$WL'Ŵ?dD"jjz&ܘ7F#OKwNl}D#a:yeO2aB"M-D/Oى<{ɑ:&+7eoo1]gti<S8oc$V4r(B/NQy{jg) v/ڈtn0i mfdVtD-Ʊ_Ϲ2%G#!Is2Kӧn۳\G!\#dղ{+OԠ1߈jzW'rxea?p+s!{ChR꾗Wd-(F10Q b`(F10Q b`(F10Q b?@ wQvIENDB`ic13㓉PNG  IHDR\rfsRGBDeXIfMM*igI@IDATxW]G'}=:4EtO{'}=t[(BzЫ4RL(dB=Ѯr <k=Xds>{ΝZ2w C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C|zn;ۍ{n7q0ؗ+Y7u#l[~n7S!/t32;p8hz0P9@S#om;#݆59#5zw?zWgsϷގ՟{4ݧg^[MtkCk?/pԹ{s˺+ᑛz_{c@];7xܮ;kv݊/V&Htƻݪ{]|E G#ѻ7L=8܃ N?2Mv#JPnX;/%/qЧ K)I}߯O lJ<1Z] / ꫃尥{xB7oރ7cTE ~[| |:iow;?~z ּ.~'2^ n,<]K^@'<% ܺ&B|A>l9{=酭[^t7s/h[}?yL*7 Th7:+{ fQ40óǧ>=Cn/zdp+H;6<-H4s;0_yw|/~'fۘ+/q$]?Ȁe!0 </d3^b#  rԐcp![_~*WqQc@YO2׭y.WL^NSF<GNu[ igU~eJj~ۣ=|Oti< xX+`[򹙇?ș&\$xwu`C#aFh\9W\{שu>-|鴵eKsd⛋}0%Hٳǹ|.LÝJ>bꜸ@r܊tr]>'b,=2A O2 Z ILnۺۜaLY+8m .s[?󷃃9f(8% GB8Lc^+uYjŴ"=+[c-,Eciv]O0-d=; |_DRrf|L/tpe%fSi6\juݱZ=7E$HoWdAXrL[lM灗sH2xuoPx3uJRFX/U8?NX_RG{|oI$OW~.`,Տ 6+0Oy0e4R )ɴBpWBlb^Yn}њ_1X圃WA&/s9~/ mߟ+t(4ֺ#%fu7#>'zL3X$"`#GKU`~ Q=k;W&!!)\10̇Ye,}JIsf6/fќXdk"hfT_ z} 2Vƺcᇛ+_2n1^=\ ϯ FCgt9T/̦F3>7 V|3E{ Oz`vֈJ#J!03_s/ ShgsIƟQ`g|$NϤ _86P* y*o! +xVڼ?q18$fGCCHdq !H% TTtz[⪹gEB)I "ȴW zY+th'IdkA`؛sE/c560'`Lb" ]~K'( j! |cQt,fLw1ڣ{g5+t3P+ c!\+(8~4laAD>+LUyBf|{o0{?ALF@yt}eQ9MM'2?-hhprw<~J9­ uМ܂#4?/2Y[ӁAB<Ԅ=3W愰_5:{~#y)p<Pj,[Vٖ\ї7˽,fx6!Z~Q-mBq=,=E%tnΗ#@8S8T:j.˭)E\ҋυM ѿ9\@V\'NF u"-+do^!] +S6ǵs+f f\H!SyLE?{A2lDMWpjgᄲ`Xg)%?-Rx;Vy.K}Sn+j27w._#P`rX>eUpepw俽0(7 V"7Aٞpk%f1>|^h}J|WҎyV,eqnT/wiKNStL~: (JTz1ӛ9I0x"gd/y/+My,p-&27rfddVږ\1\fa2B`:D23ƒۑJx)&;L"Bv$x°fߺXUOBp ]@Ն!b6Wsʪ%\7Ka4BÑĄXvs& G)҇MopuU#HdtK+̥ӹRQw#FuRU31oK 鋹O6ٟRsegɗ_0"(z;by4L3%'D{[M ߞJbIU'MUJ>ʖQˁ;ʓ+.U.X!z(n g0({6Ȇ-ml.L`{&T|0AwKF}r|>ػ +oR /U`Z=\_g~ zs[P;|/m&v_}"RdS[*9 Hrږ-f 6dz뚊`s8BjLm_ y5Vd`ё(]`|*F- Oԑc ~ odW@\C 1#%.1E-"b-ʼn)rrS׃1<\eurFC`9Zɽõшl833x]T!-< dȪ6a̎Rz%wFPX+jNuC4/j*Z7 |Nx0Wv?h%$!9/FbUԼ(|0!i~#*VZq{y|!rmޟiX-<}$A@eYMn̯K?Ip&'Cg \H!5jsɜ>#n=Chqa|Һ_齜~&8$^~r foK oZ`eĆ`=cY@y,/8.sƃwqŵ)ۛ9t8_VX3\uu& &TЅs#jHwa :㼚:cAq"!9?0Q&K=a+*@Jm [7c֍gDdD2߆ V`<="DPC)/x~KKn&M^=/ƕN\_ߟ-3V IYV|R_Y(|*Ո1źT[u˹ EI'L ߏӺd1n o(>ʒ`F\g7/}|ΚʠtWFt7OP.ǔ?_24zdBC6,=A*vڞ$C'péc+ЂQ! 8wݬ=w·8NsC4 <Ií1+(') 0Ԛ D=urG&^O;0RJkS>A`7=&'ڙO{`*x56؀@t6"420-7AP?1 *wpn a^ZVFOhÝXө<\ʳ,SuGuCp)`󇃗;=wH͋}U6< M0/P|EK6#6bv ='%$N%ƾ%!7,IW#ib`0]2S|zyDBI$MNE24P4#F((x^  H@=6z3QA,o%#!.UFLXVqcL A#q nJ0ʟݐ^"cM"gȭONW5PI vqj dLg7:yYL ?X^2-Y᾽ uzY7^ZaYhy?6.FpXx85e2;Nc4"%G=e1J ͎I_78S(ݭ9w%}_ LaOG! 8{Xw<*MD{.6_cfC5f .Ftl"͜7X`$ @l#R[Br.denw> 5@dD,c9ؘ4pži9-#"з&wRз4½R| -^1fRԾnb.=&:`QYux.K>)YBr+C&5`ɝE^G!KND$C8֊`d܅ Ɯ!Q`tt!h==WG_M}۞x;D&rb}*bt]xʈi E$2- !_p!m+ٷ; ԡYŸ^ n\?uƉ0c`FBnяҜt ; 'r,ԇx>Sb*7ᑮeӊa{V wU_YvuHMieznں=} k҆|hcrE5S0&XYeN!=:.a~×8x`"L 7T B^^f03JbG&LwB vF=X{ *&~G(JFˠNXbLZ-ʼѨ4ȦX &@XE4{` D`ܖOl+V$?SbihRz=jJe#,ے,"d4eЬ`B]ނܸqaqܙ|+؊g- uᴘr'İJcfaO͞ a3fAzvYfs.ZZ{؀>I|* t0-uɁ 0D"ӏoq0="x t1cHگYJ~A.H) }7?,ݗ_RaE'vW|>Pj{1.qg'{!-aꏠ­KD{2`pn:fgFYBX4 OLS)ltSp5*jb2*:/uµ3ri9B^~pªh\jkj@ԑtcP!tQtt.x#drzIJt֢赺HMS Qn `%.eULWx`IdWLD []*ZL?3mkhjppLH3n y/y"(^ߺ ;cɝIg(=S{Top殙#%jKPHׅO;P}m(j~8GȺ\4BD{~_o1b*-cojwY(4/kaYI+ BEhV"*Aa7B/S)%DR7)WSNEpj ."i{짥*ߚT[Q `hs>$"eJ["D!MS(q%, 5գ ±@rh> M(hPk^ɯ= H,T]|:'gOiIşrS!%_k,2f̮% )z23җiD⡻qϵ\X1x8Ic?@I4K5xKEL`+Ut&ne[=ϲWQ4C(/N>_O_0+~SAr?( @M rQTT)Z1ZapOPGiZ2Zi_h`_)crJLp-H{`f!e3s ˏoq o4XEvf6Sm- 3I:4R|uC#C$Vy O0iFsHmtt0ԭ$vuJ֡nޜ\'\fp? vG"+:ijR,.0 .*ZI}3Os+UU=^QJT8vXb$QDMvJFکdnnە]Ͼ?O?dFi%)K~-3/;r b!Vڝp+ 8XbD boyxgcb,"0ܩ0~oA+҉uI!X橔QZi ]$'9 1mҿ5g2mn3W2`ߥ =cr@ߘs3J;%p@PhdZײՖi3"aJ #23-N:3w;Am큓D+ ,p@t 4,jtOJ )Su)Ӈ?|8pg!kg3rDPYq ,-̦Gc;%1VH> y Gς01%@P'IVd6hŅ0ع]U<'aUXgkF|)`ƒF':fU͆qFŸW9b6%,$u+H%u붕"liG,Og9E5#ڃyT77Yߧ݆)(oF 0 b*W,|0["#$Ϧ>589)cr9 X/>]*uQ!RdzO]l]GHzuD=VF Be9!⫹x>0`$E 3OT~Psl5 GT8鿛!JRp/53|@. feiaճ&jMݪO29E1:!=wmDM?H9SOEچWbEzcjӽ Or9LWTKaz'<[֋ KTmKx֮C&>wCLDs2tqsKg1"&$Tnsy▣hxx-n 2\ө3|8e/*D]Qi*+$ yKd g#2Rh0*bPuPA M@iVā7,{h[5UX? ܙawGI'%P,mߚτۦoCS`g*#Z)=AU rYW ׀ZVm6A" G)8^*^y#φe²H_Ep,l :A4N2O> )b!"ALVPq(dnl*)*廣JZsF"ݒbG",_|xӂv)?m}|qڟ=  S5ñdbvڸ҄WRl"m3@{J-P]=TWd-&F2(K.cgӡe}uVU 䎙`] Ě/ꃡ+Cy]%mU1ܔfuOd\ &{5t7?FV6Uܴ--)XovHeQ M.J-l6_ܧ{MZKsf{>TnB=\3ij9 ˀE'A +Ҕ,kQf`|2. lRg_cQ黄&ml+M#Bnô)S=(vn `}+?`/Q֒ kts;~!D_Մdf5"ViMChZ&\AS碙*rRyMHXWW~?L@BZ ,$,UW^, iV3։&~)*Q5OjP,k=-t#5vM8Q1^iǨhr*k;qiU>j)e)Nb,2<눋Db[nFcv` L_b,H ^3-D[jYciBcXy!R_7e OX`bT. ~);LiJn!`XK.{~|aUf!D7} ]m`T%5$D@6w[ ʙ[ w7^~Wv; ̬ 3ScZ sp(VA諄rq2N4a]UXU11SoY>wCU^x>b<[A|הXEYy'V{/n[ !5k'!5p(/7j4ZA<+xP]JwNpᕶn"%o'@a8$:aQF<(Kh}4,Jk3# ?Fz"zLjH&Aí 0?b2RCP̸& jD*C.b!!p#HUn6jdL灟ğc`9 ;0enͷWSdܖ^]|{ܒ 6"3\0.s\9X/'c2R{@mJ rHb:3.R/V0S 3$qRbYqP"Ke >څKx,<XAQw5yVI+x0 ev G$aKκ!A`ZR @J3E!Ҿ֕1TV0AÖx>;z뿼 BV3 2pkdczdقʋȜϮDk0M wlC!,ۗ, +k氮(}Ƞ­ bЃȃ);tnhsi/̪ _/-_0@8ަ`=.`V!'vSo| CB>*]_?< z O3PGz1e B~:=`nۄM>ڦ-hw; 2K%j_޼Ͳ*'Lʨ.fzADj|Pmb",AXL#y冾LvXWLEX̘,Kn.e ձLe7!|8ӽ (0>+Vbe/J|"VWYsºr e]L˅<o`-*]Op9`d4LbW`݁XDxaDJCaRt8e5Y3$#&W@/[ HIRBT.ΞtBJ x-cD:sk-'sDM!*#_M8LіyNKzwu, BVC?;o lkU\ݣ.6WOFs=eisFgD3%n0ڥ1pLz7ūz/nO_1[}F}r0sD( ?6?/; EƜ ۱nA$I;%H02ˠ˦`Y&`ޯwȷ?'`xg4oǭbG!3LIq\X6סt 9[LeqRs}Úc^F੒z%kq3f.P'MR=/P$lVka '=R"~8"@ۥ ZLMH Xp+Sk; ;vkqZg&Ή:1x{HJM$ɄZ| ła#2;;[I (N2U-@bV@CXbFҘ|.}d9^[هO"./nYo._f'\x۹"+PٯsWK`¼t:\$'#/aOe FV. q:_h197+$"t$,pX}9b>708C %X*WB/hzn1\$:@@hv%JG˰Yp $>4T D4!z𿼈l\z2]l|-^4ZGqT|DT;OΦZ2 Rzep`[.8BBLb-%1u Vm6gi,bxzSe A~@ِ̂388֮n_K'CPYY)t"ܥK2=Z>n90d!r xv}74<  yS-%eBܞ%,$lsB^XOLWf1b&ByiC`@]w? Y; X8~ ܿFu~c9b, <+ڥu]tmn_n @ٗ3A(]:t $a̯3,Gp&wXosMf2_]kڅ`7+@IKm*L7|ko0XD71 T069k+VZR`a8K+p%'^mγL:~~.[&< nD=_9 M˳ߓY  kFHZA|r/ )./흰z.N%S!ϿyLK3?|ZAKSS~`W\7R ǡS1—eܵ?iYRanXi*XBe%>z Y'##07s^dJi@h~-la>O3yIU35?'P[7mY a| b@9bHA4f $YeY`~#Z,0!pҲш6ߴ2o}vD I0${*mp%gapgj,0٨i*nɱhշVWg sN&Up_ѻH-쌰Y~"dMӱ,s'0t5O[i VLbyA\b_oه H㑂 7 sdY#o!h$zXrRZ^+& NK\Wo\Dlݴ,-xϒŊhe45aE{O@K`i'M)Dt!JŠ)EDJЏ K| d{pFX WG` w:Ycdܗ_w>gy/#~𳮏LٮrU# W^YcVKb9~ga݁)?%@ BRYsSB` `^+nP<~dF nTK2'b'j[:mTiLuPi&zjَ:0N/'W/Ό~`s0 3/G}4G@+Úd,IR\ Pho3+ߠ^iP^߷\li?< `\Ufo3I|'gcoecV&X#X9ERYlXM0w)ܫyYkWHgN8ClXl('؋Ŭ-|%[SpB%"xˑn{奊N!*~a-4ZD!VnDHH00w-UZF5=ІlVͯ=V0KoH Hf#]BLl V^MSHCVcc0ar9'ՍHJ/tل_?uCpځ+a2)n{f'~J3ʐ'm/%"M  ŐU@4`|DJouXHr/sd6CIp{&; XڴC<@b'b!q!dH|Bc9erjyHcli랗*(@pAd$Q*9XYj";ӄ s&U_ xò@xY|K ?Z0xLlO,mPaÄݐG JL4lD{σ_!z[k r[Z w=3Cx'X}6(`  _oړ'G9V=d[iHvL< ܏ `=duQ5c^Nϯ3uT&+1GE12-@2rP0 H+s`]S\/S>AAR"2w&,!WD88Ko٘ׯϐY-kscmA4ѷ)P6]ZP,旡23 `|sf$}/1\; 9;`mMV:Y$lX*f\Ui'WK3ᙀ;G+Yj?*^ioMtEZp©@i <Փ0W\DԃPm~H} 7lY, | @[/~0b=tilUGuԲΌhB9kJYjmaV|3 @h3rR9#W_X[޳N]1HxyڛElxűskF@S}^.B/&\L==jǰŰo>`RqBETBn"& Xy&с~7Օ^/1|WeW6*ɉY H0n8J830lJ]ixϬqj=fq⓳N}hkBGK_2IN4s/$M{QЊ04;8Y]P`v:muYB;Ei0"¶5[1Bw_UyKcm Vh]=eyr =e{VaEo"(-3'GEӳ8e˗,.KJ@WhQ`-X.cVlndLd#=q#9BJRaג,4h"Wb:IM}2c4 'u0B0?vY0L+6DzȫYk#V݃ A'FNsb2K[`BMgnbd fl'.#1Dԭ첾2H4qL?@{|8`Uyi_V,a[RB7eB&`@{(1ݛ7=wǥS񽷧 ($f6 eV2TMq5BI;,۔%yΰ2\J f s9'-MPlM{Q/&**~ښْ HC""l@ =LDQuJ r jA @KrlbmCN[nGӷ"=|D3;0;넙םZh"E0'<3%=gi0&P*Pbl]Xt \ sr4!OS6^.Gm>tD06/oEz x -}wk9BkYFƨӌԸXR(@L=xp Yc\*:n\op{m>[mbzʗ5~ܗl7.>lyTҮd1ED(%"`G%M%J8>Hߜf ׾` ò.$|1| R̃gk韙tD`X[i#pN L 9 !2IEyk^Dscٚ)w%Vr*=" !7aꡚC!0ݑFFo* .<ɉ`i2a}Yn.4ccg `pScu_.LcTO2x! K "^7׀)}[Ojaˣ H#/4aBK̏-sDjp,}f1&gAp Q$xj@X'H 6MJ4rLXm*ʾi+4r!!WR#(,YTbUTiDB|$.MԊse7l{3fY߬vٗ ikŽ%2U*+5CsFfvHoӟAiyuiW /{ 78~v4WM~4q{E'"7x>.D“Pɪ2?&+r\N^cp1}`t0@uW)F(( C)3iItGrtvb'sžW%L{)jY'$00!]AپY3ҷmϩG b&ɒ4jkW(&F.S͖ * MQ@0rz&Y(.Ek`ъEWooP_&Րԙ@|tЪ&^@@8=+I=XX j!5YLGf) }'{=G+.lLi590K(8pD@'[/[ #ʴ T֢]/$Sb ͕Z| yb׿{qΨצo#ͧg p)Wv.|}qѢ &%;689GilbMvԕL`;LDa_1r8Gj%|@ra0>~Ai8=XjZA~Y-7Xד0Rk*Kc|E@FZ+6ĐLu4RN|r pB$pm8N3pr6Ca ^8&D `HZ v2 h{3{#{t_g37XF% JyWDZKdH"30 bda s,oT0_Y{oHfL>`x~4b`bal%bi Xlgz,rT\9|*5@T{Y0TFiF8&t.b\5c-x[y5l 9 t** >80%8-`|n&\j !$йcb7ڐCñg;' u(+p[| ^'Q72I=Ryt%pe<ÜAX'Ţdq2|& 3AzDidpi}\*c\ Bxĵ/@Uv?z3hc2cA & BOHwpXY)3*;+1>piF1z&q(Q"±yzLH@ff58M;g'77cs6P Hށ@D `8+8 . i术0-)a)A$ށ-< A8e| ?RPK+A ` hhZY "Z 'M/};9r[3M3 <ڳRV͒sT?V[Zb#+}31)1E14rBxγm=Ys܈v?ϧ֚T "|DW@/ \\ora>` ;#j~ eȗX ob/XS`Uوr7* s X'ȏh&wjk=>,c8 X.=CȔJJb|f hPZ, %x>ndb,s>:%rw"hó8I c!Z'0^o&gu`W9>|In)CcNli0 Fmb`oQSVIb?K c"niTV .M`9&@*C,@aͶo? I&pJ1[vX 5clGNm8Yۀ8\ZB0if@d`b9Sp>\L nڮd,1 Ti^*kMZlyB}!Inb^,0Arە~2B`OF\"թf a3X:n:ƽ=؄z9q/[Z :A#\5%hۺBPV7a77я,_^MdPYWX`@¤F <<#K"we^G%t+qRILi>'x̱"J @ɚ!nKKK˺S=՘냶Bȼ6~+3OeBfVTv|0Ʋ/_o{7,C>0b#irxh}D ,/"5$l<XBW:S{f}S=4o+I^Ƀjy'BEbs-wâEZAfF%J~]L{}%X&P1WrDvR@ڭ54M%eΰؗ,_^yi_~͠IRQ#"e3G:-n94F۰̮*G-5\Qnl:K'T\!Q05p cH~0+ =sVQu͆>{BG}#eaO"<K_C_zem2ou2_pyƹ4͡ Bf09##K*+RFJQw^+R_ bHiZb Gr4\6ۿ^Y^ ǡܷ#^saK*]Xkb`6b}0_ ҿ:6,lLYhb5 W @o<oS9yu,]/U>a$ZڲIFeD@VcIJH,m#TĄm,Ytrq_ 6V|;awSOD9z± #Ҫ|rZ|QDf4vn-ĩ^X"VV V,oS},Јz7wCP8(WB>_q4Ʋ]|و w?o@?ef 6 6RB_bX0>c,T4;X Y)eXrΙn-zl8Ib Uzi.uȣBz5_ZfA ]~!.g H;a=n6_{ߏXY#*Gy{oŤgT^2s5%h1>i p|[ !ϓΧZgMi-K6Zf3Ui}ю`ѱG-uΧV,ԡaau8!qqykkO˂ͱ=NS7m_[zmPZA׭Bg{FPl Ea`UUz=ik|&B}Rmo(FQ;?I'lɇ*3HM##jUS/+ Ap@cOPS{'z z"G+ypYr=dξ+Ĭ4KˆjLLaqB E^t.88>4;FBtZ*jLN6vq|xsaUMO'Kٳ{n gPYZ<̆,*M[,%z6w336>!v J!||rA"#EP$s4DK>8R2=%(ގccGfȣI}me6>_7"DL@]!C0q$inD.X :bq~'=Hce53[jn &9ك$ƯmѯZϥ>'L֩>U<ʬ/q} X¹hHa .>f?ڴu@-/UЭD7ܨʉxjCp~+YCpj p!ۆO 1> VZm($acH( ǖU(=na#/=WVc[Q>"w6EzYN5`,R"*ATwx~r- q4w^`_ww~Q CXRERbN0P^\ńnpoB]P|`{^nh,xĦWXs)ʪa#P.bR k_qLM&fLKX{P67QgLkX`27_{ iloF7-uۣ*[3e4x9x #n`nHG3`'p ݬ-SY}H|p/Jz0eiO AcuʚU)/ lg Ě& 1?wJzUZrC0=T`CdzhU=2{$&-GہٲpQL@Cۊl2Wt\9&M +H OkN#B"#8@U3z#̼#OeϔOa/%”oϲ(1BC߽F a+WJ7hT*! p;z`늵}Z^U'Co#`D"D T2,6xFZ1``Y ,i&k҃F\fbL)Ԧ5ମ^1'g,N,o #vH9JkAYͅ_ͫwJ2wF\ |4p 1] p->\R*p ,B }3%Ļ0ԶXnf x] w`ƛė%=bnDZME]ȠR^'#mȯŀpuxbd@/UhV &; 2k3M%ڭ8XyNR#. y=v \ߏI_<BeS36>ƥ4x*M30#@g5P{!<۸9߿y?v?L[ҺUwBJj7$ ka܆^u؍8Q (NF+[Lj7t g\kF¢`  eL(%ks4Ls0*5P">(2ƒⳗPx2|YE:Ů^6|LQA)`!p><47hbÌ_" |!Lh'of+ \Ĭ61~@4͜~#x࣏h(>CY.'k*ٗ1Ի0W&,hoc;͍Vsj~Mi;>q_+Y,Ce.n'o7YbN9Yi^rL挐f w .澧CAã. s> Qnj '/AY@3$!45t_P1 n\=DTx6!2} k@|R)Ԟwxl]zkRC pkbKʯ3 }B B.͘c 1Mlvl,-!p}B6yN+R> IC`ZZUz⹧6ST#\#jB0?f*A8g\5;'E,H'Y.ZqYoNO6Mߓ) bQjrAkĪS"HK%j])tڡYpbUs<3ڠr]s:@Q kKW>baX ~DIZ]zo0,* 9bfъD @"8vq3!JUJF&l,_*6(˄"01tˠؖ=b b&JcT,{<~ pG`'WI)vS.R y L+ dbjZ7 b`1qfo#Al]nN9KYj!֣=)AdL+ o^xجhPN+Ií+"P38Rmb+`2kh횀DU{ժ#곞4@Tې64S{+iKZHG>ۄq10"|@(VaN'* AwiJ9ru9F7E?HQlA)[3ă֭bH)R3Z\.vL =L16 Db]e psm;D160G!87ac %D֍șHQJ(hkp.g&:" G Uiڭ ;{>ѱhP!N4y< 5wCgȡx%mɤ/ tRP~.ͥV(g]֓]$$/#EPB`NXpm20k&ƆUu7}K$bVr@-֙mg:ZP\}/FӇbE)LS!+xhGڒ^Yi PyVQw| v>O/e0ˢ3ۃ aXvz&½).A]3j5,+X z#o._H_wJm^g¬/Z; -NR vd|”0l5YU޲ɲ0~3;d6 T7MQ˩@Ò8)i0u^|'"2-cQ`3vh˺#t%N_`9?lʹ΍Ŧ#_Xj&J|Iioq- <0pǤ6g!li @%= *f ½)\pEx(2'එDB`i Hw2N71Y1 ^lPZ+ob `~Aui F210=]72ZVbrI\8OdRM=3YE ? P(w~0?Z|0!6M.J=6q]ZR|t<xp5_`\0}!b ~1ty4f'%Py4Q9K#"*ܜ3 NQ<-!ɒBx͆nm-N@4*Gz]xC0*u[WGx`t&_ZlA;\@8ۺm7 %y7Pz|aAtO5M̂/VA @? Z rv1Bc @ `IPgސRP r$9!3hBbh ` "l˳ <FͱK2UR5dȱBsoh rAztFp| , ? 뾳P|=9JF =ۨc~;`ۢ2}`b4hbv>x >,5M1ot`P`Y懺5S{x? _!؝=`ijxB;|ḩHe  RB`&r@ % %Dն?3_6)Hf:X/5z36~yP/3i BMx5[dhLiF ^A_H y~ͼWg#n,3>?4ՇqںfD#x362~d2b)m V„`V|jі4]X!Џ)h AcZ X>_ n6eVq7<(]; xwҁSs 30ڠy_+Ǝ@6 $!7"V0V+$<#r1 _ u\y{k!/:ċY鈧=bg,Kd\zh3`@s 1bb4WH`snzH(L3,z9ƔrE&sH?bk >egcl8`p<-7=+Qđ{|QQBpo`;Qx|Y>> C)9Ox-8/ \x'7Tnm#ZpͶ<4r|Z c5=04Pbr` ,DoL[ۘF$OvŴٗ4[(L_1..WGwbnEo}:p]mK/@N4 tp4BD/@ߟYdT AZI"Db~>,`} kő9d+ J4 ܋=ú§duWm`Cj{{{-UBĎDə2fN#$0s*vlV- ^ \FH0g4DE`O Ӯ^s!ͅp1 斺=QڔIf?BxP1B' j@~|uZ6ٙ^iTj\ᇋ;YCk֠p^8:+m<6=.K\C܊F,]xO5ZgYT`B1/@5W ɴJ{IWH+z?,R/Y6/ 'DZ=W/8]Qҥ-@fqNnHI*0J")Ml,^\]v5NY\{oٽb<Hm.Ue O|f(XK!G Όwjσݽm1a\?ՠzRXKD4б+p_ {EMl>^= E@M`Mc LOi]Z0'B'+D+60f*\ ; C4GOȌFG80!! VlQ/^tgv&tɰ7 ed~!b>hk _6>ު#ӲX{ -KҖ-fklv&QA?dSsS:P2chp'%FK%ff.!g/-gvS6hnFl KHXhuA i]OK`֜ZxA% ef+ 7{L-P{,O9"@>Tfʤ&]1-yTy%J"4 11$J LkHg{43BZECTZ]>ȞΈ"nj]cL@fnY&Ak$}SȂOpj ]C㯘Ȫ>Cbh/axz, * KxE3\R0¸4^땮mmǢNc]ؒσڭvױEx NS5.TCp Saj}+Ö\7Wɇ'Uq3p]„;.JAEl.,CY.p1 ASZ +{HyDZ=0RR&3Z(215as1+>`@o핮!}0U7OEHbٕ_ DD0!iE*7VF0? ndm}J4 <[uڗ¸o)?Sv(O%fTO5cdhcc\˖]ݪsӃe8>h]xx 4)J.)XBWh2R2Y AfHi[OIV;*6o̭A-4kl@6S.ć,1XccM!>Z <+`Xһo)jxd'(v/Tkߍ$*6ڒGRyG~1߻=X ^x JMz]6*͌E;++Ƃ\xx-X-*xJڮy9}e%>-Mّ~r ;Fgt0ico&!*U)N["D!GgGoN[2# VӁJ?潯k5-~NXbj恶`-sﯽLuu BT0%$͕=M.`U7ƛX Yi&WPy0ab` ɗ[=pLѴ -*PIwb>`#mޥ,FgB"O#h +\C)!R " `DY2pS6UI^d6~oDFñg]Bcoo ͠_Xdzduە ">HY"t—`7YI!Zsw1<%G+Fbfd;Rb^+x&}~#e4wIHk\R&3LoH}i/l#ѿ͔g0B̧ᘰ3B7>-OK ˱x Az#c>Y( jPJhŌ%^0*|f1hb\+ohYr q2\ITi5Da!/8 ֞CH 10]:hKړ@DwoMYbʪxaW'L{!s RPKw6BfZ͜9- (OiK73wKFg3sMD^<\05 'SMPaJ Ѻ ,uazXl,Z|>@]ɩ+ ,@ZDoI_Y+vEq0K@@~!3rj5P lĹݠ-V1SVU~; G-/{~y/CoZҞάUъmUcDA6-`K}q+bدy+7#սC~m648Df~@-A`!1iՀhȹu~lJ\ϒKLD)-jsPSݲ*;vcy8]u*n)Xc}\Im,vΫGT?̽sVm f.{# L 6+Il47pT\l^yhݲ6@&@F&vWto~R .C!/F6 ;HG(⪐? y]F\`|滥 h#iDUn,HϪ}Ћ9 3eqp9 ĭqy#|Uʎ1 ''A-hyrUzSL0obČ/`ߕU^ n=%]W::+ rG -]N@*x>i󺲤QG)w-dcc@&`^/ 6.Ć^(TSML~f*eYgn L#jsiۉ^f^fbǶu@(p/oOB{`V,~UTz'2zrc<f_咇<`\jBh3/fTA@`P f7n-ڰ%OyىۚMSsqRZD4L 'W*>3vYxtti@4hm+#`qr1lmW?x,e,/^! ] ) sRW!b!gʹR=6kbwz`OJi,_!AR6 XKgнx9~U'!A$'MM)lƃbj"l._ZO!^[w|4+Q䴙 ⳴p㖵\S?]O[҃1^;X5W:MCKiPuc<%—5Xk'8l,7a;掞I؝mO>>vjSZޚ5ZgcfRWnB2?1"l{gHbi'mv~?_~/SiôHtA%s.6RjDʽ4Q#! @1#PFf: 0ظk[78'u)\ wG>|Շ@3wrE_EE*V1 uqfv}1l}Vwv'nbܻ%-(o=S C?N)K:o\W,TۄX33FEhUǍeIiU&'wܻdJwGa2 3w|-;+ljS}4%*uH~rJcHoc2,_y{./0=_ID{F; i~һSk^[kU !FұG#4JbJ2͗(# ,q-.sF^~HT%O fbzD#) uX}zd<6-8=$o5;mabbe@z<vb'ĘTgq,rSU? N,KF)J|0E8^ p#0u= a1s' 9I&M.zx mdg񓡆)w z`/]ޗ6j7{`G#'}h=V>KK "Ucc)&LdX|_TVvd2'"j5c1BxP1ӆ,7ӎJ0ysOj6aJ"FG>Xc]7zj7w@,.+nͭM*ݢ5k@MN.hMp?ݏӪΣ}{o%@^ _ Q3HUN1g.4MrԠa{'d9rP? QB`ʥɳ-}>O,i?k.ԡ!>MKִ3:1"~QVL BV:,y|zvz2<3;l*DݸXɨguJAEú ί:MKF-CΥWF&E_EF'Y`Pܰ2KApgX9Lбxo> 7XጂbqJ5$\ ֐j7B/scA 0KE*<ir",f?i*HÌ\#OLɐ;n~\y8m!w@v5H0X?i|]R o0{ȑ6X0Sh.Uݍ:c)YM_eINO /KL;oaM|rn"tnWzY܍s1һMOllד=M{"ʪR 1*f~e.;۠t>}BB(6ܳBA5+\iIh}^wk*p̲~ZYf@Gz27_L'|Y%0 B74M0 KMvٝ3wHJ<6l^C*3= RY4mн#Mì5>DEX=|j4=\݌nse73ZOg ^=vaOЯi­Pg`6@ &sa˓ƓYs,VOlQAF/`-MEƥƨf8sQj2 7piK@Q Ch k?;6Њ_Yt/_=ie8nm@?5${΢Hy,\:?_\Z3k˽kf>f2H5/zEf>" 3qi &h ot/®0I\s Lw ˭K^.32kݿI37o-\:-c8+bMFvo*0jzqF!~pwD:`AMZTz`bV  o H4\Kk-h+P;@\]) AE纴 (|UA덨_!kSkAȋm5 >`b齥Q1EV5sR"bc~O=nsI*!E/EXœ~ j ċ01U_Kbh41 @)K,v?LMD4m9Tˬ>sڑnaM.ӿOog,z nfmx\c|8[,YNk"+,GG9;3:8U;~b MZ%ɡD#. / F4D(cP@A2-@YVoyc>[ن=ٚ u~`M MI~>DK1c)SGs"e%d͟&Lb2>VC09)/rt$UV;& 4퀝FXRL8U dCN+C6C9QIڀ!"7G`.-!o(ʯZ)M*= x&15 O L`… ߓ3JZ0D@7*)1 S;kzLBaD rԃ,c?eYpu: Υ v |qO@ J5f-n;C%jh؟{HYA|OA gZ/A#pHpC`+0fa09Oȴ#݇_=Wm~}d6gn?w̎ooK+~@hIDz:r]\"\)4`uάw%Fa|nlMy.p9 H`6÷F;脾"lӲfzEش+yB0bW|QjUIA`\Z"&]|vX*~59]G~n,%JPQjPt1+[i 8;ۙ_S}׽"6P{P<tw[t`Jp{Fd`F w4vRu 25 9A@= pC"pKE$BrYЭsT# V/B5ݍ#be$g-(fb0ܛy3B?Dnc.#(3aтF&?mƃ1X A|³6.GGqޜG g)ݖ= fqcut?!0tGW77?3*Pn ΢z%,A+0!!MPe4bd>\Pfɼ>o/ג#09 iV 6g!ab .͹OX9l/dgລ&XhlD k҆@^s*#TZUF MoR n5T5=\Y3^h-qҳOBOfD,h[ %'if6g8iavB`|\|s&Wa"J2-Qn^7BkF:bf\ *d,s,"N[ Jq탩5xH T CMV6ԝo0|`惩ăp&(_ʕJl!i&C?eVhAB6ʁv Ed%#pcn6z2"z4dI9D%1 {4=\G4K La^ > nFj B sTՠ o +^ vF| FQ"aAƒ> )0-MsGk'7@oo#/u7AM6gO-+/ gC*3V(;f@lOM2bG[a[ ?Cx2gȵKЏBj}-9ߟnL"Xq,nG`b·"vd)7WQ; ZC yD fvqѕLjZyЍ兠oĄ?Cφˤ+ BfLĚ`2f3s4Qii3|-VŇD\nɸńjCHV`n-҃)K3Ʊ1qBvڙ;٫ f֋Z՘C3_ ;m,ˮVkͶk+n owN;gٚ:턮X> U L X*Ecp(nbfibMyp=XY hgUxF?5J- dFv+2ǟ$- Ȕ>#b.ӀBL:RtZAd6>@!3E:{'C,}o$$]W1>&X`b-`v)-a1#8oz[kG<68~lHi x)b|7]Ѐs`}iqmCk_1 h=7+B_0KhE$Q!+fiX$E"8KE9l"Yqm@n =A٩˵#+.?FQM, 㘗UHHcc>YؘK.}h m{¶6reB@1 &P=-2" ċ{ K@,2.ȨPGYw %U pX0*Zؙ3V1;cy`0l] qs||0]ZK+jfhzxBHDA 94a#<-+`gR,}y8𓀳aۃ a$$w`&{A?f/W4lE*-1d .pYو(b$YB}d_v>i[dX~LX'Ů Nx}cG_fd] a-|^J v˷3%BsP#EA?oZ'ܿ Fxãm2PB{3><#%*uFdA#,앤kt]0#Uxy8?"&dŴfsv]A1Y;Y@= Jc<ğs)օشWW.:kuS1M`k*ux$! S֠hW<̑n"xSlb_Y]ȡ|6̴@[΃X5ႸHb)ެw!+ٻCdy}L01{izфJ+Y#$@/1mOߢi[{lG̖g``C h:O<f/Sk? * '1;|\m5 pRȉZBЕ?9& z~ b`fr %ԫij;Y{r @>\NI5y! x`sdŠҁ |t̟lL|k3 H'89LLlN.Q!L"r0;I횤eߐŰ}ў'fk矺G `2B!#mX/̈́-66"_?ˁAtsYPH2Á+5brQ&4K{Qn)RI#t&1,$E`JǂR>9XϏy:XI/+&Py:Ihzӂ[b5NXq Lhx;kŇ!_;9W뭘З2v\ )DlA`ҢHkrUa@=VS_KsXse5X:5+T"R8R~fE>?y1j%|'%̹}Ưp1ZA[ާ`yT.|$;_,Tz2u!A]L MG!l Pf q]1Yó 4ӽ IOH*6]o|P7N Kͻs4D>C8͚L=B@!cl15eTS)P0O; dh9)X[z8 ntPt>mQ CԘ+Ihg)]DPlNkͫ|΅yHPh[CH齑RXq!zKxb\6ڕ% q-/omBPe0ETgFsElH4*Z YX, 1j4쯤TLLD!hRx>O1bXޭ͗MTP*ɗShyfOzP5!C& `eA@j0:1ZA.[Q,Zs.!n皧JM !7}վ>c{iԱ-5-c oafxryd˙ь6~ dk2`RA_&>_ ֚6ۧMQxtr.w,R#ܽx[zQNK==W{cA(,L- hZx+ç.mzaBh2#&B>wD$CMz$^TrO9HaԴԆ !H[ %a4E={Rڪ/ъ^"r{H o |BF˽ox>ڣFܟw'U@F ? L,;jw40,2@[iMb*;ܫx&hp c)$7]pLCoP68g CЇ|~MQPDD̵wJ+[0=}*@`[|>{Iax4_()GC}iEBss[)߯V1'eZ]H]<2& ?sXy&1'<1ͅy$IPy/v 5awo §XPaKSꋷF;'sTr/m@ib˲ VH*ͺHjhC=H$ry _ 9m)v/Q P s ~ ejWI@E"~ 4 24#h?&8wLkS/aɝ eZ$xlO?oeĿb4'#j (XUYXjh`8 GHTbڝ!ܴD_P4WWZΆPx,9fBZ2aMMؔڗhC҄+vú  ·оT]wvצR/AKz. {kQ`cA@˒Lo =$5YFsf@̔s!{Y_6;`Z!^k " |UM ;ҧJl󦴂pD~N0u$cF!®Y\8=#]:| rua1-+kw&>;1H87K=PIDz1N>|Y% FN gRkZ6n|X_0o!u)`R ο+k0=D ߟ#"|+lig @^BB- ЇT(D ^z/$6߼zN~Usyqck5>/\f1]Dv7e@QJvMضpYXm!.|TVo`֊, 5D?*UnIg[>,J;""IDATaR˝њ~|9b9X!T3p|"t7F7Zh񡖇/,"Y;˿ wTPS \7&}9zBm3{7S"ji[fp`dH%p'SCX;zƾ/N  O!>植'{ͅW)J =fWXg'c1HwB+N ڪ> 744Z(pkh{.WM?JH,NDifApgryܑuac$2-UW3_5 % |Ab d2Č0fB $8ƸEMsDЅvI Rm&9kb{j)6 +81GhcTpXA/T21)U'XXb(}ׇC[8QNLܹLx6HWPei cL6կZLF|K[u65e4#!stH$?IlȏXJ; >)M<)`Lk5S n A(J;r% Z&uЎRAP䒈"#qB%2kaK[{!Vb滕"\C\iqA,FXM 3MVdI=fu»~()!W DSڴZ=DC s47p|6#0~ ߓ}/hdƌڶ׾`zIAmO03e58¸o_uB CYDc>>E"c7C Й.EIu1Gv SFkpnׇ;O&!m ޘ.!ӊ~pq_]wYZ&1zk{w nfЀv< m)@['vHG~>PEТY-] _LT_yeJTV$7U&XHN\>I Hm׾a,g+!\KB$_x5lz%ۓsy<2|qAU>Ik|T$4:[ Wʟvd Cǩo&mpJp Xf ^O/=aغђXIki+~Bhi|SrQ!KӶab 4??DJO1ַ$r8cЎz\Tͬ^>[KnL?3i"31hiքά_mʏ!҅$ &8Z`4w%j#s%E7s71vӾ&2„&7Lwxa87 G+ڧqizLoÌbU\VIm>+՛? (#> 7t`ba nUD" ||eL;$q=0g[ D$6AcA7m9WJxo{,&A,巡0?ẋVӱaU X Ww6唆n-mug;BЙX[fٖ{D߹s[zN;;:!BhWU)ZP={62AI|0 *Qcyb) NoBq!6b"Hk (sOFcYh_~ jI [쭌~Du˙~*4xܞÁWxl iM&|+pDb3s42~ŊCKo--`;"\nJOV)ߛ~_nlVx|(I;o-_st BOXX@V+0!ĔevaT 1eT3Mt%Ah'rGvKk9t S"L+2tȐ ։tkbF<QweL~-kkpƌB6S_ T}O/-WpJMMooG]J ^dK90Q0 `(6A71RY EHhx  Bcb2'gfV*C@MKs' U¥IN-f/ q"GL{HW_kaxw jU'&D+pg)yb CY; Vj4[i:ify. F%?'B ږf%sQ/QYӱi#SFK  ^ Pxoscl !J{.Ǯ1LȿJW +mӊ&!xH)>LxSiZ Qw"vZ N^hנH%φ~= :>_ T_{M+X<hG F? S>yqZKWQat/Sd0m#Fhhq/o#d1.c8bAezT̞,!X kA73oR^,m0#Pʊ+nFUEMryvcJ<;WpSAޙʕΣa=P"gmS㰺YiqށH4 1 0f: =Q4&\vBnnTOCtI2 _nlC**aɰxJ-yw YV4/l8&bXm4uMpMD!o^T{KbVsʟPY b u&pnw;*VcCWm -Y t'-0rI]'vF~]" |)RDeޯfR9+v$}m%2-4AS 2>?Si6 ,nX -Y:saXuVZw5gxxc{ow#Qu7L|mU@ë`0Kכy |m W#SX,Te\p"TK9n"x7 @>,o?&pnb6IМxbJY8[tivm7!TX,MX ~b}/&lg,"Cxpo*rt#ҿk6/Vq^ nx9,U Tz!.UڽkW ZQչbísZšzRYkd+- $(IaP$3 QjݍZQ=:[YP-.fAsZ++v䊺cX&-pH&4} I&hx0L+gU y`7-D&p{ԵdBd+>Q^b" +")A5< Jx;iBroܮTcBq`qOH8v߿-W}i{a߲҄aFgdu܀nڎr|B zC,/d2ͤ y*,4D\ԟB7-7ڄx湒Y9 !J z'b:k1ya-eT܁6LA1~~#p0&끅 ןim\1}Wm' e^r4*td V ,֚jo);QH:ߣ) 4ףOd]}|kL3HD9w[1߈+q1o "}`2Ha^0@74f~&dהڗfJ=W`JJz.NJw\3aFx5Y`i^0S2ǥ^O.=oue/~ue/>;XbTĴ6_/z AK6%*Cu`n!~-{umhʳd/Qh0f2ݪ7Rk$( Nun}10 X݇ aJ k+!Mj^}1lHK0TMWyLMgR"V4քkf5Ơ !]!GPCkDZ.ŋ.%p@* R+[Z{w^Jꂷu^~) hPT9 !@9ߔQ t&ZRBHFwS_.%ꑀYe>/ oovڠi6ˠ\/A =wbbLh1~y! Z?v# ,KBVkcG~7#"Y>Y1΋ub5O›5n0@QŽ$9 Cr]ZX7BVH҈s+4#pP*مa(36v|Pkvd@hms=R b/fhZ@j]kE`~,)fh~т̳`k&PovуEZ`X 8enY*T">6B-^ ]Ѝ=  &#yw˽ƌdb~HY H{$3 bQ? "`oȾg 38"XqYi3$]}XDtiN덏NOcNlm]:p%@ ۜ'pfamUiجb`aZí{?1gk] Mdk,Uh&0e@=Dzl!b{,/D M M45]0ye3R=CPQ!FBsg,4,z?BE2=ut~%>C^x` j3*=wYݍ[BbuoEةP!`0t2 _A_gS>V KАivYŁ<\/Ƭgzwc"D>i$MX!i$}@HBGa$ߖ8r79r}ߵKc_4]Zih .\s0n!̧1\y/M`s-,õLj{AaϠ]7Ɖj|4jIn;eadz;˶|+C\LP{}bwIO)"!Y"B"ξݓXD+JůM?JĤurj1H©*F |X^|{mѺ)! %}{#pǧh(@hвmL+1lEj򸄒 ifX+@L6vFzWv^𮎞.k(h|N 1f\^T Dq)|?Zb̔#u~#b4/zkywUt&s2c5_!7&bDŽ.<TTq =qY *Pv 1l1xUUpWږWbG8V[].-lhy' @NNbm`njl!4چ>fk"1Lڽ&O[*b6"erlւ;e/^Uh{ +Ncs7Z` 54Zƌ:Osd HNPT~_ehE_+yK }cW"89%=LNv#.t1h]d8JЭim7gq~Qs"^!]ezΌ=Ն0ߣ/x&'ḟ| ԒM~5$Bij~O!EhϜlՄVU5i{v 1 b!fyW+n]o̺קܣOAlwK|jwe3 -b=/5bo- r0 V‘0C1BR?(.Z? SBCTۺG @ yv}[|!y᪈ȗ!MX~Wr6:]@H1״~qL*V b דxbo+O`צ]D Qjjp",1\&*Gs><W ,xhم;Qgi, 6vMDٴ|+w&U~*?>t_sg?;Uz`Nj''[G5 \VոkFYؓc{X  aLβDZA9v/W>?OÜkNLiMw#QԿYxA|;ޫ A^6 7C(f"4&ϓTϼV`83I gM+lw,R/Α7 oɊמÞ)x2`[ķY[=ВGPom]PfSݻK@*J"* N*:׺ ":A"UA\ MD6>=\Y=}O{~dOk)8Y+-.x]|'ٶx5ӱ2b?t|Ia=,l[M9jCE.ל}53:x}惤TtJ*%.RV|=GtMZ:K&фn{ ܈N=GEYn*^ՌSVS\2pJ2O;f6ZZagL?p\_$zbK~f w?HEәZsEuSVTϩ 0:*k>ֽHX,IQb)l/s2SFDZmƙ}OQR/+s(G*s_&8sI=y-QxO RIV}#T3FR{hd9 :}MݷOgʿ@@@@@@@@@@@@@@@@@@@@@@OU(IENDB`ic08㓉PNG  IHDR\rfsRGBDeXIfMM*igI@IDATxW]G'}=:4EtO{'}=t[(BzЫ4RL(dB=Ѯr <k=Xds>{ΝZ2w C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C !0C|zn;ۍ{n7q0ؗ+Y7u#l[~n7S!/t32;p8hz0P9@S#om;#݆59#5zw?zWgsϷގ՟{4ݧg^[MtkCk?/pԹ{s˺+ᑛz_{c@];7xܮ;kv݊/V&Htƻݪ{]|E G#ѻ7L=8܃ N?2Mv#JPnX;/%/qЧ K)I}߯O lJ<1Z] / ꫃尥{xB7oރ7cTE ~[| |:iow;?~z ּ.~'2^ n,<]K^@'<% ܺ&B|A>l9{=酭[^t7s/h[}?yL*7 Th7:+{ fQ40óǧ>=Cn/zdp+H;6<-H4s;0_yw|/~'fۘ+/q$]?Ȁe!0 </d3^b#  rԐcp![_~*WqQc@YO2׭y.WL^NSF<GNu[ igU~eJj~ۣ=|Oti< xX+`[򹙇?ș&\$xwu`C#aFh\9W\{שu>-|鴵eKsd⛋}0%Hٳǹ|.LÝJ>bꜸ@r܊tr]>'b,=2A O2 Z ILnۺۜaLY+8m .s[?󷃃9f(8% GB8Lc^+uYjŴ"=+[c-,Eciv]O0-d=; |_DRrf|L/tpe%fSi6\juݱZ=7E$HoWdAXrL[lM灗sH2xuoPx3uJRFX/U8?NX_RG{|oI$OW~.`,Տ 6+0Oy0e4R )ɴBpWBlb^Yn}њ_1X圃WA&/s9~/ mߟ+t(4ֺ#%fu7#>'zL3X$"`#GKU`~ Q=k;W&!!)\10̇Ye,}JIsf6/fќXdk"hfT_ z} 2Vƺcᇛ+_2n1^=\ ϯ FCgt9T/̦F3>7 V|3E{ Oz`vֈJ#J!03_s/ ShgsIƟQ`g|$NϤ _86P* y*o! +xVڼ?q18$fGCCHdq !H% TTtz[⪹gEB)I "ȴW zY+th'IdkA`؛sE/c560'`Lb" ]~K'( j! |cQt,fLw1ڣ{g5+t3P+ c!\+(8~4laAD>+LUyBf|{o0{?ALF@yt}eQ9MM'2?-hhprw<~J9­ uМ܂#4?/2Y[ӁAB<Ԅ=3W愰_5:{~#y)p<Pj,[Vٖ\ї7˽,fx6!Z~Q-mBq=,=E%tnΗ#@8S8T:j.˭)E\ҋυM ѿ9\@V\'NF u"-+do^!] +S6ǵs+f f\H!SyLE?{A2lDMWpjgᄲ`Xg)%?-Rx;Vy.K}Sn+j27w._#P`rX>eUpepw俽0(7 V"7Aٞpk%f1>|^h}J|WҎyV,eqnT/wiKNStL~: (JTz1ӛ9I0x"gd/y/+My,p-&27rfddVږ\1\fa2B`:D23ƒۑJx)&;L"Bv$x°fߺXUOBp ]@Ն!b6Wsʪ%\7Ka4BÑĄXvs& G)҇MopuU#HdtK+̥ӹRQw#FuRU31oK 鋹O6ٟRsegɗ_0"(z;by4L3%'D{[M ߞJbIU'MUJ>ʖQˁ;ʓ+.U.X!z(n g0({6Ȇ-ml.L`{&T|0AwKF}r|>ػ +oR /U`Z=\_g~ zs[P;|/m&v_}"RdS[*9 Hrږ-f 6dz뚊`s8BjLm_ y5Vd`ё(]`|*F- Oԑc ~ odW@\C 1#%.1E-"b-ʼn)rrS׃1<\eurFC`9Zɽõшl833x]T!-< dȪ6a̎Rz%wFPX+jNuC4/j*Z7 |Nx0Wv?h%$!9/FbUԼ(|0!i~#*VZq{y|!rmޟiX-<}$A@eYMn̯K?Ip&'Cg \H!5jsɜ>#n=Chqa|Һ_齜~&8$^~r foK oZ`eĆ`=cY@y,/8.sƃwqŵ)ۛ9t8_VX3\uu& &TЅs#jHwa :㼚:cAq"!9?0Q&K=a+*@Jm [7c֍gDdD2߆ V`<="DPC)/x~KKn&M^=/ƕN\_ߟ-3V IYV|R_Y(|*Ո1źT[u˹ EI'L ߏӺd1n o(>ʒ`F\g7/}|ΚʠtWFt7OP.ǔ?_24zdBC6,=A*vڞ$C'péc+ЂQ! 8wݬ=w·8NsC4 <Ií1+(') 0Ԛ D=urG&^O;0RJkS>A`7=&'ڙO{`*x56؀@t6"420-7AP?1 *wpn a^ZVFOhÝXө<\ʳ,SuGuCp)`󇃗;=wH͋}U6< M0/P|EK6#6bv ='%$N%ƾ%!7,IW#ib`0]2S|zyDBI$MNE24P4#F((x^  H@=6z3QA,o%#!.UFLXVqcL A#q nJ0ʟݐ^"cM"gȭONW5PI vqj dLg7:yYL ?X^2-Y᾽ uzY7^ZaYhy?6.FpXx85e2;Nc4"%G=e1J ͎I_78S(ݭ9w%}_ LaOG! 8{Xw<*MD{.6_cfC5f .Ftl"͜7X`$ @l#R[Br.denw> 5@dD,c9ؘ4pži9-#"з&wRз4½R| -^1fRԾnb.=&:`QYux.K>)YBr+C&5`ɝE^G!KND$C8֊`d܅ Ɯ!Q`tt!h==WG_M}۞x;D&rb}*bt]xʈi E$2- !_p!m+ٷ; ԡYŸ^ n\?uƉ0c`FBnяҜt ; 'r,ԇx>Sb*7ᑮeӊa{V wU_YvuHMieznں=} k҆|hcrE5S0&XYeN!=:.a~×8x`"L 7T B^^f03JbG&LwB vF=X{ *&~G(JFˠNXbLZ-ʼѨ4ȦX &@XE4{` D`ܖOl+V$?SbihRz=jJe#,ے,"d4eЬ`B]ނܸqaqܙ|+؊g- uᴘr'İJcfaO͞ a3fAzvYfs.ZZ{؀>I|* t0-uɁ 0D"ӏoq0="x t1cHگYJ~A.H) }7?,ݗ_RaE'vW|>Pj{1.qg'{!-aꏠ­KD{2`pn:fgFYBX4 OLS)ltSp5*jb2*:/uµ3ri9B^~pªh\jkj@ԑtcP!tQtt.x#drzIJt֢赺HMS Qn `%.eULWx`IdWLD []*ZL?3mkhjppLH3n y/y"(^ߺ ;cɝIg(=S{Top殙#%jKPHׅO;P}m(j~8GȺ\4BD{~_o1b*-cojwY(4/kaYI+ BEhV"*Aa7B/S)%DR7)WSNEpj ."i{짥*ߚT[Q `hs>$"eJ["D!MS(q%, 5գ ±@rh> M(hPk^ɯ= H,T]|:'gOiIşrS!%_k,2f̮% )z23җiD⡻qϵ\X1x8Ic?@I4K5xKEL`+Ut&ne[=ϲWQ4C(/N>_O_0+~SAr?( @M rQTT)Z1ZapOPGiZ2Zi_h`_)crJLp-H{`f!e3s ˏoq o4XEvf6Sm- 3I:4R|uC#C$Vy O0iFsHmtt0ԭ$vuJ֡nޜ\'\fp? vG"+:ijR,.0 .*ZI}3Os+UU=^QJT8vXb$QDMvJFکdnnە]Ͼ?O?dFi%)K~-3/;r b!Vڝp+ 8XbD boyxgcb,"0ܩ0~oA+҉uI!X橔QZi ]$'9 1mҿ5g2mn3W2`ߥ =cr@ߘs3J;%p@PhdZײՖi3"aJ #23-N:3w;Am큓D+ ,p@t 4,jtOJ )Su)Ӈ?|8pg!kg3rDPYq ,-̦Gc;%1VH> y Gς01%@P'IVd6hŅ0ع]U<'aUXgkF|)`ƒF':fU͆qFŸW9b6%,$u+H%u붕"liG,Og9E5#ڃyT77Yߧ݆)(oF 0 b*W,|0["#$Ϧ>589)cr9 X/>]*uQ!RdzO]l]GHzuD=VF Be9!⫹x>0`$E 3OT~Psl5 GT8鿛!JRp/53|@. feiaճ&jMݪO29E1:!=wmDM?H9SOEچWbEzcjӽ Or9LWTKaz'<[֋ KTmKx֮C&>wCLDs2tqsKg1"&$Tnsy▣hxx-n 2\ө3|8e/*D]Qi*+$ yKd g#2Rh0*bPuPA M@iVā7,{h[5UX? ܙawGI'%P,mߚτۦoCS`g*#Z)=AU rYW ׀ZVm6A" G)8^*^y#φe²H_Ep,l :A4N2O> )b!"ALVPq(dnl*)*廣JZsF"ݒbG",_|xӂv)?m}|qڟ=  S5ñdbvڸ҄WRl"m3@{J-P]=TWd-&F2(K.cgӡe}uVU 䎙`] Ě/ꃡ+Cy]%mU1ܔfuOd\ &{5t7?FV6Uܴ--)XovHeQ M.J-l6_ܧ{MZKsf{>TnB=\3ij9 ˀE'A +Ҕ,kQf`|2. lRg_cQ黄&ml+M#Bnô)S=(vn `}+?`/Q֒ kts;~!D_Մdf5"ViMChZ&\AS碙*rRyMHXWW~?L@BZ ,$,UW^, iV3։&~)*Q5OjP,k=-t#5vM8Q1^iǨhr*k;qiU>j)e)Nb,2<눋Db[nFcv` L_b,H ^3-D[jYciBcXy!R_7e OX`bT. ~);LiJn!`XK.{~|aUf!D7} ]m`T%5$D@6w[ ʙ[ w7^~Wv; ̬ 3ScZ sp(VA諄rq2N4a]UXU11SoY>wCU^x>b<[A|הXEYy'V{/n[ !5k'!5p(/7j4ZA<+xP]JwNpᕶn"%o'@a8$:aQF<(Kh}4,Jk3# ?Fz"zLjH&Aí 0?b2RCP̸& jD*C.b!!p#HUn6jdL灟ğc`9 ;0enͷWSdܖ^]|{ܒ 6"3\0.s\9X/'c2R{@mJ rHb:3.R/V0S 3$qRbYqP"Ke >څKx,<XAQw5yVI+x0 ev G$aKκ!A`ZR @J3E!Ҿ֕1TV0AÖx>;z뿼 BV3 2pkdczdقʋȜϮDk0M wlC!,ۗ, +k氮(}Ƞ­ bЃȃ);tnhsi/̪ _/-_0@8ަ`=.`V!'vSo| CB>*]_?< z O3PGz1e B~:=`nۄM>ڦ-hw; 2K%j_޼Ͳ*'Lʨ.fzADj|Pmb",AXL#y冾LvXWLEX̘,Kn.e ձLe7!|8ӽ (0>+Vbe/J|"VWYsºr e]L˅<o`-*]Op9`d4LbW`݁XDxaDJCaRt8e5Y3$#&W@/[ HIRBT.ΞtBJ x-cD:sk-'sDM!*#_M8LіyNKzwu, BVC?;o lkU\ݣ.6WOFs=eisFgD3%n0ڥ1pLz7ūz/nO_1[}F}r0sD( ?6?/; EƜ ۱nA$I;%H02ˠ˦`Y&`ޯwȷ?'`xg4oǭbG!3LIq\X6סt 9[LeqRs}Úc^F੒z%kq3f.P'MR=/P$lVka '=R"~8"@ۥ ZLMH Xp+Sk; ;vkqZg&Ή:1x{HJM$ɄZ| ła#2;;[I (N2U-@bV@CXbFҘ|.}d9^[هO"./nYo._f'\x۹"+PٯsWK`¼t:\$'#/aOe FV. q:_h197+$"t$,pX}9b>708C %X*WB/hzn1\$:@@hv%JG˰Yp $>4T D4!z𿼈l\z2]l|-^4ZGqT|DT;OΦZ2 Rzep`[.8BBLb-%1u Vm6gi,bxzSe A~@ِ̂388֮n_K'CPYY)t"ܥK2=Z>n90d!r xv}74<  yS-%eBܞ%,$lsB^XOLWf1b&ByiC`@]w? Y; X8~ ܿFu~c9b, <+ڥu]tmn_n @ٗ3A(]:t $a̯3,Gp&wXosMf2_]kڅ`7+@IKm*L7|ko0XD71 T069k+VZR`a8K+p%'^mγL:~~.[&< nD=_9 M˳ߓY  kFHZA|r/ )./흰z.N%S!ϿyLK3?|ZAKSS~`W\7R ǡS1—eܵ?iYRanXi*XBe%>z Y'##07s^dJi@h~-la>O3yIU35?'P[7mY a| b@9bHA4f $YeY`~#Z,0!pҲш6ߴ2o}vD I0${*mp%gapgj,0٨i*nɱhշVWg sN&Up_ѻH-쌰Y~"dMӱ,s'0t5O[i VLbyA\b_oه H㑂 7 sdY#o!h$zXrRZ^+& NK\Wo\Dlݴ,-xϒŊhe45aE{O@K`i'M)Dt!JŠ)EDJЏ K| d{pFX WG` w:Ycdܗ_w>gy/#~𳮏LٮrU# W^YcVKb9~ga݁)?%@ BRYsSB` `^+nP<~dF nTK2'b'j[:mTiLuPi&zjَ:0N/'W/Ό~`s0 3/G}4G@+Úd,IR\ Pho3+ߠ^iP^߷\li?< `\Ufo3I|'gcoecV&X#X9ERYlXM0w)ܫyYkWHgN8ClXl('؋Ŭ-|%[SpB%"xˑn{奊N!*~a-4ZD!VnDHH00w-UZF5=ІlVͯ=V0KoH Hf#]BLl V^MSHCVcc0ar9'ՍHJ/tل_?uCpځ+a2)n{f'~J3ʐ'm/%"M  ŐU@4`|DJouXHr/sd6CIp{&; XڴC<@b'b!q!dH|Bc9erjyHcli랗*(@pAd$Q*9XYj";ӄ s&U_ xò@xY|K ?Z0xLlO,mPaÄݐG JL4lD{σ_!z[k r[Z w=3Cx'X}6(`  _oړ'G9V=d[iHvL< ܏ `=duQ5c^Nϯ3uT&+1GE12-@2rP0 H+s`]S\/S>AAR"2w&,!WD88Ko٘ׯϐY-kscmA4ѷ)P6]ZP,旡23 `|sf$}/1\; 9;`mMV:Y$lX*f\Ui'WK3ᙀ;G+Yj?*^ioMtEZp©@i <Փ0W\DԃPm~H} 7lY, | @[/~0b=tilUGuԲΌhB9kJYjmaV|3 @h3rR9#W_X[޳N]1HxyڛElxűskF@S}^.B/&\L==jǰŰo>`RqBETBn"& Xy&с~7Օ^/1|WeW6*ɉY H0n8J830lJ]ixϬqj=fq⓳N}hkBGK_2IN4s/$M{QЊ04;8Y]P`v:muYB;Ei0"¶5[1Bw_UyKcm Vh]=eyr =e{VaEo"(-3'GEӳ8e˗,.KJ@WhQ`-X.cVlndLd#=q#9BJRaג,4h"Wb:IM}2c4 'u0B0?vY0L+6DzȫYk#V݃ A'FNsb2K[`BMgnbd fl'.#1Dԭ첾2H4qL?@{|8`Uyi_V,a[RB7eB&`@{(1ݛ7=wǥS񽷧 ($f6 eV2TMq5BI;,۔%yΰ2\J f s9'-MPlM{Q/&**~ښْ HC""l@ =LDQuJ r jA @KrlbmCN[nGӷ"=|D3;0;넙םZh"E0'<3%=gi0&P*Pbl]Xt \ sr4!OS6^.Gm>tD06/oEz x -}wk9BkYFƨӌԸXR(@L=xp Yc\*:n\op{m>[mbzʗ5~ܗl7.>lyTҮd1ED(%"`G%M%J8>Hߜf ׾` ò.$|1| R̃gk韙tD`X[i#pN L 9 !2IEyk^Dscٚ)w%Vr*=" !7aꡚC!0ݑFFo* .<ɉ`i2a}Yn.4ccg `pScu_.LcTO2x! K "^7׀)}[Ojaˣ H#/4aBK̏-sDjp,}f1&gAp Q$xj@X'H 6MJ4rLXm*ʾi+4r!!WR#(,YTbUTiDB|$.MԊse7l{3fY߬vٗ ikŽ%2U*+5CsFfvHoӟAiyuiW /{ 78~v4WM~4q{E'"7x>.D“Pɪ2?&+r\N^cp1}`t0@uW)F(( C)3iItGrtvb'sžW%L{)jY'$00!]AپY3ҷmϩG b&ɒ4jkW(&F.S͖ * MQ@0rz&Y(.Ek`ъEWooP_&Րԙ@|tЪ&^@@8=+I=XX j!5YLGf) }'{=G+.lLi590K(8pD@'[/[ #ʴ T֢]/$Sb ͕Z| yb׿{qΨצo#ͧg p)Wv.|}qѢ &%;689GilbMvԕL`;LDa_1r8Gj%|@ra0>~Ai8=XjZA~Y-7Xד0Rk*Kc|E@FZ+6ĐLu4RN|r pB$pm8N3pr6Ca ^8&D `HZ v2 h{3{#{t_g37XF% JyWDZKdH"30 bda s,oT0_Y{oHfL>`x~4b`bal%bi Xlgz,rT\9|*5@T{Y0TFiF8&t.b\5c-x[y5l 9 t** >80%8-`|n&\j !$йcb7ڐCñg;' u(+p[| ^'Q72I=Ryt%pe<ÜAX'Ţdq2|& 3AzDidpi}\*c\ Bxĵ/@Uv?z3hc2cA & BOHwpXY)3*;+1>piF1z&q(Q"±yzLH@ff58M;g'77cs6P Hށ@D `8+8 . i术0-)a)A$ށ-< A8e| ?RPK+A ` hhZY "Z 'M/};9r[3M3 <ڳRV͒sT?V[Zb#+}31)1E14rBxγm=Ys܈v?ϧ֚T "|DW@/ \\ora>` ;#j~ eȗX ob/XS`Uوr7* s X'ȏh&wjk=>,c8 X.=CȔJJb|f hPZ, %x>ndb,s>:%rw"hó8I c!Z'0^o&gu`W9>|In)CcNli0 Fmb`oQSVIb?K c"niTV .M`9&@*C,@aͶo? I&pJ1[vX 5clGNm8Yۀ8\ZB0if@d`b9Sp>\L nڮd,1 Ti^*kMZlyB}!Inb^,0Arە~2B`OF\"թf a3X:n:ƽ=؄z9q/[Z :A#\5%hۺBPV7a77я,_^MdPYWX`@¤F <<#K"we^G%t+qRILi>'x̱"J @ɚ!nKKK˺S=՘냶Bȼ6~+3OeBfVTv|0Ʋ/_o{7,C>0b#irxh}D ,/"5$l<XBW:S{f}S=4o+I^Ƀjy'BEbs-wâEZAfF%J~]L{}%X&P1WrDvR@ڭ54M%eΰؗ,_^yi_~͠IRQ#"e3G:-n94F۰̮*G-5\Qnl:K'T\!Q05p cH~0+ =sVQu͆>{BG}#eaO"<K_C_zem2ou2_pyƹ4͡ Bf09##K*+RFJQw^+R_ bHiZb Gr4\6ۿ^Y^ ǡܷ#^saK*]Xkb`6b}0_ ҿ:6,lLYhb5 W @o<oS9yu,]/U>a$ZڲIFeD@VcIJH,m#TĄm,Ytrq_ 6V|;awSOD9z± #Ҫ|rZ|QDf4vn-ĩ^X"VV V,oS},Јz7wCP8(WB>_q4Ʋ]|و w?o@?ef 6 6RB_bX0>c,T4;X Y)eXrΙn-zl8Ib Uzi.uȣBz5_ZfA ]~!.g H;a=n6_{ߏXY#*Gy{oŤgT^2s5%h1>i p|[ !ϓΧZgMi-K6Zf3Ui}ю`ѱG-uΧV,ԡaau8!qqykkO˂ͱ=NS7m_[zmPZA׭Bg{FPl Ea`UUz=ik|&B}Rmo(FQ;?I'lɇ*3HM##jUS/+ Ap@cOPS{'z z"G+ypYr=dξ+Ĭ4KˆjLLaqB E^t.88>4;FBtZ*jLN6vq|xsaUMO'Kٳ{n gPYZ<̆,*M[,%z6w336>!v J!||rA"#EP$s4DK>8R2=%(ގccGfȣI}me6>_7"DL@]!C0q$inD.X :bq~'=Hce53[jn &9ك$ƯmѯZϥ>'L֩>U<ʬ/q} X¹hHa .>f?ڴu@-/UЭD7ܨʉxjCp~+YCpj p!ۆO 1> VZm($acH( ǖU(=na#/=WVc[Q>"w6EzYN5`,R"*ATwx~r- q4w^`_ww~Q CXRERbN0P^\ńnpoB]P|`{^nh,xĦWXs)ʪa#P.bR k_qLM&fLKX{P67QgLkX`27_{ iloF7-uۣ*[3e4x9x #n`nHG3`'p ݬ-SY}H|p/Jz0eiO AcuʚU)/ lg Ě& 1?wJzUZrC0=T`CdzhU=2{$&-GہٲpQL@Cۊl2Wt\9&M +H OkN#B"#8@U3z#̼#OeϔOa/%”oϲ(1BC߽F a+WJ7hT*! p;z`늵}Z^U'Co#`D"D T2,6xFZ1``Y ,i&k҃F\fbL)Ԧ5ମ^1'g,N,o #vH9JkAYͅ_ͫwJ2wF\ |4p 1] p->\R*p ,B }3%Ļ0ԶXnf x] w`ƛė%=bnDZME]ȠR^'#mȯŀpuxbd@/UhV &; 2k3M%ڭ8XyNR#. y=v \ߏI_<BeS36>ƥ4x*M30#@g5P{!<۸9߿y?v?L[ҺUwBJj7$ ka܆^u؍8Q (NF+[Lj7t g\kF¢`  eL(%ks4Ls0*5P">(2ƒⳗPx2|YE:Ů^6|LQA)`!p><47hbÌ_" |!Lh'of+ \Ĭ61~@4͜~#x࣏h(>CY.'k*ٗ1Ի0W&,hoc;͍Vsj~Mi;>q_+Y,Ce.n'o7YbN9Yi^rL挐f w .澧CAã. s> Qnj '/AY@3$!45t_P1 n\=DTx6!2} k@|R)Ԟwxl]zkRC pkbKʯ3 }B B.͘c 1Mlvl,-!p}B6yN+R> IC`ZZUz⹧6ST#\#jB0?f*A8g\5;'E,H'Y.ZqYoNO6Mߓ) bQjrAkĪS"HK%j])tڡYpbUs<3ڠr]s:@Q kKW>baX ~DIZ]zo0,* 9bfъD @"8vq3!JUJF&l,_*6(˄"01tˠؖ=b b&JcT,{<~ pG`'WI)vS.R y L+ dbjZ7 b`1qfo#Al]nN9KYj!֣=)AdL+ o^xجhPN+Ií+"P38Rmb+`2kh횀DU{ժ#곞4@Tې64S{+iKZHG>ۄq10"|@(VaN'* AwiJ9ru9F7E?HQlA)[3ă֭bH)R3Z\.vL =L16 Db]e psm;D160G!87ac %D֍șHQJ(hkp.g&:" G Uiڭ ;{>ѱhP!N4y< 5wCgȡx%mɤ/ tRP~.ͥV(g]֓]$$/#EPB`NXpm20k&ƆUu7}K$bVr@-֙mg:ZP\}/FӇbE)LS!+xhGڒ^Yi PyVQw| v>O/e0ˢ3ۃ aXvz&½).A]3j5,+X z#o._H_wJm^g¬/Z; -NR vd|”0l5YU޲ɲ0~3;d6 T7MQ˩@Ò8)i0u^|'"2-cQ`3vh˺#t%N_`9?lʹ΍Ŧ#_Xj&J|Iioq- <0pǤ6g!li @%= *f ½)\pEx(2'එDB`i Hw2N71Y1 ^lPZ+ob `~Aui F210=]72ZVbrI\8OdRM=3YE ? P(w~0?Z|0!6M.J=6q]ZR|t<xp5_`\0}!b ~1ty4f'%Py4Q9K#"*ܜ3 NQ<-!ɒBx͆nm-N@4*Gz]xC0*u[WGx`t&_ZlA;\@8ۺm7 %y7Pz|aAtO5M̂/VA @? Z rv1Bc @ `IPgސRP r$9!3hBbh ` "l˳ <FͱK2UR5dȱBsoh rAztFp| , ? 뾳P|=9JF =ۨc~;`ۢ2}`b4hbv>x >,5M1ot`P`Y懺5S{x? _!؝=`ijxB;|ḩHe  RB`&r@ % %Dն?3_6)Hf:X/5z36~yP/3i BMx5[dhLiF ^A_H y~ͼWg#n,3>?4ՇqںfD#x362~d2b)m V„`V|jі4]X!Џ)h AcZ X>_ n6eVq7<(]; xwҁSs 30ڠy_+Ǝ@6 $!7"V0V+$<#r1 _ u\y{k!/:ċY鈧=bg,Kd\zh3`@s 1bb4WH`snzH(L3,z9ƔrE&sH?bk >egcl8`p<-7=+Qđ{|QQBpo`;Qx|Y>> C)9Ox-8/ \x'7Tnm#ZpͶ<4r|Z c5=04Pbr` ,DoL[ۘF$OvŴٗ4[(L_1..WGwbnEo}:p]mK/@N4 tp4BD/@ߟYdT AZI"Db~>,`} kő9d+ J4 ܋=ú§duWm`Cj{{{-UBĎDə2fN#$0s*vlV- ^ \FH0g4DE`O Ӯ^s!ͅp1 斺=QڔIf?BxP1B' j@~|uZ6ٙ^iTj\ᇋ;YCk֠p^8:+m<6=.K\C܊F,]xO5ZgYT`B1/@5W ɴJ{IWH+z?,R/Y6/ 'DZ=W/8]Qҥ-@fqNnHI*0J")Ml,^\]v5NY\{oٽb<Hm.Ue O|f(XK!G Όwjσݽm1a\?ՠzRXKD4б+p_ {EMl>^= E@M`Mc LOi]Z0'B'+D+60f*\ ; C4GOȌFG80!! VlQ/^tgv&tɰ7 ed~!b>hk _6>ު#ӲX{ -KҖ-fklv&QA?dSsS:P2chp'%FK%ff.!g/-gvS6hnFl KHXhuA i]OK`֜ZxA% ef+ 7{L-P{,O9"@>Tfʤ&]1-yTy%J"4 11$J LkHg{43BZECTZ]>ȞΈ"nj]cL@fnY&Ak$}SȂOpj ]C㯘Ȫ>Cbh/axz, * KxE3\R0¸4^땮mmǢNc]ؒσڭvױEx NS5.TCp Saj}+Ö\7Wɇ'Uq3p]„;.JAEl.,CY.p1 ASZ +{HyDZ=0RR&3Z(215as1+>`@o핮!}0U7OEHbٕ_ DD0!iE*7VF0? ndm}J4 <[uڗ¸o)?Sv(O%fTO5cdhcc\˖]ݪsӃe8>h]xx 4)J.)XBWh2R2Y AfHi[OIV;*6o̭A-4kl@6S.ć,1XccM!>Z <+`Xһo)jxd'(v/Tkߍ$*6ڒGRyG~1߻=X ^x JMz]6*͌E;++Ƃ\xx-X-*xJڮy9}e%>-Mّ~r ;Fgt0ico&!*U)N["D!GgGoN[2# VӁJ?潯k5-~NXbj恶`-sﯽLuu BT0%$͕=M.`U7ƛX Yi&WPy0ab` ɗ[=pLѴ -*PIwb>`#mޥ,FgB"O#h +\C)!R " `DY2pS6UI^d6~oDFñg]Bcoo ͠_Xdzduە ">HY"t—`7YI!Zsw1<%G+Fbfd;Rb^+x&}~#e4wIHk\R&3LoH}i/l#ѿ͔g0B̧ᘰ3B7>-OK ˱x Az#c>Y( jPJhŌ%^0*|f1hb\+ohYr q2\ITi5Da!/8 ֞CH 10]:hKړ@DwoMYbʪxaW'L{!s RPKw6BfZ͜9- (OiK73wKFg3sMD^<\05 'SMPaJ Ѻ ,uazXl,Z|>@]ɩ+ ,@ZDoI_Y+vEq0K@@~!3rj5P lĹݠ-V1SVU~; G-/{~y/CoZҞάUъmUcDA6-`K}q+bدy+7#սC~m648Df~@-A`!1iՀhȹu~lJ\ϒKLD)-jsPSݲ*;vcy8]u*n)Xc}\Im,vΫGT?̽sVm f.{# L 6+Il47pT\l^yhݲ6@&@F&vWto~R .C!/F6 ;HG(⪐? y]F\`|滥 h#iDUn,HϪ}Ћ9 3eqp9 ĭqy#|Uʎ1 ''A-hyrUzSL0obČ/`ߕU^ n=%]W::+ rG -]N@*x>i󺲤QG)w-dcc@&`^/ 6.Ć^(TSML~f*eYgn L#jsiۉ^f^fbǶu@(p/oOB{`V,~UTz'2zrc<f_咇<`\jBh3/fTA@`P f7n-ڰ%OyىۚMSsqRZD4L 'W*>3vYxtti@4hm+#`qr1lmW?x,e,/^! ] ) sRW!b!gʹR=6kbwz`OJi,_!AR6 XKgнx9~U'!A$'MM)lƃbj"l._ZO!^[w|4+Q䴙 ⳴p㖵\S?]O[҃1^;X5W:MCKiPuc<%—5Xk'8l,7a;掞I؝mO>>vjSZޚ5ZgcfRWnB2?1"l{gHbi'mv~?_~/SiôHtA%s.6RjDʽ4Q#! @1#PFf: 0ظk[78'u)\ wG>|Շ@3wrE_EE*V1 uqfv}1l}Vwv'nbܻ%-(o=S C?N)K:o\W,TۄX33FEhUǍeIiU&'wܻdJwGa2 3w|-;+ljS}4%*uH~rJcHoc2,_y{./0=_ID{F; i~һSk^[kU !FұG#4JbJ2͗(# ,q-.sF^~HT%O fbzD#) uX}zd<6-8=$o5;mabbe@z<vb'ĘTgq,rSU? N,KF)J|0E8^ p#0u= a1s' 9I&M.zx mdg񓡆)w z`/]ޗ6j7{`G#'}h=V>KK "Ucc)&LdX|_TVvd2'"j5c1BxP1ӆ,7ӎJ0ysOj6aJ"FG>Xc]7zj7w@,.+nͭM*ݢ5k@MN.hMp?ݏӪΣ}{o%@^ _ Q3HUN1g.4MrԠa{'d9rP? QB`ʥɳ-}>O,i?k.ԡ!>MKִ3:1"~QVL BV:,y|zvz2<3;l*DݸXɨguJAEú ί:MKF-CΥWF&E_EF'Y`Pܰ2KApgX9Lбxo> 7XጂbqJ5$\ ֐j7B/scA 0KE*<ir",f?i*HÌ\#OLɐ;n~\y8m!w@v5H0X?i|]R o0{ȑ6X0Sh.Uݍ:c)YM_eINO /KL;oaM|rn"tnWzY܍s1һMOllד=M{"ʪR 1*f~e.;۠t>}BB(6ܳBA5+\iIh}^wk*p̲~ZYf@Gz27_L'|Y%0 B74M0 KMvٝ3wHJ<6l^C*3= RY4mн#Mì5>DEX=|j4=\݌nse73ZOg ^=vaOЯi­Pg`6@ &sa˓ƓYs,VOlQAF/`-MEƥƨf8sQj2 7piK@Q Ch k?;6Њ_Yt/_=ie8nm@?5${΢Hy,\:?_\Z3k˽kf>f2H5/zEf>" 3qi &h ot/®0I\s Lw ˭K^.32kݿI37o-\:-c8+bMFvo*0jzqF!~pwD:`AMZTz`bV  o H4\Kk-h+P;@\]) AE纴 (|UA덨_!kSkAȋm5 >`b齥Q1EV5sR"bc~O=nsI*!E/EXœ~ j ċ01U_Kbh41 @)K,v?LMD4m9Tˬ>sڑnaM.ӿOog,z nfmx\c|8[,YNk"+,GG9;3:8U;~b MZ%ɡD#. / F4D(cP@A2-@YVoyc>[ن=ٚ u~`M MI~>DK1c)SGs"e%d͟&Lb2>VC09)/rt$UV;& 4퀝FXRL8U dCN+C6C9QIڀ!"7G`.-!o(ʯZ)M*= x&15 O L`… ߓ3JZ0D@7*)1 S;kzLBaD rԃ,c?eYpu: Υ v |qO@ J5f-n;C%jh؟{HYA|OA gZ/A#pHpC`+0fa09Oȴ#݇_=Wm~}d6gn?w̎ooK+~@hIDz:r]\"\)4`uάw%Fa|nlMy.p9 H`6÷F;脾"lӲfzEش+yB0bW|QjUIA`\Z"&]|vX*~59]G~n,%JPQjPt1+[i 8;ۙ_S}׽"6P{P<tw[t`Jp{Fd`F w4vRu 25 9A@= pC"pKE$BrYЭsT# V/B5ݍ#be$g-(fb0ܛy3B?Dnc.#(3aтF&?mƃ1X A|³6.GGqޜG g)ݖ= fqcut?!0tGW77?3*Pn ΢z%,A+0!!MPe4bd>\Pfɼ>o/ג#09 iV 6g!ab .͹OX9l/dgລ&XhlD k҆@^s*#TZUF MoR n5T5=\Y3^h-qҳOBOfD,h[ %'if6g8iavB`|\|s&Wa"J2-Qn^7BkF:bf\ *d,s,"N[ Jq탩5xH T CMV6ԝo0|`惩ăp&(_ʕJl!i&C?eVhAB6ʁv Ed%#pcn6z2"z4dI9D%1 {4=\G4K La^ > nFj B sTՠ o +^ vF| FQ"aAƒ> )0-MsGk'7@oo#/u7AM6gO-+/ gC*3V(;f@lOM2bG[a[ ?Cx2gȵKЏBj}-9ߟnL"Xq,nG`b·"vd)7WQ; ZC yD fvqѕLjZyЍ兠oĄ?Cφˤ+ BfLĚ`2f3s4Qii3|-VŇD\nɸńjCHV`n-҃)K3Ʊ1qBvڙ;٫ f֋Z՘C3_ ;m,ˮVkͶk+n owN;gٚ:턮X> U L X*Ecp(nbfibMyp=XY hgUxF?5J- dFv+2ǟ$- Ȕ>#b.ӀBL:RtZAd6>@!3E:{'C,}o$$]W1>&X`b-`v)-a1#8oz[kG<68~lHi x)b|7]Ѐs`}iqmCk_1 h=7+B_0KhE$Q!+fiX$E"8KE9l"Yqm@n =A٩˵#+.?FQM, 㘗UHHcc>YؘK.}h m{¶6reB@1 &P=-2" ċ{ K@,2.ȨPGYw %U pX0*Zؙ3V1;cy`0l] qs||0]ZK+jfhzxBHDA 94a#<-+`gR,}y8𓀳aۃ a$$w`&{A?f/W4lE*-1d .pYو(b$YB}d_v>i[dX~LX'Ů Nx}cG_fd] a-|^J v˷3%BsP#EA?oZ'ܿ Fxãm2PB{3><#%*uFdA#,앤kt]0#Uxy8?"&dŴfsv]A1Y;Y@= Jc<ğs)օشWW.:kuS1M`k*ux$! S֠hW<̑n"xSlb_Y]ȡ|6̴@[΃X5ႸHb)ެw!+ٻCdy}L01{izфJ+Y#$@/1mOߢi[{lG̖g``C h:O<f/Sk? * '1;|\m5 pRȉZBЕ?9& z~ b`fr %ԫij;Y{r @>\NI5y! x`sdŠҁ |t̟lL|k3 H'89LLlN.Q!L"r0;I횤eߐŰ}ў'fk矺G `2B!#mX/̈́-66"_?ˁAtsYPH2Á+5brQ&4K{Qn)RI#t&1,$E`JǂR>9XϏy:XI/+&Py:Ihzӂ[b5NXq Lhx;kŇ!_;9W뭘З2v\ )DlA`ҢHkrUa@=VS_KsXse5X:5+T"R8R~fE>?y1j%|'%̹}Ưp1ZA[ާ`yT.|$;_,Tz2u!A]L MG!l Pf q]1Yó 4ӽ IOH*6]o|P7N Kͻs4D>C8͚L=B@!cl15eTS)P0O; dh9)X[z8 ntPt>mQ CԘ+Ihg)]DPlNkͫ|΅yHPh[CH齑RXq!zKxb\6ڕ% q-/omBPe0ETgFsElH4*Z YX, 1j4쯤TLLD!hRx>O1bXޭ͗MTP*ɗShyfOzP5!C& `eA@j0:1ZA.[Q,Zs.!n皧JM !7}վ>c{iԱ-5-c oafxryd˙ь6~ dk2`RA_&>_ ֚6ۧMQxtr.w,R#ܽx[zQNK==W{cA(,L- hZx+ç.mzaBh2#&B>wD$CMz$^TrO9HaԴԆ !H[ %a4E={Rڪ/ъ^"r{H o |BF˽ox>ڣFܟw'U@F ? L,;jw40,2@[iMb*;ܫx&hp c)$7]pLCoP68g CЇ|~MQPDD̵wJ+[0=}*@`[|>{Iax4_()GC}iEBss[)߯V1'eZ]H]<2& ?sXy&1'<1ͅy$IPy/v 5awo §XPaKSꋷF;'sTr/m@ib˲ VH*ͺHjhC=H$ry _ 9m)v/Q P s ~ ejWI@E"~ 4 24#h?&8wLkS/aɝ eZ$xlO?oeĿb4'#j (XUYXjh`8 GHTbڝ!ܴD_P4WWZΆPx,9fBZ2aMMؔڗhC҄+vú  ·оT]wvצR/AKz. {kQ`cA@˒Lo =$5YFsf@̔s!{Y_6;`Z!^k " |UM ;ҧJl󦴂pD~N0u$cF!®Y\8=#]:| rua1-+kw&>;1H87K=PIDz1N>|Y% FN gRkZ6n|X_0o!u)`R ο+k0=D ߟ#"|+lig @^BB- ЇT(D ^z/$6߼zN~Usyqck5>/\f1]Dv7e@QJvMضpYXm!.|TVo`֊, 5D?*UnIg[>,J;""IDATaR˝њ~|9b9X!T3p|"t7F7Zh񡖇/,"Y;˿ wTPS \7&}9zBm3{7S"ji[fp`dH%p'SCX;zƾ/N  O!>植'{ͅW)J =fWXg'c1HwB+N ڪ> 744Z(pkh{.WM?JH,NDifApgryܑuac$2-UW3_5 % |Ab d2Č0fB $8ƸEMsDЅvI Rm&9kb{j)6 +81GhcTpXA/T21)U'XXb(}ׇC[8QNLܹLx6HWPei cL6կZLF|K[u65e4#!stH$?IlȏXJ; >)M<)`Lk5S n A(J;r% Z&uЎRAP䒈"#qB%2kaK[{!Vb滕"\C\iqA,FXM 3MVdI=fu»~()!W DSڴZ=DC s47p|6#0~ ߓ}/hdƌڶ׾`zIAmO03e58¸o_uB CYDc>>E"c7C Й.EIu1Gv SFkpnׇ;O&!m ޘ.!ӊ~pq_]wYZ&1zk{w nfЀv< m)@['vHG~>PEТY-] _LT_yeJTV$7U&XHN\>I Hm׾a,g+!\KB$_x5lz%ۓsy<2|qAU>Ik|T$4:[ Wʟvd Cǩo&mpJp Xf ^O/=aغђXIki+~Bhi|SrQ!KӶab 4??DJO1ַ$r8cЎz\Tͬ^>[KnL?3i"31hiքά_mʏ!҅$ &8Z`4w%j#s%E7s71vӾ&2„&7Lwxa87 G+ڧqizLoÌbU\VIm>+՛? (#> 7t`ba nUD" ||eL;$q=0g[ D$6AcA7m9WJxo{,&A,巡0?ẋVӱaU X Ww6唆n-mug;BЙX[fٖ{D߹s[zN;;:!BhWU)ZP={62AI|0 *Qcyb) NoBq!6b"Hk (sOFcYh_~ jI [쭌~Du˙~*4xܞÁWxl iM&|+pDb3s42~ŊCKo--`;"\nJOV)ߛ~_nlVx|(I;o-_st BOXX@V+0!ĔevaT 1eT3Mt%Ah'rGvKk9t S"L+2tȐ ։tkbF<QweL~-kkpƌB6S_ T}O/-WpJMMooG]J ^dK90Q0 `(6A71RY EHhx  Bcb2'gfV*C@MKs' U¥IN-f/ q"GL{HW_kaxw jU'&D+pg)yb CY; Vj4[i:ify. F%?'B ږf%sQ/QYӱi#SFK  ^ Pxoscl !J{.Ǯ1LȿJW +mӊ&!xH)>LxSiZ Qw"vZ N^hנH%φ~= :>_ T_{M+X<hG F? S>yqZKWQat/Sd0m#Fhhq/o#d1.c8bAezT̞,!X kA73oR^,m0#Pʊ+nFUEMryvcJ<;WpSAޙʕΣa=P"gmS㰺YiqށH4 1 0f: =Q4&\vBnnTOCtI2 _nlC**aɰxJ-yw YV4/l8&bXm4uMpMD!o^T{KbVsʟPY b u&pnw;*VcCWm -Y t'-0rI]'vF~]" |)RDeޯfR9+v$}m%2-4AS 2>?Si6 ,nX -Y:saXuVZw5gxxc{ow#Qu7L|mU@ë`0Kכy |m W#SX,Te\p"TK9n"x7 @>,o?&pnb6IМxbJY8[tivm7!TX,MX ~b}/&lg,"Cxpo*rt#ҿk6/Vq^ nx9,U Tz!.UڽkW ZQչbísZšzRYkd+- $(IaP$3 QjݍZQ=:[YP-.fAsZ++v䊺cX&-pH&4} I&hx0L+gU y`7-D&p{ԵdBd+>Q^b" +")A5< Jx;iBroܮTcBq`qOH8v߿-W}i{a߲҄aFgdu܀nڎr|B zC,/d2ͤ y*,4D\ԟB7-7ڄx湒Y9 !J z'b:k1ya-eT܁6LA1~~#p0&끅 ןim\1}Wm' e^r4*td V ,֚jo);QH:ߣ) 4ףOd]}|kL3HD9w[1߈+q1o "}`2Ha^0@74f~&dהڗfJ=W`JJz.NJw\3aFx5Y`i^0S2ǥ^O.=oue/~ue/>;XbTĴ6_/z AK6%*Cu`n!~-{umhʳd/Qh0f2ݪ7Rk$( Nun}10 X݇ aJ k+!Mj^}1lHK0TMWyLMgR"V4քkf5Ơ !]!GPCkDZ.ŋ.%p@* R+[Z{w^Jꂷu^~) hPT9 !@9ߔQ t&ZRBHFwS_.%ꑀYe>/ oovڠi6ˠ\/A =wbbLh1~y! Z?v# ,KBVkcG~7#"Y>Y1΋ub5O›5n0@QŽ$9 Cr]ZX7BVH҈s+4#pP*مa(36v|Pkvd@hms=R b/fhZ@j]kE`~,)fh~т̳`k&PovуEZ`X 8enY*T">6B-^ ]Ѝ=  &#yw˽ƌdb~HY H{$3 bQ? "`oȾg 38"XqYi3$]}XDtiN덏NOcNlm]:p%@ ۜ'pfamUiجb`aZí{?1gk] Mdk,Uh&0e@=Dzl!b{,/D M M45]0ye3R=CPQ!FBsg,4,z?BE2=ut~%>C^x` j3*=wYݍ[BbuoEةP!`0t2 _A_gS>V KАivYŁ<\/Ƭgzwc"D>i$MX!i$}@HBGa$ߖ8r79r}ߵKc_4]Zih .\s0n!̧1\y/M`s-,õLj{AaϠ]7Ɖj|4jIn;eadz;˶|+C\LP{}bwIO)"!Y"B"ξݓXD+JůM?JĤurj1H©*F |X^|{mѺ)! %}{#pǧh(@hвmL+1lEj򸄒 ifX+@L6vFzWv^𮎞.k(h|N 1f\^T Dq)|?Zb̔#u~#b4/zkywUt&s2c5_!7&bDŽ.<TTq =qY *Pv 1l1xUUpWږWbG8V[].-lhy' @NNbm`njl!4چ>fk"1Lڽ&O[*b6"erlւ;e/^Uh{ +Ncs7Z` 54Zƌ:Osd HNPT~_ehE_+yK }cW"89%=LNv#.t1h]d8JЭim7gq~Qs"^!]ezΌ=Ն0ߣ/x&'ḟ| ԒM~5$Bij~O!EhϜlՄVU5i{v 1 b!fyW+n]o̺קܣOAlwK|jwe3 -b=/5bo- r0 V‘0C1BR?(.Z? SBCTۺG @ yv}[|!y᪈ȗ!MX~Wr6:]@H1״~qL*V b דxbo+O`צ]D Qjjp",1\&*Gs><W ,xhم;Qgi, 6vMDٴ|+w&U~*?>t_sg?;Uz`Nj''[G5 \VոkFYؓc{X  aLβDZA9v/W>?OÜkNLiMw#QԿYxA|;ޫ A^6 7C(f"4&ϓTϼV`83I gM+lw,R/Α7 oɊמÞ)x2`[ķY[=ВGPom]PfSݻK@*J"* N*:׺ ":A"UA\ MD6>=\Y=}O{~dOk)8Y+-.x]|'ٶx5ӱ2b?t|Ia=,l[M9jCE.ל}53:x}惤TtJ*%.RV|=GtMZ:K&фn{ ܈N=GEYn*^ՌSVS\2pJ2O;f6ZZagL?p\_$zbK~f w?HEәZsEuSVTϩ 0:*k>ֽHX,IQb)l/s2SFDZmƙ}OQR/+s(G*s_&8sI=y-QxO RIV}#T3FR{hd9 :}MݷOgʿ@@@@@@@@@@@@@@@@@@@@@@OU(IENDB`ic04SARGB    0>. )(:t/.zz>@HnpEMzy' x@화AGCu`YqCO}1-w%ӵ 鈁 Uq{Y %:I, ;@AIIG @ +' & %$"!'3+ :Zg]o`fuvkw~sUhxzzfvժmlyzpXXkxujffWn{fjvud{}ghgҒbì iT~ ~D |8{ 7Շic14PNG  IHDRxsRGBDeXIfMM*i @IDATxɒ]G'F2SgwKZ2Yj#3YV2z- zz6jReveUfr@A@D;č{cDns;쟻O&=uttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt<,M&+\P_/-zAL|Z>C?~G?&Z泞k%K;LNMnOVwƮyd5-'!Ϟ:f:*RO/BGفxtg6^(kCe^[p^;3/~-Q$ԉɽV'M^h@]ޜπ0I{aV潘qޝ^8_o ^gW~UɼuRb^t_fyn}]Vf :/i̳5fF;t5 DkyF:MExuq}0!'^-g5EKՃK| ޿o;OP?ac/E\_wy!:z}Q.৶_Th~ut jz(VZucL qeo3'y1waWH|{@W }fİ:'AvKO]8/wAm\rwG+/kE?YѶ ϻ#N@05d\nL{¯7 P kB0xV99W@TJz6':C9 Esx zz4y1]єq}gwܷgQo<9Su0N-.#WÂ{*Sg%ތlɷ9j]haY_9O~1 aǢhu3NAy8 o΃>]xد:s44aLB=+}  ["2'vNr(!ў?ֿE2t>d ;սG )A9x}0w376ݘ\ {Sܞ;zt( pPeYzC 81y3@l[!ɻ7 ܷRd\ ;-oJz5Qp~)`#46('o+C73̩AJAl%+%wvL$$xM sz7W2nɧɾNMDR>R #e0ɹ-ux/dN}2xjN`7יBSy£]- =Jxgѳ<㹻.wпy$m?D[Nim*= Ylc6#K@7|N|_7RĽy'_ˇ/PRy^ *ֶ19WbM9r<.ֿ^ x0=\ #<}bpTb(,K(o俨!z)7#(Ƹ{p@'kjl:+Ŷ[rߦ>T%+O*(J(ʡ ?6g95>i/&*IDƂiKշG SntRq?āʵDCWRLW蚕4':,JE +^3:l(p@' }2!Xo׿ɧG\'9N˝P&4¥П!Z\cp}PC˃2|m[O um88Z33ojus.eT kĘSr9HiI-Jw7{k5yW̜QB1 D)X^1.ķ};y&)}/za^5ϟǶĊ[yie7-]+kX J[Gxhfor.U-B~ ca~9w*>liI<2 m `qcׯYr Ф&A,|gS `E)8D˵A(Oj@۵»$KH㩹R0A<1 OsKma`k-P{q6y24m8{z9 'NBG5ggEʭ'Ԓ(JrTh\r>/)Yh%&mj7rbf}0E|Fڤ)߂~ 54oPʠ+ө4VJ@.Vư+|BXM$nЈ׃?˷Vcܻ쥔1PHyr|99D&ѹ\_0b ]%۳=sXlǭ/kHhF2p D?mpZA1 sy+Q@8-n\= `]4RiғNLMYIY-ͧnE)v|4,92 9 p,K@qRb54e~V#ďNޮYBXRN,2qEVP60)#-1kSV_{yE0N;9j~ h}o FXbn17Ҕ^i5-PbPP8e y]J&'Fټ 7Z_M(m756UO/ 6B;)oė#gd-}1JOUʩ%ςQ^sv4+w1iO gC;è7'j$AھXٌb4VZMAwj1rj*kB'luw;c1o|PʕdȜ|$'_AM>/t7lC T=m}2?Sy>LܳB,j RP6 ֪4Fc3R=, \f_ b+B-={y"*YV7(yv6i~i; gZ67~zQkKA=B VWթ袑TZ<]ru§WTԌ"(ebZt[ea)T!*zzy /-~*Vv&x!{2?[bWzux&[F62DK߇mLt)K%@)8j=c,ʀXz)״f,2˭XZXqկw%qcv G-~)RuMs=)e1[|ZA:`gQˬT{ N 3)jr-Jx>kaC-8iY-D_[21yɅD @;7b>%F9\(fSvBż4D7_V"uQ@( |8LWrÈr;q,~&NZ3 PʎՊS.S0nQRfIqj<}|o5iVrdr֦\E}'@U.X\oiaq n T ( ~o*y ҳNĵ'a<0+.Voyj5`羴s@`>^3gT/T\p;ʻ`Imzh\;cc{TTe/ZS8k)A6K10s)#N_bswО>Կ^E {; P;[i*!??a S.c2{{ƈ:IN@ #P>9ڧa곾+xV<ɒ]AHR){ )}o@c?Y{C:|?Ԗ%.|ɦT,`uroEGW( l;btZPNEkcKRX2O^RjtQ1 LhwXMtRzzy L)"N zVçEMil{*jnK޷'t 4q4~ ye>sg 6`s'ϕۡ ]Ղm%d] O zehJc= {9N%kUJsKc^銒u+K~Y+U6||mS!"PP~] [tF@87};9:o4*G0BU^.v$H@) K{?{ Ĝc[jGo{h9&yM[&H$f(6k{8lD'aǕ|FEAݴEp];bpS GW]*fE2BIoF-}CeM]qԲF`ABTgV33Q kR%)Tut!`Nh`[V^Lw1>>9\JZPhKHK^ݾ9QZř)T]؎Јuks9~.}M@S2?JhU)cnmd< }X!ZC>AO^V>w^ _ WvʻVPln7`W{LuV^!7hP&y$W2RljްE=z_kޯw$4֪W hoBq>Nz|}և:{oeBky! sL]eTrrNqa`lgbkwJk#9}QOG(b,'eA@ۊrV߼"[X,{ Cfx,tiK0;ZL;% 2*n.6yeTh@(t`=0ʥ$ꛈB/ɝw9$F;Uf˲ms0^ys0ZG& (WKk+U T/_˯IJ%O,q׳uw &lkC)DIKB!p1*PFU`K`Wh%ɜg+"^dS9G@먤g\rg1+SS|ڕ[ `, wښ'No'#3j+hgЎ8Ka:A2FEHOq˭܌B|; p &Q6m۝042x}YMn (.Y Ƴ;-cB^ezJ^LoKٞZ۟vuU1ͣym{T^vPEmnYԱ$kN B6u->*[-u75`)k V1JS9Χ-.͔q缑(zjӫnuL&((js.nY"{ %Obebw1t:P0XYrҖwTTLF*Gľ~5A}2u~L=X0[٫gQ",| ԠwVRDEL{}4D= c_pNn6`%6mv[hݒ|\"ͺ? ;u谭"h4cm<+-= C[1U˛Վ3mnǛ[nbe+Q m)Ay/i @bÂv7@Q҅j. gO7QF2}Qq6{ . ήYηC#Q+Gy @jL|7?gr|Jӑg Ԉ]+&BRZ%05mhBṡfQG` \ȐR`Y?ҚN1ωh[ljXc0Y}ULO,+(,~l\Xhf?dq3b 1SEf4J;\Lv)}&:m>Q;r'Bp-0i20HZ~n۵ܜ[y #]H"$c<ihjQM/_W-Nk5<{'SӒZzv99nwm~ITQHO°4^\N>tzL|=\E9F@!RflR¶` l+7{$b9սhrK˩ ږt4;% ϖf\h[νC>"GKW 7c-Lfc x\)oVǽ-sO -ϬtN)wB ] N]@u؞hX34z[a<s3%vDT!Yb%^:1mǓީmHNokKdsxzD{Y1mm]f߇ vZmH=Vh?-$.knhMt3&хX4tR6JUju$8 '/_N R)!t%tKSz+i7nClam+ /p45C~1Z֖N$ͳ9ȣl Spy~9+ޓ$dDp"Zk]z"ۈ-_Xmf4Ls15K,g8EZ@h7Ky+oFYv+׌K=*wk83cx b~j c!x Lh'hTwr-m-޿bW# }DAF!qO Nr<ĻikЧa,M_]MIo>NPƞz7sZFo粚Z6=u~ P*7'zjُ%vpw;1n*ƲP^)vKoL&|m0$r[tv ,E[>s{oĒxm{mI02Yw 0|ΔPǵȽy  }6vu5Ճd%jbp&ct\<R *v 7^1Sdc?MCV"$eQ 1h-&,Z5`囘6{5o\T{:WWh^"xm~[Opc닮Y&qX #ƽqڞV1;/ CFELۄa[Q^#R7J2#oЕ\ϭ=O d @(;j7HqM 8炋k)7Sfy *<8hgI\$9~~3O8C~;Ҳ'B?qٓY󀦜a8#ƣ*p7}+uiT !IswҞՉ}Z`]45CPEmh@6W3S;(&@o ET, X2x>4ܨ =<j gX4uJ"1}YIf({xX+YMgyPLXSoes>؋N{[D )@<"ze8mNJ]L]]淕4ܛRni+Sјqώ|zXJ7)< }t!@mxfjJ>_E.ٛ6K$C1/ܯɁ1v*5QүJQIYZ/e 0ЋfM;OHC򸊽3,]M/0+z)Fd>>tn76ݘԸOQTxeÆrV3w͚T} ы4R078Ni0MjH́ʅU6-Μׁ;~l8WjoZY9i\g>t`U) > f+kJ*/Z#ȯrWh*- 4X; g1>Lt~[*DYUJM)Qf̫bǗkJ@om}Jܠiu5%j"8ʿ bKHgyd+ǷF|~3Ѹ)5eU>Zpi@9<>{V?(ڵJ@f`iF/\JoBAUJT SRзn]78ʽWTK ` pldtfXeߔn4^PkB`E \)F^IQƎnMBv慜3q?cJvbSM ܮ/r=!{:4 X_<3app|#ki9K6ctف^ RVKS7'OYhAƇ`q7HkU 0b^/)8!L Vo>HYl_Ѻ} 3K#~Qa2v cE S[ ~'=nx*/YA~>xߩVwO>i:x>L;QP!HUPS'SHm* ) iaKaLѠw͢^PFI-p"YgKi/R?/6=8"V\a*kOwQkA7`lRCzĴ Ս0<'Y+00P&1;j')?aU /p4"a!eLBeTRFX hR Cr"zbbMPu~?tV) q<ŗfۇe3n_p0}TrH*\1%9YW8ա,@MY4hKyF#)EDm/QȢf+ r,e:@19n-a} j[fߏeulؽ*hرY?gcVFZ .`ֆr-q[Nkֹ~@la۶a_.kV)h{3w(6j; ) ^5X/8e}K@iګ_7Uj(5(jX0{SnA٠p̪J Y14?uO/'9nEy+Q% `O ШϵRǨ~@X폶$ WI2A^>ԴMPm+sk)tb~eh㋴1Gw7K)ȬH#d}'o6JYڪhKOQ1T'&uL=&PMJhǑouΤt3n_wndKAΥ<#hBPx6=u*V*Y墵WCggBvԘ++4y?dW͈N^L7tCN|ڝ 8|l)_s57 &<9Qa[pS 1JM3)qTgnXN.i(o$y@{]^O=\6k̈́ 1!ʡ1٨/"#}|Wr#m&e0g3_/r'&Qủ˷ekKu"N!lg+{z&a4vN71GX:$oRc)v4/> ð_-*jt:loI_ˡ 3{==?KN[8!ʮP7cӡw'2ܕJ(nmS~= ^ ~SP ˶g+sЉ'oRGg2ƴfܴF>Q+^baJ=a{'ay]L-rdt+zK{အVM@Wy Ԍ֟7SߊF90}9{t p$i0\pQlgv?\CoPKpJB{GC;Sݝɬ0TG%zbR=`,nZ] ?a,,B)fш)!~o[&$ΧwA4miFZJ6vz^gkprgҟ]x6oEŔo'o}А d{ ؚh2}%a9e-Y}$jĦ;BcREK}7u79Bِޚ걋< Éx#k}l#W/zmukMOJhiN)PmLwœ3ȗjR`7 ME3/ޗKfQk$b@:kf+_Ngؘ%ǤIiژ?Pq~fiUQc43KNj5z? M`[?q|휬'u @8Tɮb+[%4{nQ.hG5sg ˁXF0gs)c Za,PR^0j!R~Dᡤ`H`xѕ sY`TkټNz895s4 _ 0pJhjȨÝKxq8b,~X UrTZ};5};4ew-(:iM[J'0u鏝nЫRhL;;г B~꒫ڶBH N ?NҠn);hXeZ-Gn!F/:mbטyr:[ f4W/GۋlrTPa@c&h^ҫiTozheAi:-OK\~_/{:T0c֯mފ`vDJܕ'(!(`cm&툏s%\53iL8 [u1  :P>i >fon=q'L?VyХv-վyԮsR1RK]i ,ƍy cu"YX_]I+<[yJ ^Nȱ#|O71w!tA)W]Du-9 0yq|^0yJ4ur =0> %2mhPRhLρwO=@9r ʩbr:,s8MQLub:0r_|8UܱlS^g$ٸ-<`_oHBd7" G #!1$ c<.~B@/vY Ϭ2$`r]DhyC}7n»)(&~KJ1uiެg+C_Y$c1.IT&eC[ '9L7+{g 伖OOky2e*;A#pҊ.Ghopg{V9I16A™UΚ5%G[<B _EPM @p]*kSDr9$u gNцܒ1U.c);.D.o<ϺuXlc|+FwN32s)w`ʽr*u)AAtZ$/K)!(bH?}߀E:'uH!E^#X /Y"Y_Dw_ȇaa^-/j>oޭ9)+3.JZ Eɝg%/Funq-*1N@`e`_$46L[xSߢELr3%_p͏&"|z:L03Kf̊;QN&y\DWZx6%+beF? JbD}0GNyx0=vya4V¼+#KH%4vc&;mQl2>cŋL,'mb jh7SFBT8鈹Z _iui}72PӖN h=/D(m̽=; !Dʴ|J&iu4=)SG!j SXiZ(6w*>0%9ͣ)IxT>spukjՏq-6 +&6N,4G)<'^z4l&|pi+ۯw|2s:x;A7mj"WoxlQ :_:Kx"RKx(nu:Y7Y2v!|eGW7f[?m(S#19Tm^@u@D_JWۺe e ǏiOd^-j1Vg.NMs^*Fh_]Zݯv7~/[}5'JnOl {o`U"Wyq5+M5Ec俿,%\d ND=#0)!e쥤G'hr_&Tƃٵ(V^F#Y\ޚ45SN0ի}+ ܤ1~L@v< PCE&i'}QHMTJ­_ck$8#_֙_#Y9ֹfB68isEL|:m^^[F{AY5_luɕ~MQEfJjג -/C,m\M0>4"P+zsLE |c:$ڑ6_lkiZ;E4X/0ZE*`b}mຄ+6V=3Xl5cp4&m&}hZ=+Nˆ܀}JIPSs|ZW q!&,ȇi ``T:gUA@v,v6.*:Nuv}Ÿ6j7OYV٘*C<[HxY ;%%c-/jSV&^]􍒫&J eR w]$Frkajc ׮¯R[Tlڸ Y1_cB_Z ;=39NJq)FY| jp!-1UgyJ \1bQ2ڲ46I&tfgTm"bW?sl7mT4)Bn9Х_pLW<$xdrX$'!G|bOgN4mF0+ VR͘Ii%d]70ߍVf0$hdҼ7XD1e@*:}( R6m/{1Ó}js )?T1kS^r6] Bο\ ,%Vsewxms;cJ)p=甚 A(k l>~V ybkj9hWX ߢ 㝗OxФvNVax_ƥƇ>7+(瓥sǔZ_Z ش y"t<> BT L 5okJneћ~yТDNPţ :[2VyiZt'h!?wM7EÅvD?p&^h?KJ]2ʰPѭ>}+S?ee6)o/2`bO>U]A.gXf#m2@g#ږ\Q+0REgrR P˘ҳX<C:@`bD)R@j٥bd%b=D8- S:Ϋ]m1i_>`H01\$}x 8"R )&E!>`J1=D 6Ÿy>C5|)vbM_ SMb={/ʴOa)(.a f=}ō%)Vg}i-nO8/SWp|ЖF j ʝ>CYI1*Ɖ",x "xyb%@OYOMXgYx-(9[%QI%8B!H-9ڃ`6|J8IㅧUӴ7U@d>]u2l;` >kt:F?!_Rn5k~97cڪ|k^+ %*6c/sLoAh2\(Z6J'*Ш(v{-9y6vgṀu_c{H(o-ZI(2F_0'zڴ4aN\dG6>G<ŝi*;a<p}olrֈ%(Tue=8 ^WhiƆQ[l kNkC- SǜotO)T9]X4eShSFʃQz3&\Aq )Cގ vjG-3KJt Yh^0dټW gx2i@/"xnas2 -S -µf\j%CsPhlw?+ڊOD1)EO]G}kHlތxZrL.ז%[:W)wTQW%(Q5\30v Pib\4:Mɶ !H7J+<[v_?nWS{147e*FGhB ;%r懲t!D,>bQ籼[bgBx[?{^x֘ K,iz^‘yЃNTw) fTAb+?>-y_L=f` ؒgn2PQ2uk] ѽc\>kCqkqjIL|t `V\ėcήmj,C`sq[@,|jx(=gT[}pTr@5'w!ļcim6e4Zu]-IZ໌ZR(uj`yŐΣ~^k% JPMmY}(eC qCG${-Er4ts±Ww`$fɪk})02!% :jȑ<"Q$Ll %0@AA;,:[;ެi] w? Jn nC<` vJ, CuR\ڊU2=Sfܗ JtB arWZvXVKC)hD*{54 MX_dtAKŎ;b#ypZr/>ʇl gfKY˨Q6(o N Rx7m.A_1 d /r-TՏ e~ߔ[qp6M1}t`&g 5hS}ܩ=aضhb~}[0p#R ;w*%{ D;E`܈,vD}bEA VC̣iWyߍuDټےrIJHH;xX׼Z-A} }s LGIke}vnve3`4y*'xO5f:zWG(1&2Չdcv2|p?3΋O! O6BNLxbaV KT]=^f՘riLwSDàLe0|k _G8d0㯜> ή᭚ 0&(˥n~pEXp.Q]c CSYF11P@-A-:ßz) zWb+dTF;aRY(1`thg\Ƶ:y|,Qԧ $ 36NSz:hס 8;NT ~o<\OBXy=%%rg> I|5 NqE;fn%ٝ"E^MmP>WlR<[7`Wr0qO-4y4Uz 軀A?I]{ـF̖t~%[X HyMp2w}crE4v:A=`*0ZH }*tmY~vOM X`]"$p A pPqϘeL"H@p7N ?S'Ocx̩Oy +ڗgՂ-EgRŰȃ ´fH.ZݲkEmGs wqr)]>9Y:WAU"b">E{5-f*2E]+C_NAtȺLuI5F~;z ~LT(A@5ͼe 态1?6);&GfWL7@l͋Xi$%)ijn`wO._TØPnιsKqOp/C֠;ML{xItyǦz |9<.5N(NV=Mt{BȄ$܂(o=8C^L//AOY.B_aotǺ|Wm6QX[lO9 wzV^ƅNHioBbXy1b.c>Mcc ¬( ͂0r"Vz^{аAR#?ɶk +yq֩|ߵ\b@L>vnRUp֝­)b e >F4Vb``R+ u>⟗x(Meu $H1HE{׀PM(B(E/,/WJ6$}/ AD`RKl<:^J~4M?%-nD@5eE &MaFnS*G>rR_\?}4>NdWw4[ذȮKKۼcsGv~3k[yOhoVXWzlA؝z>5\G!'G3RbQaצ0! \>AU`uI(55NU;esw}5!!IG 3aRbh]Ob>s`"܇s>VY~\>,mp#bX&4oJ ˣcHj=1-Akֽ6_ZxЄԏf!}Tkv^Ըf0&F+P#v}:> ~mLa^/dy1u֧fkyʀkV?܄7ER$8p .Jq q-~U61(M NJFWd~ЋR@m($.YSq?ߎf8'WO[sB 0R ]$>w[x3u݃Ӎ>ڷÊϯ'*gm $*[V:\oucjoשqR4#MvtS4A5v>!?I + uDS맂Wgx Ìnel_Z H$ ~\˼ÏPŕB'_kq>ϣ[zQw H^OpUne^6'1'wJ?3X/mTD juZMiN3ծ]Ͳ`E^)1@ZN˂-̻+RIX~l8V¨zz!Pȱ"bnhQLVp.~ώ#d0P|zE$V`f TvTO/2>_7̱$vrW5yD8 /Y´ G>BW;d(CIF?4Og-*OVŤPK%KpgƉ)hx-HáX`RMoohʑRvj۩LgY)Sy0/x.=7x?.d<<;RKMuLm0hMXJ."_2 .q=ӎi釰ZeZv-\𚜫> +Ot)8h B_MMQ(< k};g>=o-FM n9^&OSa0fxN8O-uv a ,Ty~!.ЯFE^̣M-{L@PgT dy gG 2FPg+ۧiN ק>y/K>k/VQmĕs1ħ E{e|14U-˦aQ[[HZ!a`,eYaH6^~G?!ܱShP ,#)y&$4yI0'whPoV= VY,j y˗C{lKP] 疏{ho í-03bV^RZhU M}lTD8 \Q*p99-8O{z!V*KjOD~zL4B\S0H`lň5i&\&bv0Om*^Y)8OQH=%i pSS{7zq(8bM?v4]I_+@Yߨu>5Dcih6ޚO߆}&>Hl+0"b D{,oRM1̩Ԥ|-edU'a 1XI,.wB0Jn r;YeRz[n{HCݒႾ?տٻfˮs=̻ȩG}R\qt;?+.tᲤS>O!H@7~5 J\ck9GGN{TNv  {0! C!W!J>Y݋x?ëċ05zX# %jZ9k`S0EEAꇡaN6uה\˩1O֋66nݧ<ڧnuS' ȟuP7aϋ@_b=$3J`R(ڤLl'{p~s^edu5_ǿ8)s;TswU]$oɳ>x]tK#$$䟥_M),xy6_Ci:i ) FxxvmcCOʃ$mvzdO;km`J3POiQNPKRZьa{<$+b .cSZ1U6kt#_F/ĬǫV0ŽY? `J˧D1$BB͋)ј)F[f_쿮X|`Q7jm~k9bgmP؆CF?8gëI'%ɯ716t@}xVF<Attz0uM 2w! @  0UFi`r|Tl+z~`U)A}X":``y-`.C]C2<#eR&tZisVX>=4Ĥ y3 X+ G2b/X,y&dYzFĘ`X4^EȬY|W{>4VFQ&@ R9MV!WAYqe1u JK*}nChzYLi,OybNhO{3_/-PѼ*_2E^m'ݠ%7D`ucþgϔU>¯h /aʛwF@")!zCYCWOCF9{$;蜍+cy9Oϳ(w|Cn%Rˢn3{,Su{0y3t X`x kF{1̓v< `TrVn^sK_Y@,๺c]VH8lm{O;";4_뵶i(L "\J.x!m퉳\=&un^F,qX N(#)})-` eo: .Az5+a8v qY[W J{Xzv3n_~Dho>`;kQTN)V?ߋ4(g#x?Q',<'j+uLGh.X\JS]SySR^BNHP?}Lm)? o'rT !y gN4,GUz%lcNEc`:,Pg[, ZGa A3AzD0YN+=8mºNˍ#jRZ |߆k恌0;q?Ia~G#Z<e o<4yV>'ށ{Q,J 9¼s"4WE;7~?mLYb}GB&66]Ę#Ө(2Sw|*6|?zb<~g{3U5): G3BRɸagIt.xԡL47x[:n*($\vXh0a66Gvd#MU욹;_t^9*/G۾IKڇ磂ʅRRY;-adY[Mc:eYP }ўaa(-i,i2j^MD1}t. |&ϭ6ԏU,[cUnOoW@T {4҅ Zˍ=0GR`L,q뢯[ŠsǤ`2Xf/kFݕ0/b6t<WB¨|q6OM-zW*, 2a.e_\.a̓U^Fp =t&Pq_߿ :A, ws2#?`@`lol0-?ÓO M7>ßQ mIC'#ܪX#z__q/Gꃉ7 4|[tݹr 7O2*-rGKq:`IܨJm8( ₄| '7yejЋ]xUo5mA¾R-SPF~.U!\ +|hAտ= ! 1<*Y:깪]7#EUų8jUJe3 *Áy41KUHC}@cc\si&僙)OavhիΓxMM-,}"Sj9$5k&8 'AE5gc,U^? .^Q 40 >罓^yv08=R^1h4hf%X]Bd$lFcMS РGɫ//Op *k'Y!t5aO](>Ow{yfܢ*#IB)R!ǧ?lԭLDC(\á>4PrpP\MH:[)[88b|QbYYy˹d^ j0؝)i`Vfe4Y?ҽ6-@,l#5 +K p U7B@Qa0_I7%x& ¿F&O(yakgX| ^.ȡZe- :k#eO[=t -U7\ݫf Ooz'q/;y9BRqG2}뱽yz҂3./d/R>(J|R*3bO7`ũ<yN4%l<^Sy= Գ!/"Ca^Mc[>XoR=;Jڣ4{⡉w"ɡM*>J'#y]uc-O9Oԁ( =Ӄ/py:6l֎9imOS&>J(R "qvQ*xИ8S)/(yo`0:a,q"`txҪxuG&y~x)8@]ʘ ӤY~b M;4%:Yl&xS V*~v̛@T3Z^=Hނ1sAA3QSM0B(akaڧUo7 }y(쪕1# 'CUHGr~[R:B)ޑa6') Zd근)BHh iFb. o?"H5b*hOt!n 0!nZbY3%1a$ONv\3@E1Pv)ڕp31L hw@ȫ5^,gOQ'ɕP)`~ཹy#Ev] eOD`k<-`{(]JuP1ŧ(0+u~M%bU۳cħPW.>Ƙ0-u~ZU1¿m޴|r)K(ڧ,t#_yuYWY6OӛDȹS?L̵U p1h{2Xx$D]W|S'᫔bH haG1Р5%ᱤg/^QYZ!/VޕO/ h$6ya1e3JJ{3]FA*P*ˁ,s Sמ:v߁Yzcu9WsuHJב(a.CT@t ʧa. `־Le.E/Uwl~Y= E,z9-t. XpU*zиmӰ/g"pXyC ̐\G8cԇ +ZƇiʨ7 A \utnʰ׾VJ-o .@6PQٽPhp/#N0^}[;(<ݠ[9§(3v>RRw@"n\ vډSԅ?1m aJyŒăx?q :F&R;~-'icN7 6sXmOQ~*ChcCi7n|V7@|< Y ~Uʪc(ɑ~9gš50 f6Ǯh4!2ܺ!`-i.+-mor=7$̍vp8LTTup>+ c=헧{ꍙ !X %:_qF\!(Fz!(Z!pNYֳe!j% q@tٵX+r+o!T +)#CWUiְSnط$(ne5# .}*)0s W=]JizP:o:4S*WNlL=|Rܓwl~/'qTigGH%`<_day3 ,xbx)&=pG]'E_ a,w3. {K{2\hpt+PBnvdK~I3G\y9C@ |4ҘI?uAhi `b@BWK `@d |ԁaP\$ v.H ץ_;/ mnfcѮrި{S+y)"^{1 ;!~'@bh I99l>csGKG](?i%.ig>M(m #o~4ENwdk (GMI`بPx%}5UqZ}JaY F|1*NjR6Q8$&vxJU}gv9a39S.zA=^`U WW_A>iGK{8S}ض֐r]..J/aMc17d ౰V,k69쎧3^0c.l%VB"A`c:lٝAUcVbsӵ@JiLF4 ͳ0ֿg?53h 쉠eRw?v63xy bE %.c gmpU g5'KFX 0 0Mة;ܧŲi=JmS5_lwy0=^鹈T{mxė.kKp]|(=)RB:՗}2:R=4oOƧ~خ݆aZNR~e)Z(QMvN($7{nOQDNZd<(+m \2kZW@ 렖'jW9e$G}?4ě*Qu+1 !dlꙔj'/q\@E~SB|樂Cӧ̊Nbd+VK8e6|W=bxmqE {((<3Y1@1Ty]fUeRCDJ~-Ua~ШwUoejF`eLGua WâE1SlK? +kT(F$cchȺ@b@4`Yi>[W.^+Jeejs C-*4m|bDvʘpcN`1j<۬⣇q+fR *qjT|}*bH_~,❴R(oP[=hX{]# )|js:v8`;׾BMT_9v%lu" \(U}թ@qѝlHsQԠb1,+Q; ̽@ dEg5/,:Kitt+ b˯r0"/ʸ 5h| L^CvФ5/i,)>*=Ykew^ksB5ipהDo)-{vad7+Iy$)Dz:o}նLX]̓t5xGO~S,ݠe^Ƃ0eIo[*4vk))R2mM0 tƭK:!{Fxn?/1^Fn)蒠U}/񦍑wrJva&ʶ:DJ@dU)wWHK£PHtj/ uumbh$ŀ 2-_mC.!`„WkcHep CUU&h^Reڬ:/;a়2nHV9MβD~~= a{ c@7bwE[b_8UkÖvm|I,73N)8`FY>|GS <mѢz:a8}?tCQ7oz_ A^уmJPw1, (]u9?rɓvNiۿrӷLw^+T|gZIHM6[Pg2 w2ցvx%DQrK'΃W-y UMmc>Q a1gC>,sejmRy@;) Mz2L+ޗQ a_$ izw.  uzW  ܀h|JGÀ>E`Zq~R2*21bbЅl^S+na]M4k~A2/-gf?_oW~b;Ţ 3a|Pjuu;̎ 'aXÖ8%ߪ͙=o4S . .v7@ s];gN `SS._Z-vYyyy6Q;tpQBreڑ\bܣߘ#%rQ6xU{3\$6׌ܣ2g")\I͙1x+6q%E>4`hm82Ni3'o ׵<*9-jT< Sg|A%@}вQqy<𿤫rc7Bw4 4vX yfگ0u'@s{Um@@b{C)Ĵv:j8t~NlTx7 29m']_=%lԚE'mVz| k*bpswog=4)02b5^0L5"G ]M 2rλzR>nr7gZ;0< ^1LwtZe„10(ހi4+^W(RM"H`4O9RlSR} @:; "M^8<duexVA2vAg@[6L  {ĘƇIC0M`>:P< 41E=P:|J'O$6ww~=艼BQ]UfA01$O->]ɶӹŹۏ* F[ƪ,XSeHwXZAi@VsWy(ə %(g|{9q 2ZW-@.0pe\;YF* 3Ӊpשv3_ig1DsΎ5O <11y֐Rˤnv N \;jfc`z HYm0!:zOq1S։uEi؅TS'^zr@X.y?Wa)|#7H_&v/RVD5"΋ k+AA'bLޠ15):W Q @B? v7v `ZÞ3Ec]*PֲPZutL? 9uXԇO'çs]VSHbaAX_|',0%(˚oW G(X4ӄkZcIX7eSO'=ZbvP2<<1ӳV~0H[0]I%ڧ"Azj$U̟R1{m̫ʋ6< s9b{N~~:,&K#廾S` ] c<}{aHl- 7$L1ZI3{i#skɋGgʁ cP}>ICó&fY1hr>* ibKGeRLnm #Miۣ<>02R8zP򹌩׬ah%i!‰}ځ{[\cxi5s_.X¸>:qQ0Qq?{jQy =J'n@9l T`!eRJ qx@'Ҧ;]ŋeωڮm>цϗu:3'L /7sf.uOkTK| ĺӦA`23 W,ٲ*0hu< ,0`?L<柼N*`alaPn3ښ-<0f (F1ȉ}#ax.m;gzrۻMϛTPlz;]R`o80fQwR,4Zυn2 ԰8c7c (+ fe9p퇡ND>"4hs-) uM/שu=Ѐ֩[aUUcoˁТiƄe< DѲuEsqeZ[;$l=l8S AO~~ah>ŒS/cg_HhC 8Z` f.3Cn÷(:P"c2iǙbj%toWʅJ07-̺J(ܓܔ\ F xv\Xe&A:W}1buL_j]зQ.X~iTEq*bJzE[?߯'^Pɻ`y^uj6ș`*o59MtƏFNE0dhS1a|Pέ%8< 'r (0Ny-xG2MamTܐl|S{-es 3}qlOnNP A!SK`þ[!m j,10 }qSA)c6H,R]~8˨{S >;JoZ B2 ቶOo0_=t Xauu u^U5qRC~0`y8) %RV%A:N' LQ=CI"pHV.D=fk'\jO蚶@$=o)>S(~2ѝWhjX-յUduW$EaV1*f:YST`Avs}t615u-ju73^Pl=5Pl%{xe,OэqhZo)sp(ijE0yvQ P_=>=ݒ.0tԒ1MUoq"o^x/D~my+2ei3uC`E+yJ[?(_`jIog5>窘oNa4%aOJ:y^hs;?@tֱ::6-A^嵱elwfU<ƒ ,G1|)p$L'l͝ݟXHT (Wc|>1`P:,`\<%Z`[PۦNeR-t3:>p5_L(5>T/3|nyg9):ůϔݱ(mO" >ˤ`û^; .xF[Bړ^݈~+40M.ȥĢeh60^7tLA[ejcwo#]< {JKXFWjnD(5iShBrԖk̯(7n^(k$$U S,IH A8n'6߂!L.Pf;WBﲲ|40.Ѝe;J\Iw]ssnL3_wYfIɲ5lh~[Տa2Jyy^['JAaV]Z3[c (>ZǵPZ; avܜM_?I,Xδc:W^:ҩKE+ƢcR:^9nΙ's:0s?٣N4LX3ns AxW䈟(]0#D79WI|5z;)aUrIzaZ&?rGip%a?:*P FbhM;(溩p aV<ϥA7Q@D΀}Vb:`+1Y"[;(Ur9 ;uaO&ܕޙK;>?g ]h0A8Vw\S\:b>Z-шWanc̠j Fb󕮞羠 tdgH]uI?O( 2j1_D&1~/p~ߵ/RpZG81aN^IJ;n~aXRڨx }ǾR<^lispE{;W x tKx8L?>] Fa32?W>`R2ip|1/ÅMLD<~Ӆؽ③jyaP ڥD1(!#%o0-oAXz;> a86A瘣945Ԫ^e^4[W9q1.ikqW!01o'KkO;hUnOrgÐkwůSxC5D Ըc`ZEKGtq>3q-:qˇ`?`b c:Kln)R@}1qo#7W1^k5%$O.bG.}3mѦ0Jy)0̩NP1ZlLϬ2sb~qqSwOl(89Hل~'_ o`ɓE#PJ%}P8]E/W5u)6Eu7ͦ('?鱯k9sK;ZnU殄?ˠӧcZ㛅t& PrnDw<|+ J<*b[BwLU~T{{SJ֤sr;VP {eWS󈎸Pj< S;_ L=5v=)wq#Ua l)r;sOmqzFIڹʀ˴m{(]s@}ZׯQ^(~/0s zpO71LQ0KZ]0 4VC;BB>E)Zŕ^"2:XliF>3FxwJ霶bV|rZӭ0d{.(PMYRbirZ{Q ڭE[{z k}@pP8 U-,4S @U:&dplK.e}jݦCw532D Fe@NDr.=,5:~HoFIX謅|{gt{ôYRRPC3~Xp rwkaip|4 g'K`Dq%^I]0eejFJ(OٓH6֩C.ʑuꥭʪB[126oCvU4O@GVz 7[ꩅl5o&`.j).{wF⢒U#EQs\) x'/ƀtphkazpW[D.;^9< >O [_ Tsxл;sS`Jè_ԁ i˲0P/j}9ۙ&!n:ܢr/K^K~8Bf$+_)cŻCe(w-am;3iiK썣49 i`Ab1(=EqaL@SXYpU~:VЏ#$#R6?s!uwdiwt-%`ӣqsqWSze@ 7! WRs +0`*%RYL  =?fd;x1Dx.c0FwùX^ +6!m|&1(`?p"u X,0O7KJu,@IDAT4AQx`irG-yc0TE3.wkx^J )΃ݧk޼ ~R)cVJ"`N4݁ L$H,%?:RC!NԴ6\p^p;zR8G)ˇw; .fE^D[Wr//_VOSY>n4YDQ;Zn#'O 24N ci7ġMYL̵1` Ĥs_Z@(+u,G,AX}Au,s\~1#jk}L ⵌ.Ia^E`8JM`bHXn`F]Rj\VɗJ*Ks$xvN]y9Z70.,2b̈QLgt$B'u?O^GրYn-`# 8WXlybRSeuH[hvRߓ!oiӝ=Pw˓v?Io~ٽל'Qͮ+Ƞ81҄э=PVwk7&9dyr%JK;ө\cMh)VyAh~[ 5uPR y{TyĨ`b|Ro ;輦2tc̃G1v"koÂHʬ<.z?,guXA<>wr$8 -@31) uk>@*Kw |)Psɇ]jA#f ,Q$6>+VH\պeNV> ̲Usg2j< V"!q`^LR< B䌂g -SoLowe fBbpm?t P ˾eï1yH8J9a9iz],Q+Qzܬ=\-LP//!lb@i ;ZşZB9`R>v y_~4y7oC7R]{wΆ k_kx ګK jI-a6X@5ߺ̽馅 ]k rnORӅ6}UŲ&i,N{Dy6E<3=S.b)wnP {>7||1nպ0Pwa@%=ގG$2y̴m叅Ch6 ; wG1G HI} .ӒZejK-"25vc)Oi.$8C90 [TtxrOgi3?aG!.?|S!Gi:k<ةR?&X٧aD/_kء>xrM^7kTOohSHAJNHm8Ȗ*ɢ@{cֹ޼C7B;xNtQT[tׅD7Va=Әt H*k"?g5sF8 , 2 ç{qu7eE^SJp Y28x4l*'5uo!`'XnxH-}htz![K{{Z`m p#<7vvŋQ#<8ea>-!t0N88DG7IVjRԐзJa׌-wvKt_\e½/g\efҍaU6:殲'4+z0ԛALMi V?adK>eL~&5v0g| ,/v*gf}+oΘ45x6c-+c ms؄;J:d 0uUGEP)0/tm=SrkB!S-ۣPq\("C7 z3{ 9k7am{x̉R_FOyZd@>W޼ ?hM]d\O=eb ZM8,IZDcqY?qW޳,y$>x8X0wpB'`0J?dR IɟDsfxrԉ3^=c݅. c͎,J³X%k g]ڋ9i'H %G{e.aZڡ^0YT5w&/K|` DҲr{oPΝ0B57Q=oҗ/g]C ael|Y047m~BdO_qx` -J'S4N%Og÷v)kВu7^D9YO]G_qb%AjL698 0 H/E-v%*ʹ+9Irp΁(<.Uj7E2 :Nf.z8& mOnf_D*ې޻g*[<~&Uvb )Fao`=h%za"p4V{FN'j[4B$fYfw_1a)p=ZNYA|0@OyWcX( .wRڼfWaWcDRLGli ~r%)ôGn!?ׁ@FYҨBt5G}L:mI_ɇ8e=%@rxUpZ&%i '1c EsDxlO4Q=cMBy3CkREJkؔ`X x=/++q<>B~,=2 ajԉ)?RaM>ډ 4 +aU;S S@r yl ƒ&ZE4n%'T_P;U骻`kz:ـmbP3i;l z /sΈOs<] +pBQ8GpTjY:@.B+R, W 1޵L^^]] 2m`Z9I zcaOg(s 駍L+S";gi1jЋkK:yh%z,(E#Qr2sSz۹vKlX s"tȵ?,\2L@tb4%K pJVz9^1pb(ðEɤ(C(.aj*e+(SڭXQ)QFX~.7@E֩n)X7L|=*j9̷<A苹詖TW sQaeA8H檴 }` zX-j}׼mn F`,I'ZnR ɲ.VB# k-ZX4wwy-n֯ T'm(WyqQu#(bh^n'/Eum~h+03?CPmn*=a i0D;,Ky]e=g_Pc2OyXw!(8FJ8 bj0"7czJ~ĝ1<&ItWd/jXUU]&p _oV<5-gw4Tt.4?q!e`<9;!:mF 2~To丄 z 4jh6,zC'bvAn^[baʜ6(]lgF+cTLP< ^5lnB}XPi fo;O;ԑO}Pw?a[χ8̨nD& o08j% y1#mje1J1+G:KbHyP֒xwbֻ:N3_"GRݹŃ=҅7(VC86G9uYkS(hpNzod=~;Ɉc#liކu%}Uz78cX6}|8¤wMx0EHROXӀ;< j*FhOx*|Z>S"rBc ^DhnL+>Mh=MMʊ˶ PF1W5mO}vW.dzf;Q@e[+f@jWs(hpjmj6do@ &AٴFd0017᫅5A9ԭ_Q*1w\7m8V]1֗l3FMu1; & 3%˖(LwI7gk '#ǿҍܙ\Hzm9u~|1r(dWܩׂE[G˹ @wr f7Ǝ6 {C0(,XrtsM/.eb 3ao:-L@`&Ø2jצd0xZTX|&h8Z"O0uC^a>Շ1 ӼCy Ĥe]ϬO?̢8IN2,9 [=lZ\aosǨYwbG 'ػF`7,:.^BygvLk0G9AKWqz֭UJy+ڳ'kXlѸ>h7CPq WRm93 X0Unu<`aC\4r0 Ȥ%5ԲE{&3H)7a~ "2"L[ƞjl)jy]w\ʷʅl`ڎ9ܟ'q>;; X\;piCU8Y0ؐ>恙Y||N%f>)&2>3}J:մJs-"1A7t5ֿmFf^LLЖjAЀ#jSnꀩͧ>G#>,hFl.nu ;b>eq-Xw +ҽo쨹C磡Ҭn`5][ſcq9^쉱ekMu>S9|_? ~qT*2lΫKoyV+_θpfaSM6rHn:*.M@x1cV@ք'75H0$!KKf?yaN܃\8^ͨۻuzCl=8`H;7@Y^%ϧSe![i]/剾*#k#_`o4 o[{1"L=Cxw  Ν.ލ5_6'qjZ>U>ǃP UcL<)$aݖى@H*8s+w`2/tQA^M]nN h'-6pϰ9kWw\YގH!=8 P ʎQIK\+m8`p{:ƽF)F eJ.餓q>kACXE0*Ql/|7V$7jsAaU3@Pۉ8rm(0oz3Gk [ #18~cڅu,#@ R s)G^OΥ<Bj9*K'C|%/&q+kI"_V,w` f6go,| Ɏ%M'_͓+ 6{8';Vaxc !a M6~UJqIЏ7N]&p ҪzPnvA9|)LuЌWAni3dziwFx;(;_>y@ چ{?͞g_oPpjC15NӫQ6 SرV1fO)d:^/^jQ&A!}!%C} MQk0~y577EDlgoZدx0+;?]JL}7`;U^KɀswNj܍܎dq 7 a_:Vrh2( [a $ LC3ezUk7S/rA<@_cSO3W= ܶ@s!Pǥg<|#V%$x~ ~v%؝F)%)CQ.bcU(F[WC:v޳0y{.xo2Um%WXHȝZEbU L|:I8':5D ?(t( ^mS?VOׯ7? / ug^_zhaG?|F902f+9Fks|PAyDb `ҹNbrXJn+/kȔr | "aö{<Ƶ]# ;ݶyR}uPL/!HFQBM2+#/]NC],(/9gf y[Om@!(Xj^I avv(=RSiӁ~u8O \U;ɴb'p4JkM? cb' Y6'<2Q%K$:b_cs/bKutI##!s!M4x? L|w8e~9ˁnTⵜt`L^Z|G?K-Q}CA {%#+cYDʽqW zB "n?AuΈOC/ Y;ANUqgI.ψ-|O=*5 fHv,P`8f\簼)<RΛid: }#?04D)Gwp-<徐;D9WZ.{*HJ5he-w!tmɉ (:}O 35HHKdaӃa!KosN yݹɄS88hSzPq)+ !RC.* k޷J޳OM}Jd& #7u2&аБ#4$m"HP #}Tw$|i;Oj_5FGݮ1Hpc"O8Wz-](#3>>-"NU0$|zOTڢT>SA[A}|B!G~zᕲ@KQNE9':AգXkwȡ|//:QU9ʉ}*.~]f, Ls-F[; &gB+5#nq/o/asIaj!Ɵk~.9lQ&q[6dOͺ o)tm SC1\zjg7Ki˘ףΆe 8y?k $ t]Ȅq)<"C5c0#,;ǀ!L#9QHUi %(iZInsY#Sjn(۲:A |)*3^D_荿&x!_7 #4E4}TbH gU}n~|aH]s_~.B@9;JMIWA9Z ܵiߥuW"#Ih+Bq&a>R¿=!9 f9r?JC~3|?DhT'J(&s*eC$|~H8pXPt?G}8Ɵn42$J5kHSڥm%>lH8 k= L1)o(䣋m"faG,#;9Iv!C?y^ߌ7M0%o;_% 62@>y}ZxF7ħD)V~Vc;1WġVo",Ie*2*ٌXZ[D1ҚZ5xyc=ձ'-|c!QBu ~ ǣg)5 ǜ3>甯6wϧ 's2\.2tv(O~a8W ggR'QLNwSPӢ7㳜 ^L.DS8&JwAf@%5̱|4bWvk֗,w. +u)FFLDъ֡O\˫Uɿ_B@8y-[l?,BQxp螘\ _̽v 6'R/ѹu`t ,v]%|tq (N ɢ:w"gOvecgDZW#Bq*b\j~v5˜t,p_DK'Vaz3z&'/BbU[#{Qzǘ6w1kj-q s8e=av FGm9CZS񷬦0/z7}x3}ݽAWUH24K>Fu׻X[y¨ទtѧ|#s0;쌑X1d?08IJ/z{ShdDvWrbK'<Ʒh怒QotU|N&uɘ}0)5@"%R`kjyZ8Q(Ӌ|N@_=mR *ŋh~w{-/gD_e8zn5I80H ?{{y**CA1pu*F)YW;"LN?C_,6`h(!}n܈#LF^Q 鳃;P-O6 w2*tGo1qz&Y0>O P h_q<&6 VsghJo>;98[)S8{囝' W?O:CMCEGH:-C{o=!10cK6PIrhϵI٦#C~ɠG+N=VJ?/wAsN-E^>C gҟ-b?ܒYO&0c hY6qNu`IWs)[5rL.E]m2l1GS1~U,yOڰalM7!ϗI}{~Hʤf>#HF1M C:e BHgG`t+𧜽-Rp =A&E6qN;'_.H50wf38vW+ȖeY&#r/+ͷ1}ڸNkX_񝒴<njښsoSFOw,SjɔDHֹߙ]%mwDz?fʌ>3Z TtyGQ-yO C}*o((N?s`~mg"_G䰧IlUkUܒn2lvu68Ȩ:8) ܏s(Kj&_pgV?b?K6ua2f<+Dl(C SìU( a| 0Po'g:BB$VG %/,@Ck{fC\P?O2>I3 ,y! F(wpz {lv xIQdYp32ocL 0 8T&>a=F U#rS0*B}(ShʄM>Gځ]^uFw/i#3%w-|#XJɊ:X힑?~ =/Uo3be9ci(pf׺B{rB&F6,M 4 ڵ#/;@`Xsd3_x@pY{{UҐ姇%z93;6>!ѥ̪GR ־ [l|i=|8Z絑)ej#|뼠$ ^3Y {bZ=Mx~經zp(: `)`6`6EhC='U`ly'Ki4#!Y 7!KsU}sS|?)+pTÈ;dH{[x#ܞjk2`|iz7 YwNjJUcwc=t j^ݜ־_U/$9U 7JR:r&T쥠\=|ڣȑ a;ຸ;B-ؚC Ӈ !di#M 19e/8:bPG6s-"GFp38FF*{bIEsO88|Mu][C<|9b43d)c1xo,#HGqtkIrvƂ$(M]x1 ա;r>@OmߔUuƭ/׎Feq+%xO FM)3y+L7,!<7tQsxoËkbJkQR³0Q4xch7  JϡhI\q[Z\۽U #EݻP_} WzYY$%Sᘑ[eJJ0/RJt ]X  >?|f Hk Xg9,5ɜj38^zWr,`]'-[DנNH9߸P8yw簊_*7{;>fkhDq9,إF +o60Q4{phW#b,][@d<uz[P3BJ{mGXRkna:Zwϰ!j -QmbxZ.zy| -K<ʭWDGR idGqw{48ug++︖zC{&i80K)ܚnR's!mB,Pn,8?*dӦɫn(鋶UfɈSIwKkhK=hIfO^˚Abi'((a2Lc)##&eɏa;YҚ&d!4p.\̣=)3 9/ʇMq77z!|N[}T糃ض@>`B] 0ʐ>P(Sėamj\K?}ɰ] "E29Нݷr+[w\8ҽ25i(C|Uԋ3"bPgdld'mnee!k(_'jUʥP]s҂x~y)lly V<^?dCCb%Ĩzw,(9u{iĹG^>.- H<Z' QV cpKu(|?ghiAj5nQed]y"NTG;.X o仵%,QQ~(zC~8Z~7q.!T!i$yGs=\.mRmJ;9}hjS!}K}Q G,Gd{!!}Z&2DY:<>*[{;ʁ+ǔ&w6:L֒mIAƹ4}!GRg2y%~I ,ngE+z5]{ҭE~^o Bcc ] "L(l#zkrV`#I Y]ڈ9L`<0p:̉# 4x%٘B( :P:e15U/Τ%|2;tg9c#@!?pNh EVnJ/4*%Q ZUSCS&CH92G(ǎ٭ƶ>XD\C)P[9]؄}S)~h^駼`y+{Pb#:M&So㝣OV{k KhU~=Ob#i:z6WPD]Ar&lA^lR+τh"#/gLE#ȩ1Nd,$N)]j!' l@\z9'=λ~M:; ٝSlkxԮW>- K=xB:ʈ^!͏$ 5[[9sxv,/֪3B("p#_9hTyH9'Q/Vww㔿$ot܌ U9пoRF1BOɨШP|PVex4](yS %x(÷n}O@s|s^{17TûQZQNV.Ț.8~ʻp kB󖲒8!C24:,3<DV&;0X$AWSY!0I&xYyXۯވ&nupR0=.mLU]!:!8#KP$GQs15JŠLޑinE(v7!S 3l-ۊH❺nV[5x41bPZ_I%EĶQRjcn{ ߞտv6 (7"GU^cJn#9pE$H8;JW= 67X ›s eQ>%-wPLC豝cPacm҆*s9o(44tL-y (]CqVZhOk(>FzW`a`u+"9q@O+yz*dw7;8[]5u`-:DeG>34i,v͎u!~{`zcF75-Qܶԣ-:΃u xVPEX dk.6}<iAIy)y~C/Lk@ɡ=f~ A%VFڊ#s?7T驟-Aх/C6({mA;mi۬e$K7IsOF,[01deջ޿8 \/+x'n´gF zLs >T㚣s~Gǻ+s E sc3`(~Ȏ2$n AϋZ%'' $\kQ=Jm,oS7QWB>o?Lx{56)E]můt: D hD瑍%JH]xB;Oa$Ir= tO\Nh.}! g(4rj8KW-{?\&fBp+@_t!ߞSh~ ݇k/J W&e"A x,ʇ1>r8k`i/ ̨gKiVumCNϱJx񺤽^{'DWYGzm,*qPQ[mBG1t;x8FQk`˯gQaJxtk.m~#;sxLߵݔ$6g$O/wpE!<|`YqGhUMpԼ#,$Q=L Q@ԧdFhίQ6[*(hiLQ{B 57ZZ>z^`Os|6<5Uڅ#+k=ߌC)ZY)FSFlmKɎ<=X0ڃm Rt25t!4'˸2Gy{t1W+/d?`.q^ͭs#j(.DrYorf}g/tȨ5=^YFL*1 8kSOE&o._ܯlܷάlz?Y$ Ws6])%}b0_j[Qi@wUu6Q-Fd,G6v@~J`x6 0'E^x* |0FFW8%샒2Q >^sF/c5.FGK<1HVn.>x8p)NU;nZ|;`#;çW G-UN:gΖ\}TB_eNd5]0Hiϵ{g#q|?ӱe&z)x6|O.hWx 5wU|h C:tm\z kqws^'a4- &Gԧmʰ ^ 8o`jڭ ݒT 6[_@:LQR.v?rcdE ^\%u|snϷ^BhWerQL6壽 rJ1ZHN~BjnVzLog[\•@:>3s"\Δܑpy5S^c$ԣN8%u&4ڙƹ0l'(Wf>aI};P$`-_eT.oӿSWcn_+[L,9Ԯo r+)`,n_U2:Ho::[4FocA?tݙ:6± T\#k[5⫃e,vFqrP /mrp86u{v0KKRCNmJc2"0pT$#/EΩgpy}?@X ڧ,{#ݗruKx,|=Pz'x/}yA `nP]#Lqp<<rn3d]@oLU^:5S^{E+oS䝃GwDiWw3:^|2BG MLEÅT !e<{bu0&\<@~.5?}eƧm,?]?Ȓo Zf:q|q@t$GI͆1vsi7i 7HmKK`'= }lT,BZٴ)aȔM| EVOKGG|l_NFi[݃LVΤudbq+O\u/4>ǼoJ^NcdU(aƟ#OI@SV -E+MAW7o3p=rB~ f/Y<zi(att׾9h0"RPDadLNT8_aFsTݜӋɏgl: Mwe+}ش[\$ amҴvVZt,-@=lQ/ s\/JFw(fەRP<2 Ȃ㱐sXCMM.D`>PW).8*`Ỹrf@}Pg="ɈWKAfdHw_uoa`ڍCk8utJ^xp]{Ht <_x@z5N֌Ƕv)/u{/u-iLaF~;C(o+bK5C&s-I~~[LmSG,>Pr^sDŔjxOPF़-^}ԌM9gdjὈ;gŵw~eu&9E!q!Y 9I%ƈre˃FUUcK=߮sO?>ZKD瓂VIh^F&4=J];NGrk}]IlԚ^q]S_z#-Tr9s&B*I&c?v1ҳ&A#S%)KF1 Wg~KGyG) WR/o/uuQ}{ՌAqx T%Dۣ`;VKl𓑥"KWR]Gi-:\7;~7,^v H #h\i M뉮kpֽpVy}=vrp;4..{KKZfqԤwdb} u.]al2¬}SgO/U J?sO." 4 S;S yQQ@yھ_(W{HI6-+ Ժ]r*wlrL8ΙۗߟF7gDƼ!2l'Xĉ0h{?x0XfG ~N훔GzYy;?I{ 8z(> +MvM+˕'!>>*[z=Ե~4PjvcW%l½vGVXl ӻWJfQl5`}WHtohFDE‹!"^i4G)C+m6%`TiKJ {|TүP9 ah{/8'<޳˽uPV9.Mpz%bw 7r)7qbFZ^Jr z1;$Hir_m :Ux0ڣB@0Li0Fg ~XE.)vk_j|1]lJPdrmY7=ܞoKjʍɴh[ocLftS/>NPJd(d1|cMV_,QY_bQY#B+9\}O0dׯ$1t7=0t"=9< Vvc~>_}^6D&$ aZ1 p% L8}2//JϟDxpa|#pJ' /G8(#*`勔/t=M~=v rRKr8MѐWy҈ EڨABђcL;b) 6KڡNs`=(OF{ҕBogQx`+hk\H؏B֧dD`^|t&h@-yڎs D/5Q5-y9]/*{^TqpNʃ=/m>vJMIv`q|%h5,j<{x@蓰澬=uF6Dn߄veO*Sy aovlCS2R&^!'@a< ~f. !Sh&Eh4aF\^n/UpK~n/B[jcIFڴ$MT7![}ta#:㻍ΕvfcH޺Ɖbj\m~$c!/ҋ_>C[/6D :!sw/] ,x/u4{SQ^&i2,`0@Y)}d Oî,$d!YnC`A.rgl$/2I?9Edkt^ʧV_4:Wz5͜ߌz&2̯Y0rH5p2܈{^ 9 ?%Ԁs}Y+(EU6S (J1:J8EyKA@4i{PhQǣaV-%1ҀIϝM_!)Dǁ8z\nBsJ p"r~f1w2ye,CE>)@(j(961P)O|FrNQ"q(~u}QrT,q8 R$gi%}gOJrrf^Gds>J c|ie*%KJ_D` Nff"XwF#?>z;*HfW jF%a/RJ(=ֽ-da| oø}kUCJ/fQKXoV?91B"w?FdrvK}v6u?iК} r߆fb =i4+ 0c>@I@$oWr/az y::ݽRdw/0qڕDIWFȉK. O2[QV#h&IHnlg#jDF_dqEXpwh:Q-Pk4v,8P"T92̔ sq+zxmn@h+u1;H3eT||zOZ m r|cƀĠ 캇#nȍu V[ I8i</i'JÜze?Mk]W;]8hwGHl (ow:pVi+ܡcq<}_5c0{1y,8"'|c,-ao+g{`ZtS }:;G2އd`@_x54ԡ4DPr4P?7.셩 $> 4;Яs nG`b VzHA-rb(#F)ail;M2R۶[F]STҺQr>s&`1o1Ծ^_(G B.y"w2EhVJ(~3\Jh)y4ƁR# H1|?̱39ޯ3r(μҺmtjtNj%e,>'A h k/I{)Wb4D;GFщ3*qzs r/ʢmEVG\;||юRgɲB،І@7-͹fm1M4 DH4ĚfhU+ҳsm>5VEw;'3AR)/9^NЖ5LHDzǥÝ>: .?ʧx/M0AH_Dq%dz:m3΄3@b3FQ܃ՈM0( @02s];ƹ '* eks~ň H6-W~3l& ;|&nb|V/Qᅂd1 #v7ßO< !xZ7` 7Y^U55]5T 7yа}֣.?E{2"*P/$յE[oJ1XQ{9N wuGh[Yg(|]hnoN'bdTf:-ʥ9B'w!g ߛ lc>CMFuVq9nNW?yVcя?@r9RnGoYmޟ3P]ճ_p 8S"ds:mx̀ٝuY׈l/{2KwNq[}v @s/pد>S&;YP ڂhd1O=Xc>t3MphyPQץ*sB >+q ylw˧w,F"'A?tA#LH(5<00,URG^g$WSK8M=҆uQ3?uhJ|Dx.߮`/ҊwjE8N `tu | 6yXh;1N ޙA$(Osx{%=QcS,m$ UJ'̀~jslSg||nʡ8cm+eDOQ}PjQRGy^XjQ`Cp$F qSC3V_Wϴ@>{2P2IFS1BHWop[r[!1'[H֣K?Ù{?NAƉ?JO άnBC42{Gٍ AIO9zEAtng\)W-f CzjZT$AaHG=W1iśgՇqR?Z5>F[(w bQywCƁ:eNnWoh @G#' ?F((Vdh{lZUV#x:~CGp$Ō9?FpF~{(00]Ď7Sm6Q&< b~4_Cy!VFc@IDAT^.pm<';9z>-kOm<ڝWC DH[LEIO7#3jZfƏAh`^ߨ@=÷A2՟zKvwjAųBJc|}>5yߥ%amA̛H~j~ŀ3c읓 v_Hesm/㷤Fx$g%΃~ηnf0m2 %d2ET>ZiΡ{}ߢ7x6̰ڧ'C_J$y[- ( (dLu.g](*c~,b>6kpSj0CIcJLټyH8c'S9_#[ Vc6t/ (OR'NPIk7/mtFFaoV^P\JX|&ژƀqFrQ_}EvZK+B8T|# CGy^|Jd2p)5.Fsx/INg6Yz7{z1<d.}4hTė [9 7<Z w HӏdOs;Ͼ3+u!l~l 7k8o޴(`ʔ&]ΥBV@j۷Iզy2pA8#^`k'ǍK a+{!cazFۦ! ~p5;O{y>h}nV:޻8yEa؟=']PӾ^PQ}е Igkmq~#+5&y(oXi|JTD ǂZإi}3r/7DhTUmz?}=28\ ;fL> ׭ OD7ޞ|Ktnw]X261 Q0߸ZzTlA!7ش~IVtBt,E\BA_sI>纤9p>nln8'# sgQu."2U❃Rs0c~yF&PJƚbJJ9 RY"kgh~ QXmQdm'/E+F>Ra#PʲT[8 "b=Sf1*rP8Yh࠺C|w/:R:jcUM5^rtP49b> :H9G2sMPf.TT@; G伋lzh(KN~.MvɃIsX9sgzvwhw:;rZo4S=G̟5έ<\#}Tw֑hd|s/}fꮮ+sCb 6 ` },:+-P~s-=<={s9IGƍ־) kOUtKanS%}7 1> :_YiFP] D0Q20pQ|JaQS6H}? cOXQhi0@>I$ªc Xgv9'A[*3}g>9K㡑 q?_ 0" 8R ]!%y ]qOyw͊ϷBwzwǴc9@#C2pE$pbJGtH0c"fҔTb}o5{`igw Y}"Dn|+? UeGTһ5ץ?wOa[J$8JN#uNj$ΧLF>IKQG'{Iyk/>m5Lʣx>K^ ܞBH Ў*Ȩ*'QB788¹Qy3JUcFr':?ُ5aߨQW9TVo契?ɋ,Y5*?ݧ@b[;K߉: gZIs|zugыY%>M 998 ;^Oi5 qe "*e$/4?oWFE?`S. }8UW\ci˯ 7<}7 ,=2z+@y9Lk1o s#+J厚0xDҔ_/kh Jʃ=E12LO&h3!f7f2aM+{0ʁR?dz9nk|B+%t2 i *gYhR×l0[?Zs &[r *8kNѤ˥JZJLl]Vs\gNerWxv$J97 ۾.Yv)rcq#C.y9uy9|¯)-Vɑ) #R]Fv2w&Sj6J|j<Y1 GwιhEt\' 8hP7gd?<7C C*KsțT 0G|ثj6&9zi6^,I-bg- <8RAcA"#/^ymlgeC߆=m@t0;eNM6=0&g,s 8Ӂd<Ͳ kd`dގ e B *sHo^uҏTv/,Kd ‰/2x exrc)%q]ac(  OrEUd|MJRB=`d( I bx(2 JI>2@.݌*,Wsx;-Mj#c ARmc9Đ9G{7|Rp_Q1G9œm?zOχ>9}Ao#X2d4Shm WJBseo_%a͛,eKm({izd7bX՗ԕ=(*A%9{[y لQd7~ h~N8() 6zvd@ZuȒߗ߁۪50ꎰB?*K4(=~0Z.@`:P?TɽI.ytC^H-y)"Fc- 1`]FRץ49)2V=,L!ᨛ$EYj07$Y~s濳?FSDVotX"6 5BS3$cyZ_ЙzTP"c:*RS#ޑ@ax=s_ۓt.8ᱢAz!*TwcSi/Җc[e DPܾ6eAiBrWNNtPI4v˫ 59$ntRc@]ʚա4PN.ݛA@A~}b8%;rʖqՏ#5M~|yPn*}?,= &;L\~iYsNF50ZJN '.Nawa`q^|K(.Nί5h ~z\ [T1>1@03r({[7*vIhĕYHhz4%zYEP^Yχw8x'MۨR,Lo2>׋Jb pHᡠx(ZīTxﰄRW | n(e |Pd ^H(yM~ OR]灢hy:TSA'&-n^Z{$|xpY ɦA )1hiKpu42xB+2%;ix_x]]ܣGV7w w E: <7…c_pnd &`\E sOfrBOFe:Q11]mܩf"ccHaz%Êo"{dT؍J*u!`ǜP[}I"թוgkJv)Ewf`'sGw 3ۈՉ0ã]+CRf1 D̐jM>埻9uC*y)N}ʅG}U%֩~sK5/D) Q#iOÏWZ 8>='q((~ޖPk~;3RuUȆ;(Q6F3\" n%6FEPQ0E/l.082<8upsd{Krq=B2g,g3zqh"iy#!d&z7_u8:VՈ5|8|J[& w,l[# {Xm87\re:%n`x.#VEʩ)W׃ l> oԳՉ5p;AaB '.,oyz%WW1+?gسiޔѿO/ ~e1:&.vF'ː S0|oMh`ڽo(B S66-8r6~FaRͧMLP}D|ǁwtQxd67g2S@˞R74oWgIޑ;S4pAi-v}00]1Bw[@egH4B/Ξ z§h.y} _88@6 Pn(XxG]Wڙ?@T ~?1w =ƒ JTdajGJ@\^ e*Gh8VaMWuNp'[NghߞO%2d>{K(LNαM`u=-=&ag]@E7:5BoGɐ1ȅk"t^ P;ll< T:më_qQ=`O~I<1w9@!1yfȬ+xt!¿|/I}JL  Cu`75aGC2OPM="s̟:^|%ad*TY|Q~KiWy5-ћOiRmo#-8N@WjUnI?KogbW2}Qe_ vXpqp LZ!hYH]XB>+쾗G:{xLΧaj(zA4`x> Mx$/1wejmYL5O\oH2yj R3h`3cK U39PAtrV՜r[aԳٮdѷE?6h )+C*-&Tn6w8encERL4&o^̢FIpMdaCy p&m5zYׂqg[/`#chG6?.Y$_p6Dι։weLxRW t.wg~{M=č`4vf8T31A(㹢('JDMGc[U2ɭ콅53)|B,+%)n{¼F+l hbs:0+JhKs(F]l ^j{#D D)oR-ȷ~zlb3C7I#F|*69)}mu], -_DU9(~ǖ;]W?JP"D/°ཎKq!D+JpMokыrPv;D` RK̠ws#I} OB#pIyݙU|xGqK=PO*2- Xz-/ni:ǘ3t&‡8`9W^y4Jcc؂1wy61@ Sg"ms;c=Ë0ЃIdRwt^ZǒR2ɼO+CcMP|W77|g5dL%sn %P'JRBTdHpp#)||QAD%˞QTUD4KXӆ#WΤbc>N73O7]n%SpC)SӤBjy3ϝQ[8gu^]pab\ɭ GFH;6*y2V5"}S[r~UznN&:we&䲬2]Kn=zE J~g`9 # d*J1g7CX~*] ""f #BqpaؠC0gp褳oZx ;CVԠ.f8(~ c k>/}O̲N@{$j$9^܈?uzN~aJ?p䘄n%OpCV3~Q"a%t{y<<sE%z&ɖhE+uЧW3]3V (ƨ~ٻE`-"+w%*?C_mZ m^e1Z1^RU6fs^+!\<=rKB (ɫkvZzq>zTEh^^_L|*-WpwJ=lf;SH%Gf/U1%x&lx">OWmJҳ;#ju,ĘgLСwUZJ >^hl\5JhBo8 dijLJ 1f!ã\B^)1sqC ^PL{^u  M?tzgO}=VÌ_uTV#fB74' |)ڮ*Vig%"j?edo*uЉP`ݞ䴑2NUj6緙ip{w38Hz?t^Q6"bbP,TQhha*~nݕ;IU%!|nK p8زZ^m<_}"bծ h|K)+1iLj0ƈ 9Vv-\YgL s[H,^Hiە %Rr"R6(rw";~̜Xx Ex7D_o '3\_jÃ`N؃{jmVՄm$B DY8X%vf͑z*h"f>}bD) Lt|)O!$AF{M{ ''2ϫw?<{}6|`#㻼>ˋL/ZE4~OgՐ?ls% HVZL£,M$J! ‡eoĦg?RSr}3!|FzX&5a 3vj%]HkI'<+ [);{-h@}bV KL0jm1[ w  ]Njķ[ z@B 9OpG 2cdݯBJ73eлy"V:|>Egᎏx` ~7NUUޥsi6<-u C 1qnS땃ҁW syĕKO4,j1HXdSG9cI?y}NfT`-C1Ch!Zl(AMi_m_x=(hgNr][>JB h$`x#ˊgʨ^qhbs'=;|0apUjvxbb7].҅:f QW]˃b3?2:%xzQ±Z)L ]T}vyσYJ̀Gmi(~w!m|c+R݃C9Ⱦ*wYGFf25 G]L'ɴ$jPݿz1,p&Ht][ Yo0@y7%{֪xjk{w K@p._As|ԉFv Ef;y;Sڧ@@Dx$bߏ"* hl‚d>#4©mDŽ!|Ș7e@r\ 9 3Ln=zf:|"߽I\/ӗ63Й퇋IOǯGm*ԢAw=J8-Oi ~* . zNbO(F P @) _JQnU)&hMyTpFŠR>߆R|p4xb/ ٜXg-Ud Vk[ƯhŸ <Ј5QB0>w^saN|YcoȤҦc޸oP^[+ZxuE358KʈVAH 40dC*_@~$yG Go+KBkBzBRP̎5n$ԫD̘%A`P–SVLπRˇ^0o&~Ъ޿cҖ"ŌK 9?8D't)<@Aad%0Nx3 ̄5M5Z~(bK/D S_#d|[B6nV#\?26=>2`` `fxNʁ3:yIxxR!WAWL)^ɖ6R m,x l5&;Oa AxYa^-?s\T+Z,d@d&һ<[tFm(u&m $mZ6⨮LƗu޿r4jC ƮѡeV$}d7Xн<\WE&TB*$J ;JVsdyBNr & Yxv{;'8:,5eh "渐Q<:݉9r&G9C8X.8'(\ߒnDwٽvE0HYXR{I\y@髇ksM,R%•y--glЄ>! ;xֲ;1VС̨.l?Q̧EKBUƉ? Φ;EF tKhXWp3J|˟@=/lp=<=)ԥ|FɍcJ>],]/NO'=!?F6N+#!Ndch$V2o;hc<85 H&2FxhK< F^CʨBaΠRuÒTe(?~uzǂO"J.(`}EFwB]'H …0\9Q-ֵxw peω `^vvށe4 bH Y9x>E>`S ~@ }|2Atvͳ֯W70CaX5 Xc4v_/H* {b0O0洋P_^W@7}XGH=մp}^kb/(þ0%ѴzyrV\3=,X2@ʛ mч SJ_0J!QlYݟdj̃2wǿHӮwOX=*\2JOFP`IOZ]͝gϫdK4ZS'Cɣm0²w(ܚ[>C} 1[3Ս ^c12 ,0-DYo?2g:"A`Q2@ƈc|h+JMEҞuC~"yyy13bpxqL7|u /Th壭 t MhEcƵ2~+) ס#PW2O5ͤj=&8{(P3C\>Gko|Q vT Ё@`qYdžX ºZsRՆ[Ie>aNI Oj?:}*[yqS& ÚjEJeZ~!dD,-3iy؛pa-tk!(QUD ͌s1ͼiЯ2Yq~h4Qu>o/T`[ӷjCڶ2\) 'u zVٿ Ƶ ~ù/ / l>2=y%&aOӮf*-\d(CWjUa ‹$q1R}eMLRaߧ쁭g eg8ߪ~݇YC]𤎦N8,u%H g( ߴr*[ʷhL,h]]G\ {_>Wq*yW# 9(u@ndKZ/wȌA;{'iZ{(2L۱^KߚS`QgI_T'67,%{έ= H[J/J ]+ Rc~@IDAT_/GX@wh[y@h) M;tCq(mRDpj\T0*K'>3r_vQ{'" Q=q3H(g'u3 LgQ7^E^HM yg(x ;A?/^'J^1\(I?2v~ gcL}1Pkrڜ+j;Y:rd׶|$/7z >XA(@92v{)h]ʫ^hyg 6}'\/Jps4(i~qX(m[nՈbgs>V7(Pqxica$MR6$B(Mo'֩W<% baNQC; nl|)7>wPѴӋ?v>kC(޹kTM{)^v:E5߸hCjݘ+.0w0$185yZЀ/R6qYX?g/gj$<>'"+/ O´bryy\7R w9rlz-fQ*tPx!3KL\ޥD--voIf}ä:8j%A P0ފu}m7Zp}yr9\/ڎ?dZ//]?]"`$]``M̓Fܕ..;s Xofʿ!"^nj։\=ʣ(2q0#,U6;YY d2%t&416NodRߪƽ_k}8Bx{y-g*}ttjQv9a>'L>0'S>F\3x xk5C ZH +ε)Q8+c[. >M]T  ׈7#5Q`lh:V`<k#e7ʟ1v|$\Lf%Z c2%? d.kTh ⓾dvz^uS YEkHwxN2TE$&hڻ._(~} ]&E߮xmX~)d2J`_ommF 7͊:HM g L!h։bP+!mI(5ְe{h(}Qu !XE K LC˼߬tB1BX1`ld XDa$k?OQ. oQ}Ōu:{P ŷ8R2w9g Z`b逗_0D_>%Q1bz7mN-E-Ykt"ze׫0bT_ɠˉ(B[6Q"v tzc¶x'cT&U:lk(3ʗp9l<3qhw}SF J˟HE?ɰ%1z'_ͽG?0W{<1}et9We?}Ac   1ÈrAs w7F 7JX<7.| >*S: ua k6Kl ߋ2PpPB׉s!RGڐQ[e;IkzZ/k\kvanɚA!؟Hڇ)L;dsom#87Tw[7 4 Z2KeLP X+(*W@F?'bcbɤ('rᒁzM~^Ћ6l|5wا)4siwi{w>=һ7 `> zF e51Cuq+ R<[;7x*\xћ^ tQdos9~c7ؐ[ήu0 iml-4[+/.c{g{_QuG s#8; ~/*+i횂GG'}F~JPr$E-ad&lmxH762|xPiPY[ݹ:%( K~SfS~,GϜ!]s `/cԓ%r=91Rk{͋hZuS@o-lW\=Uku8h"c|)Rxn`1K0sK ʚw]5͘I;Tp2/KHH6NTMVtgxgR}: ͈h0dˆ:ӑt{.~JL/Ыq0q 9)x7$T:xء[MCR sSʺt(eCvV'S #;O>y8WgִZqoUNG}PZNm/>Փ-Nne/+Y /;y@ aej{&7i:'EʳңI͍ǐPW nUz^W2IpG@Ln o 2|yzH٠ 0,C+!pX>ò湸F+&8< $"nB=[+m%)yNQ?O7HR>NS9jZ]uC/ YW$)Q>~ӹAdz^%l[]ջ w%⌉ | 'QA؃#ijpQxc;a444挛A]֫KMԌCŷ3K"(QLo~YڿO6,I,Aƈ#HT|Oץ$or^ac婏(ݒި ] ?^1Q~9Dp}v\#YQe4Ux. +aB iWKI{y` FE<'F 8W /xgXXm *hg~ϳ\.uhAI4x&KrAި"[AD3U}f3{\1lz7um)J.iA%`8`jUE~{L(Ze8*k r m S1lIP)]_._8Eh{1', |w|GBX ,#.EIQ7N_rRViz@dz}A՜*[@M^~fZ VܔIC]ځy[2=o UեB,2T}:(ЯP~1P^#ձXɏP (2GWzƀ(*!!̄U!~!ZBnXٍe~XA㼽^Yएe?IPf6+"ɫ#+m!l9(d)!%P'KA.\mI`]imB8fAG Ye&+nZpM2*."DAY gI_(E4i/}R6h(KD h(SDt=[ VL`D$*聍YRcpFܹC\%1[̹p~@\ِ=!0/GHȃ}/?Æ&xo]>̫mO}x"RZh&;|.؈9t@ L|jRj5 [P5%H0Y}2 eP[ڱ]`x8&1?;%o޹lj%Rrz$9!TvIMʟ}тH=;dej=@Ks=< Ef?0G Ye7 ݞ&5H0e٦2>0>0;<$gVC~l {)c_f}V7tQ< Dl~ZX]j?ȶ{smaJ<\W[3A7~ ixG (zj1_ d*ktH$01 ~ن@{3"ô/-'-#( 3C*JDjdݻDt(h*w]$eWjʟ?SyymM~޼^= X~ d ELqoI/r?5Fzo0*+GI(xˬ-6rHʐ|=_Zoot|/swoJ?;vC!e1:G͝RGoiD:8p_ r},jF"h^_s2k)f. lķbQI^& ^Ĉ|N D`UM#NꓛT{LP`gl.6%DU>hoFp1# a30?=-œ;h 8s3C-\L`~T^Kixʫ*VĀ11`Ꭱz$t r_{#F` dݘeWhVzƢ%ats^wpYܛX 0^Cir3,p: E|,) &K`Dn]5$9Ķ vW '<̕1mU4S  c,u0}&r{ۀO\ ZD{9/Xu[%r&pFD n2;12ұArkw1fJo}G9r'*Q/|ŭl\^zR|+="~% ]{R{dZEhw-b4L#.}3ʟ'E\(M3(0|zY*j3gyBFv<2M>y=Hw]eVA SEBYWqzKSoՖ͗ÿi}"$uc bW_8E1,0WN##O}*FE6mN̊p=d!v;D fQ7dLO7.Gh<L?mIP*A58wU AP/ 2㝌EAIez%Pz\8^y2}i؇FP(b (PіCsf0Oax6[!hun\y% o^y!3}NS`Q|c(s"Llfsy[)cuLmɆ#?ԋVJ/6A!#o8|/ZjWx4*X@?(p-{ ' CbY'6OiؗzO BI-;`F_])s*~`fBdC/*5 S}"'ۋۓ@LJ(%|P7 sZZQ g0/9! ^=(D`(O{wc? @Ƽz)Dgr)E^%1TDdA'qq$*,/ϟlw1-3'hz&N; ;J]Bi xC{Ԥ t\ F{Lm zڿK8ΤIk^Mˆ/;q66~tZsP/ H۰JQs'lfez ߺ|o˵exO f? dii'__>#%?}nO3* wj҅[w ryb$`fj)? NlPa PJnPP !RP{?ٿVxFdo$aok U_/,;s\/'h*8|4ڿ5f+3f]L0+ic@~) ۰ VA 3$ #/RwM;ut&F 1iVI mQ eIU'ՁU,\Ob}2}/*M߀tpsG_p9+|=;i_w3וkdS9Z;?4|oqR|2C?t6FBZ<%&#xImSS)ul)S|WSB`xR`7<8TO Ά{m^\=~4 Cȹ3'ڶ[9W:vN1.$]:X1~y i`pՏ!՝^-ELݞ16o6IMc|JτX" ډAyM&Xw8th/$ObMR_ﱴT,ClQϤA7v:T{o2DI=;/qO3I,#@XoȮ:薾ʆF";QC (0&f>l\m j=h@&"/~ >] [?z4 0OҸl U85NVe)-{XtI|jN:L(54 #Fm~5C4 *8sRd%?Mقmx >`S+ݚ N@[xMEüa.Cu:$T"rUܔKSIVh+ENB#p} Q2"`mv1̦F6!?/buuHBBDS«j_QܜYOo9cx8s'DSY'8% |BbF  l~6'ĸ./D()+|9[h۟No"dz*{1pXJ{6:x9g|L ϵ~OÅIhd~x|w4 ž0/pK1K O)vu[])ӑJI&u8r>O~Rxd>e_AsgSD"-x<9Z@f,05R}26ꂖzg̸ƚ~"MĊEď9>B{[ka)v+ ucVmjRKv^_*bS2EtLhbKToC{eRmy#z|:a {3]g3^pHbK#CP-/WȺ}Lal ˅YcWw6pH;5yeuGBl*m}cS3!r$og* ♊Ŀnd /0t y[s D&Ы@'s$͆ Wc_9!si&ٔkȀH;ٵ+[? Kf>r,%PDA8iO  l||+ ~$s7gF ~eivxQ^}Kcv7? DTcҗ K˞Eu\#Q?Œ9<#ޕBp9%c\<䵱|y5T/~_ :2ƽ6tȱ|!Bp[*^,DRP:@ې?12U-m[] $WB,AfQ,e3I~#.dP4g/gP(,H cc*h4-Դ^(ߢ#L;#ysuh{F-qIyD>]X$CNDEjܓ6ɯçw~4J}.ΓiO![K:h#=#,|aK6pZ,fSg[P~ēv6gvLdlK10hl^N> vOY "rzxM wxW,"o?0jq}p&CDiN)V-ĖEʤ bdyD?ąJN׮ZOʄk++mzԽ*ff)Š'='Oxf=>QjoR|SaﯣOw}pP{!DU#( 48we_˭OGBWf+U`$K^2M`*0&&Zx+bC#=1KHb2tE1 +XT%OpGְ(LIC:{K7-+S\mȮs^u7RVw$@i܊>\xb\0f0-.O S(s wޙ49SRw8>=N&KAy!d}"JjB&BIx7!pfB>`k h{:ɋW> VnE,ۓ,W3a&&39ylUʄ3b_S{ĥSL|""H2x3ó%Px4#zyš&d(n;!s‹p#[H!*:y-aH>BAxK{FH34:}4#Ky/ցNMM؂I< }x9  dh̄IFv!pJyp h{{xPj(yiY"?.sj.ٚI8$ols2~hth~h>}31 ?}su+t!ځ:!v#LNJ7pϭ\XƓQAmQ?G&/VɏCI# we!P/#]^GfY&l 4<1!/J 3 >yx|7sFSi(ý:\)8-mFCJi_=#P]wy:G-~ v'0 TE(~= 't5xj4XVxkۚ˳r4+~Anx6aa!7?{ϧ'x%[1U+m *Yk˪T !| bLr#X¦O0q-Dz^DnjƘw7 b. 0u۩x#ގ,YZ'x}TX}7L`ѥIw)=!5U(E;)F{%",xppV"oW CB#x) OTw{4G7@mi۱+S<ӛm)F㽛8Z Tb W g 8#"<m-LC:pw9ʼn a,5=+t{ ˎg嗹&mFiOX '>#Q&&OK sv}e?Cm ɫAuoWOm-.c[RL%}xk/<kA?Y~=(w Ruյ!:(, Vo"Exy$?b*o_ NFǹ Ӹh(^UR*ȵ}z qݛ@FFoH•v&e NEN'ƀ`9ϙBq0<ӟS~.OO3GZF~chA9^6;"k4qM5 6/ޑP?+ZTpާ!t6Jнś|i@K 1ϰ^i(77fϴqץl?xэ޸B07k#D{Z:j@* &+E<}[o`萉"F@CbW2kt/k/K^jQ7>cg'2ڎuY[rjtp q,^ׯ&  $o~7Dd:G| :FX0\onRv"Ԉ DEȿ=A6&YN~23-{4G"La+)<;`3Jűp6JE5H2Q"׷jQ69uad1Z(E^ecZf8>+{]å)&\cA߳QOҎ}GF #+:0[eVLT5xjz}ߗ2:ӡr1_1oR9S vxnEY=7o-ܙg[*.ZcЭ~+xpdqݭ?гk]6zyƥŷ,:A*4Bs+PR!`#K̭,F)Ƀ(yǵv?0@(~n )40Xk3=ۑNW{፿J8{R#Ըo2Pp: oAux& &~oݿ)ٻҧ^v`P4mbo2D "s"TތP,.$uTίGY%ǐ_e?OH٨YvՍ;e+mf48mS;KR#Ze.>%{2D.ך!s6q<2 MZ.J'&`i{\(k|` >;C ;/v^ryn|Ny3YZW#eP碽1.Wrm>1 zS_dÕ7Әw4頦 /66ۂZ ^lھfC;3"ukm'D1BH`Q^%"ZR*UKϦWנֱ1WrI6:v K|$?L j. 7A̛yU ̵ 6 ]y`HS؜v%1G؁,h(M>'6x"%=^LD$7ي} sD-<ނňjµZu guzְJ \׻yU[ (2y9*M4J!p}^y. pJ4~|;O|6| tԢ467$u:-?e1tq" SǺa)]8ޙU7xR32U;W/>CKYWSŪ*|--P;_'w-kq<C6lM!Eտ_Ī17&يZJa"UT0W  RJypC$DбR{{_X7on1*aWХӹzFۖңG kfݛ@7U9U' v> eCa*P~{c(q9HT0z $=!lvT8ɭB|1!ioIEi7Zo嬷<-cH!bpЀNJ]xp=!ˮt>*r.F~zo? kW髳)( cp?"ԭm=1bqߕ3!0ٺ=U=V\Ʊb@IDAT+j+x‡mL'iaX$ m o] p)0ܦzgR9r~yއ<&zIƏTc0i>AZ2(=*kPTӟ`u`Ј .z-A#dfls8OrG-gPQ/Š%ϟd5xU)7~55&D#2AHN !Jy,lfY Omgfg)vytzq9+xSc9( \-~ ZGЌ& ,ӫ0)T/1 m8|]N>.DTxU{&j ]jlӒQ-POBMKˮX&-`YƊ)ޡ)0'@0x v^NF~\& 'ᜲGr&5aQ]̋^X)RKk^O,-)t8L. >{g+Sl64jVaRs0 !/C lZͻCal2 )x _!^Rfz] tr_3І27`L~pHC1@{K.2Tq&Fz6%gwp|C[ޒ{?\^iA/P ,>I0~I{XOQ|v,w (,.6]vRbMBѽezE:ߛkRt‰1J [$'}}$+yB5jij`@@F mD5L\8w;ڌoܱ'L t,p{/˖A709K>!>iY8njk&=qfϊUdj5 allQ߷o , 4ttf<ϼIanl~$| a`)fZl pNJǜ%'ϋ[ X+J‰޿od"B`g -`В1b=@K!'qEER cRK ^7:Z&ZggiY q@ `ʓQ3=HxCkV|:P`Sŝ[;&Jq쨑aPTUFW 3PTG6 M޿L'J0\:hỀů-ja) ~]SDvgp#w6/ y[2)LMp sлɫS~z3'j/Djyy$Ptxd7'&}2?Ygm jt̉w"W<:BvF?Lbr3 _U6r']0FIqJUf$| M2eo# `濟&t&}4IZ9CҬ?r@3`o=A2`ikw"ƽ^*#\e!x=kxe:v˧<u *r N2PV7 ({il>9aL;AG@wIQ d+P={}y5Bsml-}</VV'eYێ?taȯӹgsP`58Ÿh6\L(γ8%1l"E!H3b; wdp$ƊW]cm6@qKO+`p@qh%Øf;r(xN8;b[+6~;s:Sk!}]V{ږ}’a&p*L{T0'?*^oFe-ogC3| 1PJ"X~`070ek[+gL Sr^[VbqW (GTZ57(w&m<\?s*=}5UEa1WoT GIon,IK(}|4Q_!D1$rn!ѻ}FFTZ3ƈObU}Ƙ5ST <eײ=ӏw@لSu띕:ffrM)?1P2xz׀>HP d|SAHcƔ!ƍ2 !~ǢU0HZ{Ԉu+S>`"ʠ0jۏ/!Z0UP/ژLU7Fa$Òj 1^ϛ=74eAncI,EcוqLP~ ߼0 mNPn wL;xF9 DZPC܈2_Dx%ugB, ) D+]"G5^˯BӜyM?>+q?nhZ %OJ L HƢDa:so1?[(s9G)`SRɲ$k~M4I[z.#2 X7N<\pk6T|:n{?Ev?@ bA. Y Ĩ" .@p&N YX){?ß֚ү!+9]G4'w6Eھ<3"B*?Kr^2Sn?~juwpXL} 2c 0Seeu "͟$q q9Wث[)U77z1؂y ʣ(QX]N _I}bN0UWU ݭAGA~L A?ׅM]pXaK(qHӁǜ)<6;Qqp|¡#xx6J |Wx78:;,ԽgpW(vΈx&$&$a_dspkP σ %[_W \Jeۜ/ Fٽ:sXbcl_ǵO)B2=l?a~WD @8|VXrC֚ ec-a⧝c lJucQujpq x p2N0Eߔf뾄,ӟFe |VzeUQ+n^*jZ|qwF cTӃ#rg 5.swD+' _%"@Qx\ƉFpT+B@M"H$L,Kybpx-'?\\X'w7-87M]M~PQ:bPA*"C3߽D=0x#b}4ayFZ&c',[Ϙ"v*6,+HXǞ^=ݛvB])"!?yp;]}ƛPIazIFӼ̒$s:VX47֜!p.)mLA7vw(M3.y{ʄ'p4 "P:SWRQP*Y`[70,5fd[Kw(fTNo\Ϋ?͝PNk\O S%(^~/.ٷCdH+D1 ڎb[&Qin=3 4z!FL~1r gu~2$',qwC-yFH rQ|ԉa#^µZXaԮksir+И`h>:Ń4Z+YfiVRx_O71bamzC, 4R 2ΙW֏.EYc@i-p/78y;`-}}7 aƈ!dG`¯b2kW,N}Ɵ5K!3g񢂎ӚFOEtPw1ջ#x6IF[h *-{vOh_5O8VON6~M eiSq! g,6.\KZTjzz2vN_cG 7a_Pr@!9æ2ݹ=i{B h%h0D0Ao: "rL}H((+SR /8a;8ALn,T78MԞ}±%%%ec--N%a}c[ZZ쓳J(rz0|qOL,~KFMߴ{Ljޚ/å?*>!vW22I07~zsyS82ti FW&\@ӄGI䛣\.aY PpxGPm(vwCie4u~9SLpӶk5?W42(╦ Y3/ARI@zq,J1eUOiRl*`x64F2r+WU# =Ok쾩9F31c\ڏữ䮍E:> {7hr%E̅s8xwarcb-'v>I) 10q3 P>&꺰苄Fj'oT+4 Jg¼wv6 @,}D Q/Xs|ڵ,&B`&y۞1x DNI= 0>3Ku~g)M,zaձ7m7 ~y&qAuuYڬ+>srs5ss%[ (BXHGc/T{GG[{kekiǽ) 7)t@Ao.W*tăGhz0ЙӇg2o?.  `;1%%J,^B(XIZD=eoTk0要bp2_0bv‹E,sȋ†ʌ{+<'=[v-+caL\4))nCtŨ|f1 C @ Dg!1/e65zuhƧA A~M^9\v !T&ynӄƤ!_V5 JbozMz{f <*o]dwv!W) E`܏B%00Y⽛ 1 pbT>Xa>Gt13r!с~Jo hS$Cd+ oSw!NŋSV:( &⇺i07z 0u\>DF*;sZBOpak'ܖ ;?_8%Ѧ0SL i\J w2'Vbbr'8V5oIsa4b(Vbx1X|W6s1²ݝ8YX0!n\oLS8uHQy{ݳypl0ۧ2H'CҩRI{X!(hu+^ܑ߿J͜&|];_Q*UQ1|#dJ8cn.O<!3{#>ᆧJژQ.y,K's:(ūxCC6[}`( `mhPT hDAQ~1-Dݿ|hbӠ:HBSC7y-e}QCQqmg=xY.s>j@D\Eݹ;7NcO_ҭ.'n2Ά!ZDYδM V uk5l{hg,Ks&72CU"V7 2 KX\ka17e; ZeWXrXDg?p|9fek%~x^K!Ů}i Z%\!>+~VbN}ulDn5Lq3 Ңb yGK9UN(W,f,H`"zfY@4<[K(~7hYIǏsMPݯP%p1='V[eK1X[}8J[Ij@)3o I ͊$C;=Se 2x!'Q:L1Jܺ~c2M QvyKMhSc$1I!b^5]/}!]2E f1֣#% I1D,7 c49-k|distDfT1HۻnB,H}dEwg?&Zz^ $\?,[);y GA^~ 6zV/M]|%kFI߉cK% <'a~i4YIJܐ\`^t,+j߆M,Xh|)h w Ymj=OV9WϮ5.kFqp:O;!2>LN+J[+yQ᜞1ԋ'<s_R2ׂDa?X!^n{h31Z$&!_` &<= KI}Zr cE|zrJxXfRGMۖXћ{ B\{۱m<ܭr«)glLM|0vh Bx ?^‹ܙ;;^I8( 6m,}Zzp6SUJw16WiR8r<4cHTESigeX2 ׻(yɔ9}z>\)5pWm];>ڍĚt Γ<(u6׻ ²0$V,acNr;PC #EV9]]ryi1lbUXٸ1EY>Eߢdnb+_OzB2|4rnn8FN1CF0p!r8`}0= & uF ̸*W)Q1EPE(`vKۤ&;A Xw,ȝ ?^LHM? 41H8Zsg2Z<,/n_sݘ]Z?zAbJ >%AO2, 벸 ֹaFVĜ/o篨LeR`˧Lɱr(+R^yj1k7z[O~keh v5F#żQ*d!ܭR喝Vb>Ǻ126E)8ƏQj_o<T yIA$9UdY?] 2|O _! kp!hD||*G8s!! <QH`b<ۣ$m}[Ƅ. P5˪RīF<-p]f !;mKPfǟI{.7ȊuݭD8j"Ɲ^:U&,8Qo&ǘ9`pMmcn>:wݚj i('ʄ?] H<.DZSz6iB0EA9xX!>TݷzvՓcS',)'8*D$oF#+HKi_$'|%В˻>J~= rԥqծ>1}_?H +ؠmZ3hobizEn8|]IC% 1.*õN~x9JÔ1+%B(Tm` A? B~)CN/`oֲGbCdzu[Z0,Ė8M![7e) 9BF8iaBkb >h@$m@ ^B"N-k1ԱJmL{\Jhɶ%X!讆~ X]X'%h 0Ɯ%II)?aU m)7hX:) ܵ8!|鶱`h%}DSY ݵܶ#l }!0k!u70^=*zg0Qw.Lxj \SFZ^KwN)Tv[͵~3UD`Sq;>ZumIC/|\ 5$8"0Y1D^trep!CZxYl=]4%Ly"$H c,,ԽzG{O.Yw^;QKWA*g2_#].2nr*IsaX,%){y}Ad@G`}uxS{@sԠͶ#~.4J헮}/G Rko Ӽ!jC\{g-Dr_&K1gbDVlyw!uaJMJ /F{X% _B ]w7{k|P`k=0M88VbpwMj+ߐED?GJ6=<g :tup18 M^*os+[X~,LƼJ?c0%vzKW߮ڑw| m!~ XzghGM͏^ ]<{L.W4pPd AGC1};)^#ΣW~QW~%ި| f SO1T2TЇ[xa_<-"!A @ AVou[jރf 8&q r/wU;M"_e\sMgwQovC)\]Jq m!p/p=(2P ̱y $N=R,a>ց:Χ} s6/n'- abO/-'p~#xTt 'a֤z8 (f׸8:kL$#A]?)|b)[;( Jl%}6[y@×vw#7bj*S}ͦl^ШwK?A>3@̽xl\%Hy2 ΃rIko<_3ˆ@izudO}E|$̾w$!ݭHmlRT(/Q/Yadz_M`v:eeXJ+\xt0}ƂQ?:uTPx]x{03?؏8M)f >PO9_.*NC0 .Ԉ(ڋ/Jxg2M++=p{?Xha ރi]M@VRS$<B`0=?,uL׶Pʽ;\Mp;\*R؍ lNq/縭?<8V!P4h/Qyfqۆ :*^G4R%&o/HapO6€$Rv˺A6Ce&+kI!m@(%X-Y_hgH'ivvVh}m"1/7 ,Wc<Ӟ<ޓh$E'`|&0{6m_q8sVsf90|aXa={P觱? >jO~~a<(~vfcZtӏ~̲`xaʷ%Fֆi! +;3<6U ~{Hs1yuiE+KH+G6i2A'pۻ8 շJk)ӄѓ)[r*rĕ?| W%d6gP^+@ϦaFSp,J}:.Un}v.qd 7~$y?h kp?S1Kܖ,q_$Y:.#+(W; ;hERދy.DVxPfB*M =8 =e>l{rzP{%_ΕDc~Ɵ?|xcmB޽s癋yo9/wM<2,o+ aXē,tS %E_OxLtO L')3sT{ hGQg(żjN9L-߸RKL(QVyO !ʡG(<( ̭fP7]zy1&]?MHטuF`hWNok~*~~5vD6  dhK^בh:TOz&n =pYs1mqDEqb&zf+_ |*|\= mL?Vɺo}k>닊aS?;JXҌ%eJ +Lv?_2|,NJ8YҽGabMV Q"PPט sRyJwkM)Oo~NsG @Y*_)It͇jR"R>z+TRcɸ KF3:6-r 2mo|JI~3MzB cJzU2kS>=U5EY,_g:6AdwiɥgW! '!х 9f5 y2"8.m1a]E–'~F.{ЛʗoD:C֩#$md<)۽a\ځ?08)[(R=9Hs.9y{~ ,{b^֥xޘy#9kf7qwKlK! ]cZoݜ eLzoԕ~{A64L1c#",E&c쇑LV;l{s*0RZiD?l;~;) r^)bqrSC&c~@l(pr0ҌF7cf輷](RF3v n1 D&G5t!Ww}0$gq]!Q}4t&E;qB oc IKV&HeTrw_%Ӓ.+y(d4FR# ( l+ x/&ϼf(gz#K {h<>ODĹwv^q[~| AC*K/9!Ě?B x,_E4Ka$\Ag2""Fڐ1Ÿ s<(S%/$5e9wu)!bǍ&oд?r2rNO]O҃&;q8l6M6fU'X10Ň@M>j?Ӗx_3Ɣ>d~0vw,В~TlNBQ9Ϧe7/%m@ˁ3W6}2#b)jr Cgd R̍`9&Q~9RږN"0j^/qWDCZ|/߻Π%)mRI44eoPӒb!5H@IDATY*C.<ڊ|Az6^| 3ڜyunxhڌv(8co&m> /Pq4yR `phtmi@VI]&N2aaRWȯ ŗDh@wg3JF* ;AZ?O`JѮ WZ̖#wW}#i_;f~2!ާcxZmi3o)I }HΙO~.,{6.-]l DHQhA&%Daά7ݣ(bPnГ$L]] Qnkz2͉C-W3x`pU6{vOrORQYx\t\ޤlC`N\-.Ćd]LfqnvobF [Ϡ>ayD:bǪ8BPu;*iɹ̧e *3[guJ\#Q{:wyЌisQ}t) PVxUyY; <qvYL,G`Ƀn0TJ#`/ُ0+cv#[Y0ZfQxӄ7ZQ ܽJ0Ͷ1"|F iL4])~6(a?x`̘!x1v^vģИ)bYN~pL]ө"<&.m'gx;y~^ν0eg PC+j ܣx Ń'/CU0M9t z{aE2jJh +to_34HG8c.({8;6a[{b | L?kFT&\vN6pl1W2nJg3?YѦ"W{EW9$J6\!,H8p;SBU-uɔrc~6[Vvi+'w!/R? qO8]%ORT.O ( iP]RF1weܾ[KƬe{<8˘CH`*/D*jifb(0-/ gQm XKq`+8s1(2c`: !. ļUHtg\FXMjY_96i<@G 3ݠ)Q|.;w|ͧK2.P} nr[6"pLoPW11g_NWg=Sӈqkw\ m}'e J X8%t> P& tjP)hs=M/l۵\\F -4 $]e/c[yW2ݽ OD{0c֝`sEŃq5bR`uQAI^KXmE ,sg_jB YD4l!P٣ifD{yIKUH 43Ƥale%?֦tV74xG4CԂC_Y!S/4Jĭr0CWS!6B2)ğO"ܥq޲Dph5v:g fnl됸-5&,UL2R-eܹ-9k1:4.y޺?, E#jCn,=Pv-mm[)*]y=Ctaя^IEy$]T ;/-^mG}39ay0qN( >+?} #l5AD9W:Q-Ȏ\O\O]vѦU.IX udG`gC t AA<˃֗m&3@Iq _up.~qbrDȟ a? y` "Jo Γ!v,;mq6e׊^垷vV&֢KhP4@77CۓI6)NdnXRB}v;yi/*7/U&(ㆇmxK?W :7pr?Ix6X4%Kn{'=s{wS< ?ǔC˗lJs-- ۝9|4,G<`̃g{`aA[a/93pQ{)?tHBV^ i{+::1pvtWԏcjY? >8I yI=T{nW_cF8,u#"s,J,/|Ss" 2)y;G9+%hv*D yܕ|@@恰 $t6}1p4n#U0Y0zֆks&N°v,#'`aeH)M p8rs&`ğfNyh ORx)v<|HͪǤ^.\keM3~7KcL8 O((nnK28|׾}SI8B1vxjz /xgڄAsPyLG茗ė'.] *G8@-O@`h\ܛѴЛ`Z3sy RăGѳLR2?8K#gN5-yO_]|\Echqwb `,Drw`{ !AAMX xPېi=1HM{fћ~$Xb ЅmpX"\;h0nqyL|Eچxy(ׁa$ۢ\C?T&fV'? ̸´ L\c~Ss#do6a * ?/ `*,7x `5IXDjWvmEoS:$nC72NP'X¨97F݉8jR,ߕ2J-KKvwJ"&Ѥ2tSqk|Sgp'WkYP=ko׼\ uS%`5IOhPg'XBO$}ՠ/d3'>o̧|@ALdJMU,0IL3(^՟7Ԋql`{X&J8kjb:5 S=j9Y S8mT~@nxy,8`0X?<« {&/<6ưlp *  0J\/7sO+%PwS~sRgw.eVsw(W }%ws8ÝRT`u!|ny0>FKy+p_]jWk+pxj(yKCכԡnez/qTv~q=类> W #,2$ds!-1Lb<yDf.P@-AKoZmqXݖҳ2ovSK+=^dK `[Oena:z'tjO'Cx=bTyx/ Dp9 =⨓Zdޖ¤C2.VO![;IUyAOB;O>7lvLBM#Dթ9 ;j/~6Pp;FO+0ZeDho0F(eBu;Was<>{bC8 a*덥_z6?y'Vcg2m?R%Qb0!JO,*z4I`JEs'ʼqSx}(mipχO}3J,RJ󡢧Jr.O" |p6L"muAYnT6#IQLYbD8-ek)'ַBV&wQ2LE4#-sWw¿l3塃ǔWT# ^Ҵw)[D(޳i3d -Kyo3S6h.. ׾Vqs<8xoGغkzW<'Q(+@R< ">Eș8!(bM܊f; d'O +F.!&s՗2^KZK#S VK+h_cj@isfCOH{zm +QHL9'yΆ< }0n,;3xMLPвZb?k4)idMP.<,apLn+e6z/I@S+d:ii.XLt8i\m{s?m#1BfBJdylU_9La~:JK_`cVSiDP$[gIrPoteb'J.mD3QXQC{_G+/Z-}g>h~WVA?0 pQO{3D\ ڼ/W.ln='" +#-kjw74yTRQ/B\%IT\E۵X<"3D=0ube?_̳&r&ziӮ!M ~#n͜EJMt9yt`/i!70  AԝhGrh-SBt¾ z.l[؍/gsL\Y}3,IJaλ>fu)V e*ܡVQ,):OnajOvv+񱜿 qg)'њ1vH!0:U|<F{6BS#6!иdJdw >>k?νs?P['ZBP3X<_պ'WO~.i`L2}G,mg}ie}#|U'Nnx Z&([TZ ylclS߉ذDs*]9 @>DV1-Ѵz-7:V+(t#JWDƌ|dDX01̹|UPvWpai*P,{^Q單z;Y u%/rnP $6y6 6K|reR^ ±!aA> ;KZ!XQhdQ۔-m=r}a:\Xݹ}= 1Ewqu(e12t(`Aܺg,YV/`jtTO= !!6@tX%`3h5$ \t u W=i!⛒p&4yB7(KSNDElއ@ݖ mZ_ʯm}JpYm@c,b|h':kʎrX>)]9?f IQ8,ua P/*S .@WH'4e^n9Jo%Ips'+'&,8>D~gy6<,Jzk7]YKEtM8 D%6.TfW>󼸯j/s qђW֤2GF- JVowQ 15xbkULNG`ӁX]ÇNnqw%5iގ`K(QlΈ'AT B,h'X"ɵª6'ܫ8, ~Re/z5mOF6Ks;sȥN?O>\O+zlϪ~ wgR)D@q,PھQ3}l^Qo^SBJ^+)&}8uJxI'K~A/s },܊ =ą!zK GsDJꀎ}=o~^WBx>)gRB3ӿa\|<<y2l@VXJN$ _˜a-2`3E[;=gw1`P6|WȴFw;By; }:(pɸiF@L(s,=W{GMp ՁG8?D]-U`ҭlL9UV:U. I8Yl$_1xl1BY6X@gDo/SDM)6bC^o &i/⠙{l)/dW suLÚwӦUh샳ԍ&}g<`X`,d`\ jCp-!P"ks. ^X8m1u(KfU8n)No뇀/ 5 tml0b3aPnD+Zggr.c︞O(ڈf^ݹ*${ 6 NUx}(tOZW᯲Zj~Д'4N AKԉkpK4~iTd&)W}xt)FRK_OhɺGJ{x"$c=PJ>Lws'9,ٖKy /=J|D }6cU7}p9t0k mrR.#X(E |:[`S)r}%h7jkMpDZ/XELhXP۟\\MD6Δ;͝ knCy)2\,-X)CY7Cy' G&AHnFmXUU6jM.[%O̮bKO ,#, ^IH4 Tʃy"ZFR T!1No ztPk80x%cKWGmIF:v=mm:峨@Ii1m}y7SG"M~)ϡPvQUN!868izm:|`TNwYFQ,ˍ*akD > Z=}@1 /J߶TuA-N#TBQ_oGrXe6Kwd"/OHBd))S)ʶa:0¦7Ҧw&1.9=-!7FjPKkԭ2 KA]icA`ϓ9䙤a&wǝ,7}"F$6}suwjYz+2Ʌ`S30cɣe|ňK#2]1xF29%w8o5oR.h xǦ9A?}Jx&.xXd%4fJ* m|(OҮ'@19~OEټo (c)> YZH.yLW$}/8 @ /D&,CLb:H@czXacn Q L4.mm  R=4vo]q .wޛU;I>S9)Z{Վ ˫G;03[OiH[%:ް%˒w{W8Pکb VD՞Thۭ]zVZ*i/'n`n?\~3d݈BujԀY`60/lC/vbr^hƪ N(xp9WW7>fKt+xUͨ2:cNxD4J_¹{3ߤ7D8\t{p^=0›2fpٔzЧ,"<* [|zT;?@6-*[MQ:gZ|oBZ~]Uk4DaV*ݘm 1I=nލݑ L^&˒YZGR9UwR,VWrT/|>QA %6BӻºI,0-BV m{#֓D6yo}k<Jxms>3՜&0l6M9han5K_De=L f[Rsecv ,Ԁ_6fKQb󾚻=Ѝ;q_;y~?ncTKQo^3 :DpWW'MI~Bp/`.鮼׽ѝ{J(꒣4 3 A)^ ̙Ko.%F0>%}|OYyOa{G^m5vjcnl4Sy_O!Mi66K(7}#wpNT|NJ d!lQn$.B%F#Dxgֽk#%>%ܸY\THo!wzCQ6L>@J\?/g!a~sUV#zEAyT)ԩn@'v%7X*žKk))tywUҕʺ4,Qbi">hk hL$mcc%WWc;&x$h;UR `{>Vw\.LCd)BTvɧ@{~ WX%D@ ).\$LMd+#Bޖx;!2f?a u> [-7p7Gd VhQZoa;=o#Y9@oc)#r"@`V#+ͣso,1+s44O`tnӻ;Vnj(ODž¬B,l7$X,i㲆Gyƈ6bDGG w_/,w' ѶgUwGgi|u} '~*UP֓|?#@?ѧ{z?aN4CEj 6IJ`˅K~ *>Koz;js>[uP Cne֗9ġ^dύi 8DC p@c}@Ļ@{w٪^;,0u1#?jiז\G@~tTS{𼶔[H`0Ѻ+lUve2a:[o?oL 0 ._g_i8c`MK֥ eE~m*<Magwshm7Ai*w ma­jHZs<jy)3@sW̕JhhInMz{9sqD FJpGdD w-:4nS_xcC:N>po0!`grAYNAmȿNխhp]܉ (4#V#! HI AZh]LAO*ϯkψI+s p?au>Xۡ֍}X)fkѲiwdaW@DZw n-rXEh5z /d\qbQ,( V4OLk^Q˄sc#)3-n݊n?83R2XI*U\(\hV<3d% ۛk܃)^T!8cxj@Ŕ7ysa⣛yB- #a<'N0Cx _kWvOSc^ܒK\2dn㹼k8ԯzW=_Q;T#hʣ|SZASW彥p:TtrT ͕  ![EҸ}<<ƒl ʮkӭ6@jLǁ0oUkZj[TDh`!|#S}+Im~(-ã#y;9G߃r>BP ;cʒ/'AEԈf!i$ލap$x4 ?F)E9D m.NDWT&ے8,q w J):;m쏛{=K mvUb.\Y*y&y Z6 6z5%SzfJBϺέwГ3 1ʌ [gP6R>xQ?StRO$1-jޡHjTY Tv7Ү{gss,8q,MA 8ڕ 3I;U!?$7_~Ђ!6BY%JgGȰpsXm;Kzˣ??,ZB\)7$i #mo 9Z۰KΒ'fe<,43^˲/iN4S9Ҹ9S{iĞU ^$AS E"(q=#[ x^Uzٲ\ ~>P杺_,5kRY;*vq mZ \^~vіIq,S/_9u  +ֈ #*FD)h1A~J!rh`?De3" Xv,ےD|F-Sj6r XhaB._VA!0<v(# Vt 8"$iji89'莰l􂊹ؽD o!8lY[ԟq-y7헂m /rNV94:hNJ+k"y9'!܄kaoko=C/Fa. a[y,@ Ff2WRL!F<@qlW[kH}VOi ʔsR`֟zٌ1*[lĈ vo BdY|Љ\(a%$@IDATߘ>-57$Z 1a\ogs$ oIyi5fN)5Lc*OYGN9<T3d8ۤÂ@4NDq?aY4҄/s&pɎRx~ۧ6}opo4NDOc7UAp[!wF.g*[̷Z1LO8 ?PKmiWvƋaݷ#dTXgLɀZ"]WK4Gwźo3MpeaK(TQ'"& ۊu/CkCX~m~%&By,P kxeI?4GZhq8/ՀGV/ECNR<~bK\O, h668䈛bX(OcεlxfJ6qu KU5 m O͐H+f3iGi³Dt- .bm!>Bf^=..7c6 0<0T h)Jl6>!`oPl<_Y.&NV(m euF2il^GyB? ZV>9q0'VLc+qkrGJ4z. *$BhqP9W<$u&gਠA]Ŕ ˉSa3ߔ<5]<6hgx73PhҽƏ/yqr/58PpsUWVAPdsyY*KWh,Pc 1@ZȋdBJBz1祒,7 :ޮV-xM.$*ԍ8Z$ڈ7fN() #B4cQA+T ]wYapr?%H0bY؋}mNBO>,{삈=5͙I(P'^~1w5?ҳPRK®r \a;gwt±I0G/ X*\jB?YSdR k{kM9&@hBZQ0}OvDc$Y{RcE]^5JD[!4e JID7~cnס[ -b`A1bot{ xtLOt o 6AHocx 4ArBp0"8 & Y!sYC\͖ HO۲YyErF1VA@i9/s7`Nf4zu@٬,*ANɢro9 v'L./fisX_f+3T8P@ M]1}z!Aƀ_)IvϦ6}Ǖ<]Q(e#7lBQZ8NO~kYxI䡠yy2-\Z?:$@ʥZQ{* J-<3wuXƼu6}VQ;Auӄ܃v+>@]C_Uqz?++zٳA'*_yofvF͋)M;^dFԦrX Uۚ](Ѿnwv :^)oV+r8<& KW O0q!TTMs&Ha6@mɩwбwBͿ;̿ q+x̀[KX}/evL#>PNh}*z_5WJhYє'{6mQ YF!ƣ?b^׌ao TXs$2i-iw|JJ Х7bP9E=U@a`ߺu%'I/ [ 2ŻAqY*QB!rp$2:E! |xvyMs/RZ#<`Xns"l! wJ~/@>.J? C½T 1Lw֠ n_tk7U}< mVT#)`K} ٮx??kع_nYTo+:m)e{O2|q Ui8&QYJ G/ AH OB(.))Ŝ{ bkEy%"ح?ݱjA4w#lXpw r M$/ʬ;rZۘ'[(am5y75ʄvN^>53ym~K`5!ZU9%CqVM}9ʦA\h˹V|q\*e+8~kwJLVR `0@ \ϽChFD`4xV y)QD_KF^h6 .MVt׮vuoȗW#ksWЬGnPmHa(+d0ýFfT.CߧxH*,,̌>*kQ7lt5r1((7ݘY[Z?m<5`*:aA|\aX@mW$jxe\mb`'cNiW?!TEX͇=}Pk&oQƀgsAV aCQV$HîVc2`Ųf40GK`"zIM$49Omcv4\ E݃)@q>07[#{ͫ ژ9%a,\?YN,KBXKQ/Ů:z~?ouc+ ^:yy^y♯CfsJYǮp+¿ -3(-r//՟3ERVZrjz}mP0vJ~!m^9J4_fO+:WYs$̫MQrʨ5 ,lF>5CN>$z.ԀLK҉#$߬}wKO-Bb{s,.l.[% rb{`%΁ãT=6QmzrdO.iYZO)FQie#eIj֋إBUfg'3*ygqrN/w"}_%}v'W/g*NP#V} !MrOÇc0_^pbr{B/(x SjRB.F*Ƨb|\ 3_OARKm*JQPnya^].?T~DKp]]6e a{Xz6;XMAZ} ul:ic֞>$X+± a]-ة(G먜KB"͜ix) Ne<(d.{]R67ؤs]1;}B/[,F\/ Q`W)YG ͯ߳!6т>e2JpR&u,^z p~g8mGs8λ6$:?ц *m9Hmxē2/ɦU~#+7dvߞx%.Պ0 k+wj$;2vݎbC@#UAo EJ#'iY5ć]fw[$ވ`VK,ݒFOuulBfή23c"0} p^eQԶPlb| G~/)< }=Oѻ7(׿ fQ8`J{ˍNm*B*W/Zt.e ӁlC@ CŔJ1h!s75RpE ls!oy=LŽgu}`OP80n<^TBWw?Dgv!"%, Tϫ/; l mcQ=LJlDv%boȅ/AjnIMqGǔctEkklº}k+B15L3F26X_^=Q,e]ȃ^Hs3x)s_\Dw#*m,f$HJ9b&mqRvl3ۏbVgԮa4`ʛOfEҘGݺe ff4LN] ùze)6%\b9d/h </y'n1sc|Jt処*A(~~,&\4h»Wͳe;$P ܖv6(#<ɘnw޹mј]UZRԕ9|X1q㙠6+Ǹ)xGh}[> mx㻐Aŗ"6 Z] zDyn^;\se *Ldu+YϦQ xv]Acw6 :1xaX6&ۛ0dcc7cǜ!oBW3/I%V_y̠P>S*ۿ̪DA](?Vmo\.0 @ܬqEKA>h8呫$ecJj40>h ' IK An,h\%;Жȹy.9)hzRQ[ –Wwv2d&=}ӐoXpq?^:EE7šen Y2rcU&jt3ck%ZF~/-*b)/\R9<#W b>S#K//'Hm`? l){8KH%XJ"!tR& WQhcІkGQ^Y4725'.ف%VhSj(\/xuaA&9 6[(;u`'L&蛠80c9OnoҐWR $BB2|RXG&6Qҋ٭>B5v5^o<ͽ2\R7Š㲜Ǵ('˽YpIJho^Ѷ1z 7RN(Ũ0g1G.sϛ( <"9JSyAB9u燫C7۸]v!_^a#l!'"$@ݯ ?V/XPڽB0Ux8N6r+iA o{c¸e4`BlA@aOwSet @xk}ͣ0t7JӅ'~ d+,mr%Qx4V==_-8GQbXpTO(,c]A7s7JSYv?0}=九rou:HDR%K{mvA%zkAA ixY@Q8Z9]Ǽ|6~ M/ 69 n2K3z0nU6ƉUT>V:֗桎ߜSƫ#G km"n}׫ E1r+0.xP ipEQhKs Fw43: OH?аqzh2G:Ėm)Q5@+*.}qYnҌpx ݏ.].?qsa@IK k`!Xz?RR@j~Dc"pdp'ZF`.YqUq)=myb!׎b=(x,V*86y3کaKѝ69ݭ W%PXXIAale181@T|o;W:\7mТK+YemsWbMOoc^2Ɍ%ev+P/nvgy\-,+ŁU|Kx-G_]!]A E^;A+<޴i d@ B{IHiRЖx;eHpg3!KW AOQns <ӑaOk3c@θ@sʇX]ʊNo/5jrV5Cm׾˕\6nX˘a)UdER_>xs1O9Qrs]eS5KKDW(On {gjT HG(>0a4Qɼś 7绥V?ɬ=o o[t$_9!(\0 % =Ah'\LwU4M[ l0bsos bu1yMvlqQ$dWwYE9kP^Ӷoy+txrF' 'FOל?5\lȖҕ1SNuN J`-[2C''`;ޡP0 G6$hJ֋󭀊 s|:y)Gض= =4eo!7bI. M[VVa0#$ʉ,\šB"䨥8Bpn s*-W)/& /hJ}] *RY2)Fa~{qs SYFMMb'Kg=mXzW軼^:3f{ըZߖ/@@9Bf|Ijn X.Ցg+9e_neu[̷G?JOS) C ,ϸ[mbm޼WJ  y8 )cɰ{wӊWҗ t _A/vjgWg2Ϸ^}/+aE BRP=q*,MC4a%yy v'Hm(l[ZsñRX6F/mg/;[wk/ IMq^МY0 7{߸ _+ l^J=CF+ :{Wv̶ӜlG'щ [l 3,h>%8pYwi*r1"Yq甛s~x<]FԠ|Y`9*2U[nMRb}5h>ZU3/Ci9maRgMsxBd˙ZIZC H-\mb-%~A4g']9V0r@xn7owQB k^0=X[u)hOڇG!Xh-,Yt`e4q_'aʜ̧IFH}U]\\/DʑJCb?h~Y %˂._p5X7{'4&E3@"BocI,CԋDe]H>{1b.MHQ:fo9R<xNv5!ePY. $"q87}q-cX2q`T5$9;^0Y4¥zec8j[q茠fo\Y`_y^W _.t%5l~jy=hŃwn|2gc" 5,n%BmF @O|uP,! lLKҭ=>rڈp=h_ds]GAKg d0C-ވK566a/`ILIw,0#TⰇi\tX[vS.֯ˁ3w(:{V>AIuD)^m^Zֿyp(Fb)"h}30lKVg˻J/G9x[U! A AY@{kO ;!9KOc 5&ʀ7AR|Wu!I4xP߰E[]sC^k9 UP@iA9{'=)/['+B,_ 4/4{ HMCxcK¯#!֧Nս];<+Pvv=9X=eYϲvOcqc,+ӏq;%+@̞՞hJ 2#qzjϋ_]h {l?eUj[yx/Kx5OT`)ڃ #[4g?Jgl*=;aLBG.˔,CvtcU|}'}60o˷zК/+1y}՘KQg:"dB}ǔ*;dhPSGx̩=Xy#J[BO2WǞ l g${^؆{s5*Y%ħb@wz݉hY͔֏*k9-21`r4}L3ބu[Rndڷ܈"`hGř^'v{:Bm?C({:OYw삠IvE44Rfr*!z?eičx8" T}.*uiC D^5 E,|Y4F1u'VB{  y.Cf*C!1Eg)9ݼN_.^1o|RP2ܮ[;7MIyIt&kY-QhǻR0*"fqy;ZF(PVmq/,V)  OÑ* XВ9x< Uim5D R}eROEzzl.]q#ҡ'9.@X,C˶#5y~w&1z}[.bj %!nIJ%50M>&MLf; gBXsa-|/%c^{1 !WunU{K+VibF^tYnm׌)YWmW0a˽ `/k!^ϢQE$$7ʥԡܲ>+m}#ƛVhJlmRľ : 'œC#,^0xN/w]Ow`Pg|ZE}f7n<UZ!|//lw.۸ڑ=Vꀅ^|/q$ǂjan#^x'Hr*9+ 9bt`1|f&rjCxysײZO ;"4ukݹ&73ܱ8GZ7\ \ 1 d?H_NxaQKl>dBT OfK>_Fp:)0X91~NWY FnT0qʊ1~njw?y]6S֋Q4 v~*8lMY% ALZs*By"q=94?OC= =K'loyYW*ÿFG>U^br5&h8oTqğWecZOk`.c@68+Z %ȼW,Hemu)z3 @)%^u#=9XBQC>6q'B  ~YoMj׽Lȟs*ވ%QClDsvY8&P,ZeZ=("/7^1E$b@SO0Pm,*.VG!1Z._vD?_Z#ٵ'B,*iѸ4+୪-h㞿|^8WԃԽe9EEjS0aIE,?RtaOYn-SNоK{R0xd0饊KifO…(3^ҟϸF=a۟Lے)0cJ5i/+@`9BO@Uiw#+tXNnnU!7[ا-p!؛;' nS 4 # ^2w:wRW!AoGr6 ?u]!Z8M_"އ9V^ jל%0#KbnU?fc.c̉{ezѳD<="Xs-Alߖ=ۏTm`3;*q>[6`YB-<ѳas)z? <c[윮I-47bVlO %\a M>&-!:}TSPiۿ hB]nEmYtNѨRtm`V9'bhGՔ(r^%R@+zC+ch,7!ׂp߅oF!N dq[C,ڟ6gO0lJS堤)'h rt \ր4? ^L }FlaFS t^͵/VlUHc6~a`⬒)'M+$⅌V4Kf ^TIS [a |4r=t .|#Pu)đRX+MkaEXi~Su &cp*Mu4FE=B+B)*)5¿3 `>+x_S@W 5j=!wlG緌1̑6UX O[9CG]Gv](3@sgyUQ5,Q + ^x~n5psv~\wL0\ f(2 g'`w]iG 816+?b!Xhn?)תrWuCߥJR_';Xq?yXEMl}KH*((KhJJy& k~rR 22 R.>e9t`98%B "!-9 @8$WׁT04D)6O(;%OV܀9NfbSܤ6b7IE v[qz+C2Zv_S?\т1 [(r-ly̠6X?"%±H%5/@׫>g D&\h4A:T?B)`RZ ZQoyB(GBŽGWBފ'' J!ZTPv<Y1يڰ~-:]HV--=Qm_M.,a-imˢ!"p w(8~\03̢)fxe3XQ˪5hg^xxKxLP\Jc$W,vv lZlfW3yqx:mIG*rmRN X6} pE[W A)hqՒY֚O^RFTy7hL+Wދ:n>$y"y=.+'7= ywlʷׁ؛y.BtƄU}<8KൿCkkI]OI]Ђt{V ɿb6VC!C9xܬy.o 'Sۿ^mi\Y 90q]m X^5K ܮ渒߿ky7R3\[) M=,#}9ȯa?uWzBjmq^s-$p+Xc; 6;I%o#vV<[+ {yڋ6/B̧L*U/`,-:݄?H òH-Z13So7o?CRI} P;RxFϤe?t`pg 9/ W!Ag v Hao#.r|p#޿Le;sRO-yV)5LW-L@EAFx3𿉪ʚWƒ0s+lOd^ oDD+@\y7bRfJ)e2҅܆ '}-zᵢHh,;7l3uU?F] ;g}67Eom5؀?|` ]ހDr!Sv|}B+\AܺAw%(y~4t1)9lApqIDmM@hl|gFo1XX.rѳdzlQc܈ܖ 1y.Q0!1it,-+4 Ʋ۳dx FLj_w MzR$)d7į+vNYiL(n;gR ^[7P@ :ht1ggSѭ(-elv=bebe9 b ZcڱWۏb;XSJc3W-nu? ;\0F ,q?+c&2Q/1)Sǀ~z/_XV' 7q|יNY|$0V/x88O<łXjbUssײ8w^wAƂ vpږ330'mK~brx<3x/Hi"8s4oRkxSg @>z{N7pKDH"=l}nr7G!V-ޔOuIlhz}Ӝ`[fR\a ɝkOqg˪D!0VegOnp՛-hŌe5~gSlI)uHtOh/gS]k uR P.~sh-fho#j8/q_>^ Ҽ2>;»TMqi` jMDr5Pq/@W|#Ȉp~͐s rJ"v :B+mr̢H12 1=:Iƹ!LP숆Q <061[=NyK +&̘uLx2y8Sy!~( v s8cSZCIE N?(̗J /o`f-mekRtJUSœ,l!`ܕkVx[80r?ţ󙓌EMlSƄI3txKَ(AgejcˏEQ+p{L8ۄ`"TP m0~s\.Z.= زp%zay RT??= b9 evN߮f>(͌9rZ;* @Xكo+B@~|WOy<*dݛ:q^xí9? SF|ym -#آZ "Eo,A! H,rW~ fDp^M19+01ƥ@Y6usju͇%t#zk`ܳ\;F+ ->9W+T׸zڛN8Sm.(gq 1=.#+TR'׀hE)D͇݋UqVH ~{9}m*e~DܝW *,a^ 2xd7( nY=nm1 v,Ֆr6n3(۔ xn|)ޥ>PbKf3E<)Es>}gKֻ@DxL$BEGe;O8 12`6WxHK6SzX8G/@W(AʼP w!/Bف@pl׆B|M -3'+'|V$ ӟ=f11Lub;yPbdML*?gb❈Q٧rpZ{?EK8B`]5u)mL­ PաdȎ"lW-X19{=kT\]fF (W958!>;-Xg`HPa H\l^G;ZH:z9(pPt"Arr\ut{,~/fvCXJ'`g Xrb v[>e~a,\bX1bZ\-IH;,dķzx~=[(C8LVQ_VpZ񱈋r]IsS8$:qxi/+{CZC_*%^yd h_K>)^ME0@w9ҷ{_7 ;ceQqW ^}xy)XGyfpSdݣHn/%nm1HdʩkyƶV|/C`j)81 v237Yu#`eUF%SZ"{!f=6jBjO UHdsSffy!q99YBOne7CKX,N I]Sфx?KMg؝#[x, xw>9rao>$A2K2B y+ߥU ;p2xdÀ@W@S9^#(!6q $yQ1:1QLW[>7 b"eyY%HwXEYS\0U{A޽:o?YOP|$, 5\Ľ(!n~hv;޴̀IK= "a!Ꮮ P}" O[ ?WS*mNv^ '1g?IRLHcj Pu9j},s2n&WI!@+ġ[礳^+p:g9*ӆU| Xwِ ZM"$ ]J{ҡhr_IɊ/[ 6rO6Y ;,YϿϸۜOz%cշs3jsQ,Fz6O뫝ceS\=fy~#^~xoIjrVٙ_GN7Rœp+\,oDꨧHL[_Sb<X|9z53VB{,zƠ,fe>qܞ~S["'!oBv Wsi|WldJho3XNWԞ.8]p2OA:9ytf\.;>YbXSaK&вk!!CwvƭnM4x9j5[{3 y=ޢbXmas21Xb2g3'|bn =#qGRng~-GL6,uvzsj'+EN/M rT*.~w""]àj[)u/Gix%`/GYiRpz9RZnqƾ+3i癈UTDm#p7.6Cl}|`{ _QLbWyN4d0xߢ'r,5s*U|ۡCv$=&{#!~u6":g#97%%^Dzׂ\ё<]Y)ykXC?~G?&Z泞k%K;LNMnOVwƮyd5-'!Ϟ:f:*RO/BGفxtg6^(kCe^[p^;3/~-Q$ԉɽV'M^h@]ޜπ0I{aV潘qޝ^8_o ^gW~UɼuRb^t_fyn}]Vf :/i̳5fF;t5 DkyF:MExuq}0!'^-g5EKՃK| ޿o;OP?ac/E\_wy!:z}Q.৶_Th~ut jz(VZucL qeo3'y1waWH|{@W }fİ:'AvKO]8/wAm\rwG+/kE?YѶ ϻ#N@05d\nL{¯7 P kB0xV99W@TJz6':C9 Esx zz4y1]єq}gwܷgQo<9Su0N-.#WÂ{*Sg%ތlɷ9j]haY_9O~1 aǢhu3NAy8 o΃>]xد:s44aLB=+}  ["2'vNr(!ў?ֿE2t>d ;սG )A9x}0w376ݘ\ {Sܞ;zt( pPeYzC 81y3@l[!ɻ7 ܷRd\ ;-oJz5Qp~)`#46('o+C73̩AJAl%+%wvL$$xM sz7W2nɧɾNMDR>R #e0ɹ-ux/dN}2xjN`7יBSy£]- =Jxgѳ<㹻.wпy$m?D[Nim*= Ylc6#K@7|N|_7RĽy'_ˇ/PRy^ *ֶ19WbM9r<.ֿ^ x0=\ #<}bpTb(,K(o俨!z)7#(Ƹ{p@'kjl:+Ŷ[rߦ>T%+O*(J(ʡ ?6g95>i/&*IDƂiKշG SntRq?āʵDCWRLW蚕4':,JE +^3:l(p@' }2!Xo׿ɧG\'9N˝P&4¥П!Z\cp}PC˃2|m[O um88Z33ojus.eT kĘSr9HiI-Jw7{k5yW̜QB1 D)X^1.ķ};y&)}/za^5ϟǶĊ[yie7-]+kX J[Gxhfor.U-B~ ca~9w*>liI<2 m `qcׯYr Ф&A,|gS `E)8D˵A(Oj@۵»$KH㩹R0A<1 OsKma`k-P{q6y24m8{z9 'NBG5ggEʭ'Ԓ(JrTh\r>/)Yh%&mj7rbf}0E|Fڤ)߂~ 54oPʠ+ө4VJ@.Vư+|BXM$nЈ׃?˷Vcܻ쥔1PHyr|99D&ѹ\_0b ]%۳=sXlǭ/kHhF2p D?mpZA1 sy+Q@8-n\= `]4RiғNLMYIY-ͧnE)v|4,92 9 p,K@qRb54e~V#ďNޮYBXRN,2qEVP60)#-1kSV_{yE0N;9j~ h}o FXbn17Ҕ^i5-PbPP8e y]J&'Fټ 7Z_M(m756UO/ 6B;)oė#gd-}1JOUʩ%ςQ^sv4+w1iO gC;è7'j$AھXٌb4VZMAwj1rj*kB'luw;c1o|PʕdȜ|$'_AM>/t7lC T=m}2?Sy>LܳB,j RP6 ֪4Fc3R=, \f_ b+B-={y"*YV7(yv6i~i; gZ67~zQkKA=B VWթ袑TZ<]ru§WTԌ"(ebZt[ea)T!*zzy /-~*Vv&x!{2?[bWzux&[F62DK߇mLt)K%@)8j=c,ʀXz)״f,2˭XZXqկw%qcv G-~)RuMs=)e1[|ZA:`gQˬT{ N 3)jr-Jx>kaC-8iY-D_[21yɅD @;7b>%F9\(fSvBż4D7_V"uQ@( |8LWrÈr;q,~&NZ3 PʎՊS.S0nQRfIqj<}|o5iVrdr֦\E}'@U.X\oiaq n T ( ~o*y ҳNĵ'a<0+.Voyj5`羴s@`>^3gT/T\p;ʻ`Imzh\;cc{TTe/ZS8k)A6K10s)#N_bswО>Կ^E {; P;[i*!??a S.c2{{ƈ:IN@ #P>9ڧa곾+xV<ɒ]AHR){ )}o@c?Y{C:|?Ԗ%.|ɦT,`uroEGW( l;btZPNEkcKRX2O^RjtQ1 LhwXMtRzzy L)"N zVçEMil{*jnK޷'t 4q4~ ye>sg 6`s'ϕۡ ]Ղm%d] O zehJc= {9N%kUJsKc^銒u+K~Y+U6||mS!"PP~] [tF@87};9:o4*G0BU^.v$H@) K{?{ Ĝc[jGo{h9&yM[&H$f(6k{8lD'aǕ|FEAݴEp];bpS GW]*fE2BIoF-}CeM]qԲF`ABTgV33Q kR%)Tut!`Nh`[V^Lw1>>9\JZPhKHK^ݾ9QZř)T]؎Јuks9~.}M@S2?JhU)cnmd< }X!ZC>AO^V>w^ _ WvʻVPln7`W{LuV^!7hP&y$W2RljްE=z_kޯw$4֪W hoBq>Nz|}և:{oeBky! sL]eTrrNqa`lgbkwJk#9}QOG(b,'eA@ۊrV߼"[X,{ Cfx,tiK0;ZL;% 2*n.6yeTh@(t`=0ʥ$ꛈB/ɝw9$F;Uf˲ms0^ys0ZG& (WKk+U T/_˯IJ%O,q׳uw &lkC)DIKB!p1*PFU`K`Wh%ɜg+"^dS9G@먤g\rg1+SS|ڕ[ `, wښ'No'#3j+hgЎ8Ka:A2FEHOq˭܌B|; p &Q6m۝042x}YMn (.Y Ƴ;-cB^ezJ^LoKٞZ۟vuU1ͣym{T^vPEmnYԱ$kN B6u->*[-u75`)k V1JS9Χ-.͔q缑(zjӫnuL&((js.nY"{ %Obebw1t:P0XYrҖwTTLF*Gľ~5A}2u~L=X0[٫gQ",| ԠwVRDEL{}4D= c_pNn6`%6mv[hݒ|\"ͺ? ;u谭"h4cm<+-= C[1U˛Վ3mnǛ[nbe+Q m)Ay/i @bÂv7@Q҅j. gO7QF2}Qq6{ . ήYηC#Q+Gy @jL|7?gr|Jӑg Ԉ]+&BRZ%05mhBṡfQG` \ȐR`Y?ҚN1ωh[ljXc0Y}ULO,+(,~l\Xhf?dq3b 1SEf4J;\Lv)}&:m>Q;r'Bp-0i20HZ~n۵ܜ[y #]H"$c<ihjQM/_W-Nk5<{'SӒZzv99nwm~ITQHO°4^\N>tzL|=\E9F@!RflR¶` l+7{$b9սhrK˩ ږt4;% ϖf\h[νC>"GKW 7c-Lfc x\)oVǽ-sO -ϬtN)wB ] N]@u؞hX34z[a<s3%vDT!Yb%^:1mǓީmHNokKdsxzD{Y1mm]f߇ vZmH=Vh?-$.knhMt3&хX4tR6JUju$8 '/_N R)!t%tKSz+i7nClam+ /p45C~1Z֖N$ͳ9ȣl Spy~9+ޓ$dDp"Zk]z"ۈ-_Xmf4Ls15K,g8EZ@h7Ky+oFYv+׌K=*wk83cx b~j c!x Lh'hTwr-m-޿bW# }DAF!qO Nr<ĻikЧa,M_]MIo>NPƞz7sZFo粚Z6=u~ P*7'zjُ%vpw;1n*ƲP^)vKoL&|m0$r[tv ,E[>s{oĒxm{mI02Yw 0|ΔPǵȽy  }6vu5Ճd%jbp&ct\<R *v 7^1Sdc?MCV"$eQ 1h-&,Z5`囘6{5o\T{:WWh^"xm~[Opc닮Y&qX #ƽqڞV1;/ CFELۄa[Q^#R7J2#oЕ\ϭ=O d @(;j7HqM 8炋k)7Sfy *<8hgI\$9~~3O8C~;Ҳ'B?qٓY󀦜a8#ƣ*p7}+uiT !IswҞՉ}Z`]45CPEmh@6W3S;(&@o ET, X2x>4ܨ =<j gX4uJ"1}YIf({xX+YMgyPLXSoes>؋N{[D )@<"ze8mNJ]L]]淕4ܛRni+Sјqώ|zXJ7)< }t!@mxfjJ>_E.ٛ6K$C1/ܯɁ1v*5QүJQIYZ/e 0ЋfM;OHC򸊽3,]M/0+z)Fd>>tn76ݘԸOQTxeÆrV3w͚T} ы4R078Ni0MjH́ʅU6-Μׁ;~l8WjoZY9i\g>t`U) > f+kJ*/Z#ȯrWh*- 4X; g1>Lt~[*DYUJM)Qf̫bǗkJ@om}Jܠiu5%j"8ʿ bKHgyd+ǷF|~3Ѹ)5eU>Zpi@9<>{V?(ڵJ@f`iF/\JoBAUJT SRзn]78ʽWTK ` pldtfXeߔn4^PkB`E \)F^IQƎnMBv慜3q?cJvbSM ܮ/r=!{:4 X_<3app|#ki9K6ctف^ RVKS7'OYhAƇ`q7HkU 0b^/)8!L Vo>HYl_Ѻ} 3K#~Qa2v cE S[ ~'=nx*/YA~>xߩVwO>i:x>L;QP!HUPS'SHm* ) iaKaLѠw͢^PFI-p"YgKi/R?/6=8"V\a*kOwQkA7`lRCzĴ Ս0<'Y+00P&1;j')?aU /p4"a!eLBeTRFX hR Cr"zbbMPu~?tV) q<ŗfۇe3n_p0}TrH*\1%9YW8ա,@MY4hKyF#)EDm/QȢf+ r,e:@19n-a} j[fߏeulؽ*hرY?gcVFZ .`ֆr-q[Nkֹ~@la۶a_.kV)h{3w(6j; ) ^5X/8e}K@iګ_7Uj(5(jX0{SnA٠p̪J Y14?uO/'9nEy+Q% `O ШϵRǨ~@X폶$ WI2A^>ԴMPm+sk)tb~eh㋴1Gw7K)ȬH#d}'o6JYڪhKOQ1T'&uL=&PMJhǑouΤt3n_wndKAΥ<#hBPx6=u*V*Y墵WCggBvԘ++4y?dW͈N^L7tCN|ڝ 8|l)_s57 &<9Qa[pS 1JM3)qTgnXN.i(o$y@{]^O=\6k̈́ 1!ʡ1٨/"#}|Wr#m&e0g3_/r'&Qủ˷ekKu"N!lg+{z&a4vN71GX:$oRc)v4/> ð_-*jt:loI_ˡ 3{==?KN[8!ʮP7cӡw'2ܕJ(nmS~= ^ ~SP ˶g+sЉ'oRGg2ƴfܴF>Q+^baJ=a{'ay]L-rdt+zK{အVM@Wy Ԍ֟7SߊF90}9{t p$i0\pQlgv?\CoPKpJB{GC;Sݝɬ0TG%zbR=`,nZ] ?a,,B)fш)!~o[&$ΧwA4miFZJ6vz^gkprgҟ]x6oEŔo'o}А d{ ؚh2}%a9e-Y}$jĦ;BcREK}7u79Bِޚ걋< Éx#k}l#W/zmukMOJhiN)PmLwœ3ȗjR`7 ME3/ޗKfQk$b@:kf+_Ngؘ%ǤIiژ?Pq~fiUQc43KNj5z? M`[?q|휬'u @8Tɮb+[%4{nQ.hG5sg ˁXF0gs)c Za,PR^0j!R~Dᡤ`H`xѕ sY`TkټNz895s4 _ 0pJhjȨÝKxq8b,~X UrTZ};5};4ew-(:iM[J'0u鏝nЫRhL;;г B~꒫ڶBH N ?NҠn);hXeZ-Gn!F/:mbטyr:[ f4W/GۋlrTPa@c&h^ҫiTozheAi:-OK\~_/{:T0c֯mފ`vDJܕ'(!(`cm&툏s%\53iL8 [u1  :P>i >fon=q'L?VyХv-վyԮsR1RK]i ,ƍy cu"YX_]I+<[yJ ^Nȱ#|O71w!tA)W]Du-9 0yq|^0yJ4ur =0> %2mhPRhLρwO=@9r ʩbr:,s8MQLub:0r_|8UܱlS^g$ٸ-<`_oHBd7" G #!1$ c<.~B@/vY Ϭ2$`r]DhyC}7n»)(&~KJ1uiެg+C_Y$c1.IT&eC[ '9L7+{g 伖OOky2e*;A#pҊ.Ghopg{V9I16A™UΚ5%G[<B _EPM @p]*kSDr9$u gNцܒ1U.c);.D.o<ϺuXlc|+FwN32s)w`ʽr*u)AAtZ$/K)!(bH?}߀E:'uH!E^#X /Y"Y_Dw_ȇaa^-/j>oޭ9)+3.JZ Eɝg%/Funq-*1N@`e`_$46L[xSߢELr3%_p͏&"|z:L03Kf̊;QN&y\DWZx6%+beF? JbD}0GNyx0=vya4V¼+#KH%4vc&;mQl2>cŋL,'mb jh7SFBT8鈹Z _iui}72PӖN h=/D(m̽=; !Dʴ|J&iu4=)SG!j SXiZ(6w*>0%9ͣ)IxT>spukjՏq-6 +&6N,4G)<'^z4l&|pi+ۯw|2s:x;A7mj"WoxlQ :_:Kx"RKx(nu:Y7Y2v!|eGW7f[?m(S#19Tm^@u@D_JWۺe e ǏiOd^-j1Vg.NMs^*Fh_]Zݯv7~/[}5'JnOl {o`U"Wyq5+M5Ec俿,%\d ND=#0)!e쥤G'hr_&Tƃٵ(V^F#Y\ޚ45SN0ի}+ ܤ1~L@v< PCE&i'}QHMTJ­_ck$8#_֙_#Y9ֹfB68isEL|:m^^[F{AY5_luɕ~MQEfJjג -/C,m\M0>4"P+zsLE |c:$ڑ6_lkiZ;E4X/0ZE*`b}mຄ+6V=3Xl5cp4&m&}hZ=+Nˆ܀}JIPSs|ZW q!&,ȇi ``T:gUA@v,v6.*:Nuv}Ÿ6j7OYV٘*C<[HxY ;%%c-/jSV&^]􍒫&J eR w]$Frkajc ׮¯R[Tlڸ Y1_cB_Z ;=39NJq)FY| jp!-1UgyJ \1bQ2ڲ46I&tfgTm"bW?sl7mT4)Bn9Х_pLW<$xdrX$'!G|bOgN4mF0+ VR͘Ii%d]70ߍVf0$hdҼ7XD1e@*:}( R6m/{1Ó}js )?T1kS^r6] Bο\ ,%Vsewxms;cJ)p=甚 A(k l>~V ybkj9hWX ߢ 㝗OxФvNVax_ƥƇ>7+(瓥sǔZ_Z ش y"t<> BT L 5okJneћ~yТDNPţ :[2VyiZt'h!?wM7EÅvD?p&^h?KJ]2ʰPѭ>}+S?ee6)o/2`bO>U]A.gXf#m2@g#ږ\Q+0REgrR P˘ҳX<C:@`bD)R@j٥bd%b=D8- S:Ϋ]m1i_>`H01\$}x 8"R )&E!>`J1=D 6Ÿy>C5|)vbM_ SMb={/ʴOa)(.a f=}ō%)Vg}i-nO8/SWp|ЖF j ʝ>CYI1*Ɖ",x "xyb%@OYOMXgYx-(9[%QI%8B!H-9ڃ`6|J8IㅧUӴ7U@d>]u2l;` >kt:F?!_Rn5k~97cڪ|k^+ %*6c/sLoAh2\(Z6J'*Ш(v{-9y6vgṀu_c{H(o-ZI(2F_0'zڴ4aN\dG6>G<ŝi*;a<p}olrֈ%(Tue=8 ^WhiƆQ[l kNkC- SǜotO)T9]X4eShSFʃQz3&\Aq )Cގ vjG-3KJt Yh^0dټW gx2i@/"xnas2 -S -µf\j%CsPhlw?+ڊOD1)EO]G}kHlތxZrL.ז%[:W)wTQW%(Q5\30v Pib\4:Mɶ !H7J+<[v_?nWS{147e*FGhB ;%r懲t!D,>bQ籼[bgBx[?{^x֘ K,iz^‘yЃNTw) fTAb+?>-y_L=f` ؒgn2PQ2uk] ѽc\>kCqkqjIL|t `V\ėcήmj,C`sq[@,|jx(=gT[}pTr@5'w!ļcim6e4Zu]-IZ໌ZR(uj`yŐΣ~^k% JPMmY}(eC qCG${-Er4ts±Ww`$fɪk})02!% :jȑ<"Q$Ll %0@AA;,:[;ެi] w? Jn nC<` vJ, CuR\ڊU2=Sfܗ JtB arWZvXVKC)hD*{54 MX_dtAKŎ;b#ypZr/>ʇl gfKY˨Q6(o N Rx7m.A_1 d /r-TՏ e~ߔ[qp6M1}t`&g 5hS}ܩ=aضhb~}[0p#R ;w*%{ D;E`܈,vD}bEA VC̣iWyߍuDټےrIJHH;xX׼Z-A} }s LGIke}vnve3`4y*'xO5f:zWG(1&2Չdcv2|p?3΋O! O6BNLxbaV KT]=^f՘riLwSDàLe0|k _G8d0㯜> ή᭚ 0&(˥n~pEXp.Q]c CSYF11P@-A-:ßz) zWb+dTF;aRY(1`thg\Ƶ:y|,Qԧ $ 36NSz:hס 8;NT ~o<\OBXy=%%rg> I|5 NqE;fn%ٝ"E^MmP>WlR<[7`Wr0qO-4y4Uz 軀A?I]{ـF̖t~%[X HyMp2w}crE4v:A=`*0ZH }*tmY~vOM X`]"$p A pPqϘeL"H@p7N ?S'Ocx̩Oy +ڗgՂ-EgRŰȃ ´fH.ZݲkEmGs wqr)]>9Y:WAU"b">E{5-f*2E]+C_NAtȺLuI5F~;z ~LT(A@5ͼe 态1?6);&GfWL7@l͋Xi$%)ijn`wO._TØPnιsKqOp/C֠;ML{xItyǦz |9<.5N(NV=Mt{BȄ$܂(o=8C^L//AOY.B_aotǺ|Wm6QX[lO9 wzV^ƅNHioBbXy1b.c>Mcc ¬( ͂0r"Vz^{аAR#?ɶk +yq֩|ߵ\b@L>vnRUp֝­)b e >F4Vb``R+ u>⟗x(Meu $H1HE{׀PM(B(E/,/WJ6$}/ AD`RKl<:^J~4M?%-nD@5eE &MaFnS*G>rR_\?}4>NdWw4[ذȮKKۼcsGv~3k[yOhoVXWzlA؝z>5\G!'G3RbQaצ0! \>AU`uI(55NU;esw}5!!IG 3aRbh]Ob>s`"܇s>VY~\>,mp#bX&4oJ ˣcHj=1-Akֽ6_ZxЄԏf!}Tkv^Ըf0&F+P#v}:> ~mLa^/dy1u֧fkyʀkV?܄7ER$8p .Jq q-~U61(M NJFWd~ЋR@m($.YSq?ߎf8'WO[sB 0R ]$>w[x3u݃Ӎ>ڷÊϯ'*gm $*[V:\oucjoשqR4#MvtS4A5v>!?I + uDS맂Wgx Ìnel_Z H$ ~\˼ÏPŕB'_kq>ϣ[zQw H^OpUne^6'1'wJ?3X/mTD juZMiN3ծ]Ͳ`E^)1@ZN˂-̻+RIX~l8V¨zz!Pȱ"bnhQLVp.~ώ#d0P|zE$V`f TvTO/2>_7̱$vrW5yD8 /Y´ G>BW;d(CIF?4Og-*OVŤPK%KpgƉ)hx-HáX`RMoohʑRvj۩LgY)Sy0/x.=7x?.d<<;RKMuLm0hMXJ."_2 .q=ӎi釰ZeZv-\𚜫> +Ot)8h B_MMQ(< k};g>=o-FM n9^&OSa0fxN8O-uv a ,Ty~!.ЯFE^̣M-{L@PgT dy gG 2FPg+ۧiN ק>y/K>k/VQmĕs1ħ E{e|14U-˦aQ[[HZ!a`,eYaH6^~G?!ܱShP ,#)y&$4yI0'whPoV= VY,j y˗C{lKP] 疏{ho í-03bV^RZhU M}lTD8 \Q*p99-8O{z!V*KjOD~zL4B\S0H`lň5i&\&bv0Om*^Y)8OQH=%i pSS{7zq(8bM?v4]I_+@Yߨu>5Dcih6ޚO߆}&>Hl+0"b D{,oRM1̩Ԥ|-edU'a 1XI,.wB0Jn r;YeRz[n{HCݒႾ?տٻfˮs=̻ȩG}R\qt;?+.tᲤS>O!H@7~5 J\ck9GGN{TNv  {0! C!W!J>Y݋x?ëċ05zX# %jZ9k`S0EEAꇡaN6uה\˩1O֋66nݧ<ڧnuS' ȟuP7aϋ@_b=$3J`R(ڤLl'{p~s^edu5_ǿ8)s;TswU]$oɳ>x]tK#$$䟥_M),xy6_Ci:i ) FxxvmcCOʃ$mvzdO;km`J3POiQNPKRZьa{<$+b .cSZ1U6kt#_F/ĬǫV0ŽY? `J˧D1$BB͋)ј)F[f_쿮X|`Q7jm~k9bgmP؆CF?8gëI'%ɯ716t@}xVF<Attz0uM 2w! @  0UFi`r|Tl+z~`U)A}X":``y-`.C]C2<#eR&tZisVX>=4Ĥ y3 X+ G2b/X,y&dYzFĘ`X4^EȬY|W{>4VFQ&@ R9MV!WAYqe1u JK*}nChzYLi,OybNhO{3_/-PѼ*_2E^m'ݠ%7D`ucþgϔU>¯h /aʛwF@")!zCYCWOCF9{$;蜍+cy9Oϳ(w|Cn%Rˢn3{,Su{0y3t X`x kF{1̓v< `TrVn^sK_Y@,๺c]VH8lm{O;";4_뵶i(L "\J.x!m퉳\=&un^F,qX N(#)})-` eo: .Az5+a8v qY[W J{Xzv3n_~Dho>`;kQTN)V?ߋ4(g#x?Q',<'j+uLGh.X\JS]SySR^BNHP?}Lm)? o'rT !y gN4,GUz%lcNEc`:,Pg[, ZGa A3AzD0YN+=8mºNˍ#jRZ |߆k恌0;q?Ia~G#Z<e o<4yV>'ށ{Q,J 9¼s"4WE;7~?mLYb}GB&66]Ę#Ө(2Sw|*6|?zb<~g{3U5): G3BRɸagIt.xԡL47x[:n*($\vXh0a66Gvd#MU욹;_t^9*/G۾IKڇ磂ʅRRY;-adY[Mc:eYP }ўaa(-i,i2j^MD1}t. |&ϭ6ԏU,[cUnOoW@T {4҅ Zˍ=0GR`L,q뢯[ŠsǤ`2Xf/kFݕ0/b6t<WB¨|q6OM-zW*, 2a.e_\.a̓U^Fp =t&Pq_߿ :A, ws2#?`@`lol0-?ÓO M7>ßQ mIC'#ܪX#z__q/Gꃉ7 4|[tݹr 7O2*-rGKq:`IܨJm8( ₄| '7yejЋ]xUo5mA¾R-SPF~.U!\ +|hAտ= ! 1<*Y:깪]7#EUų8jUJe3 *Áy41KUHC}@cc\si&僙)OavhիΓxMM-,}"Sj9$5k&8 'AE5gc,U^? .^Q 40 >罓^yv08=R^1h4hf%X]Bd$lFcMS РGɫ//Op *k'Y!t5aO](>Ow{yfܢ*#IB)R!ǧ?lԭLDC(\á>4PrpP\MH:[)[88b|QbYYy˹d^ j0؝)i`Vfe4Y?ҽ6-@,l#5 +K p U7B@Qa0_I7%x& ¿F&O(yakgX| ^.ȡZe- :k#eO[=t -U7\ݫf Ooz'q/;y9BRqG2}뱽yz҂3./d/R>(J|R*3bO7`ũ<yN4%l<^Sy= Գ!/"Ca^Mc[>XoR=;Jڣ4{⡉w"ɡM*>J'#y]uc-O9Oԁ( =Ӄ/py:6l֎9imOS&>J(R "qvQ*xИ8S)/(yo`0:a,q"`txҪxuG&y~x)8@]ʘ ӤY~b M;4%:Yl&xS V*~v̛@T3Z^=Hނ1sAA3QSM0B(akaڧUo7 }y(쪕1# 'CUHGr~[R:B)ޑa6') Zd근)BHh iFb. o?"H5b*hOt!n 0!nZbY3%1a$ONv\3@E1Pv)ڕp31L hw@ȫ5^,gOQ'ɕP)`~ཹy#Ev] eOD`k<-`{(]JuP1ŧ(0+u~M%bU۳cħPW.>Ƙ0-u~ZU1¿m޴|r)K(ڧ,t#_yuYWY6OӛDȹS?L̵U p1h{2Xx$D]W|S'᫔bH haG1Р5%ᱤg/^QYZ!/VޕO/ h$6ya1e3JJ{3]FA*P*ˁ,s Sמ:v߁Yzcu9WsuHJב(a.CT@t ʧa. `־Le.E/Uwl~Y= E,z9-t. XpU*zиmӰ/g"pXyC ̐\G8cԇ +ZƇiʨ7 A \utnʰ׾VJ-o .@6PQٽPhp/#N0^}[;(<ݠ[9§(3v>RRw@"n\ vډSԅ?1m aJyŒăx?q :F&R;~-'icN7 6sXmOQ~*ChcCi7n|V7@|< Y ~Uʪc(ɑ~9gš50 f6Ǯh4!2ܺ!`-i.+-mor=7$̍vp8LTTup>+ c=헧{ꍙ !X %:_qF\!(Fz!(Z!pNYֳe!j% q@tٵX+r+o!T +)#CWUiְSnط$(ne5# .}*)0s W=]JizP:o:4S*WNlL=|Rܓwl~/'qTigGH%`<_day3 ,xbx)&=pG]'E_ a,w3. {K{2\hpt+PBnvdK~I3G\y9C@ |4ҘI?uAhi `b@BWK `@d |ԁaP\$ v.H ץ_;/ mnfcѮrި{S+y)"^{1 ;!~'@bh I99l>csGKG](?i%.ig>M(m #o~4ENwdk (GMI`بPx%}5UqZ}JaY F|1*NjR6Q8$&vxJU}gv9a39S.zA=^`U WW_A>iGK{8S}ض֐r]..J/aMc17d ౰V,k69쎧3^0c.l%VB"A`c:lٝAUcVbsӵ@JiLF4 ͳ0ֿg?53h 쉠eRw?v63xy bE %.c gmpU g5'KFX 0 0Mة;ܧŲi=JmS5_lwy0=^鹈T{mxė.kKp]|(=)RB:՗}2:R=4oOƧ~خ݆aZNR~e)Z(QMvN($7{nOQDNZd<(+m \2kZW@ 렖'jW9e$G}?4ě*Qu+1 !dlꙔj'/q\@E~SB|樂Cӧ̊Nbd+VK8e6|W=bxmqE {((<3Y1@1Ty]fUeRCDJ~-Ua~ШwUoejF`eLGua WâE1SlK? +kT(F$cchȺ@b@4`Yi>[W.^+Jeejs C-*4m|bDvʘpcN`1j<۬⣇q+fR *qjT|}*bH_~,❴R(oP[=hX{]# )|js:v8`;׾BMT_9v%lu" \(U}թ@qѝlHsQԠb1,+Q; ̽@ dEg5/,:Kitt+ b˯r0"/ʸ 5h| L^CvФ5/i,)>*=Ykew^ksB5ipהDo)-{vad7+Iy$)Dz:o}նLX]̓t5xGO~S,ݠe^Ƃ0eIo[*4vk))R2mM0 tƭK:!{Fxn?/1^Fn)蒠U}/񦍑wrJva&ʶ:DJ@dU)wWHK£PHtj/ uumbh$ŀ 2-_mC.!`„WkcHep CUU&h^Reڬ:/;a়2nHV9MβD~~= a{ c@7bwE[b_8UkÖvm|I,73N)8`FY>|GS <mѢz:a8}?tCQ7oz_ A^уmJPw1, (]u9?rɓvNiۿrӷLw^+T|gZIHM6[Pg2 w2ցvx%DQrK'΃W-y UMmc>Q a1gC>,sejmRy@;) Mz2L+ޗQ a_$ izw.  uzW  ܀h|JGÀ>E`Zq~R2*21bbЅl^S+na]M4k~A2/-gf?_oW~b;Ţ 3a|Pjuu;̎ 'aXÖ8%ߪ͙=o4S . .v7@ s];gN `SS._Z-vYyyy6Q;tpQBreڑ\bܣߘ#%rQ6xU{3\$6׌ܣ2g")\I͙1x+6q%E>4`hm82Ni3'o ׵<*9-jT< Sg|A%@}вQqy<𿤫rc7Bw4 4vX yfگ0u'@s{Um@@b{C)Ĵv:j8t~NlTx7 29m']_=%lԚE'mVz| k*bpswog=4)02b5^0L5"G ]M 2rλzR>nr7gZ;0< ^1LwtZe„10(ހi4+^W(RM"H`4O9RlSR} @:; "M^8<duexVA2vAg@[6L  {ĘƇIC0M`>:P< 41E=P:|J'O$6ww~=艼BQ]UfA01$O->]ɶӹŹۏ* F[ƪ,XSeHwXZAi@VsWy(ə %(g|{9q 2ZW-@.0pe\;YF* 3Ӊpשv3_ig1DsΎ5O <11y֐Rˤnv N \;jfc`z HYm0!:zOq1S։uEi؅TS'^zr@X.y?Wa)|#7H_&v/RVD5"΋ k+AA'bLޠ15):W Q @B? v7v `ZÞ3Ec]*PֲPZutL? 9uXԇO'çs]VSHbaAX_|',0%(˚oW G(X4ӄkZcIX7eSO'=ZbvP2<<1ӳV~0H[0]I%ڧ"Azj$U̟R1{m̫ʋ6< s9b{N~~:,&K#廾S` ] c<}{aHl- 7$L1ZI3{i#skɋGgʁ cP}>ICó&fY1hr>* ibKGeRLnm #Miۣ<>02R8zP򹌩׬ah%i!‰}ځ{[\cxi5s_.X¸>:qQ0Qq?{jQy =J'n@9l T`!eRJ qx@'Ҧ;]ŋeωڮm>цϗu:3'L /7sf.uOkTK| ĺӦA`23 W,ٲ*0hu< ,0`?L<柼N*`alaPn3ښ-<0f (F1ȉ}#ax.m;gzrۻMϛTPlz;]R`o80fQwR,4Zυn2 ԰8c7c (+ fe9p퇡ND>"4hs-) uM/שu=Ѐ֩[aUUcoˁТiƄe< DѲuEsqeZ[;$l=l8S AO~~ah>ŒS/cg_HhC 8Z` f.3Cn÷(:P"c2iǙbj%toWʅJ07-̺J(ܓܔ\ F xv\Xe&A:W}1buL_j]зQ.X~iTEq*bJzE[?߯'^Pɻ`y^uj6ș`*o59MtƏFNE0dhS1a|Pέ%8< 'r (0Ny-xG2MamTܐl|S{-es 3}qlOnNP A!SK`þ[!m j,10 }qSA)c6H,R]~8˨{S >;JoZ B2 ቶOo0_=t Xauu u^U5qRC~0`y8) %RV%A:N' LQ=CI"pHV.D=fk'\jO蚶@$=o)>S(~2ѝWhjX-յUduW$EaV1*f:YST`Avs}t615u-ju73^Pl=5Pl%{xe,OэqhZo)sp(ijE0yvQ P_=>=ݒ.0tԒ1MUoq"o^x/D~my+2ei3uC`E+yJ[?(_`jIog5>窘oNa4%aOJ:y^hs;?@tֱ::6-A^嵱elwfU<ƒ ,G1|)p$L'l͝ݟXHT (Wc|>1`P:,`\<%Z`[PۦNeR-t3:>p5_L(5>T/3|nyg9):ůϔݱ(mO" >ˤ`û^; .xF[Bړ^݈~+40M.ȥĢeh60^7tLA[ejcwo#]< {JKXFWjnD(5iShBrԖk̯(7n^(k$$U S,IH A8n'6߂!L.Pf;WBﲲ|40.Ѝe;J\Iw]ssnL3_wYfIɲ5lh~[Տa2Jyy^['JAaV]Z3[c (>ZǵPZ; avܜM_?I,Xδc:W^:ҩKE+ƢcR:^9nΙ's:0s?٣N4LX3ns AxW䈟(]0#D79WI|5z;)aUrIzaZ&?rGip%a?:*P FbhM;(溩p aV<ϥA7Q@D΀}Vb:`+1Y"[;(Ur9 ;uaO&ܕޙK;>?g ]h0A8Vw\S\:b>Z-шWanc̠j Fb󕮞羠 tdgH]uI?O( 2j1_D&1~/p~ߵ/RpZG81aN^IJ;n~aXRڨx }ǾR<^lispE{;W x tKx8L?>] Fa32?W>`R2ip|1/ÅMLD<~Ӆؽ③jyaP ڥD1(!#%o0-oAXz;> a86A瘣945Ԫ^e^4[W9q1.ikqW!01o'KkO;hUnOrgÐkwůSxC5D Ըc`ZEKGtq>3q-:qˇ`?`b c:Kln)R@}1qo#7W1^k5%$O.bG.}3mѦ0Jy)0̩NP1ZlLϬ2sb~qqSwOl(89Hل~'_ o`ɓE#PJ%}P8]E/W5u)6Eu7ͦ('?鱯k9sK;ZnU殄?ˠӧcZ㛅t& PrnDw<|+ J<*b[BwLU~T{{SJ֤sr;VP {eWS󈎸Pj< S;_ L=5v=)wq#Ua l)r;sOmqzFIڹʀ˴m{(]s@}ZׯQ^(~/0s zpO71LQ0KZ]0 4VC;BB>E)Zŕ^"2:XliF>3FxwJ霶bV|rZӭ0d{.(PMYRbirZ{Q ڭE[{z k}@pP8 U-,4S @U:&dplK.e}jݦCw532D Fe@NDr.=,5:~HoFIX謅|{gt{ôYRRPC3~Xp rwkaip|4 g'K`Dq%^I]0eejFJ(OٓH6֩C.ʑuꥭʪB[126oCvU4O@GVz 7[ꩅl5o&`.j).{wF⢒U#EQs\) x'/ƀtphkazpW[D.;^9< >O [_ Tsxл;sS`Jè_ԁ i˲0P/j}9ۙ&!n:ܢr/K^K~8Bf$+_)cŻCe(w-am;3iiK썣49 i`Ab1(=EqaL@SXYpU~:VЏ#$#R6?s!uwdiwt-%`ӣqsqWSze@ 7! WRs +0`*%RYL  =?fd;x1Dx.c0FwùX^ +6!m|&1(`?p"u X,0O7KJu,@IDAT4AQx`irG-yc0TE3.wkx^J )΃ݧk޼ ~R)cVJ"`N4݁ L$H,%?:RC!NԴ6\p^p;zR8G)ˇw; .fE^D[Wr//_VOSY>n4YDQ;Zn#'O 24N ci7ġMYL̵1` Ĥs_Z@(+u,G,AX}Au,s\~1#jk}L ⵌ.Ia^E`8JM`bHXn`F]Rj\VɗJ*Ks$xvN]y9Z70.,2b̈QLgt$B'u?O^GրYn-`# 8WXlybRSeuH[hvRߓ!oiӝ=Pw˓v?Io~ٽל'Qͮ+Ƞ81҄э=PVwk7&9dyr%JK;ө\cMh)VyAh~[ 5uPR y{TyĨ`b|Ro ;輦2tc̃G1v"koÂHʬ<.z?,guXA<>wr$8 -@31) uk>@*Kw |)Psɇ]jA#f ,Q$6>+VH\պeNV> ̲Usg2j< V"!q`^LR< B䌂g -SoLowe fBbpm?t P ˾eï1yH8J9a9iz],Q+Qzܬ=\-LP//!lb@i ;ZşZB9`R>v y_~4y7oC7R]{wΆ k_kx ګK jI-a6X@5ߺ̽馅 ]k rnORӅ6}UŲ&i,N{Dy6E<3=S.b)wnP {>7||1nպ0Pwa@%=ގG$2y̴m叅Ch6 ; wG1G HI} .ӒZejK-"25vc)Oi.$8C90 [TtxrOgi3?aG!.?|S!Gi:k<ةR?&X٧aD/_kء>xrM^7kTOohSHAJNHm8Ȗ*ɢ@{cֹ޼C7B;xNtQT[tׅD7Va=Әt H*k"?g5sF8 , 2 ç{qu7eE^SJp Y28x4l*'5uo!`'XnxH-}htz![K{{Z`m p#<7vvŋQ#<8ea>-!t0N88DG7IVjRԐзJa׌-wvKt_\e½/g\efҍaU6:殲'4+z0ԛALMi V?adK>eL~&5v0g| ,/v*gf}+oΘ45x6c-+c ms؄;J:d 0uUGEP)0/tm=SrkB!S-ۣPq\("C7 z3{ 9k7am{x̉R_FOyZd@>W޼ ?hM]d\O=eb ZM8,IZDcqY?qW޳,y$>x8X0wpB'`0J?dR IɟDsfxrԉ3^=c݅. c͎,J³X%k g]ڋ9i'H %G{e.aZڡ^0YT5w&/K|` DҲr{oPΝ0B57Q=oҗ/g]C ael|Y047m~BdO_qx` -J'S4N%Og÷v)kВu7^D9YO]G_qb%AjL698 0 H/E-v%*ʹ+9Irp΁(<.Uj7E2 :Nf.z8& mOnf_D*ې޻g*[<~&Uvb )Fao`=h%za"p4V{FN'j[4B$fYfw_1a)p=ZNYA|0@OyWcX( .wRڼfWaWcDRLGli ~r%)ôGn!?ׁ@FYҨBt5G}L:mI_ɇ8e=%@rxUpZ&%i '1c EsDxlO4Q=cMBy3CkREJkؔ`X x=/++q<>B~,=2 ajԉ)?RaM>ډ 4 +aU;S S@r yl ƒ&ZE4n%'T_P;U骻`kz:ـmbP3i;l z /sΈOs<] +pBQ8GpTjY:@.B+R, W 1޵L^^]] 2m`Z9I zcaOg(s 駍L+S";gi1jЋkK:yh%z,(E#Qr2sSz۹vKlX s"tȵ?,\2L@tb4%K pJVz9^1pb(ðEɤ(C(.aj*e+(SڭXQ)QFX~.7@E֩n)X7L|=*j9̷<A苹詖TW sQaeA8H檴 }` zX-j}׼mn F`,I'ZnR ɲ.VB# k-ZX4wwy-n֯ T'm(WyqQu#(bh^n'/Eum~h+03?CPmn*=a i0D;,Ky]e=g_Pc2OyXw!(8FJ8 bj0"7czJ~ĝ1<&ItWd/jXUU]&p _oV<5-gw4Tt.4?q!e`<9;!:mF 2~To丄 z 4jh6,zC'bvAn^[baʜ6(]lgF+cTLP< ^5lnB}XPi fo;O;ԑO}Pw?a[χ8̨nD& o08j% y1#mje1J1+G:KbHyP֒xwbֻ:N3_"GRݹŃ=҅7(VC86G9uYkS(hpNzod=~;Ɉc#liކu%}Uz78cX6}|8¤wMx0EHROXӀ;< j*FhOx*|Z>S"rBc ^DhnL+>Mh=MMʊ˶ PF1W5mO}vW.dzf;Q@e[+f@jWs(hpjmj6do@ &AٴFd0017᫅5A9ԭ_Q*1w\7m8V]1֗l3FMu1; & 3%˖(LwI7gk '#ǿҍܙ\Hzm9u~|1r(dWܩׂE[G˹ @wr f7Ǝ6 {C0(,XrtsM/.eb 3ao:-L@`&Ø2jצd0xZTX|&h8Z"O0uC^a>Շ1 ӼCy Ĥe]ϬO?̢8IN2,9 [=lZ\aosǨYwbG 'ػF`7,:.^BygvLk0G9AKWqz֭UJy+ڳ'kXlѸ>h7CPq WRm93 X0Unu<`aC\4r0 Ȥ%5ԲE{&3H)7a~ "2"L[ƞjl)jy]w\ʷʅl`ڎ9ܟ'q>;; X\;piCU8Y0ؐ>恙Y||N%f>)&2>3}J:մJs-"1A7t5ֿmFf^LLЖjAЀ#jSnꀩͧ>G#>,hFl.nu ;b>eq-Xw +ҽo쨹C磡Ҭn`5][ſcq9^쉱ekMu>S9|_? ~qT*2lΫKoyV+_θpfaSM6rHn:*.M@x1cV@ք'75H0$!KKf?yaN܃\8^ͨۻuzCl=8`H;7@Y^%ϧSe![i]/剾*#k#_`o4 o[{1"L=Cxw  Ν.ލ5_6'qjZ>U>ǃP UcL<)$aݖى@H*8s+w`2/tQA^M]nN h'-6pϰ9kWw\YގH!=8 P ʎQIK\+m8`p{:ƽF)F eJ.餓q>kACXE0*Ql/|7V$7jsAaU3@Pۉ8rm(0oz3Gk [ #18~cڅu,#@ R s)G^OΥ<Bj9*K'C|%/&q+kI"_V,w` f6go,| Ɏ%M'_͓+ 6{8';Vaxc !a M6~UJqIЏ7N]&p ҪzPnvA9|)LuЌWAni3dziwFx;(;_>y@ چ{?͞g_oPpjC15NӫQ6 SرV1fO)d:^/^jQ&A!}!%C} MQk0~y577EDlgoZدx0+;?]JL}7`;U^KɀswNj܍܎dq 7 a_:Vrh2( [a $ LC3ezUk7S/rA<@_cSO3W= ܶ@s!Pǥg<|#V%$x~ ~v%؝F)%)CQ.bcU(F[WC:v޳0y{.xo2Um%WXHȝZEbU L|:I8':5D ?(t( ^mS?VOׯ7? / ug^_zhaG?|F902f+9Fks|PAyDb `ҹNbrXJn+/kȔr | "aö{<Ƶ]# ;ݶyR}uPL/!HFQBM2+#/]NC],(/9gf y[Om@!(Xj^I avv(=RSiӁ~u8O \U;ɴb'p4JkM? cb' Y6'<2Q%K$:b_cs/bKutI##!s!M4x? L|w8e~9ˁnTⵜt`L^Z|G?K-Q}CA {%#+cYDʽqW zB "n?AuΈOC/ Y;ANUqgI.ψ-|O=*5 fHv,P`8f\簼)<RΛid: }#?04D)Gwp-<徐;D9WZ.{*HJ5he-w!tmɉ (:}O 35HHKdaӃa!KosN yݹɄS88hSzPq)+ !RC.* k޷J޳OM}Jd& #7u2&аБ#4$m"HP #}Tw$|i;Oj_5FGݮ1Hpc"O8Wz-](#3>>-"NU0$|zOTڢT>SA[A}|B!G~zᕲ@KQNE9':AգXkwȡ|//:QU9ʉ}*.~]f, Ls-F[; &gB+5#nq/o/asIaj!Ɵk~.9lQ&q[6dOͺ o)tm SC1\zjg7Ki˘ףΆe 8y?k $ t]Ȅq)<"C5c0#,;ǀ!L#9QHUi %(iZInsY#Sjn(۲:A |)*3^D_荿&x!_7 #4E4}TbH gU}n~|aH]s_~.B@9;JMIWA9Z ܵiߥuW"#Ih+Bq&a>R¿=!9 f9r?JC~3|?DhT'J(&s*eC$|~H8pXPt?G}8Ɵn42$J5kHSڥm%>lH8 k= L1)o(䣋m"faG,#;9Iv!C?y^ߌ7M0%o;_% 62@>y}ZxF7ħD)V~Vc;1WġVo",Ie*2*ٌXZ[D1ҚZ5xyc=ձ'-|c!QBu ~ ǣg)5 ǜ3>甯6wϧ 's2\.2tv(O~a8W ggR'QLNwSPӢ7㳜 ^L.DS8&JwAf@%5̱|4bWvk֗,w. +u)FFLDъ֡O\˫Uɿ_B@8y-[l?,BQxp螘\ _̽v 6'R/ѹu`t ,v]%|tq (N ɢ:w"gOvecgDZW#Bq*b\j~v5˜t,p_DK'Vaz3z&'/BbU[#{Qzǘ6w1kj-q s8e=av FGm9CZS񷬦0/z7}x3}ݽAWUH24K>Fu׻X[y¨ទtѧ|#s0;쌑X1d?08IJ/z{ShdDvWrbK'<Ʒh怒QotU|N&uɘ}0)5@"%R`kjyZ8Q(Ӌ|N@_=mR *ŋh~w{-/gD_e8zn5I80H ?{{y**CA1pu*F)YW;"LN?C_,6`h(!}n܈#LF^Q 鳃;P-O6 w2*tGo1qz&Y0>O P h_q<&6 VsghJo>;98[)S8{囝' W?O:CMCEGH:-C{o=!10cK6PIrhϵI٦#C~ɠG+N=VJ?/wAsN-E^>C gҟ-b?ܒYO&0c hY6qNu`IWs)[5rL.E]m2l1GS1~U,yOڰalM7!ϗI}{~Hʤf>#HF1M C:e BHgG`t+𧜽-Rp =A&E6qN;'_.H50wf38vW+ȖeY&#r/+ͷ1}ڸNkX_񝒴<njښsoSFOw,SjɔDHֹߙ]%mwDz?fʌ>3Z TtyGQ-yO C}*o((N?s`~mg"_G䰧IlUkUܒn2lvu68Ȩ:8) ܏s(Kj&_pgV?b?K6ua2f<+Dl(C SìU( a| 0Po'g:BB$VG %/,@Ck{fC\P?O2>I3 ,y! F(wpz {lv xIQdYp32ocL 0 8T&>a=F U#rS0*B}(ShʄM>Gځ]^uFw/i#3%w-|#XJɊ:X힑?~ =/Uo3be9ci(pf׺B{rB&F6,M 4 ڵ#/;@`Xsd3_x@pY{{UҐ姇%z93;6>!ѥ̪GR ־ [l|i=|8Z絑)ej#|뼠$ ^3Y {bZ=Mx~經zp(: `)`6`6EhC='U`ly'Ki4#!Y 7!KsU}sS|?)+pTÈ;dH{[x#ܞjk2`|iz7 YwNjJUcwc=t j^ݜ־_U/$9U 7JR:r&T쥠\=|ڣȑ a;ຸ;B-ؚC Ӈ !di#M 19e/8:bPG6s-"GFp38FF*{bIEsO88|Mu][C<|9b43d)c1xo,#HGqtkIrvƂ$(M]x1 ա;r>@OmߔUuƭ/׎Feq+%xO FM)3y+L7,!<7tQsxoËkbJkQR³0Q4xch7  JϡhI\q[Z\۽U #EݻP_} WzYY$%Sᘑ[eJJ0/RJt ]X  >?|f Hk Xg9,5ɜj38^zWr,`]'-[DנNH9߸P8yw簊_*7{;>fkhDq9,إF +o60Q4{phW#b,][@d<uz[P3BJ{mGXRkna:Zwϰ!j -QmbxZ.zy| -K<ʭWDGR idGqw{48ug++︖zC{&i80K)ܚnR's!mB,Pn,8?*dӦɫn(鋶UfɈSIwKkhK=hIfO^˚Abi'((a2Lc)##&eɏa;YҚ&d!4p.\̣=)3 9/ʇMq77z!|N[}T糃ض@>`B] 0ʐ>P(Sėamj\K?}ɰ] "E29Нݷr+[w\8ҽ25i(C|Uԋ3"bPgdld'mnee!k(_'jUʥP]s҂x~y)lly V<^?dCCb%Ĩzw,(9u{iĹG^>.- H<Z' QV cpKu(|?ghiAj5nQed]y"NTG;.X o仵%,QQ~(zC~8Z~7q.!T!i$yGs=\.mRmJ;9}hjS!}K}Q G,Gd{!!}Z&2DY:<>*[{;ʁ+ǔ&w6:L֒mIAƹ4}!GRg2y%~I ,ngE+z5]{ҭE~^o Bcc ] "L(l#zkrV`#I Y]ڈ9L`<0p:̉# 4x%٘B( :P:e15U/Τ%|2;tg9c#@!?pNh EVnJ/4*%Q ZUSCS&CH92G(ǎ٭ƶ>XD\C)P[9]؄}S)~h^駼`y+{Pb#:M&So㝣OV{k KhU~=Ob#i:z6WPD]Ar&lA^lR+τh"#/gLE#ȩ1Nd,$N)]j!' l@\z9'=λ~M:; ٝSlkxԮW>- K=xB:ʈ^!͏$ 5[[9sxv,/֪3B("p#_9hTyH9'Q/Vww㔿$ot܌ U9пoRF1BOɨШP|PVex4](yS %x(÷n}O@s|s^{17TûQZQNV.Ț.8~ʻp kB󖲒8!C24:,3<DV&;0X$AWSY!0I&xYyXۯވ&nupR0=.mLU]!:!8#KP$GQs15JŠLޑinE(v7!S 3l-ۊH❺nV[5x41bPZ_I%EĶQRjcn{ ߞտv6 (7"GU^cJn#9pE$H8;JW= 67X ›s eQ>%-wPLC豝cPacm҆*s9o(44tL-y (]CqVZhOk(>FzW`a`u+"9q@O+yz*dw7;8[]5u`-:DeG>34i,v͎u!~{`zcF75-Qܶԣ-:΃u xVPEX dk.6}<iAIy)y~C/Lk@ɡ=f~ A%VFڊ#s?7T驟-Aх/C6({mA;mi۬e$K7IsOF,[01deջ޿8 \/+x'n´gF zLs >T㚣s~Gǻ+s E sc3`(~Ȏ2$n AϋZ%'' $\kQ=Jm,oS7QWB>o?Lx{56)E]můt: D hD瑍%JH]xB;Oa$Ir= tO\Nh.}! g(4rj8KW-{?\&fBp+@_t!ߞSh~ ݇k/J W&e"A x,ʇ1>r8k`i/ ̨gKiVumCNϱJx񺤽^{'DWYGzm,*qPQ[mBG1t;x8FQk`˯gQaJxtk.m~#;sxLߵݔ$6g$O/wpE!<|`YqGhUMpԼ#,$Q=L Q@ԧdFhίQ6[*(hiLQ{B 57ZZ>z^`Os|6<5Uڅ#+k=ߌC)ZY)FSFlmKɎ<=X0ڃm Rt25t!4'˸2Gy{t1W+/d?`.q^ͭs#j(.DrYorf}g/tȨ5=^YFL*1 8kSOE&o._ܯlܷάlz?Y$ Ws6])%}b0_j[Qi@wUu6Q-Fd,G6v@~J`x6 0'E^x* |0FFW8%샒2Q >^sF/c5.FGK<1HVn.>x8p)NU;nZ|;`#;çW G-UN:gΖ\}TB_eNd5]0Hiϵ{g#q|?ӱe&z)x6|O.hWx 5wU|h C:tm\z kqws^'a4- &Gԧmʰ ^ 8o`jڭ ݒT 6[_@:LQR.v?rcdE ^\%u|snϷ^BhWerQL6壽 rJ1ZHN~BjnVzLog[\•@:>3s"\Δܑpy5S^c$ԣN8%u&4ڙƹ0l'(Wf>aI};P$`-_eT.oӿSWcn_+[L,9Ԯo r+)`,n_U2:Ho::[4FocA?tݙ:6± T\#k[5⫃e,vFqrP /mrp86u{v0KKRCNmJc2"0pT$#/EΩgpy}?@X ڧ,{#ݗruKx,|=Pz'x/}yA `nP]#Lqp<<rn3d]@oLU^:5S^{E+oS䝃GwDiWw3:^|2BG MLEÅT !e<{bu0&\<@~.5?}eƧm,?]?Ȓo Zf:q|q@t$GI͆1vsi7i 7HmKK`'= }lT,BZٴ)aȔM| EVOKGG|l_NFi[݃LVΤudbq+O\u/4>ǼoJ^NcdU(aƟ#OI@SV -E+MAW7o3p=rB~ f/Y<zi(att׾9h0"RPDadLNT8_aFsTݜӋɏgl: Mwe+}ش[\$ amҴvVZt,-@=lQ/ s\/JFw(fەRP<2 Ȃ㱐sXCMM.D`>PW).8*`Ỹrf@}Pg="ɈWKAfdHw_uoa`ڍCk8utJ^xp]{Ht <_x@z5N֌Ƕv)/u{/u-iLaF~;C(o+bK5C&s-I~~[LmSG,>Pr^sDŔjxOPF़-^}ԌM9gdjὈ;gŵw~eu&9E!q!Y 9I%ƈre˃FUUcK=߮sO?>ZKD瓂VIh^F&4=J];NGrk}]IlԚ^q]S_z#-Tr9s&B*I&c?v1ҳ&A#S%)KF1 Wg~KGyG) WR/o/uuQ}{ՌAqx T%Dۣ`;VKl𓑥"KWR]Gi-:\7;~7,^v H #h\i M뉮kpֽpVy}=vrp;4..{KKZfqԤwdb} u.]al2¬}SgO/U J?sO." 4 S;S yQQ@yھ_(W{HI6-+ Ժ]r*wlrL8ΙۗߟF7gDƼ!2l'Xĉ0h{?x0XfG ~N훔GzYy;?I{ 8z(> +MvM+˕'!>>*[z=Ե~4PjvcW%l½vGVXl ӻWJfQl5`}WHtohFDE‹!"^i4G)C+m6%`TiKJ {|TүP9 ah{/8'<޳˽uPV9.Mpz%bw 7r)7qbFZ^Jr z1;$Hir_m :Ux0ڣB@0Li0Fg ~XE.)vk_j|1]lJPdrmY7=ܞoKjʍɴh[ocLftS/>NPJd(d1|cMV_,QY_bQY#B+9\}O0dׯ$1t7=0t"=9< Vvc~>_}^6D&$ aZ1 p% L8}2//JϟDxpa|#pJ' /G8(#*`勔/t=M~=v rRKr8MѐWy҈ EڨABђcL;b) 6KڡNs`=(OF{ҕBogQx`+hk\H؏B֧dD`^|t&h@-yڎs D/5Q5-y9]/*{^TqpNʃ=/m>vJMIv`q|%h5,j<{x@蓰澬=uF6Dn߄veO*Sy aovlCS2R&^!'@a< ~f. !Sh&Eh4aF\^n/UpK~n/B[jcIFڴ$MT7![}ta#:㻍ΕvfcH޺Ɖbj\m~$c!/ҋ_>C[/6D :!sw/] ,x/u4{SQ^&i2,`0@Y)}d Oî,$d!YnC`A.rgl$/2I?9Edkt^ʧV_4:Wz5͜ߌz&2̯Y0rH5p2܈{^ 9 ?%Ԁs}Y+(EU6S (J1:J8EyKA@4i{PhQǣaV-%1ҀIϝM_!)Dǁ8z\nBsJ p"r~f1w2ye,CE>)@(j(961P)O|FrNQ"q(~u}QrT,q8 R$gi%}gOJrrf^Gds>J c|ie*%KJ_D` Nff"XwF#?>z;*HfW jF%a/RJ(=ֽ-da| oø}kUCJ/fQKXoV?91B"w?FdrvK}v6u?iК} r߆fb =i4+ 0c>@I@$oWr/az y::ݽRdw/0qڕDIWFȉK. O2[QV#h&IHnlg#jDF_dqEXpwh:Q-Pk4v,8P"T92̔ sq+zxmn@h+u1;H3eT||zOZ m r|cƀĠ 캇#nȍu V[ I8i</i'JÜze?Mk]W;]8hwGHl (ow:pVi+ܡcq<}_5c0{1y,8"'|c,-ao+g{`ZtS }:;G2އd`@_x54ԡ4DPr4P?7.셩 $> 4;Яs nG`b VzHA-rb(#F)ail;M2R۶[F]STҺQr>s&`1o1Ծ^_(G B.y"w2EhVJ(~3\Jh)y4ƁR# H1|?̱39ޯ3r(μҺmtjtNj%e,>'A h k/I{)Wb4D;GFщ3*qzs r/ʢmEVG\;||юRgɲB،І@7-͹fm1M4 DH4ĚfhU+ҳsm>5VEw;'3AR)/9^NЖ5LHDzǥÝ>: .?ʧx/M0AH_Dq%dz:m3΄3@b3FQ܃ՈM0( @02s];ƹ '* eks~ň H6-W~3l& ;|&nb|V/Qᅂd1 #v7ßO< !xZ7` 7Y^U55]5T 7yа}֣.?E{2"*P/$յE[oJ1XQ{9N wuGh[Yg(|]hnoN'bdTf:-ʥ9B'w!g ߛ lc>CMFuVq9nNW?yVcя?@r9RnGoYmޟ3P]ճ_p 8S"ds:mx̀ٝuY׈l/{2KwNq[}v @s/pد>S&;YP ڂhd1O=Xc>t3MphyPQץ*sB >+q ylw˧w,F"'A?tA#LH(5<00,URG^g$WSK8M=҆uQ3?uhJ|Dx.߮`/ҊwjE8N `tu | 6yXh;1N ޙA$(Osx{%=QcS,m$ UJ'̀~jslSg||nʡ8cm+eDOQ}PjQRGy^XjQ`Cp$F qSC3V_Wϴ@>{2P2IFS1BHWop[r[!1'[H֣K?Ù{?NAƉ?JO άnBC42{Gٍ AIO9zEAtng\)W-f CzjZT$AaHG=W1iśgՇqR?Z5>F[(w bQywCƁ:eNnWoh @G#' ?F((Vdh{lZUV#x:~CGp$Ō9?FpF~{(00]Ď7Sm6Q&< b~4_Cy!VFc@IDAT^.pm<';9z>-kOm<ڝWC DH[LEIO7#3jZfƏAh`^ߨ@=÷A2՟zKvwjAųBJc|}>5yߥ%amA̛H~j~ŀ3c읓 v_Hesm/㷤Fx$g%΃~ηnf0m2 %d2ET>ZiΡ{}ߢ7x6̰ڧ'C_J$y[- ( (dLu.g](*c~,b>6kpSj0CIcJLټyH8c'S9_#[ Vc6t/ (OR'NPIk7/mtFFaoV^P\JX|&ژƀqFrQ_}EvZK+B8T|# CGy^|Jd2p)5.Fsx/INg6Yz7{z1<d.}4hTė [9 7<Z w HӏdOs;Ͼ3+u!l~l 7k8o޴(`ʔ&]ΥBV@j۷Iզy2pA8#^`k'ǍK a+{!cazFۦ! ~p5;O{y>h}nV:޻8yEa؟=']PӾ^PQ}е Igkmq~#+5&y(oXi|JTD ǂZإi}3r/7DhTUmz?}=28\ ;fL> ׭ OD7ޞ|Ktnw]X261 Q0߸ZzTlA!7ش~IVtBt,E\BA_sI>纤9p>nln8'# sgQu."2U❃Rs0c~yF&PJƚbJJ9 RY"kgh~ QXmQdm'/E+F>Ra#PʲT[8 "b=Sf1*rP8Yh࠺C|w/:R:jcUM5^rtP49b> :H9G2sMPf.TT@; G伋lzh(KN~.MvɃIsX9sgzvwhw:;rZo4S=G̟5έ<\#}Tw֑hd|s/}fꮮ+sCb 6 ` },:+-P~s-=<={s9IGƍ־) kOUtKanS%}7 1> :_YiFP] D0Q20pQ|JaQS6H}? cOXQhi0@>I$ªc Xgv9'A[*3}g>9K㡑 q?_ 0" 8R ]!%y ]qOyw͊ϷBwzwǴc9@#C2pE$pbJGtH0c"fҔTb}o5{`igw Y}"Dn|+? UeGTһ5ץ?wOa[J$8JN#uNj$ΧLF>IKQG'{Iyk/>m5Lʣx>K^ ܞBH Ў*Ȩ*'QB788¹Qy3JUcFr':?ُ5aߨQW9TVo契?ɋ,Y5*?ݧ@b[;K߉: gZIs|zugыY%>M 998 ;^Oi5 qe "*e$/4?oWFE?`S. }8UW\ci˯ 7<}7 ,=2z+@y9Lk1o s#+J厚0xDҔ_/kh Jʃ=E12LO&h3!f7f2aM+{0ʁR?dz9nk|B+%t2 i *gYhR×l0[?Zs &[r *8kNѤ˥JZJLl]Vs\gNerWxv$J97 ۾.Yv)rcq#C.y9uy9|¯)-Vɑ) #R]Fv2w&Sj6J|j<Y1 GwιhEt\' 8hP7gd?<7C C*KsțT 0G|ثj6&9zi6^,I-bg- <8RAcA"#/^ymlgeC߆=m@t0;eNM6=0&g,s 8Ӂd<Ͳ kd`dގ e B *sHo^uҏTv/,Kd ‰/2x exrc)%q]ac(  OrEUd|MJRB=`d( I bx(2 JI>2@.݌*,Wsx;-Mj#c ARmc9Đ9G{7|Rp_Q1G9œm?zOχ>9}Ao#X2d4Shm WJBseo_%a͛,eKm({izd7bX՗ԕ=(*A%9{[y لQd7~ h~N8() 6zvd@ZuȒߗ߁۪50ꎰB?*K4(=~0Z.@`:P?TɽI.ytC^H-y)"Fc- 1`]FRץ49)2V=,L!ᨛ$EYj07$Y~s濳?FSDVotX"6 5BS3$cyZ_ЙzTP"c:*RS#ޑ@ax=s_ۓt.8ᱢAz!*TwcSi/Җc[e DPܾ6eAiBrWNNtPI4v˫ 59$ntRc@]ʚա4PN.ݛA@A~}b8%;rʖqՏ#5M~|yPn*}?,= &;L\~iYsNF50ZJN '.Nawa`q^|K(.Nί5h ~z\ [T1>1@03r({[7*vIhĕYHhz4%zYEP^Yχw8x'MۨR,Lo2>׋Jb pHᡠx(ZīTxﰄRW | n(e |Pd ^H(yM~ OR]灢hy:TSA'&-n^Z{$|xpY ɦA )1hiKpu42xB+2%;ix_x]]ܣGV7w w E: <7…c_pnd &`\E sOfrBOFe:Q11]mܩf"ccHaz%Êo"{dT؍J*u!`ǜP[}I"թוgkJv)Ewf`'sGw 3ۈՉ0ã]+CRf1 D̐jM>埻9uC*y)N}ʅG}U%֩~sK5/D) Q#iOÏWZ 8>='q((~ޖPk~;3RuUȆ;(Q6F3\" n%6FEPQ0E/l.082<8upsd{Krq=B2g,g3zqh"iy#!d&z7_u8:VՈ5|8|J[& w,l[# {Xm87\re:%n`x.#VEʩ)W׃ l> oԳՉ5p;AaB '.,oyz%WW1+?gسiޔѿO/ ~e1:&.vF'ː S0|oMh`ڽo(B S66-8r6~FaRͧMLP}D|ǁwtQxd67g2S@˞R74oWgIޑ;S4pAi-v}00]1Bw[@egH4B/Ξ z§h.y} _88@6 Pn(XxG]Wڙ?@T ~?1w =ƒ JTdajGJ@\^ e*Gh8VaMWuNp'[NghߞO%2d>{K(LNαM`u=-=&ag]@E7:5BoGɐ1ȅk"t^ P;ll< T:më_qQ=`O~I<1w9@!1yfȬ+xt!¿|/I}JL  Cu`75aGC2OPM="s̟:^|%ad*TY|Q~KiWy5-ћOiRmo#-8N@WjUnI?KogbW2}Qe_ vXpqp LZ!hYH]XB>+쾗G:{xLΧaj(zA4`x> Mx$/1wejmYL5O\oH2yj R3h`3cK U39PAtrV՜r[aԳٮdѷE?6h )+C*-&Tn6w8encERL4&o^̢FIpMdaCy p&m5zYׂqg[/`#chG6?.Y$_p6Dι։weLxRW t.wg~{M=č`4vf8T31A(㹢('JDMGc[U2ɭ콅53)|B,+%)n{¼F+l hbs:0+JhKs(F]l ^j{#D D)oR-ȷ~zlb3C7I#F|*69)}mu], -_DU9(~ǖ;]W?JP"D/°ཎKq!D+JpMokыrPv;D` RK̠ws#I} OB#pIyݙU|xGqK=PO*2- Xz-/ni:ǘ3t&‡8`9W^y4Jcc؂1wy61@ Sg"ms;c=Ë0ЃIdRwt^ZǒR2ɼO+CcMP|W77|g5dL%sn %P'JRBTdHpp#)||QAD%˞QTUD4KXӆ#WΤbc>N73O7]n%SpC)SӤBjy3ϝQ[8gu^]pab\ɭ GFH;6*y2V5"}S[r~UznN&:we&䲬2]Kn=zE J~g`9 # d*J1g7CX~*] ""f #BqpaؠC0gp褳oZx ;CVԠ.f8(~ c k>/}O̲N@{$j$9^܈?uzN~aJ?p䘄n%OpCV3~Q"a%t{y<<sE%z&ɖhE+uЧW3]3V (ƨ~ٻE`-"+w%*?C_mZ m^e1Z1^RU6fs^+!\<=rKB (ɫkvZzq>zTEh^^_L|*-WpwJ=lf;SH%Gf/U1%x&lx">OWmJҳ;#ju,ĘgLСwUZJ >^hl\5JhBo8 dijLJ 1f!ã\B^)1sqC ^PL{^u  M?tzgO}=VÌ_uTV#fB74' |)ڮ*Vig%"j?edo*uЉP`ݞ䴑2NUj6緙ip{w38Hz?t^Q6"bbP,TQhha*~nݕ;IU%!|nK p8زZ^m<_}"bծ h|K)+1iLj0ƈ 9Vv-\YgL s[H,^Hiە %Rr"R6(rw";~̜Xx Ex7D_o '3\_jÃ`N؃{jmVՄm$B DY8X%vf͑z*h"f>}bD) Lt|)O!$AF{M{ ''2ϫw?<{}6|`#㻼>ˋL/ZE4~OgՐ?ls% HVZL£,M$J! ‡eoĦg?RSr}3!|FzX&5a 3vj%]HkI'<+ [);{-h@}bV KL0jm1[ w  ]Njķ[ z@B 9OpG 2cdݯBJ73eлy"V:|>Egᎏx` ~7NUUޥsi6<-u C 1qnS땃ҁW syĕKO4,j1HXdSG9cI?y}NfT`-C1Ch!Zl(AMi_m_x=(hgNr][>JB h$`x#ˊgʨ^qhbs'=;|0apUjvxbb7].҅:f QW]˃b3?2:%xzQ±Z)L ]T}vyσYJ̀Gmi(~w!m|c+R݃C9Ⱦ*wYGFf25 G]L'ɴ$jPݿz1,p&Ht][ Yo0@y7%{֪xjk{w K@p._As|ԉFv Ef;y;Sڧ@@Dx$bߏ"* hl‚d>#4©mDŽ!|Ș7e@r\ 9 3Ln=zf:|"߽I\/ӗ63Й퇋IOǯGm*ԢAw=J8-Oi ~* . zNbO(F P @) _JQnU)&hMyTpFŠR>߆R|p4xb/ ٜXg-Ud Vk[ƯhŸ <Ј5QB0>w^saN|YcoȤҦc޸oP^[+ZxuE358KʈVAH 40dC*_@~$yG Go+KBkBzBRP̎5n$ԫD̘%A`P–SVLπRˇ^0o&~Ъ޿cҖ"ŌK 9?8D't)<@Aad%0Nx3 ̄5M5Z~(bK/D S_#d|[B6nV#\?26=>2`` `fxNʁ3:yIxxR!WAWL)^ɖ6R m,x l5&;Oa AxYa^-?s\T+Z,d@d&һ<[tFm(u&m $mZ6⨮LƗu޿r4jC ƮѡeV$}d7Xн<\WE&TB*$J ;JVsdyBNr & Yxv{;'8:,5eh "渐Q<:݉9r&G9C8X.8'(\ߒnDwٽvE0HYXR{I\y@髇ksM,R%•y--glЄ>! ;xֲ;1VС̨.l?Q̧EKBUƉ? Φ;EF tKhXWp3J|˟@=/lp=<=)ԥ|FɍcJ>],]/NO'=!?F6N+#!Ndch$V2o;hc<85 H&2FxhK< F^CʨBaΠRuÒTe(?~uzǂO"J.(`}EFwB]'H …0\9Q-ֵxw peω `^vvށe4 bH Y9x>E>`S ~@ }|2Atvͳ֯W70CaX5 Xc4v_/H* {b0O0洋P_^W@7}XGH=մp}^kb/(þ0%ѴzyrV\3=,X2@ʛ mч SJ_0J!QlYݟdj̃2wǿHӮwOX=*\2JOFP`IOZ]͝gϫdK4ZS'Cɣm0²w(ܚ[>C} 1[3Ս ^c12 ,0-DYo?2g:"A`Q2@ƈc|h+JMEҞuC~"yyy13bpxqL7|u /Th壭 t MhEcƵ2~+) ס#PW2O5ͤj=&8{(P3C\>Gko|Q vT Ё@`qYdžX ºZsRՆ[Ie>aNI Oj?:}*[yqS& ÚjEJeZ~!dD,-3iy؛pa-tk!(QUD ͌s1ͼiЯ2Yq~h4Qu>o/T`[ӷjCڶ2\) 'u zVٿ Ƶ ~ù/ / l>2=y%&aOӮf*-\d(CWjUa ‹$q1R}eMLRaߧ쁭g eg8ߪ~݇YC]𤎦N8,u%H g( ߴr*[ʷhL,h]]G\ {_>Wq*yW# 9(u@ndKZ/wȌA;{'iZ{(2L۱^KߚS`QgI_T'67,%{έ= H[J/J ]+ Rc~@IDAT_/GX@wh[y@h) M;tCq(mRDpj\T0*K'>3r_vQ{'" Q=q3H(g'u3 LgQ7^E^HM yg(x ;A?/^'J^1\(I?2v~ gcL}1Pkrڜ+j;Y:rd׶|$/7z >XA(@92v{)h]ʫ^hyg 6}'\/Jps4(i~qX(m[nՈbgs>V7(Pqxica$MR6$B(Mo'֩W<% baNQC; nl|)7>wPѴӋ?v>kC(޹kTM{)^v:E5߸hCjݘ+.0w0$185yZЀ/R6qYX?g/gj$<>'"+/ O´bryy\7R w9rlz-fQ*tPx!3KL\ޥD--voIf}ä:8j%A P0ފu}m7Zp}yr9\/ڎ?dZ//]?]"`$]``M̓Fܕ..;s Xofʿ!"^nj։\=ʣ(2q0#,U6;YY d2%t&416NodRߪƽ_k}8Bx{y-g*}ttjQv9a>'L>0'S>F\3x xk5C ZH +ε)Q8+c[. >M]T  ׈7#5Q`lh:V`<k#e7ʟ1v|$\Lf%Z c2%? d.kTh ⓾dvz^uS YEkHwxN2TE$&hڻ._(~} ]&E߮xmX~)d2J`_ommF 7͊:HM g L!h։bP+!mI(5ְe{h(}Qu !XE K LC˼߬tB1BX1`ld XDa$k?OQ. oQ}Ōu:{P ŷ8R2w9g Z`b逗_0D_>%Q1bz7mN-E-Ykt"ze׫0bT_ɠˉ(B[6Q"v tzc¶x'cT&U:lk(3ʗp9l<3qhw}SF J˟HE?ɰ%1z'_ͽG?0W{<1}et9We?}Ac   1ÈrAs w7F 7JX<7.| >*S: ua k6Kl ߋ2PpPB׉s!RGڐQ[e;IkzZ/k\kvanɚA!؟Hڇ)L;dsom#87Tw[7 4 Z2KeLP X+(*W@F?'bcbɤ('rᒁzM~^Ћ6l|5wا)4siwi{w>=һ7 `> zF e51Cuq+ R<[;7x*\xћ^ tQdos9~c7ؐ[ήu0 iml-4[+/.c{g{_QuG s#8; ~/*+i횂GG'}F~JPr$E-ad&lmxH762|xPiPY[ݹ:%( K~SfS~,GϜ!]s `/cԓ%r=91Rk{͋hZuS@o-lW\=Uku8h"c|)Rxn`1K0sK ʚw]5͘I;Tp2/KHH6NTMVtgxgR}: ͈h0dˆ:ӑt{.~JL/Ыq0q 9)x7$T:xء[MCR sSʺt(eCvV'S #;O>y8WgִZqoUNG}PZNm/>Փ-Nne/+Y /;y@ aej{&7i:'EʳңI͍ǐPW nUz^W2IpG@Ln o 2|yzH٠ 0,C+!pX>ò湸F+&8< $"nB=[+m%)yNQ?O7HR>NS9jZ]uC/ YW$)Q>~ӹAdz^%l[]ջ w%⌉ | 'QA؃#ijpQxc;a444挛A]֫KMԌCŷ3K"(QLo~YڿO6,I,Aƈ#HT|Oץ$or^ac婏(ݒި ] ?^1Q~9Dp}v\#YQe4Ux. +aB iWKI{y` FE<'F 8W /xgXXm *hg~ϳ\.uhAI4x&KrAި"[AD3U}f3{\1lz7um)J.iA%`8`jUE~{L(Ze8*k r m S1lIP)]_._8Eh{1', |w|GBX ,#.EIQ7N_rRViz@dz}A՜*[@M^~fZ VܔIC]ځy[2=o UեB,2T}:(ЯP~1P^#ձXɏP (2GWzƀ(*!!̄U!~!ZBnXٍe~XA㼽^Yएe?IPf6+"ɫ#+m!l9(d)!%P'KA.\mI`]imB8fAG Ye&+nZpM2*."DAY gI_(E4i/}R6h(KD h(SDt=[ VL`D$*聍YRcpFܹC\%1[̹p~@\ِ=!0/GHȃ}/?Æ&xo]>̫mO}x"RZh&;|.؈9t@ L|jRj5 [P5%H0Y}2 eP[ڱ]`x8&1?;%o޹lj%Rrz$9!TvIMʟ}тH=;dej=@Ks=< Ef?0G Ye7 ݞ&5H0e٦2>0>0;<$gVC~l {)c_f}V7tQ< Dl~ZX]j?ȶ{smaJ<\W[3A7~ ixG (zj1_ d*ktH$01 ~ن@{3"ô/-'-#( 3C*JDjdݻDt(h*w]$eWjʟ?SyymM~޼^= X~ d ELqoI/r?5Fzo0*+GI(xˬ-6rHʐ|=_Zoot|/swoJ?;vC!e1:G͝RGoiD:8p_ r},jF"h^_s2k)f. lķbQI^& ^Ĉ|N D`UM#NꓛT{LP`gl.6%DU>hoFp1# a30?=-œ;h 8s3C-\L`~T^Kixʫ*VĀ11`Ꭱz$t r_{#F` dݘeWhVzƢ%ats^wpYܛX 0^Cir3,p: E|,) &K`Dn]5$9Ķ vW '<̕1mU4S  c,u0}&r{ۀO\ ZD{9/Xu[%r&pFD n2;12ұArkw1fJo}G9r'*Q/|ŭl\^zR|+="~% ]{R{dZEhw-b4L#.}3ʟ'E\(M3(0|zY*j3gyBFv<2M>y=Hw]eVA SEBYWqzKSoՖ͗ÿi}"$uc bW_8E1,0WN##O}*FE6mN̊p=d!v;D fQ7dLO7.Gh<L?mIP*A58wU AP/ 2㝌EAIez%Pz\8^y2}i؇FP(b (PіCsf0Oax6[!hun\y% o^y!3}NS`Q|c(s"Llfsy[)cuLmɆ#?ԋVJ/6A!#o8|/ZjWx4*X@?(p-{ ' CbY'6OiؗzO BI-;`F_])s*~`fBdC/*5 S}"'ۋۓ@LJ(%|P7 sZZQ g0/9! ^=(D`(O{wc? @Ƽz)Dgr)E^%1TDdA'qq$*,/ϟlw1-3'hz&N; ;J]Bi xC{Ԥ t\ F{Lm zڿK8ΤIk^Mˆ/;q66~tZsP/ H۰JQs'lfez ߺ|o˵exO f? dii'__>#%?}nO3* wj҅[w ryb$`fj)? NlPa PJnPP !RP{?ٿVxFdo$aok U_/,;s\/'h*8|4ڿ5f+3f]L0+ic@~) ۰ VA 3$ #/RwM;ut&F 1iVI mQ eIU'ՁU,\Ob}2}/*M߀tpsG_p9+|=;i_w3וkdS9Z;?4|oqR|2C?t6FBZ<%&#xImSS)ul)S|WSB`xR`7<8TO Ά{m^\=~4 Cȹ3'ڶ[9W:vN1.$]:X1~y i`pՏ!՝^-ELݞ16o6IMc|JτX" ډAyM&Xw8th/$ObMR_ﱴT,ClQϤA7v:T{o2DI=;/qO3I,#@XoȮ:薾ʆF";QC (0&f>l\m j=h@&"/~ >] [?z4 0OҸl U85NVe)-{XtI|jN:L(54 #Fm~5C4 *8sRd%?Mقmx >`S+ݚ N@[xMEüa.Cu:$T"rUܔKSIVh+ENB#p} Q2"`mv1̦F6!?/buuHBBDS«j_QܜYOo9cx8s'DSY'8% |BbF  l~6'ĸ./D()+|9[h۟No"dz*{1pXJ{6:x9g|L ϵ~OÅIhd~x|w4 ž0/pK1K O)vu[])ӑJI&u8r>O~Rxd>e_AsgSD"-x<9Z@f,05R}26ꂖzg̸ƚ~"MĊEď9>B{[ka)v+ ucVmjRKv^_*bS2EtLhbKToC{eRmy#z|:a {3]g3^pHbK#CP-/WȺ}Lal ˅YcWw6pH;5yeuGBl*m}cS3!r$og* ♊Ŀnd /0t y[s D&Ы@'s$͆ Wc_9!si&ٔkȀH;ٵ+[? Kf>r,%PDA8iO  l||+ ~$s7gF ~eivxQ^}Kcv7? DTcҗ K˞Eu\#Q?Œ9<#ޕBp9%c\<䵱|y5T/~_ :2ƽ6tȱ|!Bp[*^,DRP:@ې?12U-m[] $WB,AfQ,e3I~#.dP4g/gP(,H cc*h4-Դ^(ߢ#L;#ysuh{F-qIyD>]X$CNDEjܓ6ɯçw~4J}.ΓiO![K:h#=#,|aK6pZ,fSg[P~ēv6gvLdlK10hl^N> vOY "rzxM wxW,"o?0jq}p&CDiN)V-ĖEʤ bdyD?ąJN׮ZOʄk++mzԽ*ff)Š'='Oxf=>QjoR|SaﯣOw}pP{!DU#( 48we_˭OGBWf+U`$K^2M`*0&&Zx+bC#=1KHb2tE1 +XT%OpGְ(LIC:{K7-+S\mȮs^u7RVw$@i܊>\xb\0f0-.O S(s wޙ49SRw8>=N&KAy!d}"JjB&BIx7!pfB>`k h{:ɋW> VnE,ۓ,W3a&&39ylUʄ3b_S{ĥSL|""H2x3ó%Px4#zyš&d(n;!s‹p#[H!*:y-aH>BAxK{FH34:}4#Ky/ցNMM؂I< }x9  dh̄IFv!pJyp h{{xPj(yiY"?.sj.ٚI8$ols2~hth~h>}31 ?}su+t!ځ:!v#LNJ7pϭ\XƓQAmQ?G&/VɏCI# we!P/#]^GfY&l 4<1!/J 3 >yx|7sFSi(ý:\)8-mFCJi_=#P]wy:G-~ v'0 TE(~= 't5xj4XVxkۚ˳r4+~Anx6aa!7?{ϧ'x%[1U+m *Yk˪T !| bLr#X¦O0q-Dz^DnjƘw7 b. 0u۩x#ގ,YZ'x}TX}7L`ѥIw)=!5U(E;)F{%",xppV"oW CB#x) OTw{4G7@mi۱+S<ӛm)F㽛8Z Tb W g 8#"<m-LC:pw9ʼn a,5=+t{ ˎg嗹&mFiOX '>#Q&&OK sv}e?Cm ɫAuoWOm-.c[RL%}xk/<kA?Y~=(w Ruյ!:(, Vo"Exy$?b*o_ NFǹ Ӹh(^UR*ȵ}z qݛ@FFoH•v&e NEN'ƀ`9ϙBq0<ӟS~.OO3GZF~chA9^6;"k4qM5 6/ޑP?+ZTpާ!t6Jнś|i@K 1ϰ^i(77fϴqץl?xэ޸B07k#D{Z:j@* &+E<}[o`萉"F@CbW2kt/k/K^jQ7>cg'2ڎuY[rjtp q,^ׯ&  $o~7Dd:G| :FX0\onRv"Ԉ DEȿ=A6&YN~23-{4G"La+)<;`3Jűp6JE5H2Q"׷jQ69uad1Z(E^ecZf8>+{]å)&\cA߳QOҎ}GF #+:0[eVLT5xjz}ߗ2:ӡr1_1oR9S vxnEY=7o-ܙg[*.ZcЭ~+xpdqݭ?гk]6zyƥŷ,:A*4Bs+PR!`#K̭,F)Ƀ(yǵv?0@(~n )40Xk3=ۑNW{፿J8{R#Ըo2Pp: oAux& &~oݿ)ٻҧ^v`P4mbo2D "s"TތP,.$uTίGY%ǐ_e?OH٨YvՍ;e+mf48mS;KR#Ze.>%{2D.ך!s6q<2 MZ.J'&`i{\(k|` >;C ;/v^ryn|Ny3YZW#eP碽1.Wrm>1 zS_dÕ7Әw4頦 /66ۂZ ^lھfC;3"ukm'D1BH`Q^%"ZR*UKϦWנֱ1WrI6:v K|$?L j. 7A̛yU ̵ 6 ]y`HS؜v%1G؁,h(M>'6x"%=^LD$7ي} sD-<ނňjµZu guzְJ \׻yU[ (2y9*M4J!p}^y. pJ4~|;O|6| tԢ467$u:-?e1tq" SǺa)]8ޙU7xR32U;W/>CKYWSŪ*|--P;_'w-kq<C6lM!Eտ_Ī17&يZJa"UT0W  RJypC$DбR{{_X7on1*aWХӹzFۖңG kfݛ@7U9U' v> eCa*P~{c(q9HT0z $=!lvT8ɭB|1!ioIEi7Zo嬷<-cH!bpЀNJ]xp=!ˮt>*r.F~zo? kW髳)( cp?"ԭm=1bqߕ3!0ٺ=U=V\Ʊb@IDAT+j+x‡mL'iaX$ m o] p)0ܦzgR9r~yއ<&zIƏTc0i>AZ2(=*kPTӟ`u`Ј .z-A#dfls8OrG-gPQ/Š%ϟd5xU)7~55&D#2AHN !Jy,lfY Omgfg)vytzq9+xSc9( \-~ ZGЌ& ,ӫ0)T/1 m8|]N>.DTxU{&j ]jlӒQ-POBMKˮX&-`YƊ)ޡ)0'@0x v^NF~\& 'ᜲGr&5aQ]̋^X)RKk^O,-)t8L. >{g+Sl64jVaRs0 !/C lZͻCal2 )x _!^Rfz] tr_3І27`L~pHC1@{K.2Tq&Fz6%gwp|C[ޒ{?\^iA/P ,>I0~I{XOQ|v,w (,.6]vRbMBѽezE:ߛkRt‰1J [$'}}$+yB5jij`@@F mD5L\8w;ڌoܱ'L t,p{/˖A709K>!>iY8njk&=qfϊUdj5 allQ߷o , 4ttf<ϼIanl~$| a`)fZl pNJǜ%'ϋ[ X+J‰޿od"B`g -`В1b=@K!'qEER cRK ^7:Z&ZggiY q@ `ʓQ3=HxCkV|:P`Sŝ[;&Jq쨑aPTUFW 3PTG6 M޿L'J0\:hỀů-ja) ~]SDvgp#w6/ y[2)LMp sлɫS~z3'j/Djyy$Ptxd7'&}2?Ygm jt̉w"W<:BvF?Lbr3 _U6r']0FIqJUf$| M2eo# `濟&t&}4IZ9CҬ?r@3`o=A2`ikw"ƽ^*#\e!x=kxe:v˧<u *r N2PV7 ({il>9aL;AG@wIQ d+P={}y5Bsml-}</VV'eYێ?taȯӹgsP`58Ÿh6\L(γ8%1l"E!H3b; wdp$ƊW]cm6@qKO+`p@qh%Øf;r(xN8;b[+6~;s:Sk!}]V{ږ}’a&p*L{T0'?*^oFe-ogC3| 1PJ"X~`070ek[+gL Sr^[VbqW (GTZ57(w&m<\?s*=}5UEa1WoT GIon,IK(}|4Q_!D1$rn!ѻ}FFTZ3ƈObU}Ƙ5ST <eײ=ӏw@لSu띕:ffrM)?1P2xz׀>HP d|SAHcƔ!ƍ2 !~ǢU0HZ{Ԉu+S>`"ʠ0jۏ/!Z0UP/ژLU7Fa$Òj 1^ϛ=74eAncI,EcוqLP~ ߼0 mNPn wL;xF9 DZPC܈2_Dx%ugB, ) D+]"G5^˯BӜyM?>+q?nhZ %OJ L HƢDa:so1?[(s9G)`SRɲ$k~M4I[z.#2 X7N<\pk6T|:n{?Ev?@ bA. Y Ĩ" .@p&N YX){?ß֚ү!+9]G4'w6Eھ<3"B*?Kr^2Sn?~juwpXL} 2c 0Seeu "͟$q q9Wث[)U77z1؂y ʣ(QX]N _I}bN0UWU ݭAGA~L A?ׅM]pXaK(qHӁǜ)<6;Qqp|¡#xx6J |Wx78:;,ԽgpW(vΈx&$&$a_dspkP σ %[_W \Jeۜ/ Fٽ:sXbcl_ǵO)B2=l?a~WD @8|VXrC֚ ec-a⧝c lJucQujpq x p2N0Eߔf뾄,ӟFe |VzeUQ+n^*jZ|qwF cTӃ#rg 5.swD+' _%"@Qx\ƉFpT+B@M"H$L,Kybpx-'?\\X'w7-87M]M~PQ:bPA*"C3߽D=0x#b}4ayFZ&c',[Ϙ"v*6,+HXǞ^=ݛvB])"!?yp;]}ƛPIazIFӼ̒$s:VX47֜!p.)mLA7vw(M3.y{ʄ'p4 "P:SWRQP*Y`[70,5fd[Kw(fTNo\Ϋ?͝PNk\O S%(^~/.ٷCdH+D1 ڎb[&Qin=3 4z!FL~1r gu~2$',qwC-yFH rQ|ԉa#^µZXaԮksir+И`h>:Ń4Z+YfiVRx_O71bamzC, 4R 2ΙW֏.EYc@i-p/78y;`-}}7 aƈ!dG`¯b2kW,N}Ɵ5K!3g񢂎ӚFOEtPw1ջ#x6IF[h *-{vOh_5O8VON6~M eiSq! g,6.\KZTjzz2vN_cG 7a_Pr@!9æ2ݹ=i{B h%h0D0Ao: "rL}H((+SR /8a;8ALn,T78MԞ}±%%%ec--N%a}c[ZZ쓳J(rz0|qOL,~KFMߴ{Ljޚ/å?*>!vW22I07~zsyS82ti FW&\@ӄGI䛣\.aY PpxGPm(vwCie4u~9SLpӶk5?W42(╦ Y3/ARI@zq,J1eUOiRl*`x64F2r+WU# =Ok쾩9F31c\ڏữ䮍E:> {7hr%E̅s8xwarcb-'v>I) 10q3 P>&꺰苄Fj'oT+4 Jg¼wv6 @,}D Q/Xs|ڵ,&B`&y۞1x DNI= 0>3Ku~g)M,zaձ7m7 ~y&qAuuYڬ+>srs5ss%[ (BXHGc/T{GG[{kekiǽ) 7)t@Ao.W*tăGhz0ЙӇg2o?.  `;1%%J,^B(XIZD=eoTk0要bp2_0bv‹E,sȋ†ʌ{+<'=[v-+caL\4))nCtŨ|f1 C @ Dg!1/e65zuhƧA A~M^9\v !T&ynӄƤ!_V5 JbozMz{f <*o]dwv!W) E`܏B%00Y⽛ 1 pbT>Xa>Gt13r!с~Jo hS$Cd+ oSw!NŋSV:( &⇺i07z 0u\>DF*;sZBOpak'ܖ ;?_8%Ѧ0SL i\J w2'Vbbr'8V5oIsa4b(Vbx1X|W6s1²ݝ8YX0!n\oLS8uHQy{ݳypl0ۧ2H'CҩRI{X!(hu+^ܑ߿J͜&|];_Q*UQ1|#dJ8cn.O<!3{#>ᆧJژQ.y,K's:(ūxCC6[}`( `mhPT hDAQ~1-Dݿ|hbӠ:HBSC7y-e}QCQqmg=xY.s>j@D\Eݹ;7NcO_ҭ.'n2Ά!ZDYδM V uk5l{hg,Ks&72CU"V7 2 KX\ka17e; ZeWXrXDg?p|9fek%~x^K!Ů}i Z%\!>+~VbN}ulDn5Lq3 Ңb yGK9UN(W,f,H`"zfY@4<[K(~7hYIǏsMPݯP%p1='V[eK1X[}8J[Ij@)3o I ͊$C;=Se 2x!'Q:L1Jܺ~c2M QvyKMhSc$1I!b^5]/}!]2E f1֣#% I1D,7 c49-k|distDfT1HۻnB,H}dEwg?&Zz^ $\?,[);y GA^~ 6zV/M]|%kFI߉cK% <'a~i4YIJܐ\`^t,+j߆M,Xh|)h w Ymj=OV9WϮ5.kFqp:O;!2>LN+J[+yQ᜞1ԋ'<s_R2ׂDa?X!^n{h31Z$&!_` &<= KI}Zr cE|zrJxXfRGMۖXћ{ B\{۱m<ܭr«)glLM|0vh Bx ?^‹ܙ;;^I8( 6m,}Zzp6SUJw16WiR8r<4cHTESigeX2 ׻(yɔ9}z>\)5pWm];>ڍĚt Γ<(u6׻ ²0$V,acNr;PC #EV9]]ryi1lbUXٸ1EY>Eߢdnb+_OzB2|4rnn8FN1CF0p!r8`}0= & uF ̸*W)Q1EPE(`vKۤ&;A Xw,ȝ ?^LHM? 41H8Zsg2Z<,/n_sݘ]Z?zAbJ >%AO2, 벸 ֹaFVĜ/o篨LeR`˧Lɱr(+R^yj1k7z[O~keh v5F#żQ*d!ܭR喝Vb>Ǻ126E)8ƏQj_o<T yIA$9UdY?] 2|O _! kp!hD||*G8s!! <QH`b<ۣ$m}[Ƅ. P5˪RīF<-p]f !;mKPfǟI{.7ȊuݭD8j"Ɲ^:U&,8Qo&ǘ9`pMmcn>:wݚj i('ʄ?] H<.DZSz6iB0EA9xX!>TݷzvՓcS',)'8*D$oF#+HKi_$'|%В˻>J~= rԥqծ>1}_?H +ؠmZ3hobizEn8|]IC% 1.*õN~x9JÔ1+%B(Tm` A? B~)CN/`oֲGbCdzu[Z0,Ė8M![7e) 9BF8iaBkb >h@$m@ ^B"N-k1ԱJmL{\Jhɶ%X!讆~ X]X'%h 0Ɯ%II)?aU m)7hX:) ܵ8!|鶱`h%}DSY ݵܶ#l }!0k!u70^=*zg0Qw.Lxj \SFZ^KwN)Tv[͵~3UD`Sq;>ZumIC/|\ 5$8"0Y1D^trep!CZxYl=]4%Ly"$H c,,ԽzG{O.Yw^;QKWA*g2_#].2nr*IsaX,%){y}Ad@G`}uxS{@sԠͶ#~.4J헮}/G Rko Ӽ!jC\{g-Dr_&K1gbDVlyw!uaJMJ /F{X% _B ]w7{k|P`k=0M88VbpwMj+ߐED?GJ6=<g :tup18 M^*os+[X~,LƼJ?c0%vzKW߮ڑw| m!~ XzghGM͏^ ]<{L.W4pPd AGC1};)^#ΣW~QW~%ި| f SO1T2TЇ[xa_<-"!A @ AVou[jރf 8&q r/wU;M"_e\sMgwQovC)\]Jq m!p/p=(2P ̱y $N=R,a>ց:Χ} s6/n'- abO/-'p~#xTt 'a֤z8 (f׸8:kL$#A]?)|b)[;( Jl%}6[y@×vw#7bj*S}ͦl^ШwK?A>3@̽xl\%Hy2 ΃rIko<_3ˆ@izudO}E|$̾w$!ݭHmlRT(/Q/Yadz_M`v:eeXJ+\xt0}ƂQ?:uTPx]x{03?؏8M)f >PO9_.*NC0 .Ԉ(ڋ/Jxg2M++=p{?Xha ރi]M@VRS$<B`0=?,uL׶Pʽ;\Mp;\*R؍ lNq/縭?<8V!P4h/Qyfqۆ :*^G4R%&o/HapO6€$Rv˺A6Ce&+kI!m@(%X-Y_hgH'ivvVh}m"1/7 ,Wc<Ӟ<ޓh$E'`|&0{6m_q8sVsf90|aXa={P觱? >jO~~a<(~vfcZtӏ~̲`xaʷ%Fֆi! +;3<6U ~{Hs1yuiE+KH+G6i2A'pۻ8 շJk)ӄѓ)[r*rĕ?| W%d6gP^+@ϦaFSp,J}:.Un}v.qd 7~$y?h kp?S1Kܖ,q_$Y:.#+(W; ;hERދy.DVxPfB*M =8 =e>l{rzP{%_ΕDc~Ɵ?|xcmB޽s癋yo9/wM<2,o+ aXē,tS %E_OxLtO L')3sT{ hGQg(żjN9L-߸RKL(QVyO !ʡG(<( ̭fP7]zy1&]?MHטuF`hWNok~*~~5vD6  dhK^בh:TOz&n =pYs1mqDEqb&zf+_ |*|\= mL?Vɺo}k>닊aS?;JXҌ%eJ +Lv?_2|,NJ8YҽGabMV Q"PPט sRyJwkM)Oo~NsG @Y*_)It͇jR"R>z+TRcɸ KF3:6-r 2mo|JI~3MzB cJzU2kS>=U5EY,_g:6AdwiɥgW! '!х 9f5 y2"8.m1a]E–'~F.{ЛʗoD:C֩#$md<)۽a\ځ?08)[(R=9Hs.9y{~ ,{b^֥xޘy#9kf7qwKlK! ]cZoݜ eLzoԕ~{A64L1c#",E&c쇑LV;l{s*0RZiD?l;~;) r^)bqrSC&c~@l(pr0ҌF7cf輷](RF3v n1 D&G5t!Ww}0$gq]!Q}4t&E;qB oc IKV&HeTrw_%Ӓ.+y(d4FR# ( l+ x/&ϼf(gz#K {h<>ODĹwv^q[~| AC*K/9!Ě?B x,_E4Ka$\Ag2""Fڐ1Ÿ s<(S%/$5e9wu)!bǍ&oд?r2rNO]O҃&;q8l6M6fU'X10Ň@M>j?Ӗx_3Ɣ>d~0vw,В~TlNBQ9Ϧe7/%m@ˁ3W6}2#b)jr Cgd R̍`9&Q~9RږN"0j^/qWDCZ|/߻Π%)mRI44eoPӒb!5H@IDATY*C.<ڊ|Az6^| 3ڜyunxhڌv(8co&m> /Pq4yR `phtmi@VI]&N2aaRWȯ ŗDh@wg3JF* ;AZ?O`JѮ WZ̖#wW}#i_;f~2!ާcxZmi3o)I }HΙO~.,{6.-]l DHQhA&%Daά7ݣ(bPnГ$L]] Qnkz2͉C-W3x`pU6{vOrORQYx\t\ޤlC`N\-.Ćd]LfqnvobF [Ϡ>ayD:bǪ8BPu;*iɹ̧e *3[guJ\#Q{:wyЌisQ}t) PVxUyY; <qvYL,G`Ƀn0TJ#`/ُ0+cv#[Y0ZfQxӄ7ZQ ܽJ0Ͷ1"|F iL4])~6(a?x`̘!x1v^vģИ)bYN~pL]ө"<&.m'gx;y~^ν0eg PC+j ܣx Ń'/CU0M9t z{aE2jJh +to_34HG8c.({8;6a[{b | L?kFT&\vN6pl1W2nJg3?YѦ"W{EW9$J6\!,H8p;SBU-uɔrc~6[Vvi+'w!/R? qO8]%ORT.O ( iP]RF1weܾ[KƬe{<8˘CH`*/D*jifb(0-/ gQm XKq`+8s1(2c`: !. ļUHtg\FXMjY_96i<@G 3ݠ)Q|.;w|ͧK2.P} nr[6"pLoPW11g_NWg=Sӈqkw\ m}'e J X8%t> P& tjP)hs=M/l۵\\F -4 $]e/c[yW2ݽ OD{0c֝`sEŃq5bR`uQAI^KXmE ,sg_jB YD4l!P٣ifD{yIKUH 43Ƥale%?֦tV74xG4CԂC_Y!S/4Jĭr0CWS!6B2)ğO"ܥq޲Dph5v:g fnl됸-5&,UL2R-eܹ-9k1:4.y޺?, E#jCn,=Pv-mm[)*]y=Ctaя^IEy$]T ;/-^mG}39ay0qN( >+?} #l5AD9W:Q-Ȏ\O\O]vѦU.IX udG`gC t AA<˃֗m&3@Iq _up.~qbrDȟ a? y` "Jo Γ!v,;mq6e׊^垷vV&֢KhP4@77CۓI6)NdnXRB}v;yi/*7/U&(ㆇmxK?W :7pr?Ix6X4%Kn{'=s{wS< ?ǔC˗lJs-- ۝9|4,G<`̃g{`aA[a/93pQ{)?tHBV^ i{+::1pvtWԏcjY? >8I yI=T{nW_cF8,u#"s,J,/|Ss" 2)y;G9+%hv*D yܕ|@@恰 $t6}1p4n#U0Y0zֆks&N°v,#'`aeH)M p8rs&`ğfNyh ORx)v<|HͪǤ^.\keM3~7KcL8 O((nnK28|׾}SI8B1vxjz /xgڄAsPyLG茗ė'.] *G8@-O@`h\ܛѴЛ`Z3sy RăGѳLR2?8K#gN5-yO_]|\Echqwb `,Drw`{ !AAMX xPېi=1HM{fћ~$Xb ЅmpX"\;h0nqyL|Eچxy(ׁa$ۢ\C?T&fV'? ̸´ L\c~Ss#do6a * ?/ `*,7x `5IXDjWvmEoS:$nC72NP'X¨97F݉8jR,ߕ2J-KKvwJ"&Ѥ2tSqk|Sgp'WkYP=ko׼\ uS%`5IOhPg'XBO$}ՠ/d3'>o̧|@ALdJMU,0IL3(^՟7Ԋql`{X&J8kjb:5 S=j9Y S8mT~@nxy,8`0X?<« {&/<6ưlp *  0J\/7sO+%PwS~sRgw.eVsw(W }%ws8ÝRT`u!|ny0>FKy+p_]jWk+pxj(yKCכԡnez/qTv~q=类> W #,2$ds!-1Lb<yDf.P@-AKoZmqXݖҳ2ovSK+=^dK `[Oena:z'tjO'Cx=bTyx/ Dp9 =⨓Zdޖ¤C2.VO![;IUyAOB;O>7lvLBM#Dթ9 ;j/~6Pp;FO+0ZeDho0F(eBu;Was<>{bC8 a*덥_z6?y'Vcg2m?R%Qb0!JO,*z4I`JEs'ʼqSx}(mipχO}3J,RJ󡢧Jr.O" |p6L"muAYnT6#IQLYbD8-ek)'ַBV&wQ2LE4#-sWw¿l3塃ǔWT# ^Ҵw)[D(޳i3d -Kyo3S6h.. ׾Vqs<8xoGغkzW<'Q(+@R< ">Eș8!(bM܊f; d'O +F.!&s՗2^KZK#S VK+h_cj@isfCOH{zm +QHL9'yΆ< }0n,;3xMLPвZb?k4)idMP.<,apLn+e6z/I@S+d:ii.XLt8i\m{s?m#1BfBJdylU_9La~:JK_`cVSiDP$[gIrPoteb'J.mD3QXQC{_G+/Z-}g>h~WVA?0 pQO{3D\ ڼ/W.ln='" +#-kjw74yTRQ/B\%IT\E۵X<"3D=0ube?_̳&r&ziӮ!M ~#n͜EJMt9yt`/i!70  AԝhGrh-SBt¾ z.l[؍/gsL\Y}3,IJaλ>fu)V e*ܡVQ,):OnajOvv+񱜿 qg)'њ1vH!0:U|<F{6BS#6!иdJdw >>k?νs?P['ZBP3X<_պ'WO~.i`L2}G,mg}ie}#|U'Nnx Z&([TZ ylclS߉ذDs*]9 @>DV1-Ѵz-7:V+(t#JWDƌ|dDX01̹|UPvWpai*P,{^Q單z;Y u%/rnP $6y6 6K|reR^ ±!aA> ;KZ!XQhdQ۔-m=r}a:\Xݹ}= 1Ewqu(e12t(`Aܺg,YV/`jtTO= !!6@tX%`3h5$ \t u W=i!⛒p&4yB7(KSNDElއ@ݖ mZ_ʯm}JpYm@c,b|h':kʎrX>)]9?f IQ8,ua P/*S .@WH'4e^n9Jo%Ips'+'&,8>D~gy6<,Jzk7]YKEtM8 D%6.TfW>󼸯j/s qђW֤2GF- JVowQ 15xbkULNG`ӁX]ÇNnqw%5iގ`K(QlΈ'AT B,h'X"ɵª6'ܫ8, ~Re/z5mOF6Ks;sȥN?O>\O+zlϪ~ wgR)D@q,PھQ3}l^Qo^SBJ^+)&}8uJxI'K~A/s },܊ =ą!zK GsDJꀎ}=o~^WBx>)gRB3ӿa\|<<y2l@VXJN$ _˜a-2`3E[;=gw1`P6|WȴFw;By; }:(pɸiF@L(s,=W{GMp ՁG8?D]-U`ҭlL9UV:U. I8Yl$_1xl1BY6X@gDo/SDM)6bC^o &i/⠙{l)/dW suLÚwӦUh샳ԍ&}g<`X`,d`\ jCp-!P"ks. ^X8m1u(KfU8n)No뇀/ 5 tml0b3aPnD+Zggr.c︞O(ڈf^ݹ*${ 6 NUx}(tOZW᯲Zj~Д'4N AKԉkpK4~iTd&)W}xt)FRK_OhɺGJ{x"$c=PJ>Lws'9,ٖKy /=J|D }6cU7}p9t0k mrR.#X(E |:[`S)r}%h7jkMpDZ/XELhXP۟\\MD6Δ;͝ knCy)2\,-X)CY7Cy' G&AHnFmXUU6jM.[%O̮bKO ,#, ^IH4 Tʃy"ZFR T!1No ztPk80x%cKWGmIF:v=mm:峨@Ii1m}y7SG"M~)ϡPvQUN!868izm:|`TNwYFQ,ˍ*akD > Z=}@1 /J߶TuA-N#TBQ_oGrXe6Kwd"/OHBd))S)ʶa:0¦7Ҧw&1.9=-!7FjPKkԭ2 KA]icA`ϓ9䙤a&wǝ,7}"F$6}suwjYz+2Ʌ`S30cɣe|ňK#2]1xF29%w8o5oR.h xǦ9A?}Jx&.xXd%4fJ* m|(OҮ'@19~OEټo (c)> YZH.yLW$}/8 @ /D&,CLb:H@czXacn Q L4.mm  R=4vo]q .wޛU;I>S9)Z{Վ ˫G;03[OiH[%:ް%˒w{W8Pکb VD՞Thۭ]zVZ*i/'n`n?\~3d݈BujԀY`60/lC/vbr^hƪ N(xp9WW7>fKt+xUͨ2:cNxD4J_¹{3ߤ7D8\t{p^=0›2fpٔzЧ,"<* [|zT;?@6-*[MQ:gZ|oBZ~]Uk4DaV*ݘm 1I=nލݑ L^&˒YZGR9UwR,VWrT/|>QA %6BӻºI,0-BV m{#֓D6yo}k<Jxms>3՜&0l6M9han5K_De=L f[Rsecv ,Ԁ_6fKQb󾚻=Ѝ;q_;y~?ncTKQo^3 :DpWW'MI~Bp/`.鮼׽ѝ{J(꒣4 3 A)^ ̙Ko.%F0>%}|OYyOa{G^m5vjcnl4Sy_O!Mi66K(7}#wpNT|NJ d!lQn$.B%F#Dxgֽk#%>%ܸY\THo!wzCQ6L>@J\?/g!a~sUV#zEAyT)ԩn@'v%7X*žKk))tywUҕʺ4,Qbi">hk hL$mcc%WWc;&x$h;UR `{>Vw\.LCd)BTvɧ@{~ WX%D@ ).\$LMd+#Bޖx;!2f?a u> [-7p7Gd VhQZoa;=o#Y9@oc)#r"@`V#+ͣso,1+s44O`tnӻ;Vnj(ODž¬B,l7$X,i㲆Gyƈ6bDGG w_/,w' ѶgUwGgi|u} '~*UP֓|?#@?ѧ{z?aN4CEj 6IJ`˅K~ *>Koz;js>[uP Cne֗9ġ^dύi 8DC p@c}@Ļ@{w٪^;,0u1#?jiז\G@~tTS{𼶔[H`0Ѻ+lUve2a:[o?oL 0 ._g_i8c`MK֥ eE~m*<Magwshm7Ai*w ma­jHZs<jy)3@sW̕JhhInMz{9sqD FJpGdD w-:4nS_xcC:N>po0!`grAYNAmȿNխhp]܉ (4#V#! HI AZh]LAO*ϯkψI+s p?au>Xۡ֍}X)fkѲiwdaW@DZw n-rXEh5z /d\qbQ,( V4OLk^Q˄sc#)3-n݊n?83R2XI*U\(\hV<3d% ۛk܃)^T!8cxj@Ŕ7ysa⣛yB- #a<'N0Cx _kWvOSc^ܒK\2dn㹼k8ԯzW=_Q;T#hʣ|SZASW彥p:TtrT ͕  ![EҸ}<<ƒl ʮkӭ6@jLǁ0oUkZj[TDh`!|#S}+Im~(-ã#y;9G߃r>BP ;cʒ/'AEԈf!i$ލap$x4 ?F)E9D m.NDWT&ے8,q w J):;m쏛{=K mvUb.\Y*y&y Z6 6z5%SzfJBϺέwГ3 1ʌ [gP6R>xQ?StRO$1-jޡHjTY Tv7Ү{gss,8q,MA 8ڕ 3I;U!?$7_~Ђ!6BY%JgGȰpsXm;Kzˣ??,ZB\)7$i #mo 9Z۰KΒ'fe<,43^˲/iN4S9Ҹ9S{iĞU ^$AS E"(q=#[ x^Uzٲ\ ~>P杺_,5kRY;*vq mZ \^~vіIq,S/_9u  +ֈ #*FD)h1A~J!rh`?De3" Xv,ےD|F-Sj6r XhaB._VA!0<v(# Vt 8"$iji89'莰l􂊹ؽD o!8lY[ԟq-y7헂m /rNV94:hNJ+k"y9'!܄kaoko=C/Fa. a[y,@ Ff2WRL!F<@qlW[kH}VOi ʔsR`֟zٌ1*[lĈ vo BdY|Љ\(a%$@IDATߘ>-57$Z 1a\ogs$ oIyi5fN)5Lc*OYGN9<T3d8ۤÂ@4NDq?aY4҄/s&pɎRx~ۧ6}opo4NDOc7UAp[!wF.g*[̷Z1LO8 ?PKmiWvƋaݷ#dTXgLɀZ"]WK4Gwźo3MpeaK(TQ'"& ۊu/CkCX~m~%&By,P kxeI?4GZhq8/ՀGV/ECNR<~bK\O, h668䈛bX(OcεlxfJ6qu KU5 m O͐H+f3iGi³Dt- .bm!>Bf^=..7c6 0<0T h)Jl6>!`oPl<_Y.&NV(m euF2il^GyB? ZV>9q0'VLc+qkrGJ4z. *$BhqP9W<$u&gਠA]Ŕ ˉSa3ߔ<5]<6hgx73PhҽƏ/yqr/58PpsUWVAPdsyY*KWh,Pc 1@ZȋdBJBz1祒,7 :ޮV-xM.$*ԍ8Z$ڈ7fN() #B4cQA+T ]wYapr?%H0bY؋}mNBO>,{삈=5͙I(P'^~1w5?ҳPRK®r \a;gwt±I0G/ X*\jB?YSdR k{kM9&@hBZQ0}OvDc$Y{RcE]^5JD[!4e JID7~cnס[ -b`A1bot{ xtLOt o 6AHocx 4ArBp0"8 & Y!sYC\͖ HO۲YyErF1VA@i9/s7`Nf4zu@٬,*ANɢro9 v'L./fisX_f+3T8P@ M]1}z!Aƀ_)IvϦ6}Ǖ<]Q(e#7lBQZ8NO~kYxI䡠yy2-\Z?:$@ʥZQ{* J-<3wuXƼu6}VQ;Auӄ܃v+>@]C_Uqz?++zٳA'*_yofvF͋)M;^dFԦrX Uۚ](Ѿnwv :^)oV+r8<& KW O0q!TTMs&Ha6@mɩwбwBͿ;̿ q+x̀[KX}/evL#>PNh}*z_5WJhYє'{6mQ YF!ƣ?b^׌ao TXs$2i-iw|JJ Х7bP9E=U@a`ߺu%'I/ [ 2ŻAqY*QB!rp$2:E! |xvyMs/RZ#<`Xns"l! wJ~/@>.J? C½T 1Lw֠ n_tk7U}< mVT#)`K} ٮx??kع_nYTo+:m)e{O2|q Ui8&QYJ G/ AH OB(.))Ŝ{ bkEy%"ح?ݱjA4w#lXpw r M$/ʬ;rZۘ'[(am5y75ʄvN^>53ym~K`5!ZU9%CqVM}9ʦA\h˹V|q\*e+8~kwJLVR `0@ \ϽChFD`4xV y)QD_KF^h6 .MVt׮vuoȗW#ksWЬGnPmHa(+d0ýFfT.CߧxH*,,̌>*kQ7lt5r1((7ݘY[Z?m<5`*:aA|\aX@mW$jxe\mb`'cNiW?!TEX͇=}Pk&oQƀgsAV aCQV$HîVc2`Ųf40GK`"zIM$49Omcv4\ E݃)@q>07[#{ͫ ژ9%a,\?YN,KBXKQ/Ů:z~?ouc+ ^:yy^y♯CfsJYǮp+¿ -3(-r//՟3ERVZrjz}mP0vJ~!m^9J4_fO+:WYs$̫MQrʨ5 ,lF>5CN>$z.ԀLK҉#$߬}wKO-Bb{s,.l.[% rb{`%΁ãT=6QmzrdO.iYZO)FQie#eIj֋إBUfg'3*ygqrN/w"}_%}v'W/g*NP#V} !MrOÇc0_^pbr{B/(x SjRB.F*Ƨb|\ 3_OARKm*JQPnya^].?T~DKp]]6e a{Xz6;XMAZ} ul:ic֞>$X+± a]-ة(G먜KB"͜ix) Ne<(d.{]R67ؤs]1;}B/[,F\/ Q`W)YG ͯ߳!6т>e2JpR&u,^z p~g8mGs8λ6$:?ц *m9Hmxē2/ɦU~#+7dvߞx%.Պ0 k+wj$;2vݎbC@#UAo EJ#'iY5ć]fw[$ވ`VK,ݒFOuulBfή23c"0} p^eQԶPlb| G~/)< }=Oѻ7(׿ fQ8`J{ˍNm*B*W/Zt.e ӁlC@ CŔJ1h!s75RpE ls!oy=LŽgu}`OP80n<^TBWw?Dgv!"%, Tϫ/; l mcQ=LJlDv%boȅ/AjnIMqGǔctEkklº}k+B15L3F26X_^=Q,e]ȃ^Hs3x)s_\Dw#*m,f$HJ9b&mqRvl3ۏbVgԮa4`ʛOfEҘGݺe ff4LN] ùze)6%\b9d/h </y'n1sc|Jt処*A(~~,&\4h»Wͳe;$P ܖv6(#<ɘnw޹mј]UZRԕ9|X1q㙠6+Ǹ)xGh}[> mx㻐Aŗ"6 Z] zDyn^;\se *Ldu+YϦQ xv]Acw6 :1xaX6&ۛ0dcc7cǜ!oBW3/I%V_y̠P>S*ۿ̪DA](?Vmo\.0 @ܬqEKA>h8呫$ecJj40>h ' IK An,h\%;Жȹy.9)hzRQ[ –Wwv2d&=}ӐoXpq?^:EE7šen Y2rcU&jt3ck%ZF~/-*b)/\R9<#W b>S#K//'Hm`? l){8KH%XJ"!tR& WQhcІkGQ^Y4725'.ف%VhSj(\/xuaA&9 6[(;u`'L&蛠80c9OnoҐWR $BB2|RXG&6Qҋ٭>B5v5^o<ͽ2\R7Š㲜Ǵ('˽YpIJho^Ѷ1z 7RN(Ũ0g1G.sϛ( <"9JSyAB9u燫C7۸]v!_^a#l!'"$@ݯ ?V/XPڽB0Ux8N6r+iA o{c¸e4`BlA@aOwSet @xk}ͣ0t7JӅ'~ d+,mr%Qx4V==_-8GQbXpTO(,c]A7s7JSYv?0}=九rou:HDR%K{mvA%zkAA ixY@Q8Z9]Ǽ|6~ M/ 69 n2K3z0nU6ƉUT>V:֗桎ߜSƫ#G km"n}׫ E1r+0.xP ipEQhKs Fw43: OH?аqzh2G:Ėm)Q5@+*.}qYnҌpx ݏ.].?qsa@IK k`!Xz?RR@j~Dc"pdp'ZF`.YqUq)=myb!׎b=(x,V*86y3کaKѝ69ݭ W%PXXIAale181@T|o;W:\7mТK+YemsWbMOoc^2Ɍ%ev+P/nvgy\-,+ŁU|Kx-G_]!]A E^;A+<޴i d@ B{IHiRЖx;eHpg3!KW AOQns <ӑaOk3c@θ@sʇX]ʊNo/5jrV5Cm׾˕\6nX˘a)UdER_>xs1O9Qrs]eS5KKDW(On {gjT HG(>0a4Qɼś 7绥V?ɬ=o o[t$_9!(\0 % =Ah'\LwU4M[ l0bsos bu1yMvlqQ$dWwYE9kP^Ӷoy+txrF' 'FOל?5\lȖҕ1SNuN J`-[2C''`;ޡP0 G6$hJ֋󭀊 s|:y)Gض= =4eo!7bI. M[VVa0#$ʉ,\šB"䨥8Bpn s*-W)/& /hJ}] *RY2)Fa~{qs SYFMMb'Kg=mXzW軼^:3f{ըZߖ/@@9Bf|Ijn X.Ցg+9e_neu[̷G?JOS) C ,ϸ[mbm޼WJ  y8 )cɰ{wӊWҗ t _A/vjgWg2Ϸ^}/+aE BRP=q*,MC4a%yy v'Hm(l[ZsñRX6F/mg/;[wk/ IMq^МY0 7{߸ _+ l^J=CF+ :{Wv̶ӜlG'щ [l 3,h>%8pYwi*r1"Yq甛s~x<]FԠ|Y`9*2U[nMRb}5h>ZU3/Ci9maRgMsxBd˙ZIZC H-\mb-%~A4g']9V0r@xn7owQB k^0=X[u)hOڇG!Xh-,Yt`e4q_'aʜ̧IFH}U]\\/DʑJCb?h~Y %˂._p5X7{'4&E3@"BocI,CԋDe]H>{1b.MHQ:fo9R<xNv5!ePY. $"q87}q-cX2q`T5$9;^0Y4¥zec8j[q茠fo\Y`_y^W _.t%5l~jy=hŃwn|2gc" 5,n%BmF @O|uP,! lLKҭ=>rڈp=h_ds]GAKg d0C-ވK566a/`ILIw,0#TⰇi\tX[vS.֯ˁ3w(:{V>AIuD)^m^Zֿyp(Fb)"h}30lKVg˻J/G9x[U! A AY@{kO ;!9KOc 5&ʀ7AR|Wu!I4xP߰E[]sC^k9 UP@iA9{'=)/['+B,_ 4/4{ HMCxcK¯#!֧Nս];<+Pvv=9X=eYϲvOcqc,+ӏq;%+@̞՞hJ 2#qzjϋ_]h {l?eUj[yx/Kx5OT`)ڃ #[4g?Jgl*=;aLBG.˔,CvtcU|}'}60o˷zК/+1y}՘KQg:"dB}ǔ*;dhPSGx̩=Xy#J[BO2WǞ l g${^؆{s5*Y%ħb@wz݉hY͔֏*k9-21`r4}L3ބu[Rndڷ܈"`hGř^'v{:Bm?C({:OYw삠IvE44Rfr*!z?eičx8" T}.*uiC D^5 E,|Y4F1u'VB{  y.Cf*C!1Eg)9ݼN_.^1o|RP2ܮ[;7MIyIt&kY-QhǻR0*"fqy;ZF(PVmq/,V)  OÑ* XВ9x< Uim5D R}eROEzzl.]q#ҡ'9.@X,C˶#5y~w&1z}[.bj %!nIJ%50M>&MLf; gBXsa-|/%c^{1 !WunU{K+VibF^tYnm׌)YWmW0a˽ `/k!^ϢQE$$7ʥԡܲ>+m}#ƛVhJlmRľ : 'œC#,^0xN/w]Ow`Pg|ZE}f7n<UZ!|//lw.۸ڑ=Vꀅ^|/q$ǂjan#^x'Hr*9+ 9bt`1|f&rjCxysײZO ;"4ukݹ&73ܱ8GZ7\ \ 1 d?H_NxaQKl>dBT OfK>_Fp:)0X91~NWY FnT0qʊ1~njw?y]6S֋Q4 v~*8lMY% ALZs*By"q=94?OC= =K'loyYW*ÿFG>U^br5&h8oTqğWecZOk`.c@68+Z %ȼW,Hemu)z3 @)%^u#=9XBQC>6q'B  ~YoMj׽Lȟs*ވ%QClDsvY8&P,ZeZ=("/7^1E$b@SO0Pm,*.VG!1Z._vD?_Z#ٵ'B,*iѸ4+୪-h㞿|^8WԃԽe9EEjS0aIE,?RtaOYn-SNоK{R0xd0饊KifO…(3^ҟϸF=a۟Lے)0cJ5i/+@`9BO@Uiw#+tXNnnU!7[ا-p!؛;' nS 4 # ^2w:wRW!AoGr6 ?u]!Z8M_"އ9V^ jל%0#KbnU?fc.c̉{ezѳD<="Xs-Alߖ=ۏTm`3;*q>[6`YB-<ѳas)z? <c[윮I-47bVlO %\a M>&-!:}TSPiۿ hB]nEmYtNѨRtm`V9'bhGՔ(r^%R@+zC+ch,7!ׂp߅oF!N dq[C,ڟ6gO0lJS堤)'h rt \ր4? ^L }FlaFS t^͵/VlUHc6~a`⬒)'M+$⅌V4Kf ^TIS [a |4r=t .|#Pu)đRX+MkaEXi~Su &cp*Mu4FE=B+B)*)5¿3 `>+x_S@W 5j=!wlG緌1̑6UX O[9CG]Gv](3@sgyUQ5,Q + ^x~n5psv~\wL0\ f(2 g'`w]iG 816+?b!Xhn?)תrWuCߥJR_';Xq?yXEMl}KH*((KhJJy& k~rR 22 R.>e9t`98%B "!-9 @8$WׁT04D)6O(;%OV܀9NfbSܤ6b7IE v[qz+C2Zv_S?\т1 [(r-ly̠6X?"%±H%5/@׫>g D&\h4A:T?B)`RZ ZQoyB(GBŽGWBފ'' J!ZTPv<Y1يڰ~-:]HV--=Qm_M.,a-imˢ!"p w(8~\03̢)fxe3XQ˪5hg^xxKxLP\Jc$W,vv lZlfW3yqx:mIG*rmRN X6} pE[W A)hqՒY֚O^RFTy7hL+Wދ:n>$y"y=.+'7= ywlʷׁ؛y.BtƄU}<8KൿCkkI]OI]Ђt{V ɿb6VC!C9xܬy.o 'Sۿ^mi\Y 90q]m X^5K ܮ渒߿ky7R3\[) M=,#}9ȯa?uWzBjmq^s-$p+Xc; 6;I%o#vV<[+ {yڋ6/B̧L*U/`,-:݄?H òH-Z13So7o?CRI} P;RxFϤe?t`pg 9/ W!Ag v Hao#.r|p#޿Le;sRO-yV)5LW-L@EAFx3𿉪ʚWƒ0s+lOd^ oDD+@\y7bRfJ)e2҅܆ '}-zᵢHh,;7l3uU?F] ;g}67Eom5؀?|` ]ހDr!Sv|}B+\AܺAw%(y~4t1)9lApqIDmM@hl|gFo1XX.rѳdzlQc܈ܖ 1y.Q0!1it,-+4 Ʋ۳dx FLj_w MzR$)d7į+vNYiL(n;gR ^[7P@ :ht1ggSѭ(-elv=bebe9 b ZcڱWۏb;XSJc3W-nu? ;\0F ,q?+c&2Q/1)Sǀ~z/_XV' 7q|יNY|$0V/x88O<łXjbUssײ8w^wAƂ vpږ330'mK~brx<3x/Hi"8s4oRkxSg @>z{N7pKDH"=l}nr7G!V-ޔOuIlhz}Ӝ`[fR\a ɝkOqg˪D!0VegOnp՛-hŌe5~gSlI)uHtOh/gS]k uR P.~sh-fho#j8/q_>^ Ҽ2>;»TMqi` jMDr5Pq/@W|#Ȉp~͐s rJ"v :B+mr̢H12 1=:Iƹ!LP숆Q <061[=NyK +&̘uLx2y8Sy!~( v s8cSZCIE N?(̗J /o`f-mekRtJUSœ,l!`ܕkVx[80r?ţ󙓌EMlSƄI3txKَ(AgejcˏEQ+p{L8ۄ`"TP m0~s\.Z.= زp%zay RT??= b9 evN߮f>(͌9rZ;* @Xكo+B@~|WOy<*dݛ:q^xí9? SF|ym -#آZ "Eo,A! H,rW~ fDp^M19+01ƥ@Y6usju͇%t#zk`ܳ\;F+ ->9W+T׸zڛN8Sm.(gq 1=.#+TR'׀hE)D͇݋UqVH ~{9}m*e~DܝW *,a^ 2xd7( nY=nm1 v,Ֆr6n3(۔ xn|)ޥ>PbKf3E<)Es>}gKֻ@DxL$BEGe;O8 12`6WxHK6SzX8G/@W(AʼP w!/Bف@pl׆B|M -3'+'|V$ ӟ=f11Lub;yPbdML*?gb❈Q٧rpZ{?EK8B`]5u)mL­ PաdȎ"lW-X19{=kT\]fF (W958!>;-Xg`HPa H\l^G;ZH:z9(pPt"Arr\ut{,~/fvCXJ'`g Xrb v[>e~a,\bX1bZ\-IH;,dķzx~=[(C8LVQ_VpZ񱈋r]IsS8$:qxi/+{CZC_*%^yd h_K>)^ME0@w9ҷ{_7 ;ceQqW ^}xy)XGyfpSdݣHn/%nm1HdʩkyƶV|/C`j)81 v237Yu#`eUF%SZ"{!f=6jBjO UHdsSffy!q99YBOne7CKX,N I]Sфx?KMg؝#[x, xw>9rao>$A2K2B y+ߥU ;p2xdÀ@W@S9^#(!6q $yQ1:1QLW[>7 b"eyY%HwXEYS\0U{A޽:o?YOP|$, 5\Ľ(!n~hv;޴̀IK= "a!Ꮮ P}" O[ ?WS*mNv^ '1g?IRLHcj Pu9j},s2n&WI!@+ġ[礳^+p:g9*ӆU| Xwِ ZM"$ ]J{ҡhr_IɊ/[ 6rO6Y ;,YϿϸۜOz%cշs3jsQ,Fz6O뫝ceS\=fy~#^~xoIjrVٙ_GN7Rœp+\,oDꨧHL[_Sb<X|9z53VB{,zƠ,fe>qܞ~S["'!oBv Wsi|WldJho3XNWԞ.8]p2OA:9ytf\.;>YbXSaK&вk!!CwvƭnM4x9j5[{3 y=ޢbXmas21Xb2g3'|bn =#qGRng~-GL6,uvzsj'+EN/M rT*.~w""]àj[)u/Gix%`/GYiRpz9RZnqƾ+3i癈UTDm#p7.6Cl}|`{ _QLbWyN4d0xߢ'r,5s*U|ۡCv$=&{#!~u6":g#97%%^Dzׂ\ё<]Y)ykX $-3 s 7PatfbW4-i.9/@8i@Ekrgr`ڷ.A> q2W>FԤۜ | p L N~ M! *M % '  sYoꋇ/qkkwum9wx? $! ' " "  $  WXTF-E79+EZL 52-;KRq qxxƴVق.x YՀQɂFmjpudeZNrenhq]{d $pe|c{opn`΀ fqqi|a~UY ygZ_uQlx׀Vafyh>lSm]nWk9oJnqr|z}&`SqfvJ`Hj́3n\i\g^TGZse@Z~bwJTo{s|RNChp|BIm̢|cyT~ƳƄ_r,RHn UGƍm [DzĄ <Żxi VKcuv ;Ɓ RJpw Bĺx؁ 7CzDž 0ɳZ]|l U=^yǵ}fw.gg'v}Qd(tm}3mہic103PNG  IHDR+sRGBDeXIfMM*i@IDATxIdɚf>"| 9P0pVM`6 #T&g'yQkvm{Uk*E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(E"P@(EF}D?qOd7 3^L<#rˏ_8sއ'y:4g:@T&Cw/wM8O f6]. mF7|<{P/,%?<<,Ͻ?G<3giEg)"= W{o{9p>GuQV6*[ݹ`ЅQF??O ν;7kJ67֯EI:&G`C;rsQ].Py{4ϥ>l{=pzO{KG![2YO!>mYӖ$z-;-xt>+g?|?)VmO{sO{ni>ޟgDM! ޣֲd)S3zһ3,qEϟ}{'AkE"ptonG]E7l_|w]uߕOVyna՚N9K]N;Wr#s@(E`VPVҊ@(E"P@(EwZwD(E`r"P@(EJJnaE"pFR}Fz5 pY)-"P65ۭJ6jm]mpzZ~=v"Pn5ܪje@A/E#PwnV@(E\=5\=-"PN@ éw"PU Ѕ̯q<}vR ;`k(E85ܸni@(S;[[#lxk@(E SƟ_Q>*`/E+@Wr(Es"KD]%+JeU@(E pz AB V"P@(Eۂ@ Z"P y ]|[@(E\5\͹"P>?MNl(E"Pv`~/Ej9Қ|NO$D"P^@(E߅^l@(w|E"P@(E|6kM>fp}p}ط"P@8 v IZ(E"P΂@ gA"P"P.],.(fS@(E""PlE"py+9D`Eڼ@(E\5\͢"P.p 6"P@(E@ ޿m,["p:U@(ES 5p{K~:VZ"pU"Wu˹0: lFE"p<*?.+oK~2[P(@Dp%#P%#@4W{ X^lvx7fxo߼cxǯL'˸x(p۱ꆖ9lZ;W|zQ\rvRF2>SZޥHM8Ι]+EEw3[9w7Տx\vrq<_'>|3;AO'kU4ws?L14Oෞ"pз yв/1^6ԍs;6QEl=`hMJ ?P no))8,^lTD~5R:>Z>%ތ\CIkE=Pkup'CW4'ᷡ?sls>g1Gҿwc(n9]+u}6/`pk(Q,DU$ )w.+-wqTfkp:=WA~*j6b zzz7|>,2~S.vovcx2\ܺ?GQ$ז A.P}H,~ܠA(|0W|Yz=y P"̔{Mv6T5=Nϡ#d'C_f`$ ݖ3~(X73_9:Ĩ9u8K[]^qZqx~6LE b_ ա <؏cm:$+DobM6[h.4wd0l=1;Dhs; N~ab~TgjxeRşJ^yS^cysE~|r>_oj1fm߆?ެFF\Dn}_绸j.>!0j ꔭ0A5~BbAssN)?O?ϑUtCsoDHR2_ӯ6Tii]D^mhe폆B#AZ2q- Fq)o9!U0i'2,7=^"/X|oz7SI1J A;b~<3ÐSuyiI1ȯL-xiB+(W$uӿF>w%,Y`ZN!sr^ưr9Qp75F0#z ` ͋ݐK6yB`yΚAb*!1!ө;"~TͥIH AѢu/GH>y[{ULN;;Qxz͔ xl> җݴ5 9D=?Σ>~w;zQ}Wch~x(m>z%jU7b{9:`(@u}3>nL4m(Tz\Oc"/w|r"h1빒 R`~J)) ˱Jz"p~hCc&e\V2G~GESRPxY|Į{0ψ]oa⪄<$\'rƒ(_S"s292J"%nH/lu? s(^%KuOsMa=L@O5ưaJJΩS8EK2Ƿ17܊^>uגΧT:d#JcBBb1 ޘE"J/t "0`;Χ°6+=?N.k_C o!4fl+W6"pщy7a'M(lwcl:w6N\ۃzׇoCB')՗QS؅;aa!k<lhew27Rx重ck ߁sos~riEFm82p`dśve]zH#S-fli؟O(3>:͏})b ܀HB=Q`Lŏ>0vfۘ ) O=L+BbaGk5aÉ~F':YŬ‰|FBzb~|2ފovzc<8{R0yɈdC~@vVh|^Og? =!؇ܠS#$ڻUtq "ǒpXweP~sXx]I2?(,RLޔLIg>6E35S{;XȠWZX֒gZ{Jl皊@CF1Ц^P^/Fa],KTW~YH<K8AVBU *\1zX@ BSN:܄.)ᢸlD=4OkE.jNVf`@-qG]+2"\h֋N.-?\Z ͸@ Ku'm"Ei3"|, ~ r/=Pv%;^],NwX Pĝ5^oPb1*qvs'd P:ؗq~r IEWa~1xY)5'\tC$B5lrm!rGՉ|{?gXTEB5B6Ŕx :J$.ZڎʅN͜#zX/x9Q-1n-,Ì{7aBZ"p."&43j4vM&JaeO㓦y(6wnb]^ NocV߻>G(\usnkB4LĈdZj΄Ӂ `G6;VpKm6آ^'4#Iwj6a|,Q5=/ 2GsuG@l_tQ8zhN{Á!X)ۙem 3 ǕEXo"`͂e;T@O"p̣pzx /1s¢-8MݏgcKE l2 ˸Yx`~O.95!&/ǷEme" ( [(7]Mq"%'L.OS(E7~Af]T7^C/EPW9C8~j*ͭ%9QiQ,vKDoQ;"J|(ٕ u3aZ$ /W:B̛O9i濟T˸7}LcqtX9؏լqO qHJ;mR?@cűqz)wJ0-ms[GfNm ^NN@&K(4?#UWn8:P"%D^4w[* qi~'bE\&^JhEy)8܄%Vy(YAs F^f7k6OMPhbͲ#yxO{M^Y,Htm6bJ2IgVUxW<,G3eFpA`ޕ 1Ҷ{c)$2.SL=}\L?/E6#RH+13տb(fyOE2De_̽[T\ɼ:o Mf1*rPFQr)K:S_B(D%>5=NX 28֢Q=G&ͽ*ʤJN;L"dc)g 8bDD!dS[n"P.bQ%'u%_G@MaϽ .Wnub$R䣌64vh43CSLӲ\FG;aq^^i6iq|@a|Έ tztg*|kAG=3cJ |5#. K8X;X[U#dGܖm `9(E{woSKZ"PNBş*}?DǡHMɌ"M~_ EG\BR މDž`#Gd,|5a5\*_u5eAj.2E0ã"$RmGqGc a`mhiea/=M`' e+#)7BA~S ϵpwߘ|MqܦZE@(9P& :!۝*Qԇo0{2j7} }Dc8tܮe0'<5[,vhD)Ncta)|St:d`rg7}2̓va^LCfs-D<,gk{{wYyb`c\DjmLκF["pŒcTOT:J.eD ގnhHDP񖐔ׄ˨Pywh'/N9F+P +mq?9CSx[%AF^'4p>!ԁc 'Vas2|FdՁ!Fy1{t2[}<0l\ )B ^ nEw M!xDm6MQ[TPVkb01]vZݴҖ_CΧE>Wc#܅הkB5S[?/Bs9-]Dr2DSOz(7WB^gR93`O_|?ϛNYs[oW~g FF/pY A:ϸ#D*'+s< 7bnN9"<ʪ| 90hhuaQ0dK!8]'nn:-D㿶y4uvyIRnNY0hϖ%zJ^m*E!`ҙitEw 'hrh'F!GqEP>{RC7MBˬ)&{q'w%L7 ) GT7bS+6x+V1=3,s`1,p]w Da?w;,6 Ps^,!\c=3/Kx!b'xB2_V#i G;,uX$M_GxY>uvAP1w}&g+.@c((Ga#xLП}[sP$u:~sG$v-!O7afN+g9Q=}ιITL)qRe\S'H4l6gLdcz6ÔNS+wi+6kc8P_DȔ˫NE+@- {KڒQDxNC[Qe` xqry*JyR^(~-E-^ύx59Jxo^36+l%.]+|7c %\u`g77߼C+2wku`jo7KG@&(9/#/6e [mOrvy^,/QБ$0-s\iK֙TD,?m]ƩS}h'h~%EE_( g 87xrW`i(ϢdNܕFˋfFmzn/l(n7OߩBCwj6??3m V+]|bP+5уᗹ GWos2%QX8ތ76W0Ԃf#hŮM.z66\ w<~2."ʭ"PAPkQN(GңJQp;GH+6e'*W'_$@Ӥ#j5Fw^4DDx9e0Jgu]^tnװ쨓/X_ ϋoNg"Uq@r΋ߑ)&4_:Apk[Lnl7bVG6a{Y" yVMSfvmoMwC022"cW1BD{Sg 0+jP^*EV!MFf~}᭻o:ahQMя{K\B_yyU]ߕŀNn*+ٓgt|x,5% be6uC|.ma|h??|.aPy٭]e;}0K??*?0@DX&&hcvFIDs/WIdT!0/#-䊶0F%߆o^b3XlUQio\ӢQ洗DPqEA"5/i;m&R3J2*'Qo鿛Qe6VR ~2ã&WqT̙zc 56w8cnz}AJژPemWGǭNZŌʿίoxzB=ݷ ^@~&G=i?Ihi("9ok|Aw-jiQ+'nprMOzKktxQ0Ux<Y >/J˨I)V)0oMyn<̕l؇9WwsirYYmz1`ୡvmǟAFTӣS^!(;=ĠjeQ}k7u ԕ+o>c0D 1hףf|@(G#`CsC90(a [U9)>NÛ/WmωMl,OG6 DkЦ`LoF_6L;6lD`23k a쐠fz]^5<­MOGRu_;%74 * G?*qݾ@IDAT֦"P(KӼWïM0"PRaaJ;cݹEq%&4r/>t`o3tl)4:q[_߸Rv" ,h*Cf=w5m\/1||~ʫ.#2Eim={[{8ād`98$aY#V ^@U۔ó[f" :s(cME+nbe"Zz8a#;vNg}+}=zUa>ԚNT]f{֚ <1XpL(xgbn)B4aim;*ta`v#O& I”xϓUɓݷ};|`3Xya$zD >x.|=_Dj ':TS_ٮ ^c5,/Ca3J?N[ܭm(QQepu?GpT~)Hj xƒ k3=`k ޭ%b7uo  FϬ x.oBI0pAx'r2. LLMEXUl SgM+}=3 p =Y}__Ŵgl ܋s'"NO ,0<-g?ƿL;e 0 𓸏 Qv4X('xTMTj_Po{oE(F{4ɾgUYiB>U]Gµ_^x (.. GdEԖwg 71RGC9eםR3m[)75M?l1hj\)idC/YG 1unuPv"֎Or2+QfچZUME"pvzf2z(>FݿW.#8JBAҝ f@a} tcPXq3ʿդm^5XfI$Da 03p + 'ej6]mW;̳Լ-;OWA2]t4YR|_Lira὇x{sk!1ˇBQ4 a)0DпovV?/ @" c'z>eE eC.2^n;/K-Z*1E.(Zf!暈17SC ŬNTiV'fL7Ί75s1_ioq]µriZwym3kЦx VAk! ] d&O,P]<$2qOs7J^*,h+0%w|79K<&+E|plcy"]I[:hYE>1>(3ʿ. q,FWx$>޺pr ʳ`kdGر,~o S!e*RPFs# s(e+nտ&ɘX,1G#e5q(cɴ-c&m˙ Ĭdb]S(w C wHJtһ'z=i#`;&B:Ql~;ؔ/r&I2`qx0YJψ@}iAYIYĴ_I7f$axXlח D^6|g!,ȃsH@zG:L@м/7ֵgՍoeAgy[ChT\@8]EeaGi?LBR5=."Wf̢{kq 3SWXLR[=u1,i^|5Qw0 r;lc%M}(?ϹZ'FBt_AdɂDPu E_fj?upÓOt#F|ufE"D*|9w3#Oyr%夥iN6 `!c?C L1Pes %6&"Q12s>+46 Ӷ~- CޟYƅ,H [0 R#~><k aΔL7PL׋iY'FH7` v2H '6pB2l/dм tn udx#pm.A 7Byͽ:3/N [/]/oOK&%ѹ|Xpt GhEIhTd|/K1g񂄩S)#d?-ZqI|SJ!e6W2w_췔ݟjgum6|sBy36}1:ig'\*#`< XED?BA4z,2{bbhMZN<~:Y!m+mI !֯1n^w Lt C`f^0(v|aE`gn{+^%<6o7W9f(Kʋ'B-_G(Pyd "=hpXSR~?Acdml*E#`]ci?͆fahWC/#>@q|DeRP)[7-]QwD>JPWDâ)zK+/|0j3 ~x: &:uaxN}*"7-!H1']dH?kl v nH`đ8 DZ~}zp(SߔjV"p[?=eCE"QNQ?(`/4pZ[v/:NaRvAcvvfkA8a.uEVȓ 6i,w`yntXK0gl~$I@dX;ܾ} SeME02epFB[dw`&ǹ;6E9`3me=L;Y @A^4(uބ[WBĦ{'LW|;9&L-M\zN S%g%@"pKQ[nV<:[2`X"^0_p38~;E|,~p!=OFX/wNz^]Rg0s:ߋ@@d0 c%/OK,~_/兏_eۘz؋q+X_> aˉXU $ B14Ў'JL[bĚ~'R( _x2!*dĄ6uW9:樸D n#[~NGğ^NILV5)kK}Ooq_AJyi.a︲(L`2335">wy/ [jڟ(kLAVdض n!!C"({<8&g"J9S3[aVR;>~5 Kt{n~6"p?ԃY]`{Y$sk7a2KZ T+d]2w) C)) iӦ@4A½CF1.q&a&v+ErF= g;z梎|=ſgy֕O/b!PeƠOb\8 cMvy>}O7gGAojlH uW*j&Ӧ"PeGE2lyˍkGNеw߼ oFz76:djV>2S֚6<|rBtSP?y^D 2%,?>Abqz8}>.Ths0Sv6@(<B).t I@ 1H ! \Fl~ MݿiΝiD}y5,PZ0정U^wB&bO ;D쿫@S7l`>-}"PzrmꠂUAդ S7?=1L@K|):J8G(j$"o;!O(IBԞbꩯҴ2~ɏ=9x }_})BEb_(&~䛅 -%+ϼۈwaA'Ev32/T~ΔSܓpwDMݍ8¾NoT@@@7]ѧ{Pۡ2ƢۧL|˽;}b{ 0oPoY%douQ1˝w"o"*O%Lsȴh~D/Hr>>DYǓxJ6aĉp3#VJaU@ea蚋kżHy6 %~"S;, {$BY[+LN EZpv\"맄ʰlbQ4 ʄh DH'C3D"p~ߡbo?|Gb}0\9YYZǠugR۔F̱tQC1D}yb;˅k$zQh7C[$) EC{SQOp|޲!Kk'\@(q ?hb>S> Ί"!j7BU>SG~>Q^e,EӍl;ߣ"0mPS(Ae?}:)L[F,PjԔkE j*SWP70;q暻W۳*19ʿ(DoCnv"VL7>q 9tMDѧA% LY*\Ü/ QMebɀAN3_S[G$1<yE~ ՖI^(b d,jܙRR{yBfypL!AKo\LmK(7A{fFmޡ(Jb&V>ʌ6fDp\uQFLڽ&.L\OOO"`E((=kkqyA>Q+⺒z6BڞM+bYp iQb/,>@~+U`b(0B|y 8 Ԏ A`Æh~+ _Ok"ŽƨttBS(P~n_z4 UCrdb ]$k $X6M al2&=Zqt/RWj>/Ӳ2Ү6:W3. }-acYv$#+)NYd[v0ym4*v^ݎ .NX0X|PĀ%Ky}n ;?_˜͜Xr%8VlD‘-ⰚG4s< R #rŨsp0#oG#D=+/'"Pn(- P]3\e~L>U=֪I𷓗ug^+,Suu玣_.zt$;Nfm1F*/&ϟ)zȏs=ksqg!7H{dH 2 }2kT9GmX9t%czc@50{z'b@x# N9^{3Hf CVfJ}jEpJ4Z">xg^,퇂2g( /yռƬm*oh!wS(w+D>J_F[2ԉVg=pITT SB7uņō Ϲh-^\w@1uxB-Lq0kQ}-#FR^ne-xŤ+{vTy1}տNDJʣRz0y nOhn9*8g(& G|e\6z1] ~KLBڜڇ4d.MO&"P ~y"P ?A'E1?ܮ2hTgt*g"C X@@䆭bHaE4Ϋz0DѧS79G%액wTSogŕɳԱI+Tqy |",Sy /^,NԞZ=֔Ӫ"P\&{DnY*So^tisv̶#Gx;ӡϷCcEiM]~|~m"Z+*FE>9=E1/ 8YQ䥛z;K$F5e WKK_/gIT`B%^5P7fvP(X(~3"Xg5NG%/%lNK܇{3MC=Ux0vR"pXtSM7 ]1(M0r17c1)i$tFL}vA o:-_T=W{p|W[i}SeGQfh7suO:"W9<8||`?ʽ z*\p*EY<#A$cvE}=xiX3xf ;>m B(Km3'z +sb ɓVWeNz:y`W gAr]V-`&x&Sz`fMa6智o*EU5opy(]ZT FaE)gL;S(`>&rвq2W |;͙H~3(;"E4QzTOȑS{1% Zsb''dʽ-?i⳻)1 gnO]1m(Fq)oB)ME%jsޡ |VGge=S|&ȋ71^&sh5cv%oH)ufQ8B2%4_L HTg[VMpor'?8 %, :sr;y7B9SA5BY5"-2B#bH|7ؚ[>z"P |<(&^P|ccw E@Y  l?$OQO*C~'eq^H B(“95a Ս+F&y~4-5Xj=-ʇcj*AA/e 5 6LEFe5!SaxAC|6C2L41ఌ/1b3+DT)Z]1'-m~j;:Sp$zd^#p^Z+?_#[! l1f",O ڐs̺4}W>﨧hr6c/QhʦLKg]7)O1QByLxٖt5OgvŶ {^= $2h@a!Kak2;,p`˜ŶVqn 6~&i]9)eB#-W~TME"硠e(yFqK.炳U|5P Ǣǜ-`Iw[m'^mZʒ奞%H~SM;d9qD|Ll  5g=ަĠVڈg>.OtwA]=># pӜe0AaDQbQ%o ]_RwZY idZCMI-#Ső 콙~ov44i9.{xFM5Ǿԉ ٓ o6/3P] UEF^X\[5oyNϧ=>ME MG0u"6Aĕ%,ɓ$ gB}\ry'$>NRw|>˓eJuE`PzUc-^Obs"P~7TeκVִS(YeF9X$jzTg\+$aǑj[,vu17^k^~>ܯȐ:29$%)`‘y#9ch6jGDy79=;~} fv48Yc%^P/fQB;aD‰(ăuyɃ,z;rػD+O"p!@};DGU╍} 4 z RDpS&zި WyQ|YI"ڨ"՟22x\TU2@x+ezbγ[W}[ g9TeReig~% W*ǩ)>'(,WZ&@X0kq#K{:HQ0G)0p0M:y-' WPR^TEnz y)1nϪaj M AB", Cl"VyNv^ y9>kZ|(O4"PfO)?Z+'xnE쑗R} (Jq%`/=:d'F<:)GvHS{|gZJʾZ~ryDhxLHM{xEBL,MEoF}_J3V|@phOTS8~ʔhH؞O:QlqN떞Goj=2i8^ BGܧ<RhGNQ~M'@s%0,O(i0~B" .Ng~KtKbbnR:sJĄd)b]]l_nץJĦ2Hk}݁LC:?uh/lR׈Zɫ%'볝![y/uB2-Z+~}<2@-jeB{Wͯߊ@PXa"3={,gqSt͙&}'oIͣSZQrF!w߮"Et Puĉ)xvmnH^pJZc+boz8 Zspd} Eƙ`/q,U育 ^^WPP7䂋BYp6-#/\CKGIVg^O/y|?rzǹdhZEig>@7q^wt#%ES.9O ~.%FsѪ[_#)t<9gvxp_(m? M9X<3m#s ̄{}wٶt3ou=ۻ/n竣e|hOS|jwzDC輘,rJ,!K[F~O(@_21c Rd"B~ێ]*LHZxB>:>'#1z|1r sw'cB*Ŗ+% 7T{>Va~FtE8"@|;s`]OO Y_xx2\%>1 ?M ~[mȦ"P.pYW‹ {_=vhGS屼Me }Z˧^t|{.~/P'bLwDOc"RC'YQuM9Qn vxSLO$yք O ^/'GcI"Lx=tau+5L/'ugF ƙlLs`Ƈ~v*ix3;߮S".05dborZl {}N]QR_/0zu }gpzO7d`D?7S#D+O'/$mңG`"Vy3C8`2]uKy_E_+O"V UBۄ{[h&.y.hf'0JVyy$Os 1HR&'0g:{hrQK6GaWb bYfxhUgʝb GOp;>W}@g~ ؙ_Ռ$YvU Höʵm'mKX YWݚWMA.~d`(y;)~E;8<3 "jm+&[Lo"/\M/yv߄F7tΟcqϥ~px5G| _H^/x?ВcI9h7T 6I22Wzy)㏛%u:;#S' "]:'];͗R |GT*#&t"X3hy"J#' Br7hk4"PFJP aر|@Ga/+9.!EB붉Hk,ZLǜ?k߳U`MYRr,Eidt-Ж,MҢVZޝ,ّdibv @ ]ٕ]b p%_o/@W| nh᪅%29ُ{^bnvt8A2 QXZK@_#& _≠~Wvy#?*тJ 0(0(MCRMgaúài wS*0 @)4AWBvGGiȑQe~|ýӑяK)9~nfޡzhHR03~4ইbu.zxn_+,^ 5qb9gMOhGuqQc2{#. z}3{* O|9f2mh> i *V;HԁጰV)[wι) NZ,vhZ~{-Y! 8Aqάhs"0N=y,ixέ'2x=խA'5:T"# Q۷Y1LnYM03A" <(|wP fl0s.12uvm7aļuB>)5LP*A7ЕVezn7̥ []7rwpR^|83>,f"eHhET&ڭ2`,^OO ۵>i㳻5 | *FV-d\#̀ȼ(07eXs| j> m,\(;`]7a_"}3Ǩ@wH7jEB[/T8"lO'JlxtAJ^ʄ㩆[JL^}DtO^|BgI5~} ]@恰-3i{il%$xu#ҋy *Er;0cި7eCc|]wTU&%\! aɆO,jy,)^7Ԇu)6k8 M/D t".냲as*:i)ʾD! NFNߏh'gee@+ +GG8"^rʩ]~;D/ #oOmƚ3+q2,umYİ)ܘG! d10􁷇Cr;Z xkub4 WE69gpJAKK/Q IDmDu'oTO{T-ڒlnsFQAA=SS3EW-qIN#& )$MEƗ(ڌ`yʅorr'"K7a<3೏z/,R젏Q;~~33*@IDATtA3o. QPL9,#myY6f ?~DgJΡ̆4L T}K_UmU:ŚL͗0aHS\go_t=]1'B`e'иޫr!=c I+DFT3XO>_ t,NqMDHN{]O3ʿz^ˊIIi: Q\6 x3وc7JX"j.V 25?2[F4guD>gz"6]zH<+6IZ6H",f8b*{ß}blU K@p/ŷWYa@Q=h{t2(0(q(I}G7ՙI;qG$t362/3eUDcxb9C2K MUͮ ED<; `?O)-:J>%o*#}!'0rYF\֣*y۳b[q-L$@cYbqtjX-J}u11.{i{<ßrGʯh|&q`ǯY~wzES,nx0}9N 'P <ǝ6QiT\{6j8Q0JzmCyuM^T \\yϵD]EZ:UTDt.[u{sI%aaul )Q*%*5Z-:98+ ٌlij;@Aƅ^ NyXP{>[uWEy wX`d`}A_M ZaFU yb~`WKeɔȠ2y){ʲ: owkEXLr-$t^:>~;Smut'\WEr hg,eG)ͫrWW D0Ͼ#AAE6!B:c070pV&?|mf@q灧jY~,c1Zf{sfx*qBy\ ^^8)/B:P/l~ >WG%s,4y7+UD(Օ(> NCbF_fIt gb/$>I&Yto ~{8m rX-d1й\1[ZɍA4x~tm9ЍUao\49Qf)b:dox?^nA9ZAJ~U=F tI' [bguy3kä6 M}{+ZaSbt[1@[iMzvb$م1J [u}L ¤vu&`mCu]wR64V 3Zw` bJXF\Gb( 濶a0@ESYj-|dF{J"3RY9 E>{ [\h68TY ?aKʗEbڴ\ 6 66|g_)īexUr L>bs ʸ#ӆ$Wʒ3>yD̄\]F @GoN1kOLxf$Sה[u-DFZ 5.TI|g7ne}gm9@,~50¦S H ]?:bqx ~ 6MڞxKE6bG4 ~e/F/ 2%y⿄?ȧ2wƛ匙oK:?s |G.|_iZbyP=1BoJl(@ZE+L05ī:J3YF]kRZ DtnB R$c;N7IxAhÀ|cPG|wCZ&㫍d՝cMaF3⣟{|M̿ug)-/}~nmW_C:^?( L6$0RB;xϫڨyll;h-ħ=Rgiv#غɚv2+IݝNjLd|gK"/';V]1Wv=vZnذuGuIw_ 0&3zhdUh.{o^aP`Pؼo6R)BKmە%h"<6p]Ujw< xnƵib>Yʈ_kֆF32W%|,3m^>6ʻyϳ(-I00]P/K&>a(G"[պHZi"PO~Yd1o |<``e"5ҷt͹5u/[xҧIi)Qzgu(o6~Q_<`(PNJf;ao 7߇PFZin7:3D6OQ^QY~紑H(٤eBFo&Zt{`Sۧ]”~Ҝɐg # ɶUH, bM043%n ;? lQZe?wQ,WHG]-I{Q/æϐh0R)m7hAAOyxU0"811Rn6ٱ߮CXafqB*ƥ!YUE!B An8jKf%|uy'u}nZ '"e=*QSe7ss9y6B(JdPqVZfoHE^_\FI2My|53_o;(?N8rEWg37p}ygu]-/P%]0hm8a?|\4>h;Ǖޗ:Hk"H VhmCtO^{8[FAȺĜ ϡÄcA0S֗y[ΕnuGW`[DݗT9K(W՞\( ELy@$v]Ai1hyq&`# | a81BIlGTf -F-}9a*G(߇lȝV3ʰO񂯐p@ 1ow?(~lͯE &sGfF+vz zvn04{e'0M/|ld!q%\hsPͼks|\0 O5)Ƶv.^e3p .dp-e0K|5\J8[7 QR;=kϔrJ&CEKt,JɃy)G/K oC@tT꟪,pp*gt3&2?VrCA >M[`^&hBzNo -Ǥ]bBɛݡї;s+{#Ϗ4ϣ+E7Oީw  ,_Cυx޳\p1>S{;dܽ4U`[V Xu8DD'vՍȳf>kiCxӦ8]Rw;\q8p0,M(f^$s3Pl)}W7RskIS1}>qKz>Niƃ cDg"oSNϧqʔ3FeyqL xS-93h'{ු7)L,) zqɤvZ ȵ:Zd9(T"YOc de?PPKǮf?`HSk/0`!@օ-lg\i'W}H8m08 b!Ee-3:jNf㮠ѻg7L-âTwAAό;6Y O 3RLfR~-꤀a>HT8r)vhm8^KT*FPBU<ê S=Wu0,jpifMfH}CpZX[ GF b}gά ̀%>}Uow}O+mo#/i(m9,t/SMQ,gzP`PS4|3d} MrOrSدH%"xOiK:G[b31zC5x%9' KVxf1LQh<@u>(:(>b,a%'(68pBh%[8Mp̫iMdrUBA<2MD+E\Rh"$F,Dy;i~guTu2#!`u AQĬ62#7ԭϐhmF-笓|AImw,;mG3[{v]Ntc$Fu>LOi-AAσG6ά3܄C&l^V q[xҌVA3_z2[ߤY[O޳+M&SدG:PXgqϚzLעU+:Iy]g_aR/oOEx>eRvC6)#hSSE] pLH6rh0`,3HVm>>O{z9nf0jSvAœy j>E8!V&4=EPe" Y}9$f^M0VWG;/Cue>;.})jyUPˁF #h]:1 ⇲7># R,_Q!yBbmȼ\{/[!W\I଴$l'\{aIڨM00e:ɛ@GKԢqU葌x6A?;L5Ǝ~K=GSJ2Hgb:.ԂDHb:}7 *Qb+|>K;U =Z뉸V[@5fkS~Yeh!j{^nˋ5Kػ|zʁ(bG `HZC_6к1 gKr2., =wsb1Ž(Ka&|%^+h`'w()7Wf R] ?)\)zX`&Br_;XJ6B19a"* yx9Lx^756TR^-\.U{T{xdX&SQ# {964:xB8TUT#{;LB ftFxBLNnLp?`vЃoeJD!^B`&OPA s"Z'3,0x(i9.#T3BfԻ 'W^o 5N1|`>wJ P< Qf܂)baEm 0;[wR3`orUE7̀+\%AP#O )fٮ1>rt9ߥ{塎0JX9suF6ܬ+ºV;[f L9&Q^D'œ㡷>ͺҜi>vT~K0 |z mJ98)RWAϲqk=,Q>%N3Y6GT\/+E2Z@6z{P`PӢ@섥 w ?0: Ĝg0^ƴ:cm-a%x\a8'FT8P" l+z(#i*yrYn=d }"8>JQٳcX>K't4L <b0(0(R T@^p,|iƐQ~avw65J {?C;3_T~vgA4*7bQGN9xY/&+g_Aܟ{`w>-kE8:W]u~1ڒ;-lN>nsz Yz3 t&[EZYչBWʹ0UDٯY1D9"0=wyKbR܄4:\B"/9`Ae&E'݊wR3 e r">#JPj,.P?Wz>ڦj9 C"D0k<_f3#Xx̷SP!8P0Ͱgx$+[Y~ /gW[fcH>!Αوz8V2>zqE;Z8(a.Y2F^* 3Jy!yN?vc9C &O*'%o`}mtZ'·;6N-eVW:1l_BjQc"3{}>3 @ɖ`̃IE|HCG~1נf64 -&0I zY#?ۍSMƍ4Pqu\Q5+'ufbZ<q wcy J)D39(gjANf=O q> Mf7 ,~']`%4Q aP`P+q(hSvHޛ0saFC3`[8@[ <3eT7*y" 0}C3̭>.OufVdu9y;P="4**Ƅ$e@I܍RQ SfyUؓ{c1%K^kMkhAW1zj5m,]|HƵz;ㅺڔ~kC4ivfaP`Ps叶`r3(DDw?+$'L L AL[FopGC 1cZfcp1Q1Em^{cUJ}8!uSM_O_ד/| E8f>FndElңn"AQq-764}b IP=7d (,U>kg"!k n +e*9#-1 ҀוkS "10ص;QA?! n쳈Q|RWw*~ L]eE׃Ц"=|A^.eZ[z  VZcPox2(0(iQ`dp/(~Cw[8?N ,fi!&{Cܨ))MOmer$q /zWh0z(=>4L<8p<<3 J}PxlbfBZy0~R @OALpw|^N5NMtcZ}jx0].+L瑂۫w:)`D">\3z toWJl5Bo!/ L)%V̳0Q՚oޫ7A*Pi0(0(yP`DjWB`K2&j13˜vזL~ KdSM"%o(!fMVHZvӣOz9+ gI_mE_ͳjO)2n~cz!%%3X%/8ay.o8ZgYlWy C>Ck#46eiL}Q31xc,%X򽾞*ϐ?R|ZL2g0ΉQ\)ޏO?xvdrW滬\+of:mګ M,4Lao2Kx(޺ rTg{ D-y9>D]E\/.͝kzZ_gVSJ@̽ pϾ-| ŃWu{?=gL@)[`'om׍䅦63|VOjev~F`dUTgYTKOA 6ߵp~#O!nl?嘹hjʙ&pjĴ(W1ٿ(gZDq,*:FXDnNK'Sufe WzccOȌW\ES5 ǛlEr::^Uy9.dʍB]p[STt^t`ttqסA!6G:J{d;-@LfYOSAAOԺؐ9e3 p+uL%@hFٍ!=3ғKx.0j#H7GpeXY(=8gO<'P%(#%N\#ql+`tMO;շNEFMU>eL іXNց)[)vs򟜼1 6 #f=Ndc QRv im!wWJOon6o[gg__~ ,3\_pTD*ulr{}?1Z ^N 9Ke$+ɄP"|8R>*bj:ݮ7s)t4j(|W.s>ۼºՋ7"fJEӝWemY>II汜Yx9ߔ#Esa֎;ϐm@_沍AAEHq\tgyB!)7+%x6 x\{.=Bs Kqϝ6ۡ9%5_-ˠ\AߒߵƓU=h:N=xP4@}u~ltwMy7Iҋ*?}m}g#Πy*ޞҞ|QDY CW+p fYdGCfZ.qYeP sUD> 8-p9v@L*-ejPbSs'?qt)x^%%mX)#x;f񅔱˙1fys뿧V1++V~aQA,fےcLm}riuQ>C1!J#>Dm侹.W>@'%(1oAAAO|Ж]4?]C_D2lx=lM҇?B)@rQy0vdL2z1G:iރUdtg(lI3aL"kl]%B g((aqIfvo4[4YBxY ^8 ]/[7X|pYMϾ˱ 0p쫳L7` phJ\OVs>M Us>W _ ]|N_`96bΌq?}N;NyNJ,ŧr}8,7igpwა_p߱ w}_RrǍ퀠 |xʮ GW&#qy2͙gϲ> dX/)3{oF;:iQa]x>:irG?Fa>8şt.1'/i~XA_ L8ԣDxccUQ9~[ ]Au?`g"uSG.!,A7yuߚ)Λ`~X[JmVZ]s)E51c6]顟⊩%X`f S.A6vۋ`K=ݲmLbn?hKijy{…[F8# P8<{UXײd/e?OjE~ ˭B;9]cvyVjy2[f>}=,{[0!ո,(Eza=&NXO3**6;g8ES2~Œ\(ZF~cN g/mިʎ"D_C|sFOZ./UQr_*6ph2@\(S/0(0(p.(&BzKQp8i4*%\q7,4N _bx=+[P+\ZA v(6 ~BHtYOߙxxc.`882,Oa2=M'0#TZ]WG:P־ӌ״טˑmvYGVz2VUxR!lKY Gd8\Պ|_*LKJ=NR IP:V2%m\+RIyeZN|RQorsπlRVT]5O@[>R 30g!mr+qsFYB|5eUeu ̿T}~?V/}^CEV_VueY^S7WAZ# c ؾtwk\o }fA܇eO“ ϱLR zIǧC챷Gs} wGnk]Q)J-QIb& ρD >cHXĖ6oz&Ĕjg:>Ā{J8)ޟ ?y՝UU_=mՑH]&-U"q0ɝ3m ?ԫ~0(0(iQgj…->rw7mbޟUF$y1eRɥbB3ny))[ÂC<*zR))<1 GʠQkmR>K!S׃Bf%yK_Z)(k:bE]0|J?us1<WI7 o3)t7-1@QS>[wͥ2\tsPWKj9k_EG S#d)Q۲uJq8ŁIbv˻:9=5ILntw7黏Zܬwj}_YV%KmBԃHh5b, *zСCY~vп21t.fl~,j~UC Փo_JpEkpYPB1q~bźnú[/*5|YV}Q~Zw~m%W U"Z [ )0%sqAWdp6j ;ChBԕJ IEhlb,v]VEiΆ&#3&8ӈV\ߕj}6&_=Kidҏ:͝ɬ?;Eay7oQU1oVBq#a("0(QޘдO)tQ>+x^m Co,'jH@,oͰ:ǒ5"X%N7\"ު:1*7 ϨI?8@U&GyNY 53E+IJ.FI{P`:~ֻIYzISw /U47tU~6H)SR7=D.ǴLJqS괏@t/M|fquWλkYC  8Zwe&={Pu{ [,!Bԝb}wʮE؁J!$。}3^K(,so'q@ [˸: vXCN=§3܆~;}S\?A_?]AIE1qv2Iǁ[>@y '&u >FऴH㚎mSf~ݚBg6 }J;ir5vM{|*hol#6ZEi]{*(HuvhPV]ދ! I;!nr 9SJ/S[a5 ?r>#5%*n 0s"9T@B˃[WKlLfuӄBtFB*M+|c-g8i(˱";֡4_$BEMsAGN9B ,Ϊ^xiYvza7"ݷJ\c(L= JlT9 ﴢ<Jb$ ~~$Yǰxf)L^nL!iy;%πӹ+ ;_x7pIޡט(9N= .Sj c֬ߩ{0I;FTMeN=pj2 JT}72&wNu|КZ3AuTV%F]NRP0ޡ/x9KzQ:^nk|2_pn"2kO'?(Sg^tTnRc1In1ģ-Ƚ_kEі ~/p (bZˮQ??@Ȍz:`[C̻l~{Mh8D,KnWY48p.%G:z"J?=NN5(YhVBp(fw侤Z:’>e%Tq ,uьjpهـ9iN;|7XiXcAAsL[ؑU|onv] P`Q=Ҹ8W7aؒ=اte&)}4J?G=\ jӬUQ=%n-0&r4Pi:3ު[i%{hwR &Sƺ&!y!8OUd J8/%P Mg}:y8+]tf#tMޔLEd3-Jw5O> 7 'ߞ Ǐ◥T ?`\cB9@.! w\M t?4}-_Gab=92v͓t>]9Fh!\~gCA,3n+DŽ1Q{IysS/Ж^$x:6JHiCur(U u 3C-mΏ,H{2Dm {SڗbTkW޾֌laθ)SJt?OXte4М `zԣCAbU3[wDA%޻_99 E>iU@Y/t.w{` qd( `epږQOaI\dzAA}S[%$]9í5rݣռ"3$[w32w~cS . 0:vY.clb ܣ>ɜB>en0Dڪg| P:Ss(fJW>:eU (<)cB4Pv}eO u(a)@EΠQj;s +}Y7(pfxX a0TP~ٌJTtnTjl2bPj;?5F8}p_/U3mv^I)nfWgl]N!<'BAf=G<_x0҄1T1@7\c&Vxj)JݝN>)}͝:xͮ8<)%_r:F8Y2j$w UnQ|:l ֓&&draIs=';PrkYA9/,uɂK|ݣii|W51 )ό-m' ]y!筙Cm#XGܘIb-( ~4z)ñKOotA@A;RuƎ)mQ2!e 1Xˇ تgggRp aC:~foRdJStڏoQhbW{GurTtlH0:F#zO'z@.ӐVpO8n3q@ :ki))A:t$JNg>E&Wf~̲p]6raAJK@#lX{]x)Ne6uF_b 9"!a2>緼`z 0K(E҂qr>Us4r!e3~i^u?3{2X;aeCCB3u韇:vX-2ХsJ'*Y&}“e}]Ǡ6V Ļ_eCD;"doketT]2v24O#ꝟxPrh'$xxplp!HFqlNeɉG1S~&2x}8mQî*'5Bp4M# -(-KCPzT魖\|wN4@U߸Ɔ$ʧ]7ZIϮ1Z2,#zƞvSΔ]q8N>-:>Dְܐ6h #.TFo2"ܫ*]]E‘U=d1ޤ'r8*Nk{:c@3(\ V69M߇A7x"PumaQc WnU=N4h yO<~~AEلHNW$Է4=?5DYa|~# W 036k(ɂ %ڙp07fQn W!d ؕ;Y,ćǥú*(ipODS˺bxF 7RD)xOgfw[F'4Aq)7ԭ hYjT5o\_L:*렴Li\ezPxo+`0o+Yٔ{M]*}n S-\x:K~xM_ޅn?}_`r$wG[x<_]kb$Sz\4g fWTQ1+-M6f0[K΂%GGnӫ[BN %8+ʃb 'G)(Ri_m&%q! ^xXʸ~?[?$@OR}Ue{# W <U@p oYD8e2И1${0vR3O<)V5n? k,6]| {.'>:^Mel.n?k΅gIo/u|rWyOuTYP`[«GqUh:;+\9-ӆ7 5 jϕQmp8E+2 i馭MMG['D{ۋShg!'E0Ϟ>T%jnI^N:RdY԰Ey.XEF#c!H:WBit4# zϰwPuȠ˦it+I5.a=rCR܋!5$)6w4r| hw:t=r p"Hy#3Ğ'7ߍx/-m. 0 Aއ^:v]N!叕oJ\ `iaQu9 w5A8ߒkь@una|h{3gOf# ȣy` v֍}8'2;J G|~Ե\`SRf@PN2HI;)aw]|@2+7ۂi?W۟ = ֣Moy_/F8!R!$ͥ;}OTrO}g uLz VAqIx~ݓ]78 =efm(oЙu+ʵA-k_mwFr(C纵1hk^`&;tZGCy?#3;Ωkid3B-?=`6sR=+2oVN7DzbV#_ 7ZTv9 N'׸FioB !KRC&b$I\CwjEFGnү ?TQzfFhNM\?'( D f}7 -}ZPn2/Tt`=f%<gݓ+4 fTh`AْgŸ])7 9Fkk%"ԫVHyF:1;6)dӸZ ܙg\gYоI N^m Ϳ{p&lAmʿ\vG!rP=i 5sH1Ps>^v7ެםzk?>[ W' (r}jB}R㓀Ox|(1 @)}FKZf[&y %Uwu#0H'ns 9g)0{C$+Zd,F4xw#/ē(fmgc`ҧ祿sxţYݛ;t~3d|W./\cd~9b&]'fxF3>Z%&Y';*#Φޘ_WrIk?;3Aw9Vߙ)Fՙ5rX-Qr=(r鹚s^4y;8=4opW?t.u:e4{StFi+ze31icN۽Nt-m1㒴bAAsH !a/2>p>RawuW܁IF}cB8-49S@x썃G%zYN= <*z|(u(CҐ7a !܍O`*w~m Iuӓ~h\2w" tB4@V`8* ݪ7W*[`WaP`P#RqEwBy4F70^2vܪ#Qv,<؞I}#;Srͼg#WhBt/x*nyD'c.3Ό-W#tWt,̳Aʄo/z8ZҖP<3 b6-ЍMtt1:w:;}'eL>⫉ /s:n~7aL,A-ѧ;7U?;.cOO鞔I<{RǠH0!=ACT&G:wULGs,2FdU*cuY:ƲquW!e4p_6m|h%X4Aj <Ԛݠ2\iZwtt`'6!- S׬3J+@Cd@,Ux|[+ 5k7ż0p4 pgK%TVffv,̀6Z LRuNA ;뱎<MhYjQ6Bxx1) x<;/?ĵ|I:m :*GQ3Q-|5^D1C:ߨ\7S eaP`P|QL,A)jVFmJQl_4'ixns(LwWa+)=TW"/]_j\ Iqko8+w p?[Xp>]?I>eQ.Xw=W*uT~|nY~ Α潔9/m΋Qhgk8u3q]Yՙ""Nw{*;dpkŝ=T6讱m|D5 &# э7Es AKAc1c jkeLyB<04 DB xs--O{cR|Omp2si*\KŻwdKH7K\Z]GIf\ |p \k_>-;3]53ĮLP֘٥Գy\Z)eU J1p>>݇hʶʑvGhzjs깚1A/1Rwi pZ:d'5oWzx^&xNy&4|Oo(AVeq9w{FZu;eI`_̳Nςtnޔۙl&cAE?7O"ux4pnMx:S"Vh^z5]_ 1&3g] 0ޱC) 0v[pI霶 +d]%F*Q'j @Wr91*`?q{c_""@&tt DA'x[ɫIC.Π.D>@#Ud #ÿer=pյn[(wăex=t' |q=؇ڔv{Or9Cvu㱦~(Al oա /qt;kA^Ǘ8(^yW.`nђ3g ǫSW|gCA%_h|ƍTӁ8OU"V K{V,߂& )S&h1z]%(FdPyRk("\Sܝ44Ԡ:F7P5u*6YR^NٕIڮڔ72:.g)O~ I!,P S ֱyz\Wmsb LzeĶgO҈6X1.><S6BXPoF5ox[o (,Q8w]+G+p1gd_!)QZ0e`f ~ u@_`CBw5 w 2jfM;xD=y/SiWV˩kK|M| MR]iE@ۦqEʕk﫵6nNB?Be_/ WJqc>.;okt/Pc4׋#9ClJގd#W7niF`Up?#bw5vtVS#O/ċ4nKjsژxTKf,W.,z==--\ ($t]hU{Z_'LdF' M%u 4:$_gd V{QUgxV?-e6.4ao C y x>c𯋄>cggqAǔƣ"˚G#dJDwCE^& -'HEw߬A͇F~gXK,[㸰\ 䔎6+˧Nfyt.uCw'KYcȡkQl.kOK4_+M]UYDb{}pwL#LMGU3].+b;5A!-ed| ?sOf;vHfG/u\4|~F.*X~3 =<.+(&%`WV=Y>.8]+R;79I8wx [ԔWɤN곲(~žXqZ|\Ң(a{FGwqYʌ)y erW6嶴 '7v;vjOnˑVF(Αet+j-++5Ä=Iq$Pz m:9OMj9ۓ8ޚ/9 :@wPޤ}c%* aJ$JqZsTY$LML(jGcտJ:]z|vT;vM[,? {odb[F|A ӃD0߃"HR`hp"_2N`.ud+U 9=њ$2krEnğ'6Q/'ZWȣVq6r N]'r`RO awk$% \AЦʭ8mC'ʬ&ic$D@x:G>~1WT?l8 |o7FY:s  UüL aZqh1mkQ&FWcbAܺYtfzeKSZz @kie6/ʔw͕A'3H LLv<*&pZoRDmqkCScMRM!dub)+%Zuضd sˌI>)S7ˉx=9pU %JSә|R !0p1u~dMj)'~?S7g8* SWS6Jw,ki|Eʍ'ggv#D >E{#)=Nl$ebh XOȰ~yے2GKe|?E>x>E7HArة/,ʎq85/̯ޜGj3jo?9p P{{4GήwXd w5%` |mWL\GIXϩ({(-1i.}Fstn1;f-9jLr5c,MkX*N۴EPm@yfŝo=?ҹ=Vff:2 NKQ˟p*|kO.2=Z:~-LʎyD}`p ~<ϗRy# ȹb.pYC3ņj}sQΰBcVhUi)r2ue(-3uAsm!ؘҾL(/&YAmdIЮ}0YnIXH9GՔg]>|9Ьqv(JGMTּj<à.ɐ>O~bW{07'92ogvs(PJV>nS9,gR`AK {wNJ50X9uP||oe]P`Z6)UimF+6. ڈd6 iodYeDP9JșPQO7(,Pg,/.-J9S89z)ķ6;B]P?isƗ2ޙXj6168(OpjԷh`{|Lpvݶ[@H&K9=:œev_K\d$DW |qpSl +ȕТWY=;ѴM/.rJqv/r+5\ b'Z7:`38ٔwyە>.t-p? ځOf%c^:h1 ~*uJ͂s/8/>I`3Z93Oc3,P:Db|o-π)̘j #煆dW{xâY^L3&YGfyacX i (+uPo:},e80f2X @X2LE/ uvok@[ (@ 6u@2-&qjo[%H^قmhZ>1)A-3C(}@Cxq(2&Pv+K{+_}i1/P%ǔBLKÇ=oSi^Y#`##`^e/'6Hh!2)(ʘ`(\jJjRVC֐TV-L|ϴڳ6FZ@H.M.;egFj$Og\'nqkX[߱Vn,hTNp4|6GvӖU6V_O?RČy^K8Ǧ'EEg@~-A+zxBs`Ł`rۙ|> |:*9lCJr9yY~kfcdG<`t^!V[:rvH 7a9<RBvQ; RVV`upn7## عK_Pۼ4wwhlC]`N+sFkV('crij2E\wr௺J ^"k-mxrב2Ϟk.>drp\|f ;gSٺim~0(a 7]+s ~)5\ ԰8@FH7R{V;prwЎS7\j@vP&- Ch%PYq#mls;qWSᷲ(YYJt%6.=f K Hi3ĵ"NM %%P7œ|;v;s1!4̀ 6PNۓo՘ sqj0>RL k+c !J=0_4Ԕca}"CdOqq5nHaFҬmRA?)%䰧}2N(fIz+VϙVFʉOj*{|!^+b'<4/MT>|!?ӧ]a,}4{C߉Gry3Q ]bw|kؐ|'L9u1\|<8;~,ppY/oXR?b&Xr:e+HT.a@nADAb`o=y;C"rY8́^د GivhxwDe8.<@kZ"۰=2ֽkY]{9s>9/,zf(:zsf曳b^7Bq 4av;6ABEơV+9('8ʨq>?axtՖ-e04:ϑWi%.j>\U}xNi~Jm0qfD2(MmdPP+Mj;-\Kg|tV*"btQv)]掲a&8Vݮ!^qG;Iv_O3[|8 l͌.ZA'܁ MP  ld$IcjI]dQ^啼r&umS̅K2:Asx6N(5x;ɑHtOt H眯1'tש`lw1aDs<ޅ9RFQ,ėӼ]ˎNHeJE1+:X'4}+hh}tuwèP"au@ZJ%@G9v \{Sx:HQD |(hàcemԔA7\ ɴsa΁5'h9|Ke>ީ3Scńخx x*S 6p"XY2ʸU:J#G04i/:B犼Ij,]kC\ٱ[a9bQe);4WR̜F J~(%#8=E, Mr*ESt)CS",'#r #|^}D#dzj఑G<+᷺;4B/+6OҟsѶ1䙸QEɱ3柜 Hg\Z39s`py$L$r اSMPO/aT?qSVpљNYH}'\OBpKCP~qs?dgswQ>C  Z9NK >By@1޵gPmgObvpk"wȄ9B5ljx77GnOJ}bjxɤmpj=YY6ʅ/&!݉KS(WFb QC0-43 dKO[e"uYT<(FUV BR1"$Uly/{nUio*3,t7?Nj4A;"4&Cb)zCS!e;p`Yg7)ǥvpjE|~[/o ,gEDA| *S`!l`8$g!O|Y@Ne$kpVE:_J+*274LϷ^jw֨ qɚ:>ȇFFZl7 F`~G*+73_Ї GaT8 i8T (ǀ, f;7i\WΎm0¯(R}|l g{9s80 f?ܳ:n2䍕 +B !ϸ9T0F2aU J~AYų%ȟUlWBy4{?JMmxnخ~Q ѭLj٤ykGoPVxIZz"&Wh1s@e1\էoerl%y$}_dL|coMhFFPҿmPDKٔ/ćm]F=~ 3sU6fj쥰xs;Lef \rԾe[ '` l|F]E ]l{O凋 9n60Tj.N a-Q{4ƒɵ$upT>H١\|ƒ;l_9"e$Gd6wK'k;&gځ qZ~ҳ uoivIugB$B;o7JBD @KGC>h zc'o Q>4!?t<ߏ|Vm|0]n jJ!cKt>(b&!cqqX7(rRсbǓd Ey,EQv3BV SmtQzuڪP@*M+X%@CSN>"΁wgg̿'3 g JólJF)~5 & uLj\COl'"RקQXu_KWš("|UHm@vµȋDsT(/i +F[So}TG Woяqr ^JhXNQ{WF;_J# /gLv=kWGEGq)A*ԋcC/<ɔDRӃGvbt=9pfى' ͳ1.N_|$g8vW˓5`l* jɩ 8~51!v.~rc؜& Y~#Z+OQud8E1T$ h I@iNU^&2(8 IԑI۠LeuT6]oʗNN H2 q]y1]g_ʩ@˪#wH '><9p[[3};ݾucyx\368 l1  SԼwM '2rk@Zbr 5:s$gWNju-ȕ*1dXEW#;4TNZI ߂ Fk.^e ;eخlolp:qT=xqPUaC lU,2h$чrcӶy'+Pȵivh-$ 5=9pOK~;껹|lů&[qmG@2s=3;r>8I1TN軕]r0"PY g+vQNߪr Qk<9Ӫ])U7z.bϥ-=nas=cTDLj'xL|b];tȧS (~1g a{'c _w> 3p\=9p'Ilf\*HLqgNGt`ẝ8T 6k{b*˹g!C)y!@+S&.< m #og RkFΩ go:ڧ+CNX|r./Q߼r:A*2ѷn#1ST_l|1WQ[\BU)Z{E؉#:GGg PޑY4c#x1gsJ΁wfbӞF^2+[ 6݁P6O; j鷱%!eXT pGy[W؋Nd&27:9t4n;0, 7QM!N-by'e'q.~Fz/z}%ﮉ ##yMlՇҵoZ֬ѶkWiW:"K'eײW2:˼|v {9ķ|>͙Ds "!WCVs}!uc蛐>3QymVS siSr~䢌U孮㼚^ʊ\a΁;{wTUv0|Q |0 ^1ֳ=? Z=frgs8`K4L\RƘ#>*Խ[\Gh$ڇQ^Ge>XVI4}^OQi {Б/8hX%X9뻾iKB ,px㎑{ښ~ˁcp̸ecbC#ʝhA|eOhJw*V|ӯsc{9s8`B矹뙕~F)# )za0=<*4M^c J7m%+4_{{FЌ&d{W=[VpBdgeoG=i{En~FviZ .[k~llF;CWdAD 1H;]$]Sq_mH%م?16&W68{4(5<*ӿUx:`0d Y{C]>Lk)v6I'ĤsyP`BY7(({]Wjz QF.+*=P^8_ΛcCC$ʛmY~x2~ү>bOX?f?nQN1YSA*AZONq s8V6yL(q4Y@--UzU"%Or%"}d> t$Py#JғGWr2R J}6Y4JW>XR[IgԶC}7]OOjվזel;vU0#/~g3砎3(6x"׋mˮHmޫ^xek!KdCdF9C93:[N|h1ߓY>i%Ð<!W=wsN⣎ein!9Pţ>2% ݡ F~2F=$ۈ0_O%c>gnzƷ? w̆~>)OVt 4]u:4 2 y:u0TLx  Jr)[Ұ<:Gd"_}L<(XrQHHQ=PY\b(ToѤOKb3]&s0䥹)fɣߜV1^PSAƂU09v0a;VE/j6{?i~L M]7<8 Ũ(H9>]ٔ:gR %_nO:ӧ95S*y^>8p,tノ{dO1֙2!< +5xǹ6MSK;( km]9$;?$C<(zO!%L9smg,!#<`\g3۞_A}bxa.Qo.40C)Y&ApȢtAg7ʫ%:' z o^Sʠ -sVnj_C8W볫hs]fSޭ?D2h?LҽRwJ @~ pR] &M|x~a<~9sN80,6񗿘~\;SbF3{+/iEQ]<+yE| M0"^_KI-5+(S|(}`FNq#yF[|6)0-5Ez-)K|7ʑDW,nHzg^*O╏zN ceK#+9 IY֫=n7)$s-g%]OTmrXsm-ߨ~dӣj/I9{9s8i,&ZI`?-:1a[^5!jWɎɰY#YP|r"cr }} wY$#{+Ju\О]bv lH%*Jr5Y-䖗myP֊kK9bVqGvp֯׷uNe%;eq/B6 '^s8m\2/4ωpU2uzR IO|+(8r I#A3&d];(LjgHi|> _yѳkVv䵥6s`;3Ϸgv2fcp~BHЀAͲ)ԃG*6  6v@pt:8#l\H_dNY*p\Y el;1Xj bK) [CA*zmkMkM"A88;^kmi'NCS 1†!K BoG+H?+Nrq.C5Ϧ7"\]}Lawu jZs5p-.1Edr\LeJWm,!'_^:M-5QuisM9QT\Vu`R>`Yh3͑FʾPeAzuG^[Qi>]Bh 6'mp^ >cDP8ם;ttzhyY|Ly"mYЦbr!}kFQI]ӌ|ΐ~+jMf(_~v<"ps; 4~mrG8΀ >&Z/geLAf5/y<_'d\sdrj;ͯ n95y#cZF킶8txDNF Jh]vDK5q'?KuXTG2ՂCdnҦ1hcomGfIYWB5Guge:+W{Ut2KwdnmOo7zϰ&"c8ꗵۤVrP05m&qii t6C):yxN)yE[NL<мyW{칶`j F:k9s68?Q{>S|߬6Vʖe8 bMa QHtK"Sζļ?BvD]G@oqT>ն^˫mWFiKe/}H֑yBa)ÀWNj`&++Pʱe}jGM9JkK$sc҈3F:FQaėg<B ![\J?堶΁wu,zwf_ά{-> f2`_L7S]<.쫁!N,?}ri/eJ+> .|@lQ߳Ɓ_Dof a0')S~!S[-67Ac銎Ax!BQVW8 G(6 7ʋ|ot$Ed8 Bqn@h٢vdJIH޴ S<;_R"*á ,W^yHGʨ(yЮ rФ.e\>y&D"H x-bt.U1 w oϗI4>de;v?}<,frž67 fՑ}oS r ʷ5EعJ?|p!> '(CyΧDb$uߏɋ*sX| B*0;FikGYlCgSaC[ Eڐq%5Neivd6{\P'FeEi˶HDžDևv{e&>2,&s+A/ F(q6LmW]Q֏C)X7a)rRe9G'k"lH;:O(G \^p]k88M:˥þM6gy:")C`y#Y7؋9*a} Y>*DΈ'ʲ2U5HUP0")Jaʻ(q3(tqx_`uhvQ6 :Ki;v }LxMw35}z5]'9#os}*^Ngq)b)?/F/ևM~N ^Wzv. ;TL!OȪzܞ H{d񼁬(l~>nOL(vuo^/,J_Y 6g8v[vOli=R!;.duZI "$楡*Υ~x'En {vQw2kpЌPXGۚ:Pf}/NԞ=Ku2t˺j(Atf:M\e"ŭgnur JY;K3=k0!PbrQ:;٪z+0 G ڃo0!uWɽkb;n~s2K60.Lk#J$ݘ5l Ŝ_!B(S("XHC٩پI +5Q90X^4+S(Rݲ٠iu[Jٖjgsܓs> o9 '}ᙜQ_Y*|lj,F [6;C9y+mL"u5{`GlZ1;ga}0_֣#sEq2[({cm^ƼɃm+>R]DJ"E˳-qJ ]db+!*]vʠ*hQ䠠9Дȴ-yp3f}zԎ/3 Њ ef@bx`6O,@N^wm/'z$?4hFs[nrzbޤ_4poe 6 oN  J 4n/̩ 6$v nJ) 9񭶌lBL'GlS4@QZw*v>wF#<|1C+1Ra%^ZĶwըtM%χ~wgK=lZEB NdEȭ2NiUz%'VP&PئFsl֑&^Dh uP= mzW9SÃG3)|ɫY'yH YMtayp!Cq6˃y7^ۘܠ?U _ 2E |0?:wum0J/^Jg ^xD?1\ [o\[@CF,T>|}wqn&-;^4t++J(>{SPՉʒR%44x_єK1Ry +ҸG?*ug-Y|ڬMy?myxĉd쉋LzNy𙓌ϧߏ(ߨa-r?̼?gztNzfޛXC/;͎gfBKf:is\[m\!3`:}1՛ 2O1Mz6y {.`zn+tSxk,{mtז%^8?3^5.m9JGJÃV}9pZ*Ե@x1 |=lx<ޛRrwlJ)t#aX/ZK6gSHh 4ߠԂۆ> DiҥE5$ zCƶ81pw`Q@Ahg?0ZD( ^ ;p~G!ntPPFbs 2ycp><]5 /1`%|Oy!j {c YʁכQl@_1eS.9dtW ^6ZImfJ ۵ [R/P!:DGcL!iӾʒWK~QrC*+?lDP'J֌S [JT}?2tOjڋ7qkipwF#mns`-qQdŷ{^nog 4g?,6:5%3oeHӶ1ev1M) &!_<K`(9*((7dʰ7sXCOx!Ί5W|bv;Lvu-+spE5eh]۲=N|O~ufϥX># gxN~6pYux@IDATt𪴟+ls;Fώddxebl7x}:Zl_|2eU~\:%N0~Rה-DAgϏ@' DAKߘTR-A3#l;ބLʿAnbNhy:XAכ41` >ux8Gi2|Zg/)H ?Cݖ9Q)ˋZP'cmWs_ >3aL2kykd&{`gvf"L1t̙DjX[EuP:&ا: 8U '2_Y>Y*OJJW=xQ[Ed}Q}ɯ/RAA.[/g%+FLd.m7D9TZ [I|i1%rsn=K9_cxxgl9ڿ!Oi<r\">Tkqa΁W8ZLfܟ 7gJ m5}1pp2G[ߪEAsZgιz\ j?2&x"zIT8>Du* ńPvvi :=W׾5<,Cex9', NZL[7ȁTЩkd?.Gy.2L\Fums"%L[x_ @x;W8i~3U  'cH!%\U%[uY:x%jPi(w68 ~0A{ t< gB̵kgP̐d]SΕzpOϚT(0Q^F6#4?|ɳL>P3jlJOəmN}YPgƖw8~9s`p{O-|>sͲc9"?BZfdu쨂q0UzeHs{rA~ʇQaHimh?tlrp-`\uC|$܎ާ8[ߙ!V!DoH s:*e+evEĭySխ\oWT]h~cAyh=qn)JKO~GjYvx%XrA̺#,Ͻ7ĞS-g8xaBv-姺kIlN>`$lW.t5c>}(B[#?|B@Xvm%SЯ37yaJ>F iRa.D^ƽL N8`>I X ERBz|ݧ%EH##!4b9 ~nJh sI Ҡ"ŢnoDZP^swY iJwƝ'1GO|Q& }A>F%59se΁/^trR.5| $]i.R9\vAcYց3pVb rٹuƞpwێîy>Y*L(~EĞ%TZ9.4.hmQ_!Cod4x1ٞv'>5Wv#}8١}p+tt/2IZ>VG/L$aw!ALطDQ'yqxb_ |xL_NoM< 澂dS⁁zw᧏|6f΁`?2fQ[lGMdf~fj*9\ SE&ê4n΃(v=WA{&R %)a*MW14S\ڢ%Eekx(;G+]t.KjK)P ň|;ҿ1m|4(Mʴs*MJedXvId!%1Fx?\}/ݙ~Cm֘f3Î=xe-QKg藣gi89oxP\v9s9qp6ؼeKpo4TbeC`yR|~>[Q@O/A V2ljzRb;_GZRZq, {W$%_!vE~p__;OJ|Ҹ/ (;b 䌧C9sr(')Ӈs;ϒ֘wny q E6)KO;(/ۂ/e|>C1-ߛ}@c[Ɯ V#5i3StPN)+؞j8a%C7Ď3yێ?_֩;tfp%o:sIGʺxm6|@[$ 7V+OMw3('N q/o P p`\_h;OKoqpn^w΁/NVfQ|3Fs+. d@A ~Sj V\g?:Et蠄A| RYlxNEdoU-Jwڤ8Ľ xP9U-ͥIyvkPCy9d]r6dvjS5U@bG"Ue>r疯nAy9/EvUÚ|g*`b1"ȲD rCVjHBMvX1y.Q;I)Fo'{ wƑ.a)jB(Xω 4Mwut_֧>5:ir-҆iMu / x_YJa;?D>WP& 'J)'%z kk%D ƍ5+_#2 w @QX1VOg;܋vrqnjC%$oK]7{Ӵڄn ] IGMϱpc+Eh{Y@J'֤BٗrFdl(r'~2BvuVx߫/ }7^_T i's7S ?@Y :Ƅ@PnbF+a6)jbݤmfIfmyt.I#O8/%z+73)JEg` 6 Jxj۪Il[]x.I(῾^./}ؒߏIvK^44s`Kq`,P~.B?%'Vsup>Ƣ܌goWeM~۷)qVHB?ttU=]5hk y2x *k:SD*O(B=Tr~d+U֢. ΔjqF9) ܆^U=\nuFy(5qZ[USUaΐΈ̶hk,4(ۇ8>W_†74N5Qvi;v`_Jx:DZg{H?f|pŁ-rcN `|.OBқ(Q~ޑʂE-s1OxHysb}6OSIð4 ̙(#4`WΡp[ Sy:0ݙъJ=:^>eIe95ډ8Dw-ۡs$+FoyLNҎոV`g`F&irx^o\B=9RXVX`ϟT{{k71+$N1p^vX!1gף Qԧ, pyMRse.k1+Ҵ2򁷈~A Giܹ2t7up, z# ).L'(+xBR#.Fy k */С ɓq"=-uȓ2ԅu*Cν|o&jxɞt()So̫^Q|61oVeϞ=9Jgns|X+^ ,t c9~2 ?;8Ș579fsu0+r pO:-@ԘgٖSЂp0YA^QRlAS0Dib,k4Fn%͜ev[me9_61th:Jm?MvЫ-΂2J^Ӗ[y+TjӒd%ةm:-3/4C&⪠ o|23 M.bis i*GV_#ybHs$r 7!;!0ӳva0שp{NƉ`jqgx {rm렬b2Dkh_ (J\޵qM3춂&3^~Tc۪w/g.?0Z?Ϭ6(oL!r90J_!!G֘s0P|LV7k(aEA P:r w*ꈲT ݔPUeaMqmzUN'(Q;8L֠N̳-m2_E-\WI erш.ns\'HL})h\h"}xr68 E`(}15\{js`qfW 3ߙd}¦ ən 8NXG&_`L`Xy_%4V<=Cx ū[#`V^H^堈 K҄_PɖYI\Ly`owGl bB4mr]ZҴ}eLHrzՈҌʤ5}*nkyxgϤ#<6 Jݤ74&>Y+_}L6 _AM53tHV iD& G (8:<X)~JN (, St2&nQL%٭ANXH0~W/f.mϤH䅞(dzh 82pSLp/ѲZdFPhPQL|@=9ps*9B0vܛ5WN1G_yPb`::Q`APVe)Lr$%mq5zJ;Rg$Қf{uO^pl?hwG.>+,(w^~H=)w.JI#>qw݀ЖN;-t_x CE3z2#QvIRԨKG;,Z!IWxw4M]9Ȩ 0ܝË=)/fjcl03N W+KMIî*^唐nN(E2hv6u_tw :K&ɽu4 "[?JNB 'mwH~J:% ,zKݱ|Y5voS)}12?Ef%XBU{mn}#oŁ|Nax $>25?]WJsY#B4ߜmVUP u j܄4 qhO'EQ;sͰ+4˗ \* z@Ўc/Au-]~ i))MoqO_-o5ʵka΁GDәWjldS ap"POFp:% t[RSg#9alA(|(AraӪUXu [ZP{a>UWuSD dV(hғt)4ʎ\|ckr]Zu; {iLg7zՙ6s@\^>Q%uRH+ pYkڞ)LI P7à//Nt\ߵFK|Aw@ڨ$g?RH=9pS̯?k_=a9sWs~g2l (Vs9Z=߲4Iw-6 [:hHbzvi1CEI?+ 3F[dޒWVo@^$y2/c2}t=%D6bu!^}2;%#&PlKj-K⼖|cЀ1!09)kO"Fy_[J(SH@VN`^ u/7u{)$yo3Á OU\mּs_ ` % ZZ?$@eX/vR慌!-s~ɂrDyC fguDIgr3BO;TUA`Z2%3Da[JoM9>,{SeTNrRv06y!^HiV>GSbOڥ~΂I<+2a'N);Tm^q)}F֨]|iK/yNO AۤE%Ϸ3. ȯE܀fë«,NtVoF|L`=W\50TF\e3%#>;~L2'UZyR[%G]("a3` N`Nj#c!Жw6 8rE x+!$&߂gΝ +fB@X6置Cg;)`^0?!q ehi;t Z^+ЕICDtPZeL`S E {~Jtf.F1XN=8Utj(#0UV9PuLr?:+VlpWkZ*s۪H Uar`d^ m:<⸬ltE2;%َ&G'd2kU2SBffЀ%癷Cfn h4ZBuw-d$Yd Œf&nٱcfy _yS|^)Dr;GO Pӟ gXvѨߍyր>^YPfۿmдgG8󊗅S{8WQTšN2XG/LWkpK?"b>_ysb F@k@ɖ4a^zM7pZ'ɐf9 /ڡ{\PC%ڡ3HaP_W39 VA{bn1e;J@@(C>-N4h L uE,uGIk9tNSkFhz~-;f?Az;g#p ` 4Ne6L &VQ&9#=WJ<1ݯx@ ^\a1&G2l<3EAu-\#Rz6NTؔV:2XUסKC)ɵ=0jzOidVPEN/?M>oõH}RZhx Z'xjzg-G2BPR>RP@wTd\e_5%Oy2{)SixҲmS9LMͱ~ׅG3ôfb7<[:?82뿭`t:g^a(ꔇyh0v:&8mWF?A "GYrtw)PF3sԔG :t2LCݯGpΞw% B {DE{"A[` mJfpЈT+-.{?j ie3_2U4CǴKv ,(rFyq(3_ bzyxynM09vUM~qL|*%cl3qÎTOaW٠|!.=C~_D,lRO-inS`S(`75q2ɸgo_K+ܵøw.7Ç-X?]ȍ=¸#,"ӫTl)Cf䈃aj%H+H-31f[EʁsV?l|^2pBT R8W*>\[zGfŢA>tl~%8 "e*RgmA_]2R'H ř%Zġ 'Ym_ϴem([l&NtO;|+j v垓w;^> 䳴fϤqfwI};|<oX9l K8~.Y_G{E?rzu6 A0+\A&)pK^=|>``TWzPvT>|;hͬ/O6n^狒C3,XNK1-'0YH[d$ѠMEQM^ L }yW8{)S7f[3jqIMxAx]BDoFaT@SrHs;\>x;D1L bcb2IgYngc8e~ lܚM p蠜DUI3wэRE0R|0C(\bf W0h#B&B4 ͕r*`UU=*?;\J7$ A.cU6lȘlqYi}̀cyepJ:2x!+ԂKVAYy z:k΀kVtKuQNye&v/Pá +΁s(7*}Od bBq\yɻh_'@[C`xoՒ/;jpHGeL:K{JIc;:Ɨ=Z]fL:)e/|J7(1c>P.U ȳ+ S`)`G({wF`٦T20ḻ(++˱ A'6@R /<*~oJl)WI pYC?HEa sutE9,τ[WeW_ƹYbuys~о4leea;?!ʣN6n4`pQP4aML9 mpwִijv|y;~jhd|3܍lZU"%\gFeYezJ '8LЎ (|\:s?|Fs~0i.'?Ɣ_EV>aߙ3?#q1 j(ˮ ; lO0?-Pˊ'zPOF !6-#qlZVIZR7N?'9" NgRۙnN1|oţ>fó8\s@QF#>[ךmτ^Q>p<ɻ *4Y )5C(= ܵ%_ɜʠP=哷1˯=mp]<~w  ~@f!?_loP J>3S: \^D*Χ5—h?x*lqʑ;a潩vie[ҵϔY3 qKrޝzIpJw \TgAw}P=)t p-sH>1%Q@teV8&fui ѯ>Yx3VXgF{mX~s49UfyLnQl.5roٖnq`d6B*p{eDaf)5 _:S(W='Okn$Nq}sJM _t}4uP  *zVo!Gw nrGwC}J;VɖW%S[xꨏx_5~l^ gj?S`2lPh}&wK>yLDZnI#F^\o]!<5+ g0 'sBXÇ PYvy<сѵ &\2eÆWNybjkSf=,.? /.6qG^J^p0_$0]];2`MѿLy(|^)uұ900)PDq즃E,0Y|!}}iPCLO|g{?>9ihp6SJm;4$$y9T*tK?[rpL͹  x~qz}0@(5hI9Wގo/┉i&EeJyU QQ]FN:gUGf0ZXj3TOR(XO "i%Jt Dp ~8O͈<@IDAT9b q @]豄{QW=˓wNwF|fi3s|&;M o|Ol#81WL0NZ:$J.e\/ƴ\ƣs7p+<r+qB&wJmeO˧_;8 {gi`]9!:I[! .*<|#v UOjA3py(Fϵ>Ya(ǙDh:#`\9@|0];̟̎bk(JYC.$["5lms`0?R ŵO 9[ vjC:m$yśYa`4w* o#k;rS9xva;Sޝs+72!Wg\[^4`|lgl4] m8DI{g !Zhڞ:Y2,?)ۥI\d_Ĵd&$TO5mJCDcos\#[StB/Ey'^Χbg0Ns{n*J >,sW%)UŴ/(Z3XǷP枷v[v 8N4\ WGTu ױt>H@%AZa KP@E LY^sހL| DLPt^ee;Y0* Y[Nͳ|=Q|/9ZΉ̷Q6:QߛN>9 [ u}>`p?fC%ވ ps\1y"ׅS~U{N(o*^<^&04ܺi SL<|8 Ż*ddzдpׁO YץC\ we"^'7`,FM:k"I"o"#H6p Zd9Bo[ S"Jgg8l{zTA4B93Sρ)AN/LJw|}WF.w=iSۅY)hv37$ [c3qlHOv`+6J%5BxlR3@<:M6ʭW %Z$EeLR<~;(9En8 jL ~K[pΪeM rG1֔CqK1 YѡL,msWn93t* ?]~>~Dw![7Qa U?5N:EӋEwLQQx M0k`NmAnQ"댁?_XvHAٔ>ϞJU݀.,ĉ eN,ڎz&y$9p.y :za CsۊGKmk0+G㋻keOS`!8L_l&5יl~,Y8!UB7\,iAq<8wM 7|Y3Ga³\)#r k.|//dRTq)c7r&@F:ZMO$4Ҫc |JGtRiO?_=%L䎬BټZ%r';􄼢'_F n"7!i{Ҟټ"8&S/:Iz-DlQ9.ƽ vj0iI2dB{1z=Mʡ ݀"8SGav_Ѯ2 GLfjO aL&{[ L!3v'ΖWSٔ@X^YQ8n n"Qk MЇ-EB^DF;ȕ9%-ޤ.Ȑ-oޞ=CW4A>,HٕyMF9hI/2h&&'nh@iWSifkAڸ^/GL9k\I!8G1v=sStEa->tIʋbr& ΁9ku0hY(Sk'2啞k+Ҭ& )?󛹈WOl szzQ $K8E0vͧ`=T(ZF{/&(EY'c%_nfAk>qj\>q7yW)zO1" j$VYxLO%ᅩe딴-,4 Ws1I䅋 a- FI#ߚ*ZgZNɘ PKnC#X^ Lc(19PY|YXt|Am(d h&Kuے?GϖڛѡLY6!4rjګj'wBs.S`e͌vL=#g13~ ΁-"\C#:W0Or.z 7Bn )ik''9 `okYar\~:[{En@Pm:c}\.xug98{&;9(x=g+9hwD7Ey-P+:obV@r~ܭĽ+eQ&mbakgEsqLGuBA &_v Y<~(e7B= Fٽ9,ՙlB.q~cڔmq9A/@i*r'r/qȜ[~(oL<卵xng(:a't`g$Y!R( oKGGUZP(RBsIkBNўdly꠾LQ+zJηw"[msj}QA|zd/\lm쀫2SF̚| ofMT0hPzz)mm]f2J=)hFɇ9~1꽹f+^qČծy81j(?Ẽfro t(+ʯ=jD5I}!Æd4O7BuxRWxG^Yׇ^*2HXI*y?i9jr o9gpHS5Ygl { !"utA^sӣ;84*/#`q-fs;|b@:{ʣ0$pQ JZhWuj-F&x&{SZ=B%>4I_Ÿ2ƳV&c<:3&s U0u(,|uĄ}sm!lQsp*5L1RHqU_[| s}X o9$޵i!esvEd6<3Qt .U&ˉ|҇ۮKWl÷O=s]#+NȡuBP :Tr6ErS`Q0cYFFd h? =ݧ]LZG8pc[Qa ^*h+\٨7> ۏIQO]=2Dy0 [yf=)#'߬i|0Z枾h0 Jvf%>ccAH٠ |NB*?j.7AOj0G—\kPČL+Uʚ)oL]B zyN#?.eB;p*O| ܬ .֜>7.Y=֯( &  y r`U1,HhkgY! VU *%d; YղCS*=LQwPS̰/Naoj΄t0L + 7,P"C? .!- à,_spg+ NB=dn?yUJ4ھ:{C Gj4T[*=7Tw < r3qDp 鞳"ĸrŰ 8qTx'j͉c\x9y~rӤk`؍qir,N™W;&d8H&KS)xÖ)#|e <̡*O'[8GY&cg]Oi^dQ_fڡg7e_4U2Kr9I Jggf$vO9 ?:QHNUx[7}Az,}6Mp-i?S`>O3ˌF{m؁Cxuϻ3Rv9Ƽ yq%co qҺWB>gdߖ ;!94o&AHWH?9j's/=Y wݽ9Efu4O0V ~KtpKr|xM|̞dxkU9d HykG TLܑwO &ɵn5tmduDdbyZad# 2`YytK[Kʎ t$Jk@@X>G0d<"p;&3\TVcLA@igJ QB@g{,~ o;SP-M0O#/!0~9;2FeDyn6k%Y;fuHmKxܧP3-޷D=x)M"}[rJ@B{(B[q㣮ɞ7O u{n˅KdpX2󓲕ŇZl] 1qCZ+|1N+Q(6?> h5H?p 14 G@$}:J3`CIF'R j'9F|Pog~+AhaM/æ?A2© gǣ^b[,?Z9^g99VLjIǹxɂUYEh=Z-cpPe+xˇJx<wV"}uГCu"_hq3(8 CB)6QeΛ<~6IQ/C?5.!RCgM{ٳ)=|=M -Q`HeB룣YxLT=)0Zk{.~ɔr׎j=?su 3Zo͖KHΌ? J_^p?ɼyO3>E.6gi&~H/?xx"WgF֝3 QWؤ<[/VP;2 aN~ڈ b)Ըx9R3nZ^V|y-yZ&zAQ7+O{HgoGs9yZ 1 6] FuS9ҩ/ǜJu:TTu1~:.eQ>4&2ѝ/MiQlOLUS ,:6I0(#ˆ*,^[D;U0S`+]M|<-9),Qk~OZd <+a2jwcj}ёwNLL ? ,c{xHP'C qFD1^(NQ(FjJauJ2t U<-=\0tئUgaŸ3{QVH.jgOi 1 qK/F[qP剚C^⸐^煖SƝϚ~73B͆ݡǛ_KVtG8C2-/֙b -Q`[wR\ zFqC RSL:8|#Oc2fɚ{^w̓'f:SN iN^/+qL?W*~T\Q86pğX AS8(Jɘ8Quh狖}N#ڡo|^kE7W\ akp;` #gSwz+A+ƚgץ@ ;\3JGq'O%lypj*~< V 3/5_XɩٟILQpalt{}pU﨧޷=x)o`G8aylq%1ʒEqxmjf3[`f1%`/5NcLΣcL3$uԼ6xEu gi8E72*1$!'cDv(YlV;iD Ui8- C>t<4o %O}CQP ~qhAEl `KYubmtK,!qsl}'$mi3Y 3Bxm,o)9P|ql(>JF󬶈S$ˊ_Yy8;oP1Vg)3 W?D"^kk>ȑ;_U:#4XpP2> ie^a%!3ݙ_ z5=pDzSwNg%ўUm &ߝSK~ԕY=ɂB8O5$*+2Ѫfn T怵 ,}P9>`b>=98B_.v=tf8< @Q.YmFh7C!~ʀ[SMGlT4: ! ^㉡L:lFRHʻ t>K9,N8: :K#}T8#:|<  O*#ihrT*O? ޞ H9<MWinN (h.,Qɵ: pwCKY9YCm0 ,J;JD/}2 $+ղ?L.KS+cZ U=Cؔ'Ro:,\%re{V^eւq璯0t~-sn/c,͊Ҷɥ>83> ONNȦLhaqQLcq~Gx9G ~w4S.@Jcڊ^>ʌnCB@F}=b".ߟ+/[gG 띟Y=Pw J >J"&Z1(PpP{Ā̆*Ǚ19Amyj Y9gu8LW3o@ӏ(bzVҟLr#J\]p҅;)VS^ZY5׶ZgB<8k@Qʕ %zRN\?79GΨ xUɠ#JF{s(X{F{sRv ^ [g|$ޅSt>/w}o!;87rɪ5~i+Kd0m!kdYA:A:UkmyI8KuҊ),KQ6O\+@M5AϤǣ=[Cul<59G[Pq0{dqtAWĉ֜M=.q޳vXb<ʶJ9i(JEJvqN|\Kqpj@dT>z :y2 & u7ӑu#G; @IW?]r5P>x'Pܶ2QgC2A_^uʡ~_ FnK=v w㌋iF72e{1h"3OIb02F1> Q ~R +e/0ލe:8aIкzZ X^<|Y=.@RJeZ|Cy 'Y{hJ_ TJx8F9I|1 "Ny:+4TpN(V?,%X"fc+ > x/m]K*-;MX-?`\3+XMND'Cl_tA%x:zeTW.Ӫ81{)S`dKj73ohҩ'z'1S2F\LZA^ArExqa[ 07*-$Zy8[xcHLX_~WiT1} So'9>5Ƕ`;!U M۶%rRt@S8:Gk?'L24}y<9N`Crϖp{incc^G?{Sѿ:h保fW)Q} hCӏ1TH7 NnFqʘԸƃ{/`<(t#|ӡk,לyHf ^oTgIڳr=DꐸqP=RǝZ"~|w #HӍ͑_G&=ct kgd-k֋O[>UVc<_4p*g.*GS|A t1Qly&|)B\1J):*ob@9Sf@J@/8d[并Lm :X%oe>*1p) GNis62r55㫼N S(Ucs{@:J"TG's!wW,QWതQ,=ݼ. B<@gt&N]tn:Uhɻ8iAZ 7Ja,(CiBJvejf5Sks~=+9N 72ˏ7$`E9*9 `IOw\u=P\G>F#2 =<їtMHr=x&_ǑG 4'+dWJE> R/S~FGpm;ܩ^:J<#2Y50+̿zQ 7^곝Q[,Ս[pWf[1TJQ^0f=r#(M$>c،)''8#+5^xfWGjEP> e 8I:V>$xEhtgpeB\ >rB@28W.Y<<2r #u&#N`K>4K - SC i7Y"PJ Uc{ڎNGH FT뀯gY\S_ߵ >p7hԠϊ?nMS4S>R/Y.c/h2({ƾي'x 6p @Y𛬾 λmpǯ]a*K&jd/ʐ8tVM}\jyk2"IOZ}mo4ӵq򎄼ى ~A5 #4?eE6Hgѹ 8h(-ʎC৩ v~FG%n%  siԯΦQDxʰrGHtc'GsJBCxE& Q_p02t8DHsl:m6{ h@a NoFy0ޢjDm8`6LHZp1߱~~΋=nFwP<3~ G݀*7H;Fi$eHH()fʣНI+D.1*tU8 ʥaVndū[U#[qx^zL%lT7Y}0ӆQغS|IG4 J8Xe^^}MeM8,{eThUd%kzkw%@ˣWD5ߦsVv[Ap53=x)p8/dFq!/cNq}=i|htqV 1'F3\cO/ʫ'oҺ |4|+ ay=yoJ"9ڒ?xVr2:v[O|nOWy:*;@>2)O%y/(áŖXե\V֎E>oK;t`}=T $ġgV|t׃A`K(Gy0I:%U. a})(`VBxlBSqP#+)Cm̈́jN ז |J֭ej}SR(G3NoAQv bu[3~bT;E|mg!^&Xu~1Sh?tBo`?ƸrL|VdA^. `^|܁y>Y.wקZAI+? v qi k &UseiFSyʵrEM$ס{V~6Gt >  bo'07Sw zmڛc0Qg8_{3~\7xkߘ5UgPrr3?bWbW1#b2%"i-:mRS.Ff<ȕ*0k~Z7lPJ`GboTOq$ <DZW~8%B=@QI ?|MA%)cmܷiJuRYW}c(p􅄌8?oU*iX3?\g (jN&ьʴ f>elx*bm)ֱlLƴk@QPBog1qH ȳʕjъh =hu౷M=8nl!r+)up& |k9Q2{?~D Gvûm9(T;B{0*;#X:ȫSEjv]NF !4f^NFh; $ ̉yf@Q>uU)2.F07}(_&T QH-TPH~i@R̍V-֒M%ͪP/wǶ!=y)„'`;^. x{yyo@q*Sj_8hy>d[/xc3֖>w…3Ņ~,_lC~p< fi4hD? rEK፿ }*QVū:6/4Z1}f%\/){m\r w6kǵzYj .<!!O9SQOk41shZ}၎ٴ@nN)96i'N#I{ڏ @g/^ŧ.-S+."Tw'g~95x{ꁎdBS`D;,FUzSc+< )uM@IDATuH/|Nvc%o.+~6Cռ ·jk*!R= 44Q*-!8Rit49lFdA޻< Hi` ^;21f ܶY޹UM?(=oMƚk9;&P(UkIQ@w~~֯ ̑8ZdNu:'5j1Tac&cۘ"{N?wFqcN8_q30Tt?JCɌ +NClzqd+sx3ڮ%:kwKI CY WmvJRڌ8,mC'W0s)qXUf93m_?0S4Š([6-E]usDaEgeR#1199S0{4/ n|g8e,6nkʘ.ό :>Mlte~9];ْ<8F)V:{7p~-vlG|-= Г>d7蔜}{͵6d+{yCȇvpe+E )>I_P .g%%~ 47#  9 ׬~WN.s1xXoû<Ȯ5oᇄP ch`ŨP37 KaƝlLi똢G)e`C37'NIpá8cG{)i)ZViXeKr\4_Lnq =3fL,IJ` % gƘh[͚!2l^$(j;DBy-+Kŀ&c(Q?L]9%Z#@X Wϼ"UMԓ H]iQM*udж[Y`^eQCiN#3f4YvwFчt UjqC\>xQ`dF䇕BSN=^22L{A  8@:AXYsFꦾf#:),-]Is:^Koysڂ\oyZmpHÖVsyfh wMzt^Ƚ۹tbG7KN!o.#?w'yf eʁY`LQ*4֔xv1uhGs={͹E7tl 3š*?CCno?ix m!PlԖqxpyŇVx޽:yOSyQJļd|fY*&gE=AxjGKF@Q#?X{8%erMpe#0)W t9*'k[qLE۸g% 9o<ܞ?Mec۫̅c0dv<2 :Wɐ.K i[303g8pѡ{:mW UVL}9XPI>62`ۑ,ɮh =ӥAoθeK^#]lѝ93b?tq[csw?g|8523Fxޙ & {Ѝ0Ǔ*!ܒ'&W^JuiW% կb0P<@s y4x4O ufMh)%EE;@z3&-|}Μ(o& B@! +?J8qw!2ֵ(r![S FC?xt]_pFw%/WTW^/ FkV{E;GFI^^~Wd֟I>7MJi;^ dѴuPLQs8Ed)/6:gq܌hq3(J'k \} +Kѣw BN cڄOpբmV#!r+9᫅z Z$"J D!r Y{RD#Ʊ pJ~S1~p:fG)NLۮ_YN;Q 7ER=me; ;}C[\\~eݛ#-=xy(1;5#8x7 ivVd$[r0q!|c,3`<> WYHW8+6ށpa-jv+6"$9_L7#GA@&/4%=**{ij#.Y{=Mr&ZEɣ?6,F*[t ٜT?O5kaF*a3L&FmC2|iR 'am!u;Zc8R t&Jr0R^ Gu &޵h!yt ZPl\ҲhMr;~x4"10]_F0c6Zbiƾq9gqˡ<5xzi~B36͢FrP_Lkc;?sQ`ge}6ẹI-(CYQC+P{*.1ALJ+xIھꁞlNJ{ã)F9.`S:]up㜥CaQO78x +;:)(8jc#2{D31=аr$'8*`.zqmБ-Ozd*u\CP>If)ǘ~?o |jSNٌ)6wn_k,Fy@3,4Z~I'GY+qx13^[Gܷफ़dal-rC+'BϹ;4usFpzvjHTLU 8tۣrXI+`\H[Zvpu&]Ʋvxw|a[ga a#e /R]xwfu/S s$H_8O"ϸ`TBzx1[ym̴-iaL;!fAj"9;N(nLG1{Z-ü!x>ϝRz|AKRb {gO&dBv MO'ߖDm~ qxwA;[^V0-WG2#pA0.K9)t+VR<ГRh>|/~P:ت#wCY6Lt,Xt7Q;gd='nm"ncD "Y|T;=)L gԈqWݥw/T葭c?кi3(ޟ{9/7${}>>־jgҬQw  A}?Zڌ cArpGfC1~q ^>mbk_#ȓg89q ?ꅗ^8-23@utRb".x`xO~BGKjoȿ:wB%e4<þ+c9nm=c|Yi5{" t;r?NmHd1"vxFmk`CqOcΚ?a> mTs?1 LOj1HDYXgO+Qޜq#̎w<; j;5~1ANѱG|W07Y#V Q TAyP.)g'\ ,xb?'QH,]DF&L1vˢĉmcJE?Q#wp^pVT2WFnH޽,Yr$ib Rz#p^pћy~'K nv\P(\ I68]@P$nIqDdDf7fn'QL.?870G/U7#_/2#@¥gh8g /D.і:6$e~uv b(_3_,˗%E X*KP+s0x17YyFHXљ&@!&!ʋF}BjJ ࣡Ěln;o V7D!Oޜ]ēC &OྃAi`odn.yU+ &5=KJ0=G -Q|uW~g] <Ͻvd3 MjN)}(,1DɸAU v*y+qŌUK(=5$%cջCQ>[*Msgbo5>9X;k qO!-0+ tݛ fx)g Aa\tNX244| 79J"%5|ʂ2͜bHGH=.Vt1 `i.(gJ޳>OY0;-ĭUoH(ڄEȓ߼dH9eO{߯\Kϛ%٣iDyw$"hjqe?p$ gmGH/`G`Ba~EYBr-i(b;7_iv-ç/G S@qe ۳@rŔ䉞: #i>3&ԥ^/|W/&.Co1X#,b~\LU.1TU'EXkQB2]3(^ :;WgDTx]My2 ǂ{k2\άRPyҶ$Z<.'FA<>Ǝ M}z{2sRv2c˽rj\N?=8ek߲/+^k?cNª+ .B7'j(C]y9;8z8werWsHn;~Dd;r@OLjM3mH -G )o OtgqB[FNa$Yϵf*s^N G4d|~ՠ*AygA8iDiLTO%k_fɿmx''Ƒw6F4p-z2/#|L[Ӊ=ZytA׽~Z~7zgRXu\تq1]MCY>ݥi~H d l+Og)2ʀqÏ-Nb^ \qmw&e,`Y<-9?\)=pߎ`ꌹdoaij*qUxË M5 3 Tycbp a-AwH!S%4Ћbgd7 ~PbŽfD_u'z<>uRvдhP޳_|XV= }ޅ@6 pc"NYPC櫓O8*Bqy1鞔\3rҲS{sp9K@ʐJ3Yb ?׺.6flɾ!;5rd_x9@?=n OV7`2\"DjȤ$; >&=L'E):Řò.'(+t_ 9R7kꖨgk8ɧjpU BYx].XZ2nC} 7Cjhxbc;񂾣ޛwM7Yݝqߌ9N]gb rVxZCeW/y+ ?Q^ŷ'5Fd~orLg ?-_KtS^WB8b!c!q%"? )cdfbe8-z׉ًy~3΅F^=k~ ᝼Ir 23>NovkyWs}BqqQ9BIƮx,g ;]C{?Y(@ =x9`ϴ`hU6 @3A*׎1A Igd8N[!df3Eg ߒ_J\q'Cp25yG/d&Aj&3dq!0ޡ ag Mav܇]G'#paQ-1^[啁^b9؏Mڨp ]He՘4t%L1!U]>&-{.P\a *~z)FDZ:Z`Sp/N襐3G@[6^4X΄iVlu4 a/Fn;`nXKk4?psy)#'*J!8a; ҷm=Oz9N խiX,~5axx}b\_f6l*7O̗2@#P0'JAek7MS,궝LAɛ̈ƌ-CO#|sU ̀1E@ш8 ҡHK<^lijǎ k9Tg`Ok=Y xSel@iC;?5BIg ^'ܵfS|4*xs/9q(?.&D̰4mCgWz3(%>mVc%B襞1`y(w'9oP[ + k33(x]v->e@_OSwVp-vkKC+kڇ ÇQ|7R!hiN _5']7Au++3AWOopKV݀8Yԁ6*+SO IWawYYZ_Z%@٣qQ?GͰ`X۪=tffGj I9&+ģo!dL|g6TYk6ߍx<\5fǝU|apKJ2 5{݀Ca'tv9Gw3n&uMo$'4IO:x~W?mş~|dyvӤX>]ϸt͕VK㨎Σ ܕkciemUtpy`./܀x]հlu/6M7V FEK(poWWxcC2p8t7OL\aN{,L}kkgB!3 %hm,GJcXq`ow}c%i=&{jZ70ۦﵖQxˇS ?G3βnO/dOss9bvo+<=&ς>\*23Pg@(Ǎh9g+AfM `| 4fJ2G#%9Xa0KLaOVN />.bz.†y6yʀZ>ᛥV.7rQ^R\B{w*c!!dm@8-p- nuddu\>#2a1թ:!GM2L:TS~I]{OYk@`NlӶܓz;~òU\33 ˅HRr^)H3[IY2霏nMٴ;2xpNSYz/oဏ- aJa5s4pқh(= `d V^wSxjm̞Ge5ZZCbnNP>wI+nsq&t^N&߫1 ՘Jg-MC'iö,JSHBWҺ4<Ҫ|V^([/k4OՔR:sFNgqr=SW3d|j=-|O-'9}/ܛ][_)FFT^%:[a^O&hm3u󿹼p]k4ҊWgҸ ƨaɧbh<#lNΟϝי=#L>[au;/]a߸T/KzN6` 04KD~SP3b0p; V.${^)/zd7} bGIh=>1Y @yp—˻@2[t'ڙA6uA#P+2А~<o suzyf$!+*T>|`&j6Mn++ҧgF&5h`f1b.IG/#㍗{ƌ+x.6xS]ۘU~A>)/MXSp s"8&̧D's/z~( +miy!9;h嘩7 A4킑oGC;'w 8dp?msjso [NRt$i!иJg{E>XL/,Z% +Ѓ}9ז>Aje /SRzi΁_Gj>XP.WA re }rK‘Zy6Hs#p{c8=$[6TO;"P˙<*,ۇ- ?+L|R:!G+_yJs+Iq=?OV-V퓶DW nEgKM&9hGib|_=83{:ͻyzMv<+ a7ߛ*1tH,KcltYy &NJx .`!+%9" E^8#"t` r?;:9!ÏoHPEPy41q!5&tpPG63.%wt+i[p#?V wPV wq mЌ.(#k Y8û<#^C!gH .^;a/pV߶eHm R6Z;I>ɹ "J&Fl}X԰P,2̃ņy4w0~Aq!pVt +VB{'Ь 4xc NXl21dX2R|Z܏.Sxq5wOvBN->{+F9g|?sAfÙqdt6l8JOR_֢0ߌiy& sF31חDWOۛ5$1H_5~mue}Gw\~POg.s%?󠫕iB7E _U{vVx aɊ)8r%xd 3΀kR|%_[rHcܜ4b۫]pK=U1hbYcS:]{@GGwA0U[D{Ўq\[a:Z9'PV?P mi@zZdcu> mҏ~r|2 o$M`f:.o_-s!DQ׉r5i, e]xC!^^R6経! +PDrou>/e~wOq!~:58tj J12b"f%^.G0ԡ<9Aw.BXj{.]BU ڂTM6{9K#0B{0D8n T${'.tV6a0ї\BR^C %pY #Ƞ@]<J-*ӑ7) o]hRb`,uN` n,O}:7+̂abe2FνLq͒I|jfdb@/eE1ܴ}hh[0ܢ=g!1x~DZڭu?Ί{,eiOhpJ_Ol leOh! }ߔc\/oKSϣ=x8AwGz6$o OiqdO3_K,eɳځt!TAd6]kyHsڮ7nr mp"/AGW/9+~DgC@u]o 9>[x 8zhiO9=Wc.z{=QGt6X 氪FS:K6t^^E6^[K>N(3 e0 r:IvSgJˠ0)eYtv$%P:S{CXU(trM<1Ϧo-=yl `P$!k~lgVzi =1~'Y=Iʻ ňdXfF?OdA6GmykkI/~.ǝSEN~Xxsz<E[u8|,U`,}c?|~zu44:^{uA)Ӓ]{VP2l| .iNPӴg6_5+_´o=x8@}?xe&ϑ xN#o$ 𶳾`QlpSjd^g p 60 hJ0 J )ېZ<~$50}y7 %v6KOV4k<[˟+ZACtFlZMg6_U''^ʧI#JI>%Mwl/W8xOB.!zwh`ZP]anޣp!GOr:((ǝV9ɟ=IJY[5ؐaaKEֶ<#z#6m'hEn`gp۠5=i`kGl]㣷 EAZ@;a |> 0(̹ %:2+=v1%|t]+mPLڵKfY&$n[p8]hz7kh>YɔĽ>dM}(qݶ(M=,TXO=4TN|eW >EyۡeJ-d^oAIE@?N5giQ[-!ce5[I3B;ZN_u+8j}0$N.Cu}*g9[6ڥcL%[mo̽/Ɓe <ݖƜnN(vx}!tړ̎ dܙ8cd[3?'>y^ r,JUp)(q>ܞh iKM+fyeighj8,Gav<>O))_RV\Eh3銷Jr3JQ[41y376g{ue@vgKyO}=}Qn`{XG/@ aK΁O@`~7Z$F/YEcKTWp(2t;[ڳF,W/pvrdU(,Ζ̌W"6ѭI`(ƿEGǮye<Ւ]էR75*Z%})qSxhS~PΡޟ;|ݸ0p,ǷcTHory&Ovة{ŠMe`tXtZ+arc;1Z/ZhtieN\^.r _ M0ଭ0o? X Lq£;Oэ֧ty2Y,?%TEf!B`1튇|>p'vo厖xr17zTn :4&5ҢS?AaTԱߤ4+Q2X~ ``E=@ Y%8@Vxe!gxV6N}xvc,2Ƹʼn.N)fvrs*9&5ss),bXv]!e'Ɠl@O{gkD뉍$?})ZI}:˯ˈSa'>OÎ3*꾾zNW|uQV:ΖsEӄ{Ǵ1Ƚ>I)"F4~.38n2Nߵ–N.ο'23@ȵ-@,W-`O;_:u zh%j/'~$:8H_UR02˷ 9xIr{G~[4W|:Mzqq69$ф`ІruLItvY9:kH='3Cp ߌ?cד{{}3^1<Ź:FdfҖX4=VJ%cV~atROyBu^/qkpϬ>ڴ,4@; )Nhy[8RK5\RIXyw02Hi^H2ݔw"%0AoY͙UB,ݳq)|V/=>=P8cOCFq-kŃu+0)Y5w`u1z 2ɾX~J>st\gP'e[c–|^uӥY-١oӏeO!#e2Y U;!ΈNdr|W[Zʽ/oN8\T @7J#fyn0e)Ms+x|OJArԿxVz* >@ǠiDoQۀ}1BG@yDy"2=1X ƟWvf|:A4/HRwuvxؐ'̵0LKɁeU#qA6$S ڡ- kPČpBZ1zQ2I.'//pZ!(;zƠ9}Q JXb8mQQpTp5aY?)whcEzxK<>7U[}r6T3Ͳ9;Y.I }gy#r}ʦHdž_Mr;^"pLOr;<L~4z~E+R1 +q)/x@ 9l29W|vp98YgS ;HNZy7-_%Ȑ|4qŻsXK| z󜾁t:a%Afj߄h+ew^_҆g[97ndj봟!rs( D9t!t:A}8OS :NHP7q5goNOLcXİޖB$}vȁ F+o8̆>+5{˹;WOq.BY~`p2:Jq< ?He+F$ n0PJ| kХ/^eIG|w3(p?}a,6 J~b[&93js/b6dtI"}e"ԭN4l 07S9R]j xu]Ϧ;~(' aOB",)/F7+|w`~0xxB_^Vh.VMƊ1>s?񽅳Z$U{Y;^ 6>.^sGexD X3y 3(CwcQL_ fI¥ x%C҆^h`,6qε7M[^ dx;GE %K FK4/}˨'~Js|`l8m!ȇTׯÑG8V3Z%9J"aŠ[ @袗ӺF$xD'%Ddha cc٣,+H6m y1BƔv/k~,HG4\ru~*WG/4eÏ_9xgog7nLZˑw;Sn6cNfIqFlem^]i"W?dXdΪ:Qx^`l4&(U_J="ۿ銊y}a:;mںze s;1ftNJ\zݴsz.s4V?Ѧ& _g/9,Fw<``?RU:CB'%:|6vo ϣ6KԊ+xw hF}kfXl\Hj:1OOүܒѼr/!Q9rf| @s%2t̸=lu߇so{ K:3{*F¡6)r4k sɿ7–UWaE*O5Z >KwV;XcWit 4/]we58#Ő=\ mЃ1I¨bXVoƆ8 k2,_|:1m8' m왺qK00>77971˹-L P qי?R = {ex-Y94ڼɨv;Zס%>0&3V2=194ǃ4΁;́Qs~{3Ǜͥ1=-D^QdS*D?yyu`Vd++ V+ѓ9b2zt&6!Ë9pr8;Wwq꼒Ե+ͺ;4S|Q+Y#Jo;ٜֈImc7m!~y'm5!Oҟb7a/n A3Lo/056laףkӵO#Ea[A/ٴk gDBKtjo3J iÂ?h_&x6%__j[<hG7}yA6)SeXG'=XvrٟC8qPsÞ ߘfÛL)@ƃ6$nda+dU8qrpgvO:zs*p[y_|~mqJkN:;{&s$ڪ8i@A8tNM 3{P2~@Otƚ˛ 4!Ǩy׆o?Mfk =L,`%mҙuL0u7N1> (x0]-]W~3peY4΁g 2 Ivf_A#2y٥̺\3 omHXڨJޑ!E4hLNb!6񝑘}i#>U|nii%x9paprAMg.ws<q>A-]K sBϹ˻l/' tlkm&^箇P?IP/+|/2@)b[TsA? Q!6EK%5r dW |:x{ȋ"uP ŭs{VASxp Ĕr21W %F: *?ԗuw&ݣ-+!V(6Myfϛ"-VU~[BSI ]CGri[<)Ugyo3nzva2ҟZ 4eo|CqH/jJCxݬ~ V5`!ߴQx07K΂ʖ Pʀ)rsnsG9~? Q}e:?,@(P:3#E.\c䌄dqjES"G&ėIlCa<1SO̙q/] grum;!]73θn[3ƅgfi":AʗnuCCx2/W6:K5X~:]y材:`x"1[@Yl_tpZ8Y<:pT֣JMƑvG*EN]%6[.F^ gtRs5wDm8߾{k\P> :3@8ÙuR! ?akGk?iVݦ~\_Sf9ܿ5QAߦ@4~QߦC~6JysN7@Fbyq3@͈t,T*N~;]k-1x@C 9_5Qa3G83@ \N }d?p_b`/J$y\c4-X0t`arߎ}6G%-WoS۱5us wƙg`TzV VdXf0h{R9,@C~Z(6d.UiKW\XKY`"矺3dKSysϛegA] )Sa1hfa5357:6wDMlJĕV2 b 9\9ƽޔ=r[%maC2bzB.;oiV9ٺXV_.5}qmp.ГAy0>~i˹{rIPɇj94봇/`6+EjI!5g[ډ#rb0Pm'i'qPt1\K)Kxdz;7fB4\s6'܅spH<_<փ:a#;q*#/uu'w\^iaԆIr G}>ys1{ nbo4ݍo}F]jLի)ui0ױmr +Ρv0gC" qğNuߣD'׬ڃTy?ХFՄsSo%TMW'h`|9ޓf"lBx%;GetVPL(H8ks[p1M5˝w;#'RK8ޚFnk.DW1S';S{ی 7m8<3Lc=&fW?S/G5DP<ۖyx-7u\P:ޝɵYUcWWhU( jKYo(.V)1HK>98__' sns`VQf41jsZw 2JHdrLJ`*y~䳲KkS"tR#;bW#-[{VNG/㽉->.:|v: Ӵf|^u~FC' gC}W~FDGHU˪G&{gc _988bQv+<{e?٘ow3PN a4@DEu`ItB艙#V#IW<܏q4'5(xt1zPfA`Cčgh"LO,iJFaC )95uz}K()-G<w)bwc`09jٰ#s|{cϒ˚7`G6:-O+UfVOau_wC #jـyX&y=QR@PSe FL"=|XJ4)7&eY_=]p@IK],O VhIF:mm}^q'zS=3=$E=)Na@O>edK KoI3~ـщZ\34fzeQ|V5>pQ{j;>`/磏l|7le0d. yVH:A,3H꾚/VLYʈc9xۺ4^aU,YD &Vq13 D&˅契1(/OT!c_ }xb'kE)OI@Oj:{9p8@nL}3D 7bdU3lKJ` ,Sd:;j p,~ AfP&_0h.{eefŠ`QҟݦqYė%GJ-h1lst%jj?Z!6t7rU<3bɤCy,j[֮9{z:M3NG&p+<_?m`t*9+3p*c~I`a@t !I*/}rIC=]ʭNe1rx&Y_b<<&W >oq/Ny[6=H]frfߝX#m_Ve cd5DG]uw =V:) -xFt@J^xA15W;ē/Gc[q~,_d*n5n#~,9l"4&@/cih[n!(Ƒhtq -yN"3v, yyAxX@gA6 -/s%׉< .G.bUzV)cu5PS;҆|{9p8j. }KiԐE\%rJ"יȒWk6 l:r |.<ऐKe&93 N9V P0:C1(i^]t^TzF2*y9 W<&2`<q~G_9)gN%t -: ɻ]=t Y $K==O;&~COG +ARu۶M'FedP5)}VwrԼkWɴsdWZ/LxkuqA 4^[a`OX6+%?3<ltx_zCM@'\Z>0e[!7ֻb'lt%yvQkUg|NSr'6~#gmlz?ǪYo;0@~vdJ~t->3=J MZ£x^?Fw_3KYkC^ۓ^Z'T6Hݥ6b?0އ&eML##nR!Unir"҂g"@]Ux&WJcCDŽyW1;Ss- p'9pߏ'R༊c13Z l.fPVP ©Orz$ۍ!Z޹XAQhtK. L@,?g_ިt.HhԷK*42``ZvpVvIH~ܳt+ӧ_9rHw_Z`t, ;Htт͔ߞ9%Y ؛TOsVY0ty=Ⱎ_\YxJXElI]Q`ًk{cs8<[S9)={ސK6 v|q֮ )W.(WB[- x9KՉ>3, (*-}( vd36?\14`>@,}W̃|){dK).9. H0=9 g}"owX{F_1N3+z z`-B&ӁQHmu~T/Xj~ȷz}u:m0"_z=4  Cg$XU@,ٔ P tೂө3YM,QOӲ@!@ͱبE[ř\~3>փ[!xgӫK U`_*~'#=oLO%'Td [^d2_9 XSje?`" ?alH `0\KvahNޤ_~ ɠ=0Fn.{u2x.uf/n6rSgy}HNhv*@у1hMXIQGɅj-Oi^wQwj)Mj92΁Ł>~RkHZ-؂ude')%(3!0=IBu'F0ct\>őğg.r.m6_nNH0dn ~>Yep?40Nc7y6g(!ػcs d}&˦:73~VShҤM . c1h~1Y&9|\^+l/a:eXStϧ3g``]*\ Pg9dOwPGN<{:q(xaJFtH­C_4Hû'HJ%yEYT>`&T4Fst}ia9=7I8 Co)>іy98 1Haf1pu9pO E}QUrlz$dā)=’krJ"+>Sh0`S3X3x! `k9((|+ ВkoV~*};t{kn&Q!t} u4so6 /4zPo⫸ڃ^"rƵ;S<4 ^=nq j^y.c-lIHZ-}ԡԋٳ( j|Ҭik6i9r> 7o*6xx]9l?}i~;Z=7Me$훣L +tWvaL3oǃpKh<װзV>>alFP?lJ+Q&\~&.&OVf܃dV^Fgnb0|z'nq.lۖ<6n4?ѓ0DX_FЩ4Hz]ljhˠ5em}N%|me@~RzFy9_ـXvn`*E=I@:JguW 8J>x`;?ܠKl\CHgZc^,!<&G_Lݫzy=[)Yd.dIgQ\d~;B>O(f ]〤6Bl6 }#꾕+`!&V[}^-A9Eˊbtq;V5Z\V{@\c6g[SNj4ҧT6keG'HKGdOYuΆ5:SZgˌs3L|:-{ F,4֬Q_lH; xkz^j[=Y>`/J >;ɯ~d&&.u4EMIIKٻeȏrګ3Qd͓'7~r;n'+[,Z!(`?`7zUfITmU_亃n0ʘ"ς喰.`H+\#k`|JGJ?_SYSI%PBɰhu?ݯOZgzQi<0-Ge;<ȻEKd:KM-D 8@8A0*bFI/F:Y1>K=Ժ/6G2Zvgx'Jʹ@8Lxf1` dEPjOUH'/WҟlHQolj;FGoM2l<> }[#5FNL20"&8mG'D.#wj\1\U0ӠQC.A 2yFzw,e#8bFMqL`C]Z-xײ!qO4z<.|4pT87Hp//[[Ꜳ2'sepr.n+P!аB1>?0IG&^ J. iR! 9ˎEG4@(w%X{րPnᬠRc4C# p8ʔk!]X1, "1tRrz>-F cF +׵Vh\"8ͩ%soGL;ZVp-Ogv&aza_Y>S!#l͟/.=_=ey[M rH·OΌ̲+N) d(\% 낸Šɑf_g 8 IJOw|ޥW0esY^xP/؏1:C:ca1+M _Y~v 

U\9ȇJaU'Dsd-:{r+z6NutM FinnHL:"hUfLh%֫˻nu`/޼9fvrqvpY=} +xx@{gI ;8ҵj'Bh_PF?\O+G# `9᫻ٻ%[$Mwpc9TU-R%|z|qId2H.?~ljs"N@ pws3555_f:l4}ο]\OnA :4wTP*iޫ5buC\S4\g8 RqQ^ZƇF;"iWR@tɇ[Do%=Vӭ?ځt裝kݸJj`ƤcXzk\+{[y\mP_8gq [8[62Ep(q aP7SgNm~y37\/Ho1.{g'yy39ǒK H's/)n(*EYx2&>35Qn0Ga*f(Ū(WVn˅8^} 8q" ؛OdGՁ9-2H@Ph{I*0Y e  kG˒xZG x.OS.p=|o\"]zw~3ү&֗ cl!z}td۳]u|0cA0AݐXđ8 Y^*$p> u:zdG69 _'筶REvg,/?;L# PYd눃g`u|;?I$R#aJJه۾֐FP"+y!f [{Y(oqՕ}9q) ~Xw|y}qF&KI5};oʏz8oo'ejfgg7v 3tLZ8 s Z^Qһ/pq y]M`a&UzF^_:ҪHo ĂK[ΥGa>,θ&@}8;MA{j30=k|F\@6+$a!&(ArACW:<^ < 68,U<9O=72CiD}89y`)(ś3J482B&V=9$1@5)n(ȑ)&ꛥZQ ;LsDI/PV[_oO,eRz(yuMFLJ2<5mY%ȕ4??歽2Dž"Xt"!Zie{0Z'  gM]OqG]nHG+`qԫ9IPu}Y,>O^,ybMϓǡc 9|_V*z̖]rI[= ӁN$yrԉz뫂2xŝ8*heP,8 ^f#]U#8Q_xhK _yϔD{tOC*AzRmC}> h!ywLP'nMvX0 MվIk}&&8}&ĝ$cJM>m|ή77xG/}8GoYo;tz0d輣:??GЩ)YA V3`6D^3w $/^kIeݡ%XeGڮ[A`pmngUBiQlwl̕ugl,qi@] /=  *M%/n_c7\(1,ꐿ _4 nm.P^ENPTt:rfX剪n#bZ"V><!Ko^U:>'gf⡎ejS. |f3w0voؒ\.)$#HbR^ɘCXޙ C? ߚI<ZWVҵ]wmIKns{P٩EVtcFsٝ m DBl85UF%vg:4t#Oiقlqxgz牽=]@>?IGl?d)dˣSy=;r`Gw]Eg&LthCa_^0Gؚ ,]):mi) E6d#+2$CfS`uq ]pϝX dTv&/{ʺNEH঩$5gC|kk/V-U%ߊpSs<2JG0l9(Q~:F' PٟRoah Jv:" iU6')a+̃U*yA\:@uNS\,Ϝ fp _Li˂re)|ޡ~~|wyC|W~9g)$J@~y(^2&mAMO75\|ԀӭtV}ONDkJVN H2N59\Y&Xg2ŏ#xE~8_;)ӆd+V{-@Q?e{.h17 x`25ѡnl׭ _ 3\>.Z{2R؛>3h=nejNe6SC8U\jAY#=6V"$6:SfR=5-hhJs(^P~r_|7_to(6Mx2R>~L ͢~уקsap_|hP즁5&aZ1.zR.rL[Ε-wp#z(=\18T3l{cryo@P FhW`h >yZ\pESinr_cRͩՉ_Qrn 0`^9&`Vgq?6fu L+j׼i  yM] uj[Δ[$/yRo -ʽe[rֆ_;#XB1,_NƮL /䍆+K>z8$6TK>f=uuuߨqmP/Gp TԯG[WǓzi;XLU(SY~&~W }6G%D.| sϘ^Np ?,k`gb6H9X|*Y3LeG Ax˰r츆`}45/Z Edhf ;tчda72 8D(=,w&詎?q0B7\x|0ߘcw(֠h4wU:%/(E2QVԫ/TjU+[}xv ς~{c%Ҳ  I'u]s[g3I I+` #Pʻkj·O%H_6) 崬 px|?v'< գ92.~+dމ 9[5Ѫkݞ.Gҧc24Y%8=׃"":=堘4ӯsT\狟XZqXt7暢:NBUձ%3 tE m^:e{O#MW vs,oU@˛'/Vqtr\oږtz2Mc3َTu ' QvOXA#%I/L)$#J7NRG?o;ft:Nt"%Dz,ιՄhyرcDB/fKktp[%$_la0Fql:S_CxtO8-NO**}lo剱 &{vLɉ"=wMeFαQmB>䕱'WP۷)mx?ZdՆs7['wL{F FJ0@ݗp\h[ yIC_ ^P~~nk[)%_:}oj]xmL_X/D9MsVʉA!k熎gՍ;xds-1*q2W:KȾKw[Z9-ϵ\Vtߙ>ŠwW: +zȲep[6Vep T -^Uʭ=cW{R'W9Ze5Tlԧ5pM>u?OrHy[ϹTy7G?~:9 4Tw]=eKۇw-F;8 hGfoOAz&?Q]E$ X@h>&ޝhxh|pt,%y[aj{=[Xˤ/WjҧoU  nY: .e~{ga /rrX@ ڌὸ'{7 =6$Lڕ`I&qjnU6^_Yy(.gT"M0|l\jS662ju.^R1 iHQ`IH S3.`su+E9B7bS+ֹG tX%3b FHxgۿ:?i mZNS$)d@>+Yѿ3stzR9?Nt?R kXJh~4DR54 uxQdWrɉ"gdRbuToQͅ/l|}x +q#/f4ҫSDq;YUGe4Ĺv[~ɤ}W͛g.)Iur:R0R S8Iǔ)5So6C?էWO_ᘎ+^d2z/5,{{MD05s#5Kmw|4t\[2v)FiyK `C&!ˮAIXik$=[ uljdߋ?Z;ՏAkrCnڛŐCU~|x4ʙ艙0+ 쪺O˻{t{V{8߮RAmG>hbޙ[v{d:!lгTGLsJ(1b3cɝ;Ni`m @Q'ƙ‘M!'P+suWHj-@pQN^Q2HI/4i7wv>^L1Lɠ(2ty%L:9 ~h> $*op_jڀ1} M|^7ȱ*YPW<0嚌yugΝu e>\z[iO:Z Cyqr6F=7q*cSi_Usáb`=_VgrT/2nOPf97N+}^Cj+:V՞KNoڂlwƇ63aP98gα\f/r[/M, ^(;LhrPO~GI!οߎTI>`{W_7I^A|t -ew?z 3= D2dܯ57BwA=R[t:v,a;+^3y4hd®8 `h(^D)E~ť+= \Ǘ~&@f镃\CM{|;h|_~6bsf SY:R|V}N$4_| T_ȅ/F_iy8.WgF'z fIa #OI9(mgWf;)u T1ɗ\uX{,AL5<^B>GQ}YS6692h҃c*ǮG0QX]~3w\ rZyIгd긽F<~8rʗ1]x[ڗbGS2/kc7~ ua`Z'F 2!z򬞑E'J<#9qdWq-(s7H.jh:m9qf^0'q3J S8IEJ n}IW7'>,}ʟ>݉$ vz8G{сSֺG˹:\\ D+q[}x)#:NKW:+ZDN{@[}4'}?ܢy15`É7 -RE,ĿA5:g,c{.OYFjkڞىIx>gWuYeU:Pʯ ;n]Mn_)emR|mJ ::᭥j~b?Pi9q`2 (HDA M+VܪZBCtUCMHѕ><#IE&-g!6P]ΌO-埪쥲}ì+Ú8T^W/.Z[Y^cs%^Ƒef>pAY|q1$慆g9g);n\OY=. bmԉ R85^ x䪣ṶR;zO/DOINx&0e0j&l)zR:aׄps=ˑ']2YViCѸ9NZJ=ܳ5 t}0 NFYg8#K{.Zc`k4P 0U>Ʉ2y9q=8{s>z++Amv`vWUǷYUjBga,ËsR \G>np5*dcj֤ċⱧ6'FZoeȣ6p񐬚O^[p[J{lP]0+|KۙojƳ5>fW'Y|G"-6ςzjkmzǹ9 #Ӹ;>o{k`G|!(cv:CL`֙}16;RK8O4)d{~[zsM U@)`'0rlMŐ,04Fڧ-G$0_Wo?fF2 J-):]:0CjВ=:PE0G.%¨u";-,hmysh äq[YR_RuJjYR8x>]ּA'ESXʶO{fxM />{-;`t8WbyQi^f =ΑI|'5RɔkJ DCa3XsH8p/q} <&Vw>ërآ:E'Gܻ,(KH{&|3:(OIۤI6t 7[zk>Wޙ*]jpPJgU# _ m#pkB2y}6m-4rZݴ|qip4fExvPѹ .f'J_8/`圇| sӌ ij{;;suݻiP_rA;ƣ؃\]m}f?eb v#³F7yCyG^Jt~9n͙vcOŧd9k_Mӝ!u\+B?jɋrl2J"oj q V۳u)>ӕ/.,+2`ںm6|՜)$\$pF |mG| 毺נvk9DŽOrT!zˋƤdIJʡVW4([yeh<f#pf FgB#P[\ 8-\ 22qO*MB<ʱs ԍkyȽ+- l%6L"/!J-gr줂2i1YqJ_iuR|R$?m;~b+ۛߟXLt LRYj9aZ;lWݧIO~Պ}0iY=);2 &O&ƪ=,`^s{JC}]!ݒSGMr >d0S u%ngԭ׉ nޔVzЀk?CX|-'v yUI"}GWH{fCvxjl]!{-Gg[O4X;ߎ#׀՜Un~ ?H@Wp.ߌ Ds$,&8P~o:iz۴nLJ֟EOew~`Ѵ ;mSL`Vq?d@j1fv8sM`.ch4l8DugQ w4Dt9)8S3IW⣃MyabŸ 6Bĺhs"䉑ީs?*HiRcee ı3R ݕBr_GMP GRɇ~b^9WYUy״[%UNU#8'rx@ꐭ%Z*AnWijEd#mU;Zyg xp8YZ]C4/?F> ROcL&N$$٥Ȧ]a꫃6T4E$d>cHI2rΐD5+JߺKv:RDmz.:b'VO=Z&ߎGڎ}!V^8fmAy§}U)],-[~Ly>&1.[=qm|.P^/+Ⱥ8Ӿr֮.Eo]!͹8fO__栍GHM^!V<'~:HWt<B]~ _KO\t ̪]q~ф `:ʈByJm|6zoVG;5git'ٍ~Ǝmc^@6x`ׁ4Nԥr = /G։ ?PpP.W2 ]:> TNt2e!U5:҈GC:|Q8g 75YP"3FRw`484OfU98o6{?JFvpbvpP&%]`b#7 ÇG)A:Cr q3qywm.Z9*f"ԛhx\ =+[@ oLpj6W@'mO+aaE,wd[;5m٫8;=?Ii%p1ORm5@4~O,DMQT&&- }ޯ{6% 8']1jj߈W.C7Ȃo;z $ ' 3F]82qeo˝A4q#'q%29od]#JYX1Hw m,PcI#&+yl><[f=_]+- bOL/tFڅ= ߎb|h~:[S窣2Yz~0vE3f($ۀO-%+7j`-QYPAːՖ S<%pm?.8K7Yϐ_rP7fi|:8(v9R?/Ǽ_QL]fXbpaHB̿b^97 G7-3TtDJq_ל_qDQцVCbI8m}%൉Q6~6PG)AX NH^qْ}h ==NxH!hp9=_]zy$=BY3L4.7<^5t运[vYidO-Aoz9׶#PñI]q]]x:PzM+2 - /(؃d#]U_-ϴw/(O"#-Zip}Թ2AM[_%^-L*u$ۢ 6r@a%Α|9|7j~g"߭?|-ooLVu [QlrElQ_9yq}oC;6ԡ%-&+,D[Du(oP8 "323qX=i s*) qd讟c0Ee9W1s_.ߍ@r:L/5[dU:|p#&F~Nd* A>q8~dRNxXvhLqHqD^_+(r+ maqr[mDeiW'fHz}i@zZ B?؞׵vʃjZBn6T^ⴛzhm}".vw=q(h[: o{qWjm+r?Ii%#g8w&_N~4=չnuDOәT)ku>Y ƚ̤m[̣މvN6Ҝ /5}#J>ٶ |f7𚔏 |Iis'N0!! ~a>g9k_h@]^lӹ6U<%[m/PT_e`/:#!6[>"ɘL$?*nyyi9wxߟ֮c4m cg-<en{/ꤺO+nUʠÖZҼυGI 3*y-8/ɵ\,uQ?Q.O:W0`Gasy,)$%|\)o<)VGYܽwhe{:̎f,,KAp ;6Cn^>(xbCOz $IX@+/+'4]EbGb G;[e9g)6#̽ ?Lke9S9H"[6󜺢V—.IZk[7b{hO}KVҫOm56a͵s@la-$pmo,w0Kم?з`-x<T'3 iXHn7}ЙVX뿳L7k^9_LRgśAP6堎.Z`[(&=1TY<ޭQ{/rfOsۉtR>C*P~yVݮ`޷SLvC Q2Knq{vdξ7_R:eW&vU-<8|88=Ld8|!ٱˤO&,ADiYL(!<ǦgbQ>LD7;Rc;8:twNۘK ČK׋BK\Ud?Ѷ=]~Jq(8SfRGBw0Oq)3w^J /;9?MN[ڗ6ݱ$GFpoɁS8IyJ2>*q9@xݫ9}:8ْ2闛Iv8>' L4o5SgP\ Rs-q-P4:Ni'! |{_2 vvn_}eoBG8KJ`.ul4᷺K+}q:oԮ%6(Cgr4QP>I}SGˎfng&)i w|rh?0gm]%S8IHn5\5U{2NWirIvA޽e 3Eox`f.65uxD9gv57N;'[?'(U9&n.4|l|Pg=>&%~v$P#`ILVg} o@jd-oJmIv?dr KmggvĈIGstꅆg0nf3e%%`Vl.اϜ)jXg91(6U `nRR$!2-?hx.on(*rrڅ3J>x2Y/ q# SVKOo_q`Vhhިi%W+5M?ze{~'p:)M i/+ס!?ݖ,-h&gyX^ Y ΓUe[s%~{&&ʑ^sh8B/f??[~8ONV86Gm>V]ñt9v&ʋX2(鏡W\^[N r8&~:Hch]'vh]؞2n9tRL!CMud:vgt׾J;&v>{{Rj+Xps4OlI@=Ӵt\яmJ ]t p#zG}*XDw2yGvB/sqE>&݄&]xeP/lC]1~n86き1T^Yp HK,h# Y-ݠ_&6:g_?!;6uK҅Zzpʔ7GM+ nM}6v>W>ިv5WԊAaƯuKU@>!*-(\p.υݺ{x&QׇUpz1J @EzQ]CRTgUҧd`sU떹/L9v6`S5R:uNxj ܻ?Ow0k49ޘY3)e$>{?s(s ?_YRf3#Bu@.:<LX:qN%ۢ&j3vpn"+PYu#Aǵj;%N Ws|<ǥ|=O<\SS:..NT'xG B'. 0me0OJ($DECt+ڄCr8fo8d~dЩ1#ˎʪ̎ىv&{_ Yh3tn ?)$$,)T ;;x:/NW^u;d|t0@v4 opTiq]5Y۲A9t^3>5}w(ycʇt3>½Jx"oPS/e1s8fr%&Oa"9r5v@B}ټ>BǞ[6BSsCoC{Up qDnlSE~aTos0[:w@99KqPe7#oKwr^oC1C9 82TꒁqsCJqC!)yPĕ杨ks݊7+r28䒶lMzkgY#!Ymr)D%2} w~40dr0fJ! XY.7زu̅,Z5tQfnJe%=3B-dWGH|ɅZ8zřY*Lzn"UnK4ST['NuYt,_omd$yksmy4?r5qdKdǍ5/eD> r8ᧉ3Q [X3i[Jg<%$$2-}ڇҗbm/ GrDe3W`ﵓI>@mv z[i$?_VI7eGn&~8kNx/F0b)dm>} cq( R3m0.Z6YBkח{q|} (36c?+ ZI7VzYu硴4WBxFrvIYpn&A6|MhiZG!aے<67d9ʭ 4_F%h5wfCq ֭LMqz8 ħ >~koeG,xL0@,"޻tqZM(ke w K(YLq*']g&@c&7`i{o[FyU$?rդV{[8m%@޽5;s&tg S)IOE:@'^1cw˿ %5HjK M7HS)Hck~sLS>AK/Gelҷ wi\SipM Hzo1RN&ŹW.?՟˯kGy#hJZh^3;qflCqv}﫱 ͕ [pfxLAl>d&m=P~T>oP3iL1ZxnvBG}GF[Y(sif/N\{ v['k+d`Nt&SʫjȾ^SoH ^}-t}a }mk,@7J e[gar+pv$q8,joscr0-WF{s{.+l`028 ثHMk:(_Zp=,mf2Y{sYM:q>QFy}?w}%M1ڄ#IK&vd2vQ#bQ~>>eoGwϺ %||?ZjըDTzAcB;n> klˏCjwi)g7HQln8_n #]h=u[m]3Jkyr6xu3_lUK|״uzH}^M;َ s0L  )9-ΕyNp:Sdض,oO 4}'Nw'HUX-@L)$l$nz7}/0?8=Uw}%:_1g?)ӵQ\T])0MtUXRf8l93ޭ+VZ\E﹈0sܫk2u/|k+y=Bl'y=M{.NZ|rpy4)>-'GB~ 2qKǡW'ʫ^2NK:u_=@fV9t ,fE:E$pm hv&Uz0o̝0$0- vMXL'sfS}K:o?L2yYI??ktv?YlTYW5^::&5"Ocg҂f ncmۧ.ٓ?{3˛nxeЖCoG))o^iuJx"M7)+*ǩBSsTK( F27\@9?tB'l_/sg!CX1ʉRMMw)`EGQ8^S<׳*:HOgIx_q$9we9ȚLWq'BC;q8Yu\޷NxJ1K`ȣeê/G>>zkBfXll+;)ektӊ[ e CDЍ!:'B|<0kRK%ACű9$>|FvҮ P*{|!%Ζxy9-,6i/) yY8j|mh%/OкMŭ7akGi8%*\M sCƦi+~0mz{vX?>Ҥ}SG9ΧoCq&"vZ{}06U Cr;?4= 6E{Ck$ިEXWoKg7e2 K'J)&챉_|ȢEv;}G] jx9xv"W\g~](kxwءKV.mfe7V/ i)/Rfe_r+Z7L徝N ٧k>%ɗBzZ`\j)}svC"u\)m@a(uyS>*!幏 @i]R ,%Gyh} lS +OZJ{V1=u>Ph7=buY 0W Dȓ  om@E'cW2CK}g7~%x:IH`CU24گOh_[Vc &gQ8 ^pSڰ}o.7y=.H'3qkڭBbDGmќ_N}xzڽDTC;1wnO4}`I/jsFgnRξ<ѕ@hiUr>fLWW]쨚F hd=t GpK`u~b# *ʄMŭBtL<L5mYN[œįXJV젂>"?@%J`O]7'.*mf|= Q8TJtvP8$ߖ|wYsn8tpsVV2rtz$i킐:1)mLs+t[nk_+ FZ 1(-›Z "gqZR_%¿ROQ2QKL#vjQe?N'W͡22\{եߧFo`R"sЅASsZ\1>@2U B%谫JtU'&~2I|ɥ38% k&m5l$*HΟWja-rŷKoe bޟT>hqW sZD" DxaWOjCYiOva"%r XyrVy}8滝u>\yN%^@!2Q6\uyt(fG|\_ѷtN&xVlzE$ ='] j]g-c}o,x矼M m)#Ȓ 2/GK!o:󟧖J9WXm<R/]vi^?;f^g1X?z M%}~.!%?p/82 /fdS]Snk.NL*>uơ|7fh)6rtxԝc3<Ƌ]I'B-;'uaMX RZ7MD2>t&@)A8dzzhu9OKG4)rx b's]MhZСt5}?]'DQMFDZLP41KqsC9lQ>.?ԃXx!<Hek-VxƗgߪ 7<-1#jS)4ueQCm~ U5gJ6Ir;2~Ej9zd`RܹW䳕VK;K?=?.}꽴ח7`tX/ vq~}ʦ>OO@*X b 0=J~AG_K{|tT*WXﻥZ݋p Yǔ|tpS;>>Jw߳NC@q?vdU`yΫn<{>}]i \a/oK4$fY ^$gS)BSNkڷhD{&R매y%}E{X} ꨞsj b6 $)!x<Ͼ3ܕKm<" ڑ 2⟴GͷrplO$0:փG9#zeӜCV?DI-@B1|@RL_I@&aOiI!|~gX'x# KIW~q~w&2Z | $0ءiI|T|yQX&;m 'U[tFbf1L(p2h$5va^.F!VVU*mpggF'23s1M3b3JSrk>yb:?gNJқWK97qJ9HI_L8ԕ)Aq<ׇkk>Iu,wNN@w_؂|8|>|7#"8ij#EL1H^@7єf !()$p3 xGߗ31/o^{V[X>uQ[.[y>8쌱>sblwuψAi)nMbHٜ^[9YAeLv]yquRRйd>\U&vJʅo3L5p]_L<4C{>'d%>jMclGD--M[~[Cʢ Pp[ ny$r!}'yvV[ͦ4\.Ŀ9zea N_4@}lTP'& |Gȷ ؉,{»6Y@3Qooι8*\%-->˧LmѴ蟆EyIB] 3 w? i˹CFpyk<]5 9չ8qRJKQq:>Дel = 8Igpl=Bx]P dŋ֑Z;Ź yxPd+HȃCQ:@M LK./X{@GZdLx$qF҉ۘmVfMtzd.O$kKN&CzTJE̬O/kK:  l4J=\ʪEpS]J$63k?9]ܴѪLFW&wș3s~:}f"c.w\Txv'Vl?3|P!-^dF^WI_ &cm@k{iG9 :NtZYgK=6e:}9gI8ҞCv>Ze'[z6BPζ_TbZ:ʻϝSmqB>'3)x7jg9lrV7wӇ|?R WQm x݈nf&K3#P:u 3|Q D@lv34^%i`8y06,bWHO :4؉`4uCkxeKIS#8ڤ'C\[YߥZxnڿn&MBnGݧsHqt]G~ Yѡ4ޝ`rZQCpuf1MrekwrX1]+G>](4F!lS@Ci9qJa<}Fï@ftCyLpDZK6~ōC2BDz}idR'7ren,jc8ז|[W+ll~cӼ 0O_\>t] (pС`fN^,?3/EXekHeDksxp8}w(=/ZN-Z~S*{`/8+Ŏ.mK(mʄERaLfR/xKϵxL;E&wvI`2PrL9S[(ߒ-ⱧvŞmW{c_N&=w&{ʭ8*}uݠc~q"E9J/kZ@;2(+f|Q gS3P_o?ym]TQp+k;W> і$8 qDE#,3? |ߑZx=Tw#'s9|"DݤjS8I8{>DžNjfѮs8@W72M㷖&m576m쨪ZQ5J.q;d?whWz E&B+y㛎ɔڼV *`갴hQٮ{ܼBܴ%ʬ 6»\d+N#!VS[r< 'C“S)߳`kqdҳFғ^𥟢'ɧCvsI%ՍsƦ Y"ЍtRoBaF^݋nEs8!-r`:x)$vhO94gh NO }3ZzMO%xlC{RsK$wa2M ~[a'SP+c;;km}W^[\5طW\Sq_s{&=LS} lj ZQ'Qi*2-oEI8j-}gKH:8]#=YG8OD]Ќ7dx?>!|(vAWs\mkͱu٢=ö>J⡱`rUihgSѮL :‘);AIY(s =%W;ɶZ|A22#2#l:$b>9MA͜@N@ %"Iz`<%Hp?VK}va۶o w']ǰ[F|/x"h n+;:OQNi2\n!hi~ ܷ|ȭMcuWr3ݝu׎(۳1 ,?x j3g8^g#i?j(sA+!a̔ e=xx%ȁ8hO 3Ht'^YBꦾy&gh.N5/TT/|9;^Dw%dYlї'?{}71ˌ\kV|׳WvAykt79wh J$ZNj$˖hU1\i8~:,Ƕ=Mk>76^ ǥ_IrjPJk_F>{1zZpVDDi62ΖON4t=oؒ%?v}b~Hlcvt?& ]iS[?\к4U (K32vF>n  SfRg>Ygerc{D"8LJMM95tr9ْ_y(aW`jO܌&IJI)7`CJs'#|l^LWr^4Spg<ehn^L*-W58%K2k'^k?géÑSO2-#[~G\V LR~PvJ43b`c z *-9S\ nkk̠dz" 8qLt(/QVf 8xS2jN6A>{sy9 ?r/ o}`6"}21bOKmKWQK'zReݿ="_a_OZC J 'qj-kKέ FqWW{79|\?*m2?*+UF]M++#`|dmD+f9cgK\h</ Ynv?@ۚlޭ-G@\\=|Ty19?'xܣhۃT7FCygO)fVq/Cο E]qk-G2x4 ^dF:h*~=c7a:e픙+f~>\ykJ7-6i@IDATPzAzL"1X Tb6|@G:lZm?W[ !v3a0~-6 ) 665аvćм[m 4ʄӝW fi#t /6{Ks]3Gqk(3"$GZ\=_aK6CtߛcJ)./´)o oyhɋJS8 }Oέc xe ?RS礥JNgo LW<N-PL\~H94Ksx G\ǜEqU 00 ]n,HcDKi9-Ai6e6}oRjs,Nq)GʱOL4-caU[ ]U>',H^ M&GU!"ƞ61V6:[ r:sSq9j`%T(^{ܡx]TY._b-6uJs Nvҹ6hKҠ$8J>%փ,{)es.븓8}Ɵv QPH1d=N` ڪr:E5myxOJqo)l:e ugu9'swuY%WJk6pe806=AVYvFh_[=ëG6M4,`c+ _tpZ ^ž(+r /Cʿ򂓴R9u.֔ S?LYC.)n(ɀ-O"z5rL"d!p?ߚ-߁:әK0ZldCk=*>}L?\܃Q.A} t]!x}Hq^ Ndv*K'xo6~}_+ߪd(]As^զuV13Pܳ  1O30;B ssF~7d)݉PK YoaDV*/ V|lu2pfVzd˒չC x6?zΕR-d>|fk} ]Kņt_NXM {[|_Mɏ_>oiQy}+e:H_Ot_zw3:b! EF#W:k1Jy87#K `/EȷSXpmLV /m(#Vy[8U!΀%7K|O*,)k(32o\|RRѫۣ3X=ɋxܱb[K@p赁HA^jt GÁߏxM#uwFۋ :cXaBP/MݏGɻN]O/y~850Mzyqj%yi^~A:,P"w ӆo0;S=B1\]IO:BV/dx (Pj*81q\hOT4bq οQG!l9o0=K>~fo63/)&AZ='x xBX@3'~@*W0Ge4u q,4Ɛ퓎t@kfl÷y} ?'#ATҍӹ5ni1#`v: >:4_ 2vcpc8V8])8v ^U~mVMooKmj}M쁩p'ΐ]QPFyu*V+~|3`n(" sScg9 IΑhXr?ə6iq䕰:B_VϯiSw/vrhh˥ʏէEgRngC[g*_@ eWɚ}M9lhIt_e:oێ Ogh>'#p`?Li46#2Y.El|p}Wt _zߵAivuK'v|0MxAx*;SWN4`p_G{/Crv.bte2z/yĿ#6ͪlN-zLIc|Pns8<$cwZ|jmi:p!đ<$P5W&]Pb@˃Q`?wU;EN3u\2Uxq#@2[vUy+qpiq -hg8^ :[˳E=@. 03JJD!'L+ENjG䑭G[AjHr{sWюȁ q It%tu&?-mCb,2UEf\$98ak?s X!8w"rҢGpPp Ezԇљzzs 򑟴^'6ãhP{:gqB8AL|)O w62|K9 MJ;K Hv*[rJ70:嚴>ezw44Jh}%fiX._*CPNZ7E69:H-C䮛v'K{ ĻY.?s?K㼴q !3\tŇÆONQ;#.ρ-**so}}~><b꧓ô+rvğ[oP~uRΣ&OXg/ϵjv䘠 tsV䓀& K a{ek5 yeN[æ>QO ʐo4ߡ\)c3Wvh1S3= Sǥ ᇺr,lTG~p({=7_Vq񢼞OyI+^"%}<`u_'7xuW9Uok?SuuNb|YG姽J Fv˝[ iU0vr#FN* 'v։~V|++ً/#gVܛQR3|8st_y=vw|stA]` 󴻺Fʟҋ HS:fF XIw -M>/XS|(>N!$?ZJ\4 ] 8wQFt'cQ=4>Nױw:$-(Ge xGzeӳ?}+EeM~%CoO:Y,/£c:dƱԭe3sxuAק1>9rF"n `D`5tg pʷ ,8/#Z:G ySH] logf E1@c։bTqUƦۍa/N|FJ-sb-dqE`|r#@F{stj>ߟT< 'ܰ͆WPcxYǑ}둭CBͿ3띹7G+LvIx {f8[ \ɲ}7t<3 ݒt<b9z$ GݳЀgHhǡ!u`ަ8t#,%BE䑥@i3-i 2sZo|6 ykv+X-ƭu Gp$Խջ#=ފ;#E=`_fh&ߛ?UBw2>Dl/sg֟L6_ߴa|Y|H r})5䣏FK `/n&[ѩt6:zN &37d3.3:7uS>| ùDoG+odd{EƸG"8BL9mrЙm>qhGѵO6-xDܥ:y`<\|pИ7 u gq0=wҽ1wet`sC>*Ol:)qx&' <h1qxtyo&c8r|H~:lI|8z1o +sg62_9I٠p: %|AAI!D)]G7݇?I΀ZAAY)V 8w_}+6$x& V:.t7MBRJL ~(# vb׶ 9m8(VP|ܕ,=}mU+}?vRL#+Omn\8V17ϻr`;DRπSC(wk78\e(|8VB<+v ]c_0fڿ鿾 $~8o@kCuߞ뱀#:b Ҟ%3;}N`02Oua~si)Q:O ֛hH~+K_|Ixϒ7, 38+5 dŜ x WCk{Oım3u.xrgrsX i 4W.vNm)XW[  g6 o[X1`:4Lr`5\89-nuO\.dJIh˱ Q/[83u&l>|Q)-)tdg5 'c : 澜d\.M0stqRӹP8q q9ΚsE ͅw>4F  <4&2UN#<"*W?#N8W^T#ѶL*Ζy6Yv~1Ƚ` yhQ\3C ߒviޘ+Y!^y4k8fo⸃Vz gy@4Q}l8б:Ŗ%wL3ˋ#/ ԜN@ʯ>.e .%,QzBù flmxkG VZcxwd'ݙ TmkJh/$(?% BGuA;s 8\V{&lo59BBY;wݨWgv]Gmm=RN Pu5B*{p5)ڥar似(NSw+E\T|9teq(0iD&Ͷl mࢣPřQN˔G#4۞P[\6H:oT' snL7.G HqN& $P;s;#7a;m1iCOAﻮLuV{Rj*0#L毎WΗxpZj$VOث׼`mqT JjjQZޙZ9iəHjqp0<4Snwʇ9})Oi!zl{!7{mc:,lEl$l垗.sC7,.}8ٮc8rlv$<õp*@%moţbIG`d1rvtw[Zd,G3s")]AakOBۖD֐-%:Gi'h?wRePn7.opΒ&'NI߰r)WMюn ln[4Y֧qb#ɂv㛇Nrny(/ #Äf勇8j#\.I"BOTQ"L:/@e+r(P)A>BUAϽ| ~h37^}0EBU1|8e=2ɨWx:5VNrRPQnסcJ OCĈ^)Z3?Ғ7t~ @)m5%sDUp>Mgltwc*ɛ (>GyOŴ>CPst68t5qHnB44l0U&}0h!ltUqlt}5rه0 Plwz !P e%O|:𵑣ۏ5x 6Pgr/8>4ay˷sq~@6ҎyC Y`ڪO~p*=]?ڥa15lFfs}"^3B\u43oB!iȲ>C!cQ )@><4. 6nlidCjMeKH]zzksNCUe7mkl*} Һ<4{9ٔҶ{ |fp$]YCk,'>:~[&}e--G+N7p}d9fH8X+ X*B/G(0e#h+96B~ /ي?O4;N LZ8{jZeh_\V>W["-76C*w1{NK DI. h3_3'56[1l:>꽜_#o F8#6HXɑ0AY md?`]%xQ=uh  _Zl :uZjnʅN~?G\7dߌKl)ћz742(e/T)\"jBer& 8CAh$G Peٓ 3U0`+<^'sX[+WkQ '|WCk5]Mi'pu 0(3p%_:Ty31-R/c8r`2n#k5yN?]kw,o$~cVW}%V/o`4y ,pٛK#z,_AΤ 6{z}t23fԑ~ł[Yn{qZJgOh$P{ 0N܊E(;V7!u}^W^ő4w8a 5Q"|W㞳yExYK1^`P&^iK/kІR:%k|(w#R>pp{!E&v럁iˤXOëB2MAvmHقKУ}e{54~ϟu!H"w㧖yAN Y'Ss*]o"5Ǐ PζxyTO WRNGr*K+-}:h5OZgj6uvx;-zrDğ:}bi b{*^.ʼiy9"rgߜl܋k8 HEP%l?Cȱtmt˶2Uq(d vԀ{%IJl/#YZ_`3J<4܀pQݕ NbjN&9˿xDӰM+JOkSvɨpDg|8 0rO%oCsye)Ca[@+}tfs'fu+̭19ˁU"hgvogOovyY'2n1r܍>/Zc:SK8ZEg7ҋpՒaF1΋/ϥ'B(=)= FӡвQo%^ALGϭ,`zA lZ5mŮ҅؟`4l{H xxwuE"7ث2ޓzaquio2) k,Um{+u_.ŧ)OdٳAKmZ>XU| M?nmyyQڧ܏!.A2F3fDrHl4v34c!cD(%qMqPny(TvJTp`DB ?/ᷤfKإ-:V'<Ļ*qJg|ySbMyixs}J&7$nF(:wy nAo ~,^ӎ p\4rJqwy^,7uf0x8h6S^JMCa;0Ix©at;Ms'H@*Mm^fU+_'̀ĉG$]Ÿ-&&WxqZ0 !K _A+9=2U]h+[iR-ju0* 7m֯'+k3_ UR3v=>Ȁ#ʲW '_ Fs]|p ;Scxl$A㛃-͆x@ݑޏcgOA1]9Vį/Zw^@zOXckһv,7_c CQ[ IH@_MilP;Ҩ~?~QFeU:?k@'T/7af{fJ;Zuq?xmqU}³_C`")Π%uRPQe2:(q4Kn)ns|N⌫P_@^m9 '>nu-+y)Ì>zǝ.p"gꯍ2U pVOJmZef؇U<w}y19ˁG.t;C#7FWvZL1a7'Ȧ4t.&:1r}cK&%ͮ])7phe]*>g gI`_Z@ Wpc+?ԅ[9.s{ 682abVyA˖[@8QN^jBk>85VrbSР(`2W^%+Cm\{Tv𐌸W?W oq ǃ4%˅rDfs}"֨]{0/ 7w,}Y-yr`׭o3̘4h=ܵW8dvF/6k)xJbsh17z=Zeyn?U~$)jx+wkab p5AesAbBvߜ:uo[RPDxFuDwsO[+n߅|%xnJ~?_,c9KUO8œY'5 (ATZ_^B!cB6i#,:~{%!Ja>BD)kSF4K#H(]`cxa('zkГ8< JKw9KHpoLs>u(l[)y k;u x3Psg߫́,fyddC,W #Zo3w./Kd˅!r/MF]ow|8Fa =`iPfIm_@|l_G9^!K2u2o.D6+e>063 } pO8I t2 p,3Ey_s^N'pʇK\Fx5yv+m"7j+zN:8p욠ݍ+ C}0k­Ȳk\:_/ۋ-qO3(͹ ?vgRu;&33ڙtKK3(L`0΢=zAb ܗWm){gG,j ?) ۼzlנ2n+i{XNŠXDk/8,z[r KX<}LS j2Qmk{ye2ϡvU#O=_CJ?@peFMV!o*§#f3OP-4OJs}q9s 6P8QilBr(Wی5@IDAT)Utw y=Dy00K TA5EFv]J S@[x6qIեy# 8uf"掇/W'\G9! }6W_3+|}=ErMIHX%!w&[qQ  JH?al2(OǶ _^41hT"j0'. ϫAC#:IYY{:z󿛒VwJwhZCI^!h͏A`g]v9a]o@2tBds)f48*(|~xjwm9y z$̿e^[Yl|E֊uoCzV+'8fk*:7|/YP [x.7+R֯xG/c)WA?ñXy쏎|:nL BG) %h" Cp* s&7{Ue[5hh籏?aV㍜,+t0[;p_I<<?l$Y mQreAtZW]`󼋂UgM"ٚD;%Ú V%(Ï=~ WFK3~3nyR3%p=nx `ЇS{qZ?Rs_Az$N| +I3pJc@l92KɈK{ Y?uK7rDٚy\֗znZzVF|oӼ`qqmM5f,7,JH):%_U};پpF*]hNG^bd_$ m` VP4ۖ](zJ+pPa WĽ&}g(A% qPC_ʕ}Ox5D"ԁ94: C1Xzrhv GGpAlΝwG`'esq[hJ5,H4e˒s@HwQ+Q @@eKϷ{(Uڔ5>.z\ k.,0ekzqr]'ͳ^%{V-A|}3Ŕۅ/BG8좎lZ ++1.k8}{O]qx\\u+8+ۮmA۶2-sz?|uRo%#^np;jJ̔Imvpc@Ҵ66Rd5/96#&/r<o`餔8=É2ws~_"`%UC/\9}RNOCKkCGꝁg u3Q'3Sb 4 ]HO$ul+?`8kJY& W{؟jѥV/Ӝ? 'nXgu+UNi0l+uoDsGQB>] +@G+kQ@197FV@܋B혫nqh9I ֝5a)8j-%W$0DAH{K =i!m۰XFIi}ئgs޵<dfg#lp7g,yg*@91`Po]zogA6ΡGsπg>&d@{5wiS5;\ ttDm :ɴʿcj\.M" @ߠGsT^uݢsG_ݑ#CC{oUN+}.þCAL z#AdOo'E`"ݜ]r,]ttepBė7 {oꙙ،uvet0HF=aHS ڶ7/j+X:!qhcye~ʗOaWHCḧ́{מ.m\^K j Նc1 tnk# :@˛rFA\@u\O x<܇%Fq'ūz@$M4t631p5[klѩ1<5οp ɵ=~;J-Vzܽ@%w\khRV1JJ`d;]%Z✼klŀjqȠ롵5m9wo^Lq8FSJA Z So;|'ʔ`5Xn%2`0xRKja9%Sq2.]8x`pco'S>u[*WR JW[#y 7ŷg|gp pܱD:F8[glL% 7ϒ]_~aWSI:٥{K/cW_txkV?Hx^sk$p ISiN'n@-ǶEi)IFY^\ja]3AgCr{ {\;yOW&jc #v"}4 \7?a06@ @|0g T&z]W>^'G X1q‰1Qbv/~J6xipɀ&lu .A ("^zP}Z&բLCЧj4ӭ1Zug\6l`0Ġkpf *_l*(eqF8uq}uf\^8y3qh-ҊfYjPDTONōB<1C}OiK ǡ'ԏb~ɮ}66lɟɗ˖XRs&[r3ŕk'0<֠T2po.C|uw ѵ4^ %Ũ8-mwnUE[=I٤sSVqa7{ʉlLg5z}\<{өcSe5F m%"Uuݹ ޛ-ƷYg 3JKm؛t';S9 `~?OyOW澼b+etn6/,6nvsk.xTiӶ)o[*١ڕgf#R~])܍{3;rd{j }sVVxlo9|wI{+כwf{Ap 2|,;ķB` V)zN85xYb3y?y4^ǾveMI.NMpΤAv`a+Gyǒ^˄W`\;Gbe7{Vn'ocX44Ǧ/M+Z>-ʵʃdKtt{Ե BldDwѐG7;LБekNA:ؿǴ_é С>Gd5/ pw@ŰܽMbȫ5Jö.mJI]qdP oϞ(ޝԬ1 J r,0tG=Ce-(+C,OyhEK;l3gNImӼ$N;11WNˠ{ÿGK4[Wspɛߝq47qq\wr}Ncr,U Fqr^f]ct۪q-Cafyǩp?8DŽfI:&cij q}"R8KO(ct> V>ZO8~µIgtnwAu9J|ߣku3]KjY q7 yYvcA ),9w<5Wu2M:dN0.]d`}agW >]lFzqrA/{VjӣtzKc޽h|ʫ=*3ROv8vL|`νyW} _OhNkM|1}-#m}hpu.e̅M(tw Y-'ֻmIx!zm$|/PO>XV9"4/ay{/xXXa1 L5:!Q"T:1jY57ai$7Ԧ Uc:=&){n Hfh/uT*tq\,ҬGkRZOu;Mg={iG+iCewC9JȯNjch pCH? v'N|Bk_}:]e5 ?6P|W,'O'lך,k| n$82i5LNKy+?~YL91l!Po.dT+.u=Gt^7BCmk@ao3:|K,ř BZߗ汁璔M80cK WPS.PrW1I?ggڶ[%W:{j 1qZBu7\%35>j'] HsVLVN/OT6^rx_JgPي;SoLwjWKx"HQXp vfbM[4^F)'׀A]#b4i✃.<]ݯyE'Ʌmmn-AhRneþ2)̀Brlh9V9}.|36 JuY(~0nccr࿌|~yH(H%)Rk6=@yuV?;Wu PGuHY4GvXan?r:rL wm2SOBR{YfXJ:0o^l-@ˮ[1 ̈́/5L^-Z rt4wϏg$>~Txڠ;\wmUz)G|x𔟮1jtZ[%:_R8kK1:N/g[F˜QvB&0+/c;Mm`,6s8Kpy:2~QB S^>}`7ш|GŊg"PC~x8O[3#Gꉗꠎfi.o|ua_=üP Bfl G,֥luN)bET1<{}hFؙL˰W> *$53˸ݑ|~\5t>"\^zBi=f&l: bV_7} -À"z, 3WJ刖ji=ȓ1ЖE~k #8 :o'hxzhQpEwܽ7x Io[Xxp zD-v9©Qw:J6lmu2 {^R\.5m腲#Ljdlu95װ\7ȌYC6;7Z]\~C$$r -55M U#%H`ʋR5N/_lF c| ғ7L1:j>QdYF(`iXLp%ơw\vM=kV~m ^Df6}Ѷw& Ew@9vL(IREQRVB+稙a)| Mu+sL5g+qN>,e|vنE"t8ηi-NkGy^Bc>v>ssn6-HWÁGaA?d7^,Q!gց3BYbI2LNu@i1#L ~J"wf`C3I9gDCYyPcg~q8j4֫u.J\).nz<Ã`hݴ݋k~s f68H>[pnŕ3Fxc nMٿcς +ϖsF-{d<Ώ[ B!*򝖪,eJl'Gw./f`?k#8>4_K@:y*cGJ,Q#kTږHo."4Y,yV Wj"nwB|92~h~Lsx#=:~!~ȋ's̮gqt }t;>aO練iQ;tߵ1uxMcc?akЁ~_`*;YK:-n-)İ3iy:` TS| N*:k Sr9 nꟚƣNx.4.ԫYqB}>BxR+k4%!jNp=H"t0q)ɲ,ۢ$sPCp|s1 _9PG0c« |RC,Bd>gxT=e_dv&]/bZhW֔x=q\7LЭF.`RbM]bO_,2?{81d'$Κ+t&5ykra*IsHR&3 ])J cܠ` b ʫ2|3vD4觝BGY0!s=8}xm'ߺ7f>MnZ`yZ-V[蛹3:.sa'lq΍XΝ(O©j`snQׂZLN5gF )p2+"PUkn.-8,.p\-Y^=OKhע'm8z㶭ɷ>|d_~E(șiI4Gw΁hG,@h"9C$?;2_IUtɲq~1WwtDʵ=&hy5:7 3 3ܖ_ csXft|ѮGop XGbe~;qIPZq+Ζ~13%#.o a64#*潙OLG:F*=%RX 2!}g6KAӝՖ!ƕcgn-PGC(M髻U;Ɖ\,5g JQ{骒Rl11 @y^y1OV,0 'M>g=uRs [8xV[0I6X$u"=qcuq (m*Dܽ?h3d c!}>^bꢍy\4cӾhL:fY\ñ+ͭX`ONe#NҊ1VPZ,ɎdI3{oխbZ" \O- [=r)RU}̛<?ĉ 1$ pwsTSS33  $%BL> KKS:7ݳ*|A@/񏮕ڭ#jtG/ON}br`9-8;ש§yRm&V*L8]{.6ym-"Ο5S>B Jz5xRlV7FNuo36ټN# Bw:8< Mϰ6{FdؙmչZxG;E軙x6o&cb AxpRN \|͉/hR> ڡ8"{viacǵWS"ѮlمסS/:էܵ,r0X$ZJMY?wz8\6kx:?&xm]ȷUTc5L]$ Gx##QrN(ydlm#Umu(- La/ n˩b P̈O*> xV)5c 8_uyx@hpZH\)1FVFeR';8{:H`b/ OZ;* p^,0^lRµ4PCPo69!aM88#Y8aQO  ebVCDnhHD G X|5dS͉M_#?7Xf>kY͇&=9_恋@q?Ρ?&x47K~צWbdrB Ӟ'pδgYښ{nxАg'ql䜱O;JDٞ(½p}ijv5DФyMúd@=t4;5q, N 6 *5牚V2&+f_ozIߩ@TO'4'z~؞wgj⏾f-a̡;'b4KA+~_]ϵyAqGyx=t.6A:.apJV|xvD\ 3#ԕrsƫR% 6 03qMge:𼲕%98:/>IA:DhѶ}!TglH-WY*9)3[󧬤 '._ڌ *T+ ̲R=)DfJrxZʄ n`[SҶ|tOι^ۭ,t9.H=aIߺSyh9}Ub~Cږ[BܸoǢs-eIVssC٩/::80&k3dE3.hU+/I,b3Y~!ͱ~3bJ2?"9k_q'hϳ#{V~gXի>$C\l%ϼ*WqN mK6@L~{-/?8^ESka.oFؚ7ELw3X- ?pzg*:8r#ѬL9 Q(Kr ޶A5&xN8|p`ΘjRz\-hw- `tρ~̹^CD=J8ޙ:y3zM >zGKXQU\\gچ^`f`@17/.:(Ƹb1RAPCt^ɗj=C* ylVurv䲃8U@8 nqR͙8*דH@𪲽\uWtOmny[W4ܐcɎ\ZmALmh^1DZVzsn]r h1Ϗ)xbt{_ΧA`o~04rJU3~:<<ɤӫ5+/E/YrCݡ(9ܷB7hJN+k+Sq1C:w36YCVZ9Umғlxgj}~P3PclB68,(8ZÑwF;R4vF YLԃݒ|=ƮG9IL2 WǪ7) `Ov%rgvsLIvDZ:] MʔOô=X[VUS2nO-Fm05'oolS9bgȣ }k}cFqB>Oⅎ,NcZhod>Ĉ7敃c>*4u<5V OǸ޶ԸG5W':ݗD줰<\U\sI#li7?@JζKAW$ M[) 9OPf݆yybdXt ¿:h1O!D?2I>J-MZHW;]YΐN. ֣r>.j٬K9is:et;،F4#܇`;N" xڹ<8?j*1RL[zgZ㱅0hbK-[P!p:hʉ7-׵K{jn&:wPȃ"a_mO0;+˛N|ƈ 9?F&S샀ߌ~1``"bp)e 5'I )*@@ GDlFK=!C+CcHC*[ԙ A V( O)zNᯠC"qPMhD>Z 6xUC\‹|#Ӥw*zah=M&|}Y~cfhmȻ~_|F8E7B:idI-,%&FVAZ'_4b.&h8kX&qb?NҪ GScfi-:Kx^7 ]iq^ZǞu+9->Vd⪶*B<<Te?Fs@x)Eirr?ÍyxA](H4?K9]_;Ld[QEx:s7j[0{U슴W=g 棆ŭ#Os#iZszW^dw9bSs\Vh~#CU4ݨQM|?姊؇5%a7|A- Ci26e[=cB#H21cR..B8e?)r3G|R+3=h.ԯ^G0pπ?LiQz Oൣ hpq dZV)oI1Ypfǻ%` m{>Ə햶\2m8xRiy-S0IM\-b%$޲+E#뜀0>2x|M*!hNi3}Tr!V&B]ݥ\CK^gE3E8EQ 20g+21++L\B4d96OJrl jyt!8>@)1]£kN |mo^7,ʃ)ILiUYHćfX9Z?~'g^3& q:ޕ=}ĄƢ_wX)G{#܃jYflG-;*2I2Ԥ/dh\,9 AWWɳ_^.c[V$yvzѧ5v1!ب\pvr ܞO:#؋vfLk94vngh_iʌc@ɞiZjDžoBr턄C;"AiXM%JOn2+U6e:}:"Ɲ;3p KgS.k%sѓ;$ŮO=cE X4#O2sLC2'ϛ)L=c2oB7C q\@IDATYSdȾ5\b$q('E\+ނ~zo'S4}RtM~\wZo.$ӮPi!DÖE 2hf:R\qpJդjjY 4քf(̗<<-ɸx9|3Qҏ!u xɅ;Ry=ˤi&Mh vd;AqD0/8B޻;x5*e߹WCŻWRW~;&!ໜSږp"k-|s艬_r dr6 iw |c'SOc흤+G7组HxcSrjŃջV[_\܆ȁyd9 *)bD4xcd "Wa 9hP1Ln3&Ӯ!$=\Nxq6֐^`LdlS|::Rg l}|4"tЙƴ_k!PuhG&!THhscO94[ Y'LK8rd|y͙;1 D}tFDF?Ů9'e (-;s#U花Cqik.vrrP+^#thG_(6iCoR_}=|SNR:a.ulFClxP>OdI-7ľ> I1k>}ckL|#јgv^qҺ 7,̪?;̂wG pL~y\ &ގ FyN*GP#Wҝ={shysթ IV)nޭ|'ȭ_Rc8 iShW {o'|k hH[f5S]CS H onjñ`-+ܕ9Ο 72RBo!4Cyw^\p|qp<(JE?-Gj{!D${8L(E^k`ōKngA<ܦs^ ZJ'D|PzuIuL ;7ΔaÄb96]GYU׷c<}3{q@[dx6>_gs9cpL*q}|A~ia3I}8',(C0hq1PET#Lh98rs<|~C(<8NVpB>xL)g &Vwݗ[BL03I<_xlu<#r( &_F~M|oʒ1fǐ*]J0?1PK? WЏ/5@]?;L!#~5 y\@cU<1X? TBɖ9ٍH["Kc֔&*}]zv]ݽ~llxŐ|4Wjҋ+ے-?,9w3;%?f\9ޞt^1IL2d)x gyGAޤ.OaN%q>Cرݥ>d]fé`UmCIz-G ۼMB-ؑVMϜߖY+ͽ膓UN>|/F Brظ$>ǿ-y!h>os6Gt[x8_vԝINRuO|tm Y_RJ q@Phh m^@hrxc;wĖ<髼Bu^[}>BQl.YϠ>S_>: p9@СmEĿ2_!"n#zZYַZYY CSVޙs楞ѳ{iv.1V+K󏱄059)C9t:lpO4<௉1u(.rzH`p>ӭ/Jw]+(1}1:2#Is0+.c(J]Giɛ}V(1Pݓǎk\m eɧOB{k,] hGovl2.مx?FqK\Ӱأ8Z4{s$_K) ^dҪ[ҤSNc3e^b;r tX#dFH"]V\@_j^]9"kqMKi>iSC&,9zNJyr48lˋ$7 JEu*Y~Z0oߴ3~N&DRc(es1G"(x:CU\˧Mg+vc1 UnBW%)Ch%jrMۺvq+reV1gh?QX6&[=tZ3y}ߨ*gԭ`p3l@zA6a]* 0 d.:yIv@TPCI0̇G?.EVMЬ}jxD9>wq)ChÙ605:?_nӊE!ށmxe'W|okY]\z<\͏2ھ^x[/udzl\up0Ԯ/ N=gMwO}S=ۭRCaa=r<Ι=} t)d\ao{]6v')V)]rI~^?t ~#ÇW*;k/۝ΰci7:6z< 4m];Ы=B%uHzZ|v1d+q-mЖ/lUy ,ؠn<> -9mvyɳ˽M/m2Wc}wϗo_8AyOt滤 mm{VHbR]U2nE\AJ+Tቃ5n.O RPJR1RaAV@ } 41fbU,XWIVrߖ6䳖 qCNd!_@^`qX N)~3?PEјwz;n'&l=x?-2E"đS,X<^˹RM`* i7YpIgCݡ 9i7YaəW/mМN*)hWra'/4 ,{ih:vACW s 쓲Rk%>[;( @ Q-9װe/8EF4Fa54}rZ_y:w݉<$9Q2"&WY5<촘b:Qy/ƅ 78N-o`d'XߟՑvd2e2&I_B2vmJ{s6N(qʂ?v C~|`W;>@![;>Nt(;?!=3,y<cbmB[iFp\K\Gc;gN<ϭe/!rhK˦]xD<>)y% ^Fr,TB|}oVX,7CS_)Lȓ es8yKBewWV5?s)]w1fR7sNd;wu1wv+hˣFy !>eAJpPHwda45? a.(`MhY:k@J}:`BKCV k8ߧSm N]1rLh@A@CY 80 \?s1ˤ0l&YnDpS8Jwmh"=04a`6LGZ2y:)3yG > 3!ȖKo[[ǘP2y=zR%^ğaHG!Zl%S$]ţ`~V|ٶY=bɗI~*ԘO*ݪҖzYgur! e8sx<'P u׹Jvw4M%۩].j~vM^M?V6"- r<|r8QV nIoRyՄ߄!4+cAZ_˺3h?+q\޹H­caGp\xež5;3j0XʕFF0vt!_><τ6ܝ򲽝nѱ/mq*X/^xs2lUW0;h9GSgzW&8%㭣cKk]a=SmVF!7<2ޛKii"'"{D$w0B#,G[!sۮBʣLm|Rcg6O {CYsتCq98Pw# vL}:Eo@_SjŠ脘 ńySߩ:'CBν&QѕVi(Y 2^J2UV&#dk W)NN R+Wk2 sjЄO(dnq\s;3д\ k8Ԡ,c[ l*XAwEE8w-- `z=rlތ#ۜpBL{H Ygm*OQsPL{Mù)ajiAӡh8Kh,C\Wx_;Q:XF.(365'cI_vlad^L{up"p2FŮ'ߒ?coVVT6u5~N=NBF_ljrE5FpiMy#0r1QVMv}LU Je'#_6emp/.yByVm t'^ 1~b5ӗhЛ<_~W358$gH eMӱJBOGqtCmטw]<0@q;?EfII d KdҠ:0N̡mg\m;dKE4glDM) Ӽn ^ =IO]uuuEOx~&>t|'-p$輖P'fF>1mxREpb(m{٦~\ۼk֜§ڎ+qjds8Wv:|<;젲C;9cssJ ,aA~>q T[5+ Ŝhݫ6-'7!@q{%;ǃyϓJdU<=QxiJʘ!KG  _5UP5п$1w|K}mø%7|,Fw:6$(MRep\l4>!2(rIq&K7ihӹ|fG2hg`kޤ?_`L)T.לw&H1h_ Ж {xBc.[*>Ĺ*Εgk[JEV'[e 5gh09#(\|9lwhqtk~bpٽ5cfa2LYhX=4lKvC^Aٛ w,MИZ<:0`<|+h+_襨 + JF *RRS^gb4g"O@q$iqP,-9(YMe{~M z[{O& ᐶPEV55W-@O0"ѱ vlހP=h?>߆ Ϫ;<@ 꿴ã6\18[Ww[̬ߎ,A*},!sȥOWA:MRIV5 g -Oo2`Qְ'H.R+̌S"ɵ,\FjD)Ry/:Θcd0Jy6J-HOf[C1_bϧ/_mL{9~eL/gaxGoaL1cQj9h`/]a?V(1C_tyb-jߒ?C~]@&0&y^>]Ϥ8[o[Y:R#o88_q 7ۀ 73ӯe~Fj=ZEyxFOom]q;ҮR~uuC H\ p4=5{'A|c$ %S/^v|v~4ޤIaKvnXh#e"f7S|Tg7#cO?%is\P6z2.'Uq]K0F+(q#B1洄|&D ( Ssv&q4Čxr(7>jF-6?F;Ry1?*Yy|=kK!tAA1afnDa%åxxLj0Rw Aqڴ#):M;KpHvzݻ{z2!gɴu(6Anm*Qe.yc`+o'"9W/L2'jɽVDAh.;^ Ɯ_k #m:))Ov>^:GEKK=ɗɞVw>aatk$E;J3% V*Vv-4~9|zY @&ny|ΥI;N9AZ8bXvB?}:ek|'N34km9FI;wO '"O wtiOцHW)|raC\e&Ԍp#8/#>uS!zʶ4j}96;?ߍ$?qF&B#2y>]2,]/A9~K v~Gz!bzuaaF)(Kڅ1lN huJ\hcwjv.B t{ Vsg]{hoSd=[]c_`Gݍ/FY9>|QNX O&g^ uZG `ARׂ;,J^#)JTPP*aԴK ;te"Lj ^HL6ˢU&8&hÀehG9{6s]IIɥ|gJguLhطFd'@mx9l+Y25Q|(*kNj&AbȡIkjiqtw#k@]Q4@IsfhQ\_GbTHjɮOR7gl8okzϵa{F)H;cדs'vK+d,}1-ri  lUis)<`ể(;ڶcqfypZ~jKk*]dDՉʌ:2,84^.<_dr6~ x4^9kCSTEʍcsW+ߘ9]_ Ω6\ F\u"kZ~9H#']^IQh'_J-a6BȓB'z 3oOU vrǴ*XMc>;‘c~\bWٻڀ.mr~h 7xvȷԙ#6ʼn>kuG߿#_ÿc6,߂MyAĉ,!}d_+vV0c0F<=j`H͑(Wvde^PօKw\Qz mt u},TwxI:ՈƭixQ~nys%DUZ#y |@tק'3ynO{JYKHFɔ ȁ\>2 9 GG?*"ܛTubdv%X)_׊XjZ/~NK[2OT(4/LF}^)4:\:|Kkr/9}oqJŽW: n!3XqʸJv϶m}SYC 1сض^yhQj_Z˛Ll}!"G^&>ȘT(U wxln3=xٜS][x .8kEN@7i>E`>X $r/·l<>}w;i-=x h#3Ii5 f_x,5F %4E OגtS-y3_fxdkgÙkz{uܻGiO;[JK>`q<|LM5D9V&q7>]-JN9ù?9ɱmYUEsz ]ҤtuЗOh;ZSR=`j8V ˅VYoM> } ye{Xg?AJ/TxWk P-ΏI͓ۡug9F^8}Y` +vaV#,V~;6|L;z3=Cs^9#w2 uw噃|c0}~0s_Iګ uN*̼gL`#6oM} Vp?k3:y4eBVrliy`_#~xb`@'0㤀nd.G&X( {!/^q^¿RQk8_isKX$*BۮlN΍ V3Ԉx D|5}E>VZg4?*vK˛4ly(Չ#% Ѓ1(3f(tb8D.&Ŕ5cwN i޴qŀZ#/hӊU tT A1GEз!IɻRk ͙*4)wۣIg2PƁڿ햞.?9ߏ}5k4w˴T/K# |dvyCʮ d*r\MԞ@I2PfXmk}{2DE ̴zNۆnƝ apl_vԀh)U׼ :<nn5h#%L|7);=J^W NZ|7TG_Kd.wcuZ# 5c55 R~A G育E:V]Bsu'}(!m0BoEv6JߒR21@I7ZƤ̙s,-d9β{ѯ:w@Nu5]Og۔刬M'Nȡ1]H!=)®Tn>ۗ_\+NMOMu:`f&?ڱv&zώGY LsAc7?cᷠcB1oc[$iN)AVlKey#fx#Al/oqݝLIYS$xߞ C>WfW$J/s%0l'C 13O]q:Δ|SjMԅT1 9@+]MHuҞ?kʵ\yv黉5BE&& 5wɻ*; {>qE9۹@>Q6~~WycoywxxLz~ GjLPV}/^G^2V{:G'oeDPI|EVLʕh(tkΫ8<.m/VI ͛~WS9NE%pl.ńoԼ:=4RZ>N [4`L-;2k=YѮJ}"@{ANfEhUFo hfeBܡBKk{sl^=?Yu2Zss} -RYKut| 97Պ Ļ bH|O,"<%I! ;k>a1'WFxu|1Fm+n~pwE`A)mG!"jgSQE` $& NL|>fJcǰa|U Ǖ25τ&efXrC85Ή &_?ݝυ*fRn/o-|ү~F8" $ Yyvܞ4!LK?ᨣN4vC|`y̐SJu1Pc)m4I b8 36ѹ^p̪C6-Jcu+Xq0]TݪKd w|OCFFVLtKvf~2.+.zm2Ov0&e /dLi]x)Gg7"Rj3ZvV3z7Nhk$~g'}XKU2=9v4-i_3KU|ɯ<zs^_4`͕zvVmWuͅ6;2O+r*;Fھ2d+9"UQ_9 eVmp\\ڹO7@C0 IbR'?G5Nj󭁻SbN= o=K_oL\]tkQ"R!> Ol=a7>{N=tesW'zә6W8 \[uN 8@0ʲҪy *n9'XJ'eqİ< (\EzuPS$@9[n*4Ս>ڳ>WAtwH U/$oKg>|!O ik"u ٳm{3 SL|o 0Nomx8 ͏3ޝaLI )Py`\nҿ7U_*?5A3hcS(i5bcJC LVY>\j&X-3;蘗27fޤN\w.õL?1hͫ~!k7=Mxz4H :kZ Wc^CCp؇ʗ\OԒF:g'd KԴ D:P 1UM{}?n}83pT*B/ *|GS_^"C5֕opxE2ig ԞN~ e|H ]VgxGŇ1ܙAeS&Ù=`Jڭп-m mWxD2x-/ SmvG_02da'{ћ?K?|1oKl0i*x^<,i'75ivq?+u|c%p & 1JĘ.?^kґq9vcQw~Ё&UpmZG=G"ըR7ƨ1 po[|ys7dg.tWx鋼NoyCqY8!&.>FFXJ'g75amS^捞ā5>|1zn&1r-kmtDFp}%;}mun.|0_'6</w<մWA:pdi$#Q3d< n@H[9w:aۉΒ]yݣJ:scD05=oo;?T}ƾ0@hnU\/E9qi)qZ&G#OrN}]NPq/;-8FJUʮ GEW-[~V-„gBTXeֲ»K嫻Vڲx/NĦ3H1sr-Rת6l"JIԸJYBK W;J QVthiP>D12dUj^]mWڣ0;*KYi%+LNLq90++80M$äițudL?Yڕ8H6`XYwV.y=Y>f@<8jti<~"0Jxҝ6Ne;hr*:=1#g򬁮 ,F4 5Go-SΦqRZ 'Ou=-aꚽuy݊Zx_Qcb4gD8rV[,R׊~rSQAJ%S?13P䃝Y+0evie-GXLzbD3<[˳۹4Pk:hA2;#"{5Ni;hq,ÒIN蘩U?/_SF+RnzLV97v[-i[ x{ 4!?k@W$xjӖb6FWpI‹qg\˘_N+`ݪ:;81{4FZZOpG]KsA8@ԩMV-H#8frF| kCyǽj:)%n܍nƁdz:Dx~47wwՆRUR!L'qA/i=|9/9d 8auOx7.޴l?s{0<3nca\ E W;]Wʵ_ Ɛ\.m ҢE}A&v>f]0JihCQxmbx hp~7h+<^HLv+õ'Jh ☠vK7>&Gk+-Y3^|֞ '|5!E?Bԋ5i~̵W[C(d":chC&}!ʑ-M̽-8׿v( 5ps"2|ӟڪnQTKa:[or_NPv\OŞ1ufC{@9Rx1#3~%IƌװMx/}iZx\NwGj)c% oWZ~і᭥Db*ZЯ L^ƚ ~k[BиGeOrnyJa 5" ݣ!A8fc0se0dڤBZ23C:ik:r*˽ԩ" Z-cZt)B,,L.g4,-M'n\9ke"+#C8۵TZu27Kx`*5p/~g\Iy1X?3~*'甅7/ }J鍌Y}f֛8R nLm+irUeԇ+1DWrήa;o""v9jA8WR)֣)Gȭp==jl yʼnqz;di897>U)~Xi2I+]agZx3%;˵ɛqEܶ^Wrf/a"L( Źe-S7*?-4EmlaMƹ:/Z[0##/Yq*(Eh%9'2}ێ'I/8wU!-^j[;Vo논N:xg_Ws6v\ @/foH5̺˄SP mT -!됾/QP4m'彣 /Q]qRhRt* R@-a3dCƖMQX9>hg$}i0`itALچruproAlsvD& { ,聺%6<xkz{ߤM{HgX~3rDO絶]E%TVZgY!%1Y8B0KޮejX!RZ[ք7q1C8V4ɪl [AEic0۶K>:2)Un>Q$y*J^g0|'slw*Ո O|l/"[q-@Yuh[j>s*$ ׂ:|'=Io.ɗTn;NJ=2,ژRu0`%bֶLq-WzblrJ[zkU{qXp6AɯN zs _jY/2 BV 9E_O !$@i-anO=tybeDpUm9:)'1.Qش K IOq|Qk5aGI y`v~$@8R1ɏ"{T{9Ui= 2`_P~V*~HU2(UP(SS z S"P LH mR 䞞n ka%\V#-(_=VO&_&0Rk-_zr˦ Avڠrm%pUt,:<|S}$#l\D5xk\%#Bzc؞oP9FJv7Ve,dP3aɊ$HVC7akuϘ( V,u9*+z~2YL1.)le.\ F Hh=z8 ۋW *;|-X˽z8profߏ۔׶wX{ľ3W^~yqEKr4kup'?iplή%CwWL[\pmLa)2!'>t&qu6p8 &Ռ! bE tK`F  GO*:黶eB,΄T?) RJ%?=;7;~%?QTʷ>:sKP->fi}mb1O텀'IeV bXsΌhc2^O/ex5Sݑn[6 Cw& c__'iۮt,\NVhHt#N][cKw۴ntՏp~+jEK'+i /OOH-w4\[9 y{iU^ir G?O& Y I@A yq5SS$d<O8{?1Q9}h#Z6用e K]K{Vۀ`)\erH1̃=k4k`آ䔷el uz+/mg1WN Im؀8cb9ʹr?X#}oF~w;6Ո2H1<(4cHPߢa!5N$b4FDE?v$I{usUM-'O?& GV*p bgE&B_~(`v0;RPty Zq@s}=y9}$d`;)Am{*}}6Ž3)m >i"n3)Y3˧ U61,G!xR.^dǓ #d玜f:zBI~ '⹵A}`*{֞b 3*5%-h ̄0@SfX*?\@8-O ,l;&R۽V# Rt~ix}O{8dcZ'm[F3&Ĥ !>P/)<Bd@PØ,ʲ#+cedmUgO>(#$j7c4–gK%+mޥ~j3u]2#^àUy<`0.ܘmxn8/h~>H=>ywL{-q1, f9T2,Hhk|xlY'[Ao ;V؂?riH?=A#CJOBť')δny$^-4>TK8ǥOڣڏ>;Sa1@YmjDGe'SIGx7 Eq>c-S"1vǯoTO˰#Eڟ&%tO9|V|@h5(&~D|?'(|[^)$MP ЀgNH41q{xdfHHP(pP&j?\WJjTCR8HfF}oُfZt욙6GNu@Yi.K@D/F_E`)Nz*3P>kO~O!sa]kHrkOheGF;g RRF~aK(ʈa.C1 uz߼͑&1 Påf`laƛDS"1wC2uX$ZDs4$[fz9`㷇:HIxN8s(+7ԭ<k 3ܩ꾸UE=k-' b,[t28nop_zk¥q93f]`.M#=ӴP6>* p o>5~Rks:spŌxKW`$=ih<DMt=4XVJ/c(Ch{zZ`2mDžCL*eSa,~gz`ڬZѨc8WNt90h [58lmZ]LQA/'ЫIcNHώ*\9x=䓖<`G\<1t ¶-ɔW=4e0>xqȋ> W2v%r&x$/:^5'C 3%a5^lQxӞv K7QOqV' b9CICv5s,K!#2&&w͐2sl&Gx1De@>E7DMQL(NZ^& F #_1bDa˕ K򮰚UjsK_S*{gG4!a-:k[%O?½ H\o; 2<Jr0 5]X8cZ`Ԍ>rz%(KGZf_a?t]CLɴ}U!s'5dFА fk 9gz% *7Ѧ21Y}v@S ^/EOCS;ԝY}+2Ø~8njVdx++Z)؎qUxQ𥤧͎4:|ad K+Ǭ; .A#o0.4&'bߘqt*^9/B?aK}?$8̳ (f1KhH0%w/b3i!9 ;8,myKDiV@C !њ엻rş^xES㾰&FȀ F[ h% >`+?P\;Mf.T(]'reeW8oj'141qEެ}|t-Z(o8X&unjZ[dPT:'*״s{#qO^4@Ȯk^;p|\KgwXՎڶ OmD3ж ɆmIU&Ed2aڀ}ZA@7WM`Zѝ nqMˣ| foh'gIM(9WKuvi%k 4l &8+ ,WY{$D8ai Ks?t\_U!)-r L0*&6)oD>rzԕOFh\Fo.dU~<dzoRxTU#9Gtgs1Wu .V( XAyJ J93QS )M{F)u # L{eBgXfO\;;*3. }Nz]̦ Y=/zaay$4QCNa|n_5# 0ng[Qo~'e񞨃}!fЎ>Y`㵇!t0e` |h]uZvޕq2g`| *> k %y>_9é`Ub@gd`wp@lw [2OvSPEP8|&5h:EB1ʡs8"L; jvnei*ӺȂ:ʡk=H)z,0˟xrd#ٙ405<+6 g_Pȥ#$|i1 Q-:kz!WplBPlʾ!uQ ?]`|;x,aao#ا/RMq;F-!9 VȤ? 6i;B"D.#>cAj Bwl |4mi>9=52Ibw 1\G XZ˲4C[!3iΜyYO޹79dmCoۙgyƬE/)O$OEj{.|U-R</'{ 'c` F6:\vg1_>?TC׆!l;G欷ϧYP=u79ڠEu9ȣ뼲nYChr̿ ٿCz ć0!A$2?̀c\{4202TqוkDv% 1\7a!$>'Fae/c C[ 9P{ܧ~2t}[x9s~6ѴM5ke9Ú pZ޴Ҍ #hh:xָ;ҭ|>o˜)eLoM5%gSh\Cf;2%Fص4qB> /t& ÎW;B aaКǧx&ʫQy%›FFf5O'YJ g;uDDCփ"û}yc#&[|*Ipg*.}  d# v/C's=4Q6O]J /ሲYǾ%O38%YM]W{ 2?!>7gfqʜ&`xf-areb%m p\ߌ. ge&m p}C3rrԝmp4Hh1=_Tt60uN) HJ?Gݎ &'2o߄K-Q1Up0fZ}+dz,y o"y(߿a=e/tRXA4e I0zRzyU3ѻz@ .P3x (ȥ~rL+j8,Afq\+Ux[<@2( V,XuEC.Twz/Lj-ek38 1 kP,"C/>M)Rk^<{3 MRslGm=tn?KQӟbi&˓kW0W[FxO[aX,uWbgypޝ̙68?`yW𴄇ҟ-OҲD3wL.ZdO3q6{1F Cڎ{(,_9S<)r[T!;qD?Ng.3dr"?1ȅʣmfRЂ"EC\G5Vf8h#V sցz"b1!`(d ;xoMJ"5UoGj )p(FBV P!'YnHPKQ(n,i'e\b/ *|3 vnMuu!tBpET5 JgMVD9VtI^0cZl:AoqS a~25gs&N|%Qp'[Ti*QEz*K:%4_ h<9@jd>˃d4dpԟR0H뙺A%|Gŵq[*AXW6g"uXg P'nܵ|PkShb:}~k3Pկ"K_s=\hx^Og+)gBi&5En4 )l ΁[ѐ4-'T Nuse'hy4ljx6Gnڅ-L3<&Ѹ pwn[jnZb@[?㢶$z;IK( y4o˾;n 0l̯"!|vxHkqa(sS”oX 3 jcWV Og4U1|qDϙ^aHuꪏ0>@Z h,ndlZX&$8=d62:>~*"1,ԇo~#|D>PrHNN4(zaWY{VtEVGfo{ڨYk*r]㥊WgGz黡lo=oߟ܌pN^Ď!38._?v^ASd{N%? 'B3qDŽ$zCDIvО~-T'-Ki7'(V|N!./rwvzɗH*n c/[mѷ<1N-2C@Ve"zbYL49YM]5h ^#'9͙C? R?TȪ+X wj![v z#7`:U6s +`s!AFn\ 8ftׇ$?y$}-xMҗ,Y C}0Cl:J (0 ό1vc^Q \l0̓~JmpR e˯jxesFf%@VgXxZwBep|˯Q<+<38[pO MgqrB5 ]Onѯd1ψqbpQp{s?207}2ih?wsP[~| UO@2 ɓ~ik]ZI9B fm˗VpuZLn4\R3^zY+]u+CA#R܌S57Vaݴpg$nzUI4 -2؆ 3:5(~PxIn"Y:Ro޷M䀅cQ?%]CKh;%%&eT#alGkquPɯ=VmsYضcz^6m[1p-1N?+ӼbC@dxF#ݙ?/Y_}~_Wm6?Z:-HZiΊG0?-gg8`Ci9> mokA bIyz ҞuwHzI)`l( WdJeԱ6N^hQ9eq?~ײ2Y|6H}k1[^11bL0 iq@d|U"9"L&kUG{w̬*#"'5V153'#lg K B ,cAp<q2؂mxQ:=B<5sGhg)PUx١*a}÷}D\rˣ_Xq׈9Hr>)<7~ j;zPb0excġ-tJJ0>wkIy/M_%zԾ:i=1x#_2I14~kl a3(}eTD8!CM]Y|d~ieH8ي7G2/H&Zᵖa̴y,ζ͢߼N7i gY<^G=Y]VYFkVHl8S~ئנ\pH9|3C}J?2P8_9=lӠiz>yO/5:>X]{?rH%}Vp;ܛ}i6O-=OqIs1%;HT&;ce82DOi6tpu:s?=Rz`'45~>Q582C;ʕZyVZT$yY83{?r⡋iቻHҴ}}E=yN냑#-0# ւcn3I5P~;z3|{hI WkMg͋{PK>6ie&ୌnɶпYp<崗ꭍG*[綨_y< mm78wR?MXG u JL9krI^sks&񭢸꽳0rNJ{ OkbΌ}y}r!|Z)CDf cC9 O DxTA)^Ry@IDAT,Y9 ˋ}sOpX?D~͝q" @So>2x3psęZR6kϼCfZFtz/nl;?> ̩}h9r,>;p#1PYFMK%P,k=?L:[/i/ȋ p㓴[m >8FTiwH/, vZ19G&zՕǮRo7߳S94 ]+MZ$tJErW ^׀Q'R'aN$#9V]DL4> 9aV8h5c"-fQs>,pƂK{fħRÖm0}汈[1Bpw,ofgTDn]W}]Oөe6z9f ےW|wzٛ^2lvW8f+;1ha(7'g>q$-O=Cj|Q 5QL 4@KLQQPĄ5G(5Zu :BOOi Xe|FK:btZp0tց6Y_&áo [#*s4WW4Dn~РQgm%b8Чṕ/;[{6(ٔkuExG]dt^Ke+cZebFt) G[M$ ILH/e]2L1=8A!8*I|Ћ@ WuFǖ)P[ͽd}~󈴚 =).j8o2 .bg6 ('+).wGFjzaZu`rFEXuֻ/@H86%瑹Qf74k$i@^圇xbx/vu)ZhZh#W9ս* =c _CF^4x7WGћ;c$ py`3.\WL|Z01bJ0Lyz1 C? y +A۔-sXhF-GQ!-.1y߭(wjBlCpKK1B*j U =_\N ~V/& *q |>oFA]c¡+1ЙCP&ejG10DĨ .V^LU9 $G _-!K>&pTwE0q6bvKpi'\G6T)?lg*`e 0-2 0sNXQzի]mzN >4j}l.x?LhO?I>AG]aƥԳ Amv"TX~W}LœU:pLYieFo3h_dF?đh+^-q} !`ШD/?=Ѓfx!]%9e(9! 8{L˹@T]ld!8ŰB'}w(YuCw&歡{.ooY̟q-Ȁ ^ !AK[^Os>Ƈ!h{gmƈiBH]\pg6#לWd^r﯃82?3t\!Y,H ^Ó)7aϲC !-qʹ9lk-%.## ?=1wrlsGu Mbڪ\x69o`9@?q8\H%ʾ#'29' ]8lb!]6@3oX0o͵woCax΁B!` `1NS !U$?"f(eq0zBG]ԝ+Ė~`s`bev eݶ 10B%f ^vt)N‰36=X(W\e?'edV&Rg.ɟš*z@/IazUo3~\TvH;}opHq-&d^|є)C'O D?ks ~7RZ ;!Ok%ҥa'Dg;8Rv|AodgbЦ)8j]-IrEg ,_YCBqRŶ N OY:av8RYwűh8s:mU*Aʆ5 رh#3N DZ+<&ޏAbɠ^Ԗ#u:%Gl՛6*yи6hblBn.w3xHD)'FYpa5;KCvĈwo ߠ} RFNGd+-e]kv;c2-k'-sto+jڃ[zg4IkP;ٵùd?jm[2>ן]Q{y0*fПDh99QqD1wdjCebA@1^ ]^E^q 5P O Iy#fpj|_1ki.yzA wnR`<)sy 3߆ZK̞Q\zUem;?;suw9&WO(ʅ} M.Kn̻Ï?M*[tͤ$7_Aa8pR}Õ+:/CI3ɕ?χK}oW>k6)+>oMmC].]k)}VfS'8豅 ֓@vSC;Y#j^P6V`lZj_y d ]El]e ુ!Я@<)xXkQ^PSFcO`/%:ps} "(>2A_1((%pi/e 7f]F 2!eEq1ӵA\N J/dP`2x k=O8to~B *C7o3b#@s۾ mۃ~l 7Pkj Hmf?1L?W72*gmq!c 2I vL%\-PN6P՗7gJ6t dPpM*ܳK9SgLvZ=>/S=m6kG^36fAߧ: 8"GJKMXII*ݩ᳁@lԈvPj\g(]?Nr}dp}t`A޴|NO)X10vx3DjeY?T;n>9_y5y@ w})b}04ge зJ[Bd!8@3]yUМ:lts.4"g>@x$&qXS،ds=ȓ`#DG芠2[2 GܳOh|)ާC`).ܶPrJP3@,@L#0#l۱)mS_2[>oQG tcfd 1W#?pgOjpGN}Iig 0 M.A^Qkz>GCrR?L~RϦJSH9tI~Cs4+Qx'4VIOJ \U'eA*ѰśtG?y`6U$w5~iWCFX?Esh0 ;akeuE#nBb_[R9=_GltÀ]ƒN9mmhd_?d ^.|p% \ dW[Ǡ< { \nS64h8Q`Gia lWdz>x+V?8`+@js =@l nItKOm^eEϣ[K.? ucJ˗s1nYʈ~БV>B2RzO]O 4 /x{wJ ~ͥcJԶal!w&/]d}n@{z> p%~T668ͬt-%GʫFDJ=/erOEN౬?VsNUS>tFEo),P2ϻo,ZW+XM!V풉?vwdk\+g|A_#gWh;< qG7jmE^YyȚ[8Kzy|}0bm4 }1fgSҌ;J#2 FPr)uB #"RS`#z2\.Bs- xc-dT|Ǡl֛ط9 $̬e֐S2=9-CG58B3!* T OWB>1cG%TKci+D(miE7Ơ2MPdrMCwGE[{zdT΂LLxрMfLƫjtHp͓C,ȆЧpω=XwV iϿ %g@#H[[4v~{V98(Z$岛Ol>Ro&yVI]Nqg8W8TȼUrh峱H+' ]Ywu^ߣȰс)r}4"*$ ygL!T2@J]RvVK(z i h2CI1ǣh=Ʊ604ec ,As~kY~`t ܃58cWw=Oţ'mt@ *[nm=ӖY[ܞl&s6IaPC gMrkIpJ?oyPfUaHOՠjiFMEäxC!4i34&4FieAdZJ-r[ Ĺ}/uWؔ9Ǡ,p  2; `JV`5XCx"_& vПj -r[<%{e_&4}ʈ=E![Y-~7X=5KPeGs#Zm}0-΀`?óH IU/ƠuV8;\ËCoG=k g8h9{k$4} YF.]q!֬  G6&bah}YE'\7]ݑպV*!QfV)y7x]stڅWO_ZYA:3pDxrOx<VIv٭(*rqP)eLk*k%_4:h<,W/Z+H>C, Q`ǿ K~U"tH G3$30,`( a#; |0aBdۈsayy1yCC;C;10xF(b@P Q ^-m Iᰉ3aa*-Dy>#E!"9GX̝)9 X}qZļSKAZ~+ Թpܙ?3lvl'cx M=[߇/U/"(_0\>o?)w;\=Cg-t=FXiU Trјs\:Xi9_4G]dd_")^Z奴yKkI+ <IHd'pɳWeD a-^) KlK`~0`GGޅ+]ˁ<,qlC7nWWxue' ]/4A~RS-!|ڠgѴǖsn`.z J{n }9u>mb|^ Y};V.Vyv]^8O|܇``5pp! $/Rg+rP5>4׬O{0x°wۨ^+֡l@Ѓ|ڐF#ot䴒^`x[c,ڃ1X2x itf`#GD$3w%)^llW=;Co{Mq{KCbd ݧ^` Zdm&J|u.4qb3l0@t!X"Ȟʧp(/t5N66,!7hz*!UjKiq e#(]{8÷' rKi_$u~IvTOm=Gmu5Sj7cį㓶 9ZnÂ&0AL7`383İHYc0f1J.anIip"D05Y<Bi΋L8|l+G=ϘV>$x:jJFzuRpU?sg=V:KxᛠI; & zq]3<r*Eم=2 iKүCKI=9?!1<{ ܼOo4lQ])pmIo?/m Yl!\HBCdQYRFh%X@#-aK-}GC8+䱂A]f} ig0qA?ߓ{]VL <=><bNrM/i 7 dGï+W_'v\^|ΟM_د4lM_́k`T? XQBCA#M>H_} _b凴t٫j88EaSv!cQ|?#z:w[^,|c6{3/PaGSА#7iKbc3$=JlNYI-`oғ$̎sdi/77OҢ.zTwc^n뫲G$J,_Z<ژUqjNVzc pUtwZ_<7h)O5dveSk^Cjv /gu/scc#Pܞ/#~a+dM 2J4E ØV hF9c 7ua0rlFUW0gsrP}2l]# eWiK!`F807X!"&b/Ǩ&p"ƢD3!ZH<_]4\Lj3LJӒD._9.BN>P8uQ ~ǥnzk7az0|\z ? 7<FwMJUJF:3#A6L҇Жފi=vKږ^3˒orUA2@qpzf d3נ]?8Gy횜Pz%rB~)M{}vP#O\ _: @;-#/&KX^mɧFߐ\˥94ya{K/VX x yB\%HNG[ Ù;ď=> tb+G7!1p((,H=Gc$v~P GHi3q2=}M~_!ӹP{;u$b%SV1Ҋ+[//IȽPT0T,oh:m6 >+qeTKoqp)ѳ'=W 5ҍ!A_3')2MNpWVZ> oG%nÄznO_ʮ\$;2:,{&0;g[ͨyt"hOSH7t 1kS"AL9 ywQZY>:epWs.60Pط*1Xf0@5'1ֺThn&^xdr+m"\"wPVA:9&j6:5n[ Fpn# X(uڨz-vYn#Sޮ;\N m?mW]_iБ`'ӹ-7i4^Ŷe'~2Sڿ$d-s7:E);)&c$d>e.Yдr}<:GEe.Z6A gZ LuYڶm)I68TYO9nVaI%4\fTX0|%f*` y`AֆrH<%gȵV)~3H/]*%%DF_݀Fbk1Fn*f1młֿM ((VX2>Q5՝iWZ[HYmJ?TBQ]PZ{ƺݑ<L(-x~ #uώ ǡc=޽׍2[P29y]y7|>-!d@NEԥuF[`@Hqd E䏒KYFi-rPCf2~ .Z LBK2~֩ne讞FNsVq^-{,_t MQ7m)#;C{fu\C#&00ٔgHQYHR \\_Ub|Ĉ Y41!j5*\}43~L},@b,0r!⽦$)°eҋ@; A. ՟|zP~gC B(0 _ cƩ#KX6x: }`,>+0_ЗǡwgYvK n5qMds>PZC ?%/''u?Ciچ< ̓APVJkeM| ?7{8(ůhV¬x#}^ ga\\X| u`غN>~7g6'%\i'Gj7x˙CGMԺ!ϥ+l7o4Y[w[tqݾO`Y6p@sdgvo X+"!ǟ#V `"/Ķ(bRbAJ, g4a+vԉnNg*  =aVANlxo3x0l'd2R+Աeo˒ rݧ¨[y!$I"\`wiMfӂ:8}uވg 6ƛBG-p ~|vk1,9G7rP ~6qh4>|Rbi# r&.@g9wb缕Mh 07oZ>敇L'C PVL\0 [*%\Y&Q8PζOƉQ/E0f:{:*Slr;!|9g7$J59BECz0tRY ݮs?dT/2-/_:%t.-6B6M1gue~ufwkh dm#>'5rw|8/ݴ 4߇8~v|7Y&$~\5,_` e{ n43iׇFOKqաsu>fͫt|迺]s-pk[=S2 Z@4cx{\J"{""3rsN;ɧ҃l]5^ T[Y?u-'%`Mag'qWouRW&d,i5eoF}}2b7s  2zhSJ{|;lͷ`!0#E>e~1OURN0ATNb󴩝+rK V}3+\kPa`u]ז ]<csl!g_;G2K|=x>>XDȺ5tiMw~:dn"+'fXj`(7n/CKL >HgsZ@J[f-C򺊲 Fquco-A{(e ȭؓu_Ϛ#y0U`Tp˶~̑کRO e_R>W C]fu@,Y gNΛN_x}`@jAIZFeJgYf VԬ)kM d~QxeJyucfup]\wB]E|%WMwsڟvUhýY ck0سB _)aӻ3nlNjcxn/ȬM2+j,f]qۥjnSyNOq+/s:Gn3suz.qr(yU vvoAɖ+ג46+s%7>zӮԟ |LuϻySmx smrr KlWMyO{t"/޶VQg@?#TeY<~U#] +HZd f 4Gvω) v|QG;+/Ϋ%C45l\\n}h+Tu-V;۴}gzQ &c\eI &,"F?OCWԙ}]ѮBz$נ@IDATbh 21 b\أa}`Y@cg\NJq1+Ŋ1ѧTաUޖ/~4LPwMbێ|C`du@o8pwpdeb&/H;0=qlqأ f|Ue':~XqzxdeΫC0 IBq['t$ .W3n uvR[t]gmə}2,+fC7~g fAwy~(GS}lnqN))aOз ~tlt`_˛= cx1T=|0foeLj0΄ ȠCA`!{Ac,M2R`d ܊` ߈/܇0*q /zpM*`'HNțV3ebvT'!E 1#/RӀ8;$IfKRSzp[q 2eX[d7a89;1fCixKy.ġKePdC贛F  ֒^lLέ |=^yއȱ>8)CՉ HHqS:YzV#sAU.\myɁ}42ߛ7*P*87xnr/O+,vENnG YZP~鬾Nhc 0Ng<qt`A&0-nΖڶHmy꼈?oJOWpm2VOq5 lM>zECcx$44gz&CI*CJ? qЀ׬I{8 ԭ]zU}оhҭgh,G.&I:קدihZ}ޝաrġIN&k]{>cMOl|4Yr+ EmX}Fr?GcbQZ d,ag~ju~r'(1PK s.Wpy*է0&eKeϱJ_ʬ),(01f\K+?UxO ŰjW˜exA[arq@MP't2JH>3,Gcx&xx z6jCĄ϶oL㛓G~C-oq~{Q<#WnsZ p2Ru/S]u@%FZ[\pMǘk8;uDz䐨LQOڴOy#(YK7|Zax[mY}dcvd9L'yQqR3T6ǫf >,=45YA A?5'c׸qȀ)/F{q6N`0|UhДzC)g<ze4 5ϋ3Q~ڒ>8tڊ49"S8D?>5n|Ch2D/t>d^j=g!Wzwgὰ9;˺_u8-do[98)iCZ 7ydnCq8 ߊmR)3z  d,fdlXc.26O+zK·wa( o. !,}Na(Kq2ZJ΂3ͫ4 e0[sY1E"O'9@<*P`Ƿy#I mmmcLBM_k\0FO(; G-m}ap&l0̴xjeCz0j͋vLy>e^,SۓIu!f2 h F۠#Cf$de#ͺ}٧Q]{ȋ'ԩEx[[2,hn!3z~8*wCqb*Lж }Gc^Ƌv&|hM3+X>i",iLP}$gvAo;׃o4W 62^{!mqϽx?=%^^׌B 6 }EJ}7@F{7 іz |6a7|J.ݳ ڢ%p^}`ħ3XGxaGp_Nygy` np kYh譛`]h6!,b2Kx E33"tzՉ&j9Jv5:+ﮘmʣXrlJ.uU=qk7ӧgˉIzgD`']UR®= -|(Ӫ.E%LD?R;~id8QNjx-9>2D/{GVI0}kʰ9gփ0A&eS^ xuŌJ=Ie\my)O[VsefӹtnkYjyyC 'C<Ɖ׳h\3s"YOijg+nu1\" ?vCƬg25—/&+:S{ӫƥn̰`o[# XHf?m dt &-]Nno2A!|_udɒ#KsD s"岞\pOnzCRfS""x>3~㺇;D\u1735Yu8'mqD.Q ҵHLʂ-[w21U  L5X }ԄC{{F=`(ӴӍc@`3J/&d(.of{cH3+M/4H|#7nH+|:{w ^we&phuFی>^ 7%sGbے1zsJDlcH2*`ϻJdJ& n.9L6,: ێ&0'ytJ6J4̃Q|ejL-v݊Cij9jV*3 ߀x\,g7(UsCgϴ}GLV5hpA hp \M'"Es"F8eB.LO)6M#[FÊwһҢLVɥqE+=vp`ˋ/6՝^HLxEj A7y);' L嶼>'t;|'b`M4H  cipKe^4<'GaYY~˯/45٤34(|gn}:bFc.NN@ĚAi#`qv#ё7O^`U~Y]xǿN1(AL(Ӯx7JZMgx|ȼM3\X:L+nOyIarCy)u.Gx ǧ /]T,g? F-37aWcڃc`RG#כ#oZŇafxٯN$A -G3RCoĠUrU*y c|LeՓKA}3%E[rh*VDܛ(0̔L/oUP3vJ!#~HةLjGpc Dn0*;bVҵ(ᔹ*fikvm7[r)u՞.ƀ52 atG0$$ci=:f'|ᖻ,[c/^N-ò:\`23MԢJ蕡ǭ\@>J }I F:8jl,w`.m BD3~AfqM 8aiiD)\I-9\rZy9X3U 9m*:\wwpR'<|V{@(cu?Srp}lG?M2jHB#qųڗmL%My+Wf)g҇M .SfGJ(|, 4|mbI7A@⫁ NNl!.HqGMN={foEGҏlV\LOؾ[v vι>h,IU Cꯋ]Kc<eưMߍߦg۩.u`7G“7 * 60b n1rfضقړ89rEjJ3ʇ&,eG[ $+N$W`%1o`L9j#崜~Dd"T'pXGF߼{l2FL]cqv4C uo&>G:JKi}V˗7 D;Zp=^1]T'#ypȾ^MG|t)ZuotUPrK%!0pxӜ*9,^+S| ofѳq} _Q^''`5urT'-UउN2w2)'6O8%3c/gRpY 5!fW/2cC[x!N6^R\)_0Ъ;{2?2h_?/~tJ#w%vǭ7/WÙ}FF8AcA9~!yW&쾲f%ڳf\^A /HZ.hJV_o6‡ K+ ٦ԕ^Tt.+w(vL7|dwd{{2Tvb&c9⣮~+C_Z{2)c?ԷoT/ܚ VGгxC,[kVm3>gvc©u&?~?kW}Ň\k!:^0+J7ZC[.eo傿c:oorow(QQQ`XQB)2;}(-]j"aZO ԠL) Hj UJmq^{*:W5Jh3eOwaT(kQDAeD?M< n.%g;նt+-[.F^ Wf7Ao@0$W58%3Ɋ  ߙP~Ԧ9f9uʕm-5i[#kn&'HxU}Kɟ<;u)KKJ+Ȉ@&KRͩTqQ&0~\`Vg TYp#~ԞTVyUęb_19T]1 BAb/&^0d4L!SYp]|̑, buS-QL3GK]8VV洤)0aھQ@(O>uZ.R&pjbn`^lt[[c ܾۡ׋FSV v[#9@סtX</Z4uo'~~x$pNyul?{;_!+_9#?@cڇ?D˘Uoj>Ԧl#[p0w̵LO#lYi5eۃmj6r-*4o@C)UɾT҆eΉDFLLz̊t7Jo%P:3Kj)^eGA؃P>הI -3'8xmb . s{U! SI+ǕA«Vɩbm p@B#N$%7s%x%@>NɴH9Hҥ9*ysr'tdˬN[NypRd.2tbwv_kCJ`Q.8[N5s?LI@]gtb_d2cKaR_y  f e( %U[<\>~1=x?=Y[#Տk•#B7b6.LAHă$q$ST j“~4X1w pe /A&:Z(ЕSW ~DuÝ'E6Zs MӶ>ҷuLs$u>_x߷$r1aޫT!?$TNH L Z!g,CC%Qr3SU#_^Is˿)<|Q_sRK}sr#cV ŭ.=+;ܣA]%m2Oq"EM?#f]cymr|}.SSC^ mv$5ZXp|:^z0?^vqݵ>uKqu)6glIʪr@Hv%±HmgCC^11HzIɴHw%ɏV’{T:Jhwsɕ09_0m8EWȀz2u)oDDn%8s-odbcG4aG7IӔ 겹Ozv] &=&vWQ~?4`whCݴJ|\}0T97"!QYJ JCi7BLARVȷyQ`ڂPn6"{ݩfG(s9`ǑAԍs2[aՎds2}~ g63|+(@kc=rjk+l6}J5S7;E0PLbܢATG59wv(呸a&J(gmkZsdRŌ&Wxk䭜ܼikIDR)wÿd!XoZ"ۙ 6Fxʃn?:KI{ M>Qie3{:;z%)2"YVk$򃏴1ӻvo2Y+F.xV gKs׷֖#;C ۶"0cB;3 :v JI:/\ы]p@>tL30C÷ 1@M'ǜ'ga(ߢ$lq{QhPOO1ipLT䷰}CK)/J) O[%GӒ ̷rtIcַ,s"|S D>9B,)ӓysnR3U̘wpTNq7/`a6. Q89b>D9>7Cu K82wdr1dJB*/7JmZ+Y鲖s :}1krפ }뼔vl y? ھӓ`!Dpn/Lڂ%gXVQҶƌ (c MVwB_So@YR5H,_log f&vnO6]<iV+YT0TIe;CdFx,{ײ$w5596>^Q+8£&7u'rqh{W)W/]}6:+7V+#meU1WE+50;dO LmNڧ{gV$&2¿3ƅj҃)@DwӏWsv[EF/kEb^m$w^ʉueȩҧ%rO?Ύ*ObzF&idA>vx%z+9ICs][P>dHS{궹\R{gx롼24e뿀dSKc+'o؉q౧7~q8ab]Ź5*25P?2~91s@bc'-ϑx䋡i3QI!ϙ?8Gtc\J8tE!UmwldG2mq ߗ.5)4Q'5u2u chW.=jsf+sczu @ϵQgOdsZ Gjr,|Z+zsLrۅo|Z 2IO%5W)MʯrW¿V`O-^no[ ,~Ø_97=i@38--]K."E)3Vl ҮTܾ}AsLq4_Az~W/KeޣE^2G=saơvǓo՗r:BrW]>>GʽHhޙGKѐ"Ţ>" d'ޘ,2=Hr)*R tĪ2ElǪ>!J֫0`$2E*jLE\ɗmDI^42X:Bh79sEʑi'֍Rb-FK Z54 1|vʢ kW <6|lGAµ$xcN7{f*"S/_vŨ)!Υo/SX]7SJFV-\}PFz l[q<U$mrY>E.m$0x 0RS9$1[B' 7j9f(*/:R7%A 2N3NJ1MpF!Wfn9y_LLCkPPw; ͫ>>XeH_N_D(i.tZ/*ڰƃ&PՐJ=k)64vy;{2Zd8,?[ipUJu|>LLf^,m Pل~28.gڼS7|jņCݑAל^?:l=-Q-cmyg$&rzvp_@f۽ `ץ~\v}6ɽzR}=|k. )W,&`\BFPweV;~ x=VM<7V 6}qkَVgkS~Q`C8lt2]LJNA]!?:"<_l1>?aY1DIp !P D@Łl{|h ^>z0y<?ǸOLEճ@i);9FL861L=I+4xw'klT}Rwb/IÅ'f8j]y X&i/Paɬgu%t0 z&BBBGݿ3gǁ(©Nٜ晶!eBqFOSnWZZ`(\s=ϻ̠It>$䃄3<;ųvHn hnѼvԋ+܇3&c?$T~~V0 )vL0`Up/<Cut)m#<drc=s8G"Zt%m8!smh_i90;v|p06A#sRR9Gi1nJ(};r_3zVG L}qbP`1ǐ^(B:Q7:r&xV{9v0x3=g>fhjx^qݫ\CR] ?ƞUP'L؟Oy e ]RlG&h?L9I *’ ےO3Wj0 tZ9:A9:0`{^X¿`9R'jFdÞH-Ξ%\Zzn̄;Y!Z^gI 5W4OuIdp^&U~cw+j_;9ϵ`IwdS)}YHo~{M4ڟQ1EcT4U:HvFq ?~! ʭR<7BKye˷qH_SPӗK &t9QF%#luppp\+~DDaWr(bHDB9x^m 8 )7ʾ_!j V9RDZ}FW@o[sƖo DqdtROЀܶS: tMwƚhmKe\IN׌/E3D1FZQ.xshd2\ l|{Ic fe_@%n/K1W8`.0 Q6dF6Q=銘ĹT^-Dž}ǡY*?>zVjy٤F'75 䛬8i1 L N"GdSfEA°2T(ߑ960-{nѤc{XE/"v;x!Y Jڍ8{d#A6=go=ad;k[mv<{k?Ns\/_[y:*ߟctrxlN ܃?pgSM^7lg%ѵ Xn_yhA\=dAU6^ ^ʹk#֣0t =nxaBV|vBՇ PkMóh|I|&r"M$[%KvHk0k>CZJ\;o÷ec=ItiQ= @'K|+~id+ <6ή]s"m3h.Now)s_E$o:Gc cQ8\j]1=YǴG+ۼG#v9>CA8Rc)-HBAD 8/;|=[7gH$r&Vh_뀇cD>ɴg&@fɞI}[ֹe,uMg3 WiUB2NxYX)C~/D# <[m@ Ї~0eGXwAt0 $#@VRg)8aZ'REH&e(Fw_罾CZ]uJ[|fuE&ty ın+ܖ=:Ms/"J9KHQ)Oκh*9hsyxοکsxN+!sĈv=M>4.n{WR:<1FNiVٗR;׻*>msCGv[x8$qZg"d< ^഼fWvXTq,ws7R%=j|^Ӏ_<?x^vѕ= 5k6VTyHMbQrRF0U˞^ _n~ǷN(Վ& whx2YKM1$8«Zr_r Oɠ 9E9pTN;&j]~|3 ӫc@ɦq\+yzw$9̕]8^Z~G2ڇwhKş!m#7|>_!i R)d,)bM'9.~ȷoL(Ɩx COmן<>\;myv98xU1MLJ_:%:SRw8|Qo_ ʅN?C$d_˾_jЗ٤l&ޡLoqM{tkϙ YH,ZPǘ:1QQ#&uF.9`eBJH:+>}=Ze}͟2QFy*Sվ3R]-u~}rd/ܿm[v[Kχa@0{>$}iݥ7v8{ZTtrm $m'eo|To i9 U?!R3^E"qtLҗmӯ{5jU&u_j9U_qoλ6<"d(nJ!fԷʎ YdikQ]E; >ϦǐGCI]`'Gp g x(C>Եj gm gۇ^)Qw M#.-Nr{힃[ p!r#ټ@6Nf䛴H!s7$0ӕ1Az6KG,Q׬ΔP1}6|o׫h؏Uk+2Vз ~jd$"9t|Z+thW VY|N$+ycZ'ik /~Ľ%侲^oF.(} zX,TV,}>Se /'FOS_RN<~F}x'SYKSoNc> 1Tdo[\DNQ=ՙ; ̶AjJQD5F%mDgδZxz$7X!”c_:kz!ci&O_c=ڤIW]KRcbnyt?Rve;-[ԃ^Nge> Ō>爖c9y:s[ga/1?;a=N_E9t;Lt5I^rL'ԮvIE5pr"OޯdDYuBIY:' sԦE߄Ÿ >c`p䵞G#ȼ[ӿ& 6cޞڜ8^&rǘ(m/L2ى^OsVҽ떫8 sd)i9_ /n:u BK'K]x4cLCҹNVLzt)2ϖ%,Va;V8B+8)= ـ0L83Aa<^,|C»6C \oJ$$x俓3fw9eAJA^}|}@uMœm훽쮽$.*|"laЎ!v)8ZI;|unc<C28r [$_ p[JlB]l2N'Qkx9 ~/77/Dߘ#/]Lf*M|0óvX௤LP˟) BD޵ 7;^SL9w] nF6PBJ5#A]j߹<0@!/p^x2HpB2ba#Ѐ=S_՜ + &1؜QBIib | $A+Ba^*{ϢD Yz` E.ZG)CEZiqBڢVQ yB+~gČ{V~pf1(AŁ?8~Zor18,esU"K=BW*^K5}~̤RuQOݜ[ڶU#'̚w9=[,:p9nk~!JXhnѼ\pD}cqNlUts'8vS +c)A踍4CIl{6&UՋyGNe*v28Ghԩ6G:ٴ<:m L\5~1tĮhD <@;hRfcvLc$Ʀv0 V>D?൲A,-Qz0)÷I:rB љftx# vM ґ'WUO0'#2y$/\OJR$680[;vvK] FZ_0F^sM3|,mN{t~:yk3P65萀6P0 4σKmɜ8 8W)8mx#[~0J 8@ҋ"~J|4_\=Fo {r[a | ŊB8,DyW8?W"Dt*(IآXD$O E^c; #/R>&,#B?qALh`/{iCCG4tYxP3eqf:gkP2xI=1ܝ'mۖL;3Qo9l u.5|1KN ]r W*Z8ͪ>OօIlGң<03XZLoU:r+W}9WL#V08|x_q9_Do=V*GG%7Hًvߍ$Y7x\Z~ m"[%GgŲ[fa}KnގX;=WꟽU>r)\n^vT=+/lگ=~0}t0nqPxsGHfEJq#U8*@{·_ŭWURi?#E ` (HQ~`GYGDOͳQfV7 `E3bE VRG)_q*_gϔ,Z"v{)jWYQyB? BmR_LFHq43i&8v j gNjלKt@gd79.?M[IW׬027: 6AT9_Zk3:N;kZ-@Cĩ&%V* $wW2x?:q4ߝՒI?ZBGF仦9SE:rz JUvBʹ+Xzhp.׮mxIny[ۮ#zre'M+ޢ2L&L(20:׎͌dbi9ty x=孑w_(djL LY/|,Yg4E I>: dM ?W6*'X|AMksd" +;b <2Vvz=|pl]>u]<勲}. lyO!2^#I |sx^Xr5B%~2Q(Zڙw2z/b<׊"='|T8 L+<R~w!m}e+L 909C1 "a$ta-kF@<ؖ#I!P/Qg7R [f^;qx_*,Hu;:yZ `6qN%HIQفQßK=5 %<(M1:kD啞uHY퍧T`mKs~Eٞ۷sppq9Tg #v_|LJFiZI'Hq bL+FZnj9]5m09\0܇>3^〙K'0R9c)hOF+% 2.}fvu+Y:틡% I]8m<ľV`Y{g?^(W4V~V,+~a'b4k-dP2o\`9c,י?=[("eߏ/fR練^ik׆$ RkoXMVOGϵD>,[Kf谖fÕ93]6v=u{좤,XrFs` &pˆqvʠOyymᅞ]w'sOi ШG]ɗUONK3Y‡vq`vt(<?<39>q":Ĥ*H*Ek=wU#Bv3N9St&V?vZ=i90vTr s;*J얛6"!?<Ef{h~O7ID?×ު0fnkRRLߟ2 [骆a^9ikKm5l N =`6bW8Q 0qjrpSd@&+O8A Oo"sSgG Il^\^d OJL…]P-t|Ո8}vRJ-E&ېތW_0ڑ$?hACW|HE^ pHޞ;cFUa,?: CwwzVK\&t̗cVے3X~Eެ#)yWyؓL3p9g6$!&K~!r6ʴƉ>x;& #']i41]|Sٯ-ЖLC>>EPߡ{ ^>]}N.)Ԏ<VR&/iѿWkNuqw~a _A~d)|-&6\+ 5ئYzVs_K EgRC᩽ɔ=OpzWSƣ(zsyLa ax~ZjҏOF=񔬫bոG8fwƼGp"qHіs&u+bZcD dӆ/'spVDx+}Vf`;2R$UGc6.גi8?nqq̿* 'E8|F`Rζ/2pC>tL=3 RF( wjZlc:9K5ݧk1R8t=N!=SC&%~j%_ldǹ$Vc stNֹY[`85KIkr:d+וQIҢl##[ˣ+cyB:.IX!>t3)DqCʽRS(԰*FzLeqF~WߜkEO L+;~DP0DZyz:z[V1NCL~xShG-`x ?MIHl6d*bgb1#pC7L; XZH{ׇ/Ai~tY o-xYkQR|Mo;[Aȇ>_pBZl~/#/O*_+m ªC~>x?m ~!-M.:G] |jC-Rf'׌> sߒ{s$-IwEs~Un<+aD+?0?hPVkd1]Ôb^O1yE}?燣|g a!;:ɨGH0^ \.hR~*kQ jPq|3& pBI`xq='>w+`M}5W6;?ITPrn3\Z2k\wKxb[m[V87|;3-坵]׌67nᏘOΎy2679 9QW 7a<`/r8`yZu/a$ptiZZOr4u-}+iyi G8OZ~)Wԯ.VT춛_gJh3:LymeA+jbUn_c`p8OFs.&JęlIX"nC+ [fGJ :- /EIpMlrqsyMKxǡsړȊV~V`4ӂ[uB|D <7&uR<מ@+g^0] F.4[ |wi{GJ92~4+ M^sId6|#N+dSM$|FU?'AO_t&ӻ}?w/_1sQ:~l<oޜU`׶ʼn"&? AO^qexLIq98Œ ae(8glY%Nd1\-CG(uW_pAqPS2QG+| ~MV5?^ɇRǫ<7md'K7T74 8Kx8g?++]4m;qT\ioiQ~_W'>hC#AtR}613};Sui_2[MxԦ ^^!xKѷx٪N^M@;ϳ*y{ws;K &?'qZ6JIhɾ\@xnsq0oz$4];6J[>3Y$ͮ@uYՊ<llFȪd^c3&+ǑocZ.>26xHadOg@!5}uuw&B )/G[M(0mJLMp%QZ" qV7@FRK$TkL LTGJ  5#?pD6&K \60F{DI#7r#x)- 9Ru5 u3\['IYAJ+ e^RT.|RL]Ρo̮)Tqӟt-ˮqǠ_miW_w. dHk!:8%*Y؎11;+3i/A}<3ZRv_q}:y[[: 6nvhe.RNب>R-5%F6ՄǵW{W3ᰲ21jङk2Lh/,ug936L> ~;&#ad|WWӁ\sMP)p8a{ N8+.h/0@ +wb2[9SyY&Fo3{=dB{,|')N]V7vim 086֋=yBHSr]虄2>װ@x%x{YO~ /Gw:ѻt?hf<;`|뎭ܣc:÷0u5z?z=W gli't'^AtFҾ~MG)8fh:Nju PFJ͔sO{L%<6{_x|Qi5n*<oυ]/Y['ud܂ 磗+|M z>);0_F^X,g'IT֐i+_Zpfo7k/veZl3-<~)ϐ YH!ׁXUuwZX9 5vD'+l̨h>V6x|q&<;~'F}>QEhd rMk}Yv#:zb[ڨ<߮bWw)298A̅ٶ\G4}U7jrOO6 E s+A):l^_}@{oGj1@ZG+N:_m}][r]VRZ&Ϝ 9a/(b{JBz|2Al V^X6/_tr_(Fm^fqM)`S[^9 Fkp'45@i-H>MWϱUρt Nɳl3ٻg0z`Ѥ&rc<#)#|g8i<v"wOd/M$.*7ƴ8ixXVH`C E< K&ZmW"MWYyCY}¿vM#㢴<| $x1u<~9|4tcQZkZ< V1!Mk3'ŗ CQψ3~ ^9ٔOyZ؂^Z=暯R\Y1f-C<\D]-z6*@m'NcFPviU܄k>`}wʽ7/o="7/IPvr˜n2Ӯ2 Q9٭sZl2,ڙ V:Obm3Ύ™vZx2R@љiQG*?g`N_&upZõgm[u|ozd0v(CT8>Hyy]]97r c u?2k{D1\#Pa+R=o"̜H603ag89٧+\%觇+VҶFRm8,)JI}mj)|ڪ\A#|1VKSY6(5jF}3 5 O9[Vt|dy_}5{>]ֺbWB\`͛c2!st >Ỷ=PEVKnp>l&d`W+L^ ~O]zL3^~Vܛz|Bo)X\p` 8ڤ3pwsd|/A3omoy&dUF' sN˟"͆G:~|0?Y~J6'oUpxۋEVNc!_~?{ KNWe ƪvP\mQX h c·crz _}^usHv][ ~\r9aCGOY~7_}8%ߣd*^AD\PR*-B?lq2='i4<w9_:V pVqh'y=u%.}v8D1cRgX[2m/-p`57g]XA R T^΂GgogK(l N%{~JRVTHA(FrȚ` SuLNVYI\ ^Ot8;@1}#ikcdF#xXAe.`O8aM~lT0-; tkiA OP/BApglrT9'6ӞC|_ρtOQ 6&18w Nd4$ΉL2\.6K;b gʖT*WS8)ԫ {eT=M1eY~SIߔ]S=D(4 UXf`;!Uaw8&7W[QO4}0۰mj79LV>\043#&kd܏Hv\5Y`$FWrV/b+$8 ՙF[eRq7i`¯sDQˮf%N2vGV  sr)rr@Yp-q>(h]- vI&t6kk[<~w(ro*xΧ_5Y"y#Ld7Qo,@UPm#e٥ѻ!i R?NIJHehizz:R[~4-Xx߫ќN&)4xR@g7sS*OnR3Yv8Le]L]GMZjĶrz˹cO}׼HMkK nOt=l.L +bd. 1]FmӉaY1^s0nw=UYྼa1-f*S((.bUm*g|׽ԳkRG9Fo&Q&)|L[vLchBM YBm ~Ҧ8"WQ 1yqAƈa쿞}?V7Uz[ ȹ{TgpUJ[ /M;+$0`tE1^6olUL'pX&Hto@][?Aj<}հuzBq3\ :eʹ6^2÷4n1 Әݥ4[/L`dAqz+S0 +dMh@IDAT]Y$IrH7,\p7@M>6MVWUVUVNs|?wʊbnfj:=z&=ۖ4v1.sivXkɿM^1W_롞,`^p/oM 4W: >n> 3P=PxrWt?G,>iP23>'*Ү'KAPރ?tW$A)U>OP^˙%,X 8X?)(bll%`iZESJ zdRe #z[+P%~AYbzD`(υjj<_ P00s}x+!N]]ĺ6ٴY[j4]aFOs] 2`ef&cRFMG-/Җ[3GD|? Txp1䌻Whg(㥤oJOY vw-3k˂ 8C7=͙<~"kLvr`!GrZj5nKjwDJ@E^hx>v{:;a:u`ڑg Z:䞻ƻB;<Ciԙ}6=~0zNsp{-Ydߏ{jpDo ݾ3#rMio-44\^záu bCrqG,hT+K4ʿ^ ˵~|i \JƻZh'й$Hہ,m1e(3Wyً xq+HqxPlI76D.!<%/f4\(\S_嶃AْIY5>+LF09ҫN\0PQ{\08t/j>f2Sn-" 1"9GFPG$WfQu_A=R3oYJ0C۶-lPQ89e=w!(gLfOg27[qt4+A@iyw]z2Nz.miLgI 38BL\)LE#z$Cu:88FP*^Ŀ ʇ? mS/Q#3Ji[VHX=cpgOsdz.9rF/xc0o<\ޢhp-y P$:Pec]chN^a/Cۤ>[V^'袇MyMl55hsyU}8m;YZl +^WnIW9Ȃͽ4g8ʷmdXƶ.nū!m}8|*}ݧƹnlJ֠m2B;b7-c>p] `x?!cY4?Rcze"B012ϛeBSz3\]Y0akj_K=ʱ@`Ѓ/Kx9c΂0d :ܭ]`fP}1G~ g dz"[ 3-0mi\`V,e!ap _F-=Aa"ni ť_c1|j&R`c$9# H  67h388VʏozRsP1SaȠJ8̣ YKd..f#+ߔa@GN0 F˃Gs,Kk耮$Χ\Au]Vڙ'3 c> Ch#&NȀO$#|-UR<؜7 \m-ԀF,U}pe6<ق@t@kx݆3P'}8@1b~!I^ XH}Vj/i;{+=aR=j4msy ;1{ojxw&| .{,bDYQԇ+I lrAwb&Ny?= 6z2,SǓ鯹9 P+W mr,Unt;@f8n8Hnc.~7!Fo'Zf5ڈٳ.YlD 9q Y˜as%BP$, [ BsZۖKywlI+25fFV*HU@K K̚q v6L޳ZL9J&Tc"Lg d1'PՃ^s!Rd!cQz2SZ̹2ߕ+q(΂Ӑ)rPhCC[W:QYsǬZ aD[O'ZV7x.O٬Ȭ Å`{7ֻA+.;oQ8Wx#ץ+gOхstcSWK8]c sB :#̈́s: 5Y IT.+•K?񴌶DrMlV>{g3X-3t Ѯ|m=x }['Z82iW}#öRxZ1k]0rc4~)78Ӳ+@ԪkM[>W%}fqgp]42`7oٵ#uđq(G^}2'!ֻsRћ*Pm+ =p%Yme0 Mbu`yt?)ˊw6Ydc0 a@7u1wCC>b*;cRj0O} 2",bWbk!QyWXB@1D(pU58*x&V&8˹8",\ ɓYl2e 4Kwm;oL|ܻY~z,zd\#~ & s^vϰi`l池 )E|p-H1OKd ӛ1._'I2$c`/``sB<y=f[;҆L9ޜ!g j-[dK-L8FLriw8f*,Wmǰ4J }9b\qw{XK!9uq+RdgBQkIΩī $t7&hYA~O3@{x^rN&D$Bpk_za4{,#2p2ӸeR <%uz R&RQp4Rĕ+R }`ҁRlhsWxM擗_ZtsZ ەфwWNj6pnץ:@pzꭶK^r@^ekp.\˜C˯r*AS}M`Uu/0 a| P0S{ d~0yirܿ*bq<d~Wf27 \{=mrW[&VԷ'SwGTz@z9|i< nJ~kźѳ ?4hsJ.2q47uDZzݖL]AFq{y$QknX z~?7#N71_5+ZxPR]c -]}7H D2&B[( piG8``6d\7Џ!Cᇉ<'{}ૌ ~!Ԥ5٢V%W'-2+ 7,d8~u;ꋼE`pYq c |==P;#,~;Kg~H36)rQ%B?ӏO':A+>+=#D)3؍tHlTl6dVMG}wKϻޥDH[[V/ d 78wK`8kq[Ϊ3i[x2A?"G)vkqluYAerj:>ib?kX&VmKՊ>C1X ?CʌGjG0g1pԄq>6͸a,u 2Iu>g-J-9s` {.pxe1”gG;~K\?%1[V0b9uĬ A[&Xb2bmy<؟PBM<= nKSY[j ჿ -.s'RG_-}g% fK}BFV<"O SΎ&~iw(ᇙ ~!3'EoW VQ8Zueˆ!ȟ?>l`$q"t])|5z/9 w dg+t=N$7Ҵh׵3OEw I[mSv9 ɻ?}=f*oAcBgH G} dkO[䴣ݝ_i*ux]W^= (4z9~\69pL8z쟆Y5MaZ$(G-%-\s{xBttǂe[KCNjd4B+sIxo;̞/."{r|sr!9!OWvDb_M&E2(d?$9:#L4W lxW)D{fSpC c)e8x{50{o 1h`VA0= ڼ^pBX0X&$jp& ´16a_TEl4AH$%3PP&*Un~rCJߢsń\o6[6G^`X ?m xW|4]-?9Ƕʼn- /Lx')ŽYmEr \[q?+{RH +TR~S[DڒY\mkpggx$~B+AIu[|<;k<yI(pa[:2UE.DG?'.%Gѧ[`c ttt{X~!U7  |<Ϩ- 3ٶL2~ ZgBq`9 oSF{ Yge<g7aA j]]v!PA@XCBPv3.|?*3l!Ct2YRKz3=Kp <ټF'fH_4U폀m11r˿z(!Cbsr|cؚ6g "W_Qhܳ>G`YOy/8{3Yܠe$ZOa0;@NIzH qtJsptd\|u p+@vZpނw{22.LLSuf ^ c.P ̊='`1'?Zv3Bu}tb=yPL<8%`r3^JKٙtHyy҂#u(h7xj6m([^^|Cuz4M{Wg>Nb<5<ݰ^5}iϧUt3'}R*A,!X`{~b-g8w0J8fm}sz F}OkrvO)gH1w:Yωg]E9'SGH)jt->+#G'jӛ*HғS;^-]F>(nMMeTQ,i)m 4ӌ*|o꫹OYۤ1J6&2Fg3z6<| fH( +WM蔎ݿ>xK+`EEזǜAWX&x[@gO玓d6P&;ldpbZeH%}k+EW7Jq4נ!8]!|Wt4k2ձz Rx*xƞgo)VGHiCmr =F۝ԅPdTlԶK  8J@%o9HVfUU{k TbJoQ>j"i:(/wk0KPYIh-$0b ͠?BR~BԮ19,CK9Cr1|ڢw.O#bvRpXa%.NxdFEQ[ /@0`3gfP~y:&0K(<꡺3l7ۆ*ȃݨ*NTSnƈߜYW@ȷ*p)9,U-E4GtMqJYp1F[{ }Dux؆ Ȋ!s[m_0v@g@-oؿ.}69ZEr>xJJ6sdvFcޙ{8SCPPדy37c ~0. hN^b7op)]#=VJDiV 0W}qE; #I sC·tpb&oW>׆5.`^!T'G^8&#p=j'z_ 0T(aU \YCaj?!}HSG-oq/, 9ጢ$e}㖈_ }>-!ДuXorG/*{z xNסiqδ >?ɀ&vxsW|rǺIm^鳓$?6d3ͼv1GJ?F^s9=tyUz3}ҜbUً:_|zpkzIt?Ɖ"nkz[r/kӵ+d6)Bx5c+9wNYbwJ9=q]y8 &MZxX]5.ve CO]ڇ=#u.{" `P ]9y c3$m}# ]?;C-:2WԌ:{ L7+_swdlݞS_U?O`w_jmg :ƞK.o \H{jޫ t6?4oݓ;O3R!%3 ]tj06Y,Ӯ{I+IO;}?4glp 7RE1!ozܛUXfu^Ū؂1y4 d,]D(0G͞o_OLb&IQEp 3# >3RG4y$lVGOV8 VOB{Y|HPՙIp -L+lHVOr9.gsw}?¿n`omy~ed1,ewU[ J1ѽwNUG Hc%*s婼}TQЪ3H Wp4d> +Hm t1.c >k:f:A~P9rN~ ԛ5z4KXpvF>d@+,o|? ̿fHc Е0~驷^kԠ'"3kEFDs Oh$~*A3C%MyyήyVXCSd[/i-h`B/"hArsz G'"2т8B:rȺUS&vfܽ;T;Dgǂg ;Q7U3N쇳^wi ur)'7T ]}'}d~E霢 6ˆJtc&p/;Ьv"ۅ> m¯&~?;_Fp 8T8+KRp6"x^mڞ&N9p6T?d(:#_j*2JKyHf PL1\Gu1ϒϡle*=8V3w9b$p1W5rRAf Y!Yؔʢ(V,̛Ș0ز.>kA5"+`!2P\T6[ '1|T(NV{ "(K{)'Sb %l;w> .AGdLpo~`Z'UusE0O*y.J[sgZ%\z$: CW MKCUWyʚɡ 0r"Ͳޤv S;`2z3uh8-w4lX^{l1ϑI6z싹5 _Y{h;4Y/Re<J!=\_GwO1mz\HV'B۳'؜}̫^ɴ/]T#=Ҿs7ccZ.>P @]n=|_Zx>|c{ގdP`rfxd1TxYӎt *e^'-A]~/ x]@[KM#BIhE';V?"cW[HW+r<oONEE)O\3"'BU6vVDv9NMl敶I? ]Mya:NH`uëST0el6޼o`J5D4YgXF pJʱu %L2l|Q\W }xNZ[)]I6_GBoQ+G|28Ϩ$(9eNKLb#,S"MBD-p\T&A i{d1jH_e`L=}Us7=Cy'솖!^`/φ~9TC0Wѡr䍢\y3412 w/^>w ɉ8BM\r\2]@C 5 á-3Ҍ\ܓAȬ 3BM8ttDzAaaRDE-Oғx#´i#@,=ܓͮ_I ᖂU) a廉}o~@gV,8}Bdvt"ѷ pi>A-ңdPʫjIk,wK/s]sjOa?S ZT]w]93?`iI[Kk/NH5R3C湁ٽ@ :~<1Bdmq ^$> -$G.Fp@_~U:mIgBr6ΔkR{N\Q{nKO[[%;B_d|Et*[0۲κ&±QGpã8_"ivm!mj}p < gqsrR/;}.@BdBu,s&MqʣWAm0Kaǚ|s>;M2 !Qz6{@2= 22>m=W0ʋ]éHI{*p# )-S^57?4޹U;a5ՋJ*:B"Q{L΁;Z\.AǩW"X77y_}3h6$+gΦv+nSLװi1;_MRQ~5t5lA/Q&z :d,9xї14 1N{J EO4o+2wwŁCK L7 @IDATɀ@<3xEq68B (ٜhJl%GSc~ܳh5T Z<>k#*"=0'X8Jq};g`ېǸF.{5bՋ!W{2]e;z[Cޞ%OA,]+er]=KGʹ(9I{Н`=\a.0_^E^ 5x̨N$ɦ8C%7N6FqxjT_5Ygz>(w_̠m'-V *X @ܮzט  8eOyrӂ4D ^FeHne)#&Pįe'Yczf/(|Џf,P (`d@`9<}&Mjt&M ÕF˾9u^ 柞j (QR@OYialL-ܾd! u~q3l8WQgC<]Q앇z]n mg`iFL(k1_>oSZкsQ΁аk!)hQIgsy{./Wđӎ ~T!p\UAj%WݻR/N?Gj Aʎa=z+^gm[K/[7r y"c=aPkhj.4vJ-Q\+10ѯ_EВ>aD.(sS+-4thͰ #k)J:$ہ9̵CxgqHOz2.B9S׀=l>Xz\B%&= Ov~7> }Ioz6)WJL f^K8/6큄f3^v`G 1tmLc-;.?Dimix -|2+\ ,^Fq0k@o`#x,WLh-FG4eg38ҳR bFH2&ƓmCi ޞ*]d?99ެaVs҆B7, F_*jʂbGs|>;U\}k%YPK}9u3R*V* k߆.{xF{PJ/--}mP,_s6944nW/OTܬs<:x\ 7־;!x߉Zp l,}蚌/f)/=4Mb~ն^n(i%3b%W&I-sgeye}$X}u݃-rr4Srx9e^tɡXh54L-ޢ8Wl/)Wg݃Lh<ߎd|wKJUc#]AsƗΥk]px1l< b[O$Mݯ$~ux틵<'S0#Ta_^[ A-xϋbx0(O]P3;X^q p& a2eV&㔸!%\W>Ns;`% .}Xy"jO{&$ b|Y埰 jI ([J}DXFE1U#G}p\lF _̙""*` Y#aVx@R䣁}`._ưq VB!Qg7eڷwWPON Nth)Rk{^)n};W Jҭ\&hNCh%ie6&u_zmz4Ar^ńHԇ4XWiX_{5)a1e흾`7WK}A6Ċ|opVx} t9R>z?N޹:>tFtY/y:D89{yt_8f7P0FkoN_*]VR7,@{ў^ym Gէ'Lw{8L y&6el!RSR`plPTѹs j'}C}E\ ]2bmAY]%o<܈rj2d֥{sk/_U¹_ο9C簛u}4W7 w7 7~:tJ"/n G9=yщ:2#,ב \^Rx30}.5qLnCMA_=PUZo[Zeqxg[6_U/,/ڝ nlp(bܲYoCvMÐ֧z<\abJ7wdП6ʿ7B)P-W묒8r"-gش)/G5 iJϤ-Nyævtgs{N6Mn };+S$$'sSr_o[3Lu䗛TFd||xC>־ &s:/Z^؅'qOI:*M5jqx6^ϡ)u7:uI{{p \W9@V% +LycS0i[T?[ Trcw[MEӽզk-a61`OQ|Ox_Yfs:C'"&* <mMKFPDPb Va\L]0tX:1򄹛BӡOu=B gKۍ!#bE*F4FBs{*ƴ)B5ț9"]EDH TewQ<7( Bx9$l L.='۾P1~6_[=fƳ19o 1`0Z ~.ߙE<`س:|7k$1p"g8 U<;qz12«xu,o ݠ0bpU~hgdH1l%ӄFC+ͧIsJpJS~ZBm| ތm嚼M{neC}>G2~&3 ~!Fs{%Lqۗ@a!t\9ty\t!59K.O YF9_T 84IA{< 7de#Y`p䃑J_nHiFƻ#:̬L)|@=lza-$I2x2*T|4m?Ww:uQhyJߥgë ;4iL*[ .6y[Z䑷Bϻub^k^.н7PО*>őC|m@ߔ^gÍ1pt8a $A 2O?]W!sk j`o{n k+ K/Nފy6Q8azYct-goFozy ʮ~ BI[Z=9C)i%!Fv`#E©yCΦPf>K8%"p3P`K+V,TI,^ˬҡ&$ jHL4x>/Ǔ92_۩j~2oAa)va0< y 0IlX"lyM82rvZ;<I(ѢcCZХCq"$(z0ʣlG .K?Ԑo W5[ͨ Ѻ^'n^hh/ [xdUF;=o H—=JGxwh6tm$|:c&c˰m^r>2eK`D4|s7rȮ,\d6R@Su8H|C u9Dʇ7Rq [Fhwgb34YD=-z%|z1z.QVp528RVgJ'pI)},: U9 N?Y=˧3ADwJ fVkcW8y&pn ͐өcer1S|s}.n5c] =v)vTQV]By;HɖQ@T5#G(r#P&f|ØA-Ąeb7$ oULUoW}V|<>\}㥃0sY,ޜ_>9W3lhv%p,GG^ਁE(Hǘo>5S;Et@;\ypK,/մߊ(h}ch[{s/+5 ,xN".L7z ';[_@(AoW/'Κ1F'ko{K}"(=eŋm>6^Yw_>l`&tNέ̨~2{]Ü5q[CpX8B8\4Y@۞ӑ9vCIל ^I' Z˒ HC8/==C;NoK%[LT`u,~n3=U'tϹt쌾ƹ[%DUǴ>t9S2 n\&ůH2ie_C<:v:<.}5!n#!%6B5Lqo-5:7nL%?6JK'Vu0,k!Ozj'n]aKSZlh6΅ӹ6\(_:Үsnwj0]ɔ\}Rmv Җ#aka mmy xTx Vԇ&V\oDGڴ3teo]W>a㟒T" c 6,BD2l[G#*^t  Ck\| av,N`CɹrKA0 V?6go:9kj6CUltgl՛^'C;qڅ DO8 39r=07)zq6mC9fdJdKpC4. Ǜ~xg*1 ob_2)9awe\sdqlR;}ʀM$_xd;AqZŮlRYFwIԊR !ס)g!^ml%Xx)iɶmOxTZz}{Ҥυ\) 5`*"٫l?6<­8`Q]Hl%e;9ssR=WJ)IzhJ1J'5ݺo˧7DE8L(E/{'_[,f ,.: S PVPØR%]w>5A㥌x+b//w w#‹.+pڦ@Zdn) "qj(+U^9  v0> )3'BJ<(ҁ26,8Ѯ7cxǵ]LGcF1w9G  o.дKg}@irZ<а'iwh=RFZVZg{(?صE x1͋k3<+֟갸-ZVSV&Ia֦ 1lFk@2˒9{?]RD"n/9^0,U}{Bӏgss9b𖟾 !kŔ݆}CWOʵee$h*Gy:P%k٣'Yg&oZI K 3L&[pG K EO 0axa;]24 h{x.M0_'(l5SI.섑 gQ>:suڊկE #e /͸2Ռ ~S rI(مzqVXd[ŭ'!9jJOGޅg,X/Wydi?tZ_m{}rieϵ`B !w 3r^rA;9J/'+:KH#mT pa^ ے?o-|g.m$TK$(0dEvR^QzoT`+BS K^?-/)O-DhsxWJ8 2di1dЕkjrGqdJ4sj8gsbWA ǻTVWb[S&v&[kYQ}r.&VAS\IpJH#Ni r]r e-og %zMN?ZN_$vw(\\^/XNoPm[թO6usz%B%R9< $KIIWu8ǃU^_ EnG-#yq&6ȵ[? c輷XjEl~A`.B "P at(LYp"q#"뉕Pq>˟ݡl!aP&9J(& V Jzz>7=P֗<^ R$(JH(|@0S<^JsM!Q5޷/A8r_Un(Y"^[=s>+BQXϞ<0ӁyN|` .Ƃ9uCW}NV5=RZ  ^ ]mILdR5eiݜqv!XX<ϑqm q{80-o͑+ mb r0ܧK Y)6MIT`v?7o~8.s.8m$- 'e~h (|ZXcKҔ3Cl/)qx -=;mKęTt.GL7BŗVYwyuǦVz_}+-ln4M.NB{m|Y=Ԍo owtTv?+H_,ΑmmߊXȺKN/i HliPRֶh-E9`@N>z`;IT ǵ Diw 5D^"[:5SP)JW "c D~7yLJ\ߎ8ޡ].`.0H\q(~B*m8k T""\[<]k rD5W`fkW?Fw[ o82˜7v fa?-r{W|_] e(zmxQɯM FwKK^$c3*\DkˏwN5'?U.g*Q=F}Ѳ*]`XgYj$PNI۔,=&0zӎH$p+Y#5';a:Xew }G>N0`w_\Xz [@kH[d>x#3,۶ȏN7C'ѫPKeMg*'Z:|{ZQuy܄mˉ_+N"#3.:i eMK8qR2$~;|0)3ȯ4^K nWӂ~OOFYd8CUu:~U2 {^Kli LSA={^g!p9tipZO Kϡ.-Tʿ_T++"{+lݹXau Il2N?%#.lí6mOU6:l_TNeexKi_[.LQUY"O AR[]'Y49K좳R E^3m.OK93`!#sx0 YO##S_R-X\+5ˣ͟W amLRf~CN' +WQs>ó3Ln+uv~V8]9kɩsCqq).팃=588Gv E(-l[4мZ2Z:LGz=n}'YNo/r 6 bc`"G7#!2$d#uWE4m(N+[4uJk *&V+ljHAfY*r7[RX QORWSlŶo3F_/-({3o?9L%th /1R[JZW9bY1<#JqTA|5pY" MR5ԤE*SeP:f  %_ fC7LTo]I|ԝr囜2;׈{ڨl0lkOOuVxߐ!gdm1קu^q^ᘛc# pa0pNeC-ҲT.NRNKٸ#^Q66(xR>WRsol@R+3; _+澛tiaf&mÃWY%)G:ʍki-/LnP.\1gDŽ砙h-@HLy=!T)6>}v5m(=F@wUP.:k 0qlX:WۧDKF[m@:-rG L9˳: Sd}ԭ۶-+E}S:0ԡAf+{z7ƁQn=Y](ɡsI vCݖ@x${^(ȧX0\r /#dFF-F/,(QKdžb8^J`y~9ΔGmO ™M`!/2T<ȸ oAXl)J.M1` ]}HݧCZVDVf`@&1(?$Xuy)o䨸5X,8>Πsݯk-+ O/&V/2Ɋ:P\EvxN\)m I=X⍱^z7sТF ]IlXbfVxC9hiڞ/MʧI@Z:q7l\}LN˝?)h23i⳼i9tYeIq=hJ WO\+# \[GYuϖ CoޭSC-wmEak0ė+&haŵLTпd24:ds~R³Əu<:p8=)!*PLO!]]MXu2{_{:V^m J#֭W+h?p8]bM) )+* ,mPkjOxipH3,7^gNv,I41CegUwQ$@^ pu󰞂A@m . nDefd2XѶPMY?ٍC/ʋhN/>*rV6kߕmĴ^kpd'Ξ@=+ĉô$lM|>j+^F䀓Z3*i/xOA(~[41;)@%#UvXK]G8Nkۚ\S-ipW3ϨyF<`ZxDʁCMdک:NL'PڽS<9ə[m s󕾼͡PED 67нZ=#[@9#xb<6?ȫ_4.p7T1~l"˙9%9v>KVʁ ƕ+K"s;wAePGt{w: f#;ZKL˺+)n$8P3g.u&ܤkGAnp8I+M+3hGJ4شvɱlk^GVw\3z w`~+s)ɰ)8w8laKMOAhIy~\#qkLDٯ"U,^b,(9<}U01m˜w>Q& ՊB1\ y(-0":#dDyԵhzSG0Y-oQ_幞Qе ůOpdxILD,Jvdcw[k/tRmՇs% .ar \&$#$=Wo $ȅǴ"Lt iJW.+9 'A Y6Rж+}1{h{ܩhAgvT?ZSꆇ]V)83xGN_ud ­:PVߤ+_p-hgU;V(iLD1@O8jWGN?`zgw~zCx/ <m9?c$NՓ4ru}: ?ovIpˇ&^a?z^s,U?Pl^_7.$hԚ˄X߷vdx2ZV1O9|ќ6h<ܓ1; bJ:1V[oNݩo 3lOwXEϫPڈƘk盐gvB\Ijz: h${7~C+Zcck݅4;l# cw\z8O:w{;=/R?7Xu[*ۚ.N_kWX0>"~ {~VWxp(@<p`ڠ8>wstlqu<=Cw5AaQ6U8DB"/S2IKUTMT?ä.FSQ8ik-ʳ&$ΘtʥFJ"&$M`37TTi6tioQo??CeGkp=~rb>^&xaLf\7i~L{47e I?,>lȷzsmV$MMr11ƯUɥ#$!2R=|u\5G$jOF\iI1?2|pS38}2iV;9NL_k뷉r g}@Wϛ7 U]uIC_;qWzWV[Nuc8_y3g 0ٝWzD/Gm KNp$4@@k箵=qg7|IR[%|pޅ}iynur tY'dnZ!M$ɓW. =h['G3^Sџ'3y&Z1l :r)<<6½3f ?IɘVuv^6 *Qe%xԌ7vٮ"])5 eG }~Wji&BosY3я?z"I0ƑyZsOOV~J*L)DEp]EEpyGTCNx_$ڃ6;uŒ2]**Nؠ# r<$([o╃fT~I:Փi(_F;o'C= {F ? H1Q3:v[lx)q2ԲˮQi鴝O5m/3^MHl{~Cax$V'\zCr9[tOӚёIegsǠwueai؝.meRM _yg!*h;%fKrxo{E/&1+nBQNV.!6U& xMaMRLu|ꍕ'B60˭<]1j}?4"V$0$6)}Lkc|?ڥA~e}484˻Ck%ܡW-}h#-3fTFOyx+@vc/4g͋> ]I`֒~sgAk+K<˫nIW-G;Awtla4bbcpd_#A=26ԇ~p CYg<-~ă`WOr]uϭ^[[XxZ#sE\l;Hg8tP49EѨ%{J_}je樀DD;զ(ݓ5M@~p<%)m+'&I;WIU,\=A\! ycN]—hÿNom~mPl凗P+ד:vwtcpM/hj)ObwM2 V=@IDATI&8KnמzUnBz/ps< NRd3NFs+` _U>GWmKZ#i`Ubx| 8!+m CAxEyݷRadv <98c W}1\ŻbgU<6:#p >^4 ׵ (OK4idQF`R-}\U*Ƕ^݊i-aP#]~4XEdM@:jli>NjH[n\!9~"0Px\O~?R6>#Stn3&}=xlCdw7nsx:|eGN2-) i W]c-wU 7eʏLJwCr.8~_=]?˛l7ޏ,,20Þb$iP(i+9cѤ;_?[ GK;0/ kVshg.(6P:)av),?[+dn]r^ǙpXDf?Č`JଅϦ^J_D&Q&U`S߮ZvMK՘s8m^מS.n}~ѹmPz:+mP)Cξ~R1~9gr^HMs3`dBƋhbݱ<妠1Z<{wCUz4MMl9(et&8Te$C%ʹTүIefc7.y M:egVWjN C=7cB -(,W&*xgԕ^$>}mڿU~\ @i|uM 8g(N&tG%LDҬ'?Gh9{҅q~5eA&q߂2MUr;hz~4$<{ 9P`uH_]z'# WkJ~F,G~>|ӏ;9(KrW[^ۼ-çx\8^ZVH[s&y $q^In~|1RJGe5B\h'ۭ0^uc VOfԏs;p2Qi~3|S͡q?hgWsW\{X&shS>skrd ߂J/ʤ['[Q>K{Z< [AKJ]/my++Ic/Y?vcC 6Yd:cJ1Y&[=[m5|0ˇobQ߮Ru KDCjQy}|:&Cr2b"ڢ.AXKu@)N(FѴwx%JCD! nU@=!XYw!a(*y1@!2 1Ws>!MѽE׭z^C5JgF6Da-Џ}yz/J V!hSԨy}VNvi1IKZwjhQ7ջi%GC֛5VR,8:W#( \`J7`͑bgTD:[>/W`VhɈ[ צW\{ӓQKL|94'#c> 'wrIB&6&Th-bH=S ߛ)åP\YJljsTJ%xd! `I]ÜCWνwF?uҡ(-*;Rp,ʋR9krxK)$C>({Vlȱ`fծmp鎔<뽺ݼu!g=!˨@%}ESK&QѮ<.U.ll9.0= vm)y*esk$國 sؕM5||g߽4_YiBüf2 P;bL6A Gxlӱ)m}zigx& }cn9m+O{3ՠdDmn"XqDVbkg|qB<])I9SU-S ȊףL=. 6G (_G% ACxKPgtCGD,3f^P8MjVF23NBS@w85z]&K9_"G#c~Vp-9cX dbHQ663⿞:fԕ S o%CkU$Ȅo>p2P]5Σy F-Q v;rGL! '7 Xc&@t^d9H ?pʫtd&y+OS9DN Ȍ:M{f—9+/SFqë2VKLvpH38,8\Km ʭ-zVr0$M_K\Ue6J[cpJgdGĮ#^ſ9#w :Ah{v<9`^|w.^imgBg7|ptR.$Xv4/"XâMxYB_Q1~ җ0fdƋtҌ#}w߸VO1\_g";cn#M1`p3 RDu{L(6{˚n"|Ϗ+2!he}s>L!>*?RC(C8x¤al||WXV_'*y\?Gm6XPL׀;";VLL#}єޕ(e|?%l6BiMYz%A x%yoMF?qr H{Ԗɥ6p1z?e-d^<>0 XH›hn/c2џ neTm:3/ӯdkjF [Ov%G۰Ws;unœ_2GLp<6gpAoDE{huرO&pLG;F?*Aㅗg/H<>>mjOCw.߭"}Mɏf?V%zNFo͎Bez5PA~.;!E@GGh/|T~ddK}=Z$-;\[v9 ~Z884K3OQT--븥'sX"u9.{Sზlay8Σ9IZV_eWf〷_-9lyEQ]f |9r1|LgQvlʗb(pQ؄YGNyBЎQ$i#L@KDJ HT!lӱ- ͡mA) `|":=Y-f=+{@\5Eu3Ńy }p[c.ʎawњ8ný/.гJ@=Ƭ?to\-74c $d qB?2$Ps// ʴZnA~C#Om.B0qM^d'ΔH9tpt 6:W;D:t8s\xȯ[Vc2-oO \?J&{ +ɹ< S` MGi\ 1 AL@Ep]Ohp7«ozU]Xa*[ْoѪ!yղЧyQP>;7_S,Q6<-[[ ۫AW1>:晘 +4(Hi@%;ӾKM|0܅2yGOG& nmga֧om^ݸN֒qG*߼5/'k/:LoM'&ɊgGAp Ƀ'\GޒpTO_Պʯ4P~E[ko۹WO_jEl lFd{ew"kT=~ח>NL)m=IMRҴ{OQ1@A:rB&qY~}6z%ɍw:ԫetKNjuVoj4M0rï\͂#5ñ:60~%z_ɷSz}LZo&zi[d Hʴ.씋e --{Cs՗1\[FCFէO􈴯);:eRcyB>}E#y&ۂ<__#Ia Ռsy!p]=U﬊H8SG/~=WH$)0?T1 TQ"/32ع&ȜssdLoL\'QFUY[8QF)Ïse/)z0X^s:o_ C9+ho F3J+lE% ]Y1Aߍ)d~"1ps#q!lkDeI08mx '2&pƽK HoR] $=p+-+ɑTq(JNd+!i2ᏼ4KVGDFۣAǵerT[p黊~s?̃%C$<^[3zo͈%Ҝǫx?5h亼7 ԡ^aD60{X u%[nW>%X`:/He)h# -3{Bz6jկlowV?||Wh}XO^&nGbmˣv sSQf!O`w] }XS+QQ1Qh`L^e>+wiگgtL8V-HTlQZTWI9nV&y^cg$M|ŤϡѓpkoZI֏RG|c^ <}NRjӂ#ߕ3bNμ.z@#7/8O[n=[5~\&-:q]kAgdU|gNx.vJ) dj^(%&,`wnpx<ь 57?K&MЩkgY94q0D+qbEwSKB8OAgI 9/0Xto|UZL)Aʺkl,:oh2vyUV@Մ~4OmODlS4/Z౭^@#q5 %=hƝ|xW״_=3<ɶ ;SsۑRDz!>T {q:_*~l;{7K}m$=ͳy4OPϧ>J0N9H &8vWA킣sA_X$5[?O+뜓K{#I6S?(y|Hv5)СfĉK1i̝_|>) ߺY}Q2l*"bɄ]aPĝ1&ԔBs00 Pi]}jP~/܆//긑U(øSަW52|w(sFOgs~̖*0z~Fz)ocsp=L7ȴJ i8CrKLP/C)͊*IZB-\83jZBLQohU?x.}RH{S~~u>Ϫw txfI_\@qt[.(6IXܢoG坓?||:i\{;a x=Ѹ&I%(ȑ9- vx I#[o3Q.@bclxi0<êWJ72PIz-Y7҆_ګ~zxAm/ЧxkM7=#bءEۉ),QU\{{)O-b3ֶA4ȧht9W9c^ˠymmlD˧V<4NX;gQ.3<t?i{/ 7 }~^KT)擢>IJ͕#jϮ#w[ROj~]]<-ZَG쏖7!yk98WJ1iߘ0.64SJ\L67Gz`zo|mz{N#ՔIjJgLn%ɋgwW`+W#J>Te1FU!#ܢק;Z5ck\hh1hSh(9$&_SlcG$NzN'WצY3W@y8űʽI>$uzSw7lnEYuu sm oV'=Y'iO[|45řb;gӊ<=iت|$&(0?<{VnjI3 _3fA~|kO:fU~Tr?vt|y2>Y)9jц[9/Kg~gBr{ꙶMn([R1AugL6}«V}~NßhwwC5xdͮFD.;9>ěFm*z ?ҔO] hyR{"(郺^W>j #S/j+yL+5Z->jʢ^7c./[gޝQZmjXKv_J+"XbTo~ dA9:z<gF0;:i/K'F}s"I?G_bpxJ{@ r)vېb4/(S9 03"JZ)O9|I(^;?eW7ۉԾhIJPC5^rJdY?mP$q>W.B_x|Cx%Q}&>s~xmxkFǯ% Ob3lq~~砡S^^AZsQ9O8-t58y41 ţقٕIPo юa{;ԟk4J_RW-\ mS>)R:1! eՃ}FK1= G4Q7 /j nj|n!Wmku) A>x9#X:"p~3ca:>y۹&BA_Z Jwy&96y,Mgm=۹ַ^=S7W$\Ѧԗ~&Lyv'W!rl>4jY^ ֓ڳ6Ւ7[R~'_D'V4gl(v<'Y3dF.r%|ʗp\_ֱ~}]v&yx['.8&, eqWG&(/w]*NUѷ<}L֮6.(}|g܌! )Lɳr_xclO&U@}`S& * wڛŔ_;Ɯߗm:FIR3%tH/"Sř"䢣yh1elB]#yѳb' EUr)q*@ѢTM|)J?TQB& o?QbU~_] T1k#z-]h#6mSJc"'v>S`ٚǸye:3Eո(+gdWS[2|q|9Ěਝe\:n[iY }؁7%K_z>!/gA{_봥/2Ȅ7,OPSO Shb,z|R^rVI{sy[@8h–w㱌`&57l0cwXhqiFkYʦZ.rIYVn8 uB& %_j?*M&`|{ GV)kV׵wsx]VWZ0>y5"꜆AN~8p*:k`hT?S]q Y' b4`s),NWc}C-PSy/>#.ٟI OxD9@Gi_K-mǦ5D<<£~=utLu iTЎ#me$Y]v7HyITkCPXA WQn ;!HWZckLt9)JQ.pi*ۤ#`2DBSQxIyBciNjwu9m K`%&OKK۾X'z%zDn?#`yr.`'8[CG$EL]v,:3̙fޓIkr $fwh.'OhOnlχM5;ޫzpag$=gˣydYJJmIyS^M6_riRdƵsiEʯ#~n* ؒW'h^R=ʣ R. Q %5nw߃0+FgAx~Ql >"-2n5Gtƕy)u#s IzrN{F5@ }:$x eeȻ3 :4k Vcz5kxe^w|#Ե' |?\*TmA= t]-OCq@Vp<r7$(6Eg⇨[zty-jm Cڗ?'uQ a^$S]g]={sԠیalr5/?tL^%$=2M=#<5 쪏 )p L?\1r q9٫ WS|wc<+[H9>>qY靽YVb|&߹CBѻWy @Y5ecPq{(C[aRZdIY9=Mx~eR5n6GXd_8ދ_xޒ7!?wlOu#dp [ ]r~'>։[9K@AA$ّQ@CśK99\ZYQaC^a%-9SEۣȋ\pP.:3bb|aQ9~%zq.z2S;FvxW[3*ql@$5"r2:?\-BK9x1s~=Ͷo-|A(c) 70GGr\7̟Ƅ_ⷶ|WhIrCމ3KOBH3;9||؜-ձBtB}o3Di8nQTm:"k4!L"25;#ʵxVL!BߛZ6q娟b%H][|USBRrPFu{u/h#j&Q}w<[eQcm]ߋg8%&TLOwbD:[Oy nw@W€aW̡{?߮˝X{c{הOmQ _CIq3cgvqO'M ጆtM YNM&E8Nz G*q>Ch/M`nH~ p_| 2|,1 HT-u'k.X/.ʧ%_hX綎$/5kZ Bh'8zN w!?x -]~ﳑ/|vc%.,ĂeRB/l9\cui h[6aB�p˳P[Evb*-.ȡ)Hctx <B咚~'Ý:O(nyg'ҙ \[47iFm%7PzgeKiesxd]t-:tL4K='|ɱ#"%j ᙐ/Xw.Mg-ɷk,VNMh{E,,7\W/f6uzXc+8%}g)u#)yoGn;bQI Μ#fbɑr"@7%U]+ 晴n:bVNQ֨Lb)Je)!y9ZU+,Ҭ:p[D1sQz z'GĪ{ALCg׌QiڼghyO1xJ91Z![ar۹b+`Jǭ3:z#x)lֈqX&w"b6Q^ NZ˧: sг-23Jg+c۶7Q ȾiouZ&)D*si8ڇ;j:z;ȳ޾wl(r7.gUoƾv۟gؓOFb/5|ճ1DĤ ?qAy yNk1tďjv|7‹Q-x-,%7&]^9 T*?p ]<-l֧.e{5v'ۻM:qi9u&=:ѯg՗KUx2<Q2l/h ?Xt?_>g@sVjpz s :=֭>iښ`!bZǻeLHTؐ\&;ߥ𮯩Oō=j黖"v >aj;'b-%0>(-}=n< M~^z,MIBպWi9]}ngŕQG|n ԏsmA1"ߟAX q3G Uqk#׸ufNA{1iƣ~QI)j@A?vP*oSiMv5bչ<8ERC:1٪䪌[z`[nqcx?nC{]hʜ$ Q266Zg/[Z4đ żTk|`@ΛS*{͟\vBs-_Ĉ;2C}ƫ9Y*Lh#i%[hNH=E:> /h:gBD՗5g4g_3 ph NSmbEp7~)r!@@Fu8-v7t}WcMJz7"$,(._dD_-z}L*a$BLxj9g䍐!HLD&-jUAyS”4C3*pAhX_G-t:(>N?<Ïy^9`ܥ;(b} #$7Zq 7j wgj˥ᔚs=w l`m8E2ָY0ę +;eHđ n_<},{.&09n. ^W闖~p$jSH_֡Q9T<%ٝ<3_ qGzѽ :g%0׵]t^{;@> ݱ(A?^cm4'u}ȄI#{VZMvUJ?'c]Ye&}v"ocvguԗ2>j=hb1 NEۧ)U0}x on3Բ'p/NÙֱqċՉ'V}[f1:cj| +k7zيN.IQ #9Ǐ^2vٍ&3sZsϟu=IMpԕ$,ڧ-D0N_ʈNmX5{ImFw8F~c}3Ϊ|G4K*:2ʏF.V_y3ڗ<6 dT.h2'K&09jɆI[~RJE_<\MȻ}@IDATMA"l& 0jZ薝uAҫbv"dڎ=v>||QE}}qj&+W>^(/phNre^&ZJs:x҉YkcN{Rǫ[PAנ=$ʄDKGՂ;|l+X8cvбǕ("Y2$k?ZmkNv✾ȫtE޹@pY2"+?ll*|EЎMTTv)];Rlqo!e'qY^fȹݤ3XE$LYQ*Q--+uXBRebŁL)[IqܯrmsR x:NkJI+ڢ@9URpsΘ>rAo.e8HR[`tJ'Of{0\f++|!ziDE8N^̷5y ]B}:t)2*ӝ\Tb1Q @Íi4>$^K)QbאTI܊]jrxWB*|tAh\G+b4JL~0-Aӟ?7[OHh&ɿu:N-p&"|_@q4`44.iI/*&k=AܭuZQxZZ0ZP+i舞M#אOu`:5dql @ A$3Y*~f/Zt++&w~}`:){>1*rrp{V%Kʎ7+ǭ_gyyt>7ސwc۟}ȑZrBQB%0@kf51 /!3SH@RҬ>]Whe'VG'CK;~6;v L?42|hmeh| _7&( GoplV\ c32+cXUy2Iz -qN @A@ܛ{}c0 |l{:O16SWmCu~k-{MpxGӦk/@=eهdy.(-5GpE<p 0 x_Qr[|A?J1QEqP~Ȋz~JY@@*'Nښ~vg"fʼU깮eNd˸ƨ xX4l%u")Mhy-0pȶ~l{W^?VͿԋ Y9B+!L5'( :*+0-ix(աΜGm* Ɖ^\w8:2[ԖЏ<\N*-*rT|ਠ]ɣQ5t/ӑ`l8ePZq$~Y=%VXw.%.~y-\jhWU"N9!(LP&|)N@YD4d{uh~8QVeLE rk;fǨ|4agp!ޛ{?f6邵QSCL+*1ew&x$jhEVdwR;il9a ] ie:9uͣ nlYyWqur^_*+<`&Tg1Vѽ+Y|ҝKڟ N@]r/Pɷ$S[MX88*C; ~1L~=d͕/<lkvQ] ۫xϵi1Nʯxꌋ0{I$ٹ(ANStN'Xd&g,l.h'LБ ^lM3@#$ܑ?YgcL.c,SV;k{8ϼ6I%;BY%ڙ(ӝxC,[ML3 Y]卧{|b g~y'3ܩ-J~9aG+ܳ @>ymqn-#][+6oZ8$2/z=yB]3к Řr E|?e˪u:m '5p-!p Dڤ)5eQ_[6,:aw8 YiLdl)mٵJJ|kJeAPps%BHɨ "\lH0/" z&qdehָ\^a$y#bh>f|-Akz;cn75G6F]++ 7;lpqQ2jCNӟ99NR(o{NnMBKxH^9;*ip#,QǬ+6Ňay$ ] &(B7XYp]36RN,2ZqD=Lҟ-X/; ppTw|)G:"LH@x6d,s'?Z'tFDN:J mt[qD1_r݁W7|?o[bN{-j0>liinm'asGAxRt윉_:VڎȧჇk㭌ky u -gnM$Pwm ';ϓ9;4\er3/ mvmtD Kclv/F+#Φ,Ϟ &~1OsXk:35*`S,qJ8pksU@ (*,g]]\E!j MjQj(cUXrq/qװPn*M8.h CEzf[u^g|c7pMYh-;?v4k?Lfd"laj =aN Ykސ+Ƽ>0ٷwEIVtPŇHqg;JqeD,<<)Cф?])SY7ϥc7:!C9'-PE'sx~_~P78Ub{=>i(szɘ{::z`8|hpxT~y0mdL:#7$Gnx&y4Fv/ڍO֏'aYU>mzzHhd٪sdR!f}Pz2u % %_MS(eKCAM 곓058_tZ򜱞;PF0CX(}o[sˀ:饰t4fx{]h܂T|~O#_ rܕɋnaxG򄟴݀7moBu;NH7e"Sve:ZtܗƷ܇ysΤ@{Q.a^9W(|Gjx9WNƽ^=s68OZPR CI~|J] T鍸m[?y&Vɬ-L&gk7Yʓ]:h*M+Rw}?KȚQ/z iMXDQ\Ƈ\E u A?_N.Va>ǧ367aYpu7Lj5/] ajY4Xem珧kfG dbb-ѷ􎼅$ ʨ3zوQ,u ~7mj0oGVzH'Y[K5\i7L]w|&U+jQq_=㧐ITwD[݈gSQ٦15]ŎƑS0ܹmuͥ}>b0)t0ZdliC3ףAduVRd**^Qkd-%kRa-: D~"e!ˤX^v'L{jѶkX=p*8tW8fj>M9ܑɧ~iqD˩7F. C4v-6W< 'guɒm_ ,ƫ~#T6Efx0qB_x0Aح|$VgGV{"n6:¤&&M#wQ]7QEW_tG&"&KN>_d <[@7R'M6G&:t"={~1OL:`LP޼w݃/a:)4[c&*:Ev yKM9)#xLg7MM~9;̽iAȿL6etg\ǶȤ@YxϗE H |UD"=FHvQT[r֟:VVz?N:ܜmj[tz}~|>_0}W8ހMg *"?J7d{6ҁ\Kfix~#dyX 8\7oΫ7w`c2pZ'ÅX{}gIAIڸ+U ]Gá%HH.#V`"{h]F&S+,!n ۢߓJ*\~9'Ѥ$FlCN\K?%me 7e=B/(]2^VC8<:2`+T& $J3ٕиL+Q8mV"ay 1Ua0yc DS& u[c:U-catKi->qΣ2jF!o\6lgV:VyarYL=YZ>s 3s<ȯ+ *)ǗLN8ňƪkf?JȝgT~}&4Rl7H +s\i|r%R*BCciO]Sv rYɤm:[IL 8A 0Rv 6:e܄ƌ'YY"pt.9 х~\^&eu)=AIO;G.lZjnEg48aYDwx+/yh%;G>siV2k` ړN~qڥt6ɻi<ywĠs o2? ])A,27mm;\ZfwEW8RecM@lyJ91&5pпFi1M^wzbz /Cy#`[΍ܭƐ8\ԣ]~m}ybrD)1%B YאwM}UD"Es1zVg}Y G3Yi+ǫ3]%q$8z{wRӫ~6@h'?{wdG@ @CƐU],n7藨 ZvS)VUCF< -܀DCF@ @  0^]. 94 9+,ˇpʘE(?9=s,c|> !ݛSB8Ny.uͼ6mSv肃,Tʒ {@ߟ647dtsubƨ%Wc7#$ Aw&nh77GH`bC?؟|P"m5LhSHaOWJo9)2c/'9UҢ(W\4K#.emc0s)"|5e?hГmѯ.ߛ7m:ğk8#wk'3ehƲ w1MGE %hs$ob@( fPj87_Q+M zcFh))Rw.U)'3CϤ8Rʩ[qpn B&kxVPPZq.nZ+2H%+;&^YvrJsD[*]{VO{VsYWX9cTgSa)g?{1 G 279n|+22$!@ |6b}]d 7wy?+ v Uu V{=`?r[=UW&׳-)Lca=MRhh8=AgȓjnԷ{Um+u]o7O;rFO_{^WZ?OXt iw\ベ7g0秊¹sqJ? L`g4[^b7A׻D[򴴄9ޡ%PgAl"Zg$KTʒ9jN?3l t;#AAc4TLyCmS'PIy΁'f%GM}6S'zn ?NzA5_" M&zv 22@`ѯ*vq8E\5[$ykfJXsst3Vyn: !ulB eePAz EK)i' Ն=KN {S[nq'kP]Ѓ&\og|9{lq*+ c+CK0zXN)pmos[4\J ELE=—@+# vujNVk]WavHGI\KQsox'C/g1x5g9̡G%WU0d:15A:@gJYh& cW_OzgfF'/xgٶ38Ɖ;'hyܬÈˑqT,;WI$S,W ` C5Ώ`r.LN[}&c"+ԣ})^^&#KZCY<г2v:}8U/WUy)Fh1q`!lzοd'?h _vxo6CQ =)Ui¹2ё蚁_i#䌬 0v؍g\ g'.w>˫/ _/RBރ8WƲ®Fִ.8Bc|2RovEY縚/Ή8ѧIkMc )afre&{Wv̈́oZ$7AU8 RyaO}lrcrWm7F?kk3vdlh&>^Z | &,r^_@sjkL%ot)[Rfƕk/C}ZXc()ys):>6-y nԇ7]Ť Z#2VҍNX?Au&tJ׍K kDGB {yK͏`RC.۟ԏC€R؟α CHEh E"AoO_A& ߓ5dl 5L=;(EmA44.cmNIA9zg&uDRּM@W+ XO/_Ytہ 50c5?E2b`@+.G\;.cBQ#<ܫ%tDu1phεOn&1d-I:[|YY(/jt|l)R͸3=x2W E~55 \dyfv5֏&l_ױyet etfŒwfCV[tBOh$'9ejC^<VI[ OfeM0im98aVAit=km->3 .ݕVqOS<.ΜC'?GKOƁ6Jmq$|\7E[\ .O ?Sk7e Uh~浑]o8C/.ΙmTWsG]􀔿PEuc//oȓzvm;̗>R&QEW>>I !`vknΙrռ~ Ϸv+&}T0fmsw4{1Ш%.\k 1w18{m=  GYv$$o1;b3)ʓ乓8QnIƛ!GjxR k!XIqmǙa+VYúCEۙS`39hGjK Z)qVh4X>*ѥjh#dztNY/{u sh 8h $s^ fסּؚj_V o3vcƛ9ѽwGkߤqJzݕF2\g=b<,JѾ;y{$B>6{dn8l?)n08\g0 fv< E-&y}'ws]X҉xJ5;NGoj8|6䗶d@[!<:S_8 HF4KmсY=>zکL1 e刞,cKX_xV:|e`Gc`V`/ހ;v y.O:0p \w|p˼>uuIJ 3`0Ss 0*#AaK7ߙ $$# U*Yzed<1X7VebtG|?k(/ox{QbZ8XGΔl憲m)]mFf~^SX1Q0G)OmE^ܪܚ㧇TCA80yQs(мd3o̳3dVEhN?\rXqz@^MSRzPg0A t ~5BA3o !]WYR5.'k}vxwhihWU-D~/}zM A8q-Gg] ^ 3V n393}| |3|g|LuRbT,F^liW J6%K EGlwMWzuq@V.w+,Yr~3?'W##gcW?ʝykiי~;#ܳ_N{{FX'E[dv./=i,TԞa:|](8ϊ{_Z&aGX&^p_R:vhY㉌Xmʋ $\솳dЮRf >Je R'P 쓓qN\w:{n15Zo>c쏧""l' @k*_8 .W^0qw7wkDy@xyYu-0v6'2"xWl+G3L>1ߟ:*bT"q<~ 2  6}:H3;6s LoH?isL _Yɾ|W͇6bVWdKj ynpS!+chcm([!/?cKkxj]`"h2~?3&u@H *n Y׳q%8YΪ9(U 㖌bT^Ӡx8o.@sHl!,OP b8y^3V)R)ljc0~(w ḶuYEUBӅFY!0"͋~Gg3~q0|hOq(2%bOMkiK59C1K_5ecP38*4HPg,x+,Y7k㎀$wG3[B*=x-c=4ys "y`BCyow* NQn^x36.ԇ`ROpP&rf5jr;yܝ6}w*8UvҖ:r:2n莳 X[=@IDATj6 h%>϶t +#Hq{K?+O*a Ԃ^-@ЀK2Gyy;!9zHޞ/.Ĕ1'T_bw{po16E UgŨ7L ypif:MC=PSNJU1@.pQBJ<y56W_83ѧ~%qN +0;kj r/|{+kXQ |7;y}b@ =W/-$% nkSa}2G-]e< krM|NPGsSX̯a~;ŰoF$cL[l0!b*#]ڹBOYJ2>zۖ#lzip-;k/pݏc`A;_gg.aݑv[CT0=N#E^.B#w͌XUOL/cT~9K},ϑ`L@?*  %UfV~C##r/yGWZg٤vOIv,&o=`mAG UYj_ -`i"-q=DH<Hs f6 ч2f2?.g=;Zhɘz Їvp0 . xcZuJmm{jޖu8gak۟%!ϡg-o[z+v R/ѳz>)"(ݙ+gsun%kA)]'_S?CFBN^qR7jCdK  00|3Rz6ICskd0k.J+=`Le\#濘نȲ2bgV~2D`G7qH7oǝQFǒF! R5#2biqOzGOx*rnTV]3"#ׯqi?)y@NA$u"cv z/Wx=:fQPFY>H1ne^sGǬ̖>1gVX_x8 N猁Oyo gN߸az ŁޕA.t/V rL;cǙsㄟ|~ Ȕ,8'o|r,TsQV9)DKqpHkgi?}8ljgdiz{R3FZ;succ @l[qdWe֣ϻfu1`e۾z*θ̍eNo5WЁj.]{+B\tb‰ekQo9h}Y[:kMK)qKZ9.ΞM78B5XC8}_AGfɯ2"B1Ό$NCkhW%20w6>GfcS}M^wD]DJ+UlM^slAERˋMyvɨly#-SRu-#YC::!<%U1w)8 rfJNROcgNXyFXgl.IJw~o-jȫ/gwx윘o޼GZ["UybI%r}-QtK};j; Zq=K篜c0g,1}+]4?/einNfga)0=ﶖ1cG^~3vyъ<`2~%է[-v6C= *(1HQmW6N`8=1]lAʈfӹtgEJ_Q]W:WagD: ELWבM>(gct).3q3wTsU~ujX4חJYqhFسI.~95.pQ<97D<ړF}pVqQ~G/ZaP%*X8:ބ3װlŌ+L2r:-FӪ!bRyW|ƈPЅ@s/ׁ_c 5/l-n"CqjɃOmȸd~IvOwbd,~AQs)%ɄqYá)\C#eIG^"Si$h};{.̰myy$zF3޼m tVeV0Y{Q;QyOuB:VR< 3 >LDAAW<:wj 1ʂ1 E/Lޒ~C·s sʰYWChBkzz;?n!3ĸ$T̴YAr6^ov;nk [ȹ`#RUd?qr H z/]z0\FfGgS +/mpgv{]L@yuOzm YFxHB;U|eoڑ]Wyoc_N~/26GDֺFӾ̑T3 T'02Nv~2S̔o_&a:i?` )$~4F >8@=8R<[zr:#R'ܬȆRht2ӟz?2RYcp\+K^Z 5;$Pa[wXBgS `S)ߌ %"FVzDWNAs5:ąg}@uɏL#B@=QvOU4~l0aZwqoQUQx7,I J 4 9ʌ!Lȿ?*N chsɝ7~Czi: l.sXvT3S;cEj¼(H8t]po{W2Pe+cm\nz:@~~68Ň9 f8⡋zKCDV6:.ݖG ]*.x"ȹ¥m0 9I <!o|) corj}lLE^e9V&wvm<{7^iv9Fn:v۪Χ*o{shLi?Lٳb#ֽ:^"O,/}2 rre6?5|s2wRˢUKz86)]-d`hUC_z-37ŧvkC{ѩzo~m<:XhF_`; uXw@G)bkfgǶ/p0: %ϧ|xژO0NøyO? JcT\wRb1" *gE SYB0F%8GL` J2. pQt!zS&X ^];%u|E%@{s՜:?mB~~ڎA( ٖJ>ժpjݙRYi燵X ߨF‰nS+|b0LPdgB]uǫ4s(QJ6 xJ=m?' x瓡|\šwu;TH 4Ck$@2J֐5U!W]ܾ]O8Aǁ /^`NN /Գ=GNqV w+Rx\<5g^Cߎ nE7[!V&q.9SO[t^/κ;x;$24^7ϧ!? _? ϸ.+sb>U:v𮜿 >~ )i^pA\/랮Ҟgg[[cL/_=;yФ}G~Ȓa,,f\y_Ak!:\Wf6KuCl^7Ӣqux0vXq;g[#O݈x5lZޗ63e8 أiJ2XwHx&9SX: T%SIëO&Q'o{eu\`8 I_m@]ux>Ssmj݁Uzwg鍙۷PN1zqN%F@+i-v8"`dBu KthHRp$3m7ڮ~#d.?l{-EngW63ӞYs:T_9,}`ZDpvvVK_Ny#-q~: ^`3^8h_je~]ř}e̫}ͼ ]}Soyd7mbw0{gwoMԁnVܠ@|;ReXa7k [B\3j9 [B(3fQ<ֺDb`"Bih?E/Mc`O~oD3q'|HhsCOW"p6_ʴϡ'_ܶ@u޴%Xɺ b<@ufn=$%7+Vw{뼠pCf,֛aH=&C3Fh\e^LDc4xc)ZPh"Gx`#FJXi]`H RW7m$ni4| Xo!Be'rݶ1H&58s0jFv+ynʠPtC'\Ӄ3]WpH7BOڦ:}sHלM6DBg[r>԰'/Wɑ^m-^n줾@OРk,ڶ[i#&Ѽqw[HNWMmC ,MfYY 3 N~x . `(s&S&-ՁL`ʡ$zU`κ!7nbaha2a7k{O5.K -GgqoW԰b[n?1xLLSF%92\ŕ|r0xѸ bI**/WbEW{=T 56YN #.6xwX+0oLD#܁pLh0I\IKW cdEкg5w38A?-οL+&pD'QC{uEp1&XdxU$˷W5\'Lc',/.p| ^2ȴBUI]x?l[)HJbsúU/^Isp AN#˹|Df/Mh;EBg S6R$PJ@?q': Lkh4̎70"jrWNڜ_@5QQGTϚYUwb*Q~ByH 89 9~=]{S3ܙv@QZ E% 6@;YYcmí( Ni!\=F@9(FSIwD8js:ҏ0FrK5J@8˭13v08zH}8>JܜyjC9\0PC/mhφpԓ!S&넅Ϩn̗oG yJC0eڊeF1.s'} F% oj ",J4) Ie(a 5_VGKFQ$+58"Sw gyH žE)y{j7%XqE25H{9@jg.cVhp W08革/( (h@gћ|+ۃ?K7lUrC:Y1651!J+9 ZlzϘ?SYiD1@4|hǿQǨ:56h yA~vj-dMV*8T8\ѩ_^zahټsoר'/{Ȁ@O} ''Sgf|E q(uCIv{qȷ 9. ZX%E ;JsymΞ`D EѭW@6[xpxiWL''}T9.gvYA_JgmNu,l_iW;uQ&p;]h|9;m>0+/DIhwp;>3Vcb7|6x3<(t`ls(LXX0`I@c7|/zU utv tf_i*Tѷ#R#?1\6ǐ9KUc\V4*Zv؄ gEOAg 3r3[AByjUp_^ "QGF-,Q&YuREAyPP0BUf#v6m`mz5j?TjRZS=(Jg82sj[}׫A@2LDz@d4۾Idy~KYO; Cmz`En[r-yf^/F<9zچWFQ~B-;5ֿZW}40na) B>XVie mtlaf^ kʃ؍[D>i1ZYiRiuROY5Tt_LH-};o v3!>onN0 gaͰoۨ%U0+sdUXȰ!df' q1GOU!E10!"tJx?o\[1D8J߄<`851f:V2v]gP:UpQqH;S yϚeŶ|47y![_X3IW(g̵y`۷kq;R 1 \ 7wfu5MJ \UiGY}u 2J>J`&M\^]=tsp +{sc<3k )P~IpW<|e+|ӑ ;}8%@{ᇹxʤȈ苮 kV}=UuEO2tYrKK}m'[j[r)J vH;+>2*]08t 4l+ W% ߍZɏ4;q_ Rf!;ѝstLE <'ؑAn~ξɷj>tH]ia{^Gyv( g|m(8@$_ayT8G8}ƨ3,CRűtf%݆^ɫ8l"e@s= NZ+ ؈bC7%!v'2t0Na<".)l.7}(U4 !@3 aT?/82H#|@&SR*L53É#WaH|!(@ XmnjXqs*{)e - (nͼ$` 顼V%#YޚqVcL$>F:/^S֌j4怜!޵yQll2W筸qH۝Ȩa 0 {cr7JP++'_okH"^%8㢄_*7r-£`s ϔp(AϚh7N<yG[03"PΚֳ-ᔮB_2}ˡ s'BOa^P^?VBsbCQg$[X>n;ओk Hqrvߠm %9'?|Y$pĩr#-p@|oyA?Y=GMttMe*w5Z}4]Zǽ3wR녾`7 ғlxmQ@9u'0[fd|wM˼@S:*WَgfG}.6~XWs\ ~BWo"+_%/'q׌+0,,з.ď`кwR$';ȹRvhmӳHSQ 1Tq$;SM. U_,=n۬_/-+"+qH&2JWY p$l fbu+r|X@D`ADQ"Ɖf&lT㏌BƑ#:LAdQ몿qmKvs]a|J8 sd\[˶x<4(z/8_pUEkB,UpVb8ȗ&VaZ@W*WdV+#~0Z3X^x:1(ͷs/sRyvݗZFMӳ\3gY8lq>c-{_ yu][1N֖vN\ fYqM(\ tu;Q1`zaN.&s])О];nc2'~;4t|=79wyǨO8\)=Iqh3_Lƞk,DpNdug#OZNҜ, bY>,zƻ!TnRHOspib:z?8H@fl_%{w3XzuVzǾ)GA lϮ3rRG7:s)c=̩npև9t K#iWٴ}zEyy5?4`f  ,1w]e ;\fkvh~d/u3|yy k{W-M١ '(K+u/S:H|SOjJ{}>bYI}iŠ9^7@P`m"D#yM_ػ"U\B&|>fNמ\(B=FKv)J+"ʫ(a,)a볹tvnRoйU(˨ -$B[=>HF)R={ +$^% Ӳk4w^P"2#*iŶ,ZUEY( 9F+4jwt>΂VwʫkoڊEk7p8 '8 lc`rW>0H%c>'R|,?N R2X=/0ͮ;Zq/Np뼘\1uDBxCS幹|D*]aj'm|?p;|J`'='Ƒ1ٕ/nmeyMr|V} N&,=yǿWʁ\`!6NvIc8nݕC^F,x)E395S:"{#ˋCŠmE.m5e͗yu})kJՖ:?ǹqytj.S`OjNٯ̦Giv(TDvuo;_7u]OplеZ,Tm[Ȩ$?z_x27Kg?;On()8 <܂ nX"%N=(bMb,vM*3S{uNXLQ%"&p "! cJO`AGa!x}r馑C&$zijMy9q@}wG>zY!2غڂ+E }ż;F{tgl9_cI83"N+-|,~U`9mGsXu/? }8s:=6\Sm'=Xgoxz@2 [oT}iq{3Z9G).t2GXd\ Nb%*ja%%]lmYHqgsDsE( @~Ee_9Fe8d ①{P7q_ʁHصu68>S7SqPmDz7e3#7_'pThMgB Sa C;~=mçD1Pe I Q/AW*58('9X0Xy&bxCV<+X[7l9UZ߮:l:^gUi ]*P" bRy}:-)u/1Ж6[5c¹0u ?Ahn,|K 38WfkAׯ 3f__ IgF;&<_ GAgg3dn*:qG2-kV?C)$ ԌI0`)k6_ٲ,:fa W{.%Th7BAސmWqغ,F(eŜ"r^\MkH`,‡@%sB9/aqGJ%m҆(Kl SdzsE0iLqZY2VFAӵCcX*Ο(C(jxyzb%iN%㮕%m_>Mb(ʘ0Wk2GjFh:_ @+/V#\דګAV^ 60-M/0.jХN uSpd'L"p|EmKŀ8>b1¹;  Hx(dhRcBBHYj gbDp FyDTpьأmpKYO>=|S` +dSmwjsn2Gqr)5)+ -!L21*s(ؘk-B:{Lnၓ2PT+y C`3/S:MSbYr.-V+vE`EsB1Ji PCjk%}iz1OBx_F֘V'PꪾײwF纆Yk942;B9j oƫ]x3ѵspF冧<~JʱSd0m_pCxKE,"v%QBn{'Eo?;#yCrpr4"UfPOYl:#_rCcbNC7`K}k٠Q%]' Nۖ9jstf 5%P 6/I[l/BpNVrf! Gr֪Q=f7(i};uPG ,&m? pߙv8;2kdW T5G:d":`;韝62>}o_{tz^}C~Kza{POhvtq&WϘj~2h}v;!q`N#ȃ]M  A;}ԙKӡͧ$"O"[ҎK=?WF8@ڞJS}}.Fl%$pPS *l)qsLhϙnVZgɕ|4+*_LٺY頚/&^ݝk 1VcSRfy|gG侸gOnmN=s\Ayͨ8ZkZIŷC>[_8:w]8Aɣ;~BX 8O#8Y`飺W̦)]'V]'쿆O#Pn&tWFe}=‡Jzp q[X6; ["z>[6} *V9oAZ/Eq.7ْ s<;_NQpJмbڳ9t۴{}r:Iuw̽+] #0jd 1ޜ~wpl/E,#ƹbɕg m71eI sǔ: 1 ּꇀ (k/Jb( g]k7mus[o0 lia.nPoI&9_νA}EZRd \.}&;rk1Â2ɇmg39(J@yTE ';sz%jau^}9B^jk%x̗ lrܵAĵ.GB mS.gC=2YOcJXĜ<Olpv}H63iV|K{8nT?U*5]q;b\z G34 5,hoQDPala a&#qa5lch`!`4!)(z->*a6JhAa [go{(ۆ8/iP/-h_wI FqL%^XLOEBjBizWB>KF' Ԙ~Ȩ}Vh-C'GFJ) =2@Foqw1 >8?%ƭR:/PGi"X-^lq!uDgCT}^ڳK:)(Ly3F,YH>S VG dSf0s||LEſýےY?v In;|/3V+#ohZ+&CpF2[ϥkVn 2}vz,-2.mOrhz\y^ICO3P&L@p.`j|os]LiƜ8(-@G`K~ZՋ@ŕ#w阴9X\ GT^ 7Bav~ׇ{c.ޔ Nmxor Q4 'V~J(= )M34҃=X:(`[G\ Jg|J|1 c:PН2qNTx)sl_N hG Mp'ACƅz~Pj6r9My9@;hBQIxPcT83R[zT_+'Q Dlkk3ѧ>P_#<U`q)ˤaJop_lT{~w{1N3pOXDx0\ @j\.p%gO1Rß/<}E4up{E:fM~ /tsw 1mO>lghmSLj#B٫L~tC t<\I!DGjd "6tle{,mԋ63:{mo{=8P_ȕrup]8LĚöxaAnX 39ZA?g'<ȇz;GhX l:,o14a%0[t 1!Kz2}p~Ua߄}6r*V wn#PIIeӗ*RK(DfZdj&Pڽ>` n5SH7ܞOš!)yGA#F )9Gw N==x -pWqVn,D9hBreyeT* }U94zW,-=s@-}cdV0eVPOS͓s9ԗ8 ! * mZlZQQnq#\fo[#, ]"k+'9!-(FQp1I7w&5'Xbef4/y_7tۭ'8#3¯T/dpOw@+.hL/'[8v,Ϯ:4.WZ7 q_刁 ;$gSt.yUƐAdAvğǏ`D&B׏[9Y,ՎφFdn'Z:6ƥtfH%XBL= >fzB[2p„?iW늯|pŝ4_3O=ū?Ca0~\fQ;Ó@6$1QFPo_*.x lO1-qYp6͕:˺1%Ft|aR\=rCXA00Wo* {bRLj ɪ(q,A]?P |L^6Ļw3&M.9 E1Ѩ/J N4xmX6H]q*cƀkNWy)0ceOj`t9ڎ\ZΆ9J{jhO(mZzNP(R~XgL y,(ujRU {o'|`XQ{o,~QYDQ1[qp+ykd3.([1a*,fù y?*iaL8]G+t9qFlI -ūB58JI`Kz\ƶ>P,09>m˘_} 9@FLT Y`}!} *C@v>p}5\-.،HGop0P-\2('Y)#eZd%ym3B!e[`G x*+gtl p7z7OgvI0-%u?+Nڡr(IS}j#xW0tʂsLT/9M+/b<|?:s*73mKja$dҷn_AZ1FᏠ|!Ӆd-<"3RKń7$@JO<ߙz͛p`o4L`psdvOͲVƀY@>>oVE_1]!XɌ:r,!Բܣ[Sn`8惩7=r xqK>O.<Ƙ; +߆v^ueX#YCd(fUCZ)Ǒпu&_Ne΄Mf^gF\(ŠN[r՛p6Я ոQz`81./g^ GOn\s9 =qN%j 19C\Qou³}"OA{:K@PDP\]k4}TWׇ+5#F8@I 3EBv򑦳߂Eۓ^;|40|<0ݳ^ ¼K3PZJ1x\;^yN?ぢУ+a΂}Ype;sq4f$ٖSѸΞ^/yz(*!쮓7lw R٨*fG[;f"] gFBc, J,N}Rȿ{ bNoHĮ*O3+cq>C%8yn_?s7ʊp~W0c):J#֋vgَgB-fD(jCnTOf,G>Ge]4Eyyb_pu`#/0@,9߀ȣUqextGƿ|`9i f92X7>V\g#3Zg4fN카&h^:u^ <ыSSqL~+C>Y ]+q֕ _o8DpWZX137f箛v+u}o+ge@1.Ċ!@ 1LP7]oy`<4 QZa1L],9JV1E\*tpijPTZÀˎQUj6v FGڝ/AGŃt3}&~:̱_h%[0IN9"m[ң13SqيCxW1`] pp8v8^S:cU }\44#`O!5cyN+ǘ3U񟫆ג1#]dKaI S2a''|bY6=S[sԫ!< jSH }v2닉n Wp`J99znVe‰\Rry^K={k\gXۘG&sV?})cK{ɝxYw\\:cXz轳q"q_L_0bb8 I~oӛP^ PG60gfgԆ<0Lj ~ c1p !xu)HvP)8"< aaƄvD(b -C}A2Ƭp:F{L1龳*bƖb▭1)Z>;jE34!&5BWkk E!JYbB{wF| ed Vp0Np»d>}{B44-hg>oSaaZgW;xA(P}^qf 4sYr |rrRU}99?Ӑ7߷s Qb)E6g0դ(|55pB;8[=\] O笘J2lΜĒr%\I9o&^U۷D 'II^hϩ5<~L)՜ި-,9@DcNK般p&caȣ]#ӚO=`BtַoI8ZZ5Ʒm߶>+~K 9P銾Ɖ6znό7oਹQq0BZ<9[Ep  " z2 A;?!03õ+8 1i/(' veF8M1R rf&DRaqd95~}`*D2/Mp leA M¼l-2pKhp̛om^$B/KGG P ͶE@R_O*]N0YA>޸6K)0\dyx|\x-  VOW)X~˘Ԃx7霮@fFwlw͡^H˽öRNBe_nO,G9*CEjĹ d4W+ڧpsTyU,FRt?M6»SOL](=J~4)֖._}8Zر;:.}Uy2ɿ9R< #pĥOxN}qTW'S_^hj[8[ϵj1ϽݙlV6sQ qG>tS<^ÿaS:kܸȠp- L%$`ǒ% &R% yLbRS(rWZ:njr]Gh%)Aznz8 3v5B+7Wrur*^*4_p?wÒtY`m÷3H:&k}1&bS&Qcw/ae,JI 0 6󏱀EZ Cc)Ĉ0+MS.xB|9 >A;\ic q&ՒJ7^a)=Y|4¾Sq2bofتa|UpaͼaCgl<3bqQ }X9&>gB>g/O: ^! `3kP%4csqP"hѡؑY0kҟ6 ]hW\a><1KfOLCh4._]pX}3}T7ԅQԄjY.<<ҟG0Sp2Pod7Ts^c~xN(*\` Z9 g<IvOX1mN pƾ*!ٺZz16p)o ćOGvqg/M+d8';#~5|Z3ZW|Tǔ ns(o7[o]qqxh'#Lt29tΎ!2:+IJJ gdD͉JPE4+նo:.iTc Lyx/-v5_!Sĉ(uŸqyBsͧMh<`|#7Y[z&p.gQ~a NM~%_쉹]zxnqpz K0a7~0AybF}6ξYcfQkJ14V&\v2i+NT5W{ŎY&9FY0 ߮^"/6aaזκlvViR=ӳN\'' ݗ@B8D~6yٖ~Gv.'`ɷ,ʷ3Rfm<ц\3`Ƴ5SҌ@{Jo<3XEy2 , pAHTplPB9h|^~6Qǂ2__P˔ 1*: 6؍$n둝[7/*/i و~C?[v gዞ]6x^wfx8}IY?A5U΁Nj{s8qJߢq4l3{gؓy В BiJζߏ蹯ӎ h #9 q-lUcWzV5+覩ѿ t+~v\LP%yMpFnK9ŁMTG"Z4/<nu`e19԰Vlq,GRڵܕd1WpW-#:3yst!\' `f?w3俛ʘvΖa:1 LLBx51)a>G!9sO-l%E0{yW$ 쮸م>bsFs@ oni0%/ RC3ZIc`q.gB(kejnc=ϕQ qP,gT <k;=ZSJQNXu[8‹;30Y̓y5C,ZV6ZSjضKRY<^ST 0<ﳑ~lp˒׭2o#F\ISzae$7cq2a|eu|ܺqh!pp"5*I /3AZ y*ݹ[OzգL[z}Z|,"ڀd>]( O&r7zߢnV#:n/ǾF8ˤE^K;-߾q`6D:Q=ի8{2myQ{] fjW"hcm܏ޟvr$ ̜W`FaU%i=2j)IdP}`\\]'QV)-[Z"Ϗ_ԼO TʘDžj%Ux9奥ڙښnӶ%mIj)kvv n`< 5o?zsw8JU_U@ ƀ u1֤ך+yVU/ ׭]3\zɓXMw.hUȥq8vfN#_ DZ$zY%JGf~6{cߙ:$vTq>## zhC(zM z)vTI9dYyF,`pJVrRn85v+-6ۂ%9xkZq2HWYڰu~]J lwJKsV_OWZKI>;a ;5". & |K+/gt1JF"f&\ÔL"@T0adI:vAbF YeGG@N0ikX=ZYDG!RVvizbR{02"$8eM]D1Hm ʺ 9`{7w}k@kE5 &e9^1{bb f)T΋r#È`.h&آ8KQ @+HR\+eEy95ǰ6&L|yfR2?>V3NbMxr Im8;V>ʒ/(XzW:V9ITOоv:]_˿ ʷ8k }76ھP~C|>/<{t2 ׍i=O=^ hb*zY}]NI^rq NFGgJ{[Ȭ?[AʆG"(hdќDW>/ Ԍ2|@wmY\O_E b& I{ p/N82w3t}Ʒ=pƥ{*n}RVgx%e/A~:t6n@ړ6mKߛ8z!OPKCscnR`sp3!\ ץùlZd^!~why[I`x-€sl-v>!*Lzu8Uw+:u$j+l) N:[E%W'@GGXfS BO8 CSs NQ4 i/sVC BEއn-ߍaz3G H2oU\oň-pXph XSp,(U '@S (Y8wM{(9+ -Ĺnp5O2`<ؘ#ɐLKG S0-{<^^k$1Apc))gi1z.9B +FCַMS 2pc1h(,^?9 ԙC= ho>n6˼Nx[:s}2O-q>yrS-JJb>[0+Y߃y.TccDpm8w:xU4-UfvP*7#<V6nKD$zEx;=`< Π<&ʢ ;xa(_=kI] =Gs|HfF 7W &dJyڋTt27%~^ ɯE_<~ZxyFQռ5L^ 4Sʢ[ 8dqA>;|8ɣs;$D(KAoU(W[3i_Rqmk|fA?͕qZ:吕UT ty z2ϸȳ-t`MNE[yM~҂>Зnc>Bi W;,A[X/NPuWy5/0pD=^Kl[>R>K}KQ;}vBUzg「>h S> Wq]OUg+.6CjÆ>>P%$17Ѳ!Lʁa88D-AB|CTlBO:%{05KaR\t΅˩C¥ eJy7`On W83#0\l${Y\K$Jx+dIp3B&nt§COxi'jTÀ9)Mj3͑?k}dyښ'Bq. 7[QKAW(9AJcDE/)"-|6p1ױѪ@E_UUsZTuVW]q\. >஛ QMg|62Kdl gq G̞8~( OG-O !0@6׵?&xNXh(ǡX_`x?wF@۲_hSnʙ1'܎z3*gc݁1+pQ>/x; t8Rtx'VSq+Ί7ϑ~#\?ǜz.wK 0ZT3T!6ԳBBw6 /:p *ts5<Ϙ؝2_Mձڋt%1T*_\T鄔g'ۿd;YR?@koi8#ϕ/ W|^\$Ivٳ)c6iסГ{.xi|tWA?YeG^hҷVeU6<?0ڇp0`G}PqT݋cv|IWom BstO2L鄰g/]/(5 ~5ˆrֺ bh{/P03kFJ E M)0$,3Bh+@Iq~ɺ@f&+*YEDD:P^{ R4L܄ϧ^Jď=Cggg'UkiN4jfЅtϹcfF/ !Rk L1JMo(qxRCZ}\-ye)SV%E(ƗG˟1cgOCa(_`a7j %v8ֿ༷_ rZ@[-]R Gv"|v4/V1~Cowùc<\h;+? +$9/8y)ouøQ*,;AG (8\:i;Ev3 洠^!7_Vk 5E5'! /lē oM1.6ߊ?SVjqE.FA7 k@Eg>xߪK#'k`4R`,pKZZ(=' ˣfx!ePšpMz&yV%闌G҃ف_12ےٻ{Sk%¨sZF8={H_7CƁTn`<TXXn|:<*tl:=eq;Zr{1[49h|8kyՏ.$AI=s 8۰ݙk~IsW擀- y|R:R[P]g=%IA*[Xk6a3Bwu"zu)I,.T= PtKF9`>XhSd XSֶߦcAM*;e Kd\N Mpx}5^neEf;2&ў8+ƋmmMyK(Dqr5()Ssf&ќ~$T(xꗕ" )+Ч>) 4H2}Q,OCx[0pY2{[>̀z˂0'sq^I v ĸõL01u_ ;Al>Bs #1CP)AxZ'E ̶Lpepr,&Fwz=_ø%<qMݺ#c#hļ9Ͼ)acqGy@!PЗZ*_G k)[~3ZLwb=qYCP4;"m*xbвU+twr8+DWNƽ@!Y!J#['ol`MqF/c1/LgtIʚAmз"P!lj1F}=gcz;Cƴ N!vVgB0{dX\@=EJZ0[Fh= dS2 [{GlO&FEѰR)%̃7,̌wj Ë[zieI%2[&  {:|C9nyX9`[ 9{,Oq}BPͲh3Bfb %+Qt-ǔ3opG6?[Jq0_߈(L?34/s,aEEyQ6iƈ5_nJgsV\qܕ^&5ӨjsѠ|zGyY?K_ v/ ߸LlGЦɗ4{ cCY32ԋp@Gf׹~y/z+fo FquB}O8FqPaĔ)c-n gh>26q?s6|e5$R-h6R?;' ߌޞmC~CZ"A/xƑ M_a-4FQ\KR0ljе%m'ȍ/ wC_ gܚI7|06mkK\R28L_fr:+ˁW[0.ЈĶ_7zpچ8:FJ튺)`/8ww~_ږD5:L̻reטF%%Џ(pt=DQ|%_t'xSf @VLAbh'`` g/f3`>%x)}f0Q1X zKH2xZTbx"Y ^\x31}5%A}YuJ IA!]myPgl3f`TkpZ'xCJۉAq i4n92@)H2B4w^pGJsCڠEqYoڹ^muZDŽ\mkVݛ?i>K|uGf*p9/|H2p{Gc&xJ HIYyzqHI=%\ Ȍo`0;Ku]ێJ}`.&z9pd(6W+*$pʮ_L1A$el\ !S9jSղYU9-[`U6vJ-w}:*1ߛ!k 2"0B xPm8 l*0wOZcJ 3L899,tO(l:/[*s8-5) kŮ(E!#Aҟv~64t.1޺¸RԚ#-)u[֐q^)Q8@IH/8Dsbְ&oB{jy rew(_SRh J^H?_{m85P)*YBX ͟p -&]220t*~847¬gӜ3bDfyxު5JzƊ1Y&VH굄|K1/WB,x3'iks50OoIƩ>P's/ !< -@q镁h>x)̭O^ݹ ʐ/S+AmEY1{`n})e-~\1^9W>ws1He@Y{,h軡pPu(#;etQ)bDeh:Oå5"% Ddc+r3g_V~.}oYN x1M[8|e!FqrE.}Y$ `f|  M0Ο z2w6\roߺ2311xBZ-z{=΀vOsXpr&`suCƍ3|äs1\ )+W|n><}BgOn6$SWf<;vK|=Q\㙎1^c22>p!~SW뢣Z| `̪~~^TXRKzӓ{ˡV{+u-yn}QXp d<4mG}C9 :܆,2G/MY*UpW02VmO\7GO B)9rݓwa/t8ICiRGl{>AK%:L_W(N:Џ, A_X!?>ٜ9\,5kޔw}r0Vp/laee ˣ+Ja09?pUj$|M7 kaVq1G3qb~Z~7@eZP 8o"QLܯ/@EO/+[ LhgYA]xtC9UtP|t#׸8DO >ʲₓE&Sk[LV)@}^ogF` {:'M4e\xZ^ϓXGb4X;q,r.+uM݂D=^gԹϡSZmAՆVӗ\'>댁|ǼOݰCf(gSc<ZdHc}U|(r|rS_ymMs+ AgM!hrUBvk7a[En&. '@˯!t-QV]*w˾Ƚ @'_H2 z+-DMx;3"yQLZ$y_ vP;bI{=_l} l7HaVyEi;qCjdƈgqԩcs~4[%pn_{6۸Ӯ8 ] tf_ʫB8<+NYyMnu~OݔQP%!c h,L_LLQ[C8Vؖ nU3n*5)lWcjsRȑ}A-g>G.8w.l'v=+?ݼ9ODZUBt.cR7nFf=l땃ȏpd SGdwROhd6&J!x'^̞Ʊ,P"-ѭ\y],&4 }R{R}v4~4wrR]Y>X$i*o=L=Srs?m+. ԛa68*Xxp{;C4N`> nx?>s j2N XZ\u@us1.?Ѩ?_빦ZRE0f90]Ғ^, Ԃ8B@jɄDt>mLEfb7ö,.U"Va_lDŁy*S:"h +x6o`v\g ]l|So?EhM}2O\9_U`3?<|24%>u 5ewUN2+Onj'N%TbQRR b`ڕI+G(M}ȨPV=JbF=S"#2zԥ^~4<"E~bY 8$ubbŐM NW̢g4]3(d4g @c®L}xtv-(Cʅ߸|!r?<8“@9+‡9=8d\^:0r,̩WAnxj|;izpcXٴɇRp>Á򓎉,OkMF&CͲlگQiCdQVeEN= xU4w<[E_O SƂǬk-y˦Տ$ǁ^_q*C򺗖kI:0VCuF $tО?O-VIt9: $}][P#/Y9ki\/i3Ηn(c"a`Qc -&}SwP}0,(cf Yq"Dfͫ]C،3~7 ojܘ\ϣ&juYBx8902S{(-(687DHn=p$m0C d7`DO'ÓYMLuĔlN:""fg ` CSD{|14lE _Ce4+(@ᇥ#/a3Nsa`tlC[rh+DfpT̮p>^D1miq)F 7epChmg9m{W*$\{j@ sLnxK+ƒ,\g4fh\.4u\ވ82O\-Ym;~6TbՃkӘ7QsqVno~ٔy(M9$ԆO2Piы3ݣ\\2d]9gc3.}W2>A"'A>[&IVF^NTqxʟwC0[>U'r>,vF3 }v 75핧[^;tc"(ˬ?Ǻ!\? C3Kwl(%Ŏ\t6 ǶEFD=&H3mUS  Q~~&ILweQP enu&s{mlNxmqgUx ʼ@`=%3aRܙ{U~"mcG9E Vo%Ϊ ZJ-ѦkyVw&{bWM6"ֽ{pmPTV=8kkipVQN+t$xNJ0sx#dbq7чp9n'Ic?kwfQH@&R-sh+xc~B(Q#ss!甥l7Vvfb_Vk#l 6}54TgPV5nZ8D 5w?,g pF7v$5Pe?F}41'9N[f׀ZokG+38y[yMዥT RnmB讫ؔ ^2ţQ%O,e=h6޶_e hӊW7ﰵ%9qlCgbˣgmC$+2i!!y`m7yݱr:$z Ӟ*]_x#z<98G?}+a?$e:6R1lU1˲0 #X{aL<쳁78/.^̌˷"1AC9 1lugc;1fcD8O\;˔ӣqy D[Bap]%<)KĻjOE/Xy^D0 q:dRmLE}c2otp^(Qі66m5+ |*_Ei5K)A/oef< :إͳKɊ{żǩ.:\n[#_\)3vw!\ plF3ǡ,~>XSgLg_w[ЪB rγPhx1;Q r872v|<7_Nen)1O+Iq%bP?6fw_MKgjW4>X{kdMt+18?{ߟL6;{A/'Rf͂3.+{&&"lz;'V<t>KH+q Y0~JfwEy8 UvGG[ # B |-'kW㼰fdASqMs^y/rmܔ/R},1w^5TgTm sO`!Wg4l^Xyo1K"\u[P-:;!!!H %!nS"W,I_&ԫ`uqP!C\3Z Kqq"vUSᤸQ,aU0)qwb Kul#9dO ߋm>^aPTc3 CES14/ 3 xG1ٗjI81vyGybث34ԋǖokX`S 8%l,vz\OW5$REm'ݫ jJ$ -fhU Sx)!_D0Kƨ\5?(Mnɮ jr?%*F0yrjڎ/>.:G퐳ؗ[뾦|oV L2^ڤ_v}^N#ƵhQp胍WeR-L栰o~h毆zcV9OIzg-ZuVIaByޮ VX1Rp*5brV^^$?"#jŮT%?p U!%[/ _奵A#7& pI6m4Wxl`g^43-"MuZ5hմ#+W}j*18 -{28 |>iZ`պI|[/N~}ߧ`wF:;e9^:_tG6-t7AJ~쌡=1zN 8\eņ 1e i[{ŽYor{ױ'<7ΘGTbf=8| 6ibs[~኉0fOrO.wq)/GqUA}j2hBBap ˋG&e^F">p{ ~|tS{4(;2n%ݗGNCښ6y }!c[nr][mѯ{)  Eymh9 ݽ{A,slkGjDƯfe}~?#z/uT?L5W]|=?{֤Ǒ l @}%O۟ɴ626in6g<`ɷ^dև U*YH/w,7|8jGBv,^~sOZ&DR"c]9)~7K[V/F 2zI,A#ov}OΨ_:AU^lӔAG^($|k~Vo+_= !}=.hCzq~`^?8wIi\|kjbϟgSz{6l]]9P̀~q+Pnπ4{sPQ̳ vDSތgt;#[9 @`^`K&)p8Go zYv j?M݊1J^=+ t$M? ~On!ŷ {dec#4-^$c8<+/ÙsX7UV>}_ۼsKC)7aRY ?'YP1c6NCM>h 0aKsDE/<X2mZTb\h2=P :ǦcόJWd^&dxB%{>4iI~MƼv=Ko<`*>|f6Ƿ^G&g<*T.O$g8LbVZOj?OڧOК:Қ5Y^#|4w$ꡳBo+*o$N! i 2II!!>@cljw9*J0aKsnyh;Rft 5mDHdtd$[<Iێd:ac2'=T-׍O:Rmo;+det`]WV'Px,qfueB̖hxkٛ7k sAwO:M'o~ؚ}iyvY&Iq}=2ӛT̓>EH Ц~@C7hlt=8\:=ӦCV~=<<[z\q,}~  ]rQ[\WV:yڴ}NRImwMy0q~zbcgSqc|#fx _Gʊȵ3uuOc(N8tU錣u2dťݪJ^%3Y杯āL"2frG`DŽYɤڪ2!Izbbd%kbNd﴾B?)?Kjz+.nr;+EVgLbv_2dĜ?-x<~OvȮ3񣜃ky.:8r '_ӑB>lD ]&myE$p=}Q=?F?#id>1N^ ˞UQ#x$Ƃɿ--gFd65AHnI=rFa\^+?'33i3y C:L RI^և߇pVZOhwhQw|#OGK /=فiAzI>K+vzSǶbҭm~k#g_nvbw-Dy!Kv:Ho2o^/;.1["'ynƧ$ҫl\|=R+.3uK9-xYဏ~]>_^7 .nǽ6lHPwl'w/-zs -晛~;؟o>/K:8 :ŜwQ{YZǡ3"( Jh(gqb`8 %cF;G]:/#^||UPV6Wy!*eݤA/~I #ܞ ʠWZ;V~<&uYW+|~dY8 |{&fp :o ][hs8o$tEORTq|p9Zd~x@.yQ|4 gRd%\JlѬxubccnv9paÉ|fD'|eīudHyZ&d;ƕ{cy:ؖQOĔI]7z|S`|j:p'&QvP^'};>?4m>UiyA_h^9(MJ|0i>-hD%??iy_Jx{u z9v6r{htN%]ӹ>'2\.OGAYa|%[qE^cF;k?lh/^%&I]ٖt_J{nyK+wSt0C}w\Ad']3l8F}TwzM\p,45jƿDݟkjp0 iQ5>@ i~R/ pt@I3.s$_ңГ׳vyLV&0W8r{1 VeLUy~Yh2ĀVĨjJ㹖7JcBYIahL|hJ#*C7 0z]ݥRh]ѬמAA9zbX cڇWqtW'$u!W@c(.~oȧK41gS o,V?G&ݤ=nʜC:8>k/cF8Iqױb q>UzTpqt6l ZGVv\*''(僀JY}s6;[H&Ɗh7NEAʬ+њ1҉1T =X!uck]U7Ld &6eטVMgtL4;;5yaaOs`#&wBygvO2};%|0{,|yJ[xQ=Џ=.sDgsqvm^`6xm 0O|+x j!)UwZ%T_(<]𩗲T v|6kbԒ"XK+lǶn&2PZFE]lbtDpљK?W'J`̢2so<=3=d85uwl9>qFU|^Cp8Ԝߏ_ޢރ7~7}mhoʽ%`ޞ7fZ޴N'c6^ߩ+v0*ĵ9<|,O32y/#~H&rZ/rӏk:e[?`3#֭lƚ|#}I@Wi1 VZV ĮV*K i; _ǧ][s9ukJ dzbqYl֧3%OwZ!@C:zZ53ʐRxDkP6{=T\ zm0+1Fqަ1-"kD*؞ljlUQ 7*wӠ83fٲm#,N(s666:,zj'|-?OFAgj;o9Y֬]9΅XC֚{.pxb=J=&ܜ@zwghSV-W+)/JW?C:*U89yqd3reבޘoO錬gAouYݧ'FG?hp1-Ҳ&ANXE&gCۅfz!jYJo|;Z[]oc7t#XQkOC}~E~A/*0/JALdx#PV\6҉YY!R<筥hRN m}!wSSkȧ5b+ȼ NyKuiG#c#gj#xW xyK"oxG*1J !+ƢvEB5%RE+ANZ06A4Mw[8_@fA߭ PUë*՜aH(YʎD ֓bX(8](Q}>y*G Uq{QMDȳqPĶQ|COsYyw?e-y= Of׎ @mvFJ~~ܓcjp3_)K'I 1i{8Df'3_y1N~)c9)cX#C^~#s{ rv}KLmQ&p mo xw뻱L;~qT?++KrmeIiuzt]$tqaOM5KƢmtV Zv\u\g/~Xݹ.8K+k?&$D2N/F2~9{A؛#@*󃑔gWiVMZ>Z-z1 _:uL]+ z?i*|X>O@|Fv?Lp&>DƛL٦9/ZNȘ!c" ՘i^gWΠuIpo/E?ko.>pH*usy%P%GO?ZI=\zHQ8JC/Rdriև mMm1mjbߎɴ?FOUl‹q+"ʣF5P%)W%} #TC(0;d@ O|CbQY3&y#%/#-j,8B mR:K͚4A|ۧ|]~0|_GV{?6\݇lbzps4cM%Q 3e]Id'\4&uW;.DjCڡӳfa!?orx$r o'RRqݛjV_˂?Ӡp_luY=^a&+srQL3Vɑ]g+M™3v=3  -9S}>B(AцcAv^\1[Z/*+c m`h{bWÄtZ:Os$(R_bPbДPދ򋲠d(\T HVȨ|6)_WU@UAC6!,)xmhHybmOA(KU= ;O ~99MsCGjt[ycҧ9 1VBCZg{-O~1MqoʱC̷>c;"7{g'!ډEk}Bc\i8__bd'8 MdNC~: k?[ƅog e$H{X1g*T:gRկ㲇4ieze-6Ή7aݢX荍g>+^ly<\yQ6ggm?Yfaٰʞ?d{o6P,s`u&Tx$#/~4`a<.@t~k f+[fok!xUG3֗xoI Mӓt3'lRgyMGo*DvspsO _xS;ttzV^+goi#Gs,[K-_Kpr:盳pΊGWrNdŞ#DxęL3ß^\ z\lgGCkv żX#\V+0akx9M_t1慀Gd MM+sƉ|< Кcf<+ƏSe@Cl8ObK*M/BgQZO=gb `:%[ _ъ?>BcjT%_yv( #d@iZs4Z=$5SɴI; {4 ]V~9'XOl|%xQkI8F{i:c%tp|΀y,{^)J3_8Y{'hCTmֲOrϮ&=t6^|d|[Ł=p1&f&\6Xܮ(GP5xQ DSΔ-:<.liu.Y:^!i~4ȶe#7SqFyaR7Swѭg?\cH_CL݉iV^=kOLJ9]6}~5N@ڭ GYoiP_ҽ~%+XZ9s9tSp@Ȧ.&t:똉;zPqv9W18A&qd3ƕ'`&TV*ȓ:rR& ,e[iʔpM3 $;)31g 讟)%AzVdb|۱$;t}N L ZڂTeCMW5J'U9ZRnmu/灾w~EAM;ȆAtc{]ِVBI~R;iv™,!v'}ᙼ^xWi#])7ɧU^~%`+ȩ:/.T'[֎?z;zWM7먂_~{L9 Ls}:z*GFx ?~ <<˄>6ؙW<bЄvG \. EIANyV)Fy)BRO(C(7J4Օ8Q6KB4]ks8"Sԓ?Ns'MsT`asx3HJ4SuwfL΂-/|g`Jb=#&Sf䫥#ҸbPF-7 -z$uGSyzT?RZipB{3@V#"88o LL|nR?ZЅt~.3 `灩+43P)w'NQXՑT ٺּ)2ǻN(8&+ CI2-J]L ^@"^hHsmM}ʱH=rڭ'aST%Wu'_l_Ob M{'yoZ  0O?s2^qG6BĮ ;B[ةOݢ¨G9iOqٯ(e@d ~˓l^Gr(ճiU7+Pt⟕ȸih*I$(@l-p:I{Zϋ+g?49{Yp{mE\p25|sܟ7\3nÆ[-LҚzI+o.W}gRxi7?o7Dʣ/pRlVV?6)9;\~uo>rܝGP6稝 |K͕gؐPwZZʊ.wvg.Y\?!)UQ( )>YmNo^@3:ܲ{udh|Q7RFFęadLFf-nN/ T@V:Xh b8Q1\Y%3LPD0 &i({\ NX7JZB)n#OM 9J\s=담wo `b_Li/;8U}2r\s8;^1U'e=ye V8zƀ ͩr>#OV?gVȤEB|>GlW8Z3"h̪Cv5i@9*a7zWN<&}?.nf>gnƠ hJrKJ5HrqŢBƐ3cx>6f^-rXkۇ.:Ɓx@< {e Sä &)+,zԩ.u3AS@Jҵjxrޝ G 7g>_/^9/kX:ESů𬜘(DW .x:#!s_Nf3Xv(/{9Bw'1Mm_X |,Gۚ12x]e//{49#7*908cְ {>D}r,Px 'zrJ? H_oaA~A[ݷgpp6r?`~7Fn׊QU^  pV{L5L}\@.Sn>*ӉI,QA1Y͈\.}ڰ*MV}K&^q5P;jҝv ̕77ÝM5~ZR7ϩU>_&zuuFpc~]~&W:〈3σsyoq{J%IiB!_ƌ8K%!ЙG T)nWƣGw_+v΁{#{_.Vb|V3MMSWSȲ"sꇬV,ZPcl\.o_iB{lfƼki}r};ٱ׉^5k]ބ?XkYz~9^d3fC{}Dn:"DKߝǣߛдroMzgR᏾5@hN򃛬8~@(i(tEgGKrL:)H*EEw'g~i>(5ZT72zO-E {KU x|gu_I`ʫ#{:*6G[C{x>ήuo±a ~u:;9OùwW䉗&u{p´2i&޾{iZ᷂gSwset~ &ĸ"UW@D6QH@a?'P X-#dS"+Vc>(Fmqv󫶥-7ge}!vu<P \OO?!ǎPR8Noy뿛̘.JXP 0q"Q1v~Rzs?.;Nlm[ ,ppdȫ{ի GtO=.e"UzSO]זfK\T)3\sfL,Evo,&aQv\/fW?&Fp>יE4 aEMf}v߱kIOVm+L2u&-&MVUyoMPݑ~['0p,V$MY~^DZބO[}׊˟'w@;9jN|z&E ڀ8WVG$ɇ7yIS `JP%;03r-g>$HK`$+ckn~ ѳǷ<>*~4hlq5Pkq OԎXOtړ_r#`nW mUmԨ̼fl-7YTT>(W%ɿUeE# b,lg9KYgUvib rPx6y1P b~vΗC*X:Rt7QϪ4`|kZpzy.Ct8w&rZWmsƆrFkk||[O9LdCޕ`4?'!HGw{OMvA6t "aUH29RF8ђgid]kp9s9#Fc0eƖgüt@IDATI7gr@ޥ .txFLn'џq5-Ə>vl霝 l`Fu}ucz tZ-s>mN} BxSO>oW?x02LYnҵx3yL?IeB\|拍DҙHO++;ȧ`~<×S>ttl#&8=&{aimi%v%#6#[~z܃Dυ v:)ʣ.H*QqL^ClU3 %׉O e~)0b' p9(yGՀ&F`HtuT_E*?HkL0z9Z&lyQL{=׿Ruo,FJfN'+HCD?8H6[r-1w"_B3[݋ȾԕxdD'V(YP=^ߖRd[N8*V2E"%k<k Y.)cwT ;H2i dʒ}øuFzk4YmV~:t;Ӂ{>dGFH w3ϴ4z`Wwqx:p<.ZN^Kˮ YhzJNL6L\I| O/ xfVPsb{E>@:Vl ԧmi[ˮPwF/íCjȁF䔶(yXMl7]A.3Rfgf'ڢV4c<D9Ԡ=|s(ÃU:y/~up39w4FCKF0Qgb\&L7M{[g'>o i/pmq3(T{mk s(9H̼[(,'ӂ7 ga.eӶ8Q+_.΁V-%v8.Ksfdl';^$|E"&^il/|6rMf}~k [ХόLIc0 6NmrvLS^z|'D{ 3)Kz&}؞UhԅR\R~)\G3Ol]Oч92ˣ/F'lIИ;DK .rґWJG\<>N~D֨ ёik^u@ZO3bO rȷ:)z.ul3.OZB4uAtuT}ld$Q( O;8ӺFMv[WdMZͷRe0JeҸ2i),:'PM@.zLp\(]Ρ;:/e ]p]ՑwVTnI^ӗꗯ:_ZXP 05SޙQp $tg~u'ps/3\'4/оL_ HǏ[2Cb7sE=Gsi w'sVb-;0M5\oE+v_#EǠ:Nrd|a$/j+Ou][A%AIX;_1Javņ7L˟_?ۜLO>M#o!GmUAҹ.a.> (1Qj!`DRݺ'QҢxM:5:ywuԠ=_s<ӆ^~hZ(SLR۹c,8hFIws/1gc]= ixW?yrK>o.&jpkSA=cWߴϮf~}OOZ4+bɼx 7ξ{o7ƻs:N@lҁlUSlЋB8 Vmf1c=-ɉO0/ʍş^ʿ 3)v5ew-F6[жMJЏVr^ <;}%ׇ#S᤯</ b>Lԡ36#=Hk g3G+m^Y).O]蠜 u΀:. Bgi\l=XM> dsM8bې49hm ~' #vxIvME߶b3`U25E ~*+#ơ`^ѩ_Ɣ N)rHG˒\.1z|Mm}?:5ˍ_+rIΥ۪IiZe6qSߙ3rH\l =:RS[fjʱk.8Czg2 GkDg!R|NS~o;Zz-4P+4Oky|i{}gx1#?,!wѧ鸰KQdme^=>}9BZ(_yM4PNX;-[^!~Ud~A-TgW,Ȟnߦ6myʁYx/'I| *ڢxQڔ1Օŧ/ywnHeB:cy%QI y R];#GJc0H0TB c] ƴ.OY4lyBQLR8 !<71)ۺTY8.q͈&ÏХE),9k8 GWs25Zɋt|vȎgx'$ژgu6rӇ>/>gI9?5_%t^ z8SCN?'84LiM2n"J“@u|VqXGr_O3j lhBg:1NoYdžs^޳J&`VL2![0.@l\.}P{.:v:@mȄ*vB׮^ΧHUpTG5W<<Џcp9EW|P%_,<9TE˺* 6?k' &<վ 6soH_[9`OU'>)qfIy -3<$.yk{Q_0ON B_(8[v[z}A]2պWu mǣjQp>obkQ*/QE.GtѲznKN`evḈjR"UD⢴22,/ZafR"jj;~ӗ`Eag+qn /ʫ.ZAJ@w8fAvQUSH,H1\Z<<"s&^%yk2G>9$ݚ.Y SV hM ϲZGV5JyuNV?Or%Wh͞yc`-sRɜBO~79kY sMB 6٪TCy6հobLG#~x:F^,Yj㚼+ƹck# t#ǟޠz+)FMsfh-BņѣhN\42r}g#~R-hO]:!RSza ]i'9aI4Qt>O[GF=s}2`UY#dW}:wXɧhߟ3jT65W%i6s( ɡ -hʅg>?AE% C\Qu-Ůam=p{6l>3_qᏣN>aìR^/#ԙglb8 E!F}^Vy)FK[P[{h"EZLJ3'F۪a> 1O.`D kΦܳgh>[LSy#r틽d}q0L+^8^! ^f{!O9xMhv2:V /L/Y o'ed!ut[cW\+<\|Fv{[i}>P/EkSX 9+$ȓ巋3A[` /YmJkȓ΁`dG}佑{ˮaԏF '&cb[8tu\[{cDTOOKگ)xQcuO˲0J($ã҃dCo͙қJ _=I=#9`ד^z[xԳڒW/7Np/'Ţ׵rTj0qJGI| 2-nC%^ZBȴ|xd`{8 ZC 6Q<џ ڿɧtE[`QImsbK]UdYdisᄻOoG]{V5uԕ)IgO6?:-ސ[!M꿕;BSh/ānbSRRKO]q:22K@Aw'F }a0#q{c[)O)K7{O XunX/fx맦!p Z{j֋ǀphrs{FAW&}~FONiM4.s݉-{Yn>@,I/A߄^“f'rO;LG~_[/g{8_jL6]圵7+Ξry B_T`\ (n̟Oɚ(H.u(l2Y=#h1IҪ"d!IXԀHɲCᤈrƶ49b8j䲒PJ܊U\*-sJ}/2 sd9s3Y(]MD6ud%A('C+Y+n1'ypmKΙK?ε7tE e#A6W^wfȺU\b߯w\?2) ZTXY2ؿTtFz TuT/_Al+qTߦI3s9:?@|'<zYO=ױ U\Ig[%O5봻rV[kAdK?lڥl$%[yḐ/G|ۇ;WwفZ; m.JfR|rq2.~ZcK+bg.S'[ۑi\.(1V2^QeK{_yӺsXp9O0_J[èRS0WUq4QW#1RsO+M&(ʜAf坤S~1$58 /C/Lq'[*{xf5-2`b1 ^Ȅ;8MߗH3Ж/朽 |)ݞ32Y5g 6EZԈ!Wג '"biЯI'+dB~B=I/fx7'\Xr/pVdd-LepThuhumk4ؾn(TjmndY6>Dei'; ;owƘgw^AVYK;ZB*tG>bտmkURz"Z / 0(ʲuv}1pen=[j6_E}^LEޛHnऴ)*L ʣwBe/QQ&>Q14[*gz\)3qlh:B'yQQ&"pP+&xU|&1[D s}m%?paHҠr=pX|-o\-Qrؓ!/y''m 8s7΋Rg"H9{ `~ًD6CwG>;^ 3;3њp`csf8T4lqZ;>I33"@4Û=Tdou@lC鸚b50c,N ѕx {-J8Zsݠtڧ\=H;bg5;hiQ^5׋=WRak_sQ>OANկ=#5&VZg=|C[퇿~,C^A;ݨZǛVK/;?zw%~ _zvGKf,%>l=2X&?\DMݒ0m; )1j,Jʼjj̢h|n+eAڻI=tVMZ'uFq3QQ~[c@Q㇗"L:߳kמvVOC n~E9[I q KoK\Ւ[#8+#p4Hq8qv26+j )),h_$a,D)|}?|q =o!g`RᕪgaI`PGtd&IʯY) *Đ -fV/;}ТS;:A^i_m=pd?}흭wͣҺBZ o ݇haR(Zs>q9,?(c4)/u?OUhx;ag 3ÝġyZKS%RWOZ6kQ닁\K&!Iϫr|b(>ԉŖ =H&-tI{{1WL;+-okǡPL_U܃Ɉ2ڜ-}KW/YN+ЮHɻҶ%"i1"Wq4b㜑CPHU=/:1虲 IxZub=g=n)v?9YӲ+aKo݌uޚv>EFC]o&R_ϔt. >(`lAwr6_ۛh O>~7vZqMJ:ޑq; ᬬGC=9 nzT{.[~84,}QATez ѡ(J9U|`#<Ŀ*JF!'w5:U^^epS&qg93/t.IŨrl2GЬZUÈ:E$#yGt{wixTDLH(ҘF9b[_z^5~xG8^{X__lNZneZ>u> qJ*wLQV޵,ry, eCz~6)|rv`OyW1/xĔҏɿKJθ<Lڑ58[wN&=32yK ="bGՙ*bS& g鱴-N]Y61P{ke[>Ʉ0:P?:3m𬄿7U^ՈU@=vAb/9DhD16"O&ɗ>^8atra87`֤ >nٹ<ȗ&po9R.]lYvY𹊏k#$᝽E͕?? s"ʲ]7Ϋu/[L; ^TPjLu'?8.\&1QaP3 QUw1`pqJXMQQpR9G͆"3W QQ d&s2pǀbN dPE[kFáS"21&D[nZ"j^%")x_NAdyŷͫ-!ub.#[Ɇ_y9\oe4B+/]D:W'kA:~b 2>}!^Ƭ/ԑcu*V| FҊ*Rm\W*|9p]uoG[>/};6}{F/xƔs>dOLpdK)*%*ζЧ!Zs96j_"l>˧<_L7Czی(Ao}ihXNB$Am}^SB@jbmߖViV^.x .V~Iz;Wygkѭk|fl_j>iY-O Y:t۾:|4$WRŏ LJธ38Kkpv =\~U!kd|2nu)k4)ȸ  C8oXz x"ʤvxSOA|8Q0ɤ+~~0ߎ 7:ެ ?!#_nʝqJ/H X\>ꏦtܭ+Q8y3'ΛSRwth~ϲ#1]I9:3׫N̊I!^t C Ri Sg:^Oꚜs/vIcM|^h?ж~{V\@BimoJn]:gJ\Gv=|t1-ŏ`"g_A,.d}V < 5Omǒȿ>2yC{d6$ i\>JUuFN~d\HK -v7䘼ʥJZC{PC` b́=ptLb?}yIk; lP}xp¹;+>;xMߏD4۹?؅c1MϞ1dI͌wNsث2cxW/[=Hݎ?լn5A혯 1:Gt $<^G &G۠l\;llY8zLS֔) }0!턝DZy=ヰX};2 P⊧[yrsS~'Y_[\$u_ξ.~=y;+ϟf="kd,qr70ndÏ)~;cRe+]^+PG NJcAMZ׵\~=>%zq,ómJ$u|O(m%0u8N~=xܕ{B/ꄡ9#OÝXe8l|S=QTʝ)NJAZeǤb=QؖuF hYJu}4( W):CWxɸ&4#Ǝ'x]qjn?d.Q}}yYlӴu42&a~zќuqy3Iz<=y%JK}[~z K>)!(c_7a+I6wo}X7kї:>5]=4:iO ɮ P::6 (,kRZ <FwjtX'tPʃlq}A?s;'~1 L䃣GSq:IidQ*wy-F!p+Җsevd76j$P)~wcO}MCL9yic.ʛ\Z MFmОtc21>|ӋkKS1i=e(Yʴ1; *{mqs(z2>q>P_=湝zj@@JкNU l eÇM_4ïS]~T'ďG% XWK`RڡN*AryFAQhUr [6:]t:XF qJWR1Cg&~ܚL{DFЭ񏣢 P Uskd&mÉBح>㎦-2fx)>?os1ԿdӊTcss>yQ[bYMw98wl6F팀|oXABFQklťY~ݢa{мpLK#u&mMcŜwV`w.|96Wrz{B՟Ou\4y2e𘭳[Zږ9?oɕ ]&K?iUGuo69"9o eۥ];e9z8dm&.oQU@RюBu:$bE\Kփd,TXo@v5A';5\ˇh@H`cR#%IZ@CzvwWu,.J]S?i?Čf"]񮲺 (3Rb?mx$)Ʀ\(gV2T^p~|KA/UvF$4@0xĠ]0]9; cͤX2XM 's,>3|LӦmn)ޝkq͏[9uded$ɧ"uq\[CܦCyIگ3\g >E-}_4LG'4k1ُ'h9Z RzQkm gq'< '{.6^-~}?`[|Q p1ղRd%zGF[@.Jd7XE@R}_WjkHsUC9IFL:CPK5sU/FfBVC}4m r`I$TpakGGvNV)y4,pkW.G$ĮҐ::>ud,^,ItHd.tr=r9"`ϴs(3L-9_8l3.֡a4͜kbgWc~iN|ȢN*h^ʄv}N\&&y$_ bI3^6yU7$xUD/mT;.zR[ڵNpCcK!\qF|'& 6gޖ8tA$R<}}YemcѴDRH$H_Y k :^8xxx)"c-˳wg'kP&y^ǑF_A@<۴~L9^~y_4_kZ#Yۯ%g\ 8Ӯ_ρwVW.=]ʶ9d:=6  `h)M?}g畡}^ Ozp]J(r`ܫ?<{FfYfE%.e`Z.E-5AG*uA)Q( ZP}r_ꤐDQ." RUHL}0AAb|$@]pieM *t``yk YЬ2.-wɸ'(+ ^OHy,-t4҃9vrmևktQO OLikd:K/`" {= ;ár%.ܰcild;HYZ(NJ7d0WC[OkŇ+<$Dr[:W6Lf/QEl}hf[unc|?#WS L9W؉G] *O*./§Js[6!+iWae\ Rs9'0e@v\չuSҔ#q}O㌭n!BeQAM/6Z~mf'm-G~ ɨB5g/âQ̒YdlE_8T$3a>w I V]^cb0bvE:+}u[Fs h bLI..O[t\$3; b&X3e}:R|m\u F&?򹕧}}"UGZ$hͥK Nq,^#.@IDAT<:lmu:?gk/YxDZ󴾷Rq<8拾7twOܮ8wi68yOEg6 zkK{F3aT*gc&d/gkF=cҲvO\ϊk8 OSޛv}7dy)!smE;g<)I#>ɘKEތ|oc>ϼx4T4'/HVp̍p'ezY{ϸMC3+S& izh9[޴zUQp(ah$^Ts" ("| 4R?y#f0"#ȥ@n3f,S<Ҟz]!x>W76`WU /QѪrXdg[o_DO2]׭5a\ױˣ['伒Iy Ͷ<<{8} Cp!揦;oܞ t >׮[0m]L %]#O8:O-<+j B\0e&-oeXJ0@jQ(P׆baҴ\xJ-8 ܧ\Oor2D) *ҫ;3 ;/ ߍ(1/&H\U)y`{JRo)9*nqMvl0|jUz='`>)*;@Fb7QO߬V8f.f%r~9j?/U}h/My56gSV卅7 _#FlfZG=y:_\y=R^IXF/x@<6*2+:F]{=1,NԷ_̟E~vtypRUp:JfCe_XA}1~I }bD:3\sK2vlGh銅 3, 6$&_n|@&Oά@lT )Ah)΂dsVD[#cQH崯y"FӅrZ|TѲNoV[҃Lu{3r^i IL"i#z:]_ $8~3}= b{--C4yᏬva ֣:Yռ[kNx[;4S: vS) ͖Y,ݞO f抽M.ݳ<` UeC#> aFG 02_'^8tf2,_B@BHCn dxLBJ1.[.i>ċ9խm00h(S=˼'pa0("Opra&~e)6\W}OS-)7>F*U7 $14=?1G\]/: tz!yvY@k(׮Bm[3S}тNL2Dtg')-\a?A9Xr -H:қ)e'&h}qӸ/x5v>\ ɸP~䔵0vx)=ڶY8EYĩ+&^ ǟF!jy9-%W*~eoi4Ce#:8!(8/@U=@vojX0Vxظ'Fj{:ߖN]ooIq8c+ãi&}bpta9H!ڒe68i; (O1R. xl>ec v  mFo/K\N#h3l ƧqG;=F#yl79uvh7T d%%cMPs2/̲<' E0vGfJJЁk>Dw+&n58eUCOsHi߷ٻ^{WGExRwyo獯'~V/;{h^HCAin[ڌs٘^F8o <r_6i!cpG^ο=u;K ;7 <Ѹr?!^I ]z׊"4#qNv+~)@oh7ތq?q>.w3>c%zs-RK58@ljR*aΙ_8@EWJm!{o5FzZ[ZnPg|L/8ZNSjk]ʹT~(dpOy|@D2~#7hN39}^[~TTd.zU OB׬XiPJkVo~K~&#*:qnHM8i3sJ'pwY!ayK#L|?=V$Sfy^Y@-~bP(%St*2HQHb6ɾʍ^F@εiAW>šRt+LyhŹ4qˠf"P\jP6geub y+j4ϣn\Y`2Wos6j!`^iP_ʐWLl`|)+3w;"28l'%wM885-}SS}vgX?;YÉ2x>tL/m'/+e_Ty[Rx\15&.;Y;@ߴ=u`7=`v]}ml m 98p⺶#sf>%mdxY=GW J[{Ѷhz?]%oZkwtѶ--;N4H'!h:P_< ashhK+;BG}ʃ3M|ιT*cr;? vYb_Xjʹ~y e\,Wb^:9X9&ؔeɾ9X ]_OTլ>yFDzBK&$}2Pka8 :0X0JI{}al j({g'R'{g1:w?Hk,pŠ^n( Rd?E jN lg ݿ.~<=^f7~[x޸ih蘙e85[9g\[ƎKBӟH+^ԁəWr-(IX=Ή~dґi*NM~N5brI"޿|5am0$/4ןZa;'X"(63b't~B{%!' b% m^N[^Shv;DOWK8Mp?}my|hfed@ýmEG'S>,h/l4XDjENX/UXQ`YF;x?'B,ڦ",ЇP"#=sTA(C OO K)57g# m R] e lјww "a }EZhVs-x;gg#j1W_g g3s02FRvcF>H~ l$ϖŽ$E8?٭dɃL[}"p)g>xxף~p}}}2h7@}8 +{:lͻT%vnilBW["dr0&yXEn'G_˧fI!xk],@MY96^qL@šW)S_;A/`딗H4Mg PFRt }k6˃M3gt;tﵝ&qo/:3+t^.][]M|O:յ<ЖUm-Tl MaWmi|Om'\R#! 6+lQ/d)? h\DM4Vw^SMdwU{]'R  OJ:B׉]*;OQefCi7 400jAp J.r0(zhg c>"Mx^YDT%̘<-= n1lڎ_cK,$2 *1fsZe-y0c͊*mww֯=O3|L/ 1YFc<1Dq=,)=RZw_.mj/g l=OhCo>*m]̿:@fe:YfzZF۸}MqN./%KbEיR`ӎ_ieE_Atqx3ak#QDډz'VxkhSbV63H r T{׺(. !q(oP>\y82ބYʉ6tp x~)_PҠ[g;8iAgDֶc3=_ۑh*0 ce[Vjf[2ujcB9?bجqbL}i?A j82 Uxް/ÿO_f1e,wlƭgA%<ν5{ƞQg,q2G rC08ɭQVbAd(IV7n+ VmM(>#%Vnimd V\^I-@C{`'Ֆ4mZ(SjkstRXW|W1 }<^)_}VaJ:ceK hj wz|6TsLKM9zu( }J{wu"57.12'?B>+䈑 ,'ODk+{;?ǰOTq>W bcS%QP3>gxL߳Tz -P9[x3)xX{>3Av$2 { UlLly)Q)3^ 5g5F qL4VH_wK-ud\,.1U{GJ!LvCe!S`K*opH~^٘4 {p>9'S@?m̵ "p]:{g3A{O.iz YbHm~.!ADL>̇⤟Fd7 Qu" UIƚX7b6-s|6p!]M lDFڛ3jym(D_XӋ۷o5+aޥhEk,ˠ19֯ciwdL HoGq@Tn&DQ)VuR+J.uAx73O=|/o,0T/7)GK?*[Z8h`rishq6AJ sr o0S;$|o*IeD Ѥ,Y&ch0>p}jL࿎P Q0'OK(mp!'v*=<S$TOlV̒Ut"%k5rFqe|2[¬rQa~f(Cy]B/\ھ,ppV+AWyU?%:)[u/uHܚC ׽k2nJ/eICE`7T;e !3 y̘ᶳ]xFyw "?Qԑqy螺d4{zladi^ҘlZJ)<_YP帷`BNdRHM03^ '=j1Ք,G,\ё{g8ä$+@/YA#OzZ`6-τs?sHk'tQl2.}V[NZ}RDKGծt@+Ap]q Յ)'}lu>Cxӊ4| Y+'Ƀg>|6C:Պ:}yuroG+}ȁgi\yϛ{E"3ʋώBJ6 ˆAU<ޭ[q'd ![+y/_ʡĕhxOq( ˸)L@ȔewנiW| 3CHRҳ9|G R9Kx/R}p')ҕS\g&G 5Sr[O~k㺔a8~Ή5hP(Gw.cAz%,100i_6ԶF|8Wx%Ϙa>C˜>Kww) h {-FO#3m2ZP̌86Tw=H>clAq1m|˯Df^xw42MPC;Л~ e:''NI7@Oߪ 6 {x Js Egu w>U#cɧҳkUre5'at5Z?k~?"\ hqVצ$Nt 0Ic LV%a"6%D#!Z']@#}L>V*,S&Zة?-.j5jc݄ JuW vaD:GT%?jXAM$VA9O#TJupjTx_PqB05ːlvFqLGo(N+VzCڅur(G {&$J]@WT08Er<뻹H7z(OY# BŜ(*f? fJz}LA75p=빶]tK=9ZRS]rQˑgԡO>GQ?bp:6 _18qA ǎ_w_6 >$G77NOOF m;$ ޑ,Ʃ]~|h;Ltlݎ:eQqZZk24A~<\".Q[]2nKkK+rc.EڼNzX~Կ$޶V}:J NHr$L,<}!8]K,푎>BhJmt'ϪJ56 9jƋO6 2~ ?sgїvBm&vs}!mB n T_lGC/ҵU#K~5L*+=>װu?u,۬DY툋|v"XD6YڣEYV(U ,@]r&U{ yo:L WrSkF+Nr> BcβqOn&vO y5ίUl5srΈ{ [Y*QA9! e('/nz3*;a׽w8A0@ Ubl8~8L nsԓ2laN(S3n2M,ي=";} 8臑H?̙ th8ƺF1T~7V><:=)p6y "}S~DN&MgI9fc! ٹˋV , :[9_YO+ 7[uS+^~>z<5kmst(l-Z]?g|9+/ 5r7]&Jwp>EmVgh]ƻ 9x vi_/+3񒀔M4JY]9 5&h Ron a`0j"88 :٢gV)$@]sN-,.eΫה0XgP\1(BR͞IʷwʒȻzhS:!:C4ᦗ9i&rR3'2d= jD,7N sѡvҹְ򬐏8pvGfol J="`~u\%hsChYw1c"&K[?/,קZa '1.Ɣk1àrLN h}oL1v } ?< &Q722Kިsmmeh,))s-[U_RfdMF:LӾɗu˃?g\.PEز"hm^oF c{DWtg lڧ3?K#⠦Ow/=.r B8$Ǟ}KGHߣ+Nj=mŒ>.-8uX%vPԛjW.=p%zurC:/wpwIe6+zn&a՚]ʋ~MlB)3 >ɽuk3a/~*6Sg仢~̫'F3%(#)< x1S: :'ׄs&/ @X{8UKM9/ 0B*, P%>S~hgхų4(ME3s}VʶS5\ G aw”2F~/i+o=s3<S'A?q|xXvꧬhEokP(t-?Hz8$P_8)b8jƟ.XJFc vx(|E}y^:ɂecG.nEbz炼88*msT3#yIR϶8Fopb_[~x$&S''}AneESw%/,r%]Z yyU|hT&^NLl}}fz`^ޘ%)jR"#}KmDK^)OA#YyVXr ln0ީod^^?kGѥUw;rJ|ν^~]k2(;nMVˤ5{Ϙ;:oH F !^Q v)PFr  DF8)锌*cmq!cԧ>Ɍjf濩sڙX ќꐛ$'|g?B6}Ej@<$Uq\W.)Sr7`"z1N$_T٦<肯Bx.miڧlI'. ks> C6 VoQ_@^}Bc+GsRȉbåឝmÅMy sA{ǏI]X"wuߩpobLJ#RTE\[KAtZDQ*1G(<af ϱ˒%|101w]gA-ۜk,vB^* "E)r"T_84Qy{-kTk>>wp;gCv.ũ LK-orQVXp{vk=I9 <_OGN?Ǡ񾆕k-̈_h0c:eOAfOʬȈqg|(;# pނbDyI˟޻Lža7yGI|PGN]P v%nϨn40=m,֌LY_lY6[$Hh/2oSQxK9F ;s {Kao$l),g0"Ey&*Ǔ`r|Dx-lU LH*3,!ŗo* RMr<|7L3'gQFR@GړuTy^Jҙ [NX#F}oF >7kPi"wcBVtA_ZOd(d3=F |JP) .qA~{ǎk/ѥ}性]rp67~>/Jԡ09Uȇ~By@&c9\S )]$QB`rot0Z#.U$!Kx}d`c6ꌔO )^N9847~ SI29Npk+"G7NCO9<'.?QMRx 2)m&EYGЯڬJGc@hvˇ7X+(}[lɄQ96CJ koWM}l;)ȅg iUm^y_pC^?..̙é*0({i:>_>:h>XQ? Ig\V q:7]jM-C'|vK %Wd3e hRMXGW1,bDtI0t&1o[h9`cƌjٿ<-hÛD_?є9Œ.`pO1d!oyrIc'O<ύcp)̑FlgDkkC 4 ,`Kή~.:jZ4#UyϵԨMleG[]`e B{˽lYr4[cjv5zz.,г6Ҏk-k Xx3tm[Wh~m]]y>f9o~ؖ<{:.jRV?# 'puOYR+{sb>#+-țGf0,$(C+ O3OxQKCI(7L$1%Ef$m"LQ.Ud e@=@(Z*ychsKrySz336a8*jj3EYRk]slvм:hK?y>6B5Iz+u1.F {f G{@Sضqy8u㟴9N˚>4#%u]l?/fTY RvxO4/#`8͉O56MPLs49IzjEvG'Cnstǹ1LIzNNxcM|Ŋd ? T3}KPI߆SB)r/8@gy߻g99:Ki?,5M3KGe_@8g wK6c-ԙ^cǤeϮ;1t N9s{m@7`?OYyǍQ8?Xq N_t5Ze%-:~q:yhaB=3ƒ1c1V2q8'⸨: ʸAG؛e[6wnq3KH1r ,W%m_:3AK2F˚󔥼% o|F|jr9/Q A/gL/FaGf<=& x~un9Qpק*q[)\&Jvhq =lhyrK[CCVFngy'])#:v$^ӮK?ym|ofH\ԟ0^J cS3.3Ge0PIIsN7z2ZO/XhbFȪ+>CU;rr !ӌј^CEp8,ڲ׃(UK!H.UtUT*ړdeTVlt}GZ1:!Y=.69.-`^AVGrV}Bt>8{WM^ZwCIzHpƁ&ʦQu_nk9iB!OԢveYzO+%iCαQ!XRm<:3sDZ_0DaXiq曠6e1_F^PGН3'(-#,\LF_Q'/刌rdF0DؙUblb9 ~2%E&xОz@IDATlt9ܛ~G08rQWmGMiR6"U|cft>spR'> 2wM52Il{5M% \g\ &ּqvf̎0dE=J&5zzu}F] :b2^RS* @  ӡΥ -aG}< %$w͉yd˄9~&-~GQꌡ~L`=iz^KGkTGA3.8b뮁V6ѯp #kn7<}P+_X5U&3JdA~-o̓1TVa7wc4 ɏRo}DI危#"=YXyMd'.h?ewз1zZz)ɣzh cgTZ}&_!q5duqgJ+C9,F}`z(Adqi`Ny ;v epjDحY >b|Q]QUQ) t%ds3D9p AKUPo$/U)lr_W;['TJ5 U|_7 *F Iʻ6|o[;[+j@訏ŸwY}A[*uTn /FܕONlg"E&aCw۸HUL;c(A 7沱##푮pH%YWoT{썻PysBI6R)͢ثOeY8bԐW yC'n\}U'DHܴ7u.@{KvX닻wldku8Y!},8?\g\YyL%*Gނ @kgI GSbTzRz=-TY[n. ԣ,vJV]M>z.HYoSfʵ2&`^L/ Vxg̢{i0KLG>6=!tC$6; 9^- < n5U!EFLFF-/OQ ‘MYL%Y^l&[Nɨx ?ώEQ^ bm7iSs ,#jPQ>A'  kgWh^Z^IZl=BWiJr>Z$[]$36ZoDqvM_VM72_03b22ƶ|kah #6C2,[Myx12 ~z3mAS2mӋgʤ }rD5lbޞ;i"<6GODsfW\mMp-KY!Z2e m~e( 1зzŠءb= @iZؔ [3][w3`<=祽 +A2p>} :pKS}O֟?Jsv<5Qc+m|h LT=:| >+<:8[olόpY{7=mzo'oO3XLP>`d-3JR#,3x-&ܴ1SJR`{txt {YpyVChގE3@iMqM*EDӲ6T7>!8YIR[O9J!KYQ#;*le)M~r}4uwH[ PH^:$ݦCgo-+q쒙f}VslYƶ"e5P[n8muB;+C~ߖʅS0}6 e5h3mIKO@>CKZvXs/w{+0!/뾸.Ƕ@h"a2~=wtfv l(6/G\B]?,FI63Sa~b# ሲ'h-Qk]~)o3;IaTѵrf8|<}բEq,eANƑe3/uX2rM_8mN? E_ i73(hyw j0F(c*c\2*2@'慼LFy5f#I|,?=l;` 4<6F4#a=Χpѓuű 8:*Q6n`a9>"yjuy[g}ȄH-+/'hޟ_5imRl*s)}Bxn9y p-mQ?'<)ב9h)#ekHolLxe&@-Q , uh3V%}u8Vk0.}7V)mg(ΐI{}hµytV_^@'"·3IAPmIS6u/?#ݷ NJ=Ll$mʔ$R̒CFEM:_ӟ1ﮣ7c|3l)r;#W孃~Bj@ YB 6XBp͠Sz=Z8F*Ԟ^]xy`L27L//` Xb\9'󇤘|w3ߘ4lGN=  ݑqbM,vgkls~1)#I_2cMsR܁i7$EsV+s$/ ?3hN gmq 'k)_YbFFl'Ws֋v@Sa>$Rl0A>ts%KFX~Gd[S[t>O8aHzvWId33x lm˥{?gߌbA5s1y9 0Psw>(=BQ6ͬIf(R'lP7*K_J"(hl)*F+RApe\)jҢkSjbdRA1tX mKV*٬ȇ 7bfVH_[lEnk1 UƝ@MQ?҇󎱤%д|Ϳ)g*;% ? "YPhȈ#|jéy6r.Xl Ҩ cGdG-ZLv'>93L6Զ2rϞ3c]}B gsnz* <{F~~sktOC 좃opa?#v^bGtV>ǼndB"+;+2ۼ¦q}#cԣ^eK]9.gPNA)}'[t[J}41v @+}<[^iڻ< oo=#S7ĞbD hdj Qq : Xmmu" =Tgd. B`@iyB((?rv^՟jq0j iga13Q(DϙΥM/uqAPF$&F/|*clpғm mZ>wrN Smg WjS  8aP_ ;| Q##a/sL[&y4[эƳ1ѶC>|LW0߽mS/XH^rp$:&P414o3"]!gMibӐ,rgiŒ.Dbkf/?gbmЋ.$q@{ EJ ꗬBӦ2VRC8gq E4cA>۳Y%Մo\GUh-Ez^Cz=R )ehR'mJ2@J瀁TW4گ"҈)@ ElfZ 1DwLwAPr&]^K$S]?_* FbbLQR@[ކ2mGӔCSZ  kDG1/|.>dn[q`f.$܃Lagqc:FqE茡ҝɸ)YX%fLРc ]O:_m7q mJ?dZl' #?kȵ2=u|lhY){렡^zϪ񥡷96ũp(Nҙ;ۆ݁G,Ehzy}:6K_v՝N##I@+о ]l i.҂77,&cH!x} +Otp(v>x+o6ڗS4Kͨ"&JiR*]:QY޼XWu3/4 z6A`'jx܊w@qCD!u¶00۔WwNY!OM278_8\WeKcʢmtvjmO2}m/Ҟ{y(S !&%5Îd{y0Ùs̩ G'jta J8+zY&vjPκ}ы`Hs9Q8ςMtzz2dKmmg=S1 ^i疇\!H{f~g3XL@ʵ2sZJ+Ӻa@)JAmQ?yXb8_ȡ.G#`3JM;B!oL"M]2Qn#*9-gmCqRqqַIY>=syJ`e&oGkpW, )(93# 3U/C͉B~e$Iy(c3ᖲckXؒro2fOxl▏猕|K0N~R K7$ |crxkYA_4K˙yt+KR8P2T(|[_NT=JOg=> {ZbZ`[VY[Q}Nbtim2)xu?ҩ't*x/?fr2B+MEzg9GG. 4 ^]^GJJL[;+VP|}b]KB[}u&a ^;8(GX01O',r;"@$"a1 P6H kvP`< ID"M-j f#I?3y "F ƼK6wz{6'W;#BD?;mϯV^{MxFs "=) rx;O@i$E/=ZSUu@o?} O%}?ɥtN2HC;#{mpB3IW#r?A>6Zd$19Wgpֺ ѺpOfaqڶ?FDF>քNg߅¿"=*S)rnd%+b?;;H,5 OiQW-l*¹AuSARگ pchY>}2~L3r o =ژvfB;-&y:xf&o- ᙬذR9h}6t0LΜyD ;ng~x㭋-#詝>' 6zz7')f7:uۃȴË/dX#K^bqB'U¯OTlE[z(E~ӽJMH4<45ahbHʗAG<դ`ƩtzWW +.]=tľ KIOmY'&Yݎ>3D(3ռ'Fll'S&)ag ̃Ǖp\ MZnF%O/XC𵤙)?h {À EČ QMųPyfT;: mxѧ\sdz\ #G`=K͆zOgAUۺ2)2u;gSoiߩ7-W6[Yfa[n12H> M=%yYi`Uyzղwnt<\. ^]/ΰqd_%Bm9جSC-8rR?Os 7wz xhAAFSMl xXB LI8)[I)mƐ>ԶG̿ J/,Oyg;8I=p4|_p81RX!9<R lUVGEā 0 F3ި?ǩBDQҡ9^{@ &pTd99t۴}|P(FWhuC9Ҷ_k\Q ?rQ7T㶶5DZRihH3f 1b 7g< V=ɰQ2뇊:! *S OƭU&XGqgY-`,eL'h\f\el:8KW\ >;6m!#8N˕#VޓۜYY rpƜʞܵ\ޙ^a6ج<*ʁ;(nhUU1%م)OWܢO->U=(r9NFbpv=E1FWSLٗ_ZtQjlBmw\CA4O hu$ a2o@Y=^@NR`$=GU/.OPe*'"AgЋ!*}4PVfoEmt3Ɨ}S@ PVQڙښpUrZ5vjA>KjJ;OQq'4S$ M(U0UnBlbyўi/Wihg} OvxmP~K>Hx ;icŠbhvƞEnK_ diSia{v`mhi[{.͂U62m3ꗓuuԉnB}pu m{[>q@Lbc͚Iq:fR3P7Ϭȓ$0_FNMnY.а0pX!bMorsěR+B#('q+Ir"%A<9caea}Zq,&ȅyWR;}. fXW<gޗhfL~h_9 o,䭟W▙O{c^_yA91pK%8%!1}NLP;<8ȹ앥I?:1*le߃}ZKi7oCY&29/ LoyS~nv30F} _.Mߌ;@HKh_ n3>#)0> rsaݔdqE[ gf*پK "8:wwymkbx? f&+jE|!ƭg荙?3^ P OqU)ZVt&^JOU7n2K ?&qh&4j~pl_yByZfeN+8P~ ?t~֐:1.d,#TK^tqQ} L8{?gNcu3D^N"N=,єvLo/5_gzv9u& o:BNZ=#F(!ȟ Fޙk kCޝ,1ADbm{79vlu42i(1dpN~c2ktw>e+RmvɓŸSf޽g. I^ ə#eOFSycx$$4R sꃇ.2\j0 mGj5&74͝`qVXp/zpƙ76`\lG,U0fx¡?L@qh$R0X=ӗsmC`ûq{=fl K,=k'ϖSJSЊagjWC NWq}1q?'ܟ)ZrXH#o<ξ8͜%nsL8TY<3MNX AO]O}D2 6y8>~V+Apj]?<;;t}k˂O縷Fe'D P&pbpC$H霭sռg?FASHJϔ g FpLu* p@wjWE_}1gB.Ѳ`k*ی88wߴ~ (\{&.mBd?MWeLvv\UeX$p9t.`gXb/P(DQbGa` %D.{b3qCLܵ% C;Lv:SJ],F;4g3r}1J&@}swRgnpn;= 8[n)b^95^SLN( 0`¸Z>Ws&?dZaeyvӏ3+, x8\11P#ӹۊ:1{cR:L!(Qג 1#r7|Zol~.Zp';  .'cJx޾ʆcti&<BO=%0# HZҶ`xv]v"ȅY;7ea*c\\c.%yo՛hEܞ:l(vw5^{u®)0W_M&\GVBQ&z!`PQ)!Ԟ潥s?{S!f 㩛:pWJ7$A%ϯpHI_f,#B"x *kb­€yfdX46Z}zON "LQVQ=jwDxGS l1?a9:=Æu*2RQ(4CϺ@a|Zg-/ mMS^xoܡu֟>r&Tid_`C۶&Nkf@IDATg 0|/Kr3 GpG#,Fwm?y}!x'[ 2!7v@_Ff|Qi)7{D71wae3ْn=&\,28 C@g0ˬu\:xr/?`I Y҉i ra, ]:qkЮES-Ԝ@nO~-;;;gLE4䈐%nb=8SwK)5f?޷Sgov+>_yG3ZB%2Gc 0>F :'$ñ\\6n:rq0/`Q!Q6g!e5"[bҎuZ8Mȏ>]l!_N!.΋?K p 4=H`+!omB?2b)P@`S6y0†KGŖ _g3066-vWv 0d.WX32s&odf1~`bS~3ز 7EG&c f1)ޟ8C_\'<`N!\,/*2kp><&0EEf;K\Գ2B2<8=Oà6*xUp\u'Mxa<I_L,8#~y+ˈ$)au dFtI ݠn.qX ^`?G2$?ClU6wBDޘ; 1yj3eM'1b4D#u /" 9ː87s 5 a_Ae;Vap>C`{Pb!3p sFp8/ U nr -`CH>hU P!{YPG"ϗh|fX\#Fs F{aϩG3Ӹ OY~9mleng:%>|Rx)~߽u@8Rzec^]R`|]tA c`Èv/vL,y+U3O\kÜo ! s1 O|&A$;J:5nczLzSE5wH: OGϥ<z9UZ};R̛.)rWxbށ`UeWCJ |Di+SJ_}9[˼!0.M,5۝G }(k8YvbnE(~}uMJaFyC+p\:F8 BG7Ѝu4&A eMTng~J/K^ߣ$DӖ~yOxΟ㣝.@~qcH1~^Kӏ/x1`kBSc9i 0vx7kyx'^g `YM*.ޛ}GýmˣnXqucː41$~ջAD@"P"KA\k DR>y{RzU 7+y٢"Yae#}`hKme}U8B?'>ó)L[sD|7[G(ѧw aɩc:¼ߛ-a#k rT3GD^= #{v; z1JW՗rfiw QTpU\aab hQfmyy_^$_0))}h{18Ee0e4c 5*Rjx1Н] QsiXI+7EφS.o[LLyya2Ygmg Ͼ;ͨp(iG?H!vcao?3V|a2<񞷝E[ƌ~(MC9;oX^uPXQu#t矇O#;d+\߄}fH}0US#bsk۳@(YbkYӝHS60 =%ch< l<F(U @[_^:%V02W 煥mM y4}!qc3ﷇ,XT1XIs|WN/=?k7}2c~؁ \U=aWOۃSݒd{ ȏAgs^KG2xtK}<Y*6u^}x_h1Q/곹agu?F^S<0Iɾ?F; @ |}w^<@VB,uqCK]:d0Ϧ !jx ہas;/ό:{ڷƨ*?R"-*dH8aI$Mydpv,3cQcF[-~d?{UHrE/W wx ``Az|cUSR?lO-@\̙B13H$qX>s sr|\QxR-̬K M |,`(aEjꄓ 3p8\l;Cp/Yҿb43˻̽+Cb H9@<89 ^_]2|c!\9Y~>,E [,;`t!t]/ؘ@; b{S_ټ;pU~pLWx~i]5SkK\n/ h>aBg?pTmho)Íb>_RHR[h*2єonƄ1+ho探tS *ӹKƢyfu#~KMK6X1xI ovfwEϸ!:56XaoT/Cxk?4Kep/K`ȷٟIKe umiЁkflrJ#cX4*L૖. Gc`FՊa)UkFHMѺO!f:d`1Jx%s$GA(`V)m9c3 gѳp|W(Q3*Z$8TsrӃr&|PEogb:Qa=WMyǯMLŠg[fO6,WzVRƏ@5ֽ7#@l[RlU2w^8^XMAQ ̞ `Pgf=TN]:i/ԡ)K8W}W~`f#wP`  O,9#:miZE[_#>?eN me&O)j|O8LOOi$iRDQ(O" N̳n}Vw0 W׊u 1{K!MÐ6!/USuZz2Xf>~ UhbHETZ& oE4L3i0O--s}8޻?]+MjMޅ&e#mnZ6}O/ Tq98d<;_(Qszʭ  Bţ+e,`:0]Iuwxۋ` JpE%?mƹR?7X=).W`jZ r8^68{8Twgs?;}0?4!4>Mr01?lLiE) gh ;B)4KPvq5hGKK7mӈ^W( i-v o CL)l6%ib+Ow9kc )H0sILCQO,"$;%ޖzisګ.uO`#3R9x+, e8U+ↁw67cgaȑ`ҏK( $6!s 9mmb8&{[{ ],,ſ-ZԙHQfPF8I_O{]#{g3e+W˽8zW}$RfMuspK}:K aɹ>p,J7ZG]/cd~eS@> ebkߒU)ӿ: e1Ѹp ~pD `F|㮮sJ&%;+, B <1`*&؋T~>92CO1RA(^?ԡriEQxE1d |}@(UA+/h0O(:W%ҏn~ Yf#pm}/P(!^en]R>QG4ƪCYL#7˯ugp/ ?ݻ˫nC *ZqYY9Blk5a>*'"F9EBҐ0z(#:DtلK7Ff9 >%jJ[ujYe}wa*ڹmZh{$nVMyr|%{%CQ0}ΟM?}J7aL923x BFH2C`XQ'Y 5!1.H悠6䒶xm1Ƃ3WS{Iƫ-]u Qx&JķI0p03ÌPuffxFw3}NgY#ݜoL"~i7.qҌj4(3K^kK9JHB)Oi3:9[A>*4_*=ĔGu?Aᷗ߱3ړd@ C9^hLmxV~: 8>(7O>74ҭ\S^d9F41"Poj>ާx*d!e6 ^a`!osT,? QEPf/BG90\+s-ΰQ(vɸr%%0|>xdspQ|?}^^T0Ӗ{ e$1B`lB`t-6t4CM+$׽f!:07`uڔR t/9=9y`ZE.~EbE1vBH)Zǭ~(aC?m:/^qg %46(.h3Pd֟2n3fwӌ(8C1Ji0Y:w[脙FOr/R9Px\eczt4 )}*,D9/r'ϔq ᥫ3+/ C2sߝm]nPzȸ\Y8{>9otVB8rqk?{q)vήw =n> ҈P ? ly{.VX ڹfŽ!qÿɋ@! ptʨCSF"K7DUHOKhCP1p?ea`q|9m š|}_E_Q{Q.X}ϰ\n=CjԄy&Z'<|}O/8jvHyS0);F+,iTPvktN{j0#8 nֲ+<+<gjizqs msov*\0p1` @v!ɷ3n>*ٜp.8F1dlNcBvR.N"# EHU%1✀uJܣbäwG.W0Dfg/Ⱥy cMp0+}Hm֏^~>KFˣ J89a q1Ykiyz X7<#@D1 GjE|G4Z˼!nPBVF!x a ^f X]%Tr\CfZXW5=OhuXk|iXs=YurkZGekiݦH޹kI/[8_J)ߩY| a mQ^&/ICZ] hfկgj@M]_mhD0(3}A_/O J+\]Mӵ~IV"E^$Ym^~m_Y( mj+?MLݳ.i<^ʰ ac*ScbnWCȾrE@)0Uw2"d M,L8M&2>l=y,f➌Cѣ,\nb bTɟ6`316d3'ĒaaOS6"`cVy"J7yYY*bAnU٩+wZ67 h= za/}fBFDU䙱F y͏(a/nV ];c|Ĥ9qg, Op ݙϝݹ%KP @0`=?1 ɇAI?z?`c: m@Б]~zn8SѢCOCՉ ( |Dzh?Awn?46I紆BÀŲ_f0 O2r{b[+}5D̈o\r#c6 G=8oܮɣøSD&f&N׭onHZW2\c+~0plb7 1 hQCk3m3!&bs-DR@几#CD)"εYIee܄ߎw9w]ء>I 0*J"}>^\"`P҄* 'QÄI0Zfb7F`YYSFk6e 1X{>sp#B`S4bSe]Qx:cIЎ(Ym Hhg* *f)A ;9{O(~hB=c>=(|b~1Od+}XM֥JOӘ蛼j! Q|\xg4M2CB݇>GGt5ٸJNp ߰ ׌m|'-?^,!}#QMfɕGW~ fɏ<";ҾzVxdL<:_;6-Ϛܙrd`x 2Fb lQXaaX+l%0`Z˦ƹ8uA~8g((niF5*'ȣ83M1_FqFCOel(lUH#s2<Vu,9 Z@MfGxNJ;imT޻졟&$Ǘxp=e9@ WÀ6l{ ? 0Hb9R?Y)gk+V%e 0r䮰0 Xg@J2 B,!#5Fjʢ1Xc%SJ)T0Ҵ+JH&b!>rp)"<_n2C ΢tC/mzoSxp96Uuzr,WCޞlY.Edy"fH3 e9P?ĥ !bQBx=GQ@Dr56وEjkFާ6{2hE+c4k"/,sOO3WzW6&4!>C@!yn^\o^?s{x8JSҹ&#CśB}9c$3 Y@@1S\ƍh 07u >9,app&{!孯< {Y9g m1c,C=:T({3/s63 ;^dkxs0x9Eqx>.a@A\;/ Um/zIj!G&λy( ] v~ӭ`@_ _vjhרpAɉJ'OTHOY&=ȋ! g@ӹ{Vq.\}S}(\?5uH3f̨ yҟ?UuTwdu!̈E8C#4d/O6bH#b/GB bPbڈoUBC`ݿn>IͰ:Fꉈo S{uyt(rpL1YO_8)*\rĈоl%|CRĉ`K0>@[ Р6-br>O !J{Ha;W̽(]gp1TlT0<ۅۂGKQЇs0((q*_Ux` pxo7J)BP,,?H j3TC(H,ʓ7wl(CשكŁZh&EVxGBd,:R]eo?A/s_"[o i#g~0 L)O!^8B"bQ\0Y̶> "JgTDc{"f2_٠NIZ(p1^,9>.-w *#;&0((Cܢ[0kꄭm W ha…ٍyy~>NZkla'PHnm\w\8bT{VOwٓEpPAr[j|.Pz0bwu~h8_!RF7(4q U e:B.ܣ(97T%w ^. IF!EWA>F{mz:\οO@|B)9ރpg*)Z(M^Éc㯹zx3ɺ醴_ЮOr7:־|R (qAr) \XV"-|>F K)y[*T\aa1P3X o-G8 $v-(`S ZObΔ2TkMn5%D.C(& 3u'~kp"_ѣ56`r@Iu E1e`>3'F ֎chT(s20JS|68'!@N~i[J Y:h(}P}C'oUnG !R:[O7_³=G|l/,q0V?Ys_ H U u;AzC)=8 5K |_(Zh]蝱]1K0xUrb_fB{W}giHJ/p'c' rD'toҪi;tY5,7sQqfI(ySH!O>rEMkLȇޟ3l[(g0#L,3T|>g Dx;|>D(f6./ Fa1՜ aI/zK x>WZ?Ub9=|gx@wZ[2tj]ۃkV's0\$PQYIuZiƸvZx8aF-t4(,hip.J~TQ%X(dPE{%څ$^ifih؅ueWd*wQiP oP2R`>Y<%5UMkPע g8_{Z Q&u|uy υ蹲Zo%}7SB?V3s%;3J!Ɓ8g%]f1nE`!p'Q#2Ra9D[1:]7Omn42#c:;LĪCX/0CPZwL1XHqz@IL ^c(3Il@rRnf7. Łٔ ,[e e00p~P]|wȼw灩ΡQ4(ԋeQ`|uhҞG.Θ;e54jDP5 S0Ncx (ة[p1bj*`85`DǨCRQwZZ{e+a+i"+WEKLкR QsۥQP^T#hMԨ8 oFЁ,Enb8:]QcCPJ҃-r;+4ʲhCs0j+E8<^'u&EH< cTx2Ro4H ~WSW6~WǴ3d/##~vՎ 8(?eF( VNyix*XU3N}R~y[> ^aa1 ό `ܪ8afȵ"haq n>cFA!!6K)mE_ fB,$BY׉Hkzg=DX Z0ik QDƽC2{Q89]1 gC]3Sp'}e?]NzB@gRw錣IQktH`z9.[Hb03Uhck- x 8+: O3f43QKW\Zb`\Ǐ mr\ڤ^j{&볞Ϣ'ׅ6t׬m`F\Tҫ"/^\0lXgJudw[ cE1ƘB"}i>l u2!fe'f@x՝fCڛ1 DW#Yy.(t+S"ls@.f7(Ιp@5ߙCکb>9a6B[A}g9ī!25#r!wQBP@7(fm#J c{YlӨNL~C잃zft7d%QтC(_3ZVWR4iG $R<Nޟ<[\qkl., )-ۛirXu|qh~ㇼ+~x)R4܃C';I^> ~}/TjP[9D@6&񛻷勧Ϣ >M.2\r x#Nd}7䬫Ek[kobT67r U\ d31eX0(̂!pO- Y^ ĉ-H0(yG+$GrDq-e3<\3&0a򕑍nz^p72jE9+~BӤ6!\j7By( דY2}EJPhj-lKxsw+, , \}~?_UAEZg4jQ^4?*ھx^4 O(2@IDAT;|hRBǃ kPCjXwu]Ix d97NsSS_ׁQvS*yR ]Ãk;ٸSsdA y< qr$4.hZ<[p 2eU_}N,dOO"4~We[FiK8C@0Ndj}磊C0҇ N]\ɇHʼn͌.2:(_c^:22S1ƄG3.ɚ.WEAu8>lu5%L2Kr,?EؑQ&|[zKYvnbS zLfCR5)}丒o>Vq o}ճC8]ǰ=or uir3BȄS %h3]h< L@e1/vY!tq^ <3PwgϞ)h%U#>=@wЊ^=tqfZ]) hBGG˸BJPYMWΞQYGu%/_,5O[\KxFa7{=t8yd?:**HGG1)̋W0l 3oH+Dfu8e R {G*4e_YG >mώGo+8` f,.s5fs%u σMwTeF[u0BOXqfOcxV9!6NA͋Kh{]Pk @!\y0ɝ80Fa'ipq*̴={T(l zYZV=yѡ܀5̽Ǟ ;!jAO𪛩W2 9_T!7 S )*oo *crXxmFo|V7頢z04T{SPJ1_t-QQn%+ X`EУ<yd3|3t+<e 3KDy'9B%[ \Cŧ|&'P9NJ}xU6l`/_K$1:ɕioJr%|Gvo*ǖ!e<u1\/F(2ɡ=Uy!)mjQO 7>#/ `h np 'GU%ג6b`[/v^:i!u sKT0Gzfaͻb=Fa%B!;T B||/k€<˒LRYeDg}ѳ3fybҮ֮"\~Q97,6~7x$]WP^|0e]Ͷ=MlYU!BQVe<霏 B|QQV,O oLgaq00p&HN1yJ|Y0=308Px :_g<hW05E9F(sJ$txTji⢗Y*FCqI_(T]#ExR;37KͰC^X&5V%WQ7=x;O!u6r^Ul`0{W[N'4/x?1]3ҊYؘm?Q 4O9hH!ȑ7U—Xb6z8˃q>4P]_2!&xzM{2e R@W'~G.R6>gEd&=jrx?VڌṂs)sc`w+I s@cE8vTT5ֺb2ބL&w@Ɠw|_0|%Rhc`:Ek;nge.0Vp!,// kr~} !Y)/YVtWU>K%ݡnqA!E1#UG+, , OD5J_U/u72NWUg̈h_G|eefK;0f\R4k/ ShZR=5̮X^99zaJ6z*β8`گC1\3yPp) ͔Ci'%XB~Y24EPԔ2I6_ӉWN?+ӻңZcK}Ut:ѧyvbaL z!y);}˵2OY RjTju=Ozd3#N׾ɠPgmro_j%X^2 i#0^`d(ks@ܽqb!g{zqRg.y_vE$ua7\/0'&GdenxD8JO^M$p+3ڹqYYclr2"$,cx;ҿJ| p}g_="tEX8԰l:=qޢ1 Ʒ =:#zSe4 U-|mfh|ݭSsA>vYf٧׀")VaǟLVS@ٖWIMq'eND~P._MLk[Ggw{*9}?)`KM:*Sh^RajPő\ʠÿgŋ,FW:2cD97C^!#dK.}I/7nO}`C,c9g a2\W.ln֊$c%Qa,؏ ?Z:D.9ZDu (Pަ55{Bga\3`X6r&L=RV̍)[yCya̻`n@S X fMxJQ&mHX`"$lSfLh!M =8=r,hWcN\шkb0@GCKa -fg@8]gN2(A]m&{I u=EHhEqW73Q2[S znT,UP^1 s ,b\͝[C5+vbX埒\X&ѵ/;6@=7_sʿ0Z~u ,n fsuxy)ǐ\5wRWϋ-8(fhZI;3UK^2aYc#gb@Qyw1Q A  !B Èuu_e1Af \9f( 1 <Sc-e=',v@+MBr^WPDyEU\xZLX7,Vg  i݉[!I[dN̵QcȘUqȚQZXaa`a*eZMN C~ٸ{,T2_0hd8>]ŶSa5CMrSxPz&Cqe{Fn8,JTƢ'{$=Ә{4W Ɍ mKu<;udaL:1-Q҉?N>ֳ_9JgH!J#Ë/{;O1k-k\pYJ*O-gmg=eW= Jf,:Cm7%mI0b*zEbJG|9l#+8XŹHzWo\H$LfVDDn"Nr3ȵ.~\V^ c0,' ǁ \Jըx y&t?,08 n2F u6o% VXi6Bt"ȨR ?6G2g3 pHK.4JCC{5t]Ye:㤞 >t3u| 3(7[xuL<_h<\k}y>|peeZyy>]pEp=x}Ёjo)s8 :&;/S'Šn"~))t7S1uRz~f Eqfs牸 ׌ӄeʴ@f7Ct3[d b(%hT DDJ #21PV1v m|HOU>厂D-|!4;P1fs@kӒ%wGFIpq~u컴c&wń>w xևS3OFyL9<(d9mxzGNAd Y&3n~O-F1@p)-8/?d+Ï1?;>ÙTb`<asnv2 7Cd2fB.79d#}68R.QG{} q{Tp2`1]XQUIgugtHx֮ rut('uplB&ʛ3[`LýMXkwOue悽 ͞ev"_d&E95 MxS=KpA-7Jm'Q)k >KP1pJcI)QRPrp,>KR(SQj D)wvGʬ| Z  -EG=C<i=P4dvp)"k;tm7Һb+ w/p\흥1LWq/{Z3:pb|aR~x񵻞xhxv /!b @ꆎwXo[W3#`*7˗#$pgc';đrF;mKmpC"E!O;ᇡ.  x9TUp uoW9C}u   Sg+'@Y.D6,WQ65fV4׌v0]Ua,S{'U3q^[>Xrpi|cWCMͺ8 [q__P u'Ty(KPg*VXxv ?BQb#m1d215fn]cm7er jqjH 1{_iށAgx_EQ=Ntb S<܎S 4L\m2 19$|d72Ws ~ !.+ .v㺙dVy!8%ʘDiRҭ).O=b ^Gh࿴rC|rxqM*?,>s-G.8&MR>/݄ƌ@"!E9ewc,)x30f'1tdy9}^x(Nwo;ﮞnFnb`+7 &_5Ͻ|?Ì`D!Ei$P³H&8 *$$1XMˮV_G ,IݰLN!u򯒇rZ pz&*t_bՕP.[xtB$|e"^g dO#Y.2!*^WufƉfMU^f*Nx/ \ wC-y9I׳mgCwZw!4rDiY7pRd-ICw|7LIPfW/"^|\Q!O^O,`=]jZH5VV=ɜ ~=ya=*ye?Y־  Tf ķ-;"/ɾu[@Oϸ~ݲ7|Q-@YT8+, \'nY\'+\]8_Yݰw`>vszwS Aqs!bdY9 M,[A'o׳1^g="T);My?"BL t}ufzF)2xle*!rvwē .k$_(N$Y>~Q➂->cVXz +z+gpDVeHBԱ.u:(&uSGG#Y%[0<@?լ6ӿF1wN(^zU9| ޽5ɕ$wb/\{tϝ.9\dzAo2/ >Rf2[p@_4οTVfUVVfЈN{G=<I$c_?GpOus-Ly%ȣC8o2lobbs^0P/@Do=z-t6M^&0݀EqXyxLw%1(3 ]O_;]WZv:@_j^9_s]@{,wG /繿~/˲O>7dR7GN+=q_J!gL=Qѵ_l7Nq%.`@!ل`} |Nj O1pVUI.##9ho.ypI=:xtu³'89jW=p%kWeN2_ygLSm=ʁ+;Gf@}X( r|_0cKbPXX dڢx#JAf- P|6!VtK_6/DQcP. -۾طCA?ǤD=Wmc}!mhB&J^o߹ed#_F^  C?)丌Osb F:ewlp#!)dR=b3612gc''ojCk/Plj>K2)j־x`Xx%O/%;S.Z3=x멤~p>uūvvQON(+ϥ+>]goly>-VJZMi l^ ,ޯ< *|Pu|63D䳚z((:O_W>VzYm>qI@Fu/ K>N׫(`tjayq=eU?Qip`_H$%t_|Ck6]#d9x(TjLsW7wNhB:NJ$P98ӳ3!_P͹!xstDQyPG1a=soGu-(YVIi-g?qb Ԇnp 6ƞ~# @li#5 mVID)1͉7,(IrbCg# |e7@w\]=!Op_͔'a!gM[>2֎'Z(' ^YQCaC;6tfFɑ ݇z毻vu3֍we;V©!v]! ВgUn.# pL'S XIk / kPH@ 0JIYD3{ 7JA2Ǎr@ʯ?8.xǺbPh|CaFW L.evIǶpSnFHǾ ) 0n{zrHzZ6'z#u`""ni/jT2nz dD>ft= N2 #`lrd1Q;7s TK-YF`r!q{_=lPNާJzAEHإ>ԭ7wc02|#r zX~azs &[MM=*s9aαN${G CGPn:9PR 'r|Q7bn6G.$Ԟ"7[/4d55jᅾmpt[R%W>GG1 CgNKyb}JI 9䑖Z6ńǁ}@98%ɀ}Z`L5Y `..,K_PBu\%:x֯maJA f(CFM l]^+sGk@R6#[>)I5D)i4CJ=zkRjԠc3i)#xl繦ݥEvF{hP.]7|!=ds۠Oe͂Ň/SV.1sFP \pA 㔏Q za?`zF-y^')}JY:Jﴡ{E]I@rh~q 6ئg7]Q) n\'eⅻS~WсghC8J_)>t  ;=@|Ԓ`>*@\QQ`no< HB7An .k_&S|uyo/ys''{!k1J)P驔QP?E?VIGٔ˩,Ϝm_To+?uQ#h.uu.k?J̥xݍD1G90zE@uKdaъ=~#  p9YMI{UrR1~nЧv;&RoSVTH(h\?NtgmEeN97I0Q\E=d;Y'jcH?w\mle3K~)ڜ>%Az vm\uYO4oFT/>t@ ;.AMQ:?ۂkw/!^l4:9f<`Q5-o#LV8~A3-i9|=ڄhSqy7nVI_(j?* _0>ͨ0UV}  e]e;0ʙZts;AZiRsPwzkZGgu|;u2mN;k0ENkqmp`4~%y#EA"=#xZ3RyS&T Ö8'7Pg9[8z̝v #&cQJe2,ODit2 N&\s SN 'px씒<8muTK&GD ET~j !>2ѝ ;H^_֡:V6)zO.2Wwh{m΁&o㰢3ѣRuб΅ =i_慖x|hF8(}3?>I\[NIK=9b_A飀9(51 0g'2LFs-)BxZ1Iy~Z>EIQ}G^hVg|WoJ=QjD{,#(VHEap~[MoEsgpY=Kd:ja5U>{$=NrR ~S`6oځ꣍M֏|"o nst}yxC ֎emOHkl.]=cT\'D7]ڬ|W}eSbߎ6ovY{\x" pYQy\Wo\;`,02xL9ָ5bA⾅ՏRr=b53i1Cb#4i@&`H$k [{M1z%*F_mGuu|.-8x#Kvmr%'= U߁YRymЊ_eToPv +/8,QgԥK'AwSO鈇(bpN^6</wU獹r?<}GIr빊j88O9_yZhu$V#?&KVNUP;W}V% I֑qd4lpD;\$^w |]<ќGS:;b, 1a@LdD tf2d#忽gD33_x=_3a=~ F("P2jLM}9K;u JdS1rjDn&z!sݹX:G4;ELZ0l4l_jE@ׇ%9{^t$IK\]WvrV c&XqFfi8fەL |z'%0DaatD@ 9t\SR v]F9M̊rsurĘC"^ı%K}Z=kВE9>d}S>{(+=7JOhcۗzB1i}(*gQѣ:w:82A9EJQ~,J=0uY)| 64p-╲aY@IDAT\twcZa|8#84oR oJo+ 7_Qu(1F.touVW4S;GEU.ԥS䧼_&u"_茢h6%DdQ*@4ehvɞUS gseO dJ:jG_Vʍ;lKޕ~Q)H>ޓ)c"ipH4eeɷl iC:N_ 3C:%K;Z 1-wI]FMț|޳ڧ&K'kd3H2 n 9m׵8Ij\ Oi*-@diΦ}V{1"ZGסk9z?#"Zjke6ɗ"|(viv3ffk 볉iUJ6J }G\oI LqP-5uQ3}m(6iWp?G~m_\amq{'$O2Ra 8a ~ ,&K:gWiF& @~26*IxUO>84 εM>1uw0Q\|s/H{fgB^E ٔЮD_ :׺N략  mpS$y*;VF;2akL1zWuL`(<498JQq")W՜A_GGk gNDZ\Kh'm"/ *G9 @`Cs`8?P 1>j#v̟] uf5xTǔ {?L`*5kVPNGlփ{N|439fT()t{佌"yKڅ2DYUr}^))W _q$$G󗣊2:O^f|4\U׌Rx ~yB_Oh. MQ `Tgp`p G$Wo3Y2ݓrD IH8ȶg6g.yIyd#cx:QLp8GdDػI6#9eG:#cWٛRdeG.\N hܫSAsY.)h6<=:;^_g}~gYGvM;8(H}mt(ˀA7|Aؖt;N{ݺ09|G~KT'bߖD7fK!eceCb Jp| q? 4|J)l~%`Be%{w>`n79`r ~: q?>fɹ{iFxl?T u>DQhcsaD46FKorp8昹JJOF4\b " /2((koN"0)+su28p hRLX`~ivw!R0yq1HQEA]mԭJut?J'8E")X0&Lt9|hlӟL):w8ғ /-a{MhG( -6ޡgE%ᬶ۴Sq0z/\e Z?TlW[>7f/([|3xd%PgG%|7*~Y|~Z#&*0U 8{hɼ 6\2|\sf IQuE:.VJbW?|]wsvo% 47(EcF8B$װ?ezNE3Aw9Ms#`Kk>n~*UUJQroRNrk2n2upܴUG$6 eǔlSdTPx4W~Y4Gd>M˻E铢/SR7lȳFqԓLm%v^] MOdr7D ]ij&GG0=HʇpY;gۉcgaMpIK643~]!FXꔍ4if{򁽢]8Tȡsq9` 1i:/>t k`(8$q\&?X|_g sӣ{(%;H0r-h\汯.G\A;|q>I9WVO^3_EHOv>00T/^\{g ]a_ -QzTh2OOt':Ԏڶk}>Çm'848ph'|7^=A~>E AGQZX p%PW8hlG ]'A [; VX:wgZFFf^y+?dDk%ovh'|Aڑ#}{M=0̼Cd*˱OH"83O-R}BlJF͗zç5G8C&:h콒Z?$'HEIf#8:H ؘI~K&Dƺ|ND܏8MJjiF.ހx84&M2܇םZg-ƶmiefbxY]U:ƸNo=6[:ynsoq}kʥ7sWRj=:ݷE~PSޘOgÉ:x^zqu8WX20.2 hm6+,5/-[!Jqv=y{_{"uE0)u^^IeRoW4}#N!@`s ݥN9_1'5o=<MզO-"ߗ VY4808p!oŜg%ZyocȇȈl~'Wa̓0uIN9$gߓOvM=J17B;,=Rf X{:zf.t3%q֒78;c(h:vۺ)؛]c7A94^ Jr:0Dw1]>}^}\n[qu] /p쓛#s8p @|)KHU& 1S(9)k-wTeXR($w6E}es0 CX#FX #h/9tP*o-N{t~F2Be ~_U`SUYto@9뒖(~ry yAo0߭c 4#5hw*7i"~Ix}G#>"|Pso\8 5j2k)"4[vrNurm?)S6"Ȯ6rZ*Ӣ~ZN?.y.Y䧆S 2+Vd.KNe^U# [MGFI˥u;r}m XK_|!uDGp=eg3Lo%B*YmaW483a8@%p(A k^@bu L0,Ga1Y8-ǡ9V@o&C{c U?ˤva@01nȈ+ yv.E.`/}JP7y<8҈_ t$Y ׽Wts}Lז ^+Q@>DƉdm){P~]0e pv߭^'uqˇUc=X$+X) #^R?ݘ \y1pEu2c'oד2N3E*OZQh- ma M^ fw0_xy}S:|wۨ.0wv`O}@M)JH/)|Z{CmC}~ΨSH]>MGѬ) ݯ+UF'("rNS*Ḕ$* Cnp8_KNvd[Qk;pq0%.8e'B9"c=jd画鍻S4:90|%#gb\ytL+ z9G'G2sH;přkDY\s~r ck8>*iD)Di2=z(A7:8LmUYqZȭ>Trc`]F~f5=&1PY6sPGY(5; K2 }, ggR3IϣPH(IqO!Os$_^:m//a)ɿG8;l{uAmhG,Gʍ~1Lc.:U񶒀U @lc(G; Umw9ߟC # -FuSupl`\]d0U_})_%S<vϡlggw;<ÿq,A1p7e ('?MNI>)zEBދfO׳/QPžIe Mk/D~_|d 4!ɳeS ;Viper~6<-A"HHB]+@6#weH4&vymQіF,cwJ@23$D m%S9u#ʪut:07ry;,&娓|՟3Cluo'|q ip90׹2&|̌AūD%|A Ŀ)HD)r{~랕9a1.DjF0F`ayU0=G)}NjjE4 PJPTy͟g|S&QkԡC[яԥ(rBHƻS㠢\.MAD+)]ʎ8/t(-'\QxG)*/6s 8׸*WʳƎȻrXcYd*ÊgTuEcd✅6roQ IQI1BdY'HLuk8l{ qLj^v&'DZF:ʽȞv-w=e#l4̮[yz){Dqt3_Nuj"Wu#]79>ȏ=-!SE>r |Ww r'WؖQ96^(&J8 |j/s鴛 c %m=޺ԭfeTRP O(Ger*Nj9þZ6GѦjo>::uZ!ci?ζNqD:~ ,8,MBN1M[&#/NhI|M04&s"7 f=;UMqTO ^FL(]#SVrV܎GDY bQf*Nd'՞.< 囇с6 8_f/LH Z uS8vuQ])Qhx5mN;ϵ8.`tCd/~ 63 A2KGv |XGo&iYn(<.~\f{{u+s H=JE@ I7PP(L{h3 %sF}Iv }ey;B+FAEgz/);] EXߡO/.OQN?`{Tg)^*u3=fk2NDR2!eR-9 R3VddTy*2̲a|nyL[M|oavnjjjy2++/ ZvN]+Up}娡u(AIk#4*S|yzwU9xn|2*s2Mۥ_Fbח{f`P˦<;9R> ip90Q ~˽1I1λ'?S(>yq%Gc3[wa@ H4A ~hňrup *nfR"zڬVS7gEL︨} {1EЊ""tܣ 6)~\IGQFL"]EHϨR؆~]rl2JAyb#  ^.mq=A:h"z Yps VN[2H0gE)^YYtZv^})$=a)7כYE !$!SHLWK9 Funot5@{1hbqkp0]Ox-+Zق'i >H9m@e|i38p(kжOKPדQorw 0草"F1ՄQޮ翭ު*x~R?n-1h*RW{jRn{J K$1w^~e3~!t 0 TVWR]DAR,(mB2J1ipfp 迍X&nm&-iD4;_xUvdY\ ,_uhw2k'.yH:ztW[%Dij3hU /zu!]ѭS!r%ታX2#0p<޿%g}|H/ +@>2 ,:}B9-;zGj cހ@JiG@=@|L[禎Q;09<+}2҇d(WY%xP.)s0Eʹh$%_<{g֘aT(SsnD<)YpFd~YR+qWDGp,]fF;Laڲ$;Q}m1ljCE2\h -@2>HV+-ػ8h[sG{:r[ۣtR8Nٽ\2TNN\$@0R\3ι7x }g)2Z#GaΎ!QڕG;, ? x ~X]᰿?@Hr?`m_PZQ $SV%饓jt ʁse JI'/:zXw}9~J2Fm[qG^()mдEOb(hq(v3vRw x))ȿs=%*IĐNQKH̺BE arj*֥F]H~:7$L"5M9]˾#ԿeJO6K&d*EuiyAb0XclurI85R:* 7ulmY &@k嫏'層d}זqm1Xrn#36s;ymh-ڀ{g}#g%zw`⒖<93x7=dzh,< 7)&RiL Y7e臌PScJXiY@[gK Qߞl2/kGE- }VXS<{pţ  H1*[je7mjQ\T,{bآ_9RbJo$s݌jC8~S4ZM!&?yIexbuK/ 2O6_UQNw$ Q γލo,CI^ϩoI[ʙ#@4zm޿ E ݕP`LΆMj[S[# \3tˑ^ p"`nq2U2Ӯ h{brFQb5H/ρ!( ʠ H ,&0ia# D\ n +C#'3uUy)PՀ#Q]RQz'j=x;vQ86J~9xwפ ˦?{5qRd:穽 _%pP||GK+ w8FUJ1aG%G]$,9HMaX2h "˶ZmFVo*yu]F6ޟD$H?,rb?yN"66Nsg.昌bm,/wJ>-OוedJ}?m Oc&~},l9vIx."zK?ht8_6HT݇)o3i=yӻj[8y?gwU`sglzo\ƈ!0|'ja4Ȗ9j瀴αqJPbO[(F jhe3L9IqxWIhVbi<oE.Oui/ClJkN?];%tȥG\h欈fBE6m5o&j}ʞy'7u'36ỈajOIG;10 #\1w6YVv̐bV(\L%^ߎLYqT#0#mcrFD+QFUؑ4F0ZtuaCÞ,!><=23.3mܹ6'Pz䪻m]N?o.;j;lt ]Ӣ۴G/ 縇 |z VG s>t]'wǦ7Vs8}nt8CMRh ~~L~z*x9"Gz~=-El]W7F\A(uac H 7tR&j 8+ 8}T3gCׁ# +/+mbI;K{`rmS#> 3FW(x1Cgyfm#fũާGjx+/QZQDi 9]NW927#oj~oe$PiPw- }v:JQQz'n56Mݡ,G*~}*9ɁOOXJ*Y ($j~I(񴠟yG!Cq%sr#wM)'?TL{R9A\)RSFdZ ӎ}7rM'`vWi \/Tr-[p}Ơ5Fr'~_'kXPL}`nL;I[?yj~d l hϠI&}?:=k>e5|Irqг-[w'!^ N궑~vғ흉6zĐXN^fx2pGY?*!DDAy1E/ ff$@ ]O( J`҈k_y Vf ( _JpaウTN$|@;ʋvgn~W{Iq#ʝqzWg**7.d\}cuMSj/u@.^X&tғ#v5YgSx|#F;:LGrz=I$rt3`}VK^ .ژ8rȔ;ޅIvuvFW`V?{* NW \㓺Ni,*A!ǻ-o"Т|r/rw9k$ 0¼ XG@{z6Q2RI O$QjD!&=Gh]qDt?!O&枹J(U*XiYfp<)QroDoNZ}Q5=g j1FM VH7d =L$׼`nL]&/?Xc-u~]bd7R?㚬yEeOG"w$E,ߢ?Ԟ3}V&jp'r7;S U$Zfr,(JQ$&>UIt(l)(H ՘2}\W%#~J#2-bh4n`52Jx-P)_1Qְ=p`@a2wbsn^k t8js4R 4>Qbkq D)·6v: ǭ|j]Q'@FZy`b,xO)vF~ p}ܧpw$xBB_h|X(gk=}UD OZ~%Nߚ>[TiȠsk#]e+~P,) *\D}FQ"(=hjg9>/oJjgQ >i ¹eMMIWf3NťhLsٚ4>$oFQ;6Ͼq+^a0Q6ES[aWSg5rQ9M" oQd6s݁LöфLi+l:,a@~Xs V=aӢg(گGMt$#u>fh=s=-nʗGKg~[ Q10i/qI.ǀC%vO˻)WhF|g d`KSx=2?%E(T2)hRz%hu|^_GeF.qJgjձVszÖu0 ?],i#dƥ5qS$6( >-˭B\g";xȃ~O{%zkVwJ-Cv'ePt_Q!Ke{𮝷jS]_h7dǵw w ?`=L uqaa CݼGRx難IsO Jdޟp]Ft4M_P} |>)uyNў]. ߵ:^M[,S;&%Kp4֏A )ŸV `~R`~׉l#:AM蛶 t Tm[^ՋBBQ{)M-RwKn-o}P8+#/3#llĐXDydH9Q-y̞hO!ֿ.R|KI@xRuŎQqRڴ?%e*Fx8yd7+ߜZ6W9`B'́gH6ɐHMoz1xh7O(=e}qvV9̮?Mn> 9s{Q98/08vCW…"˸Cpj#u_@4&4d0BNI?R.~7&382ԭ^ʑ u sk=#o72mWSm'ՃТ7-k崠(CN\j,}LC5-:1h Mfrw# S,ʃO'G/[)F}R Ϋgx? [Ղ5bVvQ)g_FK&~hdr$ 1+`yJ BLn<=(,)*΄(/mDF`o0%&7zfML$(.be\/LGRZMm^felF P1E1nEK|Q0M3?Թ_s< ዧ/7?Mq}5-xV:×){ۨ Oԗ pV7ńnfàX,K2kOJ\?)uH._qȚ'ohF+qZ/SX3GŹ^5T=Bc (9FK)|(C'tQ(T^PMT[„))cxpz8zDpD3FͻMm"Ԕ9k]pm17(C@{.4q̴DܝxV?;Mֲ@V.n48pX _?py h!sªI f?,M}G ! G·k(K}$tf4bjS?"ʜ=B]K=`. vD0Q2TX"z%J6AP|·}/7jה1,,7'@`vb$!՞o=k<-ٔHFyݦw/s]-غ32WXԎ_O^Twh0І.v8Fqm 57$]ޘDuƿ0-yTfhR/pu6;`#gƺG~m+rq{r\x)QFȘ$`o3${Axkؘn @0X.e&T/ mM#sK'Z}@PPT4%VR*4# ʂ"ŗujeEl~7kf4rյL(~p`pbL_ؒsI"e?m՝+#N^|kYB_A*i)3n2̒zt}:yE|Z#uIds\6[\'pr#̞ꗿ.M Mۮ}/4 }?m@j߉5&{Mg-#嘚vt?_hn 4kNVOŃ bj~**꿕aϣ F1J0Iل#̳`Iah13d) KȼBZ>MH tG%~F}b2T(FC)) mYjDaPVFMiAe8+yދWM!H:` @/:`'J Qjы\ zwHPy2_0=޼Syʡ\S9h+Foڥ^QB{5xNCËN!_}Sj}ߊnSZ4'ӖM_[# \?y>J\ǁ, `ף )A`W~gos"?= )(V}߯NX1w3a@ūþڂ>SoKUDktXZf&RޘaA F1vQdmOd>O*|!aoEֱ_6ń^90wAkr ?&~g%6oF&SQ-gou9a&41*$2W}QfRJְXQEMh`pu tf$<Ę0GfEuS_K`A|u{o 0>J qS x_8)C6pAUQ+uF֭_dTC/h%C꯯1u *Q2-FJ1aW?/އKoO`JVR8&9FIup&93tT%zPHlژ@~yNB Wasa;Z^ uCQb:`doo-#[ujn!S@SoIY#ʹLwBs?+2OF?__L {.3/e=Z_R[n 4kOHYk'@٘3K0+ 07zbO*k)*6q*:ZeޝhM0r,X(ˀUiIS(qvd{I5B}64OUf_U`}kgS hz\_)p곶IE9yp j38p}l$a/`ni JWI1@:lik!R{sZ>)Kdg%IXj|ja[`:{Tҕqlt 7On>A'ZMU?:0~㘌n洛ۇIh[FmXhϻk@2e3L?Bl1=sN/|8@[Mʱũ0O@T&Zst)='nձ@2   jכUbSbz2K4J1^q"do1UD0s1Z=w y{F J @b>F@TԵym|HUZ,\-Z2- b~:EdWڔg-rMl*O/?35E?DQkZFc{yn @ip`p wώůԴ93M͘f܁DB֑I1~(JGr}_(3`^C~יR\'T50 wC_],,!޵ˎ_q\5ޟ{vSx'ݚf"{uX=1)G۔/#=pe_&d] vIOw&`j1aÁ9m1(cƈLTg>nT!J1L(1ZwPazLxEP-iSKZpV}.ٴdj]| uSe?R/jE!Kg/LڳV)?pr=}̽6IQ1JUmZeW&+sYOK OJ?/ɜLȇ Mw{RFB Y^v8t,6yp lcz%a<ڬa$<iD뉿ܙ7\{9ޜ#Ag1g \XgW'ڼC-I{4nNVEdo{z8*:r4őn 4 d'x^:(Yj-QkQwhzmzNu5|A Pj;|l5a?˿ƱdγOf-fs{eԧjon_'`s{>żkoR&FߚɆFVdoLF[f^W5}#Bpee^~̽O 6ݶozՔE)Яp3o}i  1~m>W1UUzL+Զu<{--oBR>tS].*_mɺ4n}r1`w?ϖ\{g$ְҒúh&zىɺvL^fsSIin 7.ΜorܮT.\9Wհ^ VS)~4.M~rʽʆk^NKZQ5]nfoy֖OW jln*_V9z8]^bW'3*Gj,΢ޞua̷ z,{y,__]qq{5ƒӑֱXY]_>=L ]Ő{ܼSOz|~]|p52[n=ޯ}_nLjrxi/YIYKNտv=NƮlՊddZlVH\PL<;a՝zh]4\ݙkxs⡂@w汖 3гqK(芴 5W|B=Vy7'貉sk*: ]Ofͦ窢>zug ߛ6.q_^/U&ױjfc3jm{?Y#ە\=W;A08 w~2f3ߜzWXg뚇{{8)Z&z7g}p$_/:+W/JBʫU]i[TaقΡ<ҾrrVاP{ovYj/>○ZJ/%&Wqy4:ֺfتudlUtu>t9sAqZ9jz﷿7״z^MW7QZ߼+Sqo"]#8f'K@뵿>] ~S*#nt Մ3adGpEj=`ߝNk3YXUUywouxںXޮP8|T{oVu"5մR:ߛnS\Tz>4gY햣rm}tUOU.۾vn?4Vũ295XQHDvE JG7Ц`<_|fr5yDۨo_`ڌ9ܥrp[i ^kuܬ@qqW;k4dooB%N^ǶÏҥwCkj])Og~u\#Ⱥ|go'SܘzAfKi~(SVSbz bz`j*A`VF3Vgn2Ϸ.]<˿4O)]I~c ǔ?ϩVy_5*[G$U__ꭖq0NdNjrn~vqWz?`u/NxoKm}'=7_{k3xSl:宥ڎsɟ8w}{|7GSom_JxK3&X|cw%Axu^Mj.T&m3Qd~;˪Q`+s|>Oכ`[몟AX֫Yu]_H$5 taXM5@ԓwy}d[<=q*hۭYc]PY0zuYaLoƅ6iޙm/`ͯs~RT2_7FYgyo3!}skgD"iFx_s,O<* UׯWdGHƓv {{?,g=Հ`]L| m5'=^t^bZ~}N9kK浗ϏGf=nO1Aɕ =k]8B~p:?,5ͯ\Pγ7{ݕw3nX0kk]՝0]:.:'=.3[#憯&|ҧۓ҃my2߳i*-NZڊƢN]C㖾..L/N^R[Yzc t{ի㣣rۛYk!n~?!]ch?mznMkݝ5|8K[3p~ѷM^9 .' 2nD @' tIWgsklxhgN]T®f:Ifw?m$:B;4DOHUm[q}S?yZ ؍^Ш68~{ Ytz]qmU,&uX.GD|N^Y{#]Ʊ~j&*,_[ OGt.u|zx-{\lc{i4uafo|99=?;ξR @Tń]}` (.iœv*>ٺ׳m9~"Y|LLmܦvv>bc{'z|6Gi+SB5yeڱf[iZc缨cpZ='@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @soHIENDB`ic11^PNG  IHDR szzsRGBDeXIfMM*i  bIDATX sTUƿNw@ a,7ԅV\XUlN+h9e"a0B42scnֺu{|Az~ 8v9mQ״wSɷbѠ9E:fMNiQqm^5g=)`ߛԤu6sjj-yBg ԴV>M?\y^ͼx} <;{ uתi3h )EnΒC: &hVl`3ꤕbK`),oӬZESnt`tQ|<06 pLi/vu8Z_$ sE/$>|YhSj{ EthU[^ݞڨdȨ@!@ ŏt=B6.)PD-w ʽ E۔Aӎi Jw]ХqDz:ɘepU@&)4M@kF[)J!4{Ǥ,x{H;g6A"CqC\"xR"ԁ1o0wɪ [;ru hwYg~vilk*h-ڞ_j)T0SGan'8y*b.l>)cQ.\ٹy$H`>4穫 @X1@s+"N˽27G kDS1Ҏ9F$VrdXu94.K]< LO׳!g+O#Iy%΋i}g;cbME9\ fUO߰ eYHA*_-IENDB`info>bplist00 X$versionY$archiverT$topX$objects_NSKeyedArchiver Troot U$null WNS.keysZNS.objectsV$classTname_assetcatalog-referenceTicon  !"Z$classnameX$classes\NSDictionary!#XNSObject$)27ILQS[ahp{$wfview-1.2d/resources/wfview.ico000066400000000000000000002236661415164626400170740ustar00rootroot00000000000000z '( I%H_eaabvjhaaaaffaaaf\mYm\m\mZmZmYmpjbaae_msl6 >7ywYa|c}nqunnmz<?nnnnnnnm{XD{i=n(!kI`pI=??>><N}z!V8nihelV?oYKG:;;:?Y l:ohKa Z|vz@;>?Hm3 &>pdPeodqjoy QT_hfIEex=8RY !/-=WHNU[nw>";Zi>6@ut;HF4;AS=:U{JfTIrtT3)6Dr^!_8;EUkF>OoviT]ORgq=9RY;+"v%;;=K|wOC<i=R~?G ywXK_u}EMriP(",;;E[jmI>=KbRUF;XSh63Q>foD<AMh{# 43;;nXEU{TPB:nb]S]]q]h/J<9?H:;;:Hj %Q8;:[TG;>eHiyaQNl>;;:JyRS{rF$"i#:;;@uI2(Boey>;;;EY?::qm@<:H# x+;;;;]pb\.\1^]eE;;;;CN?B?I;9es$ !2<;;=NhiFBI\VPu|gfBqSRnI::;;;:FQHO?eO?;P (A*Q>;;;@TbK<?]DI\9Jq~H\ONf}fWQKEB??arEg^FEF"Ip>;;;;Pppa[\BjB:;Nj[T//aw~wotwgLrg?" Mtl<;;:IOCLmWZ&7nyg _j;;;@JOZ~{ci~12jm:;:Q{gjEcs=_QIOOEE\AU~:K"YQ:;:R|e~_:Ah &+HeW=:;:=}s<\g6*`1RE;;;Btznu_SqV]|^J>Bt <2DX^w]B;:L_?^#yOiE;;;:Ve=K>DYIs_@QA&k2<a{g[cOQ|jP#w]<;;:YTCeoHFGAekj <Hoztk):*n=;;:Qn`xj`bFW$>Tzx/'?FkkT<;;:Kxl`XoaFm>*?JvugU-)oUB>;;;:`gJP~~NLmm[9a3;?f}}iqlrvQOraJ~0HbGJM<;;<joad^qXYbmykg}}M_`"{<;>cwstrYGKOC?QIA<{ccyq@;;=UVGAzwxi<=AGOpy]ln[KU +<;<ZdWslJH_ZKNFB(,XC;;<?=:;Xrh]:=HGFdwntbJKZhr":=;=auiowfYOKpqL'~c>;;>NF<;Qym]:ATPT~yXQ]s><U1 16<;>aw|SXdaQvu]-iYJ<;;>^l[Sn\GUsr}jVLUl]=;aWW9;;?jDzEmwV |Mp]F=;;@^}yuGlzn|zkW_WAIq 9M?;;{0-l=msm"1 S~c_]D;;<Mlnky)^}xrnan>H?;>n}d-+NenYoj^yDLKP{}E:;;>OYZuxwa`Rizpra*.bG<:Bd|iZ4NjvhTi~n(agD;;;;<=Az}Ya.A=scy}lmN?7rZA=?mHo6Ggqfd|?rwe>;;;;;:EkFwgq7yq~fZJtheyqNiPBBi.9 O`fp}hgbV`js{ KmV=;;;;;:MmDm&U.xaKFui^q q~R@>D\Vm\ /VaiXOQVg;.UNH>;;;<=:I_LEK)(p\LwXP#%dJACRP6+MnPE~T;>HlcjGD?<;;=SJ;Eil"w{^Iy^REM&r`C;<B} %rYHF|uiH=BtUqCA<;;MjMBUyw U~tGnpfRMmJVjI<;;Jx !A5DRkt}her -tD;;;:[gBH^m|yb8~~WkKVgqiSEYbuQ<;;Xg  uGnpfWti{go346qxN;;;:_nCQN. 0'MXt>KxkC<ihXF<;;H= I~K=^hSXn{gmrZZ"AK`yG;;;;maux)&# #n_aKW{c?Ms~{_Jv=9*~zE;;;<O} |Z\=<]zusUQbch.\PXfH;;;<xuq|! _i`mCQrX>Lamdy;}|Y>;?Is{ HNhN@L|so}oFCyy +FIoV@;;;;T} "LcoIjy>E`[A:Lew]xa>=Ynm >{dlcLDCEJQWFYz,Zkkzv>;;;=\xlyV*Pd|aBeuRFELhs f<;IrD  _Aulkxqn`YXUYaejQ*(icgc<;;;<JW^g0rijihMG~nl_ij4Gz?BT" >9h[kxxpg_VJX|vz9[irM:;;;;?C@Mt!zf|@?m{t`NV[x4wFH[wqfn ({0h}vtrroptkYc yacG@;;;<BCX!D]u`RgvvtngPToaa|suVQ<=Fj~gl{|u  C(2|~^jKFwjz%upy{mU;;;;<Vq^e ,Agj[[qqvprk\mueem {lA<<audr~K  BPgyteYmHs[apk]r[<;;;;DQl7 %S\X_yxYgXf--pPF;Pmu(&ekfdaTZ|q L]ha]{w@;;;;L^h& <aL@xuz_fr^LDBURPS+Ifs\B?;Jvzwb =Mqz~a`^ZH=Mrqrv SebIEml<;;>W+<i^LUOWda]P=?GQ|Lktra?=>Y|%!ELczwUE?:Lob htD?WxQ;;;>Rclh3Uou\TI<N`n }uecuT! ,Feen_D;:GyuX@ awkisj=;;;=DP@"z?rp}EAINbjqx%#-fv~ifh|.!W)UeP@;;;<Nx`DjL1xsh>;;@]if&=QemuvgUezSNhoqruI(TlzuDAG!j=xA;;J^|a]{tLI?hETD?<;;=iu #J6ifG<OqszqZZwk{" RKu|B;>Lakq_\daMNig{tqeD\nYC;;;;Jpr#6"twu@@S r[$ E;m~e>McZO_vaU?;L8Vwk}ca`W=;;;G`H!\vszXKith+ot==G{mq4" ))n?Nb[GJizT?>L\_>*FAc]M^B;;<]o%$XGt_NZ@B:{jKCN| f'HEB@EDj}r^Kb[PZFejxutwVHI=;;GYh ):GX`iWFXvhB}fZQY Q ~pzQ@?v]ifA;h5ELa{owy[C>;;?bx~("EQfbdf|tN<JtgT;;B|c >YbpqX?>ypQ>Ev0?ssJgfHP=<ESyQ!Tok]UWfby^FOgtK;G}xqq;!"4soC>?@BQsQDcV7UHo`J<;=B[,' +tF<>a{rkb`y8>hTBh~(\JL\RyiluV<;?XorksdVNhcv_ aDfO?:C`cyIRoG>ZYPFFFqJ=Og(>Vbn_O=<CSy&";xurqsqpCRuq|hR;;L\eIGnk7 C;<BbgSW}) :EUdw{eB;;<?I[.NWRdww|a[~&cE<ILZQ>PtB foE:=Quxig]bfvM#Yb~jhaG>osH;;:S3 mJMjhhfs0;^x?:<EK{\L@Fi !yWR=Oql\c_B<GlwzwX;CVeq_Ra^;;:L gd{`jquU^>]?;CGIXoeaj  Y8h]mvc_aaUDJlfv ';:Pny}{d?;;:S*HRslzmc]n{A;Suwsszs 8$vrpsuTLN\`cvnl"CWN^b^yxzvK:;;:oe ;.yR\mbPVlrrwhj}jtdcpKxe{S:AYoxgSJ!{reg\McfXRU]WT`E +@URW_]Skqv{TA;;;<6 cmo|~e\evurcclseX~)(2TZf`=:>F^TK&!\^O_iqs]EHOMOxm-=NQQRjk_Y`wVG<;?*syuUa}kqpdLl3nsdCMPFMi 0 i=cQMcopoaAKe]UieMqqqp{F;;:KElwmUKxcOHgyn_{kS>V`V]|(<'\AJQsqt\O\PCE8mwqurrszB:;9^p(dnyqqkmf>P^Xy  cytX=>G_Q!~UIhgE<=G;IQotsqrV@;9xEAwuhlwZp{!;rxXDE_,;Cma[_^oucZkbDK^iyynqM:="!op^QZrCDzmirCl(yWE?CLn! &r[LQQKZrymUE_~<ssY[}meD:F Kzce=SNQ{lH~OGFNcNImmUSeY\cPGfy_}nfeledmO:WzX.wI?pTjn~j`^Y$'q|ideaIWstuzmnqzxlcddR9oN(z_}t{|.SCHl2&LI|Ym~\f|jROtq[futkdwF:) tz`MJ_zirslP[;d#ZH?D]"80vsRWpR?JpxkzrG[ouh?Bq^nfRDQ`_mva}vn}cAp\Nc-rmVGMG;:AfSBVRVD>NssuFP'XqwrQEMa\IIhtvrsecfln_Ycrjna 3XeujLDU^uhe|]<@DIcrrvHfX9LtLMiWhlp`_c# ujNE??Ry9<h~mq}ytuqRBRtwrgfwiE09LOQLVoNiUi3[?;;<l -ipZ{sx|rcdu`BHt\W[E@ FrV\jhl}N=pWbw[:sIALkaDIHwxoMBUgfaTEB[^HIYLKAY\[lqnqqx}<S\I=A]^nB@JQii=f0LO]zZFCOmC;;=pzMI`i~I_b^UlLl{yk{DFV>BVz sUw|J;DVgxADO._ecgiST`^a^SV\f~e]m_Ej^A|8 :LuhY~[jshLaiLauvqXLbpfil!Mmps|mTzpl\Vo~r }kQCCwH I?WDQdjeoh]lvrXRZgx]^kj]VfkkA empwx|t$GoS=<IVm%M%/K;?>;?JKNsdHEUxqgwS<G[ait cGn1c<:;<>AX  ?DvrDS\S[xrcZTo~R=:BUyzj]  Ini_h}eBprYqY@;;;?V} IKfmfsnkmbl~pGcvg6KX[|aqpmnct mhobF:=eQ#Cown\Cq{qJ(Qt{vmscQpiYh~|;|wSI\+BlynLXsrttv% orxjeJQ>r'}vkea`c(MS[lzo[qqxshyqrrrtq M\|m{yfGuS?>>=Akk<M;A[tbHTaltnUdx}CCtkXdrqmteyjJ>=CO[Y ?XT>Jv}zu^q~PoS2iv\Fbt,zos`JCDZlf1upfytdIawh{k- x\\}}t>j'bmg[GAFZy|}UWztc_OXhxIogL<NsYNafbv|kKkY[b| RpgdhewybgrR`PAFaTIgfiyX{yY=<AKN0}_VRb`KYZBL^ilJ!er}m^xM=A@Vu9M"M"M"M"O!N"N!7B*{&})I;%J"N!O!A''\#!!!!r']>00K` 9!8/0.'N#w%$"$l&X"k#f%_&r&3DF(7CF(M"O!7A'-aK#`@I%>$:%G#X!Y!P".L%##r+4/(/(>%U!F//f+g.O2[<9>$<%q/F6!'-? h q e 7 L f1  :jC&BlU 4 Rayw4GHFY.(' O;,C&&[]0#D,,( ) c W 4 jwz||{ q Z j} [ 0! b h 3'M ^!N m T $ : !L}!f!`!@ D s{ N @ { [ -! 8 . 1 Z <  8 H #  ( e l ( * , ' J \ iy y O = .  A + 6 / , [| ^ #  [ z!W 2 "  i Ytq L tx \  <|s : ?} U  ; y R  $!S 6!I!M 6r~ q!O! K}v I " ? Or | c )  CA! ' E M } i!\ fz r ) Y u = + \ \ 3 E 4  $ #K l > > r u = 2 ox : .!X T r J"  # & &!T U 5"!/6 : a Q X!_!M A , !K!R  )! a Y" : d} d!\!A !K w d & ( >!D /yw!X!T , +!W + = H 9   5 tz i 5% _} j 3 ' * #  % R {v M hgqt 4 ' K c g G ' 2 ? ' !  ' ! D + 8 c V n B Su syQ !!7!P Vx~tr} z l > 9wy p!_ :  % 6 !  ' h L  K Y L ? K ( !!D .# $ ey P! H /  2 7 ) +jyn~m N 0 % 3 7! c Y 8 > . H| n @ ( 8 = 0 ) &  ! I b ^!Y ? Qq n G , # !A 0 X} a ?!O ) c l S  ' i| j 8 ! P m s ^ $ !T!] :  1 P M + & $ '  %&& <@?5!" 4! %# 0 6%!)+%(2&*! -W  2!      !7 #????????????@?@@@0088x||?|???xxx?x?8?0?0?0???|?|||<<<<<    ????wfview-1.2d/resources/wfview.png000066400000000000000000003507631415164626400171050ustar00rootroot00000000000000PNG  IHDR|{v:sBIT|d pHYsgRtEXtSoftwarewww.inkscape.org< IDATxُ$&_DR{WM6$;CHg|1 c b0a c7saƃ5ss$QH[sm6޷53#YYYիZbn e&%ɰǟ Gw`UbMsR<=.Ih}] AE^Lq,+%C:.r;2FO-}{ \b9a~܍/%&6{_{u<9n˼+RȆ{ mwC%틾:iadHtdw]["n%q%::'f` vOwh8.m܋a^vh6ŋ!`06#rdFacdH#iw va#C:#C:_2FΑȐ0ߎ6a0F0H#012#:2;(%8F`mdF (vF261ja!R[OFtFԶ$<4}o($##qwgRڣ]Aͤ~4%J-FoqGs5tD!ѐoprH\3 {jp^8)2D~QƘDS\ZSX˛ԋ:^)$S'ېnN_*:zT㸣U3SJo(Vm7>n?3#Bڳ C66bo柇f$ di} (]ە/H~9WYL]P͢c2, '>t3HIÙTEg Z1ҿr#xi(t\'Aփ3u5M%m0ܭB6+,uX\Y؃遫SIRG5BjqѬN 36S3أmq 'cGJ1}(1Ht$JUV5;q kյ{.ÓT%g-e10Z7ew8&.I7w6=K#L3Q#BC˗(# %KJ-khL32mۘXRǕFx8 S . ND7rէT[ۧ~vP-wz0}LbIlabzGaQ͹ ϏU!0J,1ϻ^;Z&S:)%ʺ2fqRN8%rO6ogX%5bI|L==j>~`D%(˼X :Y;eȵOKKD (#RnQa~<- d cRB.uK;-^C` R^܏~R"D'LpS;?ļAhH-;E4DsƦ{m sH7tw'8 jݟ t^9d=2qBUqCC`ԒRT67˱a\Oɼ-0!֥Zw{[iAn1L6ע(d(#D{{Kn[UU(-JI|2F#Io(i$: 7X3}zjא&f{0QxUsL7uj^& 6Ry:C:!K/f2#2oaTK)١F!EbRt%6lH$\X߷R! YL aZcYd ~)qSu\pJ5=O}&ѩpfgLd>R[Cj26(eR 2!ű(xkҺǤN5! DH}> T`xo|n#B%;RPlG ;EkhHpa#69\S]f8qGx*%hN]A-JFNI&G7h{LPSY%vg^Q(7Dz3t;pCJUcokZ ?<a"18<'qB®R ]"^פ]|ooQ0(Yv$V0 =O跛T<Ƚ$'/n \*ObL ua{Kc$:<%),KJ{%(W&:$ؕk RNHI׮)ݐJ6^ Y_#<9zl%Q,Ԝb$BuM?F/Q:1+sB>9: `D3r +&,3eTGb'l$2G:@P)=en` 75L+?>pP42Tdpnil5a 2>yƦ͏&y`v+2*`[YEؗنsFmJʁF\WKm! sVaRnPeKdu ̪ܵ\{0wu %k2uAKyHfNTs^ +tTben'78W%m%:q]e)фM}s I!E()Mٌ6椾Z_O3<٨ܻDCw#oVe:.nK(wPjJa?&4JRkR|mcHS7ǿVfKRVKQ uoaJgV*?XK`D8-u\cV }KјnDnD.h {fo>3mJ'"30k+"^XqEt9id3 [!e Ae.5}j^6f7woeJ2bHGd,%) Hvǐv\yYGR{m$FM奼4 8҂Y,V{d s&8#FnD4Wy&B9V1$1Zl H{^I'q. =JWix57wksq9Jt9i~sKl2&B#s2o!}y6'JG5\rXDzOopy$ebw i1~)qZ2`[?B% n_Q3J{C6P'1)}*jR9)F'>bN1 7nYȼ4eff;^cn2^%[E P릡oWw27~>ǻtWSzQ{e^8(;ZWm yl̊1'q`C(d`2]4G]/fe[e.UJ$>4%H\ۧ JKŦbʽaBIL_}͂wum#<>8c\iQ64٥ƢJ$q 2uU]&N"#k}+/!iܢ_˝R$$ݎz%^왞G!q55-&*BOZj^6CW]1W%{Y}{*4D" x_nS2jZ+΃IR-,ಎ/4,Ļ;bs9'bTQSӱ_fQzd1KB֥Tʅ_Fczo2ki$ OLaH !y&:ruo(|%qG)\}WAT  j~Ez$\$tNKg~eęدE[MC]=֤>;!vb}.b̐#lWWC9ZGxxGA1͟DFwN~[ _GᆄR'yDIQXPاG*l RĒҭ_-Ww򲙴ػ%84=I\2(]Y=C~%}l(/J߫{-ҸԋJM\Vs}=:>𼠊uW+$>z\9O-ˆ]#RKثfY \ش?3}G;_QhqT?3Dz*Tvql^Maxlw^{u&'R_*LbO-{tq0(̲[GOՐpmȇGxPyYWHdMD&,MYϢ?WzDW\@p ga[o:@-_]VWFK/F4ed?O{qLUM!#{Y_y.59JBte!CVJ.:oH TStiG!%u!%7 8C$616r?#<بr^=:ܐ'+,ux `7BXc;T]meJgeÆ< !Gю J9ɤY|O@jL:ЂfNF8p=-mwd o)uTpZm6'q1O j{CQ݇/ ҘDS7z9t{=CmTL=֣)Lƒjb1@Jmt8d8AAkM,As[^0b g-[cNHP1/ُvRjn3CQxŠJE]*?Wl;Hs]FWYdQ! F!2b!;IR0}2ֈD6ܫJ1—j{X]j/ ȃ6#zO5Ԑy^MK,r9#<9X ='Rru`->Cm,^:tg{VX֑,qrC%!KŁr S+NCN&3/W2-m֐#x[j+ j6Y!tH\v,)}* NeU?lIX-dx-ԕ(7YV踱=#( KG]@hQ=*ݚHտfFpZB!%ް#RthtxpR%+(5mkV4}#u~CW+WxS4ݛ! bmj_3M4L/w&z?DGF_޿PW*bz⌝E2AQ+yWܧ' B}z߾^Ɩw~׿H#JvH[PH6D3ץ|#<Wz{$MpyU536I|Z m3 JBն,uRt.Jm JT(JcGxއa*5?h7 |t/{5b*Ιng2"'O^^5\U X=DBh B -)XwOHϽs>nH-;MbӨY40?#b< )TqeLGSnL[ ҁq,ZOJ7ލU!r.GlԠ㎚?X^m57t2yY>T /;j?A#?J*8yw&)0%&Am݆0$ѐ&>a\2v/$8[SuJ閚䦥/oGwFWX3xUyW$ViG C$RRZ"+;C h=>:Lr6UD'Z/HMƒpKt_,U(TSY7j5ըW4ӖZǻwF!QԚ0{4L]BÅ!ŪLJ顪4.oY)EJ_˼j.[T 6̦DIXUQ;Sԍ_ P0Jg{:һkB-̴/$}r/(}dOtޚ6(2I}̢jSG~/cH$Ҙ.}Ajܜs&Fc0j1B*5&wL;uWM>ZlREͤEF3XT`~W%GRG K.8hqX0aBi8 xM8tVbUP7\OD~Д:>$q`ێj`>G!2oFx#VFٙRS1L,P_N8w6se cRc2DvFRSj '$ӫrǤ!lSQҐ[ď{5 R3jӮ :C3aTfȼ (ׯ+qT1t( IDATGx'J(GWG3s\yYIKcM$({=#wSf/f&J֬PD~T\u-0wBܳ;tgh٢4k#<(&RV ڿ7[$sQjVa_ rH^a!Lbg7(C'Kfw<;&uT:O}S/B:nfbZ T+%hBdHq ARjCR]x7ZK{gjܼqX7Ʋmnj!5X# ،q?=:FL.{ ?k QzI/+}|n9o*f'DŽ6_\y]!}~Ґ6ĻS=zI^fW䞓8#"DNKL*Kb$ZgB𜂘O.rG Abh;Βk>b ^[W fŤdU"[z< nmfӌps9Ra}չZ yVeiz x'^>i1}<K?Vh/R.uB|ބE&; s8na{^djV51Z16oo;ƼKo-IFċ' efb&Ԕ5 CoXp@eڒЅ~\tLY qm[z$\M'rw;,Z-2MX5c_nG3UcZƬxNm`l48/I>ص`ܧhݶ~VهLaEq.m2悃n:`%Բ)mV"ojXV׊몐irB-Qjj9ꆓ;rvoZ}li^tܐ);]ۣ培―5傆seJX 64\ְ%PU/0#6RcjZZuݫjmRS0%/txg>7ss^t/kf\¢j]|n'};Ꚛ58,-ܓbZ,v0ro3S.W\T8el1T>Ҳj٘?upOwC74|f\+JL(4ndR(u5ܖuM⤚55ǴQ&5$b؈[5RD;3abhOɵ5R3rSC-5u^H}d:u nEcQ"^Qsc|[- )a kNX沺hf9YKqh-5M 7F3A+MRE+f0idH,^nZ2RǙ1!PWe5gHFvmejJ RR&ޣ〶Ҕ\G/rVgo)u,\NV47YsҪwtLIgeCOr5s{}H3lZݰ,K]S1wsXzYYt rCZ^kcr+qzD[%l\aMꐶi5`^MY,;e(_!Uڧc_*񣺖uJ-#{=:LXMݡ!JR0z1Uk_`4V'7bB'43_iZ6awZoqMʃeǚ7Ѣt4wMaa0toZvH% W-u_0F-9[G֕diۣ#> y1tk[aBw%]\SnPWzޚV7xgz߄ x/ Gx0^r|i׭*[\-Mw%VbMگ=c;XƘcYB1Dp\˞ht3YKcQvjn񆕮 Dߚt3?Wphohxۮa5.8zO?Eu_kZ_NRd䢆^$U FJ+\U" Ssw0F9s F0ƞԴ7e*IF}uO)p˜܈^ޣ(ܒu7qq:NYf]hN,ip=kX{9m55TrW$E݈7N+:1zC"B%uYsZw{۲O#sj02O~fu[; }lK|i̍- Y!mJ\f\.\EH0U:e` @^eft&&NYhNqjI\oN[x^pC͢L++v@YqBi9Fާ VI!/W[6]oHՕjMoDKCxZR0{JHد!-JݖY(؊ pši5݉1g9< q@Q-5qU}eMcJh'WǪM5j삺>A>ݼLSἦ^X\crXtS͏Z ouw$Ɣ.h޽QJ6.:Qm CHp Y`ް캬Oo13902*m&,I}cU,S^%!k("׾tR $ɭ]]Qw?\~ya!mcQϫ?!K{:/5su0T_T*e r=} VS.BK 6׳42OƬʝpK4JÙ._۫c]jt'͔}^{/nǺRǗԜ&ύ{b_jl'xVwiŚےujVy5QV A(R3Jy(#,(cuP[t9gפVK{kH6q|lrHMVp b+UK}uQBVVwXfSZE%@?-_8xCڑ#AĞq2[jFϠYSxے 7ֳm՘nk-޳"u'C 5CR% ֜hho_FxK}} ^ wqU!0me'1Kp>5u!nᰶYiPMۧY-bSFY'sfq/EzsIfyVbpGfF.W2E uLdWEoX&e׬X8iY:mŬ?04 TaRaLэ( щiHK\nT9iϱ&ZĐ|HeHoY4slzZs*k^Տ2iʸ=T좰q2_ҥCW?v;jnżf~̰}=.qkNk:mM]6_]kG-k>4+DvSM&(ٸ/5fH^H&Cܴ?7۱&'C턖e+Rtt۩05͈}7c^fy22f?runo OiVG]iJG&wMtlǺaF~Z}0n*Ԕ~fɬ S Xw:d\GR+2M*tՔr4]Qw0TaZnJnRU+l+˵1\\;_x뮗YnA+ǚ[4}klhB Hu&6/X, Zuu#eш![E旮be rs:rFc}%gIfeyD\8SN&e1ʬYLz˲0 n|lM5+?TjBuM&Mc])Ts= 2!=9%fu|jŰ^aq@M:Wb(+$`SZYS:;֤>6ao$Uj/6M[=ԘZ2hi *q/m2$+VroBH|oL+_i{>:ª̴V%n;iY+[Iu.I~W"pZ n¸DU^C,7c輼R r]m:7cqzYj%FkKW[f3)5h:t4 ż!}QxM1)k?2-e+؎!]AܖIV謍GW- i|<ܩh4[RR7S1Bj톚_s[5Rgut_z)j8=<(3ܔE5/v>j͏nۯв.2pB˜N CS)Qn!$G-f/tPgh}SR==Mw+J! W[C?_K87[kGi ֑fE7K`)ƨܘL#h@EvcuL)EjNܼZ<^\JG¸+jY=~niDuii-JthEY7KMP%ɠ uBϨ{^gأc\i[*W^,ɤ=弩(pL&Xэ LE9=[6KncZ֤=}e[8paBgAE&VXw^tޒY\tP%\L( -qG^*?0')M^zWS%z*voQT+1?l똋 &nLn<SDo9Gx(ĜSZRt@۾X[EFvgªĹT] o[ .^LʻLȅjNZpܚ#Z:iGkRcnrY"Q:k5}U\RI5g ڥ!%-[>p+[\\TrYưezf0\T?JcѤX蝕o)~?B(UUK;[9{SfJm sV^oVX+g$t"ZXqܔy#ѸEU51E A暚Q_z%d{PʞI\Z$wT5TcfN WU),׾H7V<)wjr6+6$]{щ gݓ%,/͘*f5p#UrSn %>ea#=]~0i{huID*u]EM+^9xUiX ?_R*&TUTa))+G:c}ے 4V*xMM(l?ƁԬ 8莄WS] BTDd #Z[>Dz]C-xUk%1n_tds9(Ѿ/5T'5#Iaqru'0U e ܼ̌0}!0{}0\ʅ@scX05vlܼLJlI|fgbz%dD괕>ĺOf!gm\Rj$,6R*EF5,jR=-轟@-\cZ.ht{ܐ7>x1 ~k.ٻṙTdLL]-_ CE;Tz4XZXée3]ՁY :(ԕՔeSdpB ߈$ -~ /XȫX a:bL/!c%W6P]LΚYUU\d blU}NY_u{=Tώ a_츖3V7Օʲ.Ƣz2o+!asRfzjD+a؏)ajJow֪\jAxN˻ yI]x/]]eu_D^CSጕ ȱTFBFIݔQ]]';j[5b?8[6ܴ"}u>ؔC}hK\UM iYעoL("FUy! W[evOj\O&]W,Z/uw, 8< jc&DeT KCڒ2g/hH,i%M4S-MW5+. y;wڑ+f߫٭ ?L]-գ42ʚ$: " EF9ѓRE}r)DeE;ɝ!%0$kZvIw?]^5L:yXg-z `2~G-51ozIY n8#Չ)fƸ1Q=欲I9W-3&cԪEo"տ{ h2P ?NpƪdfƄ/DfbYaV'W_xcnL 8 B3U8XkN՚[kidHxQXW#[2ҎmYW̠jPIٲԒuE~m{Eb$xF>HUx<1k].x$Ue[@6RIV|otG))Q>YۏRe IDATOrYzվ  pif=35R|G@rV8Z1=ri.Mb_j_r?޼YYIEt> @.73~U=G6UdC;AG/|ͰExد5.uV?{um$*4, %,`eBR,9Ko>/Hݧ)hM&?%>7sU6=#d%Nb8loxz g]ΰOJZExLYGNB;YrԳY:` Y8CӴfp2mA"p1P Xy/yQÔ7')UnRS t SJ<ϬeR8G+T&5:3{) ϗ-ezs~Xt!7L0t|qgw$~t"D'aMc( RV#|2nIU:ZvF8CB&HZU*0-*Ly8mXF:1~$g->&Ebl`yZL6z6'2uDJ2Г<0f?goh6; w x@br [*\р>qqʺr89D zbbkViqFKdbޥgύ ;BsTƨ@~Uye'"֊ݶ qu7)i[48K=\<$~C }m_i-Ii~Y.b`fRlT3u0~Oo 8EEUa:Ӥ<6'/÷ff7zcv103~{SۧuޮPR >o8{<"ISz wWK.P$ ,q hs <4@T_'Mܤl_*dXkiglg"-ݦG!WnX4xǛ8Hs(cHԏ_`#4&VWEЌ)ؤG#Uq ,)_R4!eb/8s@20[+f-Vh_Q8XxN/B,;䶐yishUױ&R%1U`8+I[rB'̣~ hRAC623WQ%ֲ ڹ4ZQyL;A ߎ|c>ԙ!TXxĜ:a2"!uZ|B=%]7u5^~+9DdM\]Ò|J?9qg d27-ͬtm8|B)ncP$u짍s\ uh %GM- ,], {z#IxH9wxDYť=ަ~T|w/t@B;a蔾ې\uc buUlbl6sR! c`?:Vy .EfIB4@wI w7;\ɪz*BX*F2t-H;B[퉞xqQ+4xHMuIhᲫ)⒲J$P)C*XWQyygd "U K, I'p::ԾnA,o㱋˾PTIڦu& `Y&"mbq7C_'kRsYM<\]x؜:i#!ʬ5d\Z)TmNI'ywr*fC=hޤ4ZKa]2}@ 9H,6 >5\xM|Ə@DPdiew|LsI=ǥBJ7﹡yD~1yڜ>Ĉ1Nt{aZRmcZ T@&[<,0k9lMć)em.!ғ(!2ݢKu1. ֍OaXWqa|"Q!l&;bʶvS9I: <wDvtMlN*2i+ IjK HvMrjL6 lfa,N,1WsWl-,E$e큕BԘ&l/KБH ι!ƁB?f'֚ItaN|S˻%b. ghs_U%|]DzTlkGO5}S6I5a3$#rt8Pt5\b,C_E# K@z6hzL7] 1WpaRO!.w(RmQSԱiiIyǎf%a#g|He2,>硲Fi"R'[b6-CvE]k{na IK<^ ̎QfOgTUIQkbs"&Mg1^1oPa6,DȦ#ǘ(M.bekc7%hK ӄ4t<~4iFhRF٠xMIۤܠecKҭDVKr|Ls<ƣN:MVkQ!RbڬJs!6;x#b=[a+ V10ӏI+)3GyZL(a$:^&>i2N ebv03fZ0;V|NO#1^#R`b&SFTDcL$NwM\RB5Uqy7XSW5])Q1.)7qށQ[&䍌@ -f \=Rgk+[5Yzᒰ@XKG eג$j0tâϙ&6 䔜ǖޠ# 8Y(af@j ]ASޫ>;P#-HĢ5׵M|Vyi\K<6U4@L>F驢b(~ 25l&ی?Lo8?<0M 6-)1G)i7+q6Dp u?'Ψƙ{b(̤HСc}>m@4D GCƬuN&QՋ!xC=SicGU35T~ J,e*DQ a6/i ye $}6AG4aaӀ4#|cH3i%/wR5A0u Kߝ58M`B7׈>ҔC)iu!`8uD2uvxڃM_bju )0@lfmUY&8H{J:8?$b.VZC@tL 1$PIG5+𕴰*)}\ =N@ED&ENSzO eE&O}]WPіwޟSDz(2)ڼ\&MKXsc>|?4? =Ⱥܧ&רsy Hd ؾf\WB|!9Y9<s-㳍SY: Rֽc !ɑ>ӈa Â0IUe=LjlRrSGz]kIUE@R{7&LO?(R.1 ̪90&n4hY"aZ{Ca +ʴ{odaMq qKZR"v!;8ldǐM8ҠW&bg̡*ԤrNi$M8q*#1^bʀu.{akGԆpU)EeHP}F&V&iV"!i*}TvrBQ4o1GD2< /i0C-nsGyBPcUep>2Ϛi s-HUvslR8}WG=>>`x?-I C5tR@Iw9%m,&B k!e XyUCiiR)w)mul." uDG"mOuD[yvFIW9q6'-Drǘq& m#\5y: wT#%E5l5 *$|G'kJ 9b D?>.>#xL2I‰c̩DN sJp&{%р(A@'X?<̂gxH#Mj6JT̫qa{Ju[#q@HpRH̥c.] &s$4Bqq"f$}տz^{ )S0EBvi#l^-ZDL_&,!#V[lȔƖrM2aQaZ" ,e cXl^RJǘ_qb?|M]c.3ѽO¯)OȠM"{>k:Vh;` ҄;<1d ;8RyyT RlQ\e#~(Q"NHu 16P;#UC?D>72E)21H4HR3VhsvnB:.,X{!iMZ"Q=]/Dh 0j2qijQP8$ i0=a;Dչ|gjbbq66)(dVribYGfHY&27B\2f d<+ 終P8ٿu,N?L6 8lߊ-&.$1WhpG$68aFSP Hcꤸlpg` =g Z#,!GvWRյ(~Ϡx(D<1\e1Ee{F`ҏ?OEZ>\~VH4SJ|B9jb!Ru% 3 Du: n }'x<$ ErZ3-l|MsK>W9EIZU l*aD26G4m;DҕsDYLJ=m53r5D{oo wpj׆~WhfE "R!oCGa@M ?Q k*F@H XBA6Be-%m.W~i@o (p}gӞA=|>ȿwRe-̪i? BRb?ibOiTzR4xp(D0<߿.'jf2 yAWk(r!Ou!$@ō⋼ZzwůTeƁ[D0Ù3Ug+K.-*7qW"s˴qxDXH3SB-0%1xn@g7ȄjT}f#a.fH_+J7 }J)crձy&uEE} <5C= fHF#YM{7y=1r%0sEJ!~Ku|[!+΢<VlT,喔=>=(uܥ9rŖΈ"R$[ؔij?x#geO2l)"깣o} 4(pGz1FC"M$dg\f_}B$c!G4X!D+vyܢ@IghBȯt%.iLױʐ1B&,ulW,:sf nvHH x6z9% o*جQ L  /<0tȝ e"vY?(̔ƾon0Γڙ]P80n=NPE[˰Ṱ2G[=lƻꍺoMIL%|K68lv#jW=p6oSgY%e2=D|Y,y1c5KUnfy<&`ImecX%䱎:) 9?Q!1/ (f;8C4'h9Ւ5ܹ7#D ĈEMsI$Q R8TIllCk4q!wU֬{.pJEP%>x=Q0YM1ֱơM NP+o6 _ y@;Di'`9dԝ1FXՃRF_fX|mKdgĆ8I'wjOvR:ފţ=ķn0k,t<@N9G|w.)Q lb/qp8qCrh9N2M""2wdc,[+Ӣ4>)u,&ħAcJo֍ i`q&8]fxidua5_!,ۇԳ՞4xBCM 8ȣ=R%=0ox03BNwph]w)$u1U HSzom; ݨKQ"Ɇ 1?UO+`H_?T("S?=X)YM]}@u\$O* xPRc%\~9HnQ1tv{a6pG'6f% 9TPDMhҡ0j 5F߿ȡxj\qo<)[)JP{?RZ Yz5o¢4BFc H/Ll:Vج\|k513p9LP֮fנK%fME=, BY?-$S*m&L'zlh+ V>M뚡 \jĢl$衡Y*Q Mܦ/IisҲ5I>ǂ_&وHD͢GUML%6%}I|M)iBTLMR>5,|pWdџWQG4syDs=Cg,RWD 5ʥH_S %B6p$,M6P!fX&bV - R0̌6}Dnmز}2lCS|IPir6M22KZ-l 3@X" @ؐ I){IN=SHYUIJ**Fih/(:bYlzmh۬ bvpIq!h$^*, gÔ4I!Fcd{%.FQF w)4t](cLB]ҌV*Kʼ ph zD@Iyh(9CdzzJ6={7Ba3 "ЦEBViQ>ƘXk ,6x 0&Ș \-iQGТDBwX!dDVQ5㓑hZ΄..7uInfB|Tͣ^ ڝq:::DD3˦u)b-̃(RtO.uޠN;r}6p 3a8KK,1fo(phR&eM%S[75 Ufa7Eq%\i-z7&=- ^iS3QXLϓ=h/:ELr@L)?A'C<"`=۬TpUܢy`HxODM$@; O 6iphm,X3X+=B)h궰iy" ) `N^\j?"Mo>x#,G _o5 1atC& gx4X :00E5jC9tFoL`3T7ӯ)hH_R*.,{*x J@N&t Di6E˙X ,6q2@=HY~ K;SȻRcy?S֫! 5?6^)-2XH5lNe"Bl^js:}\U2e*漖٧P+: 8luY@QhkiHYZLk4H91A1e]9n6)X@e#Ԋ$Yv\Ru6t4F!59٫>!Pg0C6K6tX<$蚁_&)Hun\x< gieGFC|D\rd$6qL+W>)'Nwv1)UTrI5[Ԉ iy`Eg:,W 6Ꙏ~ yTpq }EWg+m" !%^b':3KOfJnm%lii^J#-KÈ׵LW $bS!$!>SYGG?m EksI/Vl H'f78,5 D^; FK˺:J{dԽ=mTxE+Dtl [[8+r]\{]3X^ HS׹Sz oRRG/bCKr@VG78s4S ."Jo>;,i#6ƫn Xf 6۸Y;Ëxɢa#Y?m+:2J&Naeu^CZ/bW F lr^M ԱOSP䯙dSG1$Wnn]\.%,ՇPj M9EX}.kZfJK C="G@Bϋh|(K(T9E[d~87j@QcKVƠ*EXZ}tR`4AHGʘdcj"Q mas'k m_)-p|#9G+s72Qa뺜%GrEs/tg0I?d/(eVMI׷}N _wp|NS)_odĩ0bE8iŷV>_Q$B2 sΘ!f6; Gy s 'zaʻe-}೧xgXnS>leYn`S&*ufM  "NQlHE:lRn62bOGBJ16 K]hަvdbtz-yչYb~>83'=6jc-h`uԌ>DD2eaS~pIT?+&ax&F }tzk\fxa3DD:7aD؅W _2G; jXV\4Gy)2KHX{"ގa7ifԓ)[B.7t('-CjP 0fU,,an^ $x$JoTJ{DX؜"< 6@3fP3bzaO'pq GS)5 AfFkLe&Oao&ƐZ,2ACׄQ3mgyQ.b)t)bMU2<.IZ 1bUĻܛ;x<ӾzwTvq!K Zbx~53*)#-QQRQ:66sC$3qbg]S!ZAޙ7i ۗ `!6"^"BF U8R#-j# s*xҭgmP&&!~wpyq{xjXHJKhS"4&_cX@3qHK&6otAo(+&`*4ZM..))pAz]f5=Jp2\QsÂTW}uXֱ+FH}os)H $!MD$ޠx;ϔ5hsi_{h3+JIc/r@azKàMg<@=uC @l:'p1.Y9;A66w(fFK\6XIDohu}#p"5]&SSM|Vi[ISc84Pj)7nj'q^L>%bt&xS.ϥOprCfkLgKBZn>w(R偮ptؤ2I#|giiTqEJ %iD[ >6M]8q"(1S#W< D+JgLф[Je;xm Ybe RDz GH[ט1䱋2!{GG|Iܜ•bkDXl Exܡf& RA2Cl6qyώz;@kcc镙y*E/>*>[+2)Of1~Պ%Rvpq2.50S,45Y,`՛nP7un{w` } $TA+v1>,!5gXH{8I3vȨIo@ŽXY X_-h? m%m!}f9Zd?,: 1sq$[L )o0~ Bl_3A9Q#l {D{G-Dּ9EOa0WoK)3ѩM1Vf!I'h=ؤ<֣Fsr)u+3Ɵ jTnf\7$!Rr>KQq >1Rc`\R>/Ѷ.uj:u,ib}ҽV󫢥kkWɦyjn-t;&H/~JGGҀ VLsn~^ߗz%X ^?:~AMe Rh)`YgZB]z3y" *Mvp(.1,aXLk1&"Hk$7$Ҡnz~H=ǰ{fR`H>BJs]g(Ⱛ߻6ި)vfm-=2k j  3.Jkھ붭Bf&( (o|@jE'7jN !iښ 1RL0ٚqyǩqt4G>4b]CņY ZFozvp'{X@)Yq%2Qu$HW:50NEyܤT`! IDATOµ#Z2D|@[d0#Kw3Mu9W A-N"  }w\Q:x(L`=Ր"l}5TZ$k!b\:dz b{eA06ycz 5FOMlަO exB@Px@Jpaݎ\?`9}o#JYr12@&!(vW=;yOq-Ĉs|u7 /rFKlü֑؛4[xϾÍS w5\#Oo;gCG6'ur+4Y(@HM vcRm/LL4S,-d] b@R"Bo(H=wNYTsN^Hb_s{`(xOxL&1DLq4Ӣ/4fUWd2MmfVj*Y8! Ro .0I QW™J$#`O bY-úADF ]"b[(9tZH-uׇx ]+M/a3y!dtjG&eYqyMl֕^ˌtt o7{L~gL DJT*{:g&bfz}s8b1q]VWIU%j $@pDng~_LL(z#9>ghʉ}Aq 's^H$hp7 e!S¼FiXP,2aK 3b D*Ai:Qip]NgEqJ*g29ΡpSTxPDdTq~S\p :={65?LvQ#{CBNV@^K%jLpq+2Wy2t UzCa6'b0MR`m tEp_[2lԔV]\Vp{*1 }1R) kr OT9&HF@Lp-"m.@5: ԉc?!3K,1-<6I <Adsb"jB1 DW;G:2=4bFɉ1gsEm扙"aPoeݛ؞9:6#̐5 9Zg)*v8Sy[G W4vDG<~\-h9JfRd4"DĚam\"2xthLxT3eI :X^i!sB/-u[XmhlJ61?0%I`^,q1R.5zŕ_g IMQ/i\&>PIUFeR}7Q9LAD+<GR=UH9A( -Fř` =FH$}ʣ`lfC]t_9@ 晰}r;[ǣOS s^MmU.kd##$(谤c }[e8q9ϕr!m--ʼn@WH(qq^tOQ15+з,*͡ ̫*SvO[&52bP\ﲟΩlTTFS?ЊGF8< ⼎ @#ȉ;b5r&Y&,La9FCOD)I ͐KC~\?E5Y\qsLh9^V`2k[=I|WR1Ԍh$G_H"%"r6z&]ʐx1 R#3F5X!y7kFj@&C &]U^۸ʊ1.K2]tI!U8c#zrs1TzFŬS|@'|9>ƚBJEsh4{OwC%k%aoxA nj|]nQaF27j"p3O,g@UNun1ΛG> x-1[Y8E]ېRrY;5iuX#9#?rV pe%"iO(R'b  3e5X! \xeK nQ!!+:үhr5^Ԕ!"Dzg } q0opL+BHvwtC>25"2S9Gl dn(p,"KtO5$JHFQ㤜gTȈt L*5s${K3Ϩ1INO`Q)Tlos$8!Wip6  .0ЍږAl̮#)`xoő.F";kSc_AtT/q.81, њ4JJ}g{Ler2vqHp{E&RyfS̐q7cT'j;r1#:ELC3GjWh@*׈ ol: 8%8K];uleX!&y RJz;En^r5=l* x4xF q/L,]S}2ަYZ<{rȊ@%"v- }vKٶFH} (eBJG8{ؑΑ`/#{r[&7ʯ fPFl\oL URRdxbػd.":+@O5vʮPi\BK+Q\_g)6kX1-im 3Cp9#l&- uli:Mbާ 1+,fF钉YCX/UPlt[TRR#g'-H }_tȹCD6"qǯA_̾>u=*d'H6L9hԲΐyT8U!۴qCБ~h?91^q:'pz0#V{WtJ|6 hj++j\>98Gf_֖)"Pʢb0 k\눾On{YV5p_P+ĴJ\t6isTps3B|ڳ$)F3tqLb%iPHIğ3}L0#CU!%'dx1˵!m9ËfQٍLec,3*q}H4o*c$/]"`p&dPa#($< KNځtzo],2fGC2>T)QaFi>whF6m2fm"VK:sB{-g-Rr*9K#e6ȑf8/+1p`>P?x[9ąEaЮvͣK]6빡ntys2c[1GNpTbOfUSds1SAL 2?gzحX0^@-͒pG{zRҽL2ҏKXK61*#ܣ)9#5őT&#% @D|֞QfX23#M\vm A沧Ց~aFn0)ߧNdŎKU7C|h̥1M! g`-tlYᕾO!d\r `KEkE{blV:ef@Y,|7c )!o1Bg~V3UXps "'aE>*cFNp㈨cK|FUVD%r_HDD<2Jެ8Oh+DU;y!*<I#r$:'i2b=AaTOM›@&7GUsg3t0I#8]fvp42kF=hk5ccv}43Vdu`mDh O(6vx_ +GicڰX/^Jkd,aAG:|ur Mu#ϴpq+s\ԑ3a@#Y]WSd|ls *KxhxG["if/_^53#>rm*n_:huE7{ }e,whi!F|$6| NQV w&yy:\4ZGSh)_iTy\ijT dZ MM xF<cp.ݡ2rn'zJDU}";÷ǪFVAX|)m1U@xMq6H鍙̔8Tw_4 510ja>r̈́rD0{o=Ed~12b)t-Q8̘aR1[TXѴRj;1YfO`³@=_̨i k@`~ϑ`cAo @Zhсd m8QZf-3re9sZZM>7qm=< *N I!#U Q # CCIxaF(zo jf.6fIIx@#琑: ~y8A'sO8Tť7RF^DWT㱃˲[LĜsPh~f6.'}H ._/=vLU/-%$뽬iFd.**̋. O: >/0j7*:&rhl𘐏%" oUp!!_)=r=T m㤶ps7+x<# cRsK̊~N ;YXjaK |VO KOs ~vPc&.'t9wKtm}d$1ScG;F.P}QIJuͭ~/ЦNRtmm» '&ə&ᚾm&PX|DmFB8ΑS>ҮIdRE/pYsI~'jʰžWѾV'Ϣ27Ojt(DYp(f 9Mǒ4k'.I7D{Ϫ,GLs| !(䱍ˊ!ݞ q:R!aKkvدh,G:*)p GEɆ9ǿQ U/0 %pe6E̲^#ctCnr7G2 }|<<qxc4?=^v IDATgy:̒ ?g%V )˄& Zo\-@j!9).Wi0Yp"$#Kt" 8 iPԴQvb滣]fg w?[>w-rH=GZ!ees7 #цNڠT%\ᷣ* Li"JhKD6A -]+۫M̰Dh,h{="+$ dԲNB]c7chwЬ:>_iFր,gHWZ13/:ؙ^esclߣyExve4r-ATIvxT4 Uue"JWD>p eKyDq0E+~i&fPm O%#&%") .?K,=&Bkx=x(DX*9o촩&ⱅk>]zsqQx^4`%~C:]Ii4] ُ!Sڿ|@h!@#6H(@]!D̃bii}vE#ZKYhk_V zF!?fhdM~7  FH#aUD.7a@2pM xZ/kV,uf(8kMVdj9G$oǑa|"sQcD/.Z8|Aղ4eڎj/ANp:'c0O{@+B-A$6M[ZpH: :R+E!hEk) (} zӀHf?Bc|@SKXS$!gR{J5(t\ ctb3:|K|D'aA@:.5փS|q>FLV٠{dXj=X D#![ij/E RD!5cc6;K~皁V9CHe¡)W|k8R8E6 I+}缣,fOn#"sz 2*(dF!̉8lh$-fx%m^%c "F;D'lk/˦, q>9i)Sj&22$C qaaBꮡ'FKVfĶ l)EZ@i[]Ajl;8_!Øs3cO7}&URDE`ᤘaMĻM{G=@)-\22&uwVa̠U~H߄@гn:@3!"~H+gzhZ:OCdo*9 ֽt4i&.~.e]xngu`tth!H%L:A(fH2qZ2(agHVK&Be`֦QA@>$<~,=|EI!bҢ v9^< Dk 6s ! /5b_($FB;e':]X3 /on3B1b.*<% b)$ء <:x&]%3}rU ~OV!mtZzͷR"Qn\/)(w;[/-64,|H`in#u!hçb[!!!kϽTMnp7{H :8EYG:p!Ѩ[>q6w𐰧W<>+L.DRQ ()BfZzS]h-+jd/pexՙrjdCC`#V̤͘L,õ@i[1>fq̽kѐ&4Z7 w;n># ș&=vJ6qYb촉B9SWI@BN8IUt-MӞ/H4y׆#DF=6Fɒ$Xꐌb"rk%-7@g3J` (' =ٯZVda@\<-:,oh192AOo`#v4ƤҌ`08 M4K*|L{TţF-y^? h_p3Y6OrH Y;;Tu/XW !B8.H9m~  Dd +j=M'(hÌ4pޘ&Vr>y!< d˚\~lS8,P2'},m+ o, <-fc[ p&fΞs7im%UR#S53(Q1R~nx`kK3\ *6/Sc15NJDf{ 9N):=uip |]Va<0k,HZZ2f3:L!2hY˯2xlѿTi H\"WXSjZ9Ș"85JFѩ2u32Úi#Ōe8m鿁džu"yѴWW*ML'@irh87K2 4M?X-s>$,-v {Re^'WC\n+}p STɹHƘKI:Tzm =Μ~@C~QOۀ/40Tm\Y6C 'y>n"D+BW޷& i"`2BfT˿ Rg $^׵,j&ܲQxpsM$vHlB0Qpy˄P,a$tpCG|fdfrSV nSabFuƪ:T&ܫzLbIQGgxĵ. Kn6<"aL`GF^[6J.K[(_W~6bCGk "hU-lJM-V LF[`򋺼,G Bpi˱B>Jf O#\IYsswqYgKK!`l"ؼef(Ѧa}ЎYUT%s:$,0ky Xp Q.Xd/hhJ{mzfN=hOFv k$SZVeOյhV* ꩒rep"vNս',gZV)K*8Rbsmi 9Uʼ:tIc)WB9GpvMpx@3 fE > İݢaEdN2`SUU^/֏Lf:C\Pqn&{n@#i< ,s^IAJU2R-m-)U6"7p!2mJtf Pɀ& Ζ&8|,B9&y~܎ 6m~eB'Jk 0E $!T\dhWNrL_CvP !; 4U1CCPGDzvp{E~ 赘ujO-.dIo#//#=M6?Η eL<{h.. u8Qtd%Rm\bQZHZy:C En 8\M>Q)=$ *~rzM4 #Ţ~krS4d|D \Rg,:Ml{yZ2^mHh0Cqg댝lE g̲S|o#Vv@z5dD&=hmP!嗌\(0߯*8R_RWeyu]]?pޞ~AUW|}:=/E$|'$1;#N1$bgf=בޠSgH?rbfm\6ޞ-g.QgSpUZ='FYR4bYFjCrHyd\aeB8p"ºE8y~ӛ4xf.]ev_d vyb:8xZ/'8-! YFz rJN+ȡJ $ȼ?hr Wy WOCW~i=݆f9NeEI=4oRls \ thox&=5k1{[GItFۈz9s rIAIb"rK҈~Uh}<#gr v{Z9:Pg`}NM<)=r{ gcH'Izō9QHirW'%[yϒr+栊CpSEbj 'Tc~UݿI$CN4޵}8ׁp~(fNr:RcGp-KWP%z?F $6R?M)ֲoo!ima,鷱 {}6HDzhC屢9H9rji1r8IYη@,C2^Oc`F>AH99EB.O1 _w i#ԫu\Jd MK.=~gK.N_s)$'λ4x/JFG K5\G};<&g:Îa. ˥$/;:3֢brmb?$\ jOarR-uTI0NʇTIqxoѢKws|$yF5nk_ ffF3d"-H%#4q-ggir]'ۚ*mrK/̕n1E!iiou3Y$}tW1 k8)IlаlZ57i lbE!YHq,AOg3Ѧ3*\}ߌA_5}{؂2ޢ<NZ<$.x4T}wf9ÏVbd>۵t2; MmD特FIZs%]|JUY\uL»oˑM?IW\FْdLof͔3LjFmrR{2Af냲cjyJ'4r1HRR8pH`v-l#fY+C⓫c0D|O-j f8 WkXGDAYNrʨ=ˆ*;Lpt}e#j6 wc$<$6QO?hM\\!mᱎ"hjk92jsylrK?e04JI:|F"1u\٩FzI #142Ϭmմ߃vtJt?6Oq - Q}s+ jcX. r'їoG1'ϼ8L2J7i"m6 ,uDlIR&`P>P7uX_yڅ!iKFV':ЊD˻xɽH`? Rv*f3?3$\)eW42RzcQM6-+/zB`U5 G2}s!bXZZ \+hfZme4&͈7p>Kit3]uMʼf20IY8)gXlgdCZ&PpL[4vwõlQ7Eh q 6h#$cK8Ĥ! iqz0]#oǑCRN~q"?]١,&Ոпo$VtPT]f۝BɁT| Zdm*F Y:y"Xl 8G?caV rI1p!!$HH̗Ú^B.[C(BuWGV>4 !$-pk`63RWх|Vf;ȉ {̗S=5:9g-lEbhѠ6ء IDAT#'<󊽊HbxSk3טש}|kcvXͧT3F 嚙L|vx>5?rbEB6Oo>9ԹԨeW.3'!fĜԡ6pԉ3Twqӛ(]5G0[[Y1}ijoj@138C@BVn#d:sFe5'D"&mD#2 (Sfzn ^&)B +%mjf^+`Xъ^\#S饖[ @)=Ro͠ ؀Pϋ,%8Cc:n#`M6=2B[ ]@E&Bޢ+A2ZfA]0lⳅ"=PTڽ/gy7kh+I8V=&nPcFMej0pJ) g[s6$CʒqY0:H{':-vP$)-(@@Vۯ0NԩtAcR`?%C{4hi0hВe[Axwb?:*.$# 'Y'A I=lq;!5\R LgeLp= YZ -* 焢$r&l S?"uE黄9Dy |!E1 N@`2.Ȟ|H#DhzP#Y5ҎiUF5\Fixܤ="V #Sʚs'xaJ%JЖLe|:kqb5A wJ{ fX}A\=2C|y6YeyF^k4(82m.hKƲղU-#1 b.DA"*ÃS>V]iVɚ[@cۓx2wǡ\E E R HL 祖R0M ]Rf"2O2}#e kfOsgY:<"DJl]q*g调z]۸p033 -c`svz߆]#"#sO=.?cUBE: %"69Ir>K&D$e]G2cREwW9A #.rTúV!y:xotS2>ZWv6$f.blj!+:X0]1E `1dXb{h&%2!*M&wQ#eUֵgF%*m]G8+ j/)\dVPm)zĩ]nR+u.rXTə"䜧}BzO3fP̒C[8S Qg1W[ Ko+*$!*/ljy'h*B(5F xv.Rm=K$|/hA:1.L!d x#e'ܥbeѬB"G\e YaKU~Y+\H/s%eH3&S hBDQgM2\38HMZnHH_2$ $RڿJhYg[(5\-WS="Rbe#CD0B#=>'洎Y L#BB<@vQ"2rr[yi2CLXfINybnM (D2?>w;нB&./c,glqO6Xr\~EqC;F <9'a/pS6ޠN<$ZO74p¤ Wi2G\+Pg75 y7fU<ӧ]*o߮#lq"_$un35v\&Ix" SՔ˴iw{+252aMqŧCc$u^.i3Aݙ=diy -DX$w^]nkn&HB]\v).,m2>ʧ*eM<~a툜yb%Q mHQ1inaTޓGȆR < SeTW,sWhr|d] t)B`u; _8}\oD<զ ٥4-^&^EΑ2\ bmJ@Hhd4BbU#Ц\ lT梮4`"LeAfȲD,cs"C~ BQ?tVgIemo&GVOus܋)~OtdTizKUNs4A 8A K-_6K{d\zb`yL1M)ỏkPA4B0 M|1Mq]$-R6pq!"ٺe.-q ww9t1C͏dhY),Qa M0) ;{wAʷhk_;*l2NRb"Z:W8DĕC{ >) k,3AKz( EUkDQ,yZ;M \aTDOd<1@obFيȰX' ن[KMl"x|DqMjzK <YFƄc1aHJws6Ǵ2I!Bf bQbeY"b^Y#&bVX(#dM!턵{e1> 8D _%cD~'H6 ;JE+w82, Xcϖk FlR텛Fg/pM[q<2%ޣ D|?ŋW>ǽ}feYs,ޡQfܥZyۤ=+.>i|rIYA /6,@ g}uȤuh_oؤ\YXppJO1MD4ǍFc@}Hɀlri1ui9)qJ]6i ½5C%+l$FJ͋U~J/KBfp0B_ū+7M'ha!i >2jVhK3 1?sjY5w |KEEdgd e &ze.CDR^Mi͈5+0HOnj[y)0%/zm,L;g1f4$,m.%RrKO;zH PrT'qI&%XC-lPb +yra [IS,f=A{Xd&c q=u&ଦe[: =˜QEfm»4vJ'YzO tCDu*wh9]'zCbƩ@KZ{ 9R*To{&isDԻXx7MR+/Tii%v]Q:FʮwJ ۴WPUrZ\T.8EH?йMwZM~~@zkThcD fut$P6M-Z_ U#;g+ು~K`աHD&t>81z{B脕gM"s1CeB kTL!cRiR]H<$o~݌%WT ʤA7M;e U\{X!~|KbokYqI>$48۵aL1oQfkRcV7Nb Juۣ+p wu)boGEJw S%y\<|w)O6rTQ q#HOh-m7N9ڴpJ׳<躵 ,3i]ǦZ$/o4p <"f 0>_ґ܉D rQ/Nh`#uteqa0'F1ϴ\ #`0oH@ I{la5|&5WEۺ:)!Vօ} CckC_ji@ in &3f2*&u Pj Sj| ~@>6Yߩ: N IDAT~@4Ͱqd=g!mopm4?~EG(SS*|Qp,1k=amk0eu\.eAIwK>ic鵑kk})k"T[XXnN$2zMHLmK۸L _Pag{Gظ|E{Yߣpw:N+$t "w^}8M6F32ό*9;FCD[n֗Gsu 5}, LM~Ap_ 0OLM\\MԔcl}2AT|c?Nۅ`LjXT1 9!y,h)o}cs ƗsXd\8vK!!buKqƷ aܛ"L]S<ֵqGAc*Lrilcʬ赱J I˸dlVa.7p mNlq >)U.Բ@*2OWv85pXUJ#8gȆh%MbŜkYaaN qxhJ-,nR"A;$'fI'Zr:O+Ҷpԓy9qi(?VŗwAw25G9KG '|M#d <?3E\Q7<1%Rqhe,,qa@iђ.? N#&ຎ5l5)]A5rLzpSφ?-D mAT wR~H"b;;?~YʰX?$ WD%p "6 %hA[KQu)rdul^l"˸vI{وvGz!R`(w!3\ s̳הyF_aD# F͜zvڔZ)Y!k^{ۛLmqn8nI'XUH}9Uy/G^/&Ms;Sd3NklkBbKҙwڮJSbc#'dW;Cs-:s-fƴwM0퍘 ~g#D#s kU=|{h x>^BK}aqyOn10leRBPҕ|Gy|C\ᔘ{}{1eKɃ< Y%ewFc,IpU֭-O'{a7p)kEh3켤%GG" D{2!֫ Y6em>w~mD5ƙ2.'w^X ܔݽp{O2Rwd--ȥWlJ9Bdt8Kaݤ: uqB| `B|I$jP$eceEExd9 Jf-f L[º7W 2M9VU q3mquJ,a8 ܢde_Qq!^'%%Ҝ4ꘖMgįJ}k웢RyBBX= &H0?ɀd2x7wfQ4pž'"r .VGNNocAhrw)1M)=rr*21b xHu }k-lJdzA^|ӢB&;h}"Ok7pm&Y b( ul~.*Ӻ@  >Cmx=s gʗ+e'+H9B>'v%ҔI>Y\gmF3=jFm&LXzB5f`* I6v_F |waౄ4?gz1_inVi41+&vN,)V]̛4p6' ]M\}= H[Ƙ&b GɏYpĄ'!?M){ 8e< za'[HOh*vh#NN2W[*|i.2)@meSC*X 8JiFhI8Ibѻs6$߾_ߑTK0q?d]|nR"FiVv6l2Я)fC6Ri7MXgR̀~YxP ![ `90')\ҎAvF.3҆zb E^%훽 gLDraB.˽Bn5:%RXTQÌRdPʉlj5l2F-5ZuűC~Ŀ# Y '7:v#ޡ ;4tJ.FNL%9F25]wqU$ :>$ϒeXֈ਻4q\РYDhdicd'B+K2)p1Z8<ӱNC\NѦ6ML5ŪvuM~^jm Tp¦B:ðNp*Ud?-%FPhf}_gǮ5ByW>`w\{W"BoUF1 &Y2JSS -ynRRQn6扔ؔ/9^#.yJ,RB?o^1- W;JT87g\^S/Odn7 cIV M"ey-H>;ܤDbOxKJ!¾ap߿ ƍ0Ac^n!#]a3NBYfהykSO+kSAnQⱞVMB7E|Ou쪀oWMiyMѷ00K!NPes]̌X@Q$h^ǮvB9U֑?ymSn'A)7(qzߜw<&`?MC{CSwT1'M!3ps{q0)VJ5 grֶ4[vY-^ˌRm,᱁o&*C f}`.OzLbr/?_O^D谁MTqTpmN8yQ& YbQAu#a +enAJs턉&[S۔V$yGoekkEf#v6 b1IN5eޤaz<2qiP;'jψX/.Ңk,U4ֲ&nW9DYH eLHQY;:zE_ߑp@) a*PaEٌHF2 I̛]LdC21RVߑ߾ l\ o366&܃dt&]\mRM ejѱ2*_^6+a,M4̣Ԟ݋`PͿEƥUlEb^PP.q@z8LhpC$LR)4I8E9'"C&a >igvHehh!ͼ}1>J9-d>p+)~KZ8oc=1|^EDZ5,hJגڻ^Uܚn/A4g"sHB{Cv)c:6w #MqG@@z@'(H7ppɸL8ʻǠe7LvO=JX4rFaR! aH6O引n! }X$"):cP֋Vͯ iϻ3fiB5tk9 EYхFTP^ gT'x@Q"N\|;W9(4a 3L0rQ&hu)T%X4Sԟ4DA׌Rsls;2j<r6irTFKeϸo'5s)gĂ GUuggtd5$e=sMp g->AIB{Q~d/.i15px9܇uzD/|٦D!JR/52 =]Cc2۸<M>Ư ȃ!}_IJ88+f<+Qi*d8ZMDcw }wi!^B) zPi̐k5CUsۭƇ Jw6cj\73w 8BEZ;ƅb.xFI:RU&6kڼYpyNk SV*T{r)O#Èޗ,k:Y^9Bξ64<cW>~7 fKBf@Ӛ=$!(p o4FOK|fT HձM@ Q?U%˜}9| -:ĴyɁOrpr y10Quiq4K4b| -\ޤQ$5꜎̏@ Ҧ q:[=̆\mdZj*۔+zb#~nno ._˰@eںw?L%2~DuK c[8|#-MjZQ-,yDi>S "y>TBJ$LƣSDjѾRp(pIu[!}###GT㷔z(QT 3 Vk&xO! y1 Z#v@o<IIlKE [Dܥ?(O7h=ZDِm9ӺZ)i_0F#Y*QdW,>PEz4j*D%=-5d9qxDG 9|O <]_)V 3NH9<<$ںM֍3֡K[KGxHycg]VG E|'njTe &'ISYM 2b} D$\W?QSnEd:ڰue:mszU: Y bNA F+* Sl^UBn+i(bMK">:!: q9uP3 NE6r)̨KinQV%Ol. DifCrc0i^+,"A|iM5g T#gtK.ԃ:4"Q(홯mՃH2$Lr>/7lPnxĴ{an Malӆ!Ƹ8yecAg^u-:Z.{lj}H<FY15madZ3"'_CEE9\J,?58Kkt5Nr))?]euĘ~kx??W IDATҥmܹ>) 9L*z *HsdQ,%<>!CJ]%[IbtG1 8ОȘ<3UcFL$B5#&cSOtS^EoR,6ų IyR`6k|#O0^zkL@h"1CVdܲ M\:QCs批CE3OWf ȋ̼q2 _ c/ bv"=/\eL08k{r$]vO`f#!cPάma2qp(I%8mVXSBh;y =@ JH,976Q鉲\X ϲ18_I X PqKS2xd/xgbF 0\g 8D!. +z+*I Ljwsb:QVzT+m}O3/j#: Dpd X1J4j7} ԢBu|>) 7Nҋ5"nё47drÌqb*00,!/Q`0eAGt]g#@%"Nbdc}ifL r->uǗl)Ӻ^&N8UZTJb$8AfAw#Li~# "cvw]+c 3ZDXpIMe9q % {i[H A00a)f[sn[WT\sy mc1o$xecfc4lsUP_~G6v~Wń3Q/"b/cCGT\#S+ܛ2"?'@l"jX^+⶚w?y߃[ ek|%#j>!Ul|RnQ.Ӥ8"Vv@[,ő cW)i{ %">#cġΛCO6t5w smy]2xXdlkȈN nhP5(Dz̽?i#-E hp8?&싯8tqir Vba[~]Jŗ)IFR߶ wTp3KoL6֋;r^E~GzF߯/zGXPq[1w zҏ5F3 'FĚM YM,(HEkRԐ'Y@齕9<) 1!"_~1lg<_S'kC$ ġiq&bXIY<#Q^@y4S\Dی}5lx@/<8Gl"ϡqI0U;2VN^3!3#L:!aםƧdQ;;HRu1I3''yBdֹ8c԰#.>iױWW{@PcgGP^-U7L: Oe^L+/-}価eCObV&pKAau\$gFq<"b>_6?fAK3f>3)cĬiѱ8oБ\c4)JxH牙W9@c6H.+dDrTF^]5E2q<cBvK\7#,eʚ8C#>,B|6W{Ec|LP&MAc $%2$s,+cYBQE¯eX,iNGA* #SB!btz7Ԧ4%?=d sĜa2[9NB_n_ EL{5"15UrvXܤS-2}މN)6.ɲrfVw+\B8-}MWYדIe6 yB6qTLq4 p |6pi EB3g<-輩H76 \,B9wX'`Mnq6>kxؤ@]R9*[F$1pS2)'hӤ>;A*7b&&0AGWz/Mx y/s&34 -j,QH6rJ z6A2ZobƽHA'b6D!'5҅t,rHFgOF,BB݁]W7%49cQBt3,ew $XASh@| Z xDX"Czt8>D~'Ͷ>S-,Fȟ/>"fK$w : p& Mr*ȨX[zѰ 1ȃ(-^xt ECDa;c6'}݁/dL"])s,fic d٠m5 `ʃ*r {=-Ub"NCFbj;ҐI&(OqBʉq :"!h6Ja~7MBQrѳ͌M 4u/ g ;QtQq0⬐r&S]'> Y]H+oArqwlcj9rJ?MMpUaH9Lu|{X"cTŷ;bq2K-rE|J|&a2~K0ҐF޻JIE0Gȳ.&cl3o!㦞븎%<Ե$"3)JK!z6d@5f_s4 {k&8BU"egH*U81NǶq2i,mŚ M;H4Y ϶B vx ,VIz6c ~@N8O\T(Dٯ3Oa2)3- ]BNI%*5wTi 0CFza1K6 6V ٓeו;sʩ恅BU VSmG4CC[?;3!+#$Hmb5f Y9g~Xk{7 躰Dfܳko}"k2{5Wr7u̦xH1bv4\EUÐH}e/*{`J)lQP9kYӏ)ٻS"ixk5^αkL0éc_bB"vp)aa{Z ܀"֔ٽI YȌD|&KbDdz:qVnAaqFl:2U!BfbƴHFU .79uWyz&glbTV(Ԝ^:dߦTe>[!#esTDžO6HHGM7HSkUxoγeײh#,.*C,>ӅmVnq"N. >#$sr{1et R?2%ڜ&U{-b.s\Y05nᰈHePz'r}E [qbt,Ң4\bR;< `)H#O>†LSG,Jk; > ]f.> edOc\g'&/1 Oiu>꧉"Η|}v7.3@TdWPz^@&!oaG#|FEL"p?'hHt 21B0*1՞A~HH(_n䆿I%C1|3XySWeS6圊nf?O@GU| _4|3u~CD.vZ0lx#iil<ߔ+*ga_qځ+ <jh}hM7EsV:I= Ԉ$B,63N#U`ח;A3i IS+tJB_#)u>ŢeGF):`)RG<&G7hR"!빊φ"Z bfD #cUEd&ʮ EUf Ma!rFއAlsY"oР@]5ᮨq,k.5yVݮ1/K4Vi=DJ )2M² ?w(m|m\"-b->( e}:G1w"ev=: Mf %#(/:t o¢: ^F;(UShc2tbpbAW(p""Q0A'`N^-l XKT8bdrz i.R&3#0HY#~ELH$2ujl᲌5D7#E=Mf&Czmc8HuJS jc4m&4 |q"۝X"f)N56Jw;*@ә2hum$*coe,N1#s(#N'߻s=-' 50eJyڬe}&]`sN3#U]6l&x'!/` O)"&o~>CĻ+$X)m6hKf 80 =RIx"Ui1*3 ֹX=R(w8N8IILwY" ?fUyf$n\)ښ&\ =푲SۚQʁrB7a}] 1CtF4EBV'%)eza8ieC5CyZաf/t::BԄqYifFƽRN$8"_7.f̉XN侫X[foR&T}S!]~Jyb4iNKm&;}*ܠA;+#+g 2JSvp;&@Lvm}=e,8B67 My~^q C>+N?%"Jrh!s;ߌs& x 2L:_vGADj !J Pnaq4-cYQ. X\*SfCg8955Jmf7kr8K#J8AHHt.cZ)*?c@fGad)'[6O𨐰5X{ jd+m,lQTбLj@7T~FWSR!v෉Y&LNmE%p': )N6s$iFw&F8$Lp&f)sĴdd81[xD Wid" 5D A6 fT,DԸdX%f^{>aᐰ2&2A HqZ6##*<)MlVUjlO4~ XF@nd'ӌͮ j FfK vq)p=D(hm1D*b X}6GAA~qk/-L[=zX_s?_y@:?$]s.m#rjw3=;2$ (te>)3\s|*G!z7#(!$_yLjHib!V6Onl7픝})N?-SC *~uU6i@X&c\*̽BL ; O&(߄2f6ⱷpJ$`#mr!Mt%l$=,B(0G} \eE6^avϵafLƐ-:ߵ07ExAff-}`&UK'FTn~ +Nscl+fYuIxB(#/rJf̜4 <2 kTvğKG-,p6Od(EBT{ÏHyZ۹@mU X7Hׁ|?M9s"&q(QBvp%h| Q6^2RWbn4fn-:erc%5 j_*38MffdY65Na1-WTJ 0Y/ &z2ĖiGfsf;4ALW4,Wl!}QpA902 “<{ q d"2@0̡:8=H<.ިqF22>eF4 F#u5Z4&!1 PPC酙jc ,59ZEb!<,ʚim#f:w6xU !Y؋u- < ZH4=?9M#e*)UvK-:dbQ%'8MR 믚 kjLeK~#1$ aI9I|Fu,5sxDBVT?)%fx:2è㰮)"fH/)f}AO{At֠(iOĬ%q"M'u_}QaR28`t-m\̼U%BTI+, }oh4/xXRO84K #-rDq!ɡ7 P%]2YyB e=95_!V C!aL^_R0 . R2tCaSEʔH"4P%E(Δ ,rQ;*n)[,gbNƦ'.%'YrTetJ!Wi2Nw#2T0$y PQ ay69L%̿Ī*Fclk&Z!D #ңQ"D1*M 3`<^-d3}w/g`2Hp$gViD .)H5.Vo^^?o +%4Ax!b=:v}Wh5@2V~Bvh{ᡏ#4ߣ~$[6J4!( w: BH0'@RM4RBM%tDAjkf2hZ>ǣHg SD cB_Ke5npJ ga'|LFv-Vop;SD ߦ3悩9WHl:mަF k,\gRu*KZ#~x1" kV&VV;,#D{LUpM\RޤT'(b tlSΙ枦i|NxG: BlDϔ}R441A27if#8M04$\ HS~zhi6)z mh`yx]m$)2dہsi\C4!>)㜧?эshqhRM' szn2ˈAUcX3;@4H3x$%VY@B4CQ3Y~H>}{=nSd~b0 6+UB&h7?fsڲ*fbTHP'!yɚ,eYq ^QoS!udl0z>)q d~I]ĬٰCOA7Մg*=Nkh+PrLSYSy_5\>+_y.DѷS]=niqRB<+fE 3ALa@0s=i]Ȇkg۵O*kpOn1Lt//WJ(ke5bq(dS"l4S>H^M4x63ϱ <@ /ҢHNv(6+ i]lngW(i%e!& Z%&@FjPdT5{)";AA<}d77rH #^1JySl* "eAԌYCLFJ5Dh1Cu8e c{oc4A)E Q3[$DLSA:)bsV:}b@ٺi^+t2Kt~,iϼF4ʂ eH8O48G $>8u{aEFB#$4^9GXeUelUs:w(KޜG;Bbi觰id EvqYcgXTcl)y{5eVUto/ =|BM\>6nkX< f\W^LKF3Y#%Kq:!1%"Ȃ(,Vll( #c$٠ /6R ལkxԱu!sO)#={Y:(P׾1WnTy \<2HÔ'uaY}&ۺlు:>X@ bfHZ~Da౪1'h$YEjA=V2 w)"*1Ƚ-*D0kK!:QanwM]^ #@ʯ(oFt]^iW?CgI!Œd;YN'.==`F+7Q`_^gTs b<0XQ66`I}"fqtvzo3,k][5O4?R2}foY[{+eTY2m.? ^yI Ne$?PSJ& ӤuGrG00[:Cj̜H*w(ee{=a|DEyL}O95z! ~JAgcE-şoκ Ki6c,wu x<z?5n$SkύVwg_2 A0<"U5vI)=^d M&jXɦ󈂺=ΛWU:dD{1 l1]q. &3lC_7᪞WO5C)"ebsf 3KӦ8D\ɢqyϕdžD? lpq6D_ W'&`cgcR({DiIȹ%"fY;rIY7)4aDEG͆˹`˿gZ|&0EN5rk6JYšNJO9)z,mevξ,_3GAEє5{YGctʏUU,N0CyS<'ڔ=\9D4'f0FikFnsK=ʬo;+9iER‚$-9H)l:%E)s Dm SV&b"j z9 r_3F3bEi@-ϙ:.fY{3YВi&lNl)ܠByNGG1] "wLH9=Wre~/I@aAS=TE!ʄF3|iIuNgl0fAC6K)PǣH8mV2G=&1b.bi?fq?hhlzlFBJYӟ5}D ҹyҴe @j*5{XD\ɜG/>hP$natQՁ EL6.o6M 4! +$-fFwEzWA 52 IDATA{8Wi*ψRВb%/s?':ͬoJuSc5 "BDLJ [B?fU@ XENp&)O *tK0.]vQ$0 "aHARX mP:8"Y5I3T{>>0LJHDlQR6?p*ט!f[0w4ѷi!e1wvROec0A7Zd} \<νs="k!3xq5h63JvaBEJzFHI7-\>1ula+ Av*]vMAicYCRS0oJ^eAXӑW@\;!zX,)1MQ(Huqvօ2y8ocM\, W0\#)-|CtG7bXi*ϳH/A-gQS8 Pc1K(ym^բ\?Np5<YO|-E a}@nmiߩE,> ?_\V#٢Er^4@9 {Lt5ˑ#y?RSV(,5Ɋas@H/Cq6'-5~5366KZ6=5l6pEv"(M~%Ovd=RNd)%DX\vI /)]"t 6hg<*1 8gGj9ǔhb Ό!5eջIGY_7@6'8zʊ :KcnYGʷ_LH 2 }ihK>#LpuV^;qHroP@X)~ 6uv[!u?R[ Ƅ]U3{ |'xVU $Lgbrs3&|7FlaXet@C|ƈC*F׻M65fJ+O8ʺ|.);8+H`Թlt;12̩#˹1z'[B>6]nhǙ+`7z팀tx\$9͉>ύD0dWqgo E\EaTY. C7|핕I >GCj|I{xL_㡟oT!j-h#у9-.u~w ҫk#RLFŠ2>& (~ jf^EZzݗ$Ε/30x|:ktޤ'n`yx*j ''y9;LjGWz?$XcJj gC `|ϱFH! q ު%\x .5$CZYFiByU&M>1 u1A0%KDaI} ,b42tmg#2_jgEll}/hSEB zSiu-dƜlcSKgԜs'-,FG)I|4)SI* m&H!;]V3¢*&1vV{T'r"$&e'sL1XOp9uƲh٤K B<?:Kğͮ{˻$3<>>sIx&sOEU5 u߳t[K_ՃӇxLe&)`D LmVv J(-$&@S%f,Z4 #>YQBr?YR ch=qgA)u"'b Xc >_Rd+<Ӌ5\(m-|@ aRrSSx&gNs\p0߭ ?}hJGүU WU[萏zHmbqBV-NA HǏ5x҅oP?tn(' )APlL@5}XU GC))>rpq=eHtq )6_Sf!WFw04>cxVce^b=θ[y v!>tS׌oX$tIفf yoqI 7R-4jqJkYB:$cefF-_LDn%{q9IK:"*91CJIӇx|~>'s96>H|S},PMFsGx|:1 蓺]:, X$`73#fww w"3 qCɗ^/u!ex=,!qBU!#&4cL1uFA3 Uۋ!YA٭$YEVuQ$пǥtS?*]?=FY7ØaI>_m\;:v3.2AVe6t4(~GL"ipp_z40+h2a ي sRqױK{;Z2qX  "s2xڡ4Ҙؔy4uz٥/qXDYl[P(1y:("+9Il(|"Sw4ӕRUk#vN6+Xŧ \bWl (? "/x:cLnJI6?Lu,]3։㔎 \b4]x`Zio!u_& `E=AԬme/ܧ@S!ɻ$ĸt$Uo@ BB~g4y7)BjA'a9  &ƨTsd \>ljY"8p$Ғe9Zؙcq0 2-,ʔ&:&ZpY` B<=~X3llF~`ڍ2>yŔa Sm]R);Yoi;WF3 F,Be6cls ghSL#]|Urc}/CnlҰH JF2uTd]B,N`(M[@;s x*56W=FҥQ5B0"^2y~L1Z3!6)aR,X97b]}3i) >'UIr5k9S&0d{=$}-uӾ~ΏD[z4I!khgqH\}O:dG (3[O"n+Q:,@ȏCXT "H|<},)SqNW/o q+X<U:܋E.atYgv9*Rd~RQ@Q@( )ׁ ꌱ&e-RF-Qpt 5L1hfX( PsUŊm,Y|f&|zfoC̨y~aOW_ e6y|30ZkBˤ?S)20I2IuZ"/ X/66}mnQ"ei|lޢ1`DoVnΚ"`p px*mKV`F7s mHbN!2N) {ߥM$J̒b]D@lF 81oH`,UeI^,Ed":n^ bos\{HyIJg43ZfCG 3qy457H)㤔)R*Xl1mJ8tg9MJ"1%[t"T) M Xl('!tCoL"!%6X,F~/y=`aøm0ѣ#ȯ:W#kީ;-]$ڝb^6I*Zc*O(p=~m+ʔWe]8* In}r""tl44Qi6[E|ZQߠG&bq!VpKyޠEp iR.ECx;Q5*"ogFNo)Q"z-/1u[$VoAY}BJP0N(" Af˛.#,ѵJ w.qyY) Ho?BJ!-bF:-BʔE* ӄ8:flŠ= Uow0dBcp ݑ)4ZHA:1m \,w$e't`s2=Ff;uˏVG$S;w.qb%$V Ɲj k zY?@zDh0?3|LiB C4!ӄl5e*ԙ$uQD4<.p[֞A 5Z\ ]bz)m! xCR`ilH0FC=2SN9%]&{|N|mlcVMcB=UT. Z^/68DlRg3sJHFm\L-mZ"(!=;&_>IF3k)Z;m1.uY&{˯Yba;!#Uth g*RtuSIuF,RxsTMk3buHln݊WeFTK-I IDATڏ"bn/0ipV;OrQ{AkI)BXXÛtx6Yp%G(eBw8EcuH/mjcr gv)j@S|~A5 iBgZ.) F{ %+7!X,6ƾ̦͉ 1'e*1Ȏ46u_ՠș&ؠ,]MiR_O?ֵږo{VAqLs0g -< *`L樚dpFK*>B4%v 49BC10cyx uUez@uxm 1Xo{w\6A k`#۔(0ب|TS uHiR#|">T ?RᡵťT։ht;y$R,NatR>E=iSb[?gEI,~ğ`Wa3jh )N3t.o7p_EZdMT@BBi"~K;j_v44E-Nqj+1)[*,᳁E;oɒHmZ>'H-\6 V4q ig {3I<i?ƧQΧpreVM20b<HN3\ l nx6g )eaA͆pt+R YR6.TfcH*hE$@rVOh9eUH"-\jP%'`nX)8YPK2W5IBm+ &Mn7 9S>.L~ǜJq& }1=}]Q1:p9@,tF}CE Yrjs033L_p:Ak 4 6's؏(RRLore$TYZ ]Z\L7rS\cg;`'<xwqN\rf̘&}u+1Ӫadt9UܡJ Sqx<1cC.SM!pjnXY0`|ExlkKthQJcY%TƅQNpňkiޑ4I(k\ OC׊pq 98  r]$=  g~I5|h*WyXΙX1^e8o-%:.P=O˻#UօHK)͏0 nn$8M}j!䡺(\LmX}͇4 m9!FE9˷E~g  ~*S%/o9)lwtI jtEcM|d+2lx&1E8ݷJ&Yǽ?*pxu\N &k$T (Q*5*AiBˢ"^ӬߋCpf|G9M Ip) |&V>;Ө)>!ϕܥs|]#ݯ  .[+RYz'Sq|q4EDLD5*C%c(=\XI`yEF4a!BRjkP&mcyL#laSԓmVvH "V531"*-hv_&9cQЙ}Iq%[LQ&! ENQ6wZVr3x=wT&}2!9-. ZٙQf#%YcJM`_#0$Lg' WiDv3BJHq9K,㱌8uD)="gL![w|IPoi]Jg{@;;K颥|;HJLf/u pcl:q 12,f3He&X="g0ģA*^2E@le*d9əP 2!R:.cJ븼J9љ:.EgԦ{F80[mHol. ϘlA]|@,1 rlӤ0A M8qtf8e29j5:6%}S^+w\$5M(*WuE>m ,.uT;cS*g 7N஑=V?CSMA&{`Of0EęC9:<xWZ; igH'o!Cg֙1#bz)_Q'0%#Lx%(L8z5N2-c7c͚gl/ z=kTѪ$ib>/]9p SkN?:LsfiD ZJvuUн;cĜ$J[3!$A\UưfL)䦁'g lఄ Q6_SGʯ{v b~*H?aMoSALyC sx.fgd=҃ԱldBF[ ,87\Kn1C X7'a0UxX:s9wB-?]F D>wgG_@YB`es1+^FVph'%/]e:0R9:w%N2}>"MShx7sTƆ0A_O)6{KܣD sVnX'ccż68|G C{qQgZo!Xf9Z~! e l확QJ&c,TKx*0 m,6M(QK2J=I7ynť\7E"(jdqnG WR3Al@+CKd\,b>F;[k(0*>)/) 4-\p= ~Z'Lk)7eT(1 BMUQ86Pa (%Xe .~.A^6"m>:\#PrB4#Hg ) txܯRs5pn(ռ82DX|v]/e~Iu_V"gx&zؖRBawȿuM 9E!3*3a8s_IxϺn`&"~oO_h!)}'G,ᰊ".y@!04% THƣ0 =<b-됷8 xܡ#|XeKOŸv:M=RaA>xP%<HUo~)qK bC; jT$~e.?*fi!XN,*0`kihHs(k$fY/H+oџkn_E=;ml6p2(6}m(?A-U %DZm1Bx_>i#m+EMS*qd})pRs.!⫣Hx5X)AkܡW=La_KQgAÐi4fDdDHIX|K$"ylI557i5N3Ϙπ?ao"Jߋ}!$x8T#AFRMCݟ瘿Y a 35c%\~֎;yi">I_V#[9+%Jx{R,!e(`Xܦ I8G@W@5 ee>HF^M'T^^ mlnQe.sar=ՠ$T?~).EV2Q'#cb$Xe/ sGmTGP lVpuː63sr;ӋuV߽0GS< ?N]WG4,d;< =%}M<=w% 0 13c3S!b X$U:S6CK[{Ο)n mPET=+,ýHre" xL+`@^=Ձ 8ܧȶ.~ii- IC5l[O W[;xLI-5Z8IiaQ@ZO0Kʾn)YRY,.w5,r I6Yp)|C ߏQ]u#X0Skdq}dBQ\)w|wyf,Z'ʝm]^Q`o\blM$iCߍ)" y%8oidZ+- Ĉs W`e;ʺG7NLhdoL;*ɬ50AxGolb2T &1:6wdtHXbZ"nN ftg[\0\H d}c@xuXvR{5tcCKAL*?c4^3ĴY׊HMGC6q @ mdUq{^;DB`3IHE\7 Z`-ʜ#॒"roJ%wakP'b:;`zī`739ъ^ xt}DT_+HfelSJQ'1aB #$Oeg |D;z+tB6/qYc]ꆖG_ +ZzR3c*P&RF,fy:6i5|4s% ͳY"-Z"S7 >"T) u=?ؖY (_r! $k, ke_suՑ<_hMMs0&.)K8\f8kjPCj-⟩r6WrB ft7hgyP4r5{D6n| _Qa[˴& <_i"戲q*Tk69E$IF]ZtyWw~">|$߲ul:Hq 2_LLe",D/$ڢ0ϖ3HtZ "7[ޏy<0kS-BI9A5ٰTaS=Kʘ5OQ2+)p(!tG4v Lv iAq;~x> 5gCBF%.{$5uߣ Q%.㽕mbʫ=SrI4915eF >= UǶf#HHS :Bd3=ql=z ͛<ߥ9„CȂxXĬLBNCV8IEYB#eR[gb"p΂`<<1"3.,88`>):|]=hS'M:Y)`liѸXjN2K<$ J2]ǟ9v ] !1lR !eJKAlVtlبGʦΝH?ٮGd0psto) ^0<6q)p2<5 'h7/ {3i~Y14iZyvQ;[j/xr2e]}\`!B1txN+3d5%-1OSx,S sr2@F99E o]6*tYyn q>3\WZ^*ӽQu2_vAGfo/J(\פu2^RĘu5D^Xکb`#X / i'_mbr =Ac ~2+DL!A僨,d'ZN:X <3:&C}ùXL*OXSMyiQ7Clj8C0ݒ8؈@D}@mKy`}R5%"m:z2 P18/QE +S'ؕ\免 q"\,4-)uH;45c,) ޑ3_70%-' *)Y(̤A01P^9T8'D<l+(Esj0$U2QxnؼjNPy#/qRv -llʺ z2#]Zt$dWɮjM]3q\zt]bDKYݵi?/}P$\֔'WE̡+"2Me5O npG!c~ AX ~v1b 2 ǖ@1#-S/rH:d.3Mh5>^P%R(d넯y:Q ;|7sD)lD /dg#-BX1WxNKeuXv ю' UDzP6,O˥r;Y-Chw/׌1M1E7 xJ4;i5m|XTFiyRb){ ]JHKo8I:0i8gKkY& qY >wߛ]ҔLPF]-"+N=f-2*Q$Vl6_Pc;N1 J>z@+y@YgzP3gKQ|5rf e-AXƊz ϰci*sc#Xӗ&fZj, n1-vV"F R#YA)hqAjʛȄmDC+E>4ٽ֜K:4ʝvMJ|D'Y%(pqx*iXf-6[C(IBb:VG?>%h3Mv᣹,Q"y^є6RnY0T`?h =J.E& )q)WߩBL"R F{L+lj:^gS?Lr1MkD|$>eD ^jODv!?7Hf'&㷴avmsLq)Ё ͔_ )\ IDAT!/bmGY% .ʷ<%xnv `[#[Zza "'9*}1ӕŜ"b鰍|R.!Kxܢ)ܿIƉ)0?elR;][$$$Eb2 `} ѲQ1K BuT"kI1,_J}J»D=:`Eh <3٘&fKtxğ*@[օc :T'&XC>׌QնpA:[M6PzB5I 8mޡ".!NFT3ϋRx䍺ҌiiUij9J/6aRKgtY,^౦$N఑CnR==kūJتz9M73iP.i0%HJ@J*]жՠIcA Eʻ.MR4)+o yT&/@ڱOiˊ6k+RN~C)a=çs`f𨈴'KOTєbދzI ]9DS>M@at((ѫ87|%ڧ^f$2n%,!Z\R{mt_b-EtU c<5}Ak-<"y45UzV a[tRKS$zyiܤOh; SDԸ;>舋ôs.gbqz7FBֲ;M)J2D2 e?K%l a2p MSj{e\X&pmR^u=oO𹣉 xO3BŠIy\>"l֕TtUI50P/0t8O#|jr:sp >SP^w/nC{M?Ca(Sii"R>l㲪"d\'7)2Zued\yqn6ͬc@ xis: \1[ښe ѱˆPQ@$RXzOh#mо*HX)>[͛;:I3G7}~9ct垛;tl,bI={V*')(!k{C,q+_K:\$&%1F߹˜twS9M `%<2}nqH} ,#aʸH6vCeY1r,pwҕ1 6o|p|ihvؤta֥NJ)Xg?  yZm}7y6nb݇+KM- EG9)g0M_Q c,IȌӬC|)E,lruj1r9DG)rXH&%<͗FZ*p9M Gìb,.6t}ur{9AHEL:=F' }%.)0+ ]U 9aC&}qVc*$yHLdCY$`0ǴyAS{bO)Q[d1Z5~e"NURNcLe_7iK{F3vzdoӦ < e%V2)SSN#%ݠBXCuÙ]L§4Hz4p9G/]\s|%NYوa*==\&2x%b8l.7v 6&2lMnX i |^3I۴R>)#%2 (pkܦvƈ9Ocq% iks")g ~JFX(H7h}lr2c4?;SJzOmV!<,EcdphdŦ Uh |Sӡ%\6ph!~ /rzF?GVcPg&1Op6c^9udgX \ ¾ֽb5Y <䑪ݲDw |nQfKZK<ӹ݉*B&lϊE4avbE||-?of1bt>[y6PHOH .+f0;sY7ꋴy}\Ī ҐRbdBt/X4reD<]wC,ġ= )',VB)NsqplL`Si[< )sk}XL":;! I"! eŦC@6M5\nQ5@ho!uGS|jPnռp@݉zלL+;Lᷕ!Ð`Ebִl҈:SKML@hlȑ51JRxli^$re>AFy"JJ2;+x H)Rd:좃hw~KSHB2o I*22Ȫ]DƦ[AEP̈:bP mtr:nL\>5lS|y:ڏ邒1`4uc0!wG}`@hsMj96.UbDJ  %W8^ wb;Pgo֞4w3Um,Nu1rC%kzQ UmmyK3޵#K+GYYZSBkAB j,2Hʫ"O;x{XyNmlTF 4*3?/>B՛U<Ɖ`8$!/ [-DV_QL6u\b,Vp#&ֵ@TjEUjszҏ؋ X VAMXIpqnb .F`~u\8* =ܨc|A*Boi,ԅttQ~IiL6i4%yC N /pYm;4ycf8`eXXSfqsWmiiBd`fL|l$"@%M4:Hx |L8Mg:{ $$\=}^?VE1L$u>u#@HEM%ɺU}:ӄTHh66x3jR {:Û=O:SH)M!bm.%}쌞ƈy+B6e"bj*\`h vr5H"eIqJ^ Xn$D=2=O yȼ v&r܇*1Mtѭ?}8Bc?\R'gF#h\hy<͕-G_Tx9:$>1V&/$e,k镮"e i"?1f!31b@"~@kPU"Ɏkɗ.{a~UF/H.EP Ҧ9Eif S%<=xY+SK(a!VnSɄJ8Ii>cy"y$ B5gVA=^^h# | .7*|vm#Q_Xҥ 2-"K/tC,eT:8MIY#59) ŧF1K9=J &9 yT4yffneA#WTh0" ZmyԏkYV*"y{JCe&t,=ÔrmR0k8z Ƕs<}ÅT:juJ$$J 91ٗ) qYtN *m$&#$hO&2!k# 6E\#r3)?ҟ 5lɒn< !82.xt*Ѝ.E-4+q&b^ݔX;SA+6qXq&`~DmSsm,wX:nR3j_Cһc@7qv9ٖHx_ce9}^q(`~Am;nY&38-&16L*;; BmJlS؄.8MB?Wc@5|NՑ%.1"3umFSW2"Ȣ)uKuqH ˶a d4eKh}6} z|fT`lDjT6yG+8#0"8{QJ+*v&*)P =W[Uu@w/&Sn m\B**^nu1{=|)I@\e><">wh)]/9O gUIS`-%!WiSR7k뺲w̩F yfs)-nOb?o_?ݲj 1c(Ԝ;sVVfVfP"@n&9^+A;Na/wiQ5:!mZx_XmlIVI8C4B!AHPAȢ~ 4.$7f̡nb e=@^D!+Drԥv}Yr6 /mq~u@ N FW8@;Qtn}ǘ1T(bm쿒̌MJWa4Ǜ$0>K"/$o(Y-gVGYĀ/q@fx% |;bCl4(h`qEB=KT-%Ẉ{PĐ*6ߢ!.هCOM.P? 'P!}fR7 1&18LOЏuI$\ %)u{}?g18HZ D5Q[K0/GQ tv9RZWb`Q6DRUHp`*%e ? Vɸs_2OkSs:Nqq0J> jTB Mn$$x=?eu `ܥ8iy||AIJ'z.w I"i9y^UJXߊ8DߧC)cR"a[ԝD`!e>R6^C BY MKTa|cc {D5BcKp~odV!{\ԱYNRTrn!1iOkQ!"1)"^#JB3m Ǿ0@2I(ļ8AgGLA2a>lt/0(x6(%iC/)sX ^C%u2IXf{z[&W(T U9 n|b\(SN)V1AhhGلHWg\Y6J~Ed (?D3=qStҀz\Ȓ0fVC~fjbI:wȻw 5y@,<@ FˆF"t_z n%u U*Nxf[Ho)-lqN:>jLT|GwI@&$`c?)ܡ@:1 rXd@Mdk"26_S#NVʚB MgI 9_Sd^H -ˢqHvGm,p^?`pw)Mnp5y+8[qɽC9BJ[6&5l"j-1|8N;HC *B/rG A^dܪIB VMJz6IwW%_W>3$XSG)KWJ$;ڲꄦ˛{bso(L]\as+3e\ \lOQM{Iy|Z \sOs"qR*+|CmJ_GoWqtٙ"?9 y 8b%<>=[E+tbfQSM{&𤠝_6CpDg1BN&ClYE,C)>i`Q9@(l,>J9k/o1GxsL#>5*AsR$k\̴=@D3aЛ!kج`:SKVƳ:M|ڮfCJ~ʢ(%*9:-Qa&5.؊K2GF̪m4!!-ixcé@VmXT=5R b:n{]zHC3D\4u<*(Q<#)&?A!BNs۽G#Bb؉$+BC 88r\(M|>05%maR&Gd״T/(Mv 0}iAK\T"s29a4o@bP$bG9ة^$|Vi#tgb^cٷ/R=;{@sAj>>%u_.ML-1'G)Y~>(U1MhJSPG/=Q/)~i G8B9y~m LmvP2ma̗x6/O+3"8Wj(ϩ* >9)|eҊCgҳbU:bCՇRN=. :/%ZLRADopC!5m8B}-ֱE>w(}-ǘQ۸AT4gA}L FYt~7 \ YQ)=~02$_ gqo/:6n=ҋ1 p,af9ו ]š@6M,p /aRÓFi0A 9 j꺆)md9_S b~H7i& NQ@3{ץK A\g 5)YlOyÒ(;;@&ٜh`rCct vIwf-Hz-Du_][pPLIJvKK1>LOͧL'ZaMKrW)[$A |Aª oJɔ8\DV?F޽K wR &JG,S3x6r 68=y)(+x#k!` lZ"WyZ݀g'nyxXܧn;*rVBUc~6+̉ h2`:x@S.1M,#]AW`3.AaIb~HBKulf C~UJb-El9 3PJ 9b@}?@eߋ b J]T]ڢUX|iFҞ-ƾ .4Ť?URL]8:$MVJM,pEQM?ZrF&8ֱ۬ U@s0A !FϾJnN'7*Р4z&VHϴ9@C\M BQsǮ߮ƪ!%UXܤ |b\!(y(%.L .{X %<<}&'(ZeFU%]8$~+>OʓAH5kI(EUdf6l- `K*|,Bd퓇oRl1uv!B`iIF;j!F͡^Ou/c/T4&!&M}{“m}6D`sOVWբг@6&UDᨼv]l+յЖ ְV,ۘThe.{&x@n9:D(Q~jIݭո|]e,piRf=H&CQ&Z"`(ӮDcap`yY"d>l,ظƒyd$F/nF^LnVވAԐDIhNS& Ua*6iAnu3iEAb JHg2TvnbM,K55ez- zC?RlW\EvHڅWPE\3;I=P-L2=k( "ަw2GHefd(cs8cUla!`7im# COxNMZx7%lU)0j{#iYa_ybZg^(w9GgI&EVŅA$Z\_g&J.X)Kly*S0{LW>IGfy|65LBk#u L.@TuU97KM uHX$a-EdM#GM)wHZ$T`t$$(P?SstXᐼ-1Xaݫf9ڢ!py}ɫ<=i2=/BR"/xX|FER“b% vEڋPܓњ^ɽQK W0GJ,Fw@>aB~@%w|E nT!d^}&hq*SRplo! eQ8CW1u9Xj+^ayXLj0!#d1A'4x,@aieJ!5GH6p9ő[c0IKBO9f@ڋ"k4Ye)|PLRZhMtAɠ@:HOƓם2?"6 IxP]E0 7yXe%<=΄JPN8Gtv"Ui*t$ Ǡɛ4Ebޡ)|f 9~|] .Gi@,)E\lu-׫ULpY2_h^gAzNb3E<ӻ6M2A2?r<],I 3O&Gz !G J[ lz 11X5!7|`Ǒ[TsJD"b1KYlbqM=rMTLi}TfX0s!q\8?y*ױяMY fV@e ueaPq0+էkFq ؤ4iHc e`M` suj̬Q4DĖC\ަ1I2W.]E=8c!T߀1מDjy6y |N>+|v 2A0# z s7F)%[utv?(+DHl\"w}6p`,th5e8^*W)Qm^gI dԲul`s"M>բP;2"z5}2|7t82R&JmߔpoexZW@y #- :,Q_FiPجJ٪Y(Ϙ&w8F/(ё~9 #2I+H\4ޤŦ2d|? 8,ԨSk=Ot}L# 3X(Мцi5ױ*CLQHnM?j͎y1)>P %jX뷱P>ȊXX7Kݛ/R6J$ݗiP>i?8W sԩbёޣ/t Yi$2"Sr?f}R1 *aB GhI"3vmRg+SƟ$Ő@jI!._Q* A?'z(F'9GoqqIhc!m߉+u!N"W燻~HP [fC)S" ӛ6 d۴ )J-]$(?TOYWTܘ[f4U$N>&'$xWi{1ޤIWcls)\o1*5JQbͲ~$bHdG89|C&V.:$rUis,;"!i|>>|A@}wpRWt<a'&?ePfU[&D!Mcq?f}lчStX[ ܢ&W+$l Ho'_)0CH HulVdKP+ĝEהOd$T9v,pl3#d!u #I# nkEfC5f ^Bc!HK0=Q!R8Nܕ`{40< W*NP! A8ۺ0RC |AkMw2]Qy 2\eFz] \r(AS:|FQ4"/ొ*6u]MXz Je #'5BL~d,nP&HU3Gl=X$TI ~kNԺ,RPD|:Dlb%gP?5_`;,PdY1q1c2eYi H9BHǖ2F pPss(ΝaoC}63&(û48zC/K:4aF6L}2ϊP'5 cqkx:0i=@UA#Zl=M6d_?6N#zMQ~_Q<GݒuG~7WJS>M: ygx6g^lb%rWCh.^۷]įL?Ni?x&g8Gx/ɝpxLW!Ӎ0_"[Eƈkʑ :Av#3-˼اk)n͕C x6<.:LKyxhK "feU(]+x3aP5fxk&SՕ9C΀a@A˔Y"SWsW!q]]2T #Eb^rh-E d j8 ?hr*X9T!#&`J֘ (p3( FZ -XIBBM_``!kxCOC$]VpKZLУheEHaRH6ձiA bJ549ǶN{x\יlL0ĭ+G)sn#%V%ʴuJ-Cg@ߠ2TUBdEx"PN0@IgR-(0H|~[r?X1SVjS(S@ױ1зWڷk-nIDAT(wLXq1-q[C5q2Wy6/qBU#eacsu,2%HekUZTӬ=ŷm)in6MB6Ї읖=TcŇxpy*CAB^-?;E4hPOF`שzp6hS*3G;{H!VY<>>r|k t~$BSv7&=7eDˆ"`Hr z~ZxiSū a!d(tTߧ7)d cLSȴ4M.iea2M>w \#W>~}s6]U_hoiN9P}ՔQQ,G*?eGg(C )C?!/vAN()-:JX`lʜVGfKU 6hg8"ī$϶Bk9{4!3} L ߣ79X~ #L9 >qZ4bfBOZ&APLMmcK&Zz/Ga~&$& /*jޣ#٤B߼z8V⡯h-*} cy+xҝ!Lω@H9VYP"7GGcw?BCM ѓEgޡ5Te34IL ">` j&\0&#`H]6iJtP U1Y1gG t' lZ(5lnR.Cyx 00\%$>Smjk7G#c6˺H$d`lr9D_%# ;}97lY4`uّi"ZX|N˔ M^w}0yCM6ĭ!a:"8yjJZ% *2|LQX_w1{Xb0hHâ4r6Nt)Lz'Ud}徸œI-n;k,X=)i~rW(q  m1E& 6H'׭y FXģʜǥ@/1JT-elr"?6`kK81S2 =OKHj/IG ޠf2^&"M?$YBl \=\Sw.[w*-([C)!Pں^As7 %eMb1o(Sց:=΢D̟$JMx@fఊ?/GXfws8(Ewֹ:B 26.qi-O3Y̏ǰEz< %#1*ȤC#[Qrp%\~?cy%((2MMBDOip\qqy&,'D|p5N&]M|ҢS[;KGԘфC#: 1U~ V&@V0j5hk˽~c+1=Gk`Q`fMK1?gԥ'˟qiqNDl5P3C# ~o[^7(ru(qdþ5󓡏7Hx tR, 1M'†=%}6W)q"+=A OVJKB &HNίҢBȧLCYʩÔLx#dT,?Q4aT[Da$<JfRuB{lWhsdߦCFSژc= HAv{M &7~mSྰ} WϘ(#_ :#UN=kDS6a$\)5_0Mg y '\-"{}2&M.S,19Hi{_Sn:P\WVZr Ռv$i#r"WD:"6|K*&񅪟KYĐ@d<48yw(.ߧ5DTv/scO&wqFE\:i @L i3C1`/J=+Q9뒴G}y|>7gLr2 _2?bsKwwjDt$fcu:<~"ޣ!2'b 5&)ؼJh1K8T0` | NF!_%ےhE#`e{TYX+8G?> N8{X|M(#>BQ|RU!!Pk6WiC}?Lp)PN1By >>U<i<%?c˔=ƌkIU@W)Q Q(gR 9cŌޗƣE}$FǏQ $ %O*WԊ$w§T392@bA4Sb 1P1$gu5q-lc)Q{dFE$,ri_Ξa7hr䱋7m9cIo#pX0s- 6ҿvg qEkO[%c|3o`X HD3ycH+'c .R1ѭFϞ+3<#3x#'f" mX>qy(wJ6UHHʣ## q" Y c4*c ϩH?Sٻ.Te'2\/3z.I" q%d?! _Wc!#È1uL|,1M Xѥ%acv 훺3d]A3%$uh(1ZɵE2s8w6[J~6Z6I\%ʩJ`0K-a <,?s|N#1LWMLXҁF c6)m39КsW{tU@:P"M4fD6Q /KzQ3jJc;e2M]ㆉۢHd`P :?SrV{vBo[ʬlaV\ͻupsMVtuA3 6ɖ5a~K@&͎ -d3o3f׬{b8@G _f3IdsMKk=G80]LRsL eTey$؆mLnQ 4\[ $heT,yH#` xsٸr<9,pcQ#aX$,sLfLT2?OHAYj:& سlT]_49LX_QooUlD$ᰈcx|$LVz5};gIb O2s41ͧ}9vgc2K"S>cH-3,s8}?uM5hƔY>ǁy.`S*#ō1'⶟$)(VdTDU6;uN0\Kk$|<Kv왳IENDB`wfview-1.2d/rigcommander.cpp000066400000000000000000003651351415164626400162300ustar00rootroot00000000000000#include "rigcommander.h" #include #include "rigidentities.h" #include "logcategories.h" // Copytight 2017-2020 Elliott H. Liggett // This file parses data from the radio and also forms commands to the radio. // The radio physical interface is handled by the commHandler() instance "comm" // // See here for a wonderful CI-V overview: // http://www.plicht.de/ekki/civ/civ-p0a.html // // The IC-7300 "full" manual also contains a command reference. // How to make spectrum display stop using rigctl: // echo "w \0xFE\0xFE\0x94\0xE0\0x27\0x11\0x00\0xFD" | rigctl -m 3073 -r /dev/ttyUSB0 -s 115200 -vvvvv // Note: When sending \x00, must use QByteArray.setRawData() rigCommander::rigCommander() { rigState.mutex = new QMutex(); QMutexLocker locker(rigState.mutex); rigState.filter = 0; rigState.mode = 0; rigState.ptt = 0; rigState.currentVfo = 0; rigState.duplex = dmSplitOff; } rigCommander::~rigCommander() { closeComm(); delete rigState.mutex; } void rigCommander::commSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate, QString vsp) { // construct // TODO: Bring this parameter and the comm port from the UI. // Keep in hex in the UI as is done with other CIV apps. // civAddr = 0x94; // address of the radio. Decimal is 148. civAddr = rigCivAddr; // address of the radio. Decimal is 148. usingNativeLAN = false; // --- setup(); // --- this->rigSerialPort = rigSerialPort; this->rigBaudRate = rigBaudRate; comm = new commHandler(rigSerialPort, rigBaudRate); ptty = new pttyHandler(vsp); // data from the comm port to the program: connect(comm, SIGNAL(haveDataFromPort(QByteArray)), this, SLOT(handleNewData(QByteArray))); // data from the ptty to the rig: connect(ptty, SIGNAL(haveDataFromPort(QByteArray)), comm, SLOT(receiveDataFromUserToRig(QByteArray))); // data from the program to the comm port: connect(this, SIGNAL(dataForComm(QByteArray)), comm, SLOT(receiveDataFromUserToRig(QByteArray))); connect(this, SIGNAL(toggleRTS(bool)), comm, SLOT(setRTS(bool))); // data from the rig to the ptty: connect(comm, SIGNAL(haveDataFromPort(QByteArray)), ptty, SLOT(receiveDataFromRigToPtty(QByteArray))); connect(comm, SIGNAL(haveSerialPortError(QString, QString)), this, SLOT(handleSerialPortError(QString, QString))); connect(ptty, SIGNAL(haveSerialPortError(QString, QString)), this, SLOT(handleSerialPortError(QString, QString))); connect(this, SIGNAL(getMoreDebug()), comm, SLOT(debugThis())); connect(this, SIGNAL(getMoreDebug()), ptty, SLOT(debugThis())); connect(this, SIGNAL(discoveredRigID(rigCapabilities)), ptty, SLOT(receiveFoundRigID(rigCapabilities))); emit commReady(); sendState(); // Send current rig state to rigctld } void rigCommander::commSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp) { // construct // TODO: Bring this parameter and the comm port from the UI. // Keep in hex in the UI as is done with other CIV apps. // civAddr = 0x94; // address of the radio. Decimal is 148. civAddr = rigCivAddr; // address of the radio. Decimal is 148. usingNativeLAN = true; // --- setup(); // --- if (udp == Q_NULLPTR) { udp = new udpHandler(prefs,rxSetup,txSetup); udpHandlerThread = new QThread(this); udp->moveToThread(udpHandlerThread); connect(this, SIGNAL(initUdpHandler()), udp, SLOT(init())); connect(udpHandlerThread, SIGNAL(finished()), udp, SLOT(deleteLater())); udpHandlerThread->start(); emit initUdpHandler(); //this->rigSerialPort = rigSerialPort; //this->rigBaudRate = rigBaudRate; ptty = new pttyHandler(vsp); // Data from UDP to the program connect(udp, SIGNAL(haveDataFromPort(QByteArray)), this, SLOT(handleNewData(QByteArray))); // data from the rig to the ptty: connect(udp, SIGNAL(haveDataFromPort(QByteArray)), ptty, SLOT(receiveDataFromRigToPtty(QByteArray))); // Audio from UDP connect(udp, SIGNAL(haveAudioData(audioPacket)), this, SLOT(receiveAudioData(audioPacket))); // data from the program to the rig: connect(this, SIGNAL(dataForComm(QByteArray)), udp, SLOT(receiveDataFromUserToRig(QByteArray))); // data from the ptty to the rig: connect(ptty, SIGNAL(haveDataFromPort(QByteArray)), udp, SLOT(receiveDataFromUserToRig(QByteArray))); connect(this, SIGNAL(haveChangeLatency(quint16)), udp, SLOT(changeLatency(quint16))); connect(this, SIGNAL(haveSetVolume(unsigned char)), udp, SLOT(setVolume(unsigned char))); connect(udp, SIGNAL(haveBaudRate(quint32)), this, SLOT(receiveBaudRate(quint32))); // Connect for errors/alerts connect(udp, SIGNAL(haveNetworkError(QString, QString)), this, SLOT(handleSerialPortError(QString, QString))); connect(udp, SIGNAL(haveNetworkStatus(QString)), this, SLOT(handleStatusUpdate(QString))); connect(ptty, SIGNAL(haveSerialPortError(QString, QString)), this, SLOT(handleSerialPortError(QString, QString))); connect(this, SIGNAL(getMoreDebug()), ptty, SLOT(debugThis())); connect(this, SIGNAL(discoveredRigID(rigCapabilities)), ptty, SLOT(receiveFoundRigID(rigCapabilities))); emit haveAfGain(rxSetup.localAFgain); } // data from the comm port to the program: emit commReady(); sendState(); // Send current rig state to rigctld pttAllowed = true; // This is for developing, set to false for "safe" debugging. Set to true for deployment. } void rigCommander::closeComm() { qDebug(logRig()) << "Closing rig comms"; if (comm != Q_NULLPTR) { delete comm; } comm = Q_NULLPTR; if (udpHandlerThread != Q_NULLPTR) { udpHandlerThread->quit(); udpHandlerThread->wait(); } udp = Q_NULLPTR; if (ptty != Q_NULLPTR) { delete ptty; } ptty = Q_NULLPTR; } void rigCommander::setup() { // common elements between the two constructors go here: setCIVAddr(civAddr); spectSeqMax = 0; // this is now set after rig ID determined payloadSuffix = QByteArray("\xFD"); lookingForRig = false; foundRig = false; oldScopeMode = spectModeUnknown; pttAllowed = true; // This is for developing, set to false for "safe" debugging. Set to true for deployment. } void rigCommander::process() { // new thread enters here. Do nothing but do check for errors. if(comm!=Q_NULLPTR && comm->serialError) { emit haveSerialPortError(rigSerialPort, QString("Error from commhandler. Check serial port.")); } } void rigCommander::handleSerialPortError(const QString port, const QString errorText) { qInfo(logRig()) << "Error using port " << port << " message: " << errorText; emit haveSerialPortError(port, errorText); } void rigCommander::handleStatusUpdate(const QString text) { emit haveStatusUpdate(text); } bool rigCommander::usingLAN() { return usingNativeLAN; } void rigCommander::receiveBaudRate(quint32 baudrate) { emit haveBaudRate(baudrate); } void rigCommander::findRigs() { // This function sends data to 0x00 ("broadcast") to look for any connected rig. lookingForRig = true; foundRig = false; QByteArray data; QByteArray data2; //data.setRawData("\xFE\xFE\xa2", 3); data.setRawData("\xFE\xFE\x00", 3); data.append((char)compCivAddr); // wfview's address, 0xE1 data2.setRawData("\x19\x00", 2); // get rig ID data.append(data2); data.append(payloadSuffix); emit dataForComm(data); // HACK for testing radios that do not respond to rig ID queries: //this->model = model736; //this->determineRigCaps(); return; } void rigCommander::prepDataAndSend(QByteArray data) { data.prepend(payloadPrefix); //printHex(data, false, true); data.append(payloadSuffix); if(data[4] != '\x15') { // We don't print out requests for meter levels qDebug(logRig()) << "Final payload in rig commander to be sent to rig: "; printHex(data); } emit dataForComm(data); } void rigCommander::powerOn() { QByteArray payload; for(int i=0; i < 150; i++) { payload.append("\xFE"); } payload.append(payloadPrefix); // FE FE 94 E1 payload.append("\x18\x01"); payload.append(payloadSuffix); // FD qDebug(logRig()) << "Power ON command in rigcommander to be sent to rig: "; printHex(payload); emit dataForComm(payload); } void rigCommander::powerOff() { QByteArray payload; payload.setRawData("\x18\x00", 2); prepDataAndSend(payload); } void rigCommander::enableSpectOutput() { QByteArray payload("\x27\x11\x01"); prepDataAndSend(payload); } void rigCommander::disableSpectOutput() { QByteArray payload; payload.setRawData("\x27\x11\x00", 3); prepDataAndSend(payload); } void rigCommander::enableSpectrumDisplay() { // 27 10 01 QByteArray payload("\x27\x10\x01"); prepDataAndSend(payload); } void rigCommander::disableSpectrumDisplay() { // 27 10 00 QByteArray payload; payload.setRawData("\x27\x10\x00", 3); prepDataAndSend(payload); } void rigCommander::setSpectrumBounds(double startFreq, double endFreq, unsigned char edgeNumber) { if((edgeNumber > 4) || (!edgeNumber)) { return; } unsigned char freqRange = 1; // 1 = VHF, 2 = UHF, 3 = L-Band switch(rigCaps.model) { case model9700: if(startFreq > 148) { freqRange++; if(startFreq > 450) { freqRange++; } } break; case model705: case model7300: case model7610: case model7850: // Some rigs do not go past 60 MHz, but we will not encounter // requests for those ranges since they are derived from the rig's existing scope range. // start value of freqRange is 1. if(startFreq > 1.6) freqRange++; if(startFreq > 2.0) freqRange++; if(startFreq > 6.0) freqRange++; if(startFreq > 8.0) freqRange++; if(startFreq > 11.0) freqRange++; if(startFreq > 15.0) freqRange++; if(startFreq > 20.0) freqRange++; if(startFreq > 22.0) freqRange++; if(startFreq > 26.0) freqRange++; if(startFreq > 30.0) freqRange++; if(startFreq > 45.0) freqRange++; if(startFreq > 60.0) freqRange++; if(startFreq > 74.8) freqRange++; if(startFreq > 108.0) freqRange++; if(startFreq > 137.0) freqRange++; if(startFreq > 400.0) freqRange++; break; case modelR8600: freqRange = 1; edgeNumber = 1; break; default: return; break; } QByteArray lowerEdge = makeFreqPayload(startFreq); QByteArray higherEdge = makeFreqPayload(endFreq); QByteArray payload; payload.setRawData("\x27\x1E", 2); payload.append(freqRange); payload.append(edgeNumber); payload.append(lowerEdge); payload.append(higherEdge); prepDataAndSend(payload); } void rigCommander::getScopeMode() { // center or fixed QByteArray payload; payload.setRawData("\x27\x14\x00", 3); prepDataAndSend(payload); } void rigCommander::getScopeEdge() { QByteArray payload; payload.setRawData("\x27\x16", 2); prepDataAndSend(payload); } void rigCommander::setScopeEdge(char edge) { // 1 2 or 3 // 27 16 00 0X if((edge <1) || (edge >4)) return; QByteArray payload; payload.setRawData("\x27\x16\x00", 3); payload.append(edge); prepDataAndSend(payload); } void rigCommander::getScopeSpan() { getScopeSpan(false); } void rigCommander::getScopeSpan(bool isSub) { QByteArray payload; payload.setRawData("\x27\x15", 2); payload.append(static_cast(isSub)); prepDataAndSend(payload); } void rigCommander::setScopeSpan(char span) { // See ICD, page 165, "19-12". // 2.5k = 0 // 5k = 2, etc. if((span <0 ) || (span >9)) return; QByteArray payload; double freq; // MHz payload.setRawData("\x27\x15\x00", 3); // next 6 bytes are the frequency switch(span) { case 0: // 2.5k freq = 2.5E-3; break; case 1: // 5k freq = 5.0E-3; break; case 2: freq = 10.0E-3; break; case 3: freq = 25.0E-3; break; case 4: freq = 50.0E-3; break; case 5: freq = 100.0E-3; break; case 6: freq = 250.0E-3; break; case 7: freq = 500.0E-3; break; case 8: freq = 1000.0E-3; break; case 9: freq = 2500.0E-3; break; default: return; break; } payload.append( makeFreqPayload(freq)); payload.append("\x00"); // printHex(payload, false, true); prepDataAndSend(payload); } void rigCommander::setSpectrumMode(spectrumMode spectMode) { QByteArray specModePayload; specModePayload.setRawData("\x27\x14\x00", 3); specModePayload.append( static_cast(spectMode) ); prepDataAndSend(specModePayload); } void rigCommander::getSpectrumRefLevel() { QByteArray payload; payload.setRawData("\x27\x19\x00", 3); prepDataAndSend(payload); } void rigCommander::getSpectrumRefLevel(unsigned char mainSub) { QByteArray payload; payload.setRawData("\x27\x19", 2); payload.append(mainSub); prepDataAndSend(payload); } void rigCommander::setSpectrumRefLevel(int level) { //qInfo(logRig()) << __func__ << ": Setting scope to level " << level; QByteArray setting; QByteArray number; QByteArray pn; setting.setRawData("\x27\x19\x00", 3); if(level >= 0) { pn.setRawData("\x00", 1); number = bcdEncodeInt(level*10); } else { pn.setRawData("\x01", 1); number = bcdEncodeInt( (-level)*10 ); } setting.append(number); setting.append(pn); //qInfo(logRig()) << __func__ << ": scope reference number: " << number << ", PN to: " << pn; //printHex(setting, false, true); prepDataAndSend(setting); } void rigCommander::getSpectrumCenterMode() { QByteArray specModePayload; specModePayload.setRawData("\x27\x14", 2); prepDataAndSend(specModePayload); } void rigCommander::getSpectrumMode() { QByteArray specModePayload; specModePayload.setRawData("\x27\x14", 2); prepDataAndSend(specModePayload); } void rigCommander::setFrequency(unsigned char vfo, freqt freq) { QByteArray freqPayload = makeFreqPayload(freq); QByteArray cmdPayload; cmdPayload.append(freqPayload); if (vfo == 0) { rigState.mutex->lock(); rigState.vfoAFreq = freq; rigState.mutex->unlock(); cmdPayload.prepend('\x00'); } else { rigState.mutex->lock(); rigState.vfoBFreq = freq; rigState.mutex->unlock(); cmdPayload.prepend(vfo); cmdPayload.prepend('\x25'); } //printHex(cmdPayload, false, true); prepDataAndSend(cmdPayload); } QByteArray rigCommander::makeFreqPayload(freqt freq) { QByteArray result; quint64 freqInt = freq.Hz; unsigned char a; int numchars = 5; for (int i = 0; i < numchars; i++) { a = 0; a |= (freqInt) % 10; freqInt /= 10; a |= ((freqInt) % 10)<<4; freqInt /= 10; result.append(a); //printHex(result, false, true); } return result; } QByteArray rigCommander::makeFreqPayload(double freq) { quint64 freqInt = (quint64) (freq * 1E6); QByteArray result; unsigned char a; int numchars = 5; for (int i = 0; i < numchars; i++) { a = 0; a |= (freqInt) % 10; freqInt /= 10; a |= ((freqInt) % 10)<<4; freqInt /= 10; result.append(a); //printHex(result, false, true); } //qInfo(logRig()) << "encoded frequency for " << freq << " as int " << freqInt; //printHex(result, false, true); return result; } void rigCommander::setRitEnable(bool ritEnabled) { QByteArray payload; if(ritEnabled) { payload.setRawData("\x21\x01\x01", 3); } else { payload.setRawData("\x21\x01\x00", 3); } prepDataAndSend(payload); } void rigCommander::getRitEnabled() { QByteArray payload; payload.setRawData("\x21\x01", 2); prepDataAndSend(payload); } void rigCommander::getRitValue() { QByteArray payload; payload.setRawData("\x21\x00", 2); prepDataAndSend(payload); } void rigCommander::setRitValue(int ritValue) { QByteArray payload; QByteArray freqBytes; freqt f; bool isNegative = false; payload.setRawData("\x21\x00", 2); if(ritValue < 0) { isNegative = true; ritValue *= -1; } if(ritValue > 9999) return; f.Hz = ritValue; freqBytes = makeFreqPayload(f); freqBytes.truncate(2); payload.append(freqBytes); payload.append(QByteArray(1,(char)isNegative)); prepDataAndSend(payload); } void rigCommander::setMode(mode_info m) { QByteArray payload; if(rigCaps.model==model706) { m.filter = '\x01'; } if(m.mk == modeWFM) { m.filter = '\x01'; } payload.setRawData("\x06", 1); payload.append(m.reg); payload.append(m.filter); prepDataAndSend(payload); QMutexLocker locker(rigState.mutex); rigState.mode = m.reg; rigState.filter = m.filter; } void rigCommander::setMode(unsigned char mode, unsigned char modeFilter) { QByteArray payload; if(mode < 0x22 + 1) { // mode command | filter // 0x01 | Filter 01 automatically // 0x04 | user-specififed 01, 02, 03 | note, is "read the current mode" on older rigs // 0x06 | "default" filter is auto payload.setRawData("\x06", 1); // cmd 06 needs filter specified //payload.setRawData("\x04", 1); // cmd 04 will apply the default filter, but it seems to always pick FIL 02 payload.append(mode); if(rigCaps.model==model706) { payload.append("\x01"); // "normal" on IC-706 } else { if(mode == 0x06) { payload.append(0x01); } else { payload.append(modeFilter); } } prepDataAndSend(payload); QMutexLocker locker(rigState.mutex); rigState.mode = mode; rigState.filter = modeFilter; } } void rigCommander::setDataMode(bool dataOn, unsigned char filter) { QByteArray payload; payload.setRawData("\x1A\x06", 2); if(dataOn) { payload.append("\x01", 1); // data mode on payload.append(filter); } else { payload.append("\x00\x00", 2); // data mode off, bandwidth not defined per ICD. } prepDataAndSend(payload); QMutexLocker locker(rigState.mutex); rigState.datamode = dataOn; } void rigCommander::getFrequency() { // figure out frequency and then respond with haveFrequency(); // send request to radio // 1. make the data QByteArray payload("\x03"); prepDataAndSend(payload); } void rigCommander::getMode() { QByteArray payload("\x04"); prepDataAndSend(payload); } void rigCommander::getDataMode() { QByteArray payload("\x1A\x06"); prepDataAndSend(payload); } void rigCommander::setDuplexMode(duplexMode dm) { QByteArray payload; if(dm==dmDupAutoOff) { payload.setRawData("\x1A\x05\x00\x46\x00", 5); } else if (dm==dmDupAutoOn) { payload.setRawData("\x1A\x05\x00\x46\x01", 5); } else { payload.setRawData("\x0F", 1); payload.append((unsigned char) dm); } prepDataAndSend(payload); } void rigCommander::getDuplexMode() { QByteArray payload; // Duplex mode: payload.setRawData("\x0F", 1); prepDataAndSend(payload); // Auto Repeater Mode: payload.setRawData("\x1A\x05\x00\x46", 4); prepDataAndSend(payload); } void rigCommander::getTransmitFrequency() { QByteArray payload; payload.setRawData("\x1C\x03", 2); prepDataAndSend(payload); } void rigCommander::setTone(quint16 tone) { QByteArray fenc = encodeTone(tone); QByteArray payload; payload.setRawData("\x1B\x00", 2); payload.append(fenc); //qInfo() << __func__ << "TONE encoded payload: "; printHex(payload); prepDataAndSend(payload); } void rigCommander::setTSQL(quint16 tsql) { QByteArray fenc = encodeTone(tsql); QByteArray payload; payload.setRawData("\x1B\x01", 2); payload.append(fenc); //qInfo() << __func__ << "TSQL encoded payload: "; printHex(payload); prepDataAndSend(payload); } void rigCommander::setDTCS(quint16 dcscode, bool tinv, bool rinv) { QByteArray denc = encodeTone(dcscode, tinv, rinv); QByteArray payload; payload.setRawData("\x1B\x02", 2); payload.append(denc); //qInfo() << __func__ << "DTCS encoded payload: "; printHex(payload); prepDataAndSend(payload); } QByteArray rigCommander::encodeTone(quint16 tone) { return encodeTone(tone, false, false); } QByteArray rigCommander::encodeTone(quint16 tone, bool tinv, bool rinv) { // This function is fine to use for DTCS and TONE QByteArray enct; unsigned char inv=0; inv = inv | (unsigned char)rinv; inv = inv | ((unsigned char)tinv) << 4; enct.append(inv); unsigned char hundreds = tone / 1000; unsigned char tens = (tone-(hundreds*1000)) / 100; unsigned char ones = (tone -(hundreds*1000)-(tens*100)) / 10; unsigned char dec = (tone -(hundreds*1000)-(tens*100)-(ones*10)); enct.append(tens | (hundreds<<4)); enct.append(dec | (ones <<4)); return enct; } quint16 rigCommander::decodeTone(QByteArray eTone) { bool t; bool r; return decodeTone(eTone, t, r); } quint16 rigCommander::decodeTone(QByteArray eTone, bool &tinv, bool &rinv) { // index: 00 01 02 03 04 // CTCSS: 1B 01 00 12 73 = PL 127.3, decode as 1273 // D(T)CS: 1B 01 TR 01 23 = T/R Invert bits + DCS code 123 if (eTone.length() < 5) { return 0; } tinv = false; rinv = false; quint16 result = 0; if((eTone.at(2) & 0x01) == 0x01) tinv = true; if((eTone.at(2) & 0x10) == 0x10) rinv = true; result += (eTone.at(4) & 0x0f); result += ((eTone.at(4) & 0xf0) >> 4) * 10; result += (eTone.at(3) & 0x0f) * 100; result += ((eTone.at(3) & 0xf0) >> 4) * 1000; return result; } void rigCommander::getTone() { QByteArray payload; payload.setRawData("\x1B\x00", 2); prepDataAndSend(payload); } void rigCommander::getTSQL() { QByteArray payload; payload.setRawData("\x1B\x01", 2); prepDataAndSend(payload); } void rigCommander::getDTCS() { QByteArray payload; payload.setRawData("\x1B\x02", 2); prepDataAndSend(payload); } void rigCommander::getRptAccessMode() { QByteArray payload; payload.setRawData("\x16\x5D", 2); prepDataAndSend(payload); } void rigCommander::setRptAccessMode(rptAccessTxRx ratr) { QByteArray payload; payload.setRawData("\x16\x5D", 2); payload.append((unsigned char)ratr); prepDataAndSend(payload); } void rigCommander::setIPP(bool enabled) { QByteArray payload; payload.setRawData("\x16\x65", 2); if(enabled) { payload.append("\x01"); } else { payload.append("\x00"); } prepDataAndSend(payload); } void rigCommander::getIPP() { QByteArray payload; payload.setRawData("\x16\x65", 2); prepDataAndSend(payload); } void rigCommander::setSatelliteMode(bool enabled) { QByteArray payload; payload.setRawData("\x16\x5A", 2); if(enabled) { payload.append("\x01"); } else { payload.append("\x00"); } prepDataAndSend(payload); } void rigCommander::getSatelliteMode() { QByteArray payload; payload.setRawData("\x16\x5A", 2); prepDataAndSend(payload); } void rigCommander::getPTT() { //if(rigCaps.useRTSforPTT && !usingNativeLAN) //{ // emit havePTTStatus(comm->rtsStatus()); //} else { QByteArray payload; payload.setRawData("\x1C\x00", 2); prepDataAndSend(payload); //} } void rigCommander::getBandStackReg(char band, char regCode) { QByteArray payload("\x1A\x01"); payload.append(band); // [01 through 11] payload.append(regCode); // [01...03]. 01 = latest, 03 = oldest prepDataAndSend(payload); } void rigCommander::setPTT(bool pttOn) { //bool pttAllowed = false; if(pttAllowed) { QByteArray payload("\x1C\x00", 2); payload.append((char)pttOn); prepDataAndSend(payload); QMutexLocker locker(rigState.mutex); rigState.ptt = pttOn; } } void rigCommander::setCIVAddr(unsigned char civAddr) { // Note: This sets the radio's CIV address // the computer's CIV address is defined in the header file. this->civAddr = civAddr; payloadPrefix = QByteArray("\xFE\xFE"); payloadPrefix.append(civAddr); payloadPrefix.append((char)compCivAddr); } void rigCommander::handleNewData(const QByteArray& data) { emit haveDataForServer(data); parseData(data); } void rigCommander::receiveAudioData(const audioPacket& data) { emit haveAudioData(data); } void rigCommander::parseData(QByteArray dataInput) { // TODO: Clean this up. // It finally works very nicely, needs to be streamlined. // int index = 0; volatile int count = 0; // debug purposes // use this: QList dataList = dataInput.split('\xFD'); QByteArray data; // qInfo(logRig()) << "data list has this many elements: " << dataList.size(); if (dataList.last().isEmpty()) { dataList.removeLast(); // if the original ended in FD, then there is a blank entry at the end. } // Only thing is, each frame is missing '\xFD' at the end. So append! Keeps the frames intact. for(index = 0; index < dataList.count(); index++) { data = dataList[index]; data.append('\xFD'); // because we expect it to be there. // foreach(listitem) // listitem.append('\xFD'); // continue parsing... count++; // Data echo'd back from the rig start with this: // fe fe 94 e0 ...... fd // Data from the rig that is not an echo start with this: // fe fe e0 94 ...... fd (for example, a reply to a query) // Data from the rig that was not asked for is sent to controller 0x00: // fe fe 00 94 ...... fd (for example, user rotates the tune control or changes the mode) //qInfo(logRig()) << "Data received: "; //printHex(data, false, true); if(data.length() < 4) { if(data.length()) { // Finally this almost never happens // qInfo(logRig()) << "Data length too short: " << data.length() << " bytes. Data:"; //printHex(data, false, true); } // no //return; // maybe: // continue; } if(!data.startsWith("\xFE\xFE")) { // qInfo(logRig()) << "Warning: Invalid data received, did not start with FE FE."; // find 94 e0 and shift over, // or look inside for a second FE FE // Often a local echo will miss a few bytes at the beginning. if(data.startsWith('\xFE')) { data.prepend('\xFE'); // qInfo(logRig()) << "Warning: Working with prepended data stream."; parseData(payloadIn); return; } else { //qInfo(logRig()) << "Error: Could not reconstruct corrupted data: "; //printHex(data, false, true); // data.right(data.length() - data.find('\xFE\xFE')); // if found do not return and keep going. return; } } if((unsigned char)data[02] == civAddr) { // data is or begins with an echoback from what we sent // find the first 'fd' and cut it. Then continue. //payloadIn = data.right(data.length() - data.indexOf('\xfd')-1); // qInfo(logRig()) << "[FOUND] Trimmed off echo:"; //printHex(payloadIn, false, true); //parseData(payloadIn); //return; } incomingCIVAddr = data[03]; // track the CIV of the sender. switch(data[02]) { // case civAddr: // can't have a variable here :-( // // data is or begins with an echoback from what we sent // // find the first 'fd' and cut it. Then continue. // payloadIn = data.right(data.length() - data.indexOf('\xfd')-1); // //qInfo(logRig()) << "Trimmed off echo:"; // //printHex(payloadIn, false, true); // parseData(payloadIn); // break; // case '\xE0': case (char)0xE0: case (char)compCivAddr: // data is a reply to some query we sent // extract the payload out and parse. // payload = getpayload(data); // or something // parse (payload); // recursive ok? payloadIn = data.right(data.length() - 4); if(payloadIn.contains("\xFE")) { //qDebug(logRig()) << "Corrupted data contains FE within message body: "; //printHex(payloadIn); break; } parseCommand(); break; case '\x00': // data send initiated by the rig due to user control // extract the payload out and parse. if((unsigned char)data[03]==compCivAddr) { // This is an echo of our own broadcast request. // The data are "to 00" and "from E1" // Don't use it! qDebug(logRig()) << "Caught it! Found the echo'd broadcast request from us! Rig has not responded to broadcast query yet."; } else { payloadIn = data.right(data.length() - 4); // Removes FE FE E0 94 part if(payloadIn.contains("\xFE")) { //qDebug(logRig()) << "Corrupted data contains FE within message body: "; //printHex(payloadIn); break; } parseCommand(); } break; default: // could be for other equipment on the CIV network. // just drop for now. // relaySendOutData(data); break; } } /* if(dataList.length() > 1) { qInfo(logRig()) << "Recovered " << count << " frames from single data with size" << dataList.count(); } */ } void rigCommander::parseCommand() { // note: data already is trimmed of the beginning FE FE E0 94 stuff. bool isSpectrumData = payloadIn.startsWith(QByteArray().setRawData("\x27\x00", 2)); if( (!isSpectrumData) && (payloadIn[00] != '\x15') ) { // We do not log spectrum and meter data, // as they tend to clog up any useful logging. printHex(payloadIn); } switch(payloadIn[00]) { case 00: // frequency data parseFrequency(); break; case 03: parseFrequency(); break; case '\x25': if((int)payloadIn[1] == 0) { emit haveFrequency(parseFrequency(payloadIn, 5)); } break; case '\x01': //qInfo(logRig()) << "Have mode data"; this->parseMode(); break; case '\x04': //qInfo(logRig()) << "Have mode data"; this->parseMode(); break; case '\x05': //qInfo(logRig()) << "Have frequency data"; this->parseFrequency(); break; case '\x06': //qInfo(logRig()) << "Have mode data"; this->parseMode(); break; case '\x0F': emit haveDuplexMode((duplexMode)(unsigned char)payloadIn[1]); rigState.mutex->lock(); rigState.duplex = (duplexMode)(unsigned char)payloadIn[1]; rigState.mutex->unlock(); break; case '\x11': emit haveAttenuator((unsigned char)payloadIn.at(1)); rigState.mutex->lock(); rigState.attenuator = (unsigned char)payloadIn.at(1); rigState.mutex->unlock(); break; case '\x12': emit haveAntenna((unsigned char)payloadIn.at(1), (bool)payloadIn.at(2)); rigState.mutex->lock(); rigState.antenna = (unsigned char)payloadIn.at(1); rigState.rxAntenna = (bool)payloadIn.at(2); rigState.mutex->unlock(); break; case '\x14': // read levels parseLevels(); break; case '\x15': // Metering such as s, power, etc parseLevels(); break; case '\x16': parseRegister16(); break; case '\x19': // qInfo(logRig()) << "Have rig ID: " << (unsigned int)payloadIn[2]; // printHex(payloadIn, false, true); model = determineRadioModel(payloadIn[2]); // verify this is the model not the CIV rigCaps.modelID = payloadIn[2]; determineRigCaps(); qInfo(logRig()) << "Have rig ID: decimal: " << (unsigned int)rigCaps.modelID; break; case '\x21': // RIT and Delta TX: parseRegister21(); break; case '\x26': if((int)payloadIn[1] == 0) { // This works but LSB comes out as CW? // Also, an opportunity to read the data mode // payloadIn = payloadIn.right(3); // this->parseMode(); } break; case '\x27': // scope data //qInfo(logRig()) << "Have scope data"; //printHex(payloadIn, false, true); parseWFData(); //parseSpectrum(); break; case '\x1A': if(payloadIn[01] == '\x05') { parseDetailedRegisters1A05(); } else { parseRegisters1A(); } break; case '\x1B': parseRegister1B(); break; case '\x1C': parseRegisters1C(); break; case '\xFB': // Fine Business, ACK from rig. break; case '\xFA': // error qDebug(logRig()) << "Error (FA) received from rig."; printHex(payloadIn, false ,true); break; default: // This gets hit a lot when the pseudo-term is // using commands wfview doesn't know yet. // qInfo(logRig()) << "Have other data with cmd: " << std::hex << payloadIn[00]; // printHex(payloadIn, false, true); break; } // is any payload left? } void rigCommander::parseLevels() { //qInfo(logRig()) << "Received a level status readout: "; // printHex(payloadIn, false, true); // wrong: unsigned char level = (payloadIn[2] * 100) + payloadIn[03]; unsigned char hundreds = payloadIn[2]; unsigned char tens = (payloadIn[3] & 0xf0) >> 4; unsigned char units = (payloadIn[3] & 0x0f); unsigned char level = ((unsigned char)100*hundreds) + (10*tens) + units; //qInfo(logRig()) << "Level is: " << (int)level << " or " << 100.0*level/255.0 << "%"; // Typical RF gain response (rather low setting): // "INDEX: 00 01 02 03 04 " // "DATA: 14 02 00 78 fd " if(payloadIn[0] == '\x14') { switch(payloadIn[1]) { case '\x01': // AF level - ignore if LAN connection. if (udp == Q_NULLPTR) { emit haveAfGain(level); rigState.mutex->lock(); rigState.afGain = level; rigState.mutex->unlock(); } break; case '\x02': // RX RF Gain emit haveRfGain(level); rigState.mutex->lock(); rigState.rfGain = level; rigState.mutex->unlock(); break; case '\x03': // Squelch level emit haveSql(level); rigState.mutex->lock(); rigState.squelch = level; rigState.mutex->unlock(); break; case '\x07': // Twin BPF Inner, or, IF-Shift level if(rigCaps.hasTBPF) emit haveTPBFInner(level); else emit haveIFShift(level); break; case '\x08': // Twin BPF Outer emit haveTPBFOuter(level); break; case '\x09': // CW Pitch - ignore for now break; case '\x0A': // TX RF level emit haveTxPower(level); rigState.mutex->lock(); rigState.txPower = level; rigState.mutex->unlock(); break; case '\x0B': // Mic Gain emit haveMicGain(level); rigState.mutex->lock(); rigState.micGain = level; rigState.mutex->unlock(); break; case '\x0C': // CW Keying Speed - ignore for now break; case '\x0D': // Notch filder setting - ignore for now break; case '\x0E': // compressor level emit haveCompLevel(level); rigState.mutex->lock(); rigState.compLevel = level; rigState.mutex->unlock(); break; case '\x12': // NB level - ignore for now break; case '\x15': // monitor level emit haveMonitorLevel(level); rigState.mutex->lock(); rigState.monitorLevel = level; rigState.mutex->unlock(); break; case '\x16': // VOX gain emit haveVoxGain(level); rigState.mutex->lock(); rigState.voxGain = level; rigState.mutex->unlock(); break; case '\x17': // anti-VOX gain emit haveAntiVoxGain(level); rigState.mutex->lock(); rigState.antiVoxGain = level; rigState.mutex->unlock(); break; default: qInfo(logRig()) << "Unknown control level (0x14) received at register " << QString("0x%1").arg((int)payloadIn[1],2,16) << " with level " << QString("0x%1").arg((int)level,2,16) << ", int=" << (int)level; printHex(payloadIn); break; } return; } if(payloadIn[0] == '\x15') { switch(payloadIn[1]) { case '\x02': // S-Meter emit haveMeter(meterS, level); rigState.mutex->lock(); rigState.sMeter = level; rigState.mutex->unlock(); break; case '\x04': // Center (IC-R8600) emit haveMeter(meterCenter, level); rigState.mutex->lock(); rigState.sMeter = level; rigState.mutex->unlock(); break; case '\x11': // RF-Power meter emit haveMeter(meterPower, level); rigState.mutex->lock(); rigState.powerMeter = level; rigState.mutex->unlock(); break; case '\x12': // SWR emit haveMeter(meterSWR, level); rigState.mutex->lock(); rigState.swrMeter = level; rigState.mutex->unlock(); break; case '\x13': // ALC emit haveMeter(meterALC, level); rigState.mutex->lock(); rigState.alcMeter = level; rigState.mutex->unlock(); break; case '\x14': // COMP dB reduction emit haveMeter(meterComp, level); rigState.mutex->lock(); rigState.compMeter = level; rigState.mutex->unlock(); break; case '\x15': // VD (12V) emit haveMeter(meterVoltage, level); rigState.mutex->lock(); rigState.voltageMeter = level; rigState.mutex->unlock(); break; case '\x16': // ID emit haveMeter(meterCurrent, level); rigState.mutex->lock(); rigState.currentMeter = level; rigState.mutex->unlock(); break; default: qInfo(logRig()) << "Unknown meter level (0x15) received at register " << (unsigned int) payloadIn[1] << " with level " << level; break; } return; } } void rigCommander::setIFShift(unsigned char level) { QByteArray payload("\x14\x07"); payload.append(bcdEncodeInt(level)); prepDataAndSend(payload); } void rigCommander::setTPBFInner(unsigned char level) { QByteArray payload("\x14\x07"); payload.append(bcdEncodeInt(level)); prepDataAndSend(payload); } void rigCommander::setTPBFOuter(unsigned char level) { QByteArray payload("\x14\x08"); payload.append(bcdEncodeInt(level)); prepDataAndSend(payload); } void rigCommander::setTxPower(unsigned char power) { QByteArray payload("\x14\x0A"); payload.append(bcdEncodeInt(power)); prepDataAndSend(payload); } void rigCommander::setMicGain(unsigned char gain) { QByteArray payload("\x14\x0B"); payload.append(bcdEncodeInt(gain)); prepDataAndSend(payload); } void rigCommander::getModInput(bool dataOn) { setModInput(inputMic, dataOn, true); } void rigCommander::setModInput(rigInput input, bool dataOn) { setModInput(input, dataOn, false); } void rigCommander::setModInput(rigInput input, bool dataOn, bool isQuery) { // The input enum is as follows: // inputMic=0, // inputACC=1, // inputUSB=3, // inputLAN=5, // inputACCA, // inputACCB}; QByteArray payload; QByteArray inAsByte; if(isQuery) input = inputMic; switch(rigCaps.model) { case model9700: payload.setRawData("\x1A\x05\x01\x15", 4); payload.append((unsigned char)input); break; case model7610: payload.setRawData("\x1A\x05\x00\x91", 4); payload.append((unsigned char)input); break; case model7300: payload.setRawData("\x1A\x05\x00\x66", 4); payload.append((unsigned char)input); break; case model7850: payload.setRawData("\x1A\x05\x00\x63", 4); switch(input) { case inputMic: inAsByte.setRawData("\x00", 1); break; case inputACCA: inAsByte.setRawData("\x01", 1); break; case inputACCB: inAsByte.setRawData("\x02", 1); break; case inputUSB: inAsByte.setRawData("\x08", 1); break; case inputLAN: inAsByte.setRawData("\x09", 1); break; default: return; } payload.append(inAsByte); break; case model705: payload.setRawData("\x1A\x05\x01\x18", 4); switch(input) { case inputMic: inAsByte.setRawData("\x00", 1); break; case inputUSB: inAsByte.setRawData("\x01", 1); break; case inputLAN: // WLAN inAsByte.setRawData("\x03", 1); break; default: return; } payload.append(inAsByte); break; case model7700: payload.setRawData("\x1A\x05\x00\x32", 4); if(input==inputLAN) { // NOTE: CIV manual says data may range from 0 to 3 // But data 0x04 does correspond to LAN. payload.append("\x04"); } else { payload.append((unsigned char)input); } break; case model7600: payload.setRawData("\x1A\x05\x00\x30", 4); payload.append((unsigned char)input); break; case model7100: payload.setRawData("\x1A\x05\x00\x90", 4); payload.append((unsigned char)input); break; case model7200: payload.setRawData("\x1A\x03\x23", 3); switch(input) { case inputMic: payload.setRawData("\x00", 1); break; case inputUSB: payload.setRawData("\x03", 1); break; case inputACC: payload.setRawData("\x01", 1); break; default: return; } default: break; } if(dataOn) { if(rigCaps.model==model7200) { payload[2] = payload[2] + 1; } else { payload[3] = payload[3] + 1; } } if(isQuery) { payload.truncate(4); } prepDataAndSend(payload); } void rigCommander::setModInputLevel(rigInput input, unsigned char level) { switch(input) { case inputMic: setMicGain(level); break; case inputACCA: setACCGain(level, 0); break; case inputACCB: setACCGain(level, 1); break; case inputACC: setACCGain(level); break; case inputUSB: setUSBGain(level); break; case inputLAN: setLANGain(level); break; default: break; } } void rigCommander::getModInputLevel(rigInput input) { switch(input) { case inputMic: getMicGain(); break; case inputACCA: getACCGain(0); break; case inputACCB: getACCGain(1); break; case inputACC: getACCGain(); break; case inputUSB: getUSBGain(); break; case inputLAN: getLANGain(); break; default: break; } } QByteArray rigCommander::getUSBAddr() { QByteArray payload; switch(rigCaps.model) { case model705: payload.setRawData("\x1A\x05\x01\x16", 4); break; case model9700: payload.setRawData("\x1A\x05\x01\x13", 4); break; case model7200: payload.setRawData("\x1A\x03\x25", 3); break; case model7100: case model7610: payload.setRawData("\x1A\x05\x00\x89", 4); break; case model7300: payload.setRawData("\x1A\x05\x00\x65", 4); break; case model7850: payload.setRawData("\x1A\x05\x00\x61", 4); break; case model7600: payload.setRawData("\x1A\x05\x00\x29", 4); break; default: break; } return payload; } void rigCommander::getUSBGain() { QByteArray payload = getUSBAddr(); prepDataAndSend(payload); } void rigCommander::setUSBGain(unsigned char gain) { QByteArray payload = getUSBAddr(); payload.append(bcdEncodeInt(gain)); prepDataAndSend(payload); } QByteArray rigCommander::getLANAddr() { QByteArray payload; switch(rigCaps.model) { case model705: payload.setRawData("\x1A\x05\x01\x17", 4); break; case model9700: payload.setRawData("\x1A\x05\x01\x14", 4); break; case model7610: payload.setRawData("\x1A\x05\x00\x90", 4); break; case model7850: payload.setRawData("\x1A\x05\x00\x62", 4); break; case model7700: payload.setRawData("\x1A\x05\x01\x92", 4); break; default: break; } return payload; } void rigCommander::getLANGain() { QByteArray payload = getLANAddr(); prepDataAndSend(payload); } void rigCommander::setLANGain(unsigned char gain) { QByteArray payload = getLANAddr(); payload.append(bcdEncodeInt(gain)); prepDataAndSend(payload); } QByteArray rigCommander::getACCAddr(unsigned char ab) { QByteArray payload; // Note: the manual for the IC-7600 does not call out a // register to adjust the ACC gain. // 7850: ACC-A = 0, ACC-B = 1 switch(rigCaps.model) { case model9700: payload.setRawData("\x1A\x05\x01\x12", 4); break; case model7100: payload.setRawData("\x1A\x05\x00\x87", 4); break; case model7610: payload.setRawData("\x1A\x05\x00\x88", 4); break; case model7300: payload.setRawData("\x1A\x05\x00\x64", 4); break; case model7850: // Note: 0x58 = ACC-A, 0x59 = ACC-B if(ab==0) { // A payload.setRawData("\x1A\x05\x00\x58", 4); } else { // B payload.setRawData("\x1A\x05\x00\x59", 4); } break; case model7700: payload.setRawData("\x1A\x05\x00\x30", 4); break; default: break; } return payload; } void rigCommander::getACCGain() { QByteArray payload = getACCAddr(0); prepDataAndSend(payload); } void rigCommander::getACCGain(unsigned char ab) { QByteArray payload = getACCAddr(ab); prepDataAndSend(payload); } void rigCommander::setACCGain(unsigned char gain) { QByteArray payload = getACCAddr(0); payload.append(bcdEncodeInt(gain)); prepDataAndSend(payload); } void rigCommander::setACCGain(unsigned char gain, unsigned char ab) { QByteArray payload = getACCAddr(ab); payload.append(bcdEncodeInt(gain)); prepDataAndSend(payload); } void rigCommander::setCompLevel(unsigned char compLevel) { QByteArray payload("\x14\x0E"); payload.append(bcdEncodeInt(compLevel)); prepDataAndSend(payload); } void rigCommander::setMonitorLevel(unsigned char monitorLevel) { QByteArray payload("\x14\x0E"); payload.append(bcdEncodeInt(monitorLevel)); prepDataAndSend(payload); } void rigCommander::setVoxGain(unsigned char gain) { QByteArray payload("\x14\x16"); payload.append(bcdEncodeInt(gain)); prepDataAndSend(payload); } void rigCommander::setAntiVoxGain(unsigned char gain) { QByteArray payload("\x14\x17"); payload.append(bcdEncodeInt(gain)); prepDataAndSend(payload); } void rigCommander::getRfGain() { QByteArray payload("\x14\x02"); prepDataAndSend(payload); } void rigCommander::getAfGain() { QByteArray payload("\x14\x01"); prepDataAndSend(payload); } void rigCommander::getIFShift() { QByteArray payload("\x14\x07"); prepDataAndSend(payload); } void rigCommander::getTPBFInner() { QByteArray payload("\x14\x07"); prepDataAndSend(payload); } void rigCommander::getTPBFOuter() { QByteArray payload("\x14\x08"); prepDataAndSend(payload); } void rigCommander::getSql() { QByteArray payload("\x14\x03"); prepDataAndSend(payload); } void rigCommander::getTxLevel() { QByteArray payload("\x14\x0A"); prepDataAndSend(payload); } void rigCommander::getMicGain() { QByteArray payload("\x14\x0B"); prepDataAndSend(payload); } void rigCommander::getCompLevel() { QByteArray payload("\x14\x0E"); prepDataAndSend(payload); } void rigCommander::getMonitorLevel() { QByteArray payload("\x14\x15"); prepDataAndSend(payload); } void rigCommander::getVoxGain() { QByteArray payload("\x14\x16"); prepDataAndSend(payload); } void rigCommander::getAntiVoxGain() { QByteArray payload("\x14\x17"); prepDataAndSend(payload); } void rigCommander::getLevels() { // Function to grab all levels getRfGain(); //0x02 getAfGain(); // 0x01 getSql(); // 0x03 getTxLevel(); // 0x0A getMicGain(); // 0x0B getCompLevel(); // 0x0E // getMonitorLevel(); // 0x15 // getVoxGain(); // 0x16 // getAntiVoxGain(); // 0x17 } void rigCommander::getMeters(meterKind meter) { switch(meter) { case meterS: getSMeter(); break; case meterCenter: getCenterMeter(); break; case meterSWR: getSWRMeter(); break; case meterPower: getRFPowerMeter(); break; case meterALC: getALCMeter(); break; case meterComp: getCompReductionMeter(); break; case meterVoltage: getVdMeter(); break; case meterCurrent: getIDMeter(); break; default: break; } } void rigCommander::getSMeter() { QByteArray payload("\x15\x02"); prepDataAndSend(payload); } void rigCommander::getCenterMeter() { QByteArray payload("\x15\x04"); prepDataAndSend(payload); } void rigCommander::getRFPowerMeter() { QByteArray payload("\x15\x11"); prepDataAndSend(payload); } void rigCommander::getSWRMeter() { QByteArray payload("\x15\x12"); prepDataAndSend(payload); } void rigCommander::getALCMeter() { QByteArray payload("\x15\x13"); prepDataAndSend(payload); } void rigCommander::getCompReductionMeter() { QByteArray payload("\x15\x14"); prepDataAndSend(payload); } void rigCommander::getVdMeter() { QByteArray payload("\x15\x15"); prepDataAndSend(payload); } void rigCommander::getIDMeter() { QByteArray payload("\x15\x16"); prepDataAndSend(payload); } void rigCommander::setSquelch(unsigned char level) { sendLevelCmd(0x03, level); } void rigCommander::setRfGain(unsigned char level) { sendLevelCmd(0x02, level); } void rigCommander::setAfGain(unsigned char level) { if (udp == Q_NULLPTR) { sendLevelCmd(0x01, level); } else { emit haveSetVolume(level); } } void rigCommander::setRefAdjustCourse(unsigned char level) { // 1A 05 00 72 0000-0255 QByteArray payload; payload.setRawData("\x1A\x05\x00\x72", 4); payload.append(bcdEncodeInt((unsigned int)level)); prepDataAndSend(payload); } void rigCommander::setRefAdjustFine(unsigned char level) { qInfo(logRig()) << __FUNCTION__ << " level: " << level; // 1A 05 00 73 0000-0255 QByteArray payload; payload.setRawData("\x1A\x05\x00\x73", 4); payload.append(bcdEncodeInt((unsigned int)level)); prepDataAndSend(payload); } void rigCommander::setTime(timekind t) { QByteArray payload; switch(rigCaps.model) { case model705: payload.setRawData("\x1A\x05\x01\x66", 4); break; case model7300: payload.setRawData("\x1A\x05\x00\x95", 4); break; case model7610: payload.setRawData("\x1A\x05\x01\x59", 4); break; case model7700: payload.setRawData("\x1A\x05\x00\x59", 4); break; case model7850: payload.setRawData("\x1A\x05\x00\x96", 4); break; case model9700: payload.setRawData("\x1A\x05\x01\x80", 4); break; case modelR8600: payload.setRawData("\x1A\x05\x01\x32", 4); break; default: return; break; } payload.append(convertNumberToHex(t.hours)); payload.append(convertNumberToHex(t.minutes)); //qDebug(logRig()) << "Setting time to this: "; //printHex(payload); prepDataAndSend(payload); } void rigCommander::setDate(datekind d) { QByteArray payload; switch(rigCaps.model) { case model705: payload.setRawData("\x1A\x05\x01\x65", 4); break; case model7300: payload.setRawData("\x1A\x05\x00\x94", 4); break; case model7610: payload.setRawData("\x1A\x05\x01\x58", 4); break; case model7700: payload.setRawData("\x1A\x05\x00\x58", 4); break; case model7850: payload.setRawData("\x1A\x05\x00\x95", 4); break; case model9700: payload.setRawData("\x1A\x05\x01\x79", 4); break; case modelR8600: payload.setRawData("\x1A\x05\x01\x31", 4); break; default: return; break; } // YYYYMMDD payload.append(convertNumberToHex(d.year/100)); // 20 payload.append(convertNumberToHex(d.year - 100*(d.year/100))); // 21 payload.append(convertNumberToHex(d.month)); payload.append(convertNumberToHex(d.day)); //qDebug(logRig()) << "Setting date to this: "; //printHex(payload); prepDataAndSend(payload); } void rigCommander::setUTCOffset(timekind t) { QByteArray payload; switch(rigCaps.model) { case model705: payload.setRawData("\x1A\x05\x01\x70", 4); break; case model7300: payload.setRawData("\x1A\x05\x00\x96", 4); break; case model7610: payload.setRawData("\x1A\x05\x01\x62", 4); break; case model7700: payload.setRawData("\x1A\x05\x00\x61", 4); break; case model7850: // Clock 1: payload.setRawData("\x1A\x05\x00\x99", 4); break; case model9700: payload.setRawData("\x1A\x05\x01\x84", 4); break; case modelR8600: payload.setRawData("\x1A\x05\x01\x35", 4); break; default: return; break; } payload.append(convertNumberToHex(t.hours)); payload.append(convertNumberToHex(t.minutes)); payload.append((unsigned char)t.isMinus); //qDebug(logRig()) << "Setting UTC Offset to this: "; //printHex(payload); prepDataAndSend(payload); } unsigned char rigCommander::convertNumberToHex(unsigned char num) { // Two digit only if(num > 99) { qInfo(logRig()) << "Invalid numeric conversion from num " << num << " to hex."; return 0xFA; } unsigned char result = 0; result = (num/10) << 4; result |= (num - 10*(num/10)); qDebug(logRig()) << "Converting number: " << num << " to hex: " + QString("0x%1").arg(result, 2, 16, QChar('0')); return result; } void rigCommander::sendLevelCmd(unsigned char levAddr, unsigned char level) { QByteArray payload("\x14"); payload.append(levAddr); // careful here. The value in the units and tens can't exceed 99. // ie, can't do this: 01 f2 payload.append((int)level/100); // make sure it works with a zero // convert the tens: int tens = (level - 100*((int)level/100))/10; // convert the units: int units = level - 100*((int)level/100); units = units - 10*((int)(units/10)); // combine and send: payload.append((tens << 4) | (units) ); // make sure it works with a zero prepDataAndSend(payload); } void rigCommander::getRefAdjustCourse() { // 1A 05 00 72 QByteArray payload; payload.setRawData("\x1A\x05\x00\x72", 4); prepDataAndSend(payload); } void rigCommander::getRefAdjustFine() { // 1A 05 00 73 QByteArray payload; payload.setRawData("\x1A\x05\x00\x73", 4); prepDataAndSend(payload); } void rigCommander::parseRegisters1C() { // PTT lives here // Not sure if 02 is the right place to switch. // TODO: test this function switch(payloadIn[01]) { case '\x00': parsePTT(); break; case '\x01': // ATU status (on/off/tuning) parseATU(); break; default: break; } } void rigCommander::parseRegister21() { // Register 21 is RIT and Delta TX int ritHz = 0; freqt f; QByteArray longfreq; // Example RIT value reply: // Index: 00 01 02 03 04 05 // DATA: 21 00 32 03 00 fd switch(payloadIn[01]) { case '\x00': // RIT frequency // longfreq = payloadIn.mid(2,2); longfreq.append(QByteArray(3,'\x00')); f = parseFrequency(longfreq, 3); if(payloadIn.length() < 5) break; ritHz = f.Hz*((payloadIn.at(4)=='\x01')?-1:1); emit haveRitFrequency(ritHz); break; case '\x01': // RIT on/off if(payloadIn.at(02) == '\x01') { emit haveRitEnabled(true); } else { emit haveRitEnabled(false); } break; case '\x02': // Delta TX setting on/off break; default: break; } } void rigCommander::parseATU() { // qInfo(logRig()) << "Have ATU status from radio. Emitting."; // Expect: // [0]: 0x1c // [1]: 0x01 // [2]: 0 = off, 0x01 = on, 0x02 = tuning in-progress emit haveATUStatus((unsigned char) payloadIn[2]); } void rigCommander::parsePTT() { // read after payloadIn[02] if(payloadIn[2] == (char)0) { // PTT off emit havePTTStatus(false); } else { // PTT on emit havePTTStatus(true); } QMutexLocker locker(rigState.mutex); rigState.ptt = (bool)payloadIn[2]; } void rigCommander::parseRegisters1A() { // The simpler of the 1A stuff: // 1A 06: data mode on/off // 07: IP+ enable/disable // 00: memory contents // 01: band stacking memory contents (last freq used is stored here per-band) // 03: filter width // 04: AGC rate // qInfo(logRig()) << "Looking at register 1A :"; // printHex(payloadIn, false, true); // "INDEX: 00 01 02 03 04 " // "DATA: 1a 06 01 03 fd " (data mode enabled, filter width 3 selected) QMutexLocker locker(rigState.mutex); switch(payloadIn[01]) { case '\x00': // Memory contents break; case '\x01': // band stacking register parseBandStackReg(); break; case '\x06': // data mode // emit havedataMode( (bool) payloadIn[somebit]) // index // 03 04 // XX YY // XX = 00 (off) or 01 (on) // YY: filter selected, 01 through 03.; // if YY is 00 then XX was also set to 00 emit haveDataMode((bool)payloadIn[03]); rigState.datamode = (bool)payloadIn[03]; break; case '\x07': // IP+ status break; default: break; } } void rigCommander::parseRegister1B() { quint16 tone=0; bool tinv = false; bool rinv = false; switch(payloadIn[01]) { case '\x00': // "Repeater tone" tone = decodeTone(payloadIn); emit haveTone(tone); rigState.mutex->lock(); rigState.ctcss = tone; rigState.mutex->unlock(); break; case '\x01': // "TSQL tone" tone = decodeTone(payloadIn); emit haveTSQL(tone); rigState.mutex->lock(); rigState.tsql = tone; rigState.mutex->unlock(); break; case '\x02': // DTCS (DCS) tone = decodeTone(payloadIn, tinv, rinv); emit haveDTCS(tone, tinv, rinv); rigState.mutex->lock(); rigState.dtcs = tone; rigState.mutex->unlock(); break; case '\x07': // "CSQL code (DV mode)" tone = decodeTone(payloadIn); rigState.mutex->lock(); rigState.csql = tone; rigState.mutex->unlock(); break; default: break; } } void rigCommander::parseRegister16() { //"INDEX: 00 01 02 03 " //"DATA: 16 5d 00 fd " // ^-- mode info here switch(payloadIn.at(1)) { case '\x5d': emit haveRptAccessMode((rptAccessTxRx)payloadIn.at(2)); break; case '\x02': // Preamp emit havePreamp((unsigned char)payloadIn.at(2)); rigState.mutex->lock(); rigState.preamp = (unsigned char)payloadIn.at(2); rigState.mutex->unlock(); break; default: break; } } void rigCommander::parseBandStackReg() { //qInfo(logRig()) << "Band stacking register response received: "; //printHex(payloadIn, false, true); // Reference output, 20 meters, regCode 01 (latest): // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 " // "DATA: 1a 01 05 01 60 03 23 14 00 00 03 10 00 08 85 00 08 85 fd " char band = payloadIn[2]; char regCode = payloadIn[3]; freqt freqs = parseFrequency(payloadIn, 7); //float freq = (float)freqs.MHzDouble; bool dataOn = (payloadIn[11] & 0x10) >> 4; // not sure... char mode = payloadIn[9]; char filter = payloadIn[10]; // 09, 10 mode // 11 digit RH: data mode on (1) or off (0) // 11 digit LH: CTCSS 0 = off, 1 = TONE, 2 = TSQL // 12, 13 : tone freq setting // 14, 15 tone squelch freq setting // if more, memory name (label) ascii qInfo(logRig()) << "BSR in rigCommander: band: " << QString("%1").arg(band) << " regCode: " << (QString)regCode << " freq Hz: " << freqs.Hz << ", mode: " << (unsigned int)mode << ", filter: " << (unsigned int)filter << " data: " << dataOn; //qInfo(logRig()) << "mode: " << (QString)mode << " dataOn: " << dataOn; //qInfo(logRig()) << "Freq Hz: " << freqs.Hz; emit haveBandStackReg(freqs, mode, filter, dataOn); } void rigCommander::parseDetailedRegisters1A05() { // It seems a lot of misc stuff is under this command and subcommand. // 1A 05 ... // 00 01 02 03 04 ... // 02 and 03 make up a BCD'd number: // 0001, 0002, 0003, ... 0101, 0102, 0103... // 04 is a typical single byte response // 04 05 is a typical 0-255 response // This file processes the registers which are radically different in each model. // It is a work in progress. // TODO: inputMod source and gain for models: 7700, and 7600 int level = (100*bcdHexToUChar(payloadIn[4])) + bcdHexToUChar(payloadIn[5]); int subcmd = bcdHexToUChar(payloadIn[3]) + (100*bcdHexToUChar(payloadIn[2])); rigInput input; input = (rigInput)bcdHexToUChar(payloadIn[4]); int inputRaw = bcdHexToUChar(payloadIn[4]); switch(rigCaps.model) { case model9700: switch(subcmd) { case 72: // course reference emit haveRefAdjustCourse( bcdHexToUChar(payloadIn[5]) + (100*bcdHexToUChar(payloadIn[4])) ); break; case 73: // fine reference emit haveRefAdjustFine( bcdHexToUChar(payloadIn[5]) + (100*bcdHexToUChar(payloadIn[4])) ); break; case 112: emit haveACCGain(level, 5); break; case 113: emit haveUSBGain(level); break; case 114: emit haveLANGain(level); break; case 115: emit haveModInput(input, false); break; case 116: emit haveModInput(input, true); break; default: break; } break; case model7850: switch(subcmd) { case 63: switch(inputRaw) { case 0: input = inputMic; break; case 1: input = inputACCA; break; case 2: input = inputACCB; break; case 8: input = inputUSB; break; case 9: input = inputLAN; break; default: input = inputUnknown; break; } emit haveModInput(input, false); break; case 64: switch(inputRaw) { case 0: input = inputMic; break; case 1: input = inputACCA; break; case 2: input = inputACCB; break; case 8: input = inputUSB; break; case 9: input = inputLAN; break; default: input = inputUnknown; break; } emit haveModInput(input, true); break; case 58: emit haveACCGain(level, 0); break; case 59: emit haveACCGain(level, 1); break; case 61: emit haveUSBGain(level); break; case 62: emit haveLANGain(level); break; default: break; } break; case model7610: switch(subcmd) { case 91: emit haveModInput(input, false); break; case 92: emit haveModInput(input, true); break; case 88: emit haveACCGain(level, 5); break; case 89: emit haveUSBGain(level); break; case 90: emit haveLANGain(level); break; default: break; } return; case model7600: switch(subcmd) { case 30: emit haveModInput(input, false); break; case 31: emit haveModInput(input, true); break; case 29: emit haveUSBGain(level); break; default: break; } return; case model7300: switch(subcmd) { case 64: emit haveACCGain(level, 5); break; case 65: emit haveUSBGain(level); break; case 66: emit haveModInput(input, false); break; case 67: emit haveModInput(input, true); break; default: break; } return; case model7100: switch(subcmd) { case 87: emit haveACCGain(level, 5); break; case 89: emit haveUSBGain(level); break; case 90: emit haveModInput(input, false); break; case 91: emit haveModInput(input, true); break; default: break; } break; case model705: switch(subcmd) { case 116: emit haveUSBGain(level); break; case 117: emit haveLANGain(level); break; case 118: switch(inputRaw) { case 0: input = inputMic; break; case 1: input = inputUSB; break; case 3: input = inputLAN; break; default: input = inputUnknown; break; } emit haveModInput(input, false); break; case 119: switch(inputRaw) { case 0: input = inputMic; break; case 1: input = inputUSB; break; case 3: input = inputLAN; break; default: input = inputUnknown; break; } emit haveModInput(input, true); break; default: break; } break; default: break; } } void rigCommander::parseWFData() { freqt freqSpan; bool isSub; switch(payloadIn[1]) { case 0: // Chunk of spectrum parseSpectrum(); break; case 0x10: // confirming scope is on break; case 0x11: // confirming output enabled/disabled of wf data. break; case 0x14: // fixed or center emit haveSpectrumMode(static_cast((unsigned char)payloadIn[3])); // [1] 0x14 // [2] 0x00 // [3] 0x00 (center), 0x01 (fixed), 0x02, 0x03 break; case 0x15: // read span in center mode // [1] 0x15 // [2] to [8] is span encoded as a frequency isSub = payloadIn.at(2)==0x01; freqSpan = parseFrequency(payloadIn, 6); emit haveScopeSpan(freqSpan, isSub); qInfo(logRig()) << "Received 0x15 center span data: for frequency " << freqSpan.Hz; //printHex(payloadIn, false, true); break; case 0x16: // read edge mode center in edge mode emit haveScopeEdge((char)payloadIn[2]); qInfo(logRig()) << "Received 0x16 edge in center mode:"; printHex(payloadIn, false, true); // [1] 0x16 // [2] 0x01, 0x02, 0x03: Edge 1,2,3 break; case 0x17: // Hold status (only 9700?) qInfo(logRig()) << "Received 0x17 hold status - need to deal with this!"; printHex(payloadIn, false, true); break; case 0x19: // scope reference level // [1] 0x19 // [2] 0x00 // [3] 10dB digit, 1dB digit // [4] 0.1dB digit, 0 // [5] 0x00 = +, 0x01 = - parseSpectrumRefLevel(); break; default: qInfo(logRig()) << "Unknown waveform data received: "; printHex(payloadIn, false, true); break; } } mode_info rigCommander::createMode(mode_kind m, unsigned char reg, QString name) { mode_info mode; mode.mk = m; mode.reg = reg; mode.name = name; return mode; } centerSpanData rigCommander::createScopeCenter(centerSpansType s, QString name) { centerSpanData csd; csd.cstype = s; csd.name = name; return csd; } void rigCommander::determineRigCaps() { //TODO: Determine available bands (low priority, rig will reject out of band requests anyway) std::vector standardHF; std::vector standardVU; // Most commonly supported "HF" bands: standardHF = {band6m, band10m, band10m, band12m, band15m, band17m, band20m, band30m, band40m, band60m, band80m, band160m}; standardVU = {band70cm, band2m}; std::vector commonModes; commonModes = { createMode(modeLSB, 0x00, "LSB"), createMode(modeUSB, 0x01, "USB"), createMode(modeFM, 0x05, "FM"), createMode(modeAM, 0x02, "AM"), createMode(modeCW, 0x03, "CW"), createMode(modeCW_R, 0x07, "CW-R"), createMode(modeRTTY, 0x04, "RTTY"), createMode(modeRTTY_R, 0x08, "RTTY-R") }; rigCaps.model = model; rigCaps.civ = incomingCIVAddr; rigCaps.hasDD = false; rigCaps.hasDV = false; rigCaps.hasDataModes = true; // USB-D, LSB-D, etc rigCaps.hasATU = false; rigCaps.hasCTCSS = false; rigCaps.hasDTCS = false; rigCaps.hasTBPF = false; rigCaps.hasIFShift = false; rigCaps.spectSeqMax = 0; rigCaps.spectAmpMax = 0; rigCaps.spectLenMax = 0; rigCaps.scopeCenterSpans = { createScopeCenter(cs2p5k, "±2.5k"), createScopeCenter(cs5k, "±5k"), createScopeCenter(cs10k, "±10k"), createScopeCenter(cs25k, "±25k"), createScopeCenter(cs50k, "±50k"), createScopeCenter(cs100k, "±100k"), createScopeCenter(cs250k, "±250k"), createScopeCenter(cs500k, "±500k") }; rigCaps.hasFDcomms = true; // false for older radios // Clear inputs/preamps/attenuators lists in case we have re-connected. rigCaps.preamps.clear(); rigCaps.attenuators.clear(); rigCaps.inputs.clear(); rigCaps.inputs.append(inputMic); rigCaps.hasAttenuator = true; // Verify that all recent rigs have attenuators rigCaps.attenuators.push_back('\x00'); rigCaps.hasPreamp = true; rigCaps.preamps.push_back('\x00'); rigCaps.hasAntennaSel = false; rigCaps.hasRXAntenna = false; rigCaps.hasTransmit = true; rigCaps.hasPTTCommand = true; rigCaps.useRTSforPTT = false; // Common, reasonable defaults for most supported HF rigs: rigCaps.bsr[band160m] = 0x01; rigCaps.bsr[band80m] = 0x02; rigCaps.bsr[band40m] = 0x03; rigCaps.bsr[band30m] = 0x04; rigCaps.bsr[band20m] = 0x05; rigCaps.bsr[band17m] = 0x06; rigCaps.bsr[band15m] = 0x07; rigCaps.bsr[band12m] = 0x08; rigCaps.bsr[band10m] = 0x09; rigCaps.bsr[band6m] = 0x10; rigCaps.bsr[bandGen] = 0x11; // Bands that seem to change with every model: rigCaps.bsr[band2m] = 0x00; rigCaps.bsr[band70cm] = 0x00; rigCaps.bsr[band23cm] = 0x00; // These bands generally aren't defined: rigCaps.bsr[band4m] = 0x00; rigCaps.bsr[band60m] = 0x00; rigCaps.bsr[bandWFM] = 0x00; rigCaps.bsr[bandAir] = 0x00; rigCaps.bsr[band630m] = 0x00; rigCaps.bsr[band2200m] = 0x00; switch(model){ case model7300: rigCaps.modelName = QString("IC-7300"); rigCaps.rigctlModel = 3073; rigCaps.hasSpectrum = true; rigCaps.spectSeqMax = 11; rigCaps.spectAmpMax = 160; rigCaps.spectLenMax = 475; rigCaps.inputs.append(inputUSB); rigCaps.inputs.append(inputACC); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasATU = true; rigCaps.hasCTCSS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.push_back('\x20'); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.bands = standardHF; rigCaps.bands.push_back(band4m); rigCaps.bands.push_back(bandGen); rigCaps.bands.push_back(band630m); rigCaps.bands.push_back(band2200m); rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x71"); break; case modelR8600: rigCaps.modelName = QString("IC-R8600"); rigCaps.rigctlModel = 3079; rigCaps.hasSpectrum = true; rigCaps.spectSeqMax = 11; rigCaps.spectAmpMax = 160; rigCaps.spectLenMax = 475; rigCaps.inputs.clear(); rigCaps.hasLan = true; rigCaps.hasEthernet = true; rigCaps.hasWiFi = false; rigCaps.hasTransmit = false; rigCaps.hasPTTCommand = false; rigCaps.hasCTCSS = true; rigCaps.hasDTCS = true; rigCaps.hasDV = true; rigCaps.hasTBPF = true; rigCaps.attenuators.push_back('\x10'); rigCaps.attenuators.push_back('\x20'); rigCaps.attenuators.push_back('\x30'); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.hasAntennaSel = true; rigCaps.antennas = {0x00, 0x01, 0x02}; rigCaps.bands = standardHF; rigCaps.bands.insert(rigCaps.bands.end(), standardVU.begin(), standardVU.end()); rigCaps.bands.insert(rigCaps.bands.end(), {band23cm, band4m, band630m, band2200m, bandGen}); rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), { createMode(modeWFM, 0x06, "WFM"), createMode(modeS_AMD, 0x11, "S-AM (D)"), createMode(modeS_AML, 0x14, "S-AM(L)"), createMode(modeS_AMU, 0x15, "S-AM(U)"), createMode(modeP25, 0x16, "P25"), createMode(modedPMR, 0x18, "dPMR"), createMode(modeNXDN_VN, 0x19, "NXDN-VN"), createMode(modeNXDN_N, 0x20, "NXDN-N"), createMode(modeDCR, 0x21, "DCR")}); rigCaps.scopeCenterSpans.insert(rigCaps.scopeCenterSpans.end(), {createScopeCenter(cs1M, "±1M"), createScopeCenter(cs2p5M, "±2.5M")}); rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x92"); break; case model9700: rigCaps.modelName = QString("IC-9700"); rigCaps.rigctlModel = 3081; rigCaps.hasSpectrum = true; rigCaps.spectSeqMax = 11; rigCaps.spectAmpMax = 160; rigCaps.spectLenMax = 475; rigCaps.inputs.append(inputLAN); rigCaps.inputs.append(inputUSB); rigCaps.inputs.append(inputACC); rigCaps.hasLan = true; rigCaps.hasEthernet = true; rigCaps.hasWiFi = false; rigCaps.hasDD = true; rigCaps.hasDV = true; rigCaps.hasCTCSS = true; rigCaps.hasDTCS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.push_back('\x10'); rigCaps.preamps.push_back('\x01'); rigCaps.bands = standardVU; rigCaps.bands.push_back(band23cm); rigCaps.bsr[band23cm] = 0x03; rigCaps.bsr[band70cm] = 0x02; rigCaps.bsr[band2m] = 0x01; rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), {createMode(modeDV, 0x17, "DV"), createMode(modeDD, 0x22, "DD")}); rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x01\x27"); break; case model910h: rigCaps.modelName = QString("IC-910H"); rigCaps.rigctlModel = 3044; rigCaps.hasSpectrum = false; rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasDD = false; rigCaps.hasDV = false; rigCaps.hasCTCSS = true; rigCaps.hasDTCS = true; rigCaps.hasATU = false; rigCaps.attenuators.insert(rigCaps.attenuators.end(),{ '\x10' , '\x20', '\x30'}); rigCaps.preamps.push_back('\x01'); rigCaps.bands = standardVU; rigCaps.bands.push_back(band23cm); rigCaps.bsr[band23cm] = 0x03; rigCaps.bsr[band70cm] = 0x02; rigCaps.bsr[band2m] = 0x01; rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x58"); break; case model7600: rigCaps.modelName = QString("IC-7600"); rigCaps.rigctlModel = 3063; rigCaps.hasSpectrum = false; rigCaps.inputs.append(inputACC); rigCaps.inputs.append(inputUSB); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasCTCSS = false; rigCaps.hasDTCS = false; rigCaps.hasTBPF = true; rigCaps.attenuators.insert(rigCaps.attenuators.end(), {0x00, 0x06, 0x12, 0x18}); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.antennas = {0x00, 0x01}; rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bsr[bandGen] = 0x11; rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), {createMode(modePSK, 0x12, "PSK"), createMode(modePSK_R, 0x13, "PSK-R")}); rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x97"); break; case model7610: rigCaps.modelName = QString("IC-7610"); rigCaps.rigctlModel = 3078; rigCaps.hasSpectrum = true; rigCaps.spectSeqMax = 15; rigCaps.spectAmpMax = 200; rigCaps.spectLenMax = 689; rigCaps.inputs.append(inputLAN); rigCaps.inputs.append(inputUSB); rigCaps.inputs.append(inputACC); rigCaps.hasLan = true; rigCaps.hasEthernet = true; rigCaps.hasWiFi = false; rigCaps.hasCTCSS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.insert(rigCaps.attenuators.end(), {'\x03', '\x06', '\x09', '\x12',\ '\x15', '\x18', '\x21', '\x24',\ '\x27', '\x30', '\x33', '\x36', '\x39', '\x42', '\x45'}); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.hasAntennaSel = true; rigCaps.antennas = {0x00, 0x01}; rigCaps.hasATU = true; rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bands.push_back(band630m); rigCaps.bands.push_back(band2200m); rigCaps.modes = commonModes; rigCaps.hasRXAntenna = true; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x01\x12"); break; case model7850: rigCaps.modelName = QString("IC-785x"); rigCaps.rigctlModel = 3075; rigCaps.hasSpectrum = true; rigCaps.spectSeqMax = 15; rigCaps.spectAmpMax = 136; rigCaps.spectLenMax = 689; rigCaps.inputs.append(inputLAN); rigCaps.inputs.append(inputUSB); rigCaps.inputs.append(inputACCA); rigCaps.inputs.append(inputACCB); rigCaps.hasLan = true; rigCaps.hasEthernet = true; rigCaps.hasWiFi = false; rigCaps.hasATU = true; rigCaps.hasCTCSS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.insert(rigCaps.attenuators.end(), {'\x03', '\x06', '\x09', '\x12', '\x15', '\x18', '\x21'}); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.hasAntennaSel = true; rigCaps.antennas = {0x00, 0x01, 0x02, 0x03}; rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bands.push_back(band630m); rigCaps.bands.push_back(band2200m); rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), {createMode(modePSK, 0x12, "PSK"), createMode(modePSK_R, 0x13, "PSK-R")}); rigCaps.hasRXAntenna = true; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x01\x55"); break; case model705: rigCaps.modelName = QString("IC-705"); rigCaps.rigctlModel = 3085; rigCaps.hasSpectrum = true; rigCaps.spectSeqMax = 11; rigCaps.spectAmpMax = 160; rigCaps.spectLenMax = 475; rigCaps.inputs.append(inputLAN); rigCaps.inputs.append(inputUSB); rigCaps.hasLan = true; rigCaps.hasEthernet = false; rigCaps.hasWiFi = true; rigCaps.hasDD = true; rigCaps.hasDV = true; rigCaps.hasATU = true; rigCaps.hasCTCSS = true; rigCaps.hasDTCS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.insert(rigCaps.attenuators.end(),{ '\x10' , '\x20'}); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.bands = standardHF; rigCaps.bands.insert(rigCaps.bands.end(), standardVU.begin(), standardVU.end()); rigCaps.bands.push_back(bandGen); rigCaps.bands.push_back(bandAir); rigCaps.bands.push_back(bandWFM); rigCaps.bsr[band70cm] = 0x14; rigCaps.bsr[band2m] = 0x13; rigCaps.bsr[bandAir] = 0x12; rigCaps.bsr[bandWFM] = 0x11; rigCaps.bsr[bandGen] = 0x15; rigCaps.bands.push_back(band630m); rigCaps.bands.push_back(band2200m); rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), {createMode(modeWFM, 0x06, "WFM"), createMode(modeDV, 0x17, "DV")}); rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x01\x31"); break; case model7000: rigCaps.modelName = QString("IC-7000"); rigCaps.rigctlModel = 3060; rigCaps.hasSpectrum = false; rigCaps.inputs.append(inputACC); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasCTCSS = true; rigCaps.hasDTCS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.push_back('\x12'); rigCaps.preamps.push_back('\x01'); rigCaps.bands = standardHF; rigCaps.bands.insert(rigCaps.bands.end(), standardVU.begin(), standardVU.end()); rigCaps.bands.push_back(bandGen); rigCaps.bsr[band2m] = 0x11; rigCaps.bsr[band70cm] = 0x12; rigCaps.bsr[bandGen] = 0x13; rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x92"); break; case model7410: rigCaps.modelName = QString("IC-7410"); rigCaps.rigctlModel = 3067; rigCaps.hasSpectrum = false; rigCaps.inputs.append(inputACC); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = true; rigCaps.hasATU = true; rigCaps.hasCTCSS = true; rigCaps.hasDTCS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.push_back('\x20'); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.antennas = {0x00, 0x01}; rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bsr[bandGen] = 0x11; rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x40"); break; case model7100: rigCaps.modelName = QString("IC-7100"); rigCaps.rigctlModel = 3070; rigCaps.hasSpectrum = false; rigCaps.inputs.append(inputUSB); rigCaps.inputs.append(inputACC); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasCTCSS = true; rigCaps.hasDTCS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.push_back('\x12'); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.bands = standardHF; rigCaps.bands.insert(rigCaps.bands.end(), standardVU.begin(), standardVU.end()); rigCaps.bands.push_back(band4m); rigCaps.bands.push_back(bandGen); rigCaps.bsr[band2m] = 0x11; rigCaps.bsr[band70cm] = 0x12; rigCaps.bsr[bandGen] = 0x13; rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), {createMode(modeWFM, 0x06, "WFM"), createMode(modeDV, 0x17, "DV")}); rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x95"); break; case model7200: rigCaps.modelName = QString("IC-7200"); rigCaps.rigctlModel = 3061; rigCaps.hasSpectrum = false; rigCaps.inputs.append(inputUSB); rigCaps.inputs.append(inputACC); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasCTCSS = true; rigCaps.hasDTCS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.push_back('\x20'); rigCaps.preamps.push_back('\x01'); rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bsr[bandGen] = 0x11; rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x03\x48"); break; case model7700: rigCaps.modelName = QString("IC-7700"); rigCaps.rigctlModel = 3062; rigCaps.hasSpectrum = false; rigCaps.inputs.append(inputLAN); //rigCaps.inputs.append(inputSPDIF); rigCaps.inputs.append(inputACC); rigCaps.hasLan = true; rigCaps.hasEthernet = true; rigCaps.hasWiFi = false; rigCaps.hasCTCSS = true; rigCaps.hasTBPF = true; rigCaps.attenuators.insert(rigCaps.attenuators.end(), {'\x06', '\x12', '\x18'}); rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.hasAntennaSel = true; rigCaps.antennas = {0x00, 0x01, 0x02, 0x03}; // not sure if 0x03 works rigCaps.hasATU = true; rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bands.push_back(band630m); rigCaps.bands.push_back(band2200m); rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), {createMode(modePSK, 0x12, "PSK"), createMode(modePSK_R, 0x13, "PSK-R")}); rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x95"); break; case model706: rigCaps.modelName = QString("IC-706"); rigCaps.rigctlModel = 3009; rigCaps.hasSpectrum = false; rigCaps.inputs.clear(); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasPTTCommand = false; rigCaps.useRTSforPTT = true; rigCaps.hasDataModes = false; rigCaps.attenuators.push_back('\x20'); rigCaps.bands = standardHF; rigCaps.bands.insert(rigCaps.bands.end(), standardVU.begin(), standardVU.end()); rigCaps.bands.push_back(bandGen); rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), createMode(modeWFM, 0x06, "WFM")); rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x00"); break; case model718: rigCaps.modelName = QString("IC-718"); rigCaps.rigctlModel = 3013; rigCaps.hasSpectrum = false; rigCaps.inputs.clear(); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = false; rigCaps.hasPTTCommand = false; rigCaps.useRTSforPTT = true; rigCaps.hasIFShift = true; rigCaps.hasDataModes = false; rigCaps.attenuators.push_back('\x20'); rigCaps.preamps.push_back('\x01'); rigCaps.bands = {band10m, band10m, band12m, band15m, band17m, band20m, band30m, band40m, band60m, band80m, band160m, bandGen}; rigCaps.modes = { createMode(modeLSB, 0x00, "LSB"), createMode(modeUSB, 0x01, "USB"), createMode(modeAM, 0x02, "AM"), createMode(modeCW, 0x03, "CW"), createMode(modeCW_R, 0x07, "CW-R"), createMode(modeRTTY, 0x04, "RTTY"), createMode(modeRTTY_R, 0x08, "RTTY-R") }; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x00"); break; case model736: rigCaps.modelName = QString("IC-736"); rigCaps.rigctlModel = 3020; rigCaps.hasSpectrum = false; rigCaps.inputs.clear(); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = false; rigCaps.hasPTTCommand = false; rigCaps.useRTSforPTT = true; rigCaps.hasDataModes = false; rigCaps.hasIFShift = true; // untested rigCaps.attenuators.push_back('\x20'); rigCaps.preamps.push_back('\x01'); rigCaps.bands = standardHF; rigCaps.modes = { createMode(modeLSB, 0x00, "LSB"), createMode(modeUSB, 0x01, "USB"), createMode(modeAM, 0x02, "AM"), createMode(modeFM, 0x05, "FM"), createMode(modeCW, 0x03, "CW"), createMode(modeCW_R, 0x07, "CW-R"), }; break; case model756pro: rigCaps.modelName = QString("IC-756 Pro"); rigCaps.rigctlModel = 3027; rigCaps.hasSpectrum = false; rigCaps.inputs.clear(); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasTBPF = true; rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.attenuators.insert(rigCaps.attenuators.end(),{ '\x06' , '\x12', '\x18'}); rigCaps.antennas = {0x00, 0x01}; rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bsr[bandGen] = 0x11; rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x00"); break; case model756proii: rigCaps.modelName = QString("IC-756 Pro II"); rigCaps.rigctlModel = 3027; rigCaps.hasSpectrum = false; rigCaps.inputs.clear(); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasTBPF = true; rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.attenuators.insert(rigCaps.attenuators.end(),{ '\x06' , '\x12', '\x18'}); rigCaps.antennas = {0x00, 0x01}; rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bsr[bandGen] = 0x11; rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x00"); break; case model756proiii: rigCaps.modelName = QString("IC-756 Pro III"); rigCaps.rigctlModel = 3027; rigCaps.hasSpectrum = false; rigCaps.inputs.clear(); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasTBPF = true; rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.attenuators.insert(rigCaps.attenuators.end(),{ '\x06' , '\x12', '\x18'}); rigCaps.antennas = {0x00, 0x01}; rigCaps.bands = standardHF; rigCaps.bands.push_back(bandGen); rigCaps.bsr[bandGen] = 0x11; rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x00"); break; case model9100: rigCaps.modelName = QString("IC-9100"); rigCaps.rigctlModel = 3068; rigCaps.hasSpectrum = false; rigCaps.inputs.append(inputUSB); // TODO, add commands for this radio's inputs rigCaps.inputs.append(inputACC); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasATU = true; rigCaps.hasDV = true; rigCaps.hasTBPF = true; rigCaps.preamps.push_back('\x01'); rigCaps.preamps.push_back('\x02'); rigCaps.attenuators.insert(rigCaps.attenuators.end(),{ '\x20' }); rigCaps.antennas = {0x00, 0x01}; rigCaps.bands = standardHF; rigCaps.bands.insert(rigCaps.bands.end(), standardVU.begin(), standardVU.end()); rigCaps.bands.push_back(band23cm); rigCaps.bands.push_back(bandGen); rigCaps.bsr[band2m] = 0x11; rigCaps.bsr[band70cm] = 0x12; rigCaps.bsr[band23cm] = 0x13; rigCaps.bsr[bandGen] = 0x14; rigCaps.modes = commonModes; rigCaps.modes.insert(rigCaps.modes.end(), {createMode(modeDV, 0x17, "DV")}); break; default: rigCaps.modelName = QString("IC-0x%1").arg(rigCaps.modelID, 2, 16); rigCaps.hasSpectrum = false; rigCaps.spectSeqMax = 0; rigCaps.spectAmpMax = 0; rigCaps.spectLenMax = 0; rigCaps.inputs.clear(); rigCaps.hasLan = false; rigCaps.hasEthernet = false; rigCaps.hasWiFi = false; rigCaps.hasFDcomms = false; rigCaps.hasPreamp = false; rigCaps.hasAntennaSel = false; rigCaps.attenuators.push_back('\x10'); rigCaps.attenuators.push_back('\x12'); rigCaps.attenuators.push_back('\x20'); rigCaps.bands = standardHF; rigCaps.bands.insert(rigCaps.bands.end(), standardVU.begin(), standardVU.end()); rigCaps.bands.insert(rigCaps.bands.end(), {band23cm, band4m, band630m, band2200m, bandGen}); rigCaps.modes = commonModes; rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x00"); qInfo(logRig()) << "Found unknown rig: 0x" << QString("%1").arg(rigCaps.modelID, 2, 16); break; } haveRigCaps = true; if(!usingNativeLAN) comm->setUseRTSforPTT(rigCaps.useRTSforPTT); if(lookingForRig) { lookingForRig = false; foundRig = true; qDebug(logRig()) << "---Rig FOUND from broadcast query:"; this->civAddr = incomingCIVAddr; // Override and use immediately. payloadPrefix = QByteArray("\xFE\xFE"); payloadPrefix.append(civAddr); payloadPrefix.append((char)compCivAddr); // if there is a compile-time error, remove the following line, the "hex" part is the issue: qInfo(logRig()) << "Using incomingCIVAddr: (int): " << this->civAddr << " hex: " << hex << this->civAddr; emit discoveredRigID(rigCaps); } else { if(!foundRig) { emit discoveredRigID(rigCaps); foundRig = true; } emit haveRigID(rigCaps); } } void rigCommander::parseSpectrum() { if(!haveRigCaps) { qDebug(logRig()) << "Spectrum received in rigCommander, but rigID is incomplete."; return; } if(rigCaps.spectSeqMax == 0) { // there is a chance this will happen with rigs that support spectrum. Once our RigID query returns, we will parse correctly. qInfo(logRig()) << "Warning: Spectrum sequence max was zero, yet spectrum was received."; return; } // Here is what to expect: // payloadIn[00] = '\x27'; // payloadIn[01] = '\x00'; // payloadIn[02] = '\x00'; // // Example long: (sequences 2-10, 50 pixels) // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 " // "DATA: 27 00 00 07 11 27 13 15 01 00 22 21 09 08 06 19 0e 20 23 25 2c 2d 17 27 29 16 14 1b 1b 21 27 1a 18 17 1e 21 1b 24 21 22 23 13 19 23 2f 2d 25 25 0a 0e 1e 20 1f 1a 0c fd " // ^--^--(seq 7/11) // ^-- start waveform data 0x00 to 0xA0, index 05 to 54 // // Example medium: (sequence #11) // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 " // "DATA: 27 00 00 11 11 0b 13 21 23 1a 1b 22 1e 1a 1d 13 21 1d 26 28 1f 19 1a 18 09 2c 2c 2c 1a 1b fd " // Example short: (sequence #1) includes center/fixed mode at [05]. No pixels. // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 " // "DATA: 27 00 00 01 11 01 00 00 00 14 00 00 00 35 14 00 00 fd " // ^-- mode 00 (center) or 01 (fixed) // ^--14.00 MHz lower edge // ^-- 14.350 MHz upper edge // ^-- possibly 00=in range 01 = out of range // Note, the index used here, -1, matches the ICD in the owner's manual. // Owner's manual + 1 = our index. // divs: Mode: Waveinfo: Len: Comment: // 2-10 var var 56 Minimum wave information w/waveform data // 11 10 26 31 Minimum wave information w/waveform data // 1 1 0 18 Only Wave Information without waveform data freqt fStart; freqt fEnd; unsigned char sequence = bcdHexToUChar(payloadIn[03]); //unsigned char sequenceMax = bcdHexToDecimal(payloadIn[04]); // unsigned char waveInfo = payloadIn[06]; // really just one byte? //qInfo(logRig()) << "Spectrum Data received: " << sequence << "/" << sequenceMax << " mode: " << scopeMode << " waveInfo: " << waveInfo << " length: " << payloadIn.length(); // Sequnce 2, index 05 is the start of data // Sequence 11. index 05, is the last chunk // Sequence 11, index 29, is the actual last pixel (it seems) // It looks like the data length may be variable, so we need to detect it each time. // start at payloadIn.length()-1 (to override the FD). Never mind, index -1 bad. // chop off FD. if ((sequence == 1) && (sequence < rigCaps.spectSeqMax)) { spectrumMode scopeMode = (spectrumMode)bcdHexToUChar(payloadIn[05]); // 0=center, 1=fixed if(scopeMode != oldScopeMode) { //TODO: support the other two modes (firmware 1.40) // Modes: // 0x00 Center // 0x01 Fixed // 0x02 Scroll-C // 0x03 Scroll-F emit haveSpectrumMode(scopeMode); oldScopeMode = scopeMode; } // wave information spectrumLine.clear(); // For Fixed, and both scroll modes, the following produces correct information: fStart = parseFrequency(payloadIn, 9); spectrumStartFreq = fStart.MHzDouble; fEnd = parseFrequency(payloadIn, 14); spectrumEndFreq = fEnd.MHzDouble; if(scopeMode == spectModeCenter) { // "center" mode, start is actuall center, end is bandwidth. spectrumStartFreq -= spectrumEndFreq; spectrumEndFreq = spectrumStartFreq + 2*(spectrumEndFreq); // emit haveSpectrumCenterSpan(span); } if (payloadIn.length() > 400) // Must be a LAN packet. { payloadIn.chop(1); //spectrumLine.append(payloadIn.mid(17,475)); // write over the FD, last one doesn't, oh well. spectrumLine.append(payloadIn.right(payloadIn.length()-17)); // write over the FD, last one doesn't, oh well. emit haveSpectrumData(spectrumLine, spectrumStartFreq, spectrumEndFreq); } } else if ((sequence > 1) && (sequence < rigCaps.spectSeqMax)) { // spectrum from index 05 to index 54, length is 55 per segment. Length is 56 total. Pixel data is 50 pixels. // sequence numbers 2 through 10, 50 pixels each. Total after sequence 10 is 450 pixels. payloadIn.chop(1); spectrumLine.insert(spectrumLine.length(), payloadIn.right(payloadIn.length() - 5)); // write over the FD, last one doesn't, oh well. //qInfo(logRig()) << "sequence: " << sequence << "spec index: " << (sequence-2)*55 << " payloadPosition: " << payloadIn.length() - 5 << " payload length: " << payloadIn.length(); } else if (sequence == rigCaps.spectSeqMax) { // last spectrum, a little bit different (last 25 pixels). Total at end is 475 pixels (7300). payloadIn.chop(1); spectrumLine.insert(spectrumLine.length(), payloadIn.right(payloadIn.length() - 5)); //qInfo(logRig()) << "sequence: " << sequence << " spec index: " << (sequence-2)*55 << " payloadPosition: " << payloadIn.length() - 5 << " payload length: " << payloadIn.length(); emit haveSpectrumData(spectrumLine, spectrumStartFreq, spectrumEndFreq); } } void rigCommander::parseSpectrumRefLevel() { // 00: 27 // 01: 19 // 02: 00 (fixed) // 03: XX // 04: x0 // 05: 00 (+) or 01 (-) unsigned char negative = payloadIn[5]; int value = bcdHexToUInt(payloadIn[3], payloadIn[4]); value = value / 10; if(negative){ value *= (-1*negative); } emit haveSpectrumRefLevel(value); } unsigned char rigCommander::bcdHexToUChar(unsigned char in) { unsigned char out = 0; out = in & 0x0f; out += ((in & 0xf0) >> 4)*10; return out; } unsigned int rigCommander::bcdHexToUInt(unsigned char hundreds, unsigned char tensunits) { // convert: // hex data: 0x41 0x23 // convert to uint: // uchar: 4123 unsigned char thousands = ((hundreds & 0xf0)>>4); unsigned int rtnVal; rtnVal = (hundreds & 0x0f)*100; rtnVal += ((tensunits & 0xf0)>>4)*10; rtnVal += (tensunits & 0x0f); rtnVal += thousands * 1000; return rtnVal; } unsigned char rigCommander::bcdHexToUChar(unsigned char hundreds, unsigned char tensunits) { // convert: // hex data: 0x01 0x23 // convert to uchar: // uchar: 123 //unsigned char thousands = ((hundreds & 0xf0)>>4); unsigned char rtnVal; rtnVal = (hundreds & 0x0f)*100; rtnVal += ((tensunits & 0xf0)>>4)*10; rtnVal += (tensunits & 0x0f); //rtnVal += thousands * 1000; return rtnVal; } QByteArray rigCommander::bcdEncodeInt(unsigned int num) { if(num > 9999) { qInfo(logRig()) << __FUNCTION__ << "Error, number is too big for four-digit conversion: " << num; return QByteArray(); } char thousands = num / 1000; char hundreds = (num - (1000*thousands)) / 100; char tens = (num - (1000*thousands) - (100*hundreds)) / 10; char units = (num - (1000*thousands) - (100*hundreds) - (10*tens)); char b0 = hundreds | (thousands << 4); char b1 = units | (tens << 4); //qInfo(logRig()) << __FUNCTION__ << " encoding value " << num << " as hex:"; //printHex(QByteArray(b0), false, true); //printHex(QByteArray(b1), false, true); QByteArray result; result.append(b0).append(b1); return result; } void rigCommander::parseFrequency() { freqt freq; freq.Hz = 0; freq.MHzDouble = 0; // process payloadIn, which is stripped. // float frequencyMhz // payloadIn[04] = ; // XX MHz // payloadIn[03] = ; // XX0 KHz // payloadIn[02] = ; // X.X KHz // payloadIn[01] = ; // . XX KHz // printHex(payloadIn, false, true); frequencyMhz = 0.0; if(payloadIn.length() == 7) { // 7300 has these digits too, as zeros. // IC-705 or IC-9700 with higher frequency data available. frequencyMhz += 100*(payloadIn[05] & 0x0f); frequencyMhz += (1000*((payloadIn[05] & 0xf0) >> 4)); freq.Hz += (payloadIn[05] & 0x0f) * 1E6 * 100; freq.Hz += ((payloadIn[05] & 0xf0) >> 4) * 1E6 * 1000; } freq.Hz += (payloadIn[04] & 0x0f) * 1E6; freq.Hz += ((payloadIn[04] & 0xf0) >> 4) * 1E6 * 10; frequencyMhz += payloadIn[04] & 0x0f; frequencyMhz += 10*((payloadIn[04] & 0xf0) >> 4); // KHz land: frequencyMhz += ((payloadIn[03] & 0xf0) >>4)/10.0 ; frequencyMhz += (payloadIn[03] & 0x0f) / 100.0; frequencyMhz += ((payloadIn[02] & 0xf0) >> 4) / 1000.0; frequencyMhz += (payloadIn[02] & 0x0f) / 10000.0; frequencyMhz += ((payloadIn[01] & 0xf0) >> 4) / 100000.0; frequencyMhz += (payloadIn[01] & 0x0f) / 1000000.0; freq.Hz += payloadIn[01] & 0x0f; freq.Hz += ((payloadIn[01] & 0xf0) >> 4)* 10; freq.Hz += (payloadIn[02] & 0x0f) * 100; freq.Hz += ((payloadIn[02] & 0xf0) >> 4) * 1000; freq.Hz += (payloadIn[03] & 0x0f) * 10000; freq.Hz += ((payloadIn[03] & 0xf0) >>4) * 100000; freq.MHzDouble = frequencyMhz; rigState.mutex->lock(); rigState.vfoAFreq = freq; rigState.mutex->unlock(); emit haveFrequency(freq); } freqt rigCommander::parseFrequency(QByteArray data, unsigned char lastPosition) { // process payloadIn, which is stripped. // float frequencyMhz // payloadIn[04] = ; // XX MHz // payloadIn[03] = ; // XX0 KHz // payloadIn[02] = ; // X.X KHz // payloadIn[01] = ; // . XX KHz //printHex(data, false, true); // TODO: Check length of data array prior to reading +/- position // NOTE: This function was written on the IC-7300, which has no need for 100 MHz and 1 GHz. // Therefore, this function has to go to position +1 to retrieve those numbers for the IC-9700. // TODO: 64-bit value is incorrect, multiplying by wrong numbers. float freq = 0.0; freqt freqs; freqs.MHzDouble = 0; freqs.Hz = 0; // MHz: freq += 100*(data[lastPosition+1] & 0x0f); freq += (1000*((data[lastPosition+1] & 0xf0) >> 4)); freq += data[lastPosition] & 0x0f; freq += 10*((data[lastPosition] & 0xf0) >> 4); freqs.Hz += (data[lastPosition] & 0x0f) * 1E6; freqs.Hz += ((data[lastPosition] & 0xf0) >> 4) * 1E6 * 10; // 10 MHz if(data.length() >= lastPosition+1) { freqs.Hz += (data[lastPosition+1] & 0x0f) * 1E6 * 100; // 100 MHz freqs.Hz += ((data[lastPosition+1] & 0xf0) >> 4) * 1E6 * 1000; // 1000 MHz } // Hz: freq += ((data[lastPosition-1] & 0xf0) >>4)/10.0 ; freq += (data[lastPosition-1] & 0x0f) / 100.0; freq += ((data[lastPosition-2] & 0xf0) >> 4) / 1000.0; freq += (data[lastPosition-2] & 0x0f) / 10000.0; freq += ((data[lastPosition-3] & 0xf0) >> 4) / 100000.0; freq += (data[lastPosition-3] & 0x0f) / 1000000.0; freqs.Hz += (data[lastPosition-1] & 0x0f) * 10E3; // 10 KHz freqs.Hz += ((data[lastPosition-1] & 0xf0) >> 4) * 100E3; // 100 KHz freqs.Hz += (data[lastPosition-2] & 0x0f) * 100; // 100 Hz freqs.Hz += ((data[lastPosition-2] & 0xf0) >> 4) * 1000; // 1 KHz freqs.Hz += (data[lastPosition-3] & 0x0f) * 1; // 1 Hz freqs.Hz += ((data[lastPosition-3] & 0xf0) >> 4) * 10; // 10 Hz freqs.MHzDouble = (double)(freqs.Hz / 1000000.0); return freqs; } void rigCommander::parseMode() { unsigned char filter; if(payloadIn[2] != '\xFD') { filter = payloadIn[2]; } else { filter = 0; } rigState.mutex->lock(); rigState.mode = (unsigned char)payloadIn[01]; rigState.filter = filter; rigState.mutex->unlock(); emit haveMode((unsigned char)payloadIn[01], filter); } void rigCommander::startATU() { QByteArray payload("\x1C\x01\x02"); prepDataAndSend(payload); } void rigCommander::setATU(bool enabled) { QByteArray payload; if(enabled) { payload.setRawData("\x1C\x01\x01", 3); } else { payload.setRawData("\x1C\x01\x00", 3); } prepDataAndSend(payload); } void rigCommander::getATUStatus() { //qInfo(logRig()) << "Sending out for ATU status in RC."; QByteArray payload("\x1C\x01"); prepDataAndSend(payload); } void rigCommander::getAttenuator() { QByteArray payload("\x11"); prepDataAndSend(payload); } void rigCommander::getPreamp() { QByteArray payload("\x16\x02"); prepDataAndSend(payload); } void rigCommander::getAntenna() { // This one might neet some thought // as it seems each antenna has to be checked. // Maybe 0x12 alone will do it. QByteArray payload("\x12"); prepDataAndSend(payload); } void rigCommander::setAttenuator(unsigned char att) { QByteArray payload("\x11"); payload.append(att); prepDataAndSend(payload); } void rigCommander::setPreamp(unsigned char pre) { QByteArray payload("\x16\x02"); payload.append(pre); prepDataAndSend(payload); } void rigCommander::setAntenna(unsigned char ant, bool rx) { QByteArray payload("\x12"); payload.append(ant); if (rigCaps.hasRXAntenna) { payload.append((unsigned char)rx); // 0x00 = use for TX and RX } prepDataAndSend(payload); } void rigCommander::getRigID() { QByteArray payload; payload.setRawData("\x19\x00", 2); prepDataAndSend(payload); } void rigCommander::setRigID(unsigned char rigID) { // This function overrides radio model detection. // It can be used for radios without Rig ID commands, // or to force a specific radio model qInfo(logRig()) << "Sending rig ID to: (int)" << (int)rigID; lookingForRig = true; foundRig = false; // needed because this is a fake message and thus the value is uninitialized // this->civAddr comes from how rigCommander is setup and should be accurate. this->incomingCIVAddr = this->civAddr; this->model = determineRadioModel(rigID); rigCaps.modelID = rigID; rigCaps.model = determineRadioModel(rigID); determineRigCaps(); } void rigCommander::changeLatency(const quint16 value) { emit haveChangeLatency(value); } void rigCommander::sayAll() { QByteArray payload; payload.setRawData("\x13\x00", 2); prepDataAndSend(payload); } void rigCommander::sayFrequency() { QByteArray payload; payload.setRawData("\x13\x01", 2); prepDataAndSend(payload); } void rigCommander::sayMode() { QByteArray payload; payload.setRawData("\x13\x02", 2); prepDataAndSend(payload); } // Other: QByteArray rigCommander::stripData(const QByteArray &data, unsigned char cutPosition) { QByteArray rtndata; if(data.length() < cutPosition) { return rtndata; } rtndata = data.right(cutPosition); return rtndata; } void rigCommander::sendState() { emit stateInfo(&rigState); } void rigCommander::getDebug() { // generic debug function for development. emit getMoreDebug(); } void rigCommander::printHex(const QByteArray &pdata) { printHex(pdata, false, true); } void rigCommander::printHex(const QByteArray &pdata, bool printVert, bool printHoriz) { qDebug(logRig()) << "---- Begin hex dump -----:"; QString sdata("DATA: "); QString index("INDEX: "); QStringList strings; for(int i=0; i < pdata.length(); i++) { strings << QString("[%1]: %2").arg(i,8,10,QChar('0')).arg((unsigned char)pdata[i], 2, 16, QChar('0')); sdata.append(QString("%1 ").arg((unsigned char)pdata[i], 2, 16, QChar('0')) ); index.append(QString("%1 ").arg(i, 2, 10, QChar('0'))); } if(printVert) { for(int i=0; i < strings.length(); i++) { //sdata = QString(strings.at(i)); qDebug(logRig()) << strings.at(i); } } if(printHoriz) { qDebug(logRig()) << index; qDebug(logRig()) << sdata; } qDebug(logRig()) << "----- End hex dump -----"; } void rigCommander::dataFromServer(QByteArray data) { //qInfo(logRig()) << "emit dataForComm()"; emit dataForComm(data); } wfview-1.2d/rigcommander.h000066400000000000000000000346431415164626400156720ustar00rootroot00000000000000#ifndef RIGCOMMANDER_H #define RIGCOMMANDER_H #include #include #include #include #include "commhandler.h" #include "pttyhandler.h" #include "udphandler.h" #include "rigidentities.h" #include "repeaterattributes.h" #include "freqmemory.h" // This file figures out what to send to the comm and also // parses returns into useful things. // 0xE1 is new default, 0xE0 was before. // note: using a define because switch case doesn't even work with const unsigned char. Surprised me. #define compCivAddr 0xE1 enum meterKind { meterNone=0, meterS, meterCenter, meterSWR, meterPower, meterALC, meterComp, meterVoltage, meterCurrent, meterRxdB, meterTxMod, meterRxAudio, meterLatency }; enum spectrumMode { spectModeCenter=0x00, spectModeFixed=0x01, spectModeScrollC=0x02, spectModeScrollF=0x03, spectModeUnknown=0xff }; struct freqt { quint64 Hz; double MHzDouble; }; struct datekind { uint16_t year; unsigned char month; unsigned char day; }; struct timekind { unsigned char hours; unsigned char minutes; bool isMinus; }; struct rigStateStruct { QMutex *mutex; freqt vfoAFreq; freqt vfoBFreq; unsigned char currentVfo; bool ptt; unsigned char mode; unsigned char filter; duplexMode duplex; bool datamode; unsigned char antenna; bool rxAntenna; // Tones quint16 ctcss; quint16 tsql; quint16 dtcs; quint16 csql; // Levels unsigned char preamp; unsigned char attenuator; unsigned char modInput; unsigned char afGain; unsigned char rfGain; unsigned char squelch; unsigned char txPower; unsigned char micGain; unsigned char compLevel; unsigned char monitorLevel; unsigned char voxGain; unsigned char antiVoxGain; // Meters unsigned char sMeter; unsigned char powerMeter; unsigned char swrMeter; unsigned char alcMeter; unsigned char compMeter; unsigned char voltageMeter; unsigned char currentMeter; // Functions bool fagcFunc=false; bool nbFunc=false; bool compFunc=false; bool voxFunc = false; bool toneFunc = false; bool tsqlFunc = false; bool sbkinFunc = false; bool fbkinFunc = false; bool anfFunc = false; bool nrFunc = false; bool aipFunc = false; bool apfFunc = false; bool monFunc = false; bool mnFunc = false; bool rfFunc = false; bool aroFunc = false; bool muteFunc = false; bool vscFunc = false; bool revFunc = false; bool sqlFunc = false; bool abmFunc = false; bool bcFunc = false; bool mbcFunc = false; bool ritFunc = false; bool afcFunc = false; bool satmodeFunc = false; bool scopeFunc = false; bool resumeFunc = false; bool tburstFunc = false; bool tunerFunc = false; bool lockFunc = false; }; class rigCommander : public QObject { Q_OBJECT public: rigCommander(); ~rigCommander(); bool usingLAN(); public slots: void process(); void commSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate,QString vsp); void commSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp); void closeComm(); // Power: void powerOn(); void powerOff(); // Spectrum: void enableSpectOutput(); void disableSpectOutput(); void enableSpectrumDisplay(); void disableSpectrumDisplay(); void setSpectrumBounds(double startFreq, double endFreq, unsigned char edgeNumber); void setSpectrumMode(spectrumMode spectMode); void getSpectrumCenterMode(); void getSpectrumMode(); void setSpectrumRefLevel(int level); void getSpectrumRefLevel(); void getSpectrumRefLevel(unsigned char mainSub); void setScopeSpan(char span); void getScopeSpan(bool isSub); void getScopeSpan(); void setScopeEdge(char edge); void getScopeEdge(); void getScopeMode(); // Frequency, Mode, BSR: void setFrequency(unsigned char vfo, freqt freq); void getFrequency(); void setMode(unsigned char mode, unsigned char modeFilter); void setMode(mode_info); void getMode(); void setDataMode(bool dataOn, unsigned char filter); void getDataMode(); void getBandStackReg(char band, char regCode); void getRitEnabled(); void getRitValue(); void setRitValue(int ritValue); void setRitEnable(bool ritEnabled); // PTT, ATU, ATT, Antenna, and Preamp: void getPTT(); void setPTT(bool pttOn); void startATU(); void setATU(bool enabled); void getATUStatus(); void getAttenuator(); void getPreamp(); void getAntenna(); void setAttenuator(unsigned char att); void setPreamp(unsigned char pre); void setAntenna(unsigned char ant, bool rx); // Repeater: void setDuplexMode(duplexMode dm); void getDuplexMode(); void getTransmitFrequency(); void setTone(quint16 tone); void setTSQL(quint16 tsql); void getTSQL(); void getTone(); void setDTCS(quint16 dcscode, bool tinv, bool rinv); void getDTCS(); void setRptAccessMode(rptAccessTxRx ratr); void getRptAccessMode(); // Get Levels: void getLevels(); // all supported levels void getRfGain(); void getAfGain(); void getSql(); void getIFShift(); void getTPBFInner(); void getTPBFOuter(); void getTxLevel(); void getMicGain(); void getCompLevel(); void getMonitorLevel(); void getVoxGain(); void getAntiVoxGain(); void getUSBGain(); void getLANGain(); void getACCGain(); void getACCGain(unsigned char ab); void getModInput(bool dataOn); void getModInputLevel(rigInput input); // Set Levels: void setSquelch(unsigned char level); void setRfGain(unsigned char level); void setAfGain(unsigned char level); void setIFShift(unsigned char level); void setTPBFInner(unsigned char level); void setTPBFOuter(unsigned char level); void setTxPower(unsigned char power); void setMicGain(unsigned char gain); void setUSBGain(unsigned char gain); void setLANGain(unsigned char gain); void setACCGain(unsigned char gain); void setACCGain(unsigned char gain, unsigned char ab); void setCompLevel(unsigned char compLevel); void setMonitorLevel(unsigned char monitorLevel); void setVoxGain(unsigned char gain); void setAntiVoxGain(unsigned char gain); void setModInput(rigInput input, bool dataOn); void setModInputLevel(rigInput input, unsigned char level); // NB, NR, IP+: void setIPP(bool enabled); void getIPP(); // Maybe put some of these into a struct? // setReceiverDSPParam(dspParam param); //void getNRLevel(); //void getNREnabled(); //void getNBLevel(); //void getNBEnabled(); //void getNotchEnabled(); //void getNotchLevel(); //void setNotchEnabled(bool enabled); //void setNotchLevel(unsigned char level); // Meters: void getSMeter(); void getCenterMeter(); void getRFPowerMeter(); void getSWRMeter(); void getALCMeter(); void getCompReductionMeter(); void getVdMeter(); void getIDMeter(); void getMeters(meterKind meter); // all supported meters per transmit or receive // Rig ID and CIV: void getRigID(); void findRigs(); void setRigID(unsigned char rigID); void setCIVAddr(unsigned char civAddr); // Calibration: void getRefAdjustCourse(); void getRefAdjustFine(); void setRefAdjustCourse(unsigned char level); void setRefAdjustFine(unsigned char level); // Time and Date: void setTime(timekind t); void setDate(datekind d); void setUTCOffset(timekind t); // Satellite: void setSatelliteMode(bool enabled); void getSatelliteMode(); // UDP: void handleNewData(const QByteArray& data); void receiveAudioData(const audioPacket& data); void handleSerialPortError(const QString port, const QString errorText); void changeLatency(const quint16 value); void dataFromServer(QByteArray data); void receiveBaudRate(quint32 baudrate); // Speech: void sayFrequency(); void sayMode(); void sayAll(); // Housekeeping: void handleStatusUpdate(const QString text); void sendState(); void getDebug(); signals: // Communication: void commReady(); void haveSerialPortError(const QString port, const QString errorText); void haveStatusUpdate(const QString text); void dataForComm(const QByteArray &outData); void toggleRTS(bool rtsOn); // UDP: void haveChangeLatency(quint16 value); void haveDataForServer(QByteArray outData); void haveAudioData(audioPacket data); void initUdpHandler(); void haveSetVolume(unsigned char level); void haveBaudRate(quint32 baudrate); // Spectrum: void haveSpectrumData(QByteArray spectrum, double startFreq, double endFreq); // pass along data to UI void haveSpectrumBounds(); void haveScopeSpan(freqt span, bool isSub); void haveSpectrumMode(spectrumMode spectmode); void haveScopeEdge(char edge); void haveSpectrumRefLevel(int level); // Rig ID: void haveRigID(rigCapabilities rigCaps); void discoveredRigID(rigCapabilities rigCaps); // Frequency, Mode, data, and bandstack: void haveFrequency(freqt freqStruct); void haveMode(unsigned char mode, unsigned char filter); void haveDataMode(bool dataModeEnabled); void haveBandStackReg(freqt f, char mode, char filter, bool dataOn); void haveRitEnabled(bool ritEnabled); void haveRitFrequency(int ritHz); // Repeater: void haveDuplexMode(duplexMode); void haveRptAccessMode(rptAccessTxRx ratr); void haveTone(quint16 tone); void haveTSQL(quint16 tsql); void haveDTCS(quint16 dcscode, bool tinv, bool rinv); // Levels: void haveRfGain(unsigned char level); void haveAfGain(unsigned char level); void haveSql(unsigned char level); void haveTPBFInner(unsigned char level); void haveTPBFOuter(unsigned char level); void haveIFShift(unsigned char level); void haveTxPower(unsigned char level); void haveMicGain(unsigned char level); void haveCompLevel(unsigned char level); void haveMonitorLevel(unsigned char level); void haveVoxGain(unsigned char gain); void haveAntiVoxGain(unsigned char gain); // Modulation source and gain: void haveModInput(rigInput input, bool isData); void haveLANGain(unsigned char gain); void haveUSBGain(unsigned char gain); void haveACCGain(unsigned char gain, unsigned char ab); void haveModSrcGain(rigInput input, unsigned char gain); // Meters: void haveMeter(meterKind meter, unsigned char level); void haveSMeter(unsigned char level); void haveRFMeter(unsigned char level); void haveSWRMeter(unsigned char); void haveALCMeter(unsigned char); void haveCompMeter(unsigned char dbreduction); void haveVdMeter(unsigned char voltage); void haveIdMeter(unsigned char current); // Calibration: void haveRefAdjustCourse(unsigned char level); void haveRefAdjustFine(unsigned char level); // PTT and ATU: void havePTTStatus(bool pttOn); void haveATUStatus(unsigned char status); void haveAttenuator(unsigned char att); void havePreamp(unsigned char pre); void haveAntenna(unsigned char ant,bool rx); // Rig State void stateInfo(rigStateStruct* state); // Housekeeping: void getMoreDebug(); void finished(); private: void setup(); QByteArray stripData(const QByteArray &data, unsigned char cutPosition); void parseData(QByteArray data); // new data come here void parseCommand(); unsigned char bcdHexToUChar(unsigned char in); unsigned char bcdHexToUChar(unsigned char hundreds, unsigned char tensunits); unsigned int bcdHexToUInt(unsigned char hundreds, unsigned char tensunits); QByteArray bcdEncodeInt(unsigned int); void parseFrequency(); freqt parseFrequency(QByteArray data, unsigned char lastPosition); // supply index where Mhz is found QByteArray makeFreqPayload(double frequency); QByteArray makeFreqPayload(freqt freq); QByteArray encodeTone(quint16 tone, bool tinv, bool rinv); QByteArray encodeTone(quint16 tone); unsigned char convertNumberToHex(unsigned char num); quint16 decodeTone(QByteArray eTone); quint16 decodeTone(QByteArray eTone, bool &tinv, bool &rinv); void parseMode(); void parseSpectrum(); void parseWFData(); void parseSpectrumRefLevel(); void parseDetailedRegisters1A05(); void parseRegisters1A(); void parseRegister1B(); void parseRegisters1C(); void parseRegister16(); void parseRegister21(); void parseBandStackReg(); void parsePTT(); void parseATU(); void parseLevels(); // register 0x14 void sendLevelCmd(unsigned char levAddr, unsigned char level); QByteArray getLANAddr(); QByteArray getUSBAddr(); QByteArray getACCAddr(unsigned char ab); void setModInput(rigInput input, bool dataOn, bool isQuery); void sendDataOut(); void prepDataAndSend(QByteArray data); void debugMe(); void printHex(const QByteArray &pdata); void printHex(const QByteArray &pdata, bool printVert, bool printHoriz); mode_info createMode(mode_kind m, unsigned char reg, QString name); centerSpanData createScopeCenter(centerSpansType s, QString name); commHandler* comm = Q_NULLPTR; pttyHandler* ptty = Q_NULLPTR; udpHandler* udp=Q_NULLPTR; QThread* udpHandlerThread = Q_NULLPTR; void determineRigCaps(); QByteArray payloadIn; QByteArray echoPerfix; QByteArray replyPrefix; QByteArray genericReplyPrefix; QByteArray payloadPrefix; QByteArray payloadSuffix; QByteArray rigData; QByteArray spectrumLine; double spectrumStartFreq; double spectrumEndFreq; struct rigCapabilities rigCaps; rigStateStruct rigState; bool haveRigCaps; model_kind model; quint8 spectSeqMax; quint16 spectAmpMax; quint16 spectLenMax; spectrumMode oldScopeMode; bool usingNativeLAN; // indicates using OEM LAN connection (705,7610,9700,7850) bool lookingForRig; bool foundRig; double frequencyMhz; unsigned char civAddr; unsigned char incomingCIVAddr; // place to store the incoming CIV. bool pttAllowed; QString rigSerialPort; quint32 rigBaudRate; QString ip; int cport; int sport; int aport; QString username; QString password; QString serialPortError; }; #endif // RIGCOMMANDER_H wfview-1.2d/rigctld.cpp000066400000000000000000001251011415164626400151740ustar00rootroot00000000000000#include "rigctld.h" #include "logcategories.h" static struct { quint64 mode; const char* str; } mode_str[] = { { RIG_MODE_AM, "AM" }, { RIG_MODE_CW, "CW" }, { RIG_MODE_USB, "USB" }, { RIG_MODE_LSB, "LSB" }, { RIG_MODE_RTTY, "RTTY" }, { RIG_MODE_FM, "FM" }, { RIG_MODE_WFM, "WFM" }, { RIG_MODE_CWR, "CWR" }, { RIG_MODE_RTTYR, "RTTYR" }, { RIG_MODE_AMS, "AMS" }, { RIG_MODE_PKTLSB, "PKTLSB" }, { RIG_MODE_PKTUSB, "PKTUSB" }, { RIG_MODE_PKTFM, "PKTFM" }, { RIG_MODE_PKTFMN, "PKTFMN" }, { RIG_MODE_ECSSUSB, "ECSSUSB" }, { RIG_MODE_ECSSLSB, "ECSSLSB" }, { RIG_MODE_FAX, "FAX" }, { RIG_MODE_SAM, "SAM" }, { RIG_MODE_SAL, "SAL" }, { RIG_MODE_SAH, "SAH" }, { RIG_MODE_DSB, "DSB"}, { RIG_MODE_FMN, "FMN" }, { RIG_MODE_PKTAM, "PKTAM"}, { RIG_MODE_P25, "P25"}, { RIG_MODE_DSTAR, "D-STAR"}, { RIG_MODE_DPMR, "DPMR"}, { RIG_MODE_NXDNVN, "NXDN-VN"}, { RIG_MODE_NXDN_N, "NXDN-N"}, { RIG_MODE_DCR, "DCR"}, { RIG_MODE_AMN, "AMN"}, { RIG_MODE_PSK, "PSK"}, { RIG_MODE_PSKR, "PSKR"}, { RIG_MODE_C4FM, "C4FM"}, { RIG_MODE_SPEC, "SPEC"}, { RIG_MODE_NONE, "" }, }; rigCtlD::rigCtlD(QObject* parent) : QTcpServer(parent) { } rigCtlD::~rigCtlD() { qInfo(logRigCtlD()) << "closing rigctld"; } //void rigCtlD::receiveFrequency(freqt freq) //{ // emit setFrequency(0, freq); // emit setFrequency(0, freq); //} void rigCtlD::receiveStateInfo(rigStateStruct* state) { qInfo("Setting rig state"); rigState = state; } int rigCtlD::startServer(qint16 port) { if (!this->listen(QHostAddress::Any, port)) { qInfo(logRigCtlD()) << "could not start on port " << port; return -1; } else { qInfo(logRigCtlD()) << "started on port " << port; } return 0; } void rigCtlD::incomingConnection(qintptr socket) { rigCtlClient* client = new rigCtlClient(socket, rigCaps, rigState, this); connect(this, SIGNAL(onStopped()), client, SLOT(closeSocket())); } void rigCtlD::stopServer() { qInfo(logRigCtlD()) << "stopping server"; emit onStopped(); } void rigCtlD::receiveRigCaps(rigCapabilities caps) { qInfo(logRigCtlD()) << "Got rigcaps for:" << caps.modelName; this->rigCaps = caps; } rigCtlClient::rigCtlClient(int socketId, rigCapabilities caps, rigStateStruct* state, rigCtlD* parent) : QObject(parent) { commandBuffer.clear(); sessionId = socketId; rigCaps = caps; rigState = state; socket = new QTcpSocket(this); this->parent = parent; if (!socket->setSocketDescriptor(sessionId)) { qInfo(logRigCtlD()) << " error binding socket: " << sessionId; return; } connect(socket, SIGNAL(readyRead()), this, SLOT(socketReadyRead()), Qt::DirectConnection); connect(socket, SIGNAL(disconnected()), this, SLOT(socketDisconnected()), Qt::DirectConnection); connect(parent, SIGNAL(sendData(QString)), this, SLOT(sendData(QString)), Qt::DirectConnection); qInfo(logRigCtlD()) << " session connected: " << sessionId; } void rigCtlClient::socketReadyRead() { QByteArray data = socket->readAll(); commandBuffer.append(data); QString sep = "\n"; static int num = 0; bool longReply = false; char responseCode = 0; QStringList response; bool setCommand = false; if (commandBuffer.endsWith('\n')) { qDebug(logRigCtlD()) << sessionId << "command received" << commandBuffer; commandBuffer.chop(1); // Remove \n character if (commandBuffer.endsWith('\r')) { commandBuffer.chop(1); // Remove \n character } // We have a full line so process command. if (rigState == Q_NULLPTR) { qInfo(logRigCtlD()) << "no rigState!"; return; } if (commandBuffer[num] == ";" || commandBuffer[num] == "|" || commandBuffer[num] == ",") { sep = commandBuffer[num].toLatin1(); num++; } else if (commandBuffer[num] == "+") { longReply = true; sep = "\n"; num++; } else if (commandBuffer[num] == "#") { return; } else if (commandBuffer[num].toLower() == "q") { closeSocket(); return; } if (commandBuffer[num] == "\\") { num++; } QStringList command = commandBuffer.mid(num).split(" "); QMutexLocker locker(rigState->mutex); if (command[0] == 0xf0 || command[0] == "chk_vfo") { QString resp; if (longReply) { resp.append(QString("ChkVFO: ")); } resp.append(QString("%1").arg(rigState->currentVfo)); response.append(resp); } else if (command[0] == "dump_state") { // Currently send "fake" state information until I can work out what is required! response.append("1"); response.append(QString("%1").arg(rigCaps.rigctlModel)); response.append("0"); for (bandType band : rigCaps.bands) { response.append(generateFreqRange(band)); } response.append("0 0 0 0 0 0 0"); if (rigCaps.hasTransmit) { for (bandType band : rigCaps.bands) { response.append(generateFreqRange(band)); } } response.append("0 0 0 0 0 0 0"); response.append("0x1ff 1"); response.append("0x1ff 0"); response.append("0 0"); response.append("0x1e 2400"); response.append("0x2 500"); response.append("0x1 8000"); response.append("0x1 2400"); response.append("0x20 15000"); response.append("0x20 8000"); response.append("0x40 230000"); response.append("0 0"); response.append("9900"); response.append("9900"); response.append("10000"); response.append("0"); QString preamps=""; if (rigCaps.hasPreamp) { for (unsigned char pre : rigCaps.preamps) { if (pre == 0) continue; preamps.append(QString("%1 ").arg(pre*10)); } if (preamps.endsWith(" ")) preamps.chop(1); } else { preamps = "0"; } response.append(preamps); QString attens = ""; if (rigCaps.hasAttenuator) { for (unsigned char att : rigCaps.attenuators) { if (att == 0) continue; attens.append(QString("%1 ").arg(att,0,16)); } if (attens.endsWith(" ")) attens.chop(1); } else { attens = "0"; } response.append(attens); response.append("0xffffffffffffffff"); response.append("0xffffffffffffffff"); response.append("0xfffffffff7ffffff"); response.append("0xfffffff083ffffff"); response.append("0xffffffffffffffff"); response.append("0xffffffffffffffbf"); /* response.append("0x3effffff"); response.append("0x3effffff"); response.append("0x7fffffff"); response.append("0x7fffffff"); response.append("0x7fffffff"); response.append("0x7fffffff"); */ response.append("done"); } else if (command[0] == "f" || command[0] == "get_freq") { QString resp; if (longReply) { resp.append(QString("Frequency: ")); } if (rigState->currentVfo == 0) { resp.append(QString("%1").arg(rigState->vfoAFreq.Hz)); } else { resp.append(QString("%1").arg(rigState->vfoBFreq.Hz)); } response.append(resp); } else if (command[0] == "F" || command[0] == "set_freq") { setCommand = true; freqt freq; bool ok=false; double newFreq=0.0f; unsigned char vfo=0; if (command.length() == 2) { newFreq = command[1].toDouble(&ok); } else if (command.length() == 3) // Includes VFO { newFreq = command[2].toDouble(&ok); if (command[1] == "VFOB") { vfo = 1; } } if (ok) { freq.Hz = static_cast(newFreq); qDebug(logRigCtlD()) << QString("Set frequency: %1 (%2)").arg(freq.Hz).arg(command[1]); emit parent->setFrequency(vfo, freq); emit parent->setFrequency(vfo, freq); emit parent->setFrequency(vfo, freq); emit parent->setFrequency(vfo, freq); emit parent->setFrequency(vfo, freq); } } else if (command[0] == "1" || command[0] == "dump_caps") { response.append(QString("Caps dump for model: %1").arg(rigCaps.modelID)); response.append(QString("Model Name:\t%1").arg(rigCaps.modelName)); response.append(QString("Mfg Name:\tIcom")); response.append(QString("Backend version:\t0.1")); response.append(QString("Backend copyright:\t2021")); if (rigCaps.hasTransmit) { response.append(QString("Rig type:\tTransceiver")); } else { response.append(QString("Rig type:\tReceiver")); } if (rigCaps.hasPTTCommand) { response.append(QString("PTT type:\tRig capable")); } response.append(QString("DCD type:\tRig capable")); response.append(QString("Port type:\tNetwork link")); } else if (command[0] == "t" || command[0] == "get_ptt") { if (rigCaps.hasPTTCommand) { QString resp; if (longReply) { resp.append(QString("PTT: ")); } resp.append(QString("%1").arg(rigState->ptt)); response.append(resp); } else { responseCode = -1; } } else if (command.length() > 1 && (command[0] == "T" || command[0] == "set_ptt")) { setCommand = true; if (rigCaps.hasPTTCommand) { emit parent->setPTT(bool(command[1].toInt())); emit parent->setPTT(bool(command[1].toInt())); emit parent->setPTT(bool(command[1].toInt())); emit parent->setPTT(bool(command[1].toInt())); emit parent->setPTT(bool(command[1].toInt())); } else { responseCode = -1; } } else if (command[0] == "v" || command[0] == "get_vfo") { QString resp; if (longReply) { resp.append("VFO: "); } if (rigState->currentVfo == 0) { resp.append("VFOA"); } else { resp.append("VFOB"); } response.append(resp); } else if (command.length() > 1 && (command[0] == "V" || command[0] == "set_vfo")) { setCommand = true; if (command[1] == "?") { response.append("set_vfo: ?"); response.append("VFOA"); response.append("VFOB"); response.append("Sub"); response.append("Main"); response.append("MEM"); } else if (command[1] == "VFOB" || command[1] == "Sub") { emit parent->setVFO(1); } else { emit parent->setVFO(0); } } else if (command[0] == "s" || command[0] == "get_split_vfo") { if (longReply) { response.append(QString("Split: %1").arg(rigState->duplex)); } else { response.append(QString("%1").arg(rigState->duplex)); } QString resp; if (longReply) { resp.append("TX VFO: "); } if (rigState->currentVfo == 0) { resp.append(QString("%1").arg("VFOB")); } else { resp.append(QString("%1").arg("VFOA")); } response.append(resp); } else if (command.length() > 1 && (command[0] == "S" || command[0] == "set_split_vfo")) { setCommand = true; if (command[1] == "1") { emit parent->setDuplexMode(dmSplitOn); rigState->duplex = dmSplitOn; } else { emit parent->setDuplexMode(dmSplitOff); rigState->duplex = dmSplitOff; } } else if (command[0] == "\xf3" || command[0] == "get_vfo_info") { if (longReply) { //response.append(QString("set_vfo: %1").arg(command[1])); if (command[1] == "VFOB") { response.append(QString("Freq: %1").arg(rigState->vfoBFreq.Hz)); } else { response.append(QString("Freq: %1").arg(rigState->vfoAFreq.Hz)); } response.append(QString("Mode: %1").arg(getMode(rigState->mode, rigState->datamode))); response.append(QString("Width: %1").arg(getFilter(rigState->mode, rigState->filter))); response.append(QString("Split: %1").arg(rigState->duplex)); response.append(QString("SatMode: %1").arg(0)); // Need to get satmode } else { if (command[1] == "VFOB") { response.append(QString("%1").arg(rigState->vfoBFreq.Hz)); } else { response.append(QString("%1").arg(rigState->vfoAFreq.Hz)); } response.append(QString("%1").arg(getMode(rigState->mode, rigState->datamode))); response.append(QString("%1").arg(getFilter(rigState->mode, rigState->filter))); } } else if (command[0] == "i" || command[0] == "get_split_freq") { QString resp; if (longReply) { resp.append("TX VFO: "); } if (rigState->currentVfo == 0) { resp.append(QString("%1").arg(rigState->vfoBFreq.Hz)); } else { resp.append(QString("%1").arg(rigState->vfoAFreq.Hz)); } response.append(resp); } else if (command.length() > 1 && (command[0] == "I" || command[0] == "set_split_freq")) { setCommand = true; freqt freq; bool ok = false; double newFreq = 0.0f; newFreq = command[1].toDouble(&ok); if (ok) { freq.Hz = static_cast(newFreq); qDebug(logRigCtlD()) << QString("set_split_freq: %1 (%2)").arg(freq.Hz).arg(command[1]); emit parent->setFrequency(1, freq); emit parent->setFrequency(1, freq); emit parent->setFrequency(1, freq); emit parent->setFrequency(1, freq); emit parent->setFrequency(1, freq); } } else if (command.length() > 2 && (command[0] == "X" || command[0] == "set_split_mode")) { setCommand = true; } else if (command.length() > 0 && (command[0] == "x" || command[0] == "get_split_mode")) { if (longReply) { response.append(QString("TX Mode: %1").arg(getMode(rigState->mode, rigState->datamode))); response.append(QString("TX Passband: %1").arg(getFilter(rigState->mode, rigState->filter))); } else { response.append(QString("%1").arg(getMode(rigState->mode, rigState->datamode))); response.append(QString("%1").arg(getFilter(rigState->mode, rigState->filter))); } } else if (command[0] == "m" || command[0] == "get_mode") { if (longReply) { response.append(QString("Mode: %1").arg(getMode(rigState->mode, rigState->datamode))); response.append(QString("Passband: %1").arg(getFilter(rigState->mode, rigState->filter))); } else { response.append(QString("%1").arg(getMode(rigState->mode, rigState->datamode))); response.append(QString("%1").arg(getFilter(rigState->mode, rigState->filter))); } } else if (command[0] == "M" || command[0] == "set_mode") { // Set mode setCommand = true; int width = -1; QString vfo = "VFOA"; QString mode = "USB"; if (command.length() == 3) { width = command[2].toInt(); mode = command[1]; } else if (command.length() == 4) { width = command[3].toInt(); mode = command[2]; vfo = command[1]; } qDebug(logRigCtlD()) << "setting mode: VFO:" << vfo << getMode(mode) << mode << "width" << width; if (width != -1 && width <= 1800) width = 2; else width = 1; emit parent->setMode(getMode(mode), width); if (mode.mid(0, 3) == "PKT") { emit parent->setDataMode(true, width); emit parent->setDataMode(true, width); } else { emit parent->setDataMode(false, width); emit parent->setDataMode(false, width); } } else if (command[0] == "s" || command[0] == "get_split_vfo") { if (longReply) { response.append(QString("Split: 1")); response.append(QString("TX VFO: VFOB")); } else { response.append("1"); response.append("VFOb"); } } else if (command[0] == "j" || command[0] == "get_rit") { QString resp; if (longReply) { resp.append("RIT: "); } resp.append(QString("%1").arg(0)); response.append(resp); } else if (command[0] == "J" || command[0] == "set_rit") { setCommand = true; } else if (command[0] == "y" || command[0] == "get_ant") { qInfo(logRigCtlD()) << "get_ant:"; if (command.length() > 1) { if (longReply) { response.append(QString("AntCurr: %1").arg(getAntName((unsigned char)command[1].toInt()))); response.append(QString("Option: %1").arg(0)); response.append(QString("AntTx: %1").arg(getAntName(rigState->antenna))); response.append(QString("AntRx: %1").arg(getAntName(rigState->antenna))); } else { response.append(QString("%1").arg(getAntName((unsigned char)command[1].toInt()))); response.append(QString("%1").arg(0)); response.append(QString("%1").arg(getAntName(rigState->antenna))); response.append(QString("%1").arg(getAntName(rigState->antenna))); } } } else if (command[0] == "Y" || command[0] == "set_ant") { qInfo(logRigCtlD()) << "set_ant:"; setCommand = true; } else if (command[0] == "z" || command[0] == "get_xit") { QString resp; if (longReply) { resp.append("XIT: "); } resp.append(QString("%1").arg(0)); response.append(resp); } else if (command[0] == "Z" || command[0] == "set_xit") { setCommand = true; } else if (command.length() > 1 && (command[0] == "l" || command[0] == "get_level")) { QString resp; int value = 0; if (longReply) { resp.append("Level Value: "); } if (command[1] == "STRENGTH") { if (rigCaps.model == model7610) value = getCalibratedValue(rigState->sMeter, IC7610_STR_CAL); else if (rigCaps.model == model7850) value = getCalibratedValue(rigState->sMeter, IC7850_STR_CAL); else value = getCalibratedValue(rigState->sMeter, IC7300_STR_CAL); //qInfo(logRigCtlD()) << "Calibration IN:" << rigState->sMeter << "OUT" << value; resp.append(QString("%1").arg(value)); } else if (command[1] == "AF") { resp.append(QString("%1").arg((float)rigState->afGain / 255.0)); } else if (command[1] == "RF") { resp.append(QString("%1").arg((float)rigState->rfGain / 255.0)); } else if (command[1] == "SQL") { resp.append(QString("%1").arg((float)rigState->squelch / 255.0)); } else if (command[1] == "COMP") { resp.append(QString("%1").arg((float)rigState->compLevel / 255.0)); } else if (command[1] == "MICGAIN") { resp.append(QString("%1").arg((float)rigState->micGain / 255.0)); } else if (command[1] == "MON") { resp.append(QString("%1").arg((float)rigState->monitorLevel / 255.0)); } else if (command[1] == "VOXGAIN") { resp.append(QString("%1").arg((float)rigState->voxGain / 255.0)); } else if (command[1] == "ANTIVOX") { resp.append(QString("%1").arg((float)rigState->antiVoxGain / 255.0)); } else if (command[1] == "RFPOWER") { resp.append(QString("%1").arg((float)rigState->txPower / 255.0)); } else if (command[1] == "PREAMP") { resp.append(QString("%1").arg((float)rigState->preamp / 255.0)); } else if (command[1] == "ATT") { resp.append(QString("%1").arg((float)rigState->attenuator / 255.0)); } else { resp.append(QString("%1").arg(value)); } response.append(resp); } else if (command.length() > 2 && (command[0] == "L" || command[0] == "set_level")) { unsigned char value=0; setCommand = true; if (command[1] == "AF") { value = command[2].toFloat() * 255; emit parent->setAfGain(value); rigState->afGain = value; } else if (command[1] == "RF") { value = command[2].toFloat() * 255; emit parent->setRfGain(value); rigState->rfGain = value; } else if (command[1] == "SQL") { value = command[2].toFloat() * 255; emit parent->setSql(value); rigState->squelch = value; } else if (command[1] == "COMP") { value = command[2].toFloat() * 255; emit parent->setCompLevel(value); rigState->compLevel = value; } else if (command[1] == "MICGAIN") { value = command[2].toFloat() * 255; emit parent->setMicGain(value); rigState->micGain = value; } else if (command[1] == "MON") { value = command[2].toFloat() * 255; emit parent->setMonitorLevel(value); rigState->monitorLevel = value; } else if (command[1] == "VOXGAIN") { value = command[2].toFloat() * 255; emit parent->setVoxGain(value); rigState->voxGain = value; } else if (command[1] == "ANTIVOX") { value = command[2].toFloat() * 255; emit parent->setAntiVoxGain(value); rigState->antiVoxGain = value; } else if (command[1] == "ATT") { value = command[2].toFloat(); emit parent->setAttenuator(value); rigState->attenuator = value; } else if (command[1] == "PREAMP") { value = command[2].toFloat()/10; emit parent->setPreamp(value); rigState->preamp = value; } qInfo(logRigCtlD()) << "Setting:" << command[1] << command[2] << value; } else if (command.length()>1 && (command[0] == "u" || command[0] == "get_func")) { QString resp=""; bool result = 0; if (longReply) { resp.append(QString("Func Status: ")); } if (command[1] == "FAGC") { result=rigState->fagcFunc; } else if (command[1] == "NB") { result=rigState->nbFunc; } else if (command[1] == "COMP") { result=rigState->compFunc; } else if (command[1] == "VOX") { result = rigState->voxFunc; } else if (command[1] == "TONE") { result = rigState->toneFunc; } else if (command[1] == "TSQL") { result = rigState->tsqlFunc; } else if (command[1] == "SBKIN") { result = rigState->sbkinFunc; } else if (command[1] == "FBKIN") { result = rigState->fbkinFunc; } else if (command[1] == "ANF") { result = rigState->anfFunc; } else if (command[1] == "NR") { result = rigState->nrFunc; } else if (command[1] == "AIP") { result = rigState->aipFunc; } else if (command[1] == "APF") { result = rigState->apfFunc; } else if (command[1] == "MON") { result = rigState->monFunc; } else if (command[1] == "MN") { result = rigState->mnFunc; } else if (command[1] == "RF") { result = rigState->rfFunc; } else if (command[1] == "ARO") { result = rigState->aroFunc; } else if (command[1] == "MUTE") { result = rigState->muteFunc; } else if (command[1] == "VSC") { result = rigState->vscFunc; } else if (command[1] == "REV") { result = rigState->revFunc; } else if (command[1] == "SQL") { result = rigState->sqlFunc; } else if (command[1] == "ABM") { result = rigState->abmFunc; } else if (command[1] == "BC") { result = rigState->bcFunc; } else if (command[1] == "MBC") { result = rigState->mbcFunc; } else if (command[1] == "RIT") { result = rigState->ritFunc; } else if (command[1] == "AFC") { result = rigState->afcFunc; } else if (command[1] == "SATMODE") { result = rigState->satmodeFunc; } else if (command[1] == "SCOPE") { result = rigState->scopeFunc; } else if (command[1] == "RESUME") { result = rigState->resumeFunc; } else if (command[1] == "TBURST") { result = rigState->tburstFunc; } else if (command[1] == "TUNER") { result = rigState->tunerFunc; } else if (command[1] == "LOCK") { result = rigState->lockFunc; } else { qInfo(logRigCtlD()) << "Unimplemented func:" << command[0] << command[1]; } resp.append(QString("%1").arg(result)); response.append(resp); } else if (command.length() >2 && (command[0] == "U" || command[0] == "set_func")) { setCommand = true; if (command[1] == "FAGC") { rigState->fagcFunc = (bool)command[2].toInt(); } else if (command[1] == "NB") { rigState->nbFunc = (bool)command[2].toInt(); } else if (command[1] == "COMP") { rigState->compFunc = (bool)command[2].toInt(); } else if (command[1] == "VOX") { rigState->voxFunc = (bool)command[2].toInt(); } else if (command[1] == "TONE") { rigState->toneFunc = (bool)command[2].toInt(); } else if (command[1] == "TSQL") { rigState->tsqlFunc = (bool)command[2].toInt(); } else if (command[1] == "SBKIN") { rigState->sbkinFunc = (bool)command[2].toInt(); } else if (command[1] == "FBKIN") { rigState->fbkinFunc = (bool)command[2].toInt(); } else if (command[1] == "ANF") { rigState->anfFunc = (bool)command[2].toInt(); } else if (command[1] == "NR") { rigState->nrFunc = (bool)command[2].toInt(); } else if (command[1] == "AIP") { rigState->aipFunc = (bool)command[2].toInt(); } else if (command[1] == "APF") { rigState->apfFunc = (bool)command[2].toInt(); } else if (command[1] == "MON") { rigState->monFunc = (bool)command[2].toInt(); } else if (command[1] == "MN") { rigState->mnFunc = (bool)command[2].toInt(); } else if (command[1] == "RF") { rigState->rfFunc = (bool)command[2].toInt(); } else if (command[1] == "ARO") { rigState->aroFunc = (bool)command[2].toInt(); } else if (command[1] == "MUTE") { rigState->muteFunc = (bool)command[2].toInt(); } else if (command[1] == "VSC") { rigState->vscFunc = (bool)command[2].toInt(); } else if (command[1] == "REV") { rigState->revFunc = (bool)command[2].toInt(); } else if (command[1] == "SQL") { rigState->sqlFunc = (bool)command[2].toInt(); } else if (command[1] == "ABM") { rigState->abmFunc = (bool)command[2].toInt(); } else if (command[1] == "BC") { rigState->bcFunc = (bool)command[2].toInt(); } else if (command[1] == "MBC") { rigState->mbcFunc = (bool)command[2].toInt(); } else if (command[1] == "RIT") { rigState->ritFunc = (bool)command[2].toInt(); } else if (command[1] == "AFC") { rigState->afcFunc = (bool)command[2].toInt(); } else if (command[1] == "SATMODE") { rigState->satmodeFunc = (bool)command[2].toInt(); } else if (command[1] == "SCOPE") { rigState->scopeFunc = (bool)command[2].toInt(); } else if (command[1] == "RESUME") { rigState->resumeFunc = (bool)command[2].toInt(); } else if (command[1] == "TBURST") { rigState->tburstFunc = (bool)command[2].toInt(); } else if (command[1] == "TUNER") { rigState->tunerFunc = (bool)command[2].toInt(); } else if (command[1] == "LOCK") { rigState->lockFunc = (bool)command[2].toInt(); } else { qInfo(logRigCtlD()) << "Unimplemented func:" << command[0] << command[1] << command[2]; } qInfo(logRigCtlD()) << "Setting:" << command[1] << command[2]; } else if (command.length() > 1 && (command[0] == 0x88 || command[0] == "get_powerstat")) { QString resp; if (longReply && command.length() > 1) { resp.append(QString("Power Status: ")); } resp.append(QString("%1").arg(1)); // Always reply with ON response.append(resp); } else if (command.length() > 1 && (command[0] == 0x87 || command[0] == "set_powerstat")) { setCommand = true; if (command[1] == "0") { emit parent->sendPowerOff(); } else { emit parent->sendPowerOn(); } } else { qInfo(logRigCtlD()) << "Unimplemented command" << commandBuffer; } if (longReply) { if (command.length() == 2) sendData(QString("%1: %2%3").arg(command[0]).arg(command[1]).arg(sep)); if (command.length() == 3) sendData(QString("%1: %2 %3%4").arg(command[0]).arg(command[1]).arg(command[2]).arg(sep)); if (command.length() == 4) sendData(QString("%1: %2 %3 %4%5").arg(command[0]).arg(command[1]).arg(command[2]).arg(command[3]).arg(sep)); } if (setCommand || responseCode != 0 || longReply) { if (responseCode == 0) { response.append(QString("RPRT 0")); } else { response.append(QString("RPRT %1").arg(responseCode)); } } for (QString str : response) { if (str != "") sendData(QString("%1%2").arg(str).arg(sep)); } if (sep != "\n") { sendData(QString("\n")); } commandBuffer.clear(); sep = " "; num = 0; } } void rigCtlClient::socketDisconnected() { qInfo(logRigCtlD()) << sessionId << "disconnected"; socket->deleteLater(); this->deleteLater(); } void rigCtlClient::closeSocket() { socket->close(); } void rigCtlClient::sendData(QString data) { qDebug(logRigCtlD()) << "Sending:" << data; if (socket != Q_NULLPTR && socket->isValid() && socket->isOpen()) { socket->write(data.toLatin1()); } else { qInfo(logRigCtlD()) << "socket not open!"; } } QString rigCtlClient::getFilter(unsigned char mode, unsigned char filter) { if (mode == 3 || mode == 7 || mode == 12 || mode == 17) { switch (filter) { case 1: return QString("1200"); case 2: return QString("500"); case 3: return QString("250"); } } else if (mode == 4 || mode == 8) { switch (filter) { case 1: return QString("2400"); case 2: return QString("500"); case 3: return QString("250"); } } else if (mode == 2) { switch (filter) { case 1: return QString("9000"); case 2: return QString("6000"); case 3: return QString("3000"); } } else if (mode == 5) { switch (filter) { case 1: return QString("15000"); case 2: return QString("10000"); case 3: return QString("7000"); } } else { // SSB or unknown mode switch (filter) { case 1: return QString("3000"); case 2: return QString("2400"); case 3: return QString("1800"); } } return QString(""); } QString rigCtlClient::getMode(unsigned char mode, bool datamode) { QString ret; if (datamode) { ret="PKT"; } switch (mode) { case 0: ret.append("LSB"); break; case 1: ret.append("USB"); break; case 2: ret.append("AM"); break; case 3: ret.append("CW"); break; case 4: ret.append("RTTY"); break; case 5: ret.append("FM"); break; case 6: ret.append("WFM"); break; case 7: ret.append("CWR"); break; case 8: ret.append("RTTYR"); break; case 12: ret.append("USB"); break; case 17: ret.append("LSB"); break; case 22: ret.append("FM"); break; } return ret; } unsigned char rigCtlClient::getMode(QString modeString) { if (modeString == QString("LSB")) { return 0; } else if (modeString == QString("USB")) { return 1; } else if (modeString == QString("AM")) { return 2; } else if (modeString == QString("CW")) { return 3; } else if (modeString == QString("RTTY")) { return 4; } else if (modeString == QString("FM")) { return 5; } else if (modeString == QString("WFM")) { return 6; } else if (modeString == QString("CWR")) { return 7; } else if (modeString == QString("RTTYR")) { return 8; } else if (modeString == QString("PKTUSB")) { return 1; } else if (modeString == QString("PKTLSB")) { return 0; } else if (modeString == QString("PKTFM")) { return 22; } else { return 0; } return 0; } QString rigCtlClient::generateFreqRange(bandType band) { unsigned int lowFreq = 0; unsigned int highFreq = 0; switch (band) { case band2200m: lowFreq = 135000; highFreq = 138000; break; case band630m: lowFreq = 493000; highFreq = 595000; break; case band160m: lowFreq = 1800000; highFreq = 2000000; break; case band80m: lowFreq = 3500000; highFreq = 4000000; break; case band60m: lowFreq = 5250000; highFreq = 5450000; break; case band40m: lowFreq = 7000000; highFreq = 7300000; break; case band30m: lowFreq = 10100000; highFreq = 10150000; break; case band20m: lowFreq = 14000000; highFreq = 14350000; break; case band17m: lowFreq = 18068000; highFreq = 18168000; break; case band15m: lowFreq = 21000000; highFreq = 21450000; break; case band12m: lowFreq = 24890000; highFreq = 24990000; break; case band10m: lowFreq = 28000000; highFreq = 29700000; break; case band6m: lowFreq = 50000000; highFreq = 54000000; break; case band4m: lowFreq = 70000000; highFreq = 70500000; break; case band2m: lowFreq = 144000000; highFreq = 148000000; break; case band70cm: lowFreq = 420000000; highFreq = 450000000; break; case band23cm: lowFreq = 1240000000; highFreq = 1400000000; break; case bandAir: lowFreq = 108000000; highFreq = 137000000; break; case bandWFM: lowFreq = 88000000; highFreq = 108000000; break; case bandGen: lowFreq = 10000; highFreq = 30000000; break; } QString ret = ""; if (lowFreq > 0 && highFreq > 0) { ret = QString("%1 %2 0x%3 %4 %5 0x%6 0x%7").arg(lowFreq).arg(highFreq).arg(getRadioModes(),0,16).arg(-1).arg(-1).arg(0x16000003,0,16).arg(getAntennas(),0,16); } return ret; } unsigned char rigCtlClient::getAntennas() { unsigned char ant=0; for (unsigned char i : rigCaps.antennas) { ant |= 1<= cal.size) { return cal.table[i - 1].val; } else if (cal.table[i].raw == cal.table[i - 1].raw) { return cal.table[i].val; } interp = ((cal.table[i].raw - meter) * (cal.table[i].val - cal.table[i - 1].val)) / (cal.table[i].raw - cal.table[i - 1].raw); return cal.table[i].val - interp; } wfview-1.2d/rigctld.h000066400000000000000000000531471415164626400146530ustar00rootroot00000000000000/* This file contains portions of the Hamlib Interface - API header * Copyright(c) 2000 - 2003 by Frank Singleton * Copyright(c) 2000 - 2012 by Stephane Fillod */ #ifndef RIGCTLD_H #define RIGCTLD_H #include #include #include #include #include #include #include #include #include #include "rigcommander.h" #define CONSTANT_64BIT_FLAG(BIT) (1ull << (BIT)) #define RIG_MODE_NONE 0 /*!< '' -- None */ #define RIG_MODE_AM CONSTANT_64BIT_FLAG (0) /*!< \c AM -- Amplitude Modulation */ #define RIG_MODE_CW CONSTANT_64BIT_FLAG (1) /*!< \c CW -- CW "normal" sideband */ #define RIG_MODE_USB CONSTANT_64BIT_FLAG (2) /*!< \c USB -- Upper Side Band */ #define RIG_MODE_LSB CONSTANT_64BIT_FLAG (3) /*!< \c LSB -- Lower Side Band */ #define RIG_MODE_RTTY CONSTANT_64BIT_FLAG (4) /*!< \c RTTY -- Radio Teletype */ #define RIG_MODE_FM CONSTANT_64BIT_FLAG (5) /*!< \c FM -- "narrow" band FM */ #define RIG_MODE_WFM CONSTANT_64BIT_FLAG (6) /*!< \c WFM -- broadcast wide FM */ #define RIG_MODE_CWR CONSTANT_64BIT_FLAG (7) /*!< \c CWR -- CW "reverse" sideband */ #define RIG_MODE_RTTYR CONSTANT_64BIT_FLAG (8) /*!< \c RTTYR -- RTTY "reverse" sideband */ #define RIG_MODE_AMS CONSTANT_64BIT_FLAG (9) /*!< \c AMS -- Amplitude Modulation Synchronous */ #define RIG_MODE_PKTLSB CONSTANT_64BIT_FLAG (10) /*!< \c PKTLSB -- Packet/Digital LSB mode (dedicated port) */ #define RIG_MODE_PKTUSB CONSTANT_64BIT_FLAG (11) /*!< \c PKTUSB -- Packet/Digital USB mode (dedicated port) */ #define RIG_MODE_PKTFM CONSTANT_64BIT_FLAG (12) /*!< \c PKTFM -- Packet/Digital FM mode (dedicated port) */ #define RIG_MODE_ECSSUSB CONSTANT_64BIT_FLAG (13) /*!< \c ECSSUSB -- Exalted Carrier Single Sideband USB */ #define RIG_MODE_ECSSLSB CONSTANT_64BIT_FLAG (14) /*!< \c ECSSLSB -- Exalted Carrier Single Sideband LSB */ #define RIG_MODE_FAX CONSTANT_64BIT_FLAG (15) /*!< \c FAX -- Facsimile Mode */ #define RIG_MODE_SAM CONSTANT_64BIT_FLAG (16) /*!< \c SAM -- Synchronous AM double sideband */ #define RIG_MODE_SAL CONSTANT_64BIT_FLAG (17) /*!< \c SAL -- Synchronous AM lower sideband */ #define RIG_MODE_SAH CONSTANT_64BIT_FLAG (18) /*!< \c SAH -- Synchronous AM upper (higher) sideband */ #define RIG_MODE_DSB CONSTANT_64BIT_FLAG (19) /*!< \c DSB -- Double sideband suppressed carrier */ #define RIG_MODE_FMN CONSTANT_64BIT_FLAG (21) /*!< \c FMN -- FM Narrow Kenwood ts990s */ #define RIG_MODE_PKTAM CONSTANT_64BIT_FLAG (22) /*!< \c PKTAM -- Packet/Digital AM mode e.g. IC7300 */ #define RIG_MODE_P25 CONSTANT_64BIT_FLAG (23) /*!< \c P25 -- APCO/P25 VHF,UHF digital mode IC-R8600 */ #define RIG_MODE_DSTAR CONSTANT_64BIT_FLAG (24) /*!< \c D-Star -- VHF,UHF digital mode IC-R8600 */ #define RIG_MODE_DPMR CONSTANT_64BIT_FLAG (25) /*!< \c dPMR -- digital PMR, VHF,UHF digital mode IC-R8600 */ #define RIG_MODE_NXDNVN CONSTANT_64BIT_FLAG (26) /*!< \c NXDN-VN -- VHF,UHF digital mode IC-R8600 */ #define RIG_MODE_NXDN_N CONSTANT_64BIT_FLAG (27) /*!< \c NXDN-N -- VHF,UHF digital mode IC-R8600 */ #define RIG_MODE_DCR CONSTANT_64BIT_FLAG (28) /*!< \c DCR -- VHF,UHF digital mode IC-R8600 */ #define RIG_MODE_AMN CONSTANT_64BIT_FLAG (29) /*!< \c AM-N -- Narrow band AM mode IC-R30 */ #define RIG_MODE_PSK CONSTANT_64BIT_FLAG (30) /*!< \c PSK - Kenwood PSK and others */ #define RIG_MODE_PSKR CONSTANT_64BIT_FLAG (31) /*!< \c PSKR - Kenwood PSKR and others */ #define RIG_MODE_DD CONSTANT_64BIT_FLAG (32) /*!< \c DD Mode IC-9700 */ #define RIG_MODE_C4FM CONSTANT_64BIT_FLAG (33) /*!< \c Yaesu C4FM mode */ #define RIG_MODE_PKTFMN CONSTANT_64BIT_FLAG (34) /*!< \c Yaesu DATA-FM-N */ #define RIG_MODE_SPEC CONSTANT_64BIT_FLAG (35) /*!< \c Unfiltered as in PowerSDR */ #define RIG_LEVEL_NONE 0 /*!< '' -- No Level */ #define RIG_LEVEL_PREAMP CONSTANT_64BIT_FLAG(0) /*!< \c PREAMP -- Preamp, arg int (dB) */ #define RIG_LEVEL_ATT CONSTANT_64BIT_FLAG(1) /*!< \c ATT -- Attenuator, arg int (dB) */ #define RIG_LEVEL_VOXDELAY CONSTANT_64BIT_FLAG(2) /*!< \c VOXDELAY -- VOX delay, arg int (tenth of seconds) */ #define RIG_LEVEL_AF CONSTANT_64BIT_FLAG(3) /*!< \c AF -- Volume, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_RF CONSTANT_64BIT_FLAG(4) /*!< \c RF -- RF gain (not TX power) arg float [0.0 ... 1.0] */ #define RIG_LEVEL_SQL CONSTANT_64BIT_FLAG(5) /*!< \c SQL -- Squelch, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_IF CONSTANT_64BIT_FLAG(6) /*!< \c IF -- IF, arg int (Hz) */ #define RIG_LEVEL_APF CONSTANT_64BIT_FLAG(7) /*!< \c APF -- Audio Peak Filter, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_NR CONSTANT_64BIT_FLAG(8) /*!< \c NR -- Noise Reduction, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_PBT_IN CONSTANT_64BIT_FLAG(9) /*!< \c PBT_IN -- Twin PBT (inside) arg float [0.0 ... 1.0] */ #define RIG_LEVEL_PBT_OUT CONSTANT_64BIT_FLAG(10) /*!< \c PBT_OUT -- Twin PBT (outside) arg float [0.0 ... 1.0] */ #define RIG_LEVEL_CWPITCH CONSTANT_64BIT_FLAG(11) /*!< \c CWPITCH -- CW pitch, arg int (Hz) */ #define RIG_LEVEL_RFPOWER CONSTANT_64BIT_FLAG(12) /*!< \c RFPOWER -- RF Power, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_MICGAIN CONSTANT_64BIT_FLAG(13) /*!< \c MICGAIN -- MIC Gain, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_KEYSPD CONSTANT_64BIT_FLAG(14) /*!< \c KEYSPD -- Key Speed, arg int (WPM) */ #define RIG_LEVEL_NOTCHF CONSTANT_64BIT_FLAG(15) /*!< \c NOTCHF -- Notch Freq., arg int (Hz) */ #define RIG_LEVEL_COMP CONSTANT_64BIT_FLAG(16) /*!< \c COMP -- Compressor, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_AGC CONSTANT_64BIT_FLAG(17) /*!< \c AGC -- AGC, arg int (see enum agc_level_e) */ #define RIG_LEVEL_BKINDL CONSTANT_64BIT_FLAG(18) /*!< \c BKINDL -- BKin Delay, arg int (tenth of dots) */ #define RIG_LEVEL_BALANCE CONSTANT_64BIT_FLAG(19) /*!< \c BAL -- Balance (Dual Watch) arg float [0.0 ... 1.0] */ #define RIG_LEVEL_METER CONSTANT_64BIT_FLAG(20) /*!< \c METER -- Display meter, arg int (see enum meter_level_e) */ #define RIG_LEVEL_VOXGAIN CONSTANT_64BIT_FLAG(21) /*!< \c VOXGAIN -- VOX gain level, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_ANTIVOX CONSTANT_64BIT_FLAG(22) /*!< \c ANTIVOX -- anti-VOX level, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_SLOPE_LOW CONSTANT_64BIT_FLAG(23) /*!< \c SLOPE_LOW -- Slope tune, low frequency cut, arg int (Hz) */ #define RIG_LEVEL_SLOPE_HIGH CONSTANT_64BIT_FLAG(24) /*!< \c SLOPE_HIGH -- Slope tune, high frequency cut, arg int (Hz) */ #define RIG_LEVEL_BKIN_DLYMS CONSTANT_64BIT_FLAG(25) /*!< \c BKIN_DLYMS -- BKin Delay, arg int Milliseconds */ /*!< These are not settable */ #define RIG_LEVEL_RAWSTR CONSTANT_64BIT_FLAG(26) /*!< \c RAWSTR -- Raw (A/D) value for signal strength, specific to each rig, arg int */ //#define RIG_LEVEL_SQLSTAT CONSTANT_64BIT_FLAG(27) /*!< \c SQLSTAT -- SQL status, arg int (open=1/closed=0). Deprecated, use get_dcd instead */ #define RIG_LEVEL_SWR CONSTANT_64BIT_FLAG(28) /*!< \c SWR -- SWR, arg float [0.0 ... infinite] */ #define RIG_LEVEL_ALC CONSTANT_64BIT_FLAG(29) /*!< \c ALC -- ALC, arg float */ #define RIG_LEVEL_STRENGTH CONSTANT_64BIT_FLAG(30) /*!< \c STRENGTH -- Effective (calibrated) signal strength relative to S9, arg int (dB) */ /* RIG_LEVEL_BWC (1<<31) */ /*!< Bandwidth Control, arg int (Hz) */ #define RIG_LEVEL_RFPOWER_METER CONSTANT_64BIT_FLAG(32) /*!< \c RFPOWER_METER -- RF power output meter, arg float [0.0 ... 1.0] (percentage of maximum power) */ #define RIG_LEVEL_COMP_METER CONSTANT_64BIT_FLAG(33) /*!< \c COMP_METER -- Audio output level compression meter, arg float (dB) */ #define RIG_LEVEL_VD_METER CONSTANT_64BIT_FLAG(34) /*!< \c VD_METER -- Input voltage level meter, arg float (V, volts) */ #define RIG_LEVEL_ID_METER CONSTANT_64BIT_FLAG(35) /*!< \c ID_METER -- Current draw meter, arg float (A, amperes) */ #define RIG_LEVEL_NOTCHF_RAW CONSTANT_64BIT_FLAG(36) /*!< \c NOTCHF_RAW -- Notch Freq., arg float [0.0 ... 1.0] */ #define RIG_LEVEL_MONITOR_GAIN CONSTANT_64BIT_FLAG(37) /*!< \c MONITOR_GAIN -- Monitor gain (level for monitoring of transmitted audio) arg float [0.0 ... 1.0] */ #define RIG_LEVEL_NB CONSTANT_64BIT_FLAG(38) /*!< \c NB -- Noise Blanker level, arg float [0.0 ... 1.0] */ #define RIG_LEVEL_RFPOWER_METER_WATTS CONSTANT_64BIT_FLAG(39) /*!< \c RFPOWER_METER_WATTS -- RF power output meter, arg float [0.0 ... MAX] (output power in watts) */ #define RIG_LEVEL_SPECTRUM_MODE CONSTANT_64BIT_FLAG(40) /*!< \c SPECTRUM_MODE -- Spectrum scope mode, arg int (see enum rig_spectrum_mode_e). Supported modes defined in rig caps. */ #define RIG_LEVEL_SPECTRUM_SPAN CONSTANT_64BIT_FLAG(41) /*!< \c SPECTRUM_SPAN -- Spectrum scope span in center mode, arg int (Hz). Supported spans defined in rig caps. */ #define RIG_LEVEL_SPECTRUM_EDGE_LOW CONSTANT_64BIT_FLAG(42) /*!< \c SPECTRUM_EDGE_LOW -- Spectrum scope low edge in fixed mode, arg int (Hz) */ #define RIG_LEVEL_SPECTRUM_EDGE_HIGH CONSTANT_64BIT_FLAG(43) /*!< \c SPECTRUM_EDGE_HIGH -- Spectrum scope high edge in fixed mode, arg int (Hz) */ #define RIG_LEVEL_SPECTRUM_SPEED CONSTANT_64BIT_FLAG(44) /*!< \c SPECTRUM_SPEED -- Spectrum scope update speed, arg int (highest is fastest, define rig-specific granularity) */ #define RIG_LEVEL_SPECTRUM_REF CONSTANT_64BIT_FLAG(45) /*!< \c SPECTRUM_REF -- Spectrum scope reference display level, arg float (dB, define rig-specific granularity) */ #define RIG_LEVEL_SPECTRUM_AVG CONSTANT_64BIT_FLAG(46) /*!< \c SPECTRUM_AVG -- Spectrum scope averaging mode, arg int (see struct rig_spectrum_avg_mode). Supported averaging modes defined in rig caps. */ #define RIG_LEVEL_SPECTRUM_ATT CONSTANT_64BIT_FLAG(47) /*!< \c SPECTRUM_ATT -- Spectrum scope attenuator, arg int (dB). Supported attenuator values defined in rig caps. */ #define RIG_LEVEL_TEMP_METER CONSTANT_64BIT_FLAG(48) /*!< \c TEMP_METER -- arg int (C, centigrade) */ #define RIG_FUNC_NONE 0 /*!< '' -- No Function */ #define RIG_FUNC_FAGC CONSTANT_64BIT_FLAG (0) /*!< \c FAGC -- Fast AGC */ #define RIG_FUNC_NB CONSTANT_64BIT_FLAG (1) /*!< \c NB -- Noise Blanker */ #define RIG_FUNC_COMP CONSTANT_64BIT_FLAG (2) /*!< \c COMP -- Speech Compression */ #define RIG_FUNC_VOX CONSTANT_64BIT_FLAG (3) /*!< \c VOX -- Voice Operated Relay */ #define RIG_FUNC_TONE CONSTANT_64BIT_FLAG (4) /*!< \c TONE -- CTCSS Tone TX */ #define RIG_FUNC_TSQL CONSTANT_64BIT_FLAG (5) /*!< \c TSQL -- CTCSS Activate/De-activate RX */ #define RIG_FUNC_SBKIN CONSTANT_64BIT_FLAG (6) /*!< \c SBKIN -- Semi Break-in (CW mode) */ #define RIG_FUNC_FBKIN CONSTANT_64BIT_FLAG (7) /*!< \c FBKIN -- Full Break-in (CW mode) */ #define RIG_FUNC_ANF CONSTANT_64BIT_FLAG (8) /*!< \c ANF -- Automatic Notch Filter (DSP) */ #define RIG_FUNC_NR CONSTANT_64BIT_FLAG (9) /*!< \c NR -- Noise Reduction (DSP) */ #define RIG_FUNC_AIP CONSTANT_64BIT_FLAG (10) /*!< \c AIP -- RF pre-amp (AIP on Kenwood, IPO on Yaesu, etc.) */ #define RIG_FUNC_APF CONSTANT_64BIT_FLAG (11) /*!< \c APF -- Auto Passband/Audio Peak Filter */ #define RIG_FUNC_MON CONSTANT_64BIT_FLAG (12) /*!< \c MON -- Monitor transmitted signal */ #define RIG_FUNC_MN CONSTANT_64BIT_FLAG (13) /*!< \c MN -- Manual Notch */ #define RIG_FUNC_RF CONSTANT_64BIT_FLAG (14) /*!< \c RF -- RTTY Filter */ #define RIG_FUNC_ARO CONSTANT_64BIT_FLAG (15) /*!< \c ARO -- Auto Repeater Offset */ #define RIG_FUNC_LOCK CONSTANT_64BIT_FLAG (16) /*!< \c LOCK -- Lock */ #define RIG_FUNC_MUTE CONSTANT_64BIT_FLAG (17) /*!< \c MUTE -- Mute */ #define RIG_FUNC_VSC CONSTANT_64BIT_FLAG (18) /*!< \c VSC -- Voice Scan Control */ #define RIG_FUNC_REV CONSTANT_64BIT_FLAG (19) /*!< \c REV -- Reverse transmit and receive frequencies */ #define RIG_FUNC_SQL CONSTANT_64BIT_FLAG (20) /*!< \c SQL -- Turn Squelch Monitor on/off */ #define RIG_FUNC_ABM CONSTANT_64BIT_FLAG (21) /*!< \c ABM -- Auto Band Mode */ #define RIG_FUNC_BC CONSTANT_64BIT_FLAG (22) /*!< \c BC -- Beat Canceller */ #define RIG_FUNC_MBC CONSTANT_64BIT_FLAG (23) /*!< \c MBC -- Manual Beat Canceller */ #define RIG_FUNC_RIT CONSTANT_64BIT_FLAG (24) /*!< \c RIT -- Receiver Incremental Tuning */ #define RIG_FUNC_AFC CONSTANT_64BIT_FLAG (25) /*!< \c AFC -- Auto Frequency Control ON/OFF */ #define RIG_FUNC_SATMODE CONSTANT_64BIT_FLAG (26) /*!< \c SATMODE -- Satellite mode ON/OFF */ #define RIG_FUNC_SCOPE CONSTANT_64BIT_FLAG (27) /*!< \c SCOPE -- Simple bandscope ON/OFF */ #define RIG_FUNC_RESUME CONSTANT_64BIT_FLAG (28) /*!< \c RESUME -- Scan auto-resume */ #define RIG_FUNC_TBURST CONSTANT_64BIT_FLAG (29) /*!< \c TBURST -- 1750 Hz tone burst */ #define RIG_FUNC_TUNER CONSTANT_64BIT_FLAG (30) /*!< \c TUNER -- Enable automatic tuner */ #define RIG_FUNC_XIT CONSTANT_64BIT_FLAG (31) /*!< \c XIT -- Transmitter Incremental Tuning */ #define RIG_FUNC_NB2 CONSTANT_64BIT_FLAG (32) /*!< \c NB2 -- 2nd Noise Blanker */ #define RIG_FUNC_CSQL CONSTANT_64BIT_FLAG (33) /*!< \c CSQL -- DCS Squelch setting */ #define RIG_FUNC_AFLT CONSTANT_64BIT_FLAG (34) /*!< \c AFLT -- AF Filter setting */ #define RIG_FUNC_ANL CONSTANT_64BIT_FLAG (35) /*!< \c ANL -- Noise limiter setting */ #define RIG_FUNC_BC2 CONSTANT_64BIT_FLAG (36) /*!< \c BC2 -- 2nd Beat Cancel */ #define RIG_FUNC_DUAL_WATCH CONSTANT_64BIT_FLAG (37) /*!< \c DUAL_WATCH -- Dual Watch / Sub Receiver */ #define RIG_FUNC_DIVERSITY CONSTANT_64BIT_FLAG (38) /*!< \c DIVERSITY -- Diversity receive */ #define RIG_FUNC_DSQL CONSTANT_64BIT_FLAG (39) /*!< \c DSQL -- Digital modes squelch */ #define RIG_FUNC_SCEN CONSTANT_64BIT_FLAG (40) /*!< \c SCEN -- scrambler/encryption */ #define RIG_FUNC_SLICE CONSTANT_64BIT_FLAG (41) /*!< \c Rig slice selection -- Flex */ #define RIG_FUNC_TRANSCEIVE CONSTANT_64BIT_FLAG (42) /*!< \c TRANSCEIVE -- Send radio state changes automatically ON/OFF */ #define RIG_FUNC_SPECTRUM CONSTANT_64BIT_FLAG (43) /*!< \c SPECTRUM -- Spectrum scope data output ON/OFF */ #define RIG_FUNC_SPECTRUM_HOLD CONSTANT_64BIT_FLAG (44) /*!< \c SPECTRUM_HOLD -- Pause spectrum scope updates ON/OFF */ #if 0 static struct { quint64 func; const char* str; } rig_func_str[] = { { RIG_FUNC_FAGC, "FAGC" }, { RIG_FUNC_NB, "NB" }, { RIG_FUNC_COMP, "COMP" }, { RIG_FUNC_VOX, "VOX" }, { RIG_FUNC_TONE, "TONE" }, { RIG_FUNC_TSQL, "TSQL" }, { RIG_FUNC_SBKIN, "SBKIN" }, { RIG_FUNC_FBKIN, "FBKIN" }, { RIG_FUNC_ANF, "ANF" }, { RIG_FUNC_NR, "NR" }, { RIG_FUNC_AIP, "AIP" }, { RIG_FUNC_APF, "APF" }, { RIG_FUNC_MON, "MON" }, { RIG_FUNC_MN, "MN" }, { RIG_FUNC_RF, "RF" }, { RIG_FUNC_ARO, "ARO" }, { RIG_FUNC_LOCK, "LOCK" }, { RIG_FUNC_MUTE, "MUTE" }, { RIG_FUNC_VSC, "VSC" }, { RIG_FUNC_REV, "REV" }, { RIG_FUNC_SQL, "SQL" }, { RIG_FUNC_ABM, "ABM" }, { RIG_FUNC_BC, "BC" }, { RIG_FUNC_MBC, "MBC" }, { RIG_FUNC_RIT, "RIT" }, { RIG_FUNC_AFC, "AFC" }, { RIG_FUNC_SATMODE, "SATMODE" }, { RIG_FUNC_SCOPE, "SCOPE" }, { RIG_FUNC_RESUME, "RESUME" }, { RIG_FUNC_TBURST, "TBURST" }, { RIG_FUNC_TUNER, "TUNER" }, { RIG_FUNC_XIT, "XIT" }, { RIG_FUNC_NB2, "NB2" }, { RIG_FUNC_DSQL, "DSQL" }, { RIG_FUNC_AFLT, "AFLT" }, { RIG_FUNC_ANL, "ANL" }, { RIG_FUNC_BC2, "BC2" }, { RIG_FUNC_DUAL_WATCH, "DUAL_WATCH"}, { RIG_FUNC_DIVERSITY, "DIVERSITY"}, { RIG_FUNC_CSQL, "CSQL" }, { RIG_FUNC_SCEN, "SCEN" }, { RIG_FUNC_TRANSCEIVE, "TRANSCEIVE" }, { RIG_FUNC_SPECTRUM, "SPECTRUM" }, { RIG_FUNC_SPECTRUM_HOLD, "SPECTRUM_HOLD" }, { RIG_FUNC_NONE, "" }, }; static struct { quint64 level; const char* str; } rig_level_str[] = { { RIG_LEVEL_PREAMP, "PREAMP" }, { RIG_LEVEL_ATT, "ATT" }, { RIG_LEVEL_VOXDELAY, "VOXDELAY" }, { RIG_LEVEL_AF, "AF" }, { RIG_LEVEL_RF, "RF" }, { RIG_LEVEL_SQL, "SQL" }, { RIG_LEVEL_IF, "IF" }, { RIG_LEVEL_APF, "APF" }, { RIG_LEVEL_NR, "NR" }, { RIG_LEVEL_PBT_IN, "PBT_IN" }, { RIG_LEVEL_PBT_OUT, "PBT_OUT" }, { RIG_LEVEL_CWPITCH, "CWPITCH" }, { RIG_LEVEL_RFPOWER, "RFPOWER" }, { RIG_LEVEL_MICGAIN, "MICGAIN" }, { RIG_LEVEL_KEYSPD, "KEYSPD" }, { RIG_LEVEL_NOTCHF, "NOTCHF" }, { RIG_LEVEL_COMP, "COMP" }, { RIG_LEVEL_AGC, "AGC" }, { RIG_LEVEL_BKINDL, "BKINDL" }, { RIG_LEVEL_BALANCE, "BAL" }, { RIG_LEVEL_METER, "METER" }, { RIG_LEVEL_VOXGAIN, "VOXGAIN" }, { RIG_LEVEL_ANTIVOX, "ANTIVOX" }, { RIG_LEVEL_SLOPE_LOW, "SLOPE_LOW" }, { RIG_LEVEL_SLOPE_HIGH, "SLOPE_HIGH" }, { RIG_LEVEL_BKIN_DLYMS, "BKIN_DLYMS" }, { RIG_LEVEL_RAWSTR, "RAWSTR" }, { RIG_LEVEL_SWR, "SWR" }, { RIG_LEVEL_ALC, "ALC" }, { RIG_LEVEL_STRENGTH, "STRENGTH" }, { RIG_LEVEL_RFPOWER_METER, "RFPOWER_METER" }, { RIG_LEVEL_COMP_METER, "COMP_METER" }, { RIG_LEVEL_VD_METER, "VD_METER" }, { RIG_LEVEL_ID_METER, "ID_METER" }, { RIG_LEVEL_NOTCHF_RAW, "NOTCHF_RAW" }, { RIG_LEVEL_MONITOR_GAIN, "MONITOR_GAIN" }, { RIG_LEVEL_NB, "NB" }, { RIG_LEVEL_RFPOWER_METER_WATTS, "RFPOWER_METER_WATTS" }, { RIG_LEVEL_SPECTRUM_MODE, "SPECTRUM_MODE" }, { RIG_LEVEL_SPECTRUM_SPAN, "SPECTRUM_SPAN" }, { RIG_LEVEL_SPECTRUM_EDGE_LOW, "SPECTRUM_EDGE_LOW" }, { RIG_LEVEL_SPECTRUM_EDGE_HIGH, "SPECTRUM_EDGE_HIGH" }, { RIG_LEVEL_SPECTRUM_SPEED, "SPECTRUM_SPEED" }, { RIG_LEVEL_SPECTRUM_REF, "SPECTRUM_REF" }, { RIG_LEVEL_SPECTRUM_AVG, "SPECTRUM_AVG" }, { RIG_LEVEL_SPECTRUM_ATT, "SPECTRUM_ATT" }, { RIG_LEVEL_TEMP_METER, "TEMP_METER" }, { RIG_LEVEL_NONE, "" }, }; #endif struct cal_table { int size; /*!< number of plots in the table */ struct { int raw; /*!< raw (A/D) value, as returned by \a RIG_LEVEL_RAWSTR */ int val; /*!< associated value, basically the measured dB value */ } table[32]; /*!< table of plots */ }; typedef struct cal_table cal_table_t; #define IC7610_STR_CAL { 16, \ { \ { 0, -54 }, /* S0 */ \ { 11, -48 }, \ { 21, -42 }, \ { 34, -36 }, \ { 50, -30 }, \ { 59, -24 }, \ { 75, -18 }, \ { 93, -12 }, \ { 103, -6 }, \ { 124, 0 }, /* S9 */ \ { 145, 10 }, \ { 160, 20 }, \ { 183, 30 }, \ { 204, 40 }, \ { 222, 50 }, \ { 246, 60 } /* S9+60dB */ \ } } #define IC7850_STR_CAL { 3, \ { \ { 0, -54 }, /* S0 */ \ { 120, 0 }, /* S9 */ \ { 241, 60 } /* S9+60 */ \ } } #define IC7300_STR_CAL { 7, \ { \ { 0, -54 }, \ { 10, -48 }, \ { 30, -36 }, \ { 60, -24 }, \ { 90, -12 }, \ { 120, 0 }, \ { 241, 64 } \ } } class rigCtlD : public QTcpServer { Q_OBJECT public: explicit rigCtlD(QObject *parent=Q_NULLPTR); virtual ~rigCtlD(); int startServer(qint16 port); void stopServer(); rigCapabilities rigCaps; signals: void onStarted(); void onStopped(); void sendData(QString data); void setFrequency(unsigned char vfo, freqt freq); void setPTT(bool state); void setMode(unsigned char mode, unsigned char modeFilter); void setDataMode(bool dataOn, unsigned char modeFilter); void setVFO(unsigned char vfo); void setSplit(unsigned char split); void setDuplexMode(duplexMode dm); // Power void sendPowerOn(); void sendPowerOff(); // Att/preamp void setAttenuator(unsigned char att); void setPreamp(unsigned char pre); //Level set void setRfGain(unsigned char level); void setAfGain(unsigned char level); void setSql(unsigned char level); void setMicGain(unsigned char); void setCompLevel(unsigned char); void setTxPower(unsigned char); void setMonitorLevel(unsigned char); void setVoxGain(unsigned char); void setAntiVoxGain(unsigned char); void setSpectrumRefLevel(int); public slots: virtual void incomingConnection(qintptr socketDescriptor); void receiveRigCaps(rigCapabilities caps); void receiveStateInfo(rigStateStruct* state); // void receiveFrequency(freqt freq); private: rigStateStruct* rigState = Q_NULLPTR; }; class rigCtlClient : public QObject { Q_OBJECT public: explicit rigCtlClient(int socket, rigCapabilities caps, rigStateStruct *state, rigCtlD* parent = Q_NULLPTR); int getSocketId(); public slots: void socketReadyRead(); void socketDisconnected(); void closeSocket(); void sendData(QString data); protected: int sessionId; QTcpSocket* socket = Q_NULLPTR; QString commandBuffer; private: rigCapabilities rigCaps; rigStateStruct* rigState = Q_NULLPTR; rigCtlD* parent; QString getMode(unsigned char mode, bool datamode); unsigned char getMode(QString modeString); QString getFilter(unsigned char mode, unsigned char filter); QString generateFreqRange(bandType band); unsigned char getAntennas(); quint64 getRadioModes(); QString getAntName(unsigned char ant); int getCalibratedValue(unsigned char meter,cal_table_t cal); }; #endif wfview-1.2d/rigidentities.cpp000066400000000000000000000034401415164626400164100ustar00rootroot00000000000000#include "rigidentities.h" #include "logcategories.h" // Copytight 2017-2021 Elliott H. Liggett model_kind determineRadioModel(unsigned char rigID) { model_kind rig; switch(rigID) { case model7100: rig = model7100; break; case model7200: rig = model7200; break; case model7300: rig = model7300; break; case modelR8600: rig = modelR8600; break; case model7000: rig = model7000; break; case model7410: rig = model7410; break; case model7600: rig = model7600; break; case model7610: rig = model7610; break; case model7700: rig = model7700; break; case model7800: rig = model7800; break; case model7850: rig = model7850; break; case model9700: rig = model9700; break; case model706: rig = model706; break; case model705: rig = model705; break; case model718: rig = model718; break; case model736: rig = model736; break; case model910h: rig = model910h; break; case model756pro: rig = model756pro; break; case model756proii: rig = model756proii; break; case model756proiii: rig = model756proiii; break; case model9100: rig = model9100; break; default: rig = modelUnknown; break; } return rig; } wfview-1.2d/rigidentities.h000066400000000000000000000055321415164626400160610ustar00rootroot00000000000000#ifndef RIGIDENTITIES_H #define RIGIDENTITIES_H #include #include #include #include #include "freqmemory.h" // Credit for parts of CIV list: // http://www.docksideradio.com/Icom%20Radio%20Hex%20Addresses.htm // 7850 and 7851 have the same commands and are essentially identical enum model_kind { model7100 = 0x88, model7200 = 0x76, model7300 = 0x94, modelR8600 = 0x96, model7600 = 0x7A, model7610 = 0x98, model7700 = 0x74, model7800 = 0x6A, model7000 = 0x70, model7410 = 0x80, model7850 = 0x8E, model9700 = 0xA2, model705 = 0xA4, model706 = 0x58, model718 = 0x5E, model736 = 0x40, model756pro = 0x5C, model756proii = 0x64, model756proiii = 0x6E, model910h = 0x60, model9100 = 0x7C, modelUnknown = 0xFF }; enum rigInput{ inputMic=0, inputACC=1, inputUSB=3, inputLAN=5, inputACCA, inputACCB, inputNone, inputUnknown=0xff }; enum bandType { band23cm=0, band70cm, band2m, bandAir, bandWFM, band4m, band6m, band10m, band12m, band15m, band17m, band20m, band30m, band40m, band60m, band80m, band160m, band630m, band2200m, bandGen }; enum centerSpansType { cs2p5k = 0, cs5k = 1, cs10k = 2, cs25k = 3, cs50k = 4, cs100k = 5, cs250k = 6, cs500k = 7, cs1M = 8, cs2p5M = 9 }; struct centerSpanData { centerSpansType cstype; QString name; }; model_kind determineRadioModel(unsigned char rigID); struct rigCapabilities { model_kind model; quint8 civ; quint8 modelID; int rigctlModel; QString modelName; bool hasLan; // OEM ethernet or wifi connection bool hasEthernet; bool hasWiFi; bool hasFDcomms; QVector inputs; bool hasSpectrum; quint8 spectSeqMax; quint16 spectAmpMax; quint16 spectLenMax; bool hasDD; bool hasDV; bool hasATU; bool hasCTCSS; bool hasDTCS; bool hasTransmit; bool hasPTTCommand; bool useRTSforPTT; bool hasAttenuator; bool hasPreamp; bool hasAntennaSel; bool hasDataModes; bool hasIFShift; bool hasTBPF; bool hasRXAntenna; std::vector attenuators; std::vector preamps; std::vector antennas; std::vector scopeCenterSpans; std::vector bands; unsigned char bsr[20] = {0}; std::vector modes; QByteArray transceiveCommand; }; #endif // RIGIDENTITIES_H wfview-1.2d/ring/000077500000000000000000000000001415164626400137775ustar00rootroot00000000000000wfview-1.2d/ring/LICENSE000066400000000000000000000020561415164626400150070ustar00rootroot00000000000000MIT License Copyright (c) 2018 Trevor Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wfview-1.2d/ring/README.md000066400000000000000000000010351415164626400152550ustar00rootroot00000000000000# Ring Library ## Overview This library provides source for a multi-producer multi-consumer lock-free ring buffer. It provides a very simple interface for writing and reading from the buffer. The source includes a `Ring_` class, that provides the raw implementation and C-like facilities, as well as a templated `Ring` class for typed reads and writes. ## Contact If you have any questions, concerns, or recommendations please feel free to e-mail me at kmdreko@gmail.com. If you notice a bug or defect, create an issue to report it. wfview-1.2d/ring/ring.cpp000066400000000000000000000224401415164626400154440ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////////// // FILE: ring.cpp // DATE: 2016-02-25 // AUTH: Trevor Wilson // DESC: Implements a lock-free, multi-consumer, multi-producer ring buffer // class //////////////////////////////////////////////////////////////////////////////// // Copyright (c) 2016 Trevor Wilson // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #include "ring.h" using namespace wilt; #include // - std::memcpy Ring_::Ring_() : beg_(nullptr) , end_(nullptr) { std::atomic_init(&used_, static_cast(0)); std::atomic_init(&free_, static_cast(0)); std::atomic_init(&rbuf_, static_cast(0)); std::atomic_init(&rptr_, static_cast(0)); std::atomic_init(&wptr_, static_cast(0)); std::atomic_init(&wbuf_, static_cast(0)); } Ring_::Ring_(std::size_t size) : beg_(new char[size]) , end_(beg_ + size) { std::atomic_init(&used_, static_cast(0)); std::atomic_init(&free_, static_cast(size)); std::atomic_init(&rbuf_, beg_); std::atomic_init(&rptr_, beg_); std::atomic_init(&wptr_, beg_); std::atomic_init(&wbuf_, beg_); } Ring_::Ring_(Ring_&& ring) : beg_(ring.beg_) , end_(ring.end_) { std::atomic_init(&used_, ring.used_.load()); std::atomic_init(&free_, ring.free_.load()); std::atomic_init(&rbuf_, ring.rbuf_.load()); std::atomic_init(&rptr_, ring.rptr_.load()); std::atomic_init(&wptr_, ring.wptr_.load()); std::atomic_init(&wbuf_, ring.wbuf_.load()); ring.beg_ = nullptr; ring.end_ = nullptr; ring.used_.store(0); ring.free_.store(0); ring.rbuf_.store(nullptr); ring.rptr_.store(nullptr); ring.wptr_.store(nullptr); ring.wbuf_.store(nullptr); } Ring_& Ring_::operator= (Ring_&& ring) { delete[] beg_; beg_ = ring.beg_; end_ = ring.end_; used_.store(ring.used_.load()); free_.store(ring.free_.load()); rbuf_.store(ring.rbuf_.load()); rptr_.store(ring.rptr_.load()); wptr_.store(ring.wptr_.load()); wbuf_.store(ring.wbuf_.load()); ring.beg_ = nullptr; ring.end_ = nullptr; ring.used_.store(0); ring.free_.store(0); ring.rbuf_.store(nullptr); ring.rptr_.store(nullptr); ring.wptr_.store(nullptr); ring.wbuf_.store(nullptr); return *this; } Ring_::~Ring_() { delete[] beg_; } std::size_t Ring_::size() const { // The 'used' space can be negative in an over-reserved case, but it can be // clamped to 0 for simplicity. auto s = used_.load(); return s < 0 ? 0 : static_cast(s); } std::size_t Ring_::capacity() const { return static_cast(end_ - beg_); } void Ring_::read(void* data, std::size_t length) noexcept { auto block = acquire_read_block_(length); copy_read_block_(block, (char*)data, length); release_read_block_(block, length); } void Ring_::write(const void* data, std::size_t length) noexcept { auto block = acquire_write_block_(length); copy_write_block_(block, (const char*)data, length); release_write_block_(block, length); } bool Ring_::try_read(void* data, std::size_t length) noexcept { auto block = try_acquire_read_block_(length); if (block == nullptr) return false; copy_read_block_(block, (char*)data, length); release_read_block_(block, length); return true; } bool Ring_::try_write(const void* data, std::size_t length) noexcept { auto block = try_acquire_write_block_(length); if (block == nullptr) return false; copy_write_block_(block, (const char*)data, length); release_write_block_(block, length); return true; } char* Ring_::normalize_(char* ptr) { return ptr < end_ ? ptr : ptr - capacity(); } char* Ring_::acquire_read_block_(std::size_t length) { auto size = static_cast(length); while (true) // loop while conflict { auto old_rptr = rptr_.load(std::memory_order_consume); // read rptr while (used_.load(std::memory_order_consume) < size) // check for data ; // spin until success auto new_rptr = normalize_(old_rptr + size); // get block end used_.fetch_sub(size); // reserve if (rptr_.compare_exchange_strong(old_rptr, new_rptr)) // try commit return old_rptr; // committed used_.fetch_add(size, std::memory_order_relaxed); // un-reserve } } char* Ring_::try_acquire_read_block_(std::size_t length) { auto size = static_cast(length); while (true) // loop while conflict { auto old_rptr = rptr_.load(std::memory_order_consume); // read rptr if (used_.load(std::memory_order_consume) < size) // check for data return nullptr; // return failure auto new_rptr = normalize_(old_rptr + size); // get block end used_.fetch_sub(size); // reserve if (rptr_.compare_exchange_strong(old_rptr, new_rptr)) // try commit return old_rptr; // committed used_.fetch_add(size, std::memory_order_relaxed); // un-reserve } } void Ring_::copy_read_block_(const char* block, char* data, std::size_t length) { if (block + length < end_) { std::memcpy(data, block, length); } else { auto first = end_ - block; std::memcpy(data, block, first); std::memcpy(data + first, beg_, length - first); } } void Ring_::release_read_block_(char* old_rptr, std::size_t length) { auto new_rptr = normalize_(old_rptr + length); // get block end while (rbuf_.load() != old_rptr) // check for earlier reads ; // spin until reads complete rbuf_.store(new_rptr); // finish commit free_.fetch_add(length, std::memory_order_relaxed); // add to free space } char* Ring_::acquire_write_block_(std::size_t length) { auto size = static_cast(length); while (true) // loop while conflict { auto old_wbuf = wbuf_.load(std::memory_order_consume); // read wbuf while (free_.load(std::memory_order_consume) < size) // check for space ; // spin until success auto new_wbuf = normalize_(old_wbuf + size); // get block end free_.fetch_sub(size); // reserve if (wbuf_.compare_exchange_strong(old_wbuf, new_wbuf)) // try commit return old_wbuf; // committed free_.fetch_add(size, std::memory_order_relaxed); // un-reserve } } char* Ring_::try_acquire_write_block_(std::size_t length) { auto size = static_cast(length); while (true) // loop while conflict { auto old_wbuf = wbuf_.load(std::memory_order_consume); // read wbuf if (free_.load(std::memory_order_consume) < size) // check for space return nullptr; // return failure auto new_wbuf = normalize_(old_wbuf + size); // get block end free_.fetch_sub(size); // reserve if (wbuf_.compare_exchange_strong(old_wbuf, new_wbuf)) // try commit return old_wbuf; // committed free_.fetch_add(size, std::memory_order_relaxed); // un-reserve } } void Ring_::copy_write_block_(char* block, const char* data, std::size_t length) { if (block + length < end_) { std::memcpy(block, data, length); } else { auto first = end_ - block; std::memcpy(block, data, first); std::memcpy(beg_, data + first, length - first); } } void Ring_::release_write_block_(char* old_wbuf, std::size_t length) { auto new_wbuf = normalize_(old_wbuf + length); // get block end while (wptr_.load() != old_wbuf) // wait for earlier writes ; // spin until writes complete wptr_.store(new_wbuf); // finish commit used_.fetch_add(length, std::memory_order_relaxed); // add to used space } wfview-1.2d/ring/ring.h000066400000000000000000000361571415164626400151230ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////////// // FILE: ring.h // DATE: 2016-02-25 // AUTH: Trevor Wilson // DESC: Defines a lock-free, multi-consumer, multi-producer ring buffer class //////////////////////////////////////////////////////////////////////////////// // Copyright (c) 2016 Trevor Wilson // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #ifndef WILT_RING_H #define WILT_RING_H #include // - std::atomic #include // - std::size_t // - std::ptrdiff_t #include // - ::new(ptr) #include // - std::is_nothrow_copy_constructible // - std::is_nothrow_move_constructible // - std::is_nothrow_move_assignable // - std::is_nothrow_destructible #include // - std::move namespace wilt { ////////////////////////////////////////////////////////////////////////////// // This structure aims to access elements in a ring buffer from multiple // concurrent readers and writers in a lock-free manner. // // The class works by allocating the array and storing two pointers (for the // beginning and end of the allocated space). Two atomic pointers are used to // track the beginning and end of the currently used storage space. To // facilitate concurrent reads and writes, theres a read buffer pointer before // the read pointer for data currently being read, and a corresponding write // buffer pointer beyond the write pointer for data currently being written. // These buffer pointers cannot overlap. Just using these pointers suffer from // some minute inefficiencies and a few ABA problems. Therfore, atomic // integers are used to store the currently used and currently free sizes. // // It allows multiple readers and multiple writers by implementing a reserve- // commit system. A thread wanting to read will check the used size to see if // there's enough data. If there is, it subtracts from the used size to // 'reserve' the read. It then does a compare-exchange to 'commit' by // increasing the read pointer. If that fails, then it backs out ('un- // reserves') by adding back to the used size and tries again. If it // succeeds, then it proceeds to read the data. In order to complete, the // reader must update the read buffer pointer to where it just finished // reading from. However, because other readers that started before may not be // done yet, the reader must wait until the read buffer pointer points to // where the read started. Only, then is the read buffer pointer updated, and // the free size increased. So while this implementation is lock-free, it is // not wait-free. This same principle works the same when writing (ammended // for the appropriate pointers). // // If two readers try to read at the same time and there is only enough data // for one of them. The used size MAY be negative because they both 'reserve' // the data. This is an over-reserved state. But the compare-exchange will // only allow one reader to 'commit' to the read and the other will 'un- // reserve' the read. // // |beg |rptr used=5 |wbuf - unused // |----|----|++++|====|====|====|====|====|++++|----| + modifying // free=3 |rbuf |wptr |end = used // // The diagram above shows a buffer of size 10 storing 5 bytes with a reader // reading one byte and one writer reading one byte. // // Out of the box, the class works by reading and writing raw bytes from POD // data types and arrays. A wrapper could allow for a nicer interface for // pushing and popping elements. As it stands, this structure cannot be easily // modified to store types of variable size. class Ring_ { private: //////////////////////////////////////////////////////////////////////////// // TYPE DEFINITIONS //////////////////////////////////////////////////////////////////////////// typedef char* data_ptr; typedef std::atomic size_type; typedef std::atomic atom_ptr; private: //////////////////////////////////////////////////////////////////////////// // PRIVATE MEMBERS //////////////////////////////////////////////////////////////////////////// // Beginning and end pointers don't need to be atomic because they don't // change. used_ and free_ can be negative in certain cases (and that's ok). data_ptr beg_; // pointer to beginning of data block data_ptr end_; // pointer to end of data block alignas(64) size_type used_; // size of unreserved used space alignas(64) size_type free_; // size of unreserved free space alignas(64) atom_ptr rbuf_; // pointer to beginning of data being read atom_ptr rptr_; // pointer to beginning of data alignas(64) atom_ptr wptr_; // pointer to end of data atom_ptr wbuf_; // pointer to end of data being written public: //////////////////////////////////////////////////////////////////////////// // CONSTRUCTORS AND DESTRUCTORS //////////////////////////////////////////////////////////////////////////// // Constructs a ring without a buffer (capacity() == 0) Ring_(); // Constructs a ring with a buffer with a size Ring_(std::size_t size); // Moves the buffer between rings, assumes no concurrent operations Ring_(Ring_&& ring); // Moves the buffer between rings, assumes no concurrent operations on // either ring. Deallocates the buffer Ring_& operator= (Ring_&& ring); // No copying Ring_(const Ring_&) = delete; Ring_& operator= (const Ring_&) = delete; // Deallocates the buffer ~Ring_(); public: //////////////////////////////////////////////////////////////////////////// // QUERY FUNCTIONS //////////////////////////////////////////////////////////////////////////// // Functions only report on the state of the ring // Returns the current amount of non-reserved used space (amount of written // data that a read hasn't yet reserved). Over-reserved scenarios mean this // number is not the ultimate source of truth with concurrent operations, // but its the closest safe approximation. This, of course, doesn't report // writes that have not completed. std::size_t size() const; // Maximum amount of data that can be held std::size_t capacity() const; public: //////////////////////////////////////////////////////////////////////////// // ACCESSORS AND MODIFIERS //////////////////////////////////////////////////////////////////////////// // All operations assume object has not been moved. Blocking operations run // until operation is completed. Non-blocking operations fail if there is // not enough space void read(void* data, std::size_t length) noexcept; void write(const void* data, std::size_t length) noexcept; bool try_read(void* data, std::size_t length) noexcept; bool try_write(const void* data, std::size_t length) noexcept; protected: //////////////////////////////////////////////////////////////////////////// // PROTECTED FUNCTIONS //////////////////////////////////////////////////////////////////////////// // Helper functions // Wraps a pointer within the array. Assumes 'beg_ <= ptr < end_+capacity()' char* normalize_(char*); char* acquire_read_block_(std::size_t length); char* try_acquire_read_block_(std::size_t length); void copy_read_block_(const char* block, char* data, std::size_t length); void release_read_block_(char* block, std::size_t length); char* acquire_write_block_(std::size_t length); char* try_acquire_write_block_(std::size_t length); void copy_write_block_(char* block, const char* data, std::size_t length); void release_write_block_(char* block, std::size_t length); char* begin_alloc_() { return beg_; } const char* begin_alloc_() const { return beg_; } char* end_alloc_() { return end_; } const char* end_alloc_() const { return end_; } char* begin_data_() { return rptr_; } const char* begin_data_() const { return rptr_; } char* end_data_() { return wptr_; } const char* end_data_() const { return wptr_; } }; // class Ring_ template class Ring : protected Ring_ { public: //////////////////////////////////////////////////////////////////////////// // CONSTRUCTORS AND DESTRUCTORS //////////////////////////////////////////////////////////////////////////// // Constructs a ring without a buffer (capacity() == 0) Ring(); // Constructs a ring with a buffer with a size Ring(std::size_t size); // Moves the buffer between rings, assumes no concurrent operations Ring(Ring&& ring); // Moves the buffer between rings, assumes no concurrent operations on // either ring. Deallocates the buffer Ring& operator= (Ring&& ring); // No copying Ring(const Ring_&) = delete; Ring& operator= (const Ring_&) = delete; // Deallocates the buffer, destructs stored data. ~Ring(); public: //////////////////////////////////////////////////////////////////////////// // QUERY FUNCTIONS //////////////////////////////////////////////////////////////////////////// // Functions only report on the state of the ring // Returns the current amount of non-reserved used space (amount of written // data that a read hasn't yet reserved). Over-reserved scenarios mean this // number is not the ultimate source of truth with concurrent operations, // but its the closest safe approximation. This, of course, doesn't report // writes that have not completed. std::size_t size() const; // Maximum amount of data that can be held std::size_t capacity() const; public: //////////////////////////////////////////////////////////////////////////// // ACCESSORS AND MODIFIERS //////////////////////////////////////////////////////////////////////////// // All operations assume object has not been moved. Blocking operations run // until operation is completed. Non-blocking operations fail if there is // not enough space void read(T& data) noexcept; // blocking read void write(const T& data) noexcept; // blocking write void write(T&& data) noexcept; // blocking write bool try_read(T& data) noexcept; // non-blocking read bool try_write(const T& data) noexcept; // non-blocking write bool try_write(T&& data) noexcept; // non-blocking write private: //////////////////////////////////////////////////////////////////////////// // PRIVATE HELPER FUNCTIONS //////////////////////////////////////////////////////////////////////////// void destruct_(); }; // class Ring template Ring::Ring() : Ring_() { } template Ring::Ring(std::size_t size) : Ring_(size * sizeof(T)) { } template Ring::Ring(Ring&& ring) : Ring_(std::move(ring)) { } template Ring& Ring::operator= (Ring&& ring) { destruct_(); Ring_::operator= (ring); return *this; } template Ring::~Ring() { destruct_(); } template void Ring::destruct_() { if (size() == 0) return; auto itr = begin_data_(); auto end = end_data_(); do { auto t = reinterpret_cast(itr); t->~T(); itr = normalize_(itr + sizeof(T)); } while (itr != end); } template std::size_t Ring::size() const { return Ring_::size() / sizeof(T); } template std::size_t Ring::capacity() const { return Ring_::capacity() / sizeof(T); } template void Ring::read(T& data) noexcept { static_assert(std::is_nothrow_move_assignable::value, "T move assignment must not throw"); static_assert(std::is_nothrow_destructible::value, "T destructor must not throw"); auto block = acquire_read_block_(sizeof(T)); // critical section auto t = reinterpret_cast(block); data = std::move(*t); t->~T(); release_read_block_(block, sizeof(T)); } template void Ring::write(const T& data) noexcept { static_assert(std::is_nothrow_copy_constructible::value, "T copy constructor must not throw"); auto block = acquire_write_block_(sizeof(T)); // critical section new(block) T(data); release_write_block_(block, sizeof(T)); } template void Ring::write(T&& data) noexcept { static_assert(std::is_nothrow_move_constructible::value, "T move constructor must not throw"); auto block = acquire_write_block_(sizeof(T)); // critical section new(block) T(std::move(data)); release_write_block_(block, sizeof(T)); } template bool Ring::try_read(T& data) noexcept { static_assert(std::is_nothrow_move_assignable::value, "T move assignment must not throw"); static_assert(std::is_nothrow_destructible::value, "T destructor must not throw"); auto block = try_acquire_read_block_(sizeof(T)); if (block == nullptr) return false; // critical section auto t = reinterpret_cast(block); data = std::move(*t); t->~T(); release_read_block_(block, sizeof(T)); return true; } template bool Ring::try_write(const T& data) noexcept { static_assert(std::is_nothrow_copy_constructible::value, "T copy constructor must not throw"); auto block = try_acquire_write_block_(sizeof(T)); if (block == nullptr) return false; // critical section new(block) T(data); release_write_block_(block, sizeof(T)); return true; } template bool Ring::try_write(T&& data) noexcept { static_assert(std::is_nothrow_move_constructible::value, "T move constructor must not throw"); auto block = try_acquire_write_block_(sizeof(T)); if (block == nullptr) return false; // critical section new(block) T(std::move(data)); release_write_block_(block, sizeof(T)); return true; } } // namespace wilt #endif // !WILT_RING_Hwfview-1.2d/satellitesetup.cpp000066400000000000000000000004161415164626400166140ustar00rootroot00000000000000#include "satellitesetup.h" #include "ui_satellitesetup.h" #include "logcategories.h" satelliteSetup::satelliteSetup(QWidget *parent) : QDialog(parent), ui(new Ui::satelliteSetup) { ui->setupUi(this); } satelliteSetup::~satelliteSetup() { delete ui; } wfview-1.2d/satellitesetup.h000066400000000000000000000004731415164626400162640ustar00rootroot00000000000000#ifndef SATELLITESETUP_H #define SATELLITESETUP_H #include namespace Ui { class satelliteSetup; } class satelliteSetup : public QDialog { Q_OBJECT public: explicit satelliteSetup(QWidget *parent = 0); ~satelliteSetup(); private: Ui::satelliteSetup *ui; }; #endif // SATELLITESETUP_H wfview-1.2d/satellitesetup.ui000066400000000000000000000331361415164626400164540ustar00rootroot00000000000000 satelliteSetup 0 0 563 311 0 0 563 311 563 311 Satellite Setup 9 9 541 291 QLayout::SetFixedSize 10 10 10 10 0 0 Qt::Horizontal 40 20 120 0 Qt::Horizontal 75 true Satellite Setup: 120 0 Qt::Horizontal Qt::Horizontal 40 20 0 Type: Linear Inverting Linear Non-Inverting FM Qt::Horizontal 40 20 0 Uplink: 0 0 30 0 Downlink: 30 0 Qt::Horizontal 40 20 0 0 0 0 Uplink from: 0 0 30 0 To: 0 0 30 0 Qt::Horizontal 40 20 0 0 0 0 Downlink from: 0 0 30 0 To: 0 0 30 0 Qt::Horizontal 40 20 0 0 0 0 Telemetry: 0 0 30 0 Qt::Horizontal 40 20 0 Additional Spectrum Margin (KHz) 100 10 (added to both sides) Qt::Horizontal 40 20 0 Set VFOs Set Spectrum Add Markers Qt::Vertical 20 40 wfview-1.2d/transceiveradjustments.cpp000066400000000000000000000060331415164626400203550ustar00rootroot00000000000000#include "transceiveradjustments.h" #include "ui_transceiveradjustments.h" transceiverAdjustments::transceiverAdjustments(QWidget *parent) : QWidget(parent), ui(new Ui::transceiverAdjustments) { ui->setupUi(this); #ifndef QT_DEBUG ui->transmitterControlsGroupBox->setVisible(false); // no controls available so far ui->bassRxLabel->setVisible(false); ui->bassRxSlider->setVisible(false); ui->trebleRxLabel->setVisible(false); ui->trebleRxSlider->setVisible(false); ui->NRRxCheckBox->setVisible(false); ui->NRRxSlider->setVisible(false); ui->notchRxChkBox->setVisible(false); ui->notchRxSlider->setVisible(false); ui->NBRxChkBox->setVisible(false); ui->NBRxSlider->setVisible(false); ui->bandwidthGroupBox->setVisible(false); this->window()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); this->window()->resize(QSizePolicy::Minimum, QSizePolicy::Minimum); #endif } transceiverAdjustments::~transceiverAdjustments() { rigCaps.inputs.clear(); rigCaps.preamps.clear(); rigCaps.attenuators.clear(); rigCaps.antennas.clear(); delete ui; } void transceiverAdjustments::on_IFShiftSlider_valueChanged(int value) { if(rigCaps.hasIFShift) { emit setIFShift(value); } else { unsigned char inner = ui->TPBFInnerSlider->value(); unsigned char outer = ui->TPBFOuterSlider->value(); int shift = value - previousIFShift; inner = qMax( 0, qMin(255,int (inner + shift)) ); outer = qMax( 0, qMin(255,int (outer + shift)) ); ui->TPBFInnerSlider->setValue(inner); ui->TPBFOuterSlider->setValue(outer); previousIFShift = value; } } void transceiverAdjustments::on_TPBFInnerSlider_valueChanged(int value) { emit setTPBFInner(value); } void transceiverAdjustments::on_TPBFOuterSlider_valueChanged(int value) { emit setTPBFOuter(value); } void transceiverAdjustments::setRig(rigCapabilities rig) { this->rigCaps = rig; if(!rigCaps.hasIFShift) updateIFShift(128); //ui->IFShiftSlider->setVisible(rigCaps.hasIFShift); //ui->IFShiftLabel->setVisible(rigCaps.hasIFShift); ui->TPBFInnerSlider->setVisible(rigCaps.hasTBPF); ui->TPBFInnerLabel->setVisible(rigCaps.hasTBPF); ui->TPBFOuterSlider->setVisible(rigCaps.hasTBPF); ui->TPBFInnerLabel->setVisible(rigCaps.hasTBPF); haveRigCaps = true; } // These are accessed by wfmain when we receive new values from rigCommander: void transceiverAdjustments::updateIFShift(unsigned char level) { ui->IFShiftSlider->blockSignals(true); ui->IFShiftSlider->setValue(level); ui->IFShiftSlider->blockSignals(false); } void transceiverAdjustments::updateTPBFInner(unsigned char level) { ui->TPBFInnerSlider->blockSignals(true); ui->TPBFInnerSlider->setValue(level); ui->TPBFInnerSlider->blockSignals(false); } void transceiverAdjustments::updateTPBFOuter(unsigned char level) { ui->TPBFOuterSlider->blockSignals(true); ui->TPBFOuterSlider->setValue(level); ui->TPBFOuterSlider->blockSignals(false); } wfview-1.2d/transceiveradjustments.h000066400000000000000000000020041415164626400200140ustar00rootroot00000000000000#ifndef TRANSCEIVERADJUSTMENTS_H #define TRANSCEIVERADJUSTMENTS_H #include #include #include "rigidentities.h" namespace Ui { class transceiverAdjustments; } class transceiverAdjustments : public QWidget { Q_OBJECT public: explicit transceiverAdjustments(QWidget *parent = 0); ~transceiverAdjustments(); signals: void setIFShift(unsigned char level); void setTPBFInner(unsigned char level); void setTPBFOuter(unsigned char level); public slots: void setRig(rigCapabilities rig); void updateIFShift(unsigned char level); void updateTPBFInner(unsigned char level); void updateTPBFOuter(unsigned char level); private slots: void on_IFShiftSlider_valueChanged(int value); void on_TPBFInnerSlider_valueChanged(int value); void on_TPBFOuterSlider_valueChanged(int value); private: Ui::transceiverAdjustments *ui; rigCapabilities rigCaps; bool haveRigCaps = false; int previousIFShift = 128; }; #endif // TRANSCEIVERADJUSTMENTS_H wfview-1.2d/transceiveradjustments.ui000066400000000000000000000330061415164626400202100ustar00rootroot00000000000000 transceiverAdjustments 0 0 832 337 Form 10 10 10 10 Transmitter 0 0 0 0 0 230 Qt::Vertical Comp 0 230 Qt::Vertical 0 23 Bass 0 230 Qt::Vertical 0 23 Treble 0 230 Qt::Vertical 0 23 TBD Receiver 0 230 Qt::Vertical 0 23 Bass 0 230 Qt::Vertical 0 23 Treble 0 230 255 Qt::Vertical IF Shift 0 0 0 0 0 230 255 Qt::Vertical PBF Inner 0 0 0 0 0 230 255 Qt::Vertical PBF Outer 0 230 Qt::Vertical NR 0 230 Qt::Vertical NB 0 230 Qt::Vertical Notch Bandwidth Low High Filter Qt::Vertical 20 40 wfview-1.2d/udphandler.cpp000066400000000000000000001516641415164626400157070ustar00rootroot00000000000000// Copyright 2021 Phil Taylor M0VSE // This code is heavily based on "Kappanhang" by HA2NON, ES1AKOS and W6EL! #include "udphandler.h" #include "logcategories.h" udpHandler::udpHandler(udpPreferences prefs, audioSetup rx, audioSetup tx) : controlPort(prefs.controlLANPort), civPort(0), audioPort(0), rxSetup(rx), txSetup(tx) { this->port = this->controlPort; this->username = prefs.username; this->password = prefs.password; this->compName = prefs.clientName.mid(0,8) + "-wfview"; qInfo(logUdp()) << "Starting udpHandler user:" << username << " rx latency:" << rxSetup.latency << " tx latency:" << txSetup.latency << " rx sample rate: " << rxSetup.samplerate << " rx codec: " << rxSetup.codec << " tx sample rate: " << txSetup.samplerate << " tx codec: " << txSetup.codec; // Try to set the IP address, if it is a hostname then perform a DNS lookup. if (!radioIP.setAddress(prefs.ipAddress)) { QHostInfo remote = QHostInfo::fromName(prefs.ipAddress); foreach(QHostAddress addr, remote.addresses()) { if (addr.protocol() == QAbstractSocket::IPv4Protocol) { radioIP = addr; qInfo(logUdp()) << "Got IP Address :" << prefs.ipAddress << ": " << addr.toString(); break; } } if (radioIP.isNull()) { qInfo(logUdp()) << "Error obtaining IP Address for :" << prefs.ipAddress << ": " << remote.errorString(); return; } } // Convoluted way to find the external IP address, there must be a better way???? QString localhostname = QHostInfo::localHostName(); QList hostList = QHostInfo::fromName(localhostname).addresses(); foreach(const QHostAddress & address, hostList) { if (address.protocol() == QAbstractSocket::IPv4Protocol && address.isLoopback() == false) { localIP = QHostAddress(address.toString()); } } } void udpHandler::init() { udpBase::init(); // Perform UDP socket initialization. // Connect socket to my dataReceived function. QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpHandler::dataReceived); /* Connect various timers */ tokenTimer = new QTimer(); areYouThereTimer = new QTimer(); pingTimer = new QTimer(); idleTimer = new QTimer(); connect(tokenTimer, &QTimer::timeout, this, std::bind(&udpHandler::sendToken, this, 0x05)); connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0)); connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing); connect(idleTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, true, 0, 0)); // Start sending are you there packets - will be stopped once "I am here" received areYouThereTimer->start(AREYOUTHERE_PERIOD); } udpHandler::~udpHandler() { if (streamOpened) { if (audio != Q_NULLPTR) { delete audio; } if (civ != Q_NULLPTR) { delete civ; } qInfo(logUdp()) << "Sending token removal packet"; sendToken(0x01); if (tokenTimer != Q_NULLPTR) { tokenTimer->stop(); delete tokenTimer; } if (watchdogTimer != Q_NULLPTR) { watchdogTimer->stop(); delete watchdogTimer; } } } void udpHandler::changeLatency(quint16 value) { emit haveChangeLatency(value); } void udpHandler::setVolume(unsigned char value) { emit haveSetVolume(value); } void udpHandler::receiveFromCivStream(QByteArray data) { emit haveDataFromPort(data); } void udpHandler::receiveAudioData(const audioPacket &data) { emit haveAudioData(data); } void udpHandler::receiveDataFromUserToRig(QByteArray data) { if (civ != Q_NULLPTR) { civ->send(data); } } void udpHandler::dataReceived() { while (udp->hasPendingDatagrams()) { lastReceived = QTime::currentTime(); QNetworkDatagram datagram = udp->receiveDatagram(); QByteArray r = datagram.data(); switch (r.length()) { case (CONTROL_SIZE): // control packet { control_packet_t in = (control_packet_t)r.constData(); if (in->type == 0x04) { // If timer is active, stop it as they are obviously there! qInfo(logUdp()) << this->metaObject()->className() << ": Received I am here from: " <isActive()) { // send ping packets every second areYouThereTimer->stop(); pingTimer->start(PING_PERIOD); idleTimer->start(IDLE_PERIOD); } } // This is "I am ready" in response to "Are you ready" so send login. else if (in->type == 0x06) { qInfo(logUdp()) << this->metaObject()->className() << ": Received I am ready"; sendLogin(); // send login packet } break; } case (PING_SIZE): // ping packet { ping_packet_t in = (ping_packet_t)r.constData(); if (in->type == 0x07 && in->reply == 0x01 && streamOpened) { // This is a response to our ping request so measure latency latency += lastPingSentTime.msecsTo(QDateTime::currentDateTime()); latency /= 2; quint32 totalsent = packetsSent; quint32 totallost = packetsLost; if (audio != Q_NULLPTR) { totalsent = totalsent + audio->packetsSent; totallost = totallost + audio->packetsLost; } if (civ != Q_NULLPTR) { totalsent = totalsent + civ->packetsSent; totallost = totallost + civ->packetsLost; } QString tempLatency; if (rxSetup.latency > audio->audioLatency) { tempLatency = QString("%1 ms").arg(audio->audioLatency,3); } else { tempLatency = QString("%1 ms").arg(audio->audioLatency,3); } QString txString=""; if (txSetup.codec == 0) { txString = "(no tx)"; } emit haveNetworkStatus(QString("

%1 rx latency: %2 / rtt: %3 ms / loss: %4/%5
").arg(txString).arg(tempLatency).arg(latency, 3).arg(totallost, 3).arg(totalsent, 3)); } break; } case (TOKEN_SIZE): // Response to Token request { token_packet_t in = (token_packet_t)r.constData(); if (in->res == 0x05 && in->type != 0x01) { if (in->response == 0x0000) { qDebug(logUdp()) << this->metaObject()->className() << ": Token renewal successful"; tokenTimer->start(TOKEN_RENEWAL); gotAuthOK = true; if (!streamOpened) { sendRequestStream(); } } else if (in->response == 0xffffffff) { qWarning() << this->metaObject()->className() << ": Radio rejected token renewal, performing login"; remoteId = in->sentid; tokRequest = in->tokrequest; token = in->token; streamOpened = false; sendRequestStream(); // Got new token response //sendToken(0x02); // Update it. } else { qWarning() << this->metaObject()->className() << ": Unknown response to token renewal? " << in->response; } } break; } case (STATUS_SIZE): // Status packet { status_packet_t in = (status_packet_t)r.constData(); if (in->type != 0x01) { if (in->error == 0xffffffff && !streamOpened) { emit haveNetworkError(radioIP.toString(), "Connection failed, wait a few minutes or reboot the radio."); qInfo(logUdp()) << this->metaObject()->className() << ": Connection failed, wait a few minutes or reboot the radio."; } else if (in->error == 0x00000000 && in->disc == 0x01) { emit haveNetworkError(radioIP.toString(), "Got radio disconnected."); qInfo(logUdp()) << this->metaObject()->className() << ": Got radio disconnected."; if (streamOpened) { // Close stream connections but keep connection open to the radio. if (audio != Q_NULLPTR) { delete audio; audio = Q_NULLPTR; } if (civ != Q_NULLPTR) { delete civ; civ = Q_NULLPTR; } streamOpened = false; } } else { civPort = qFromBigEndian(in->civport); audioPort = qFromBigEndian(in->audioport); } } break; } case(LOGIN_RESPONSE_SIZE): // Response to Login packet. { login_response_packet_t in = (login_response_packet_t)r.constData(); if (in->type != 0x01) { connectionType = in->connection; qInfo(logUdp()) << "Got connection type:" << connectionType; if (connectionType == "FTTH") { highBandwidthConnection = true; } if (connectionType != "WFVIEW") // NOT WFVIEW { if (rxSetup.codec >= 0x40 || txSetup.codec >= 0x40) { emit haveNetworkError(QString("UDP"), QString("Opus codec not supported, forcing LPCM16")); if (rxSetup.codec >= 0x40) rxSetup.codec = 0x04; if (txSetup.codec >= 0x40) txSetup.codec = 0x04; } } if (in->error == 0xfeffffff) { emit haveNetworkStatus("Invalid Username/Password"); qInfo(logUdp()) << this->metaObject()->className() << ": Invalid Username/Password"; } else if (!isAuthenticated) { if (in->tokrequest == tokRequest) { emit haveNetworkStatus("Radio Login OK!"); qInfo(logUdp()) << this->metaObject()->className() << ": Received matching token response to our request"; token = in->token; sendToken(0x02); tokenTimer->start(TOKEN_RENEWAL); // Start token request timer isAuthenticated = true; } else { qInfo(logUdp()) << this->metaObject()->className() << ": Token response did not match, sent:" << tokRequest << " got " << in->tokrequest; } } qInfo(logUdp()) << this->metaObject()->className() << ": Detected connection speed " << in->connection; } break; } case (CONNINFO_SIZE): { conninfo_packet_t in = (conninfo_packet_t)r.constData(); if (in->type != 0x01) { devName = in->name; QHostAddress ip = QHostAddress(qToBigEndian(in->ipaddress)); if (!streamOpened && in->busy) { if (in->ipaddress != 0x00 && strcmp(in->computer, compName.toLocal8Bit())) { emit haveNetworkStatus(devName + " in use by: " + in->computer + " (" + ip.toString() + ")"); sendControl(false, 0x00, in->seq); // Respond with an idle } else { civ = new udpCivData(localIP, radioIP, civPort); // TX is not supported if (txSampleRates <2 ) { txSetup.samplerate = 0; txSetup.codec = 0; } audio = new udpAudio(localIP, radioIP, audioPort, rxSetup, txSetup); QObject::connect(civ, SIGNAL(receive(QByteArray)), this, SLOT(receiveFromCivStream(QByteArray))); QObject::connect(audio, SIGNAL(haveAudioData(audioPacket)), this, SLOT(receiveAudioData(audioPacket))); QObject::connect(this, SIGNAL(haveChangeLatency(quint16)), audio, SLOT(changeLatency(quint16))); QObject::connect(this, SIGNAL(haveSetVolume(unsigned char)), audio, SLOT(setVolume(unsigned char))); streamOpened = true; emit haveNetworkStatus(devName); qInfo(logUdp()) << this->metaObject()->className() << "Got serial and audio request success, device name: " << devName; // Stuff can change in the meantime because of a previous login... remoteId = in->sentid; myId = in->rcvdid; tokRequest = in->tokrequest; token = in->token; } } else if (!streamOpened && !in->busy) { emit haveNetworkStatus(devName + " available"); identa = in->identa; identb = in->identb; sendRequestStream(); } else if (streamOpened) /* If another client connects/disconnects from the server, the server will emit a CONNINFO packet, send our details to confirm we still want the stream */ { // Received while stream is open. sendRequestStream(); } } break; } case (CAPABILITIES_SIZE): { capabilities_packet_t in = (capabilities_packet_t)r.constData(); if (in->type != 0x01) { audioType = in->audio; devName = in->name; civId = in->civ; rxSampleRates = in->rxsample; txSampleRates = in->txsample; emit haveBaudRate(qFromBigEndian(in->baudrate)); //replyId = r.mid(0x42, 16); qInfo(logUdp()) << this->metaObject()->className() << "Received radio capabilities, Name:" << devName << " Audio:" << audioType << "CIV:" << hex << civId; if (txSampleRates < 2) { // TX not supported qInfo(logUdp()) << this->metaObject()->className() << "TX audio is disabled"; } } break; } } udpBase::dataReceived(r); // Call parent function to process the rest. r.clear(); datagram.clear(); } return; } void udpHandler::sendRequestStream() { QByteArray usernameEncoded; passcode(username, usernameEncoded); conninfo_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.sentid = myId; p.rcvdid = remoteId; p.code = 0x0180; p.res = 0x03; p.commoncap = 0x8010; p.identa = identa; p.identb = identb; p.innerseq = authSeq++; p.tokrequest = tokRequest; p.token = token; memcpy(&p.name, devName.toLocal8Bit().constData(), devName.length()); p.rxenable = 1; if (this->txSampleRates > 1) { p.txenable = 1; p.txcodec = txSetup.codec; } p.rxcodec = rxSetup.codec; memcpy(&p.username, usernameEncoded.constData(), usernameEncoded.length()); p.rxsample = qToBigEndian((quint32)rxSetup.samplerate); p.txsample = qToBigEndian((quint32)txSetup.samplerate); p.civport = qToBigEndian((quint32)civPort); p.audioport = qToBigEndian((quint32)audioPort); p.txbuffer = qToBigEndian((quint32)txSetup.latency); p.convert = 1; sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); return; } void udpHandler::sendAreYouThere() { if (areYouThereCounter == 20) { qInfo(logUdp()) << this->metaObject()->className() << ": Radio not responding."; emit haveNetworkStatus("Radio not responding!"); } qInfo(logUdp()) << this->metaObject()->className() << ": Sending Are You There..."; areYouThereCounter++; udpBase::sendControl(false,0x03,0x00); } void udpHandler::sendLogin() // Only used on control stream. { qInfo(logUdp()) << this->metaObject()->className() << ": Sending login packet"; tokRequest = static_cast(rand() | rand() << 8); // Generate random token request. QByteArray usernameEncoded; QByteArray passwordEncoded; passcode(username, usernameEncoded); passcode(password, passwordEncoded); login_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.sentid = myId; p.rcvdid = remoteId; p.code = 0x0170; // Not sure what this is? p.innerseq = authSeq++; p.tokrequest = tokRequest; memcpy(p.username, usernameEncoded.constData(), usernameEncoded.length()); memcpy(p.password, passwordEncoded.constData(), passwordEncoded.length()); memcpy(p.name, compName.toLocal8Bit().constData(), compName.length()); sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); return; } void udpHandler::sendToken(uint8_t magic) { qDebug(logUdp()) << this->metaObject()->className() << "Sending Token request: " << magic; token_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.sentid = myId; p.rcvdid = remoteId; p.code = 0x0130; // Not sure what this is? p.res = magic; p.innerseq = authSeq++; p.tokrequest = tokRequest; p.token = token; sendTrackedPacket(QByteArray::fromRawData((const char *)p.packet, sizeof(p))); // The radio should request a repeat of the token renewal packet via retransmission! //tokenTimer->start(100); // Set 100ms timer for retry (this will be cancelled if a response is received) return; } // Class that manages all Civ Data to/from the rig udpCivData::udpCivData(QHostAddress local, QHostAddress ip, quint16 civPort) { qInfo(logUdp()) << "Starting udpCivData"; localIP = local; port = civPort; radioIP = ip; udpBase::init(); // Perform connection QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpCivData::dataReceived); sendControl(false, 0x03, 0x00); // First connect packet /* Connect various timers */ pingTimer = new QTimer(); idleTimer = new QTimer(); areYouThereTimer = new QTimer(); startCivDataTimer = new QTimer(); watchdogTimer = new QTimer(); connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing); connect(watchdogTimer, &QTimer::timeout, this, &udpCivData::watchdog); connect(idleTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, true, 0, 0)); connect(startCivDataTimer, &QTimer::timeout, this, std::bind(&udpCivData::sendOpenClose, this, false)); connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0)); watchdogTimer->start(WATCHDOG_PERIOD); // Start sending are you there packets - will be stopped once "I am here" received // send ping packets every 100 ms (maybe change to less frequent?) pingTimer->start(PING_PERIOD); // Send idle packets every 100ms, this timer will be reset everytime a non-idle packet is sent. idleTimer->start(IDLE_PERIOD); areYouThereTimer->start(AREYOUTHERE_PERIOD); } udpCivData::~udpCivData() { sendOpenClose(true); if (startCivDataTimer != Q_NULLPTR) { startCivDataTimer->stop(); delete startCivDataTimer; startCivDataTimer = Q_NULLPTR; } if (pingTimer != Q_NULLPTR) { pingTimer->stop(); delete pingTimer; pingTimer = Q_NULLPTR; } if (idleTimer != Q_NULLPTR) { idleTimer->stop(); delete idleTimer; idleTimer = Q_NULLPTR; } if (watchdogTimer != Q_NULLPTR) { watchdogTimer->stop(); delete watchdogTimer; watchdogTimer = Q_NULLPTR; } } void udpCivData::watchdog() { static bool alerted = false; if (lastReceived.msecsTo(QTime::currentTime()) > 2000) { if (!alerted) { qInfo(logUdp()) << " CIV Watchdog: no CIV data received for 2s, requesting data start."; if (startCivDataTimer != Q_NULLPTR) { startCivDataTimer->start(100); } alerted = true; } } else { alerted = false; } } void udpCivData::send(QByteArray d) { //qInfo(logUdp()) << "Sending: (" << d.length() << ") " << d; data_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p)+d.length(); p.sentid = myId; p.rcvdid = remoteId; p.reply = (char)0xc1; p.datalen = d.length(); p.sendseq = qToBigEndian(sendSeqB); // THIS IS BIG ENDIAN! QByteArray t = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); t.append(d); sendTrackedPacket(t); sendSeqB++; return; } void udpCivData::sendOpenClose(bool close) { uint8_t magic = 0x04; if (close) { magic = 0x00; } openclose_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.sentid = myId; p.rcvdid = remoteId; p.data = 0x01c0; // Not sure what other values are available: p.sendseq = qToBigEndian(sendSeqB); p.magic = magic; sendSeqB++; sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); return; } void udpCivData::dataReceived() { while (udp->hasPendingDatagrams()) { QNetworkDatagram datagram = udp->receiveDatagram(); //qInfo(logUdp()) << "Received: " << datagram.data(); QByteArray r = datagram.data(); switch (r.length()) { case (CONTROL_SIZE): // Control packet { control_packet_t in = (control_packet_t)r.constData(); if (in->type == 0x04) { areYouThereTimer->stop(); } else if (in->type == 0x06) { // Update remoteId remoteId = in->sentid; // Manually send a CIV start request and start the timer if it isn't received. // The timer will be stopped as soon as valid CIV data is received. sendOpenClose(false); if (startCivDataTimer != Q_NULLPTR) { startCivDataTimer->start(100); } } break; } default: { if (r.length() > 21) { data_packet_t in = (data_packet_t)r.constData(); if (in->type != 0x01) { // Process this packet, any re-transmit requests will happen later. //uint16_t gotSeq = qFromLittleEndian(r.mid(6, 2)); // We have received some Civ data so stop sending Start packets! if (startCivDataTimer != Q_NULLPTR) { startCivDataTimer->stop(); } lastReceived = QTime::currentTime(); if (quint16(in->datalen + 0x15) == (quint16)in->len) { //if (r.mid(0x15).length() != 157) emit receive(r.mid(0x15)); //qDebug(logUdp()) << "Got incoming CIV datagram" << r.mid(0x15).length(); } } } break; } } udpBase::dataReceived(r); // Call parent function to process the rest. r.clear(); datagram.clear(); } } // Audio stream udpAudio::udpAudio(QHostAddress local, QHostAddress ip, quint16 audioPort, audioSetup rxSetup, audioSetup txSetup) { qInfo(logUdp()) << "Starting udpAudio"; this->localIP = local; this->port = audioPort; this->radioIP = ip; if (txSetup.samplerate == 0) { enableTx = false; } init(); // Perform connection QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpAudio::dataReceived); rxaudio = new audioHandler(); rxAudioThread = new QThread(this); rxaudio->moveToThread(rxAudioThread); rxAudioThread->start(QThread::TimeCriticalPriority); connect(this, SIGNAL(setupRxAudio(audioSetup)), rxaudio, SLOT(init(audioSetup))); // signal/slot not currently used. connect(this, SIGNAL(haveAudioData(audioPacket)), rxaudio, SLOT(incomingAudio(audioPacket))); connect(this, SIGNAL(haveChangeLatency(quint16)), rxaudio, SLOT(changeLatency(quint16))); connect(this, SIGNAL(haveSetVolume(unsigned char)), rxaudio, SLOT(setVolume(unsigned char))); connect(rxAudioThread, SIGNAL(finished()), rxaudio, SLOT(deleteLater())); txSetup.radioChan = 1; txaudio = new audioHandler(); txAudioThread = new QThread(this); txaudio->moveToThread(txAudioThread); txAudioThread->start(QThread::TimeCriticalPriority); connect(this, SIGNAL(setupTxAudio(audioSetup)), txaudio, SLOT(init(audioSetup))); connect(txAudioThread, SIGNAL(finished()), txaudio, SLOT(deleteLater())); sendControl(false, 0x03, 0x00); // First connect packet pingTimer = new QTimer(); connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing); pingTimer->start(PING_PERIOD); // send ping packets every 100ms if (enableTx) { emit setupTxAudio(txSetup); } emit setupRxAudio(rxSetup); watchdogTimer = new QTimer(); connect(watchdogTimer, &QTimer::timeout, this, &udpAudio::watchdog); watchdogTimer->start(WATCHDOG_PERIOD); txAudioTimer = new QTimer(); txAudioTimer->setTimerType(Qt::PreciseTimer); connect(txAudioTimer, &QTimer::timeout, this, &udpAudio::sendTxAudio); areYouThereTimer = new QTimer(); connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0)); areYouThereTimer->start(AREYOUTHERE_PERIOD); } udpAudio::~udpAudio() { if (pingTimer != Q_NULLPTR) { qDebug(logUdp()) << "Stopping pingTimer"; pingTimer->stop(); delete pingTimer; pingTimer = Q_NULLPTR; } if (idleTimer != Q_NULLPTR) { qDebug(logUdp()) << "Stopping idleTimer"; idleTimer->stop(); delete idleTimer; idleTimer = Q_NULLPTR; } if (watchdogTimer != Q_NULLPTR) { qDebug(logUdp()) << "Stopping watchdogTimer"; watchdogTimer->stop(); delete watchdogTimer; watchdogTimer = Q_NULLPTR; } if (txAudioTimer != Q_NULLPTR) { qDebug(logUdp()) << "Stopping txaudio timer"; txAudioTimer->stop(); delete txAudioTimer; } if (rxAudioThread != Q_NULLPTR) { qDebug(logUdp()) << "Stopping rxaudio thread"; rxAudioThread->quit(); rxAudioThread->wait(); } if (txAudioThread != Q_NULLPTR) { qDebug(logUdp()) << "Stopping txaudio thread"; txAudioThread->quit(); txAudioThread->wait(); } qDebug(logUdp()) << "udpHandler successfully closed"; } void udpAudio::watchdog() { static bool alerted = false; if (lastReceived.msecsTo(QTime::currentTime()) > 2000) { if (!alerted) { /* Just log it at the moment, maybe try signalling the control channel that it needs to try requesting civ/audio again? */ qInfo(logUdp()) << " Audio Watchdog: no audio data received for 2s, restart required?"; alerted = true; } } else { alerted = false; } } void udpAudio::sendTxAudio() { if (txaudio == Q_NULLPTR) { return; } QByteArray audio; if (audioMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { txaudio->getNextAudioChunk(audio); // Now we have the next audio chunk, we can release the mutex. audioMutex.unlock(); if (audio.length() > 0) { int counter = 1; int len = 0; while (len < audio.length()) { QByteArray partial = audio.mid(len, 1364); audio_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p) + partial.length(); p.sentid = myId; p.rcvdid = remoteId; if (partial.length() == 0xa0) { p.ident = 0x9781; } else { p.ident = 0x0080; // TX audio is always this? } p.datalen = (quint16)qToBigEndian((quint16)partial.length()); p.sendseq = (quint16)qToBigEndian((quint16)sendAudioSeq); // THIS IS BIG ENDIAN! QByteArray tx = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); tx.append(partial); len = len + partial.length(); //qInfo(logUdp()) << "Sending audio packet length: " << tx.length(); sendTrackedPacket(tx); sendAudioSeq++; counter++; } } } else { qInfo(logUdpServer()) << "Unable to lock mutex for rxaudio"; } } void udpAudio::changeLatency(quint16 value) { emit haveChangeLatency(value); } void udpAudio::setVolume(unsigned char value) { emit haveSetVolume(value); } void udpAudio::dataReceived() { while (udp->hasPendingDatagrams()) { QNetworkDatagram datagram = udp->receiveDatagram(); //qInfo(logUdp()) << "Received: " << datagram.data().mid(0,10); QByteArray r = datagram.data(); switch (r.length()) { case (16): // Response to control packet handled in udpBase { control_packet_t in = (control_packet_t)r.constData(); if (in->type == 0x04 && enableTx) { txAudioTimer->start(TXAUDIO_PERIOD); } break; } default: { /* Audio packets start as follows: PCM 16bit and PCM8/uLAW stereo: 0x44,0x02 for first packet and 0x6c,0x05 for second. uLAW 8bit/PCM 8bit 0xd8,0x03 for all packets PCM 16bit stereo 0x6c,0x05 first & second 0x70,0x04 third */ control_packet_t in = (control_packet_t)r.constData(); if (in->type != 0x01 && in->len >= 0x20) { if (in->seq == 0) { // Seq number has rolled over. seqPrefix++; } // 0xac is the smallest possible audio packet. lastReceived = QTime::currentTime(); audioPacket tempAudio; tempAudio.seq = (quint32)seqPrefix << 16 | in->seq; tempAudio.time = lastReceived; tempAudio.sent = 0; tempAudio.data = r.mid(0x18); // Prefer signal/slot to forward audio as it is thread/safe // Need to do more testing but latency appears fine. //rxaudio->incomingAudio(tempAudio); emit haveAudioData(tempAudio); audioLatency = rxaudio->getLatency(); } break; } } udpBase::dataReceived(r); // Call parent function to process the rest. r.clear(); datagram.clear(); } } void udpBase::init() { timeStarted.start(); udp = new QUdpSocket(this); udp->bind(); // Bind to random port. localPort = udp->localPort(); qInfo(logUdp()) << "UDP Stream bound to local port:" << localPort << " remote port:" << port; uint32_t addr = localIP.toIPv4Address(); myId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (localPort & 0xffff); retransmitTimer = new QTimer(); connect(retransmitTimer, &QTimer::timeout, this, &udpBase::sendRetransmitRequest); retransmitTimer->start(RETRANSMIT_PERIOD); } udpBase::~udpBase() { qInfo(logUdp()) << "Closing UDP stream :" << radioIP.toString() << ":" << port; if (udp != Q_NULLPTR) { sendControl(false, 0x05, 0x00); // Send disconnect udp->close(); delete udp; } if (areYouThereTimer != Q_NULLPTR) { areYouThereTimer->stop(); delete areYouThereTimer; } if (pingTimer != Q_NULLPTR) { pingTimer->stop(); delete pingTimer; } if (idleTimer != Q_NULLPTR) { idleTimer->stop(); delete idleTimer; } if (retransmitTimer != Q_NULLPTR) { retransmitTimer->stop(); delete retransmitTimer; } pingTimer = Q_NULLPTR; idleTimer = Q_NULLPTR; areYouThereTimer = Q_NULLPTR; retransmitTimer = Q_NULLPTR; } // Base class! void udpBase::dataReceived(QByteArray r) { if (r.length() < 0x10) { return; // Packet too small do to anything with? } switch (r.length()) { case (CONTROL_SIZE): // Empty response used for simple comms and retransmit requests. { control_packet_t in = (control_packet_t)r.constData(); if (in->type == 0x01) { // Single packet request packetsLost++; congestion ++; txBufferMutex.lock(); QMap::iterator match = txSeqBuf.find(in->seq); if (match != txSeqBuf.end()) { // Found matching entry? // Send "untracked" as it has already been sent once. // Don't constantly retransmit the same packet, give-up eventually qDebug(logUdp()) << this->metaObject()->className() << ": Sending retransmit of " << hex << match->seqNum; match->retransmitCount++; udpMutex.lock(); udp->writeDatagram(match->data, radioIP, port); udpMutex.unlock(); } txBufferMutex.unlock(); } if (in->type == 0x04) { qInfo(logUdp()) << this->metaObject()->className() << ": Received I am here "; areYouThereCounter = 0; // I don't think that we will ever receive an "I am here" other than in response to "Are you there?" remoteId = in->sentid; if (areYouThereTimer != Q_NULLPTR && areYouThereTimer->isActive()) { // send ping packets every second areYouThereTimer->stop(); } sendControl(false, 0x06, 0x01); // Send Are you ready - untracked. } else if (in->type == 0x06) { // Just get the seqnum and ignore the rest. } break; } case (PING_SIZE): // ping packet { ping_packet_t in = (ping_packet_t)r.constData(); if (in->type == 0x07) { // It is a ping request/response if (in->reply == 0x00) { ping_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = 0x07; p.sentid = myId; p.rcvdid = remoteId; p.reply = 0x01; p.seq = in->seq; p.time = in->time; udpMutex.lock(); udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); udpMutex.unlock(); } else if (in->reply == 0x01) { if (in->seq == pingSendSeq) { // This is response to OUR request so increment counter pingSendSeq++; } else { // Not sure what to do here, need to spend more time with the protocol but will try sending ping with same seq next time. //qInfo(logUdp()) << this->metaObject()->className() << "Received out-of-sequence ping response. Sent:" << pingSendSeq << " received " << in->seq; } } else { qInfo(logUdp()) << this->metaObject()->className() << "Unhandled response to ping. I have never seen this! 0x10=" << r[16]; } } break; } default: { // All packets "should" be added to the incoming buffer. // First check that we haven't already received it. } break; } // All packets except ping and retransmit requests should trigger this. control_packet_t in = (control_packet_t)r.constData(); // This is a variable length retransmit request! if (in->type == 0x01 && in->len != 0x10) { for (quint16 i = 0x10; i < r.length(); i = i + 2) { quint16 seq = (quint8)r[i] | (quint8)r[i + 1] << 8; QMap::iterator match = txSeqBuf.find(seq); if (match == txSeqBuf.end()) { qDebug(logUdp()) << this->metaObject()->className() << ": Requested packet " << hex << seq << " not found"; // Just send idle packet. sendControl(false, 0, seq); } else { // Found matching entry? // Send "untracked" as it has already been sent once. qDebug(logUdp()) << this->metaObject()->className() << ": Sending retransmit (range) of " << hex << match->seqNum; match->retransmitCount++; udpMutex.lock(); udp->writeDatagram(match->data, radioIP, port); udpMutex.unlock(); match++; packetsLost++; congestion++; } } } else if (in->len != PING_SIZE && in->type == 0x00 && in->seq != 0x00) { rxBufferMutex.lock(); if (rxSeqBuf.isEmpty()) { if (rxSeqBuf.size() > 400) { rxSeqBuf.erase(rxSeqBuf.begin()); } rxSeqBuf.insert(in->seq, QTime::currentTime()); } else { //std::sort(rxSeqBuf.begin(), rxSeqBuf.end()); if (in->seq < rxSeqBuf.firstKey()) { qInfo(logUdp()) << this->metaObject()->className() << ": ******* seq number has rolled over ****** previous highest: " << hex << rxSeqBuf.lastKey() << " current: " << hex << in->seq; //seqPrefix++; // Looks like it has rolled over so clear buffer and start again. rxSeqBuf.clear(); rxMissing.clear(); rxBufferMutex.unlock(); return; } if (!rxSeqBuf.contains(in->seq)) { // Add incoming packet to the received buffer and if it is in the missing buffer, remove it. rxSeqBuf.insert(in->seq, QTime::currentTime()); if (rxSeqBuf.size() > 400) { rxSeqBuf.erase(rxSeqBuf.begin()); } } else { // This is probably one of our missing packets! missingMutex.lock(); QMap::iterator s = rxMissing.find(in->seq); if (s != rxMissing.end()) { qDebug(logUdp()) << this->metaObject()->className() << ": Missing SEQ has been received! " << hex << in->seq; s = rxMissing.erase(s); } missingMutex.unlock(); } } rxBufferMutex.unlock(); } } bool missing(quint16 i, quint16 j) { return (i + 1 != j); } void udpBase::sendRetransmitRequest() { // Find all gaps in received packets and then send requests for them. // This will run every 100ms so out-of-sequence packets will not trigger a retransmit request. QByteArray missingSeqs; rxBufferMutex.lock(); if (!rxSeqBuf.empty() && rxSeqBuf.size() <= rxSeqBuf.lastKey() - rxSeqBuf.firstKey()) { if ((rxSeqBuf.lastKey() - rxSeqBuf.firstKey() - rxSeqBuf.size()) > 20) { // Too many packets to process, flush buffers and start again! qDebug(logUdp()) << "Too many missing packets, flushing buffer: " << rxSeqBuf.lastKey() << "missing=" << rxSeqBuf.lastKey() - rxSeqBuf.firstKey() - rxSeqBuf.size() + 1; rxMissing.clear(); missingMutex.lock(); rxSeqBuf.clear(); missingMutex.unlock(); } else { // We have at least 1 missing packet! qDebug(logUdp()) << "Missing Seq: size=" << rxSeqBuf.size() << "firstKey=" << rxSeqBuf.firstKey() << "lastKey=" << rxSeqBuf.lastKey() << "missing=" << rxSeqBuf.lastKey() - rxSeqBuf.firstKey() - rxSeqBuf.size() + 1; // We are missing packets so iterate through the buffer and add the missing ones to missing packet list for (int i = 0; i < rxSeqBuf.keys().length() - 1; i++) { missingMutex.lock(); for (quint16 j = rxSeqBuf.keys()[i] + 1; j < rxSeqBuf.keys()[i + 1]; j++) { auto s = rxMissing.find(j); if (s == rxMissing.end()) { // We haven't seen this missing packet before qDebug(logUdp()) << this->metaObject()->className() << ": Adding to missing buffer (len=" << rxMissing.size() << "): " << j; if (rxMissing.size() > 25) { rxMissing.erase(rxMissing.begin()); } rxMissing.insert(j, 0); if (rxSeqBuf.size() > 400) { rxSeqBuf.erase(rxSeqBuf.begin()); } rxSeqBuf.insert(j, QTime::currentTime()); // Add this missing packet to the rxbuffer as we now long about it. packetsLost++; } else { if (s.value() == 4) { // We have tried 4 times to request this packet, time to give up! s = rxMissing.erase(s); } } } missingMutex.unlock(); } } } rxBufferMutex.unlock(); missingMutex.lock(); for (auto it = rxMissing.begin(); it != rxMissing.end(); ++it) { if (it.value() < 10) { missingSeqs.append(it.key() & 0xff); missingSeqs.append(it.key() >> 8 & 0xff); missingSeqs.append(it.key() & 0xff); missingSeqs.append(it.key() >> 8 & 0xff); it.value()++; } } missingMutex.unlock(); if (missingSeqs.length() != 0) { control_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.type = 0x01; p.seq = 0x0000; p.sentid = myId; p.rcvdid = remoteId; if (missingSeqs.length() == 4) // This is just a single missing packet so send using a control. { p.seq = (missingSeqs[0] & 0xff) | (quint16)(missingSeqs[1] << 8); qDebug(logUdp()) << this->metaObject()->className() << ": sending request for missing packet : " << hex << p.seq; udpMutex.lock(); udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); udpMutex.unlock(); } else { qDebug(logUdp()) << this->metaObject()->className() << ": sending request for multiple missing packets : " << missingSeqs.toHex(); missingMutex.lock(); missingSeqs.insert(0, p.packet, sizeof(p.packet)); missingMutex.unlock(); udpMutex.lock(); udp->writeDatagram(missingSeqs, radioIP, port); udpMutex.unlock(); } } } // Used to send idle and other "control" style messages void udpBase::sendControl(bool tracked = true, quint8 type = 0, quint16 seq = 0) { control_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = type; p.sentid = myId; p.rcvdid = remoteId; if (!tracked) { p.seq = seq; udpMutex.lock(); udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); udpMutex.unlock(); } else { sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); } return; } // Send periodic ping packets void udpBase::sendPing() { ping_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = 0x07; p.sentid = myId; p.rcvdid = remoteId; p.seq = pingSendSeq; p.time = timeStarted.msecsSinceStartOfDay(); lastPingSentTime = QDateTime::currentDateTime(); udpMutex.lock(); udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); udpMutex.unlock(); return; } void udpBase::sendRetransmitRange(quint16 first, quint16 second, quint16 third, quint16 fourth) { retransmit_range_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = 0x00; p.sentid = myId; p.rcvdid = remoteId; p.first = first; p.second = second; p.third = third; p.fourth = fourth; udpMutex.lock(); udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); udpMutex.unlock(); return; } void udpBase::sendTrackedPacket(QByteArray d) { // As the radio can request retransmission of these packets, store them in a buffer d[6] = sendSeq & 0xff; d[7] = (sendSeq >> 8) & 0xff; SEQBUFENTRY s; s.seqNum = sendSeq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = d; if (txBufferMutex.tryLock(100)) { if (sendSeq == 0) { // We are either the first ever sent packet or have rolled-over so clear the buffer. txSeqBuf.clear(); congestion = 0; } txSeqBuf.insert(sendSeq,s); if (txSeqBuf.size() > 400) { txSeqBuf.erase(txSeqBuf.begin()); } txBufferMutex.unlock(); } else { qInfo(logUdp()) << this->metaObject()->className() << ": txBuffer mutex is locked"; } // Stop using purgeOldEntries() as it is likely slower than just removing the earliest packet. //qInfo(logUdp()) << this->metaObject()->className() << "RX:" << rxSeqBuf.size() << "TX:" <writeDatagram(d, radioIP, port); if (congestion>10) { // Poor quality connection? udp->writeDatagram(d, radioIP, port); if (congestion>20) // Even worse so send again. udp->writeDatagram(d, radioIP, port); } if (idleTimer != Q_NULLPTR && idleTimer->isActive()) { idleTimer->start(IDLE_PERIOD); // Reset idle counter if it's running } udpMutex.unlock(); packetsSent++; return; } /// /// Once a packet has reached PURGE_SECONDS old (currently 10) then it is not likely to be any use. /// void udpBase::purgeOldEntries() { // Erase old entries from the tx packet buffer if (txBufferMutex.tryLock(100)) { if (!txSeqBuf.isEmpty()) { // Loop through the earliest items in the buffer and delete if older than PURGE_SECONDS for (auto it = txSeqBuf.begin(); it != txSeqBuf.end();) { if (it.value().timeSent.secsTo(QTime::currentTime()) > PURGE_SECONDS) { txSeqBuf.erase(it++); } else { break; } } } txBufferMutex.unlock(); } else { qInfo(logUdp()) << this->metaObject()->className() << ": txBuffer mutex is locked"; } if (rxBufferMutex.tryLock(100)) { if (!rxSeqBuf.isEmpty()) { // Loop through the earliest items in the buffer and delete if older than PURGE_SECONDS for (auto it = rxSeqBuf.begin(); it != rxSeqBuf.end();) { if (it.value().secsTo(QTime::currentTime()) > PURGE_SECONDS) { rxSeqBuf.erase(it++); } else { break; } } } rxBufferMutex.unlock(); } else { qInfo(logUdp()) << this->metaObject()->className() << ": rxBuffer mutex is locked"; } if (missingMutex.tryLock(100)) { // Erase old entries from the missing packets buffer if (!rxMissing.isEmpty() && rxMissing.size() > 50) { for (size_t i = 0; i < 25; ++i) { rxMissing.erase(rxMissing.begin()); } } missingMutex.unlock(); } else { qInfo(logUdp()) << this->metaObject()->className() << ": missingBuffer mutex is locked"; } } /// /// passcode function used to generate secure (ish) code /// /// /// pointer to encoded username or password void passcode(QString in, QByteArray& out) { const quint8 sequence[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0x47,0x5d,0x4c,0x42,0x66,0x20,0x23,0x46,0x4e,0x57,0x45,0x3d,0x67,0x76,0x60,0x41,0x62,0x39,0x59,0x2d,0x68,0x7e, 0x7c,0x65,0x7d,0x49,0x29,0x72,0x73,0x78,0x21,0x6e,0x5a,0x5e,0x4a,0x3e,0x71,0x2c,0x2a,0x54,0x3c,0x3a,0x63,0x4f, 0x43,0x75,0x27,0x79,0x5b,0x35,0x70,0x48,0x6b,0x56,0x6f,0x34,0x32,0x6c,0x30,0x61,0x6d,0x7b,0x2f,0x4b,0x64,0x38, 0x2b,0x2e,0x50,0x40,0x3f,0x55,0x33,0x37,0x25,0x77,0x24,0x26,0x74,0x6a,0x28,0x53,0x4d,0x69,0x22,0x5c,0x44,0x31, 0x36,0x58,0x3b,0x7a,0x51,0x5f,0x52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; QByteArray ba = in.toLocal8Bit(); uchar* ascii = (uchar*)ba.constData(); for (int i = 0; i < in.length() && i < 16; i++) { int p = ascii[i] + i; if (p > 126) { p = 32 + p % 127; } out.append(sequence[p]); } return; } /// /// returns a QByteArray of a null terminated string /// /// /// /// QByteArray parseNullTerminatedString(QByteArray c, int s) { //QString res = ""; QByteArray res; for (int i = s; i < c.length(); i++) { if (c[i] != '\0') { res.append(c[i]); } else { break; } } return res; } wfview-1.2d/udphandler.h000066400000000000000000000134441415164626400153450ustar00rootroot00000000000000#ifndef UDPHANDLER_H #define UDPHANDLER_H #include #include #include #include #include #include #include #include #include #include // Allow easy endian-ness conversions #include // Needed for audio #include #include #include #include "audiohandler.h" #include "packettypes.h" #define PURGE_SECONDS 10 #define TOKEN_RENEWAL 60000 #define PING_PERIOD 100 #define IDLE_PERIOD 100 #define TXAUDIO_PERIOD 10 #define AREYOUTHERE_PERIOD 500 #define WATCHDOG_PERIOD 500 #define RETRANSMIT_PERIOD 100 #define LOCK_PERIOD 100 struct udpPreferences { QString ipAddress; quint16 controlLANPort; quint16 serialLANPort; quint16 audioLANPort; QString username; QString password; QString clientName; }; void passcode(QString in, QByteArray& out); QByteArray parseNullTerminatedString(QByteArray c, int s); // Parent class that contains all common items. class udpBase : public QObject { public: ~udpBase(); void init(); void dataReceived(QByteArray r); void sendPing(); void sendRetransmitRange(quint16 first, quint16 second, quint16 third,quint16 fourth); void sendControl(bool tracked,quint8 id, quint16 seq); QTime timeStarted; QUdpSocket* udp=Q_NULLPTR; uint32_t myId = 0; uint32_t remoteId = 0; uint8_t authSeq = 0x00; uint16_t sendSeqB = 0; uint16_t sendSeq = 1; uint16_t lastReceivedSeq = 1; uint16_t pkt0SendSeq = 0; uint16_t periodicSeq = 0; quint64 latency = 0; QString username = ""; QString password = ""; QHostAddress radioIP; QHostAddress localIP; bool isAuthenticated = false; quint16 localPort=0; quint16 port=0; bool periodicRunning = false; bool sentPacketConnect2 = false; QTime lastReceived =QTime::currentTime(); QMutex udpMutex; QMutex txBufferMutex; QMutex rxBufferMutex; QMutex missingMutex; struct SEQBUFENTRY { QTime timeSent; uint16_t seqNum; QByteArray data; quint8 retransmitCount; }; QMap rxSeqBuf; QMap txSeqBuf; QMap rxMissing; void sendTrackedPacket(QByteArray d); void purgeOldEntries(); QTimer* areYouThereTimer = Q_NULLPTR; // Send are-you-there packets every second until a response is received. QTimer* pingTimer = Q_NULLPTR; // Start sending pings immediately. QTimer* idleTimer = Q_NULLPTR; // Start watchdog once we are connected. QTimer* watchdogTimer = Q_NULLPTR; QTimer* retransmitTimer = Q_NULLPTR; QDateTime lastPingSentTime; uint16_t pingSendSeq = 0; quint16 areYouThereCounter=0; quint32 packetsSent=0; quint32 packetsLost=0; quint16 seqPrefix = 0; QString connectionType=""; int congestion = 0; private: void sendRetransmitRequest(); }; // Class for all (pseudo) serial communications class udpCivData : public udpBase { Q_OBJECT public: udpCivData(QHostAddress local, QHostAddress ip, quint16 civPort); ~udpCivData(); QMutex serialmutex; signals: int receive(QByteArray); public slots: void send(QByteArray d); private: void watchdog(); void dataReceived(); void sendOpenClose(bool close); QTimer* startCivDataTimer = Q_NULLPTR; }; // Class for all audio communications. class udpAudio : public udpBase { Q_OBJECT public: udpAudio(QHostAddress local, QHostAddress ip, quint16 aport, audioSetup rxSetup, audioSetup txSetup); ~udpAudio(); int audioLatency = 0; signals: void haveAudioData(audioPacket data); void setupTxAudio(audioSetup setup); void setupRxAudio(audioSetup setup); void haveChangeLatency(quint16 value); void haveSetVolume(unsigned char value); public slots: void changeLatency(quint16 value); void setVolume(unsigned char value); private: void sendTxAudio(); void dataReceived(); void watchdog(); uint16_t sendAudioSeq = 0; audioHandler* rxaudio = Q_NULLPTR; QThread* rxAudioThread = Q_NULLPTR; audioHandler* txaudio = Q_NULLPTR; QThread* txAudioThread = Q_NULLPTR; QTimer* txAudioTimer=Q_NULLPTR; bool enableTx = true; QMutex audioMutex; }; // Class to handle the connection/disconnection of the radio. class udpHandler: public udpBase { Q_OBJECT public: udpHandler(udpPreferences prefs, audioSetup rxAudio, audioSetup txAudio); ~udpHandler(); bool streamOpened = false; udpCivData* civ = Q_NULLPTR; udpAudio* audio = Q_NULLPTR; public slots: void receiveDataFromUserToRig(QByteArray); // This slot will send data on to void receiveFromCivStream(QByteArray); void receiveAudioData(const audioPacket &data); void changeLatency(quint16 value); void setVolume(unsigned char value); void init(); signals: void haveDataFromPort(QByteArray data); // emit this when we have data, connect to rigcommander void haveAudioData(audioPacket data); // emit this when we have data, connect to rigcommander void haveNetworkError(QString, QString); void haveChangeLatency(quint16 value); void haveSetVolume(unsigned char value); void haveNetworkStatus(QString); void haveBaudRate(quint32 baudrate); private: void sendAreYouThere(); void dataReceived(); void sendRequestStream(); void sendLogin(); void sendToken(uint8_t magic); bool gotA8ReplyID = false; bool gotAuthOK = false; bool sentPacketLogin = false; bool sentPacketConnect = false; bool sentPacketConnect2 = false; bool radioInUse = false; quint16 controlPort; quint16 civPort; quint16 audioPort; audioSetup rxSetup; audioSetup txSetup; quint16 reauthInterval = 60000; QString devName; QString compName; QString audioType; //QByteArray replyId; quint16 tokRequest; quint32 token; // These are for stream ident info. char identa; quint32 identb; QByteArray usernameEncoded; QByteArray passwordEncoded; QTimer* tokenTimer = Q_NULLPTR; QTimer* areYouThereTimer = Q_NULLPTR; bool highBandwidthConnection = false; quint8 civId = 0; quint16 rxSampleRates = 0; quint16 txSampleRates = 0; }; #endif wfview-1.2d/udpserver.cpp000066400000000000000000001745721415164626400156030ustar00rootroot00000000000000#include "udpserver.h" #include "logcategories.h" #define STALE_CONNECTION 15 #define LOCK_PERIOD 10 // time to attempt to lock Mutex in ms udpServer::udpServer(SERVERCONFIG config, audioSetup outAudio, audioSetup inAudio) : config(config), outAudio(outAudio), inAudio(inAudio) { qInfo(logUdpServer()) << "Starting udp server"; } void udpServer::init() { srand(time(NULL)); // Generate random key timeStarted.start(); // Convoluted way to find the external IP address, there must be a better way???? QString localhostname = QHostInfo::localHostName(); QList hostList = QHostInfo::fromName(localhostname).addresses(); foreach(const QHostAddress & address, hostList) { if (address.protocol() == QAbstractSocket::IPv4Protocol && address.isLoopback() == false) { localIP = QHostAddress(address.toString()); } } foreach(QNetworkInterface netInterface, QNetworkInterface::allInterfaces()) { // Return only the first non-loopback MAC Address if (!(netInterface.flags() & QNetworkInterface::IsLoopBack)) { macAddress = netInterface.hardwareAddress(); } } uint32_t addr = localIP.toIPv4Address(); qInfo(logUdpServer()) << " My IP Address: " << QHostAddress(addr).toString() << " My MAC Address: " << macAddress; controlId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config.controlPort & 0xffff); civId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config.civPort & 0xffff); audioId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config.audioPort & 0xffff); qInfo(logUdpServer()) << "Server Binding Control to: " << config.controlPort; udpControl = new QUdpSocket(this); udpControl->bind(config.controlPort); QUdpSocket::connect(udpControl, &QUdpSocket::readyRead, this, &udpServer::controlReceived); qInfo(logUdpServer()) << "Server Binding CIV to: " << config.civPort; udpCiv = new QUdpSocket(this); udpCiv->bind(config.civPort); QUdpSocket::connect(udpCiv, &QUdpSocket::readyRead, this, &udpServer::civReceived); qInfo(logUdpServer()) << "Server Binding Audio to: " << config.audioPort; udpAudio = new QUdpSocket(this); udpAudio->bind(config.audioPort); QUdpSocket::connect(udpAudio, &QUdpSocket::readyRead, this, &udpServer::audioReceived); wdTimer = new QTimer(); connect(wdTimer, &QTimer::timeout, this, &udpServer::watchdog); wdTimer->start(500); } udpServer::~udpServer() { qInfo(logUdpServer()) << "Closing udpServer"; foreach(CLIENT * client, controlClients) { deleteConnection(&controlClients, client); } foreach(CLIENT * client, civClients) { deleteConnection(&civClients, client); } foreach(CLIENT * client, audioClients) { deleteConnection(&audioClients, client); } // Now all connections are deleted, close and delete the sockets. if (udpControl != Q_NULLPTR) { udpControl->close(); delete udpControl; } if (udpCiv != Q_NULLPTR) { udpCiv->close(); delete udpCiv; } if (udpAudio != Q_NULLPTR) { udpAudio->close(); delete udpAudio; } } void udpServer::receiveRigCaps(rigCapabilities caps) { this->rigCaps = caps; } #define RETRANSMIT_PERIOD 100 void udpServer::controlReceived() { // Received data on control port. while (udpControl->hasPendingDatagrams()) { QNetworkDatagram datagram = udpControl->receiveDatagram(); QByteArray r = datagram.data(); CLIENT* current = Q_NULLPTR; if (datagram.senderAddress().isNull() || datagram.senderPort() == 65535 || datagram.senderPort() == 0) return; foreach(CLIENT * client, controlClients) { if (client != Q_NULLPTR) { if (client->ipAddress == datagram.senderAddress() && client->port == datagram.senderPort()) { current = client; } } } if (current == Q_NULLPTR) { current = new CLIENT(); current->type = "Control"; current->connected = true; current->isAuthenticated = false; current->isStreaming = false; current->timeConnected = QDateTime::currentDateTime(); current->ipAddress = datagram.senderAddress(); current->port = datagram.senderPort(); current->civPort = config.civPort; current->audioPort = config.audioPort; current->myId = controlId; current->remoteId = qFromLittleEndian(r.mid(8, 4)); current->socket = udpControl; current->pingSeq = (quint8)rand() << 8 | (quint8)rand(); current->pingTimer = new QTimer(); connect(current->pingTimer, &QTimer::timeout, this, std::bind(&udpServer::sendPing, this, &controlClients, current, (quint16)0x00, false)); current->pingTimer->start(100); current->idleTimer = new QTimer(); connect(current->idleTimer, &QTimer::timeout, this, std::bind(&udpServer::sendControl, this, current, (quint8)0x00, (quint16)0x00)); current->idleTimer->start(100); current->retransmitTimer = new QTimer(); connect(current->retransmitTimer, &QTimer::timeout, this, std::bind(&udpServer::sendRetransmitRequest, this, current)); current->retransmitTimer->start(RETRANSMIT_PERIOD); current->commonCap = 0x8010; qInfo(logUdpServer()) << current->ipAddress.toString() << ": New Control connection created"; if (connMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { controlClients.append(current); connMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock connMutex()"; } } current->lastHeard = QDateTime::currentDateTime(); switch (r.length()) { case (CONTROL_SIZE): { control_packet_t in = (control_packet_t)r.constData(); if (in->type == 0x05) { qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received 'disconnect' request"; sendControl(current, 0x00, in->seq); if (current->audioClient != Q_NULLPTR) { deleteConnection(&audioClients, current->audioClient); } if (current->civClient != Q_NULLPTR) { deleteConnection(&civClients, current->civClient); } deleteConnection(&controlClients, current); return; } break; } case (PING_SIZE): { ping_packet_t in = (ping_packet_t)r.constData(); if (in->type == 0x07) { // It is a ping request/response if (in->reply == 0x00) { current->rxPingTime = in->time; sendPing(&controlClients, current, in->seq, true); } else if (in->reply == 0x01) { if (in->seq == current->pingSeq || in->seq == current->pingSeq - 1) { // A Reply to our ping! if (in->seq == current->pingSeq) { current->pingSeq++; } else { qInfo(logUdpServer()) << current->ipAddress.toString() << ": got out of sequence ping reply. Got: " << in->seq << " expecting: " << current->pingSeq; } } } } break; } case (TOKEN_SIZE): { // Token request token_packet_t in = (token_packet_t)r.constData(); current->rxSeq = in->seq; current->authInnerSeq = in->innerseq; current->identa = in->identa; current->identb = in->identb; if (in->res == 0x02) { // Request for new token qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received create token request"; sendCapabilities(current); sendConnectionInfo(current); } else if (in->res == 0x01) { // Token disconnect qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received token disconnect request"; sendTokenResponse(current, in->res); } else if (in->res == 0x04) { // Disconnect audio/civ sendTokenResponse(current, in->res); current->isStreaming = false; sendConnectionInfo(current); } else { qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received token request"; sendTokenResponse(current, in->res); } break; } case (LOGIN_SIZE): { login_packet_t in = (login_packet_t)r.constData(); qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received 'login'"; foreach(SERVERUSER user, config.users) { QByteArray usercomp; passcode(user.username, usercomp); QByteArray passcomp; passcode(user.password, passcomp); if (!user.username.trimmed().isEmpty() && !user.password.trimmed().isEmpty() && !strcmp(in->username, usercomp.constData()) && (!strcmp(in->password, user.password.toUtf8()) || !strcmp(in->password, passcomp.constData()))) { current->isAuthenticated = true; current->user = user; break; } } // Generate login response current->rxSeq = in->seq; current->clientName = in->name; current->authInnerSeq = in->innerseq; current->tokenRx = in->tokrequest; current->tokenTx = (quint8)rand() | (quint8)rand() << 8 | (quint8)rand() << 16 | (quint8)rand() << 24; if (current->isAuthenticated) { qInfo(logUdpServer()) << current->ipAddress.toString() << ": User " << current->user.username << " login OK"; } else { qInfo(logUdpServer()) << current->ipAddress.toString() << ": Incorrect username/password"; } sendLoginResponse(current, current->isAuthenticated); break; } case (CONNINFO_SIZE): { conninfo_packet_t in = (conninfo_packet_t)r.constData(); qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received request for radio connection"; // Request to start audio and civ! current->isStreaming = true; current->rxSeq = in->seq; current->rxCodec = in->rxcodec; current->txCodec = in->txcodec; current->rxSampleRate = qFromBigEndian(in->rxsample); current->txSampleRate = qFromBigEndian(in->txsample); current->txBufferLen = qFromBigEndian(in->txbuffer); current->authInnerSeq = in->innerseq; current->identa = in->identa; current->identb = in->identb; sendStatus(current); current->authInnerSeq = 0x00; sendConnectionInfo(current); qInfo(logUdpServer()) << current->ipAddress.toString() << ": rxCodec:" << current->rxCodec << " txCodec:" << current->txCodec << " rxSampleRate" << current->rxSampleRate << " txSampleRate" << current->txSampleRate << " txBufferLen" << current->txBufferLen; if (!config.lan) { // Radio is connected by USB/Serial and we assume that audio is connected as well. // Create audio TX/RX threads if they don't already exist (first client chooses samplerate/codec) audioSetup setup; setup.resampleQuality = config.resampleQuality; if (txaudio == Q_NULLPTR) { outAudio.codec = current->txCodec; outAudio.samplerate = current->txSampleRate; outAudio.latency = current->txBufferLen; txaudio = new audioHandler(); txAudioThread = new QThread(this); txaudio->moveToThread(txAudioThread); txAudioThread->start(QThread::TimeCriticalPriority); connect(this, SIGNAL(setupTxAudio(audioSetup)), txaudio, SLOT(init(audioSetup))); connect(txAudioThread, SIGNAL(finished()), txaudio, SLOT(deleteLater())); emit setupTxAudio(outAudio); hasTxAudio = datagram.senderAddress(); connect(this, SIGNAL(haveAudioData(audioPacket)), txaudio, SLOT(incomingAudio(audioPacket))); } if (rxaudio == Q_NULLPTR) { inAudio.codec = current->rxCodec; inAudio.samplerate = current->rxSampleRate; rxaudio = new audioHandler(); rxAudioThread = new QThread(this); rxaudio->moveToThread(rxAudioThread); rxAudioThread->start(QThread::TimeCriticalPriority); connect(this, SIGNAL(setupRxAudio(audioSetup)), rxaudio, SLOT(init(audioSetup))); connect(rxAudioThread, SIGNAL(finished()), rxaudio, SLOT(deleteLater())); emit setupRxAudio(inAudio); rxAudioTimer = new QTimer(); rxAudioTimer->setTimerType(Qt::PreciseTimer); connect(rxAudioTimer, &QTimer::timeout, this, std::bind(&udpServer::sendRxAudio, this)); rxAudioTimer->start(20); } } break; } default: { break; } } // Connection "may" have been deleted so check before calling common function. if (current != Q_NULLPTR) { commonReceived(&controlClients, current, r); } } } void udpServer::civReceived() { while (udpCiv->hasPendingDatagrams()) { QNetworkDatagram datagram = udpCiv->receiveDatagram(); QByteArray r = datagram.data(); CLIENT* current = Q_NULLPTR; if (datagram.senderAddress().isNull() || datagram.senderPort() == 65535 || datagram.senderPort() == 0) return; QDateTime now = QDateTime::currentDateTime(); foreach(CLIENT * client, civClients) { if (client != Q_NULLPTR) { if (client->ipAddress == datagram.senderAddress() && client->port == datagram.senderPort()) { current = client; } } } if (current == Q_NULLPTR) { current = new CLIENT(); foreach(CLIENT* client, controlClients) { if (client != Q_NULLPTR) { if (client->ipAddress == datagram.senderAddress() && client->isAuthenticated && client->civClient == Q_NULLPTR) { current->controlClient = client; client->civClient = current; } } } if (current->controlClient == Q_NULLPTR || !current->controlClient->isAuthenticated) { // There is no current controlClient that matches this civClient delete current; return; } current->type = "CIV"; current->civId = 0; current->connected = true; current->timeConnected = QDateTime::currentDateTime(); current->ipAddress = datagram.senderAddress(); current->port = datagram.senderPort(); current->myId = civId; current->remoteId = qFromLittleEndian(r.mid(8, 4)); current->socket = udpCiv; current->pingSeq = (quint8)rand() << 8 | (quint8)rand(); current->pingTimer = new QTimer(); connect(current->pingTimer, &QTimer::timeout, this, std::bind(&udpServer::sendPing, this, &civClients, current, (quint16)0x00, false)); current->pingTimer->start(100); current->idleTimer = new QTimer(); connect(current->idleTimer, &QTimer::timeout, this, std::bind(&udpServer::sendControl, this, current, 0x00, (quint16)0x00)); //current->idleTimer->start(100); // Start idleTimer after receiving iamready. current->retransmitTimer = new QTimer(); connect(current->retransmitTimer, &QTimer::timeout, this, std::bind(&udpServer::sendRetransmitRequest, this, current)); current->retransmitTimer->start(RETRANSMIT_PERIOD); qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): New connection created"; if (connMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { civClients.append(current); connMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock connMutex()"; } } switch (r.length()) { /* case (CONTROL_SIZE): { } */ case (PING_SIZE): { ping_packet_t in = (ping_packet_t)r.constData(); if (in->type == 0x07) { // It is a ping request/response if (in->reply == 0x00) { current->rxPingTime = in->time; sendPing(&civClients, current, in->seq, true); } else if (in->reply == 0x01) { if (in->seq == current->pingSeq || in->seq == current->pingSeq - 1) { // A Reply to our ping! if (in->seq == current->pingSeq) { current->pingSeq++; } else { qInfo(logUdpServer()) << current->ipAddress.toString() << ": got out of sequence ping reply. Got: " << in->seq << " expecting: " << current->pingSeq; } } } } break; } default: { if (r.length() > 0x18) { data_packet_t in = (data_packet_t)r.constData(); if (in->type != 0x01) { if (quint16(in->datalen + 0x15) == (quint16)in->len) { // Strip all '0xFE' command preambles first: int lastFE = r.lastIndexOf((char)0xfe); //qInfo(logUdpServer()) << "Got:" << r.mid(lastFE); if (current->civId == 0 && r.length() > lastFE + 2 && (quint8)r[lastFE+2] != 0xE1 && (quint8)r[lastFE + 2] > (quint8)0xdf && (quint8)r[lastFE + 2] < (quint8)0xef) { // This is (should be) the remotes CIV id. current->civId = (quint8)r[lastFE + 2]; qInfo(logUdpServer()) << current->ipAddress.toString() << ": Detected remote CI-V:" << hex << current->civId; } else if (current->civId != 0 && r.length() > lastFE + 2 && (quint8)r[lastFE+2] != 0xE1 && (quint8)r[lastFE + 2] != current->civId) { current->civId = (quint8)r[lastFE + 2]; qDebug(logUdpServer()) << current->ipAddress.toString() << ": Detected different remote CI-V:" << hex << current->civId; qInfo(logUdpServer()) << current->ipAddress.toString() << ": Detected different remote CI-V:" << hex << current->civId; } else if (r.length() > lastFE+2 && (quint8)r[lastFE+2] != 0xE1) { qDebug(logUdpServer()) << current->ipAddress.toString() << ": Detected invalid remote CI-V:" << hex << (quint8)r[lastFE+2]; } emit haveDataFromServer(r.mid(0x15)); } else { qInfo(logUdpServer()) << current->ipAddress.toString() << ": Datalen mismatch " << quint16(in->datalen + 0x15) << ":" << (quint16)in->len; } } } //break; } } if (current != Q_NULLPTR) { udpServer::commonReceived(&civClients, current, r); } } } void udpServer::audioReceived() { while (udpAudio->hasPendingDatagrams()) { QNetworkDatagram datagram = udpAudio->receiveDatagram(); QByteArray r = datagram.data(); CLIENT* current = Q_NULLPTR; if (datagram.senderAddress().isNull() || datagram.senderPort() == 65535 || datagram.senderPort() == 0) return; QDateTime now = QDateTime::currentDateTime(); foreach(CLIENT * client, audioClients) { if (client != Q_NULLPTR) { if (client->ipAddress == datagram.senderAddress() && client->port == datagram.senderPort()) { current = client; } } } if (current == Q_NULLPTR) { current = new CLIENT(); foreach(CLIENT* client, controlClients) { if (client != Q_NULLPTR) { if (client->ipAddress == datagram.senderAddress() && client->isAuthenticated && client->audioClient == Q_NULLPTR) { current->controlClient = client; client->audioClient = current; } } } if (current->controlClient == Q_NULLPTR || !current->controlClient->isAuthenticated) { // There is no current controlClient that matches this audioClient delete current; return; } current->type = "Audio"; current->connected = true; current->timeConnected = QDateTime::currentDateTime(); current->ipAddress = datagram.senderAddress(); current->port = datagram.senderPort(); current->myId = audioId; current->remoteId = qFromLittleEndian(r.mid(8, 4)); current->socket = udpAudio; current->pingSeq = (quint8)rand() << 8 | (quint8)rand(); current->pingTimer = new QTimer(); connect(current->pingTimer, &QTimer::timeout, this, std::bind(&udpServer::sendPing, this, &audioClients, current, (quint16)0x00, false)); current->pingTimer->start(100); current->retransmitTimer = new QTimer(); connect(current->retransmitTimer, &QTimer::timeout, this, std::bind(&udpServer::sendRetransmitRequest, this, current)); current->retransmitTimer->start(RETRANSMIT_PERIOD); current->seqPrefix = 0; qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): New connection created"; if (connMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { audioClients.append(current); connMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock connMutex()"; } } switch (r.length()) { case (PING_SIZE): { ping_packet_t in = (ping_packet_t)r.constData(); if (in->type == 0x07) { // It is a ping request/response if (in->reply == 0x00) { current->rxPingTime = in->time; sendPing(&audioClients, current, in->seq, true); } else if (in->reply == 0x01) { if (in->seq == current->pingSeq || in->seq == current->pingSeq - 1) { // A Reply to our ping! if (in->seq == current->pingSeq) { current->pingSeq++; } else { qInfo(logUdpServer()) << current->ipAddress.toString() << ": got out of sequence ping reply. Got: " << in->seq << " expecting: " << current->pingSeq; } } } } break; } default: { /* Audio packets start as follows: PCM 16bit and PCM8/uLAW stereo: 0x44,0x02 for first packet and 0x6c,0x05 for second. uLAW 8bit/PCM 8bit 0xd8,0x03 for all packets PCM 16bit stereo 0x6c,0x05 first & second 0x70,0x04 third */ control_packet_t in = (control_packet_t)r.constData(); if (in->type != 0x01) { // Opus packets can be smaller than this! && in->len >= 0xAC) { if (in->seq == 0) { // Seq number has rolled over. current->seqPrefix++; } if (hasTxAudio == current->ipAddress) { // 0xac is the smallest possible audio packet. audioPacket tempAudio; tempAudio.seq = (quint32)current->seqPrefix << 16 | in->seq; tempAudio.time = QTime::currentTime();; tempAudio.sent = 0; tempAudio.data = r.mid(0x18); //qInfo(logUdpServer()) << "sending tx audio " << in->seq; emit haveAudioData(tempAudio); //txaudio->incomingAudio(tempAudio); } } break; } } if (current != Q_NULLPTR) { udpServer::commonReceived(&audioClients, current, r); } } } void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) { Q_UNUSED(l); // We might need it later! if (current == Q_NULLPTR || r.isNull()) { return; } current->lastHeard = QDateTime::currentDateTime(); if (r.length() < 0x10) { // Invalid packet qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Invalid packet received, len: " << r.length(); return; } switch (r.length()) { case (CONTROL_SIZE): { control_packet_t in = (control_packet_t)r.constData(); if (in->type == 0x03) { qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Received 'are you there'"; current->remoteId = in->sentid; sendControl(current, 0x04, in->seq); } // This is This is "Are you ready" in response to "I am here". else if (in->type == 0x06) { qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Received 'Are you ready'"; current->remoteId = in->sentid; sendControl(current, 0x06, in->seq); if (current->idleTimer != Q_NULLPTR && !current->idleTimer->isActive()) { current->idleTimer->start(100); } } // This is a retransmit request else if (in->type == 0x01) { // Single packet request qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Received 'retransmit' request for " << in->seq; QMap::iterator match = current->txSeqBuf.find(in->seq); if (match != current->txSeqBuf.end() && match->retransmitCount < 5) { // Found matching entry? // Don't constantly retransmit the same packet, give-up eventually qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Sending retransmit of " << hex << match->seqNum; match->retransmitCount++; if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { current->socket->writeDatagram(match->data, current->ipAddress, current->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } } else { // Just send an idle! sendControl(current, 0x00, in->seq); } } break; } default: { //break; } } // The packet is at least 0x10 in length so safe to cast it to control_packet for processing control_packet_t in = (control_packet_t)r.constData(); if (in->type == 0x01 && in->len != 0x10) { for (quint16 i = 0x10; i < r.length(); i = i + 2) { auto match = std::find_if(current->txSeqBuf.begin(), current->txSeqBuf.end(), [&cs = in->seq](SEQBUFENTRY& s) { return s.seqNum == cs; }); if (match == current->txSeqBuf.end()) { qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Requested packet " << hex << in->seq << " not found"; // Just send idle packet. sendControl(current, 0, in->seq); } else if (match->seqNum != 0x00) { // Found matching entry? // Send "untracked" as it has already been sent once. qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Sending retransmit of " << hex << match->seqNum; match->retransmitCount++; if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { current->socket->writeDatagram(match->data, current->ipAddress, current->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } match++; } } } else if (in->type == 0x00 && in->seq != 0x00) { //if (current->type == "CIV") { // qInfo(logUdpServer()) << "Got:" << in->seq; //} if (current->rxSeqBuf.isEmpty()) { if (current->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { current->rxSeqBuf.insert(in->seq, QTime::currentTime()); current->rxMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock rxMutex()"; } } else { if (in->seq < current->rxSeqBuf.firstKey()) { qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): ******* seq number may have rolled over ****** previous highest: " << hex << current->rxSeqBuf.lastKey() << " current: " << hex << in->seq; // Looks like it has rolled over so clear buffer and start again. if (current->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { current->rxSeqBuf.clear(); current->rxMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock rxMutex()"; } if (current->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { current->rxMissing.clear(); current->missMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock missMutex()"; } return; } if (!current->rxSeqBuf.contains(in->seq)) { // Add incoming packet to the received buffer and if it is in the missing buffer, remove it. if (current->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { if (current->rxSeqBuf.size() > 400) { current->rxSeqBuf.remove(0); } current->rxSeqBuf.insert(in->seq, QTime::currentTime()); current->rxMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock rxMutex()"; } } else{ // Check whether this is one of our missing ones! if (current->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { QMap::iterator s = current->rxMissing.find(in->seq); if (s != current->rxMissing.end()) { qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Missing SEQ has been received! " << hex << in->seq; s = current->rxMissing.erase(s); } current->missMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock missMutex()"; } } } } } void udpServer::sendControl(CLIENT* c, quint8 type, quint16 seq) { //qInfo(logUdpServer()) << c->ipAddress.toString() << ": Sending control packet: " << type; control_packet p; memset(p.packet, 0x0, CONTROL_SIZE); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = type; p.sentid = c->myId; p.rcvdid = c->remoteId; if (seq == 0x00) { p.seq = c->txSeq; SEQBUFENTRY s; s.seqNum = seq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->txSeqBuf.insert(seq, s); c->txSeq++; c->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } } else { p.seq = seq; if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } } return; } void udpServer::sendPing(QList* l, CLIENT* c, quint16 seq, bool reply) { Q_UNUSED(l); /* QDateTime now = QDateTime::currentDateTime(); if (c->lastHeard.secsTo(now) > STALE_CONNECTION) { qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Deleting stale connection "; deleteConnection(l, c); return; } */ //qInfo(logUdpServer()) << c->ipAddress.toString() << ": Sending Ping"; quint32 pingTime = 0; if (reply) { pingTime = c->rxPingTime; } else { pingTime = (quint32)timeStarted.msecsSinceStartOfDay(); seq = c->pingSeq; // Don't increment pingseq until we receive a reply. } // First byte of pings "from" server can be either 0x00 or packet length! ping_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! if (reply) { p.len = sizeof(p); } p.type = 0x07; p.seq = seq; p.sentid = c->myId; p.rcvdid = c->remoteId; p.time = pingTime; p.reply = (char)reply; if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } return; } void udpServer::sendLoginResponse(CLIENT* c, bool allowed) { qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending Login response: " << c->txSeq; login_response_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = 0x00; p.seq = c->txSeq; p.sentid = c->myId; p.rcvdid = c->remoteId; p.innerseq = c->authInnerSeq; p.tokrequest = c->tokenRx; p.token = c->tokenTx; p.code = 0x0250; if (!allowed) { p.error = 0xFEFFFFFF; if (c->idleTimer != Q_NULLPTR) c->idleTimer->stop(); if (c->pingTimer != Q_NULLPTR) c->pingTimer->stop(); if (c->retransmitTimer != Q_NULLPTR) c->retransmitTimer->stop(); } else { strcpy(p.connection, "WFVIEW"); } SEQBUFENTRY s; s.seqNum = c->txSeq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->txSeqBuf.insert(c->txSeq, s); c->txSeq++; c->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } if (c->idleTimer != Q_NULLPTR) c->idleTimer->start(100); return; } void udpServer::sendCapabilities(CLIENT* c) { qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending Capabilities :" << c->txSeq; capabilities_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = 0x00; p.seq = c->txSeq; p.sentid = c->myId; p.rcvdid = c->remoteId; p.innerseq = c->authInnerSeq; p.tokrequest = c->tokenRx; p.token = c->tokenTx; p.code = 0x0298; p.res = 0x02; p.capa = 0x01; p.commoncap = c->commonCap; memcpy(p.macaddress, macAddress.toLocal8Bit(), 6); // IRU seems to expect an "Icom" mac address so replace the first 3 octets of our Mac with one in their range! memcpy(p.macaddress, QByteArrayLiteral("\x00\x90\xc7").constData(), 3); memcpy(p.name, rigCaps.modelName.toLocal8Bit(), rigCaps.modelName.length()); memcpy(p.audio, QByteArrayLiteral("ICOM_VAUDIO").constData(), 11); if (rigCaps.hasWiFi && !rigCaps.hasEthernet) { p.conntype = 0x0707; // 0x0707 for wifi rig. } else { p.conntype = 0x073f; // 0x073f for ethernet rig. } p.civ = rigCaps.civ; p.baudrate = (quint32)qToBigEndian(config.baudRate); /* 0x80 = 12K only 0x40 = 44.1K only 0x20 = 22.05K only 0x10 = 11.025K only 0x08 = 48K only 0x04 = 32K only 0x02 = 16K only 0x01 = 8K only */ if (rxaudio == Q_NULLPTR) { p.rxsample = 0x8b01; // all rx sample frequencies supported } else { if (rxSampleRate == 48000) { p.rxsample = 0x0800; // fixed rx sample frequency } else if (rxSampleRate == 32000) { p.rxsample = 0x0400; } else if (rxSampleRate == 24000) { p.rxsample = 0x0001; } else if (rxSampleRate == 16000) { p.rxsample = 0x0200; } else if (rxSampleRate == 12000) { p.rxsample = 0x8000; } } if (txaudio == Q_NULLPTR) { p.txsample = 0x8b01; // all tx sample frequencies supported p.enablea = 0x01; // 0x01 enables TX 24K mode? qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Client will have TX audio"; } else { qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Disable tx audio for client"; p.txsample = 0; } // I still don't know what these are? p.enableb = 0x01; // 0x01 doesn't seem to do anything? p.enablec = 0x01; // 0x01 doesn't seem to do anything? p.capf = 0x5001; p.capg = 0x0190; SEQBUFENTRY s; s.seqNum = p.seq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { if (c->txSeqBuf.size() > 400) { c->txSeqBuf.remove(0); } c->txSeqBuf.insert(p.seq, s); c->txSeq++; c->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } if (c->idleTimer != Q_NULLPTR) c->idleTimer->start(100); return; } // When client has requested civ/audio connection, this will contain their details // Also used to display currently connected used information. void udpServer::sendConnectionInfo(CLIENT* c) { qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending ConnectionInfo :" << c->txSeq; conninfo_packet p; memset(p.packet, 0x0, sizeof(p)); p.len = sizeof(p); p.type = 0x00; p.seq = c->txSeq; p.sentid = c->myId; p.rcvdid = c->remoteId; //p.innerseq = c->authInnerSeq; // Innerseq not used in user packet p.tokrequest = c->tokenRx; p.token = c->tokenTx; p.code = 0x0380; p.commoncap = c->commonCap; p.identa = c->identa; p.identb = c->identb; // 0x1a-0x1f is authid (random number? // memcpy(p + 0x40, QByteArrayLiteral("IC-7851").constData(), 7); memcpy(p.packet + 0x40, rigCaps.modelName.toLocal8Bit(), rigCaps.modelName.length()); // This is the current streaming client (should we support multiple clients?) if (c->isStreaming) { p.busy = 0x01; memcpy(p.computer, c->clientName.constData(), c->clientName.length()); p.ipaddress = qToBigEndian(c->ipAddress.toIPv4Address()); p.identa = c->identa; p.identb = c->identb; } SEQBUFENTRY s; s.seqNum = p.seq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { if (c->txSeqBuf.size() > 400) { c->txSeqBuf.remove(0); } c->txSeqBuf.insert(p.seq, s); c->txSeq++; c->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } if (c->idleTimer != Q_NULLPTR) c->idleTimer->start(100); return; } void udpServer::sendTokenResponse(CLIENT* c, quint8 type) { qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending Token response for type: " << type; token_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = 0x00; p.seq = c->txSeq; p.sentid = c->myId; p.rcvdid = c->remoteId; p.innerseq = c->authInnerSeq; p.tokrequest = c->tokenRx; p.token = c->tokenTx; p.code = 0x0230; p.identa = c->identa; p.identb = c->identb; p.commoncap = c->commonCap; p.res = type; SEQBUFENTRY s; s.seqNum = p.seq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { if (c->txSeqBuf.size() > 400) { c->txSeqBuf.remove(0); } c->txSeqBuf.insert(p.seq, s); c->txSeq++; c->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } if (c->idleTimer != Q_NULLPTR) c->idleTimer->start(100); return; } #define PURGE_SECONDS 60 void udpServer::watchdog() { QDateTime now = QDateTime::currentDateTime(); foreach(CLIENT * client, audioClients) { if (client != Q_NULLPTR) { if (client->lastHeard.secsTo(now) > STALE_CONNECTION) { qInfo(logUdpServer()) << client->ipAddress.toString() << "(" << client->type << "): Deleting stale connection "; deleteConnection(&audioClients, client); } } else { qInfo(logUdpServer()) << "Current client is NULL!"; } } foreach(CLIENT* client, civClients) { if (client != Q_NULLPTR) { if (client->lastHeard.secsTo(now) > STALE_CONNECTION) { qInfo(logUdpServer()) << client->ipAddress.toString() << "(" << client->type << "): Deleting stale connection "; deleteConnection(&civClients, client); } } else { qInfo(logUdpServer()) << "Current client is NULL!"; } } foreach(CLIENT* client, controlClients) { if (client != Q_NULLPTR) { if (client->lastHeard.secsTo(now) > STALE_CONNECTION) { qInfo(logUdpServer()) << client->ipAddress.toString() << "(" << client->type << "): Deleting stale connection "; deleteConnection(&controlClients, client); } } else { qInfo(logUdpServer()) << "Current client is NULL!"; } } emit haveNetworkStatus(QString("
Server connections: Control:%1 CI-V:%2 Audio:%3
").arg(controlClients.size()).arg(civClients.size()).arg(audioClients.size())); } void udpServer::sendStatus(CLIENT* c) { qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending Status"; status_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p); p.type = 0x00; p.seq = c->txSeq; p.sentid = c->myId; p.rcvdid = c->remoteId; p.innerseq = c->authInnerSeq; p.tokrequest = c->tokenRx; p.token = c->tokenTx; p.code = 0x0240; p.res = 0x03; p.unknown = 0x1000; p.unusede = (char)0x80; p.identa = c->identa; p.identb = c->identb; p.civport = qToBigEndian(c->civPort); p.audioport = qToBigEndian(c->audioPort); // Send this to reject the request to tx/rx audio/civ //memcpy(p + 0x30, QByteArrayLiteral("\xff\xff\xff\xfe").constData(), 4); SEQBUFENTRY s; s.seqNum = p.seq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { if (c->txSeqBuf.size() > 400) { c->txSeqBuf.remove(0); } c->txSeq++; c->txSeqBuf.insert(p.seq, s); c->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } } void udpServer::dataForServer(QByteArray d) { //qInfo(logUdpServer()) << "Server got:" << d; foreach(CLIENT * client, civClients) { int lastFE = d.lastIndexOf((quint8)0xfe); if (client != Q_NULLPTR && client->connected && d.length() > lastFE + 2 && ((quint8)d[lastFE + 1] == client->civId || (quint8)d[lastFE + 2] == client->civId || (quint8)d[lastFE + 1] == 0x00 || (quint8)d[lastFE + 2]==0x00 || (quint8)d[lastFE + 1] == 0xE1 || (quint8)d[lastFE + 2] == 0xE1)) { data_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = (quint16)d.length() + sizeof(p); p.seq = client->txSeq; p.sentid = client->myId; p.rcvdid = client->remoteId; p.reply = (char)0xc1; p.datalen = (quint16)d.length(); p.sendseq = client->innerSeq; QByteArray t = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); t.append(d); SEQBUFENTRY s; s.seqNum = p.seq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = t; if (client->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { if (client->txSeqBuf.size() > 400) { client->txSeqBuf.remove(0); } client->txSeqBuf.insert(p.seq, s); client->txSeq++; client->innerSeq++; client->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { client->socket->writeDatagram(t, client->ipAddress, client->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } } else { qInfo(logUdpServer()) << "Got data for different ID" << hex << (quint8)d[lastFE+1] << ":" << hex << (quint8)d[lastFE+2]; } } return; } void udpServer::sendRxAudio() { QByteArray audio; if (rxaudio) { if (audioMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { audio.clear(); rxaudio->getNextAudioChunk(audio); // Now we have the next audio chunk, we can release the mutex. audioMutex.unlock(); int len = 0; while (len < audio.length()) { audioPacket partial; partial.data = audio.mid(len, 1364); receiveAudioData(partial); len = len + partial.data.length(); } } else { qInfo(logUdpServer()) << "Unable to lock mutex for rxaudio"; } } } void udpServer::receiveAudioData(const audioPacket& d) { //qInfo(logUdpServer()) << "Server got:" << d.data.length(); foreach(CLIENT * client, audioClients) { if (client != Q_NULLPTR && client->connected) { audio_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = sizeof(p) + d.data.length(); p.sentid = client->myId; p.rcvdid = client->remoteId; p.ident = 0x0080; // audio is always this? p.datalen = (quint16)qToBigEndian((quint16)d.data.length()); p.sendseq = (quint16)qToBigEndian((quint16)client->sendAudioSeq); // THIS IS BIG ENDIAN! p.seq = client->txSeq; QByteArray t = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); t.append(d.data); SEQBUFENTRY s; s.seqNum = p.seq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = t; if (client->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { if (client->txSeqBuf.size() > 400) { client->txSeqBuf.remove(0); } client->txSeqBuf.insert(p.seq, s); client->txSeq++; client->sendAudioSeq++; client->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { client->socket->writeDatagram(t, client->ipAddress, client->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } } } return; } /// /// Find all gaps in received packets and then send requests for them. /// This will run every 100ms so out-of-sequence packets will not trigger a retransmit request. /// /// void udpServer::sendRetransmitRequest(CLIENT* c) { // Find all gaps in received packets and then send requests for them. // This will run every 100ms so out-of-sequence packets will not trigger a retransmit request. QByteArray missingSeqs; if (!c->rxSeqBuf.empty() && c->rxSeqBuf.size() <= c->rxSeqBuf.lastKey() - c->rxSeqBuf.firstKey()) { if ((c->rxSeqBuf.lastKey() - c->rxSeqBuf.firstKey() - c->rxSeqBuf.size()) > 20) { // Too many packets to process, flush buffers and start again! qDebug(logUdp()) << "Too many missing packets, flushing buffer: " << c->rxSeqBuf.lastKey() << "missing=" << c->rxSeqBuf.lastKey() - c->rxSeqBuf.firstKey() - c->rxSeqBuf.size() + 1; if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->rxMissing.clear(); c->missMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock missMutex()"; } if (c->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->rxSeqBuf.clear(); c->rxMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock rxMutex()"; } } else { // We have at least 1 missing packet! qDebug(logUdp()) << "Missing Seq: size=" << c->rxSeqBuf.size() << "firstKey=" << c->rxSeqBuf.firstKey() << "lastKey=" << c->rxSeqBuf.lastKey() << "missing=" << c->rxSeqBuf.lastKey() - c->rxSeqBuf.firstKey() - c->rxSeqBuf.size() + 1; // We are missing packets so iterate through the buffer and add the missing ones to missing packet list for (int i = 0; i < c->rxSeqBuf.keys().length() - 1; i++) { for (quint16 j = c->rxSeqBuf.keys()[i] + 1; j < c->rxSeqBuf.keys()[i + 1]; j++) { auto s = c->rxMissing.find(j); if (s == c->rxMissing.end()) { // We haven't seen this missing packet before qDebug(logUdp()) << this->metaObject()->className() << ": Adding to missing buffer (len=" << c->rxMissing.size() << "): " << j; if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->rxMissing.insert(j, 0); c->missMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock missMutex()"; } if (c->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { if (c->rxSeqBuf.size() > 400) { c->rxSeqBuf.remove(0); } c->rxSeqBuf.insert(j, QTime::currentTime()); // Add this missing packet to the rxbuffer as we now long about it. c->rxMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock rxMutex()"; } } else { if (s.value() == 4) { // We have tried 4 times to request this packet, time to give up! if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { s = c->rxMissing.erase(s); c->missMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock missMutex()"; } } } } } } } if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { for (auto it = c->rxMissing.begin(); it != c->rxMissing.end(); ++it) { if (it.value() < 10) { missingSeqs.append(it.key() & 0xff); missingSeqs.append(it.key() >> 8 & 0xff); missingSeqs.append(it.key() & 0xff); missingSeqs.append(it.key() >> 8 & 0xff); it.value()++; } } if (missingSeqs.length() != 0) { control_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.type = 0x01; p.seq = 0x0000; p.sentid = c->myId; p.rcvdid = c->remoteId; if (missingSeqs.length() == 4) // This is just a single missing packet so send using a control. { p.seq = (missingSeqs[0] & 0xff) | (quint16)(missingSeqs[1] << 8); qDebug(logUdp()) << this->metaObject()->className() << ": sending request for missing packet : " << hex << p.seq; if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } } else { qDebug(logUdp()) << this->metaObject()->className() << ": sending request for multiple missing packets : " << missingSeqs.toHex(); if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { missingSeqs.insert(0, p.packet, sizeof(p.packet)); c->socket->writeDatagram(missingSeqs, c->ipAddress, c->port); udpMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } } } c->missMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock missMutex()"; } } /// /// This function is passed a pointer to the list of connection objects and a pointer to the object itself /// Needs to stop and delete all timers, remove the connection from the list and delete the connection. /// /// /// void udpServer::deleteConnection(QList* l, CLIENT* c) { qInfo(logUdpServer()) << "Deleting" << c->type << "connection to: " << c->ipAddress.toString() << ":" << QString::number(c->port); if (c->idleTimer != Q_NULLPTR) { c->idleTimer->stop(); delete c->idleTimer; } if (c->pingTimer != Q_NULLPTR) { c->pingTimer->stop(); delete c->pingTimer; } if (c->retransmitTimer != Q_NULLPTR) { c->retransmitTimer->stop(); delete c->retransmitTimer; } if (c->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->rxSeqBuf.clear(); c->rxMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock rxMutex()"; } if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->txSeqBuf.clear(); c->txMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock txMutex()"; } if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { c->rxMissing.clear(); c->missMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock missMutex()"; } if (connMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { QList::iterator it = l->begin(); while (it != l->end()) { CLIENT* client = *it; if (client != Q_NULLPTR && client == c) { qInfo(logUdpServer()) << "Found" << client->type << "connection to: " << client->ipAddress.toString() << ":" << QString::number(client->port); it = l->erase(it); } else { ++it; } } delete c; // Is this needed or will the erase have done it? c = Q_NULLPTR; qInfo(logUdpServer()) << "Current Number of clients connected: " << l->length(); connMutex.unlock(); } else { qInfo(logUdpServer()) << "Unable to lock connMutex()"; } if (l->length() == 0) { if (rxAudioTimer != Q_NULLPTR) { rxAudioTimer->stop(); delete rxAudioTimer; rxAudioTimer = Q_NULLPTR; } if (rxAudioThread != Q_NULLPTR) { rxAudioThread->quit(); rxAudioThread->wait(); rxaudio = Q_NULLPTR; rxAudioThread = Q_NULLPTR; } if (txAudioThread != Q_NULLPTR) { txAudioThread->quit(); txAudioThread->wait(); txaudio = Q_NULLPTR; txAudioThread = Q_NULLPTR; } } } wfview-1.2d/udpserver.h000066400000000000000000000075011415164626400152330ustar00rootroot00000000000000#ifndef UDPSERVER_H #define UDPSERVER_H #include #include #include #include #include #include #include #include #include #include #include #include // Allow easy endian-ness conversions #include #include #include #include "packettypes.h" #include "rigidentities.h" #include "audiohandler.h" extern void passcode(QString in,QByteArray& out); extern QByteArray parseNullTerminatedString(QByteArray c, int s); struct SEQBUFENTRY { QTime timeSent; uint16_t seqNum; QByteArray data; quint8 retransmitCount; }; class udpServer : public QObject { Q_OBJECT public: udpServer(SERVERCONFIG config,audioSetup outAudio, audioSetup inAudio); ~udpServer(); public slots: void init(); void dataForServer(QByteArray); void receiveAudioData(const audioPacket &data); void receiveRigCaps(rigCapabilities caps); signals: void haveDataFromServer(QByteArray); void haveAudioData(audioPacket data); void haveNetworkStatus(QString); void setupTxAudio(audioSetup); void setupRxAudio(audioSetup); private: struct CLIENT { bool connected = false; QString type; QHostAddress ipAddress; quint16 port; QByteArray clientName; QDateTime timeConnected; QDateTime lastHeard; bool isStreaming; quint16 civPort; quint16 audioPort; quint16 txBufferLen; quint32 myId; quint32 remoteId; quint16 txSeq=0; quint16 rxSeq; quint16 connSeq; quint16 pingSeq; quint32 rxPingTime; // 32bit as has other info quint8 authInnerSeq; quint16 authSeq; quint16 innerSeq; quint16 sendAudioSeq; quint8 identa; quint32 identb; quint16 tokenRx; quint32 tokenTx; quint32 commonCap; quint8 wdseq; QUdpSocket* socket; QTimer* pingTimer; QTimer* idleTimer; QTimer* retransmitTimer; // Only used for audio. quint8 rxCodec; quint8 txCodec; quint16 rxSampleRate; quint16 txSampleRate; SERVERUSER user; QMap rxSeqBuf; QMap txSeqBuf; QMap rxMissing; QMutex txMutex; QMutex rxMutex; QMutex missMutex; quint16 seqPrefix; quint8 civId; bool isAuthenticated; CLIENT* controlClient = Q_NULLPTR; CLIENT* civClient = Q_NULLPTR; CLIENT* audioClient = Q_NULLPTR; }; void controlReceived(); void civReceived(); void audioReceived(); void commonReceived(QList* l,CLIENT* c, QByteArray r); void sendPing(QList *l,CLIENT* c, quint16 seq, bool reply); void sendControl(CLIENT* c, quint8 type, quint16 seq); void sendLoginResponse(CLIENT* c, bool allowed); void sendCapabilities(CLIENT* c); void sendConnectionInfo(CLIENT* c); void sendTokenResponse(CLIENT* c,quint8 type); void sendStatus(CLIENT* c); void sendRetransmitRequest(CLIENT* c); void watchdog(); void sendRxAudio(); void deleteConnection(QList *l, CLIENT* c); SERVERCONFIG config; QUdpSocket* udpControl = Q_NULLPTR; QUdpSocket* udpCiv = Q_NULLPTR; QUdpSocket* udpAudio = Q_NULLPTR; QHostAddress localIP; QString macAddress; quint32 controlId = 0; quint32 civId = 0; quint32 audioId = 0; quint8 rigciv = 0xa2; QMutex udpMutex; // Used for critical operations. QMutex connMutex; QMutex audioMutex; QList controlClients = QList(); QList civClients = QList(); QList audioClients = QList(); QTime timeStarted; rigCapabilities rigCaps; audioHandler* rxaudio = Q_NULLPTR; QThread* rxAudioThread = Q_NULLPTR; audioHandler* txaudio = Q_NULLPTR; QThread* txAudioThread = Q_NULLPTR; audioSetup outAudio; audioSetup inAudio; QTimer* rxAudioTimer=Q_NULLPTR; quint16 rxSampleRate = 0; quint16 txSampleRate = 0; quint8 rxCodec = 0; quint8 txCodec = 0; QHostAddress hasTxAudio; QTimer* wdTimer; }; #endif // UDPSERVER_Hwfview-1.2d/udpserversetup.cpp000066400000000000000000000076261415164626400166570ustar00rootroot00000000000000#include "udpserversetup.h" #include "ui_udpserversetup.h" #include "logcategories.h" extern void passcode(QString in,QByteArray& out); udpServerSetup::udpServerSetup(QWidget* parent) : QDialog(parent), ui(new Ui::udpServerSetup) { ui->setupUi(this); addUserLine("", "", 0); // Create a blank row if we never receive config. // Get any stored config information from the main form. SERVERCONFIG config; emit serverConfig(config,false); // Just send blank server config. } udpServerSetup::~udpServerSetup() { delete ui; } // Slot to receive config. void udpServerSetup::receiveServerConfig(SERVERCONFIG conf) { qInfo() << "Getting server config"; ui->enableCheckbox->setChecked(conf.enabled); ui->controlPortText->setText(QString::number(conf.controlPort)); ui->civPortText->setText(QString::number(conf.civPort)); ui->audioPortText->setText(QString::number(conf.audioPort)); int row = 0; for (int i = 0; i < ui->usersTable->rowCount(); i++) { ui->usersTable->removeRow(i); } foreach (SERVERUSER user, conf.users) { if (user.username != "" && user.password != "") { addUserLine(user.username, user.password, user.userType); row++; } } if (row == 0) { addUserLine("", "", 0); } } void udpServerSetup::accept() { qInfo() << "Server config stored"; SERVERCONFIG config; config.enabled = ui->enableCheckbox->isChecked(); config.controlPort = ui->controlPortText->text().toInt(); config.civPort = ui->civPortText->text().toInt(); config.audioPort = ui->audioPortText->text().toInt(); config.users.clear(); for (int row = 0; row < ui->usersTable->model()->rowCount(); row++) { if (ui->usersTable->item(row, 0) != NULL) { SERVERUSER user; user.username = ui->usersTable->item(row, 0)->text(); QLineEdit* password = (QLineEdit*)ui->usersTable->cellWidget(row, 1); user.password = password->text(); QComboBox* comboBox = (QComboBox*)ui->usersTable->cellWidget(row, 2); user.userType = comboBox->currentIndex(); config.users.append(user); } else { ui->usersTable->removeRow(row); } } emit serverConfig(config,true); this->hide(); } void udpServerSetup::on_usersTable_cellClicked(int row, int col) { qInfo() << "Clicked on " << row << "," << col; if (row == ui->usersTable->model()->rowCount() - 1 && ui->usersTable->item(row, 0) != NULL) { addUserLine("", "", 0); } } void udpServerSetup::onPasswordChanged() { int row = sender()->property("row").toInt(); QLineEdit* password = (QLineEdit*)ui->usersTable->cellWidget(row, 1); QByteArray pass; passcode(password->text(), pass); password->setText(pass); qInfo() << "password row" << row << "changed"; } void udpServerSetup::addUserLine(const QString& user, const QString& pass, const int& type) { ui->usersTable->insertRow(ui->usersTable->rowCount()); ui->usersTable->setItem(ui->usersTable->rowCount() - 1, 0, new QTableWidgetItem(user)); ui->usersTable->setItem(ui->usersTable->rowCount() - 1, 1, new QTableWidgetItem()); ui->usersTable->setItem(ui->usersTable->rowCount() - 1, 2, new QTableWidgetItem()); QLineEdit* password = new QLineEdit(); password->setProperty("row", (int)ui->usersTable->rowCount() - 1); password->setEchoMode(QLineEdit::PasswordEchoOnEdit); password->setText(pass); connect(password, SIGNAL(editingFinished()), this, SLOT(onPasswordChanged())); ui->usersTable->setCellWidget(ui->usersTable->rowCount() - 1, 1, password); QComboBox* comboBox = new QComboBox(); comboBox->insertItems(0, { "Full User","Full with no TX","Monitor only" }); comboBox->setCurrentIndex(type); ui->usersTable->setCellWidget(ui->usersTable->rowCount() - 1, 2, comboBox); } wfview-1.2d/udpserversetup.h000066400000000000000000000020631415164626400163120ustar00rootroot00000000000000#ifndef UDPSERVERSETUP_H #define UDPSERVERSETUP_H #include #include #include #include struct SERVERUSER { QString username; QString password; quint8 userType; }; struct SERVERCONFIG { bool enabled; bool lan; quint16 controlPort; quint16 civPort; quint16 audioPort; int audioOutput; int audioInput; quint8 resampleQuality; quint32 baudRate; QList users; }; namespace Ui { class udpServerSetup; } class udpServerSetup : public QDialog { Q_OBJECT public: explicit udpServerSetup(QWidget* parent = 0); ~udpServerSetup(); private slots: void on_usersTable_cellClicked(int row, int col); void onPasswordChanged(); public slots: void receiveServerConfig(SERVERCONFIG conf); signals: void serverConfig(SERVERCONFIG conf, bool store); private: Ui::udpServerSetup* ui; void accept(); QList userTypes; void addUserLine(const QString &user, const QString &pass, const int &type); }; #endif // UDPSERVER_H wfview-1.2d/udpserversetup.ui000066400000000000000000000254221415164626400165040ustar00rootroot00000000000000 udpServerSetup 0 0 440 361 0 0 440 0 440 480 Server Setup QLayout::SetDefaultConstraint Qt::Horizontal 40 20 0 20 16777215 20 Server Setup Qt::Horizontal 40 20 0 20 16777215 20 Enable Qt::Horizontal 40 20 0 20 16777215 20 Contol Port 0 0 0 25 16777215 25 99999 50001 true 0 0 0 20 16777215 20 Civ Port 0 25 16777215 25 99999 50002 0 20 16777215 20 Audio Port 0 25 16777215 25 99999 50003 0 0 400 160 750 330 QFrame::StyledPanel 1 0 3 false 100 false true false true false Username Password Admin Note: This allows other computers to connect to this computer's radio OK Cancel okButton clicked() udpServerSetup accept() 278 253 96 254 cancelButton clicked() udpServerSetup reject() 369 253 179 282 wfview-1.2d/ulaw.h000066400000000000000000000056311415164626400141660ustar00rootroot00000000000000#ifndef ULAW_H #define ULAW_H const int cBias = 0x84; const int cClip = 32635; static const char MuLawCompressTable[256] = { 0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7 }; static const qint16 ulaw_decode[256] = { -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412, -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, -876, -844, -812, -780, -748, -716, -684, -652, -620, -588, -556, -524, -492, -460, -428, -396, -372, -356, -340, -324, -308, -292, -276, -260, -244, -228, -212, -196, -180, -164, -148, -132, -120, -112, -104, -96, -88, -80, -72, -64, -56, -48, -40, -32, -24, -16, -8, 0, 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, 876, 844, 812, 780, 748, 716, 684, 652, 620, 588, 556, 524, 492, 460, 428, 396, 372, 356, 340, 324, 308, 292, 276, 260, 244, 228, 212, 196, 180, 164, 148, 132, 120, 112, 104, 96, 88, 80, 72, 64, 56, 48, 40, 32, 24, 16, 8, 0 }; #endif wfview-1.2d/wfmain.cpp000066400000000000000000005414571415164626400150450ustar00rootroot00000000000000#include "wfmain.h" #include "ui_wfmain.h" #include "commhandler.h" #include "rigidentities.h" #include "logcategories.h" // This code is copyright 2017-2020 Elliott H. Liggett // All rights reserved wfmain::wfmain(const QString serialPortCL, const QString hostCL, const QString settingsFile, QWidget *parent ) : QMainWindow(parent), ui(new Ui::wfmain) { QGuiApplication::setApplicationDisplayName("wfview"); QGuiApplication::setApplicationName(QString("wfview")); setWindowIcon(QIcon( QString(":resources/wfview.png"))); ui->setupUi(this); setWindowTitle(QString("wfview")); this->serialPortCL = serialPortCL; this->hostCL = hostCL; cal = new calibrationWindow(); rpt = new repeaterSetup(); sat = new satelliteSetup(); trxadj = new transceiverAdjustments(); srv = new udpServerSetup(); abtBox = new aboutbox(); connect(this, SIGNAL(sendServerConfig(SERVERCONFIG)), srv, SLOT(receiveServerConfig(SERVERCONFIG))); connect(srv, SIGNAL(serverConfig(SERVERCONFIG, bool)), this, SLOT(serverConfigRequested(SERVERCONFIG, bool))); qRegisterMetaType(); // Needs to be registered early. qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType (); qRegisterMetaType (); qRegisterMetaType (); haveRigCaps = false; setupKeyShortcuts(); setupMainUI(); setSerialDevicesUI(); setAudioDevicesUI(); setDefaultColors(); setDefPrefs(); getSettingsFilePath(settingsFile); setupPlots(); loadSettings(); // Look for saved preferences setTuningSteps(); // TODO: Combine into preferences setUIToPrefs(); setServerToPrefs(); setInitialTiming(); openRig(); rigConnections(); if (serverConfig.enabled && udp != Q_NULLPTR) { // Server connect(rig, SIGNAL(haveAudioData(audioPacket)), udp, SLOT(receiveAudioData(audioPacket))); connect(rig, SIGNAL(haveDataForServer(QByteArray)), udp, SLOT(dataForServer(QByteArray))); connect(udp, SIGNAL(haveDataFromServer(QByteArray)), rig, SLOT(dataFromServer(QByteArray))); } amTransmitting = false; connect(ui->txPowerSlider, &QSlider::sliderMoved, [&](int value) { QToolTip::showText(QCursor::pos(), QString("%1").arg(value*100/255), nullptr); }); } wfmain::~wfmain() { rigThread->quit(); rigThread->wait(); if (serverThread != Q_NULLPTR) { serverThread->quit(); serverThread->wait(); } if (rigCtl != Q_NULLPTR) { delete rigCtl; } delete rpt; delete ui; delete settings; #if defined(PORTAUDIO) Pa_Terminate(); #endif } void wfmain::closeEvent(QCloseEvent *event) { // Are you sure? if (!prefs.confirmExit) { QApplication::exit(); } QCheckBox *cb = new QCheckBox("Don't ask me again"); QMessageBox msgbox; msgbox.setText("Are you sure you wish to exit?\n"); msgbox.setIcon(QMessageBox::Icon::Question); QAbstractButton *yesButton = msgbox.addButton(QMessageBox::Yes); msgbox.addButton(QMessageBox::No); msgbox.setDefaultButton(QMessageBox::Yes); msgbox.setCheckBox(cb); QObject::connect(cb, &QCheckBox::stateChanged, [this](int state){ if (static_cast(state) == Qt::CheckState::Checked) { prefs.confirmExit=false; } else { prefs.confirmExit=true; } settings->beginGroup("Interface"); settings->setValue("ConfirmExit", this->prefs.confirmExit); settings->endGroup(); settings->sync(); }); msgbox.exec(); if (msgbox.clickedButton() == yesButton) { QApplication::exit(); } else { event->ignore(); } } void wfmain::openRig() { // This function is intended to handle opening a connection to the rig. // the connection can be either serial or network, // and this function is also responsible for initiating the search for a rig model and capabilities. // Any errors, such as unable to open connection or unable to open port, are to be reported to the user. //TODO: if(hasRunPreviously) //TODO: if(useNetwork){... // } else { // if (prefs.fileWasNotFound) { // showRigSettings(); // rig setting dialog box for network/serial, CIV, hostname, port, baud rate, serial device, etc // TODO: How do we know if the setting was loaded? // TODO: Use these if they are found if(!serialPortCL.isEmpty()) { qDebug(logSystem()) << "Serial port specified by user: " << serialPortCL; } else { qDebug(logSystem()) << "Serial port not specified. "; } if(!hostCL.isEmpty()) { qDebug(logSystem()) << "Remote host name specified by user: " << hostCL; } makeRig(); if (prefs.enableLAN) { ui->lanEnableBtn->setChecked(true); usingLAN = true; // We need to setup the tx/rx audio: emit sendCommSetup(prefs.radioCIVAddr, udpPrefs, rxSetup, txSetup, prefs.virtualSerialPort); } else { ui->serialEnableBtn->setChecked(true); if( (prefs.serialPortRadio.toLower() == QString("auto")) && (serialPortCL.isEmpty())) { findSerialPort(); } else { if(serialPortCL.isEmpty()) { serialPortRig = prefs.serialPortRadio; } else { serialPortRig = serialPortCL; } } usingLAN = false; emit sendCommSetup(prefs.radioCIVAddr, serialPortRig, prefs.serialPortBaud,prefs.virtualSerialPort); ui->statusBar->showMessage(QString("Connecting to rig using serial port ").append(serialPortRig), 1000); } } void wfmain::rigConnections() { connect(this, SIGNAL(setCIVAddr(unsigned char)), rig, SLOT(setCIVAddr(unsigned char))); connect(this, SIGNAL(sendPowerOn()), rig, SLOT(powerOn())); connect(this, SIGNAL(sendPowerOff()), rig, SLOT(powerOff())); connect(rig, SIGNAL(haveFrequency(freqt)), this, SLOT(receiveFreq(freqt))); connect(this, SIGNAL(getFrequency()), rig, SLOT(getFrequency())); connect(this, SIGNAL(getMode()), rig, SLOT(getMode())); connect(this, SIGNAL(getDataMode()), rig, SLOT(getDataMode())); connect(this, SIGNAL(setDataMode(bool, unsigned char)), rig, SLOT(setDataMode(bool, unsigned char))); connect(this, SIGNAL(getBandStackReg(char,char)), rig, SLOT(getBandStackReg(char,char))); connect(rig, SIGNAL(havePTTStatus(bool)), this, SLOT(receivePTTstatus(bool))); connect(this, SIGNAL(setPTT(bool)), rig, SLOT(setPTT(bool))); connect(this, SIGNAL(getPTT()), rig, SLOT(getPTT())); connect(rig, SIGNAL(haveBandStackReg(freqt,char,char,bool)), this, SLOT(receiveBandStackReg(freqt,char,char,bool))); connect(this, SIGNAL(setRitEnable(bool)), rig, SLOT(setRitEnable(bool))); connect(this, SIGNAL(setRitValue(int)), rig, SLOT(setRitValue(int))); connect(rig, SIGNAL(haveRitEnabled(bool)), this, SLOT(receiveRITStatus(bool))); connect(rig, SIGNAL(haveRitFrequency(int)), this, SLOT(receiveRITValue(int))); connect(this, SIGNAL(getRitEnabled()), rig, SLOT(getRitEnabled())); connect(this, SIGNAL(getRitValue()), rig, SLOT(getRitValue())); connect(this, SIGNAL(getDebug()), rig, SLOT(getDebug())); connect(this, SIGNAL(spectOutputDisable()), rig, SLOT(disableSpectOutput())); connect(this, SIGNAL(spectOutputEnable()), rig, SLOT(enableSpectOutput())); connect(this, SIGNAL(scopeDisplayDisable()), rig, SLOT(disableSpectrumDisplay())); connect(this, SIGNAL(scopeDisplayEnable()), rig, SLOT(enableSpectrumDisplay())); connect(rig, SIGNAL(haveMode(unsigned char, unsigned char)), this, SLOT(receiveMode(unsigned char, unsigned char))); connect(rig, SIGNAL(haveDataMode(bool)), this, SLOT(receiveDataModeStatus(bool))); connect(rpt, SIGNAL(getDuplexMode()), rig, SLOT(getDuplexMode())); connect(rpt, SIGNAL(setDuplexMode(duplexMode)), rig, SLOT(setDuplexMode(duplexMode))); connect(rig, SIGNAL(haveDuplexMode(duplexMode)), rpt, SLOT(receiveDuplexMode(duplexMode))); connect(rpt, SIGNAL(getTone()), rig, SLOT(getTone())); connect(rpt, SIGNAL(getTSQL()), rig, SLOT(getTSQL())); connect(rpt, SIGNAL(getDTCS()), rig, SLOT(getDTCS())); connect(rpt, SIGNAL(setTone(quint16)), rig, SLOT(setTone(quint16))); connect(rpt, SIGNAL(setTSQL(quint16)), rig, SLOT(setTSQL(quint16))); connect(rpt, SIGNAL(setDTCS(quint16,bool,bool)), rig, SLOT(setDTCS(quint16,bool,bool))); connect(rpt, SIGNAL(getRptAccessMode()), rig, SLOT(getRptAccessMode())); connect(rpt, SIGNAL(setRptAccessMode(rptAccessTxRx)), rig, SLOT(setRptAccessMode(rptAccessTxRx))); connect(rig, SIGNAL(haveTone(quint16)), rpt, SLOT(handleTone(quint16))); connect(rig, SIGNAL(haveTSQL(quint16)), rpt, SLOT(handleTSQL(quint16))); connect(rig, SIGNAL(haveDTCS(quint16,bool,bool)), rpt, SLOT(handleDTCS(quint16,bool,bool))); connect(rig, SIGNAL(haveRptAccessMode(rptAccessTxRx)), rpt, SLOT(handleRptAccessMode(rptAccessTxRx))); connect(this, SIGNAL(getDuplexMode()), rig, SLOT(getDuplexMode())); connect(this, SIGNAL(getTone()), rig, SLOT(getTone())); connect(this, SIGNAL(getTSQL()), rig, SLOT(getTSQL())); connect(this, SIGNAL(getRptAccessMode()), rig, SLOT(getRptAccessMode())); //connect(this, SIGNAL(setDuplexMode(duplexMode)), rig, SLOT(setDuplexMode(duplexMode))); //connect(rig, SIGNAL(haveDuplexMode(duplexMode)), this, SLOT(receiveDuplexMode(duplexMode))); connect(this, SIGNAL(getModInput(bool)), rig, SLOT(getModInput(bool))); connect(rig, SIGNAL(haveModInput(rigInput,bool)), this, SLOT(receiveModInput(rigInput, bool))); connect(this, SIGNAL(setModInput(rigInput, bool)), rig, SLOT(setModInput(rigInput,bool))); connect(rig, SIGNAL(haveSpectrumData(QByteArray, double, double)), this, SLOT(receiveSpectrumData(QByteArray, double, double))); connect(rig, SIGNAL(haveSpectrumMode(spectrumMode)), this, SLOT(receiveSpectrumMode(spectrumMode))); connect(this, SIGNAL(setScopeMode(spectrumMode)), rig, SLOT(setSpectrumMode(spectrumMode))); connect(this, SIGNAL(getScopeMode()), rig, SLOT(getScopeMode())); connect(this, SIGNAL(setFrequency(unsigned char, freqt)), rig, SLOT(setFrequency(unsigned char, freqt))); connect(this, SIGNAL(setScopeEdge(char)), rig, SLOT(setScopeEdge(char))); connect(this, SIGNAL(setScopeSpan(char)), rig, SLOT(setScopeSpan(char))); //connect(this, SIGNAL(getScopeMode()), rig, SLOT(getScopeMode())); connect(this, SIGNAL(getScopeEdge()), rig, SLOT(getScopeEdge())); connect(this, SIGNAL(getScopeSpan()), rig, SLOT(getScopeSpan())); connect(rig, SIGNAL(haveScopeSpan(freqt,bool)), this, SLOT(receiveSpectrumSpan(freqt,bool))); connect(this, SIGNAL(setScopeFixedEdge(double,double,unsigned char)), rig, SLOT(setSpectrumBounds(double,double,unsigned char))); connect(this, SIGNAL(setMode(unsigned char, unsigned char)), rig, SLOT(setMode(unsigned char, unsigned char))); connect(this, SIGNAL(setMode(mode_info)), rig, SLOT(setMode(mode_info))); // Levels (read and write) // Levels: Query: connect(this, SIGNAL(getLevels()), rig, SLOT(getLevels())); connect(this, SIGNAL(getRfGain()), rig, SLOT(getRfGain())); connect(this, SIGNAL(getAfGain()), rig, SLOT(getAfGain())); connect(this, SIGNAL(getSql()), rig, SLOT(getSql())); connect(this, SIGNAL(getIfShift()), rig, SLOT(getIFShift())); connect(this, SIGNAL(getTPBFInner()), rig, SLOT(getTPBFInner())); connect(this, SIGNAL(getTPBFOuter()), rig, SLOT(getTPBFOuter())); connect(this, SIGNAL(getTxPower()), rig, SLOT(getTxLevel())); connect(this, SIGNAL(getMicGain()), rig, SLOT(getMicGain())); connect(this, SIGNAL(getSpectrumRefLevel()), rig, SLOT(getSpectrumRefLevel())); connect(this, SIGNAL(getModInputLevel(rigInput)), rig, SLOT(getModInputLevel(rigInput))); // Levels: Set: connect(this, SIGNAL(setRfGain(unsigned char)), rig, SLOT(setRfGain(unsigned char))); connect(this, SIGNAL(setAfGain(unsigned char)), rig, SLOT(setAfGain(unsigned char))); connect(this, SIGNAL(setSql(unsigned char)), rig, SLOT(setSquelch(unsigned char))); connect(this, SIGNAL(setIFShift(unsigned char)), rig, SLOT(setIFShift(unsigned char))); connect(this, SIGNAL(setTPBFInner(unsigned char)), rig, SLOT(setTPBFInner(unsigned char))); connect(this, SIGNAL(setTPBFOuter(unsigned char)), rig, SLOT(setTPBFOuter(unsigned char))); connect(this, SIGNAL(setTxPower(unsigned char)), rig, SLOT(setTxPower(unsigned char))); connect(this, SIGNAL(setMicGain(unsigned char)), rig, SLOT(setMicGain(unsigned char))); connect(this, SIGNAL(setMonitorLevel(unsigned char)), rig, SLOT(setMonitorLevel(unsigned char))); connect(this, SIGNAL(setVoxGain(unsigned char)), rig, SLOT(setVoxGain(unsigned char))); connect(this, SIGNAL(setAntiVoxGain(unsigned char)), rig, SLOT(setAntiVoxGain(unsigned char))); connect(this, SIGNAL(setSpectrumRefLevel(int)), rig, SLOT(setSpectrumRefLevel(int))); connect(this, SIGNAL(setModLevel(rigInput, unsigned char)), rig, SLOT(setModInputLevel(rigInput, unsigned char))); // Levels: handle return on query: connect(rig, SIGNAL(haveRfGain(unsigned char)), this, SLOT(receiveRfGain(unsigned char))); connect(rig, SIGNAL(haveAfGain(unsigned char)), this, SLOT(receiveAfGain(unsigned char))); connect(rig, SIGNAL(haveSql(unsigned char)), this, SLOT(receiveSql(unsigned char))); connect(rig, SIGNAL(haveIFShift(unsigned char)), trxadj, SLOT(updateIFShift(unsigned char))); connect(rig, SIGNAL(haveTPBFInner(unsigned char)), trxadj, SLOT(updateTPBFInner(unsigned char))); connect(rig, SIGNAL(haveTPBFOuter(unsigned char)), trxadj, SLOT(updateTPBFOuter(unsigned char))); connect(rig, SIGNAL(haveTxPower(unsigned char)), this, SLOT(receiveTxPower(unsigned char))); connect(rig, SIGNAL(haveMicGain(unsigned char)), this, SLOT(receiveMicGain(unsigned char))); connect(rig, SIGNAL(haveSpectrumRefLevel(int)), this, SLOT(receiveSpectrumRefLevel(int))); connect(rig, SIGNAL(haveACCGain(unsigned char,unsigned char)), this, SLOT(receiveACCGain(unsigned char,unsigned char))); connect(rig, SIGNAL(haveUSBGain(unsigned char)), this, SLOT(receiveUSBGain(unsigned char))); connect(rig, SIGNAL(haveLANGain(unsigned char)), this, SLOT(receiveLANGain(unsigned char))); //Metering: connect(this, SIGNAL(getMeters(meterKind)), rig, SLOT(getMeters(meterKind))); connect(rig, SIGNAL(haveMeter(meterKind,unsigned char)), this, SLOT(receiveMeter(meterKind,unsigned char))); // Rig and ATU info: connect(this, SIGNAL(startATU()), rig, SLOT(startATU())); connect(this, SIGNAL(setATU(bool)), rig, SLOT(setATU(bool))); connect(this, SIGNAL(getATUStatus()), rig, SLOT(getATUStatus())); connect(this, SIGNAL(getRigID()), rig, SLOT(getRigID())); connect(rig, SIGNAL(haveATUStatus(unsigned char)), this, SLOT(receiveATUStatus(unsigned char))); connect(rig, SIGNAL(haveRigID(rigCapabilities)), this, SLOT(receiveRigID(rigCapabilities))); connect(this, SIGNAL(setAttenuator(unsigned char)), rig, SLOT(setAttenuator(unsigned char))); connect(this, SIGNAL(setPreamp(unsigned char)), rig, SLOT(setPreamp(unsigned char))); connect(this, SIGNAL(setAntenna(unsigned char, bool)), rig, SLOT(setAntenna(unsigned char, bool))); connect(this, SIGNAL(getPreamp()), rig, SLOT(getPreamp())); connect(rig, SIGNAL(havePreamp(unsigned char)), this, SLOT(receivePreamp(unsigned char))); connect(this, SIGNAL(getAttenuator()), rig, SLOT(getAttenuator())); connect(rig, SIGNAL(haveAttenuator(unsigned char)), this, SLOT(receiveAttenuator(unsigned char))); connect(this, SIGNAL(getAntenna()), rig, SLOT(getAntenna())); connect(rig, SIGNAL(haveAntenna(unsigned char,bool)), this, SLOT(receiveAntennaSel(unsigned char,bool))); // Speech (emitted from rig speaker) connect(this, SIGNAL(sayAll()), rig, SLOT(sayAll())); connect(this, SIGNAL(sayFrequency()), rig, SLOT(sayFrequency())); connect(this, SIGNAL(sayMode()), rig, SLOT(sayMode())); // calibration window: connect(cal, SIGNAL(requestRefAdjustCourse()), rig, SLOT(getRefAdjustCourse())); connect(cal, SIGNAL(requestRefAdjustFine()), rig, SLOT(getRefAdjustFine())); connect(rig, SIGNAL(haveRefAdjustCourse(unsigned char)), cal, SLOT(handleRefAdjustCourse(unsigned char))); connect(rig, SIGNAL(haveRefAdjustFine(unsigned char)), cal, SLOT(handleRefAdjustFine(unsigned char))); connect(cal, SIGNAL(setRefAdjustCourse(unsigned char)), rig, SLOT(setRefAdjustCourse(unsigned char))); connect(cal, SIGNAL(setRefAdjustFine(unsigned char)), rig, SLOT(setRefAdjustFine(unsigned char))); // Date and Time: connect(this, SIGNAL(setTime(timekind)), rig, SLOT(setTime(timekind))); connect(this, SIGNAL(setDate(datekind)), rig, SLOT(setDate(datekind))); connect(this, SIGNAL(setUTCOffset(timekind)), rig, SLOT(setUTCOffset(timekind))); } //void wfmain::removeRigConnections() //{ //} void wfmain::makeRig() { if (rigThread == Q_NULLPTR) { rig = new rigCommander(); rigThread = new QThread(this); // Thread: rig->moveToThread(rigThread); connect(rigThread, SIGNAL(started()), rig, SLOT(process())); connect(rigThread, SIGNAL(finished()), rig, SLOT(deleteLater())); rigThread->start(); // Rig status and Errors: connect(rig, SIGNAL(haveSerialPortError(QString, QString)), this, SLOT(receiveSerialPortError(QString, QString))); connect(rig, SIGNAL(haveStatusUpdate(QString)), this, SLOT(receiveStatusUpdate(QString))); // Rig comm setup: connect(this, SIGNAL(sendCommSetup(unsigned char, udpPreferences, audioSetup, audioSetup, QString)), rig, SLOT(commSetup(unsigned char, udpPreferences, audioSetup, audioSetup, QString))); connect(this, SIGNAL(sendCommSetup(unsigned char, QString, quint32,QString)), rig, SLOT(commSetup(unsigned char, QString, quint32,QString))); connect(rig, SIGNAL(haveBaudRate(quint32)), this, SLOT(receiveBaudRate(quint32))); connect(this, SIGNAL(sendCloseComm()), rig, SLOT(closeComm())); connect(this, SIGNAL(sendChangeLatency(quint16)), rig, SLOT(changeLatency(quint16))); connect(this, SIGNAL(getRigCIV()), rig, SLOT(findRigs())); connect(this, SIGNAL(setRigID(unsigned char)), rig, SLOT(setRigID(unsigned char))); connect(rig, SIGNAL(discoveredRigID(rigCapabilities)), this, SLOT(receiveFoundRigID(rigCapabilities))); connect(rig, SIGNAL(commReady()), this, SLOT(receiveCommReady())); if (rigCtl != Q_NULLPTR) { connect(rig, SIGNAL(stateInfo(rigStateStruct*)), rigCtl, SLOT(receiveStateInfo(rigStateStruct*))); connect(this, SIGNAL(requestRigState()), rig, SLOT(sendState())); connect(rigCtl, SIGNAL(setFrequency(unsigned char, freqt)), rig, SLOT(setFrequency(unsigned char, freqt))); connect(rigCtl, SIGNAL(setMode(unsigned char, unsigned char)), rig, SLOT(setMode(unsigned char, unsigned char))); connect(rigCtl, SIGNAL(setDataMode(bool, unsigned char)), rig, SLOT(setDataMode(bool, unsigned char))); connect(rigCtl, SIGNAL(setPTT(bool)), rig, SLOT(setPTT(bool))); connect(rigCtl, SIGNAL(sendPowerOn()), rig, SLOT(powerOn())); connect(rigCtl, SIGNAL(sendPowerOff()), rig, SLOT(powerOff())); connect(rigCtl, SIGNAL(setAttenuator(unsigned char)), rig, SLOT(setAttenuator(unsigned char))); connect(rigCtl, SIGNAL(setPreamp(unsigned char)), rig, SLOT(setPreamp(unsigned char))); connect(rigCtl, SIGNAL(setDuplexMode(duplexMode)), rig, SLOT(setDuplexMode(duplexMode))); // Levels: Set: connect(rigCtl, SIGNAL(setRfGain(unsigned char)), rig, SLOT(setRfGain(unsigned char))); connect(rigCtl, SIGNAL(setAfGain(unsigned char)), rig, SLOT(setAfGain(unsigned char))); connect(rigCtl, SIGNAL(setSql(unsigned char)), rig, SLOT(setSquelch(unsigned char))); connect(rigCtl, SIGNAL(setTxPower(unsigned char)), rig, SLOT(setTxPower(unsigned char))); connect(rigCtl, SIGNAL(setMicGain(unsigned char)), rig, SLOT(setMicGain(unsigned char))); connect(rigCtl, SIGNAL(setMonitorLevel(unsigned char)), rig, SLOT(setMonitorLevel(unsigned char))); connect(rigCtl, SIGNAL(setVoxGain(unsigned char)), rig, SLOT(setVoxGain(unsigned char))); connect(rigCtl, SIGNAL(setAntiVoxGain(unsigned char)), rig, SLOT(setAntiVoxGain(unsigned char))); connect(rigCtl, SIGNAL(setSpectrumRefLevel(int)), rig, SLOT(setSpectrumRefLevel(int))); } } } void wfmain::removeRig() { if (rigThread != Q_NULLPTR) { if (rigCtl != Q_NULLPTR) { rigCtl->disconnect(); } rigThread->disconnect(); rig->disconnect(); delete rigThread; delete rig; rig = Q_NULLPTR; } } void wfmain::findSerialPort() { // Find the ICOM radio connected, or, if none, fall back to OS default. // qInfo(logSystem()) << "Searching for serial port..."; QDirIterator it73("/dev/serial/by-id", QStringList() << "*IC-7300*", QDir::Files, QDirIterator::Subdirectories); QDirIterator it97("/dev/serial", QStringList() << "*IC-9700*A*", QDir::Files, QDirIterator::Subdirectories); QDirIterator it785x("/dev/serial", QStringList() << "*IC-785*A*", QDir::Files, QDirIterator::Subdirectories); QDirIterator it705("/dev/serial", QStringList() << "*IC-705*A", QDir::Files, QDirIterator::Subdirectories); QDirIterator it7610("/dev/serial", QStringList() << "*IC-7610*A", QDir::Files, QDirIterator::Subdirectories); QDirIterator itR8600("/dev/serial", QStringList() << "*IC-R8600*A", QDir::Files, QDirIterator::Subdirectories); if(!it73.filePath().isEmpty()) { // IC-7300 serialPortRig = it73.filePath(); // first } else if(!it97.filePath().isEmpty()) { // IC-9700 serialPortRig = it97.filePath(); } else if(!it785x.filePath().isEmpty()) { // IC-785x serialPortRig = it785x.filePath(); } else if(!it705.filePath().isEmpty()) { // IC-705 serialPortRig = it705.filePath(); } else if(!it7610.filePath().isEmpty()) { // IC-7610 serialPortRig = it7610.filePath(); } else if(!itR8600.filePath().isEmpty()) { // IC-R8600 serialPortRig = itR8600.filePath(); } else { //fall back: qInfo(logSystem()) << "Could not find Icom serial port. Falling back to OS default. Use --port to specify, or modify preferences."; #ifdef Q_OS_MAC serialPortRig = QString("/dev/tty.SLAB_USBtoUART"); #endif #ifdef Q_OS_LINUX serialPortRig = QString("/dev/ttyUSB0"); #endif #ifdef Q_OS_WIN serialPortRig = QString("COM1"); #endif } } void wfmain::receiveCommReady() { qInfo(logSystem()) << "Received CommReady!! "; if(!usingLAN) { // usingLAN gets set when we emit the sendCommSetup signal. // If we're not using the LAN, then we're on serial, and // we already know the baud rate and can calculate the timing parameters. calculateTimingParameters(); } if(prefs.radioCIVAddr == 0) { // tell rigCommander to broadcast a request for all rig IDs. // qInfo(logSystem()) << "Beginning search from wfview for rigCIV (auto-detection broadcast)"; ui->statusBar->showMessage(QString("Searching CI-V bus for connected radios."), 1000); emit getRigCIV(); issueDelayedCommand(cmdGetRigCIV); delayedCommand->start(); } else { // don't bother, they told us the CIV they want, stick with it. // We still query the rigID to find the model, but at least we know the CIV. qInfo(logSystem()) << "Skipping automatic CIV, using user-supplied value of " << prefs.radioCIVAddr; showStatusBarText(QString("Using user-supplied radio CI-V address of 0x%1").arg(prefs.radioCIVAddr, 2, 16)); if(prefs.CIVisRadioModel) { qInfo(logSystem()) << "Skipping Rig ID query, using user-supplied model from CI-V address: " << prefs.radioCIVAddr; emit setRigID(prefs.radioCIVAddr); } else { emit getRigID(); getInitialRigState(); } } } void wfmain::receiveFoundRigID(rigCapabilities rigCaps) { // Entry point for unknown rig being identified at the start of the program. //now we know what the rig ID is: //qInfo(logSystem()) << "In wfview, we now have a reply to our request for rig identity sent to CIV BROADCAST."; if(rig->usingLAN()) { usingLAN = true; } else { usingLAN = false; } receiveRigID(rigCaps); getInitialRigState(); return; } void wfmain::receiveSerialPortError(QString port, QString errorText) { qInfo(logSystem()) << "wfmain: received serial port error for port: " << port << " with message: " << errorText; ui->statusBar->showMessage(QString("ERROR: using port ").append(port).append(": ").append(errorText), 10000); // TODO: Dialog box, exit, etc } void wfmain::receiveStatusUpdate(QString text) { this->rigStatus->setText(text); } void wfmain::setupPlots() { // Line 290-- spectrumDrawLock = true; plot = ui->plot; // rename it waterfall. wf = ui->waterfall; freqIndicatorLine = new QCPItemLine(plot); freqIndicatorLine->setAntialiased(true); freqIndicatorLine->setPen(QPen(Qt::blue)); // ui->plot->addGraph(); // primary ui->plot->addGraph(0, 0); // secondary, peaks, same axis as first? ui->waterfall->addGraph(); colorMap = new QCPColorMap(wf->xAxis, wf->yAxis); colorMapData = NULL; #if QCUSTOMPLOT_VERSION < 0x020001 wf->addPlottable(colorMap); #endif colorScale = new QCPColorScale(wf); ui->tabWidget->setCurrentIndex(0); QColor color(20+200/4.0*1,70*(1.6-1/4.0), 150, 150); plot->graph(1)->setLineStyle(QCPGraph::lsLine); plot->graph(1)->setPen(QPen(color.lighter(200))); plot->graph(1)->setBrush(QBrush(color)); freqIndicatorLine->start->setCoords(0.5,0); freqIndicatorLine->end->setCoords(0.5,160); // Plot user interaction connect(plot, SIGNAL(mouseDoubleClick(QMouseEvent*)), this, SLOT(handlePlotDoubleClick(QMouseEvent*))); connect(wf, SIGNAL(mouseDoubleClick(QMouseEvent*)), this, SLOT(handleWFDoubleClick(QMouseEvent*))); connect(plot, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(handlePlotClick(QMouseEvent*))); connect(wf, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(handleWFClick(QMouseEvent*))); connect(wf, SIGNAL(mouseWheel(QWheelEvent*)), this, SLOT(handleWFScroll(QWheelEvent*))); connect(plot, SIGNAL(mouseWheel(QWheelEvent*)), this, SLOT(handlePlotScroll(QWheelEvent*))); spectrumDrawLock = false; } void wfmain::setupMainUI() { ui->bandStkLastUsedBtn->setVisible(false); ui->bandStkVoiceBtn->setVisible(false); ui->bandStkDataBtn->setVisible(false); ui->bandStkCWBtn->setVisible(false); ui->baudRateCombo->insertItem(0, QString("115200"), 115200); ui->baudRateCombo->insertItem(1, QString("57600"), 57600); ui->baudRateCombo->insertItem(2, QString("38400"), 38400); ui->baudRateCombo->insertItem(3, QString("28800"), 28800); ui->baudRateCombo->insertItem(4, QString("19200"), 19200); ui->baudRateCombo->insertItem(5, QString("9600"), 9600); ui->baudRateCombo->insertItem(6, QString("4800"), 4800); ui->baudRateCombo->insertItem(7, QString("2400"), 2400); ui->baudRateCombo->insertItem(8, QString("1200"), 1200); ui->baudRateCombo->insertItem(9, QString("300"), 300); ui->spectrumModeCombo->addItem("Center", (spectrumMode)spectModeCenter); ui->spectrumModeCombo->addItem("Fixed", (spectrumMode)spectModeFixed); ui->spectrumModeCombo->addItem("Scroll-C", (spectrumMode)spectModeScrollC); ui->spectrumModeCombo->addItem("Scroll-F", (spectrumMode)spectModeScrollF); ui->modeSelectCombo->addItem("LSB", 0x00); ui->modeSelectCombo->addItem("USB", 0x01); ui->modeSelectCombo->addItem("FM", 0x05); ui->modeSelectCombo->addItem("AM", 0x02); ui->modeSelectCombo->addItem("CW", 0x03); ui->modeSelectCombo->addItem("CW-R", 0x07); ui->modeSelectCombo->addItem("RTTY", 0x04); ui->modeSelectCombo->addItem("RTTY-R", 0x08); ui->modeFilterCombo->addItem("1", 1); ui->modeFilterCombo->addItem("2", 2); ui->modeFilterCombo->addItem("3", 3); ui->modeFilterCombo->addItem("Setup...", 99); ui->tuningStepCombo->blockSignals(true); ui->tuningStepCombo->addItem("1 Hz", (unsigned int) 1); ui->tuningStepCombo->addItem("10 Hz", (unsigned int) 10); ui->tuningStepCombo->addItem("100 Hz", (unsigned int) 100); ui->tuningStepCombo->addItem("1 kHz", (unsigned int) 1000); ui->tuningStepCombo->addItem("2.5 kHz", (unsigned int) 2500); ui->tuningStepCombo->addItem("5 kHz", (unsigned int) 5000); ui->tuningStepCombo->addItem("6.125 kHz", (unsigned int) 6125); // PMR ui->tuningStepCombo->addItem("8.333 kHz", (unsigned int) 8333); // airband stepsize ui->tuningStepCombo->addItem("9 kHz", (unsigned int) 9000); // European medium wave stepsize ui->tuningStepCombo->addItem("10 kHz", (unsigned int) 10000); ui->tuningStepCombo->addItem("12.5 kHz", (unsigned int) 12500); ui->tuningStepCombo->addItem("25 kHz", (unsigned int) 25000); ui->tuningStepCombo->addItem("100 kHz", (unsigned int) 100000); ui->tuningStepCombo->addItem("250 kHz", (unsigned int) 250000); ui->tuningStepCombo->addItem("1 MHz", (unsigned int) 1000000); //for 23 cm and HF ui->tuningStepCombo->setCurrentIndex(2); ui->tuningStepCombo->blockSignals(false); ui->wfthemeCombo->addItem("Jet", QCPColorGradient::gpJet); ui->wfthemeCombo->addItem("Cold", QCPColorGradient::gpCold); ui->wfthemeCombo->addItem("Hot", QCPColorGradient::gpHot); ui->wfthemeCombo->addItem("Thermal", QCPColorGradient::gpThermal); ui->wfthemeCombo->addItem("Night", QCPColorGradient::gpNight); ui->wfthemeCombo->addItem("Ion", QCPColorGradient::gpIon); ui->wfthemeCombo->addItem("Gray", QCPColorGradient::gpGrayscale); ui->wfthemeCombo->addItem("Geography", QCPColorGradient::gpGeography); ui->wfthemeCombo->addItem("Hues", QCPColorGradient::gpHues); ui->wfthemeCombo->addItem("Polar", QCPColorGradient::gpPolar); ui->wfthemeCombo->addItem("Spectrum", QCPColorGradient::gpSpectrum); ui->wfthemeCombo->addItem("Candy", QCPColorGradient::gpCandy); ui->meter2selectionCombo->addItem("None", meterNone); ui->meter2selectionCombo->addItem("SWR", meterSWR); ui->meter2selectionCombo->addItem("ALC", meterALC); ui->meter2selectionCombo->addItem("Compression", meterComp); ui->meter2selectionCombo->addItem("Voltage", meterVoltage); ui->meter2selectionCombo->addItem("Current", meterCurrent); ui->meter2selectionCombo->addItem("Center", meterCenter); ui->meter2Widget->hide(); ui->meter2selectionCombo->show(); ui->meter2selectionCombo->setCurrentIndex((int)prefs.meter2Type); ui->secondaryMeterSelectionLabel->show(); // Future ideas: //ui->meter2selectionCombo->addItem("Transmit Audio", meterTxMod); //ui->meter2selectionCombo->addItem("Receive Audio", meterRxAudio); //ui->meter2selectionCombo->addItem("Latency", meterLatency); spans << "2.5k" << "5.0k" << "10k" << "25k"; spans << "50k" << "100k" << "250k" << "500k"; ui->scopeBWCombo->insertItems(0, spans); edges << "1" << "2" << "3" << "4"; ui->scopeEdgeCombo->insertItems(0, edges); ui->splitter->setHandleWidth(5); // Set scroll wheel response (tick interval) // and set arrow key response (single step) ui->rfGainSlider->setTickInterval(100); ui->rfGainSlider->setSingleStep(10); ui->afGainSlider->setTickInterval(100); ui->afGainSlider->setSingleStep(10); ui->sqlSlider->setTickInterval(100); ui->sqlSlider->setSingleStep(10); ui->txPowerSlider->setTickInterval(100); ui->txPowerSlider->setSingleStep(10); ui->micGainSlider->setTickInterval(100); ui->micGainSlider->setSingleStep(10); ui->scopeRefLevelSlider->setTickInterval(50); ui->scopeRefLevelSlider->setSingleStep(20); ui->freqMhzLineEdit->setValidator( new QDoubleValidator(0, 100, 6, this)); ui->controlPortTxt->setValidator(new QIntValidator(this)); qDebug(logSystem()) << "Running with debugging options enabled."; #ifdef QT_DEBUG ui->debugBtn->setVisible(true); ui->satOpsBtn->setVisible(true); #else ui->debugBtn->setVisible(false); ui->satOpsBtn->setVisible(false); #endif rigStatus = new QLabel(this); ui->statusBar->addPermanentWidget(rigStatus); ui->statusBar->showMessage("Connecting to rig...", 1000); pttLed = new QLedLabel(this); ui->statusBar->addPermanentWidget(pttLed); pttLed->setState(QLedLabel::State::StateOk); connectedLed = new QLedLabel(this); ui->statusBar->addPermanentWidget(connectedLed); rigName = new QLabel(this); rigName->setAlignment(Qt::AlignRight); ui->statusBar->addPermanentWidget(rigName); rigName->setText("NONE"); rigName->setFixedWidth(60); freq.MHzDouble = 0.0; freq.Hz = 0; oldFreqDialVal = ui->freqDial->value(); ui->tuneLockChk->setChecked(false); freqLock = false; connect(ui->tabWidget, SIGNAL(currentChanged(int)), this, SLOT(updateSizes(int))); connect( ui->txPowerSlider, &QSlider::valueChanged, [=](const int &newValue) { statusFromSliderPercent("Tx Power", newValue);} ); connect( ui->rfGainSlider, &QSlider::valueChanged, [=](const int &newValue) { statusFromSliderPercent("RF Gain", newValue);} ); connect( ui->afGainSlider, &QSlider::valueChanged, [=](const int &newValue) { statusFromSliderPercent("AF Gain", newValue);} ); connect( ui->micGainSlider, &QSlider::valueChanged, [=](const int &newValue) { statusFromSliderPercent("TX Audio Gain", newValue);} ); connect( ui->sqlSlider, &QSlider::valueChanged, [=](const int &newValue) { statusFromSliderPercent("Squelch", newValue);} ); // -200 0 +200.. take log? connect( ui->scopeRefLevelSlider, &QSlider::valueChanged, [=](const int &newValue) { statusFromSliderRaw("Scope Ref Level", newValue);} ); connect( ui->wfLengthSlider, &QSlider::valueChanged, [=](const int &newValue) { statusFromSliderRaw("Waterfall Length", newValue);} ); connect(this->trxadj, &transceiverAdjustments::setIFShift, [=](const unsigned char &newValue) { issueCmdUniquePriority(cmdSetIFShift, newValue);} ); connect(this->trxadj, &transceiverAdjustments::setTPBFInner, [=](const unsigned char &newValue) { issueCmdUniquePriority(cmdSetTPBFInner, newValue);} ); connect(this->trxadj, &transceiverAdjustments::setTPBFOuter, [=](const unsigned char &newValue) { issueCmdUniquePriority(cmdSetTPBFOuter, newValue);} ); } void wfmain::updateSizes(int tabIndex) { if(!haveRigCaps) return; // This function does nothing unless you are using a rig without spectrum. // This is a hack. It is not great, but it seems to work ok. if(!rigCaps.hasSpectrum) { // Set "ignore" size policy for non-selected tabs: for(int i=0;itabWidget->count();i++) if((i!=tabIndex) && tabIndex != 0) ui->tabWidget->widget(i)->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); // allows size to be any size that fits the tab bar if(tabIndex==0 && !rigCaps.hasSpectrum) { ui->tabWidget->widget(0)->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); ui->tabWidget->widget(0)->setMaximumSize(ui->tabWidget->widget(0)->minimumSizeHint()); ui->tabWidget->widget(0)->adjustSize(); // tab this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); this->setMaximumSize(QSize(929, 270)); this->setMinimumSize(QSize(929, 270)); resize(minimumSize()); adjustSize(); // main window adjustSize(); } else if(tabIndex==0 && rigCaps.hasSpectrum) { // At main tab (0) and we have spectrum: ui->tabWidget->widget(0)->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); resize(minimumSizeHint()); adjustSize(); // Without this call, the window retains the size of the previous tab. } else { // At some other tab, with or without spectrum: ui->tabWidget->widget(tabIndex)->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); this->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); this->setMinimumSize(QSize(994, 455)); // not large enough for settings tab this->setMaximumSize(QSize(65535,65535)); } } else { ui->tabWidget->widget(tabIndex)->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); ui->tabWidget->widget(tabIndex)->setMaximumSize(65535,65535); //ui->tabWidget->widget(0)->setMinimumSize(); } } void wfmain::getSettingsFilePath(QString settingsFile) { if (settingsFile.isNull()) { settings = new QSettings(); } else { QString file = settingsFile; QFile info(settingsFile); QString path=""; if (!QFileInfo(info).isAbsolute()) { path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (path.isEmpty()) { path = QDir::homePath(); } path = path + "/"; file = info.fileName(); } qInfo(logSystem()) << "Loading settings from:" << path + file; settings = new QSettings(path + file, QSettings::Format::IniFormat); } } void wfmain::setInitialTiming() { loopTickCounter = 0; delayedCmdIntervalLAN_ms = 70; // interval for regular delayed commands, including initial rig/UI state queries delayedCmdIntervalSerial_ms = 100; // interval for regular delayed commands, including initial rig/UI state queries delayedCmdStartupInterval_ms = 250; // interval for rigID polling delayedCommand = new QTimer(this); delayedCommand->setInterval(delayedCmdStartupInterval_ms); // 250ms until we find rig civ and id, then 100ms. delayedCommand->setSingleShot(false); connect(delayedCommand, SIGNAL(timeout()), this, SLOT(sendRadioCommandLoop())); // TODO: Remove this: // periodicPollingTimer = new QTimer(this); // periodicPollingTimer->setInterval(10); // periodicPollingTimer->setSingleShot(false); //connect(periodicPollingTimer, SIGNAL(timeout()), this, SLOT(sendRadioCommandLoop())); pttTimer = new QTimer(this); pttTimer->setInterval(180*1000); // 3 minute max transmit time in ms pttTimer->setSingleShot(true); connect(pttTimer, SIGNAL(timeout()), this, SLOT(handlePttLimit())); timeSync = new QTimer(this); connect(timeSync, SIGNAL(timeout()), this, SLOT(setRadioTimeDateSend())); waitingToSetTimeDate = false; lastFreqCmdTime_ms = QDateTime::currentMSecsSinceEpoch() - 5000; // 5 seconds ago } void wfmain::setServerToPrefs() { // Start server if enabled in config if (serverConfig.enabled) { serverConfig.lan = prefs.enableLAN; udp = new udpServer(serverConfig,rxSetup,txSetup); serverThread = new QThread(this); udp->moveToThread(serverThread); connect(this, SIGNAL(initServer()), udp, SLOT(init())); connect(serverThread, SIGNAL(finished()), udp, SLOT(deleteLater())); if (!prefs.enableLAN && udp != Q_NULLPTR) { connect(udp, SIGNAL(haveNetworkStatus(QString)), this, SLOT(receiveStatusUpdate(QString))); } serverThread->start(); emit initServer(); connect(this, SIGNAL(sendRigCaps(rigCapabilities)), udp, SLOT(receiveRigCaps(rigCapabilities))); } } void wfmain::setUIToPrefs() { ui->fullScreenChk->setChecked(prefs.useFullScreen); on_fullScreenChk_clicked(prefs.useFullScreen); ui->useDarkThemeChk->setChecked(prefs.useDarkMode); on_useDarkThemeChk_clicked(prefs.useDarkMode); ui->useSystemThemeChk->setChecked(prefs.useSystemTheme); on_useSystemThemeChk_clicked(prefs.useSystemTheme); ui->drawPeakChk->setChecked(prefs.drawPeaks); on_drawPeakChk_clicked(prefs.drawPeaks); drawPeaks = prefs.drawPeaks; ui->wfAntiAliasChk->setChecked(prefs.wfAntiAlias); on_wfAntiAliasChk_clicked(prefs.wfAntiAlias); ui->wfInterpolateChk->setChecked(prefs.wfInterpolate); on_wfInterpolateChk_clicked(prefs.wfInterpolate); ui->wfLengthSlider->setValue(prefs.wflength); prepareWf(prefs.wflength); ui->wfthemeCombo->setCurrentIndex(ui->wfthemeCombo->findData(prefs.wftheme)); colorMap->setGradient(static_cast(prefs.wftheme)); ui->useCIVasRigIDChk->blockSignals(true); ui->useCIVasRigIDChk->setChecked(prefs.CIVisRadioModel); ui->useCIVasRigIDChk->blockSignals(false); } void wfmain::setAudioDevicesUI() { #if defined(RTAUDIO) #if defined(Q_OS_LINUX) RtAudio* audio = new RtAudio(RtAudio::Api::LINUX_ALSA); #elif defined(Q_OS_WIN) RtAudio* audio = new RtAudio(RtAudio::Api::WINDOWS_WASAPI); #elif defined(Q_OS_MACX) RtAudio* audio = new RtAudio(RtAudio::Api::MACOSX_CORE); #endif // Enumerate audio devices, need to do before settings are loaded. std::map apiMap; apiMap[RtAudio::MACOSX_CORE] = "OS-X Core Audio"; apiMap[RtAudio::WINDOWS_ASIO] = "Windows ASIO"; apiMap[RtAudio::WINDOWS_DS] = "Windows DirectSound"; apiMap[RtAudio::WINDOWS_WASAPI] = "Windows WASAPI"; apiMap[RtAudio::UNIX_JACK] = "Jack Client"; apiMap[RtAudio::LINUX_ALSA] = "Linux ALSA"; apiMap[RtAudio::LINUX_PULSE] = "Linux PulseAudio"; apiMap[RtAudio::LINUX_OSS] = "Linux OSS"; apiMap[RtAudio::RTAUDIO_DUMMY] = "RtAudio Dummy"; std::vector< RtAudio::Api > apis; RtAudio::getCompiledApi(apis); qInfo(logAudio()) << "RtAudio Version " << QString::fromStdString(RtAudio::getVersion()); qInfo(logAudio()) << "Compiled APIs:"; for (unsigned int i = 0; i < apis.size(); i++) { qInfo(logAudio()) << " " << QString::fromStdString(apiMap[apis[i]]); } RtAudio::DeviceInfo info; qInfo(logAudio()) << "Current API: " << QString::fromStdString(apiMap[audio->getCurrentApi()]); unsigned int devices = audio->getDeviceCount(); qInfo(logAudio()) << "Found " << devices << " audio device(s) *=default"; for (unsigned int i = 1; i < devices; i++) { info = audio->getDeviceInfo(i); if (info.outputChannels > 0) { qInfo(logAudio()) << (info.isDefaultOutput ? "*" : " ") << "(" << i << ") Output Device : " << QString::fromStdString(info.name); ui->audioOutputCombo->addItem(QString::fromStdString(info.name), i); } if (info.inputChannels > 0) { qInfo(logAudio()) << (info.isDefaultInput ? "*" : " ") << "(" << i << ") Input Device : " << QString::fromStdString(info.name); ui->audioInputCombo->addItem(QString::fromStdString(info.name), i); } } delete audio; #elif defined(PORTAUDIO) // Use PortAudio device enumeration PaError err; err = Pa_Initialize(); if (err != paNoError) { qInfo(logAudio()) << "ERROR: Cannot initialize Portaudio"; } qInfo(logAudio()) << "PortAudio version: " << Pa_GetVersionInfo()->versionText; int numDevices; numDevices = Pa_GetDeviceCount(); qInfo(logAudio()) << "Pa_CountDevices returned" << numDevices; const PaDeviceInfo* info; for (int i = 0; i < numDevices; i++) { info = Pa_GetDeviceInfo(i); if (info->maxInputChannels > 0) { qInfo(logAudio()) << (i == Pa_GetDefaultInputDevice() ? "*" : " ") << "(" << i << ") Output Device : " << info->name; ui->audioInputCombo->addItem(info->name, i); } if (info->maxOutputChannels > 0) { qInfo(logAudio()) << (i == Pa_GetDefaultOutputDevice() ? "*" : " ") << "(" << i << ") Input Device : " << info->name; ui->audioOutputCombo->addItem(info->name, i); } } #else // If no external library is configured, use QTMultimedia // Enumerate audio devices, need to do before settings are loaded. const auto audioOutputs = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); for (const QAudioDeviceInfo& deviceInfo : audioOutputs) { ui->audioOutputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); } const auto audioInputs = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); for (const QAudioDeviceInfo& deviceInfo : audioInputs) { ui->audioInputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); } // Set these to default audio devices initially. rxSetup.port = QAudioDeviceInfo::defaultOutputDevice(); txSetup.port = QAudioDeviceInfo::defaultInputDevice(); #endif } void wfmain::setSerialDevicesUI() { ui->serialDeviceListCombo->blockSignals(true); ui->serialDeviceListCombo->addItem("Auto", 0); int i = 0; foreach(const QSerialPortInfo & serialPortInfo, QSerialPortInfo::availablePorts()) { portList.append(serialPortInfo.portName()); #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) ui->serialDeviceListCombo->addItem(QString("/dev/")+serialPortInfo.portName(), i++); #else ui->serialDeviceListCombo->addItem(serialPortInfo.portName(), i++); #endif } #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) ui->serialDeviceListCombo->addItem("Manual...", 256); #endif ui->serialDeviceListCombo->blockSignals(false); ui->vspCombo->blockSignals(true); #ifdef Q_OS_WIN ui->vspCombo->addItem(QString("None"), i++); foreach(const QSerialPortInfo & serialPortInfo, QSerialPortInfo::availablePorts()) { ui->vspCombo->addItem(serialPortInfo.portName()); } #else // Provide reasonable names for the symbolic link to the pty device #ifdef Q_OS_MAC QString vspName = QStandardPaths::standardLocations(QStandardPaths::DownloadLocation)[0] + "/rig-pty"; #else QString vspName=QDir::homePath()+"/rig-pty"; #endif for (i=1;i<8;i++) { ui->vspCombo->addItem(vspName + QString::number(i)); if (QFile::exists(vspName+QString::number(i))) { auto * model = qobject_cast(ui->vspCombo->model()); auto *item = model->item(ui->vspCombo->count()-1); item->setEnabled(false); } } ui->vspCombo->addItem(vspName+QString::number(i)); ui->vspCombo->addItem(QString("None"), i++); #endif ui->vspCombo->setEditable(true); ui->vspCombo->blockSignals(false); } void wfmain::setupKeyShortcuts() { keyF1 = new QShortcut(this); keyF1->setKey(Qt::Key_F1); connect(keyF1, SIGNAL(activated()), this, SLOT(shortcutF1())); keyF2 = new QShortcut(this); keyF2->setKey(Qt::Key_F2); connect(keyF2, SIGNAL(activated()), this, SLOT(shortcutF2())); keyF3 = new QShortcut(this); keyF3->setKey(Qt::Key_F3); connect(keyF3, SIGNAL(activated()), this, SLOT(shortcutF3())); keyF4 = new QShortcut(this); keyF4->setKey(Qt::Key_F4); connect(keyF4, SIGNAL(activated()), this, SLOT(shortcutF4())); keyF5 = new QShortcut(this); keyF5->setKey(Qt::Key_F5); connect(keyF5, SIGNAL(activated()), this, SLOT(shortcutF5())); keyF6 = new QShortcut(this); keyF6->setKey(Qt::Key_F6); connect(keyF6, SIGNAL(activated()), this, SLOT(shortcutF6())); keyF7 = new QShortcut(this); keyF7->setKey(Qt::Key_F7); connect(keyF7, SIGNAL(activated()), this, SLOT(shortcutF7())); keyF8 = new QShortcut(this); keyF8->setKey(Qt::Key_F8); connect(keyF8, SIGNAL(activated()), this, SLOT(shortcutF8())); keyF9 = new QShortcut(this); keyF9->setKey(Qt::Key_F9); connect(keyF9, SIGNAL(activated()), this, SLOT(shortcutF9())); keyF10 = new QShortcut(this); keyF10->setKey(Qt::Key_F10); connect(keyF10, SIGNAL(activated()), this, SLOT(shortcutF10())); keyF11 = new QShortcut(this); keyF11->setKey(Qt::Key_F11); connect(keyF11, SIGNAL(activated()), this, SLOT(shortcutF11())); keyF12 = new QShortcut(this); keyF12->setKey(Qt::Key_F12); connect(keyF12, SIGNAL(activated()), this, SLOT(shortcutF12())); keyControlT = new QShortcut(this); keyControlT->setKey(Qt::CTRL + Qt::Key_T); connect(keyControlT, SIGNAL(activated()), this, SLOT(shortcutControlT())); keyControlR = new QShortcut(this); keyControlR->setKey(Qt::CTRL + Qt::Key_R); connect(keyControlR, SIGNAL(activated()), this, SLOT(shortcutControlR())); keyControlI = new QShortcut(this); keyControlI->setKey(Qt::CTRL + Qt::Key_I); connect(keyControlI, SIGNAL(activated()), this, SLOT(shortcutControlI())); keyControlU = new QShortcut(this); keyControlU->setKey(Qt::CTRL + Qt::Key_U); connect(keyControlU, SIGNAL(activated()), this, SLOT(shortcutControlU())); keyStar = new QShortcut(this); keyStar->setKey(Qt::Key_Asterisk); connect(keyStar, SIGNAL(activated()), this, SLOT(shortcutStar())); keySlash = new QShortcut(this); keySlash->setKey(Qt::Key_Slash); connect(keySlash, SIGNAL(activated()), this, SLOT(shortcutSlash())); keyMinus = new QShortcut(this); keyMinus->setKey(Qt::Key_Minus); connect(keyMinus, SIGNAL(activated()), this, SLOT(shortcutMinus())); keyPlus = new QShortcut(this); keyPlus->setKey(Qt::Key_Plus); connect(keyPlus, SIGNAL(activated()), this, SLOT(shortcutPlus())); keyShiftMinus = new QShortcut(this); keyShiftMinus->setKey(Qt::SHIFT + Qt::Key_Minus); connect(keyShiftMinus, SIGNAL(activated()), this, SLOT(shortcutShiftMinus())); keyShiftPlus = new QShortcut(this); keyShiftPlus->setKey(Qt::SHIFT + Qt::Key_Plus); connect(keyShiftPlus, SIGNAL(activated()), this, SLOT(shortcutShiftPlus())); keyControlMinus = new QShortcut(this); keyControlMinus->setKey(Qt::CTRL + Qt::Key_Minus); connect(keyControlMinus, SIGNAL(activated()), this, SLOT(shortcutControlMinus())); keyControlPlus = new QShortcut(this); keyControlPlus->setKey(Qt::CTRL + Qt::Key_Plus); connect(keyControlPlus, SIGNAL(activated()), this, SLOT(shortcutControlPlus())); keyQuit = new QShortcut(this); keyQuit->setKey(Qt::CTRL + Qt::Key_Q); connect(keyQuit, SIGNAL(activated()), this, SLOT(on_exitBtn_clicked())); keyPageUp = new QShortcut(this); keyPageUp->setKey(Qt::Key_PageUp); connect(keyPageUp, SIGNAL(activated()), this, SLOT(shortcutPageUp())); keyPageDown = new QShortcut(this); keyPageDown->setKey(Qt::Key_PageDown); connect(keyPageDown, SIGNAL(activated()), this, SLOT(shortcutPageDown())); keyF = new QShortcut(this); keyF->setKey(Qt::Key_F); connect(keyF, SIGNAL(activated()), this, SLOT(shortcutF())); keyM = new QShortcut(this); keyM->setKey(Qt::Key_M); connect(keyM, SIGNAL(activated()), this, SLOT(shortcutM())); keyDebug = new QShortcut(this); keyDebug->setKey(Qt::CTRL + Qt::SHIFT + Qt::Key_D); connect(keyDebug, SIGNAL(activated()), this, SLOT(on_debugBtn_clicked())); } void wfmain::setDefPrefs() { defPrefs.useFullScreen = false; defPrefs.useDarkMode = true; defPrefs.useSystemTheme = false; defPrefs.drawPeaks = true; defPrefs.wfAntiAlias = false; defPrefs.wfInterpolate = true; defPrefs.stylesheetPath = QString("qdarkstyle/style.qss"); defPrefs.radioCIVAddr = 0x00; // previously was 0x94 for 7300. defPrefs.CIVisRadioModel = false; defPrefs.serialPortRadio = QString("auto"); defPrefs.serialPortBaud = 115200; defPrefs.enablePTT = false; defPrefs.niceTS = true; defPrefs.enableRigCtlD = false; defPrefs.rigCtlPort = 4533; defPrefs.virtualSerialPort = QString("none"); defPrefs.localAFgain = 255; defPrefs.wflength = 160; defPrefs.wftheme = static_cast(QCPColorGradient::gpJet); defPrefs.confirmExit = true; defPrefs.confirmPowerOff = true; defPrefs.meter2Type = meterNone; udpDefPrefs.ipAddress = QString(""); udpDefPrefs.controlLANPort = 50001; udpDefPrefs.serialLANPort = 50002; udpDefPrefs.audioLANPort = 50003; udpDefPrefs.username = QString(""); udpDefPrefs.password = QString(""); udpDefPrefs.clientName = QHostInfo::localHostName(); } void wfmain::loadSettings() { qInfo(logSystem()) << "Loading settings from " << settings->fileName(); // Basic things to load: // UI: (full screen, dark theme, draw peaks, colors, etc) settings->beginGroup("Interface"); prefs.useFullScreen = settings->value("UseFullScreen", defPrefs.useFullScreen).toBool(); prefs.useDarkMode = settings->value("UseDarkMode", defPrefs.useDarkMode).toBool(); prefs.useSystemTheme = settings->value("UseSystemTheme", defPrefs.useSystemTheme).toBool(); prefs.wftheme = settings->value("WFTheme", defPrefs.wftheme).toInt(); prefs.drawPeaks = settings->value("DrawPeaks", defPrefs.drawPeaks).toBool(); prefs.wfAntiAlias = settings->value("WFAntiAlias", defPrefs.wfAntiAlias).toBool(); prefs.wfInterpolate = settings->value("WFInterpolate", defPrefs.wfInterpolate).toBool(); prefs.wflength = (unsigned int) settings->value("WFLength", defPrefs.wflength).toInt(); prefs.stylesheetPath = settings->value("StylesheetPath", defPrefs.stylesheetPath).toString(); ui->splitter->restoreState(settings->value("splitter").toByteArray()); restoreGeometry(settings->value("windowGeometry").toByteArray()); restoreState(settings->value("windowState").toByteArray()); setWindowState(Qt::WindowActive); // Works around QT bug to returns window+keyboard focus. prefs.confirmExit = settings->value("ConfirmExit", defPrefs.confirmExit).toBool(); prefs.confirmPowerOff = settings->value("ConfirmPowerOff", defPrefs.confirmPowerOff).toBool(); prefs.meter2Type = static_cast(settings->value("Meter2Type", defPrefs.meter2Type).toInt()); settings->endGroup(); // Load color schemes: // Per this bug: https://forum.qt.io/topic/24725/solved-qvariant-will-drop-alpha-value-when-save-qcolor/5 // the alpha channel is dropped when converting raw qvariant of QColor. Therefore, we are storing as unsigned int and converting back. settings->beginGroup("DarkColors"); prefs.colorScheme.Dark_PlotBackground = QColor::fromRgba(settings->value("Dark_PlotBackground", defaultColors.Dark_PlotBackground.rgba()).toUInt()); prefs.colorScheme.Dark_PlotAxisPen = QColor::fromRgba(settings->value("Dark_PlotAxisPen", defaultColors.Dark_PlotAxisPen.rgba()).toUInt()); prefs.colorScheme.Dark_PlotLegendTextColor = QColor::fromRgba(settings->value("Dark_PlotLegendTextColor", defaultColors.Dark_PlotLegendTextColor.rgba()).toUInt()); prefs.colorScheme.Dark_PlotLegendBorderPen = QColor::fromRgba(settings->value("Dark_PlotLegendBorderPen", defaultColors.Dark_PlotLegendBorderPen.rgba()).toUInt()); prefs.colorScheme.Dark_PlotLegendBrush = QColor::fromRgba(settings->value("Dark_PlotLegendBrush", defaultColors.Dark_PlotLegendBrush.rgba()).toUInt()); prefs.colorScheme.Dark_PlotTickLabel = QColor::fromRgba(settings->value("Dark_PlotTickLabel", defaultColors.Dark_PlotTickLabel.rgba()).toUInt()); prefs.colorScheme.Dark_PlotBasePen = QColor::fromRgba(settings->value("Dark_PlotBasePen", defaultColors.Dark_PlotBasePen.rgba()).toUInt()); prefs.colorScheme.Dark_PlotTickPen = QColor::fromRgba(settings->value("Dark_PlotTickPen", defaultColors.Dark_PlotTickPen.rgba()).toUInt()); prefs.colorScheme.Dark_PeakPlotLine = QColor::fromRgba(settings->value("Dark_PeakPlotLine", defaultColors.Dark_PeakPlotLine.rgba()).toUInt()); prefs.colorScheme.Dark_TuningLine = QColor::fromRgba(settings->value("Dark_TuningLine", defaultColors.Dark_TuningLine.rgba()).toUInt()); settings->endGroup(); settings->beginGroup("LightColors"); prefs.colorScheme.Light_PlotBackground = QColor::fromRgba(settings->value("Light_PlotBackground", defaultColors.Light_PlotBackground.rgba()).toUInt()); prefs.colorScheme.Light_PlotAxisPen = QColor::fromRgba(settings->value("Light_PlotAxisPen", defaultColors.Light_PlotAxisPen.rgba()).toUInt()); prefs.colorScheme.Light_PlotLegendTextColor = QColor::fromRgba(settings->value("Light_PlotLegendTextColo", defaultColors.Light_PlotLegendTextColor.rgba()).toUInt()); prefs.colorScheme.Light_PlotLegendBorderPen = QColor::fromRgba(settings->value("Light_PlotLegendBorderPen", defaultColors.Light_PlotLegendBorderPen.rgba()).toUInt()); prefs.colorScheme.Light_PlotLegendBrush = QColor::fromRgba(settings->value("Light_PlotLegendBrush", defaultColors.Light_PlotLegendBrush.rgba()).toUInt()); prefs.colorScheme.Light_PlotTickLabel = QColor::fromRgba(settings->value("Light_PlotTickLabel", defaultColors.Light_PlotTickLabel.rgba()).toUInt()); prefs.colorScheme.Light_PlotBasePen = QColor::fromRgba(settings->value("Light_PlotBasePen", defaultColors.Light_PlotBasePen.rgba()).toUInt()); prefs.colorScheme.Light_PlotTickPen = QColor::fromRgba(settings->value("Light_PlotTickPen", defaultColors.Light_PlotTickPen.rgba()).toUInt()); prefs.colorScheme.Light_PeakPlotLine = QColor::fromRgba(settings->value("Light_PeakPlotLine", defaultColors.Light_PeakPlotLine.rgba()).toUInt()); prefs.colorScheme.Light_TuningLine = QColor::fromRgba(settings->value("Light_TuningLine", defaultColors.Light_TuningLine.rgba()).toUInt()); settings->endGroup(); // Radio and Comms: C-IV addr, port to use settings->beginGroup("Radio"); prefs.radioCIVAddr = (unsigned char) settings->value("RigCIVuInt", defPrefs.radioCIVAddr).toInt(); if(prefs.radioCIVAddr!=0) { ui->rigCIVManualAddrChk->setChecked(true); ui->rigCIVaddrHexLine->blockSignals(true); ui->rigCIVaddrHexLine->setText(QString("%1").arg(prefs.radioCIVAddr, 2, 16)); ui->rigCIVaddrHexLine->setEnabled(true); ui->rigCIVaddrHexLine->blockSignals(false); } else { ui->rigCIVManualAddrChk->setChecked(false); ui->rigCIVaddrHexLine->setEnabled(false); } prefs.CIVisRadioModel = (bool)settings->value("CIVisRadioModel", defPrefs.CIVisRadioModel).toBool(); prefs.serialPortRadio = settings->value("SerialPortRadio", defPrefs.serialPortRadio).toString(); int serialIndex = ui->serialDeviceListCombo->findText(prefs.serialPortRadio); if (serialIndex != -1) { ui->serialDeviceListCombo->setCurrentIndex(serialIndex); } prefs.serialPortBaud = (quint32) settings->value("SerialPortBaud", defPrefs.serialPortBaud).toInt(); ui->baudRateCombo->blockSignals(true); ui->baudRateCombo->setCurrentIndex( ui->baudRateCombo->findData(prefs.serialPortBaud) ); ui->baudRateCombo->blockSignals(false); if (prefs.serialPortBaud > 0) { serverConfig.baudRate = prefs.serialPortBaud; } prefs.virtualSerialPort = settings->value("VirtualSerialPort", defPrefs.virtualSerialPort).toString(); int vspIndex = ui->vspCombo->findText(prefs.virtualSerialPort); if (vspIndex != -1) { ui->vspCombo->setCurrentIndex(vspIndex); } else { ui->vspCombo->addItem(prefs.virtualSerialPort); ui->vspCombo->setCurrentIndex(ui->vspCombo->count()-1); } prefs.localAFgain = (unsigned char) settings->value("localAFgain", defPrefs.localAFgain).toUInt(); rxSetup.localAFgain = prefs.localAFgain; settings->endGroup(); // Misc. user settings (enable PTT, draw peaks, etc) settings->beginGroup("Controls"); prefs.enablePTT = settings->value("EnablePTT", defPrefs.enablePTT).toBool(); ui->pttEnableChk->setChecked(prefs.enablePTT); prefs.niceTS = settings->value("NiceTS", defPrefs.niceTS).toBool(); settings->endGroup(); settings->beginGroup("LAN"); prefs.enableLAN = settings->value("EnableLAN", defPrefs.enableLAN).toBool(); if(prefs.enableLAN) { ui->baudRateCombo->setEnabled(false); ui->serialDeviceListCombo->setEnabled(false); //ui->udpServerSetupBtn->setEnabled(false); } else { ui->baudRateCombo->setEnabled(true); ui->serialDeviceListCombo->setEnabled(true); //ui->udpServerSetupBtn->setEnabled(true); } ui->lanEnableBtn->setChecked(prefs.enableLAN); ui->connectBtn->setEnabled(true); prefs.enableRigCtlD = settings->value("EnableRigCtlD", defPrefs.enableRigCtlD).toBool(); ui->enableRigctldChk->setChecked(prefs.enableRigCtlD); prefs.rigCtlPort = settings->value("RigCtlPort", defPrefs.rigCtlPort).toInt(); ui->rigctldPortTxt->setText(QString("%1").arg(prefs.rigCtlPort)); // Call the function to start rigctld if enabled. on_enableRigctldChk_clicked(prefs.enableRigCtlD); udpPrefs.ipAddress = settings->value("IPAddress", udpDefPrefs.ipAddress).toString(); ui->ipAddressTxt->setEnabled(ui->lanEnableBtn->isChecked()); ui->ipAddressTxt->setText(udpPrefs.ipAddress); udpPrefs.controlLANPort = settings->value("ControlLANPort", udpDefPrefs.controlLANPort).toInt(); ui->controlPortTxt->setEnabled(ui->lanEnableBtn->isChecked()); ui->controlPortTxt->setText(QString("%1").arg(udpPrefs.controlLANPort)); udpPrefs.username = settings->value("Username", udpDefPrefs.username).toString(); ui->usernameTxt->setEnabled(ui->lanEnableBtn->isChecked()); ui->usernameTxt->setText(QString("%1").arg(udpPrefs.username)); udpPrefs.password = settings->value("Password", udpDefPrefs.password).toString(); ui->passwordTxt->setEnabled(ui->lanEnableBtn->isChecked()); ui->passwordTxt->setText(QString("%1").arg(udpPrefs.password)); rxSetup.isinput = false; txSetup.isinput = true; rxSetup.latency = settings->value("AudioRXLatency", "150").toInt(); ui->rxLatencySlider->setEnabled(ui->lanEnableBtn->isChecked()); ui->rxLatencySlider->setValue(rxSetup.latency); ui->rxLatencySlider->setTracking(false); // Stop it sending value on every change. txSetup.latency = settings->value("AudioTXLatency", "150").toInt(); ui->txLatencySlider->setEnabled(ui->lanEnableBtn->isChecked()); ui->txLatencySlider->setValue(txSetup.latency); ui->txLatencySlider->setTracking(false); // Stop it sending value on every change. ui->audioSampleRateCombo->blockSignals(true); rxSetup.samplerate = settings->value("AudioRXSampleRate", "48000").toInt(); txSetup.samplerate = rxSetup.samplerate; ui->audioSampleRateCombo->setEnabled(ui->lanEnableBtn->isChecked()); int audioSampleRateIndex = ui->audioSampleRateCombo->findText(QString::number(rxSetup.samplerate)); if (audioSampleRateIndex != -1) { ui->audioSampleRateCombo->setCurrentIndex(audioSampleRateIndex); } ui->audioSampleRateCombo->blockSignals(false); // Add codec combobox items here so that we can add userdata! ui->audioRXCodecCombo->addItem("LPCM 1ch 16bit", 4); ui->audioRXCodecCombo->addItem("LPCM 1ch 8bit", 2); ui->audioRXCodecCombo->addItem("uLaw 1ch 8bit", 1); ui->audioRXCodecCombo->addItem("LPCM 2ch 16bit", 16); ui->audioRXCodecCombo->addItem("uLaw 2ch 8bit", 32); ui->audioRXCodecCombo->addItem("PCM 2ch 8bit", 8); ui->audioRXCodecCombo->addItem("Opus 1ch", 64); ui->audioRXCodecCombo->addItem("Opus 2ch", 128); ui->audioRXCodecCombo->blockSignals(true); rxSetup.codec = settings->value("AudioRXCodec", "4").toInt(); ui->audioRXCodecCombo->setEnabled(ui->lanEnableBtn->isChecked()); for (int f = 0; f < ui->audioRXCodecCombo->count(); f++) if (ui->audioRXCodecCombo->itemData(f).toInt() == rxSetup.codec) ui->audioRXCodecCombo->setCurrentIndex(f); ui->audioRXCodecCombo->blockSignals(false); ui->audioTXCodecCombo->addItem("LPCM 1ch 16bit", 4); ui->audioTXCodecCombo->addItem("LPCM 1ch 8bit", 2); ui->audioTXCodecCombo->addItem("uLaw 1ch 8bit", 1); ui->audioTXCodecCombo->addItem("Opus 1ch", 64); ui->audioRXCodecCombo->blockSignals(true); txSetup.codec = settings->value("AudioTXCodec", "4").toInt(); ui->audioTXCodecCombo->setEnabled(ui->lanEnableBtn->isChecked()); for (int f = 0; f < ui->audioTXCodecCombo->count(); f++) if (ui->audioTXCodecCombo->itemData(f).toInt() == txSetup.codec) ui->audioTXCodecCombo->setCurrentIndex(f); ui->audioRXCodecCombo->blockSignals(false); ui->audioOutputCombo->blockSignals(true); rxSetup.name = settings->value("AudioOutput", "").toString(); qInfo(logGui()) << "Got Audio Output: " << rxSetup.name; int audioOutputIndex = ui->audioOutputCombo->findText(rxSetup.name); if (audioOutputIndex != -1) { ui->audioOutputCombo->setCurrentIndex(audioOutputIndex); #if defined(RTAUDIO) rxSetup.port = ui->audioOutputCombo->itemData(audioOutputIndex).toInt(); #elif defined(PORTAUDIO) rxSetup.port = ui->audioOutputCombo->itemData(audioOutputIndex).toInt(); #else QVariant v = ui->audioOutputCombo->currentData(); rxSetup.port = v.value(); #endif } ui->audioOutputCombo->blockSignals(false); ui->audioInputCombo->blockSignals(true); txSetup.name = settings->value("AudioInput", "").toString(); qInfo(logGui()) << "Got Audio Input: " << txSetup.name; int audioInputIndex = ui->audioInputCombo->findText(txSetup.name); if (audioInputIndex != -1) { ui->audioInputCombo->setCurrentIndex(audioInputIndex); #if defined(RTAUDIO) txSetup.port = ui->audioInputCombo->itemData(audioInputIndex).toInt(); #elif defined(PORTAUDIO) txSetup.port = ui->audioInputCombo->itemData(audioInputIndex).toInt(); #else QVariant v = ui->audioInputCombo->currentData(); txSetup.port = v.value(); #endif } ui->audioInputCombo->blockSignals(false); rxSetup.resampleQuality = settings->value("ResampleQuality", "4").toInt(); txSetup.resampleQuality = rxSetup.resampleQuality; udpPrefs.clientName = settings->value("ClientName", udpDefPrefs.clientName).toString(); settings->endGroup(); settings->beginGroup("Server"); serverConfig.enabled = settings->value("ServerEnabled", false).toBool(); serverConfig.controlPort = settings->value("ServerControlPort", 50001).toInt(); serverConfig.civPort = settings->value("ServerCivPort", 50002).toInt(); serverConfig.audioPort = settings->value("ServerAudioPort", 50003).toInt(); int numUsers = settings->value("ServerNumUsers", 2).toInt(); serverConfig.users.clear(); for (int f = 0; f < numUsers; f++) { SERVERUSER user; user.username = settings->value("ServerUsername_" + QString::number(f), "").toString(); user.password = settings->value("ServerPassword_" + QString::number(f), "").toString(); user.userType = settings->value("ServerUserType_" + QString::number(f), 0).toInt(); serverConfig.users.append(user); } settings->endGroup(); // Memory channels settings->beginGroup("Memory"); int size = settings->beginReadArray("Channel"); int chan = 0; double freq; unsigned char mode; bool isSet; // Annoying: QSettings will write the array to the // preference file starting the array at 1 and ending at 100. // Thus, we are writing the channel number each time. // It is also annoying that they get written with their array // numbers in alphabetical order without zero padding. // Also annoying that the preference groups are not written in // the order they are specified here. for(int i=0; i < size; i++) { settings->setArrayIndex(i); chan = settings->value("chan", 0).toInt(); freq = settings->value("freq", 12.345).toDouble(); mode = settings->value("mode", 0).toInt(); isSet = settings->value("isSet", false).toBool(); if(isSet) { mem.setPreset(chan, freq, (mode_kind)mode); } } settings->endArray(); settings->endGroup(); emit sendServerConfig(serverConfig); } void wfmain::saveSettings() { qInfo(logSystem()) << "Saving settings to " << settings->fileName(); // Basic things to load: // UI: (full screen, dark theme, draw peaks, colors, etc) settings->beginGroup("Interface"); settings->setValue("UseFullScreen", prefs.useFullScreen); settings->setValue("UseSystemTheme", prefs.useSystemTheme); settings->setValue("UseDarkMode", prefs.useDarkMode); settings->setValue("DrawPeaks", prefs.drawPeaks); settings->setValue("WFAntiAlias", prefs.wfAntiAlias); settings->setValue("WFInterpolate", prefs.wfInterpolate); settings->setValue("WFTheme", prefs.wftheme); settings->setValue("StylesheetPath", prefs.stylesheetPath); settings->setValue("splitter", ui->splitter->saveState()); settings->setValue("windowGeometry", saveGeometry()); settings->setValue("windowState", saveState()); settings->setValue("WFLength", prefs.wflength); settings->setValue("ConfirmExit", prefs.confirmExit); settings->setValue("ConfirmPowerOff", prefs.confirmPowerOff); settings->setValue("Meter2Type", (int)prefs.meter2Type); settings->endGroup(); // Radio and Comms: C-IV addr, port to use settings->beginGroup("Radio"); settings->setValue("RigCIVuInt", prefs.radioCIVAddr); settings->setValue("CIVisRadioModel", prefs.CIVisRadioModel); settings->setValue("SerialPortRadio", prefs.serialPortRadio); settings->setValue("SerialPortBaud", prefs.serialPortBaud); settings->setValue("VirtualSerialPort", prefs.virtualSerialPort); settings->setValue("localAFgain", prefs.localAFgain); settings->endGroup(); // Misc. user settings (enable PTT, draw peaks, etc) settings->beginGroup("Controls"); settings->setValue("EnablePTT", prefs.enablePTT); settings->setValue("NiceTS", prefs.niceTS); settings->endGroup(); settings->beginGroup("LAN"); settings->setValue("EnableLAN", prefs.enableLAN); settings->setValue("EnableRigCtlD", prefs.enableRigCtlD); settings->setValue("RigCtlPort", prefs.rigCtlPort); settings->setValue("IPAddress", udpPrefs.ipAddress); settings->setValue("ControlLANPort", udpPrefs.controlLANPort); settings->setValue("SerialLANPort", udpPrefs.serialLANPort); settings->setValue("AudioLANPort", udpPrefs.audioLANPort); settings->setValue("Username", udpPrefs.username); settings->setValue("Password", udpPrefs.password); settings->setValue("AudioRXLatency", rxSetup.latency); settings->setValue("AudioTXLatency", txSetup.latency); settings->setValue("AudioRXSampleRate", rxSetup.samplerate); settings->setValue("AudioRXCodec", rxSetup.codec); settings->setValue("AudioTXSampleRate", txSetup.samplerate); settings->setValue("AudioTXCodec", txSetup.codec); settings->setValue("AudioOutput", rxSetup.name); settings->setValue("AudioInput", txSetup.name); settings->setValue("ResampleQuality", rxSetup.resampleQuality); settings->setValue("ClientName", udpPrefs.clientName); settings->endGroup(); // Memory channels settings->beginGroup("Memory"); settings->beginWriteArray("Channel", (int)mem.getNumPresets()); preset_kind temp; for(int i=0; i < (int)mem.getNumPresets(); i++) { temp = mem.getPreset((int)i); settings->setArrayIndex(i); settings->setValue("chan", i); settings->setValue("freq", temp.frequency); settings->setValue("mode", temp.mode); settings->setValue("isSet", temp.isSet); } settings->endArray(); settings->endGroup(); // Note: X and Y get the same colors. See setPlotTheme() function settings->beginGroup("DarkColors"); settings->setValue("Dark_PlotBackground", prefs.colorScheme.Dark_PlotBackground.rgba()); settings->setValue("Dark_PlotAxisPen", prefs.colorScheme.Dark_PlotAxisPen.rgba()); settings->setValue("Dark_PlotLegendTextColor", prefs.colorScheme.Dark_PlotLegendTextColor.rgba()); settings->setValue("Dark_PlotLegendBorderPen", prefs.colorScheme.Dark_PlotLegendBorderPen.rgba()); settings->setValue("Dark_PlotLegendBrush", prefs.colorScheme.Dark_PlotLegendBrush.rgba()); settings->setValue("Dark_PlotTickLabel", prefs.colorScheme.Dark_PlotTickLabel.rgba()); settings->setValue("Dark_PlotBasePen", prefs.colorScheme.Dark_PlotBasePen.rgba()); settings->setValue("Dark_PlotTickPen", prefs.colorScheme.Dark_PlotTickPen.rgba()); settings->setValue("Dark_PeakPlotLine", prefs.colorScheme.Dark_PeakPlotLine.rgba()); settings->setValue("Dark_TuningLine", prefs.colorScheme.Dark_TuningLine.rgba()); settings->endGroup(); settings->beginGroup("LightColors"); settings->setValue("Light_PlotBackground", prefs.colorScheme.Light_PlotBackground.rgba()); settings->setValue("Light_PlotAxisPen", prefs.colorScheme.Light_PlotAxisPen.rgba()); settings->setValue("Light_PlotLegendTextColor", prefs.colorScheme.Light_PlotLegendTextColor.rgba()); settings->setValue("Light_PlotLegendBorderPen", prefs.colorScheme.Light_PlotLegendBorderPen.rgba()); settings->setValue("Light_PlotLegendBrush", prefs.colorScheme.Light_PlotLegendBrush.rgba()); settings->setValue("Light_PlotTickLabel", prefs.colorScheme.Light_PlotTickLabel.rgba()); settings->setValue("Light_PlotBasePen", prefs.colorScheme.Light_PlotBasePen.rgba()); settings->setValue("Light_PlotTickPen", prefs.colorScheme.Light_PlotTickPen.rgba()); settings->setValue("Light_PeakPlotLine", prefs.colorScheme.Light_PeakPlotLine.rgba()); settings->setValue("Light_TuningLine", prefs.colorScheme.Light_TuningLine.rgba()); settings->endGroup(); // This is a reference to see how the preference file is encoded. settings->beginGroup("StandardColors"); settings->setValue("white", QColor(Qt::white).rgba()); settings->setValue("black", QColor(Qt::black).rgba()); settings->setValue("red_opaque", QColor(Qt::red).rgba()); settings->setValue("red_translucent", QColor(255,0,0,128).rgba()); settings->setValue("green_opaque", QColor(Qt::green).rgba()); settings->setValue("green_translucent", QColor(0,255,0,128).rgba()); settings->setValue("blue_opaque", QColor(Qt::blue).rgba()); settings->setValue("blue_translucent", QColor(0,0,255,128).rgba()); settings->setValue("cyan", QColor(Qt::cyan).rgba()); settings->setValue("magenta", QColor(Qt::magenta).rgba()); settings->setValue("yellow", QColor(Qt::yellow).rgba()); settings->endGroup(); settings->beginGroup("Server"); settings->setValue("ServerEnabled", serverConfig.enabled); settings->setValue("ServerControlPort", serverConfig.controlPort); settings->setValue("ServerCivPort", serverConfig.civPort); settings->setValue("ServerAudioPort", serverConfig.audioPort); settings->setValue("ServerNumUsers", serverConfig.users.count()); for (int f = 0; f < serverConfig.users.count(); f++) { settings->setValue("ServerUsername_" + QString::number(f), serverConfig.users[f].username); settings->setValue("ServerPassword_" + QString::number(f), serverConfig.users[f].password); settings->setValue("ServerUserType_" + QString::number(f), serverConfig.users[f].userType); } settings->endGroup(); settings->sync(); // Automatic, not needed (supposedly) } void wfmain::showHideSpectrum(bool show) { if(show) { wf->show(); plot->show(); } else { wf->hide(); plot->hide(); } // Controls: ui->spectrumGroupBox->setVisible(show); ui->spectrumModeCombo->setVisible(show); ui->scopeBWCombo->setVisible(show); ui->scopeEdgeCombo->setVisible(show); ui->scopeEnableWFBtn->setVisible(show); ui->scopeRefLevelSlider->setEnabled(show); ui->wfLengthSlider->setEnabled(show); ui->wfthemeCombo->setVisible(show); ui->toFixedBtn->setVisible(show); ui->clearPeakBtn->setVisible(show); // And the labels: ui->specEdgeLabel->setVisible(show); ui->specModeLabel->setVisible(show); ui->specSpanLabel->setVisible(show); ui->specThemeLabel->setVisible(show); // And the layout for space: ui->specControlsHorizLayout->setEnabled(show); ui->splitter->setVisible(show); ui->plot->setVisible(show); ui->waterfall->setVisible(show); ui->spectrumGroupBox->setEnabled(show); // Window resize: updateSizes(ui->tabWidget->currentIndex()); } void wfmain::prepareWf() { prepareWf(160); } void wfmain::prepareWf(unsigned int wfLength) { // All this code gets moved in from the constructor of wfmain. if(haveRigCaps) { showHideSpectrum(rigCaps.hasSpectrum); if(!rigCaps.hasSpectrum) { return; } // TODO: Lock the function that draws on the spectrum while we are updating. spectrumDrawLock = true; spectWidth = rigCaps.spectLenMax; wfLengthMax = 1024; this->wfLength = wfLength; // fixed for now, time-length of waterfall // Initialize before use! QByteArray empty((int)spectWidth, '\x01'); spectrumPeaks = QByteArray( (int)spectWidth, '\x01' ); //wfimage.resize(wfLengthMax); if((unsigned int)wfimage.size() < wfLengthMax) { unsigned int i=0; unsigned int oldSize = wfimage.size(); for(i=oldSize; i<(wfLengthMax); i++) { wfimage.append(empty); } } else { // Keep wfimage, do not trim, no performance impact. //wfimage.remove(wfLength, wfimage.size()-wfLength); } wfimage.squeeze(); //colorMap->clearData(); colorMap->data()->clear(); colorMap->data()->setValueRange(QCPRange(0, wfLength-1)); colorMap->data()->setKeyRange(QCPRange(0, spectWidth-1)); colorMap->setDataRange(QCPRange(0, rigCaps.spectAmpMax)); colorMap->setGradient(static_cast(ui->wfthemeCombo->currentData().toInt())); if(colorMapData == Q_NULLPTR) { colorMapData = new QCPColorMapData(spectWidth, wfLength, QCPRange(0, spectWidth-1), QCPRange(0, wfLength-1)); } else { //delete colorMapData; // TODO: Figure out why it crashes if we delete first. colorMapData = new QCPColorMapData(spectWidth, wfLength, QCPRange(0, spectWidth-1), QCPRange(0, wfLength-1)); } colorMap->setData(colorMapData); wf->yAxis->setRangeReversed(true); wf->xAxis->setVisible(false); spectrumDrawLock = false; } else { qInfo(logSystem()) << "Cannot prepare WF view without rigCaps. Waiting on this."; return; } } // Key shortcuts (hotkeys) void wfmain::shortcutF11() { if(onFullscreen) { this->showNormal(); onFullscreen = false; } else { this->showFullScreen(); onFullscreen = true; } ui->fullScreenChk->setChecked(onFullscreen); } void wfmain::shortcutF1() { ui->tabWidget->setCurrentIndex(0); } void wfmain::shortcutF2() { ui->tabWidget->setCurrentIndex(1); } void wfmain::shortcutF3() { ui->tabWidget->setCurrentIndex(2); ui->freqMhzLineEdit->clear(); ui->freqMhzLineEdit->setFocus(); } void wfmain::shortcutF4() { ui->tabWidget->setCurrentIndex(3); } // Mode switch keys: void wfmain::shortcutF5() { // LSB changeMode(modeLSB, false); } void wfmain::shortcutF6() { // USB changeMode(modeUSB, false); } void wfmain::shortcutF7() { // AM changeMode(modeAM, false); } void wfmain::shortcutF8() { // CW changeMode(modeCW, false); } void wfmain::shortcutF9() { // USB-D changeMode(modeUSB, true); } void wfmain::shortcutF10() { // FM changeMode(modeFM, false); } void wfmain::shortcutF12() { // Speak current frequency and mode from the radio showStatusBarText("Sending speech command to radio."); emit sayAll(); } void wfmain::shortcutControlT() { // Transmit qInfo(logSystem()) << "Activated Control-T shortcut"; showStatusBarText(QString("Transmitting. Press Control-R to receive.")); ui->pttOnBtn->click(); } void wfmain::shortcutControlR() { // Receive issueCmdUniquePriority(cmdSetPTT, false); pttTimer->stop(); } void wfmain::shortcutControlI() { // Enable ATU ui->tuneEnableChk->click(); } void wfmain::shortcutControlU() { // Run ATU tuning cycle ui->tuneNowBtn->click(); } void wfmain::shortcutStar() { // Jump to frequency tab from Asterisk key on keypad ui->tabWidget->setCurrentIndex(2); ui->freqMhzLineEdit->clear(); ui->freqMhzLineEdit->setFocus(); } void wfmain::shortcutSlash() { // Cycle through available modes ui->modeSelectCombo->setCurrentIndex( (ui->modeSelectCombo->currentIndex()+1) % ui->modeSelectCombo->count() ); on_modeSelectCombo_activated( ui->modeSelectCombo->currentIndex() ); } void wfmain::setTuningSteps() { // TODO: interact with preferences, tuning step drop down box, and current operating mode // Units are MHz: tsPlusControl = 0.010f; tsPlus = 0.001f; tsPlusShift = 0.0001f; tsPage = 1.0f; tsPageShift = 0.5f; // TODO, unbind this keystroke from the dial tsWfScroll = 0.0001f; // modified by tuning step selector tsKnobMHz = 0.0001f; // modified by tuning step selector // Units are in Hz: tsPlusControlHz = 10000; tsPlusHz = 1000; tsPlusShiftHz = 100; tsPageHz = 1000000; tsPageShiftHz = 500000; // TODO, unbind this keystroke from the dial tsWfScrollHz = 100; // modified by tuning step selector tsKnobHz = 100; // modified by tuning step selector } void wfmain::on_tuningStepCombo_currentIndexChanged(int index) { tsWfScroll = (float)ui->tuningStepCombo->itemData(index).toUInt() / 1000000.0; tsKnobMHz = (float)ui->tuningStepCombo->itemData(index).toUInt() / 1000000.0; tsWfScrollHz = ui->tuningStepCombo->itemData(index).toUInt(); tsKnobHz = ui->tuningStepCombo->itemData(index).toUInt(); } quint64 wfmain::roundFrequency(quint64 frequency, unsigned int tsHz) { quint64 rounded = 0; if(ui->tuningFloorZerosChk->isChecked()) { rounded = ((frequency % tsHz) > tsHz/2) ? frequency + tsHz - frequency%tsHz : frequency - frequency%tsHz; return rounded; } else { return frequency; } } quint64 wfmain::roundFrequencyWithStep(quint64 frequency, int steps, unsigned int tsHz) { quint64 rounded = 0; if(steps > 0) { frequency = frequency + (quint64)(steps*tsHz); } else { frequency = frequency - std::min((quint64)(abs(steps)*tsHz), frequency); } if(ui->tuningFloorZerosChk->isChecked()) { rounded = ((frequency % tsHz) > tsHz/2) ? frequency + tsHz - frequency%tsHz : frequency - frequency%tsHz; return rounded; } else { return frequency; } } void wfmain::shortcutMinus() { if(freqLock) return; freqt f; f.Hz = roundFrequencyWithStep(freq.Hz, -1, tsPlusHz); f.MHzDouble = f.Hz / (double)1E6; setUIFreq(); //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); } void wfmain::shortcutPlus() { if(freqLock) return; freqt f; f.Hz = roundFrequencyWithStep(freq.Hz, 1, tsPlusHz); f.MHzDouble = f.Hz / (double)1E6; setUIFreq(); //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); } void wfmain::shortcutShiftMinus() { if(freqLock) return; freqt f; f.Hz = roundFrequencyWithStep(freq.Hz, -1, tsPlusShiftHz); f.MHzDouble = f.Hz / (double)1E6; setUIFreq(); //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); } void wfmain::shortcutShiftPlus() { if(freqLock) return; freqt f; f.Hz = roundFrequencyWithStep(freq.Hz, 1, tsPlusShiftHz); f.MHzDouble = f.Hz / (double)1E6; setUIFreq(); //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); } void wfmain::shortcutControlMinus() { if(freqLock) return; freqt f; f.Hz = roundFrequencyWithStep(freq.Hz, -1, tsPlusControlHz); f.MHzDouble = f.Hz / (double)1E6; setUIFreq(); //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); } void wfmain::shortcutControlPlus() { if(freqLock) return; freqt f; f.Hz = roundFrequencyWithStep(freq.Hz, 1, tsPlusControlHz); f.MHzDouble = f.Hz / (double)1E6; setUIFreq(); //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); } void wfmain::shortcutPageUp() { if(freqLock) return; freqt f; f.Hz = freq.Hz + tsPageHz; f.MHzDouble = f.Hz / (double)1E6; setUIFreq(); //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); } void wfmain::shortcutPageDown() { if(freqLock) return; freqt f; f.Hz = freq.Hz - tsPageHz; f.MHzDouble = f.Hz / (double)1E6; setUIFreq(); //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); } void wfmain::shortcutF() { showStatusBarText("Sending speech command (frequency) to radio."); emit sayFrequency(); } void wfmain::shortcutM() { showStatusBarText("Sending speech command (mode) to radio."); emit sayMode(); } void wfmain::setUIFreq(double frequency) { ui->freqLabel->setText(QString("%1").arg(frequency, 0, 'f')); } void wfmain::setUIFreq() { // Call this function, without arguments, if you know that the // freqMhz variable is already set correctly. setUIFreq(freq.MHzDouble); } void wfmain:: getInitialRigState() { // Initial list of queries to the radio. // These are made when the program starts up // and are used to adjust the UI to match the radio settings // the polling interval is set at 200ms. Faster is possible but slower // computers will glitch occassionally. issueDelayedCommand(cmdGetFreq); issueDelayedCommand(cmdGetMode); issueDelayedCommand(cmdNone); issueDelayedCommand(cmdGetFreq); issueDelayedCommand(cmdGetMode); // From left to right in the UI: if (rigCaps.hasTransmit) { issueDelayedCommand(cmdGetDataMode); issueDelayedCommand(cmdGetModInput); issueDelayedCommand(cmdGetModDataInput); } issueDelayedCommand(cmdGetRxGain); issueDelayedCommand(cmdGetAfGain); issueDelayedCommand(cmdGetSql); if (rigCaps.hasTransmit) { issueDelayedCommand(cmdGetTxPower); issueDelayedCommand(cmdGetCurrentModLevel); // level for currently selected mod sources } issueDelayedCommand(cmdGetSpectrumRefLevel); issueDelayedCommand(cmdGetDuplexMode); if(rigCaps.hasSpectrum) { issueDelayedCommand(cmdDispEnable); issueDelayedCommand(cmdSpecOn); } if (rigCaps.hasTransmit) { issueDelayedCommand(cmdGetModInput); issueDelayedCommand(cmdGetModDataInput); } if(rigCaps.hasCTCSS) { issueDelayedCommand(cmdGetTone); issueDelayedCommand(cmdGetTSQL); } if(rigCaps.hasDTCS) { issueDelayedCommand(cmdGetDTCS); } issueDelayedCommand(cmdGetRptAccessMode); if(rigCaps.hasAntennaSel) { issueDelayedCommand(cmdGetAntenna); } if(rigCaps.hasAttenuator) { issueDelayedCommand(cmdGetAttenuator); } if(rigCaps.hasPreamp) { issueDelayedCommand(cmdGetPreamp); } issueDelayedCommand(cmdGetRitEnabled); issueDelayedCommand(cmdGetRitValue); if(rigCaps.hasIFShift) issueDelayedCommand(cmdGetIFShift); if(rigCaps.hasTBPF) { issueDelayedCommand(cmdGetTPBFInner); issueDelayedCommand(cmdGetTPBFOuter); } if(rigCaps.hasSpectrum) { issueDelayedCommand(cmdGetSpectrumMode); issueDelayedCommand(cmdGetSpectrumSpan); } issueDelayedCommand(cmdNone); issueDelayedCommand(cmdStartRegularPolling); if(rigCaps.hasATU) { issueDelayedCommand(cmdGetATUStatus); } delayedCommand->start(); } void wfmain::showStatusBarText(QString text) { ui->statusBar->showMessage(text, 5000); } void wfmain::on_useDarkThemeChk_clicked(bool checked) { //setAppTheme(checked); setPlotTheme(wf, checked); setPlotTheme(plot, checked); prefs.useDarkMode = checked; } void wfmain::on_useSystemThemeChk_clicked(bool checked) { setAppTheme(!checked); prefs.useSystemTheme = checked; } void wfmain::setAppTheme(bool isCustom) { if(isCustom) { #ifndef Q_OS_LINUX QFile f(":"+prefs.stylesheetPath); // built-in resource #else QFile f(PREFIX "/share/wfview/" + prefs.stylesheetPath); #endif if (!f.exists()) { printf("Unable to set stylesheet, file not found\n"); printf("Tried to load: [%s]\n", f.fileName().toStdString().c_str() ); } else { f.open(QFile::ReadOnly | QFile::Text); QTextStream ts(&f); qApp->setStyleSheet(ts.readAll()); } } else { qApp->setStyleSheet(""); } } void wfmain::setDefaultColors() { defaultColors.Dark_PlotBackground = QColor(0,0,0,255); defaultColors.Dark_PlotAxisPen = QColor(75,75,75,255); defaultColors.Dark_PlotLegendTextColor = QColor(255,255,255,255); defaultColors.Dark_PlotLegendBorderPen = QColor(255,255,255,255); defaultColors.Dark_PlotLegendBrush = QColor(0,0,0,200); defaultColors.Dark_PlotTickLabel = QColor(Qt::white); defaultColors.Dark_PlotBasePen = QColor(Qt::white); defaultColors.Dark_PlotTickPen = QColor(Qt::white); defaultColors.Dark_PeakPlotLine = QColor(Qt::yellow); defaultColors.Dark_TuningLine = QColor(Qt::cyan); defaultColors.Light_PlotBackground = QColor(255,255,255,255); defaultColors.Light_PlotAxisPen = QColor(200,200,200,255); defaultColors.Light_PlotLegendTextColor = QColor(0,0,0,255); defaultColors.Light_PlotLegendBorderPen = QColor(0,0,0,255); defaultColors.Light_PlotLegendBrush = QColor(255,255,255,200); defaultColors.Light_PlotTickLabel = QColor(Qt::black); defaultColors.Light_PlotBasePen = QColor(Qt::black); defaultColors.Light_PlotTickPen = QColor(Qt::black); defaultColors.Light_PeakPlotLine = QColor(Qt::blue); defaultColors.Light_TuningLine = QColor(Qt::blue); } void wfmain::setPlotTheme(QCustomPlot *plot, bool isDark) { if(isDark) { plot->setBackground(prefs.colorScheme.Dark_PlotBackground); //plot->setBackground(QColor(0,0,0,255)); plot->xAxis->grid()->setPen(prefs.colorScheme.Dark_PlotAxisPen); plot->yAxis->grid()->setPen(prefs.colorScheme.Dark_PlotAxisPen); plot->legend->setTextColor(prefs.colorScheme.Dark_PlotLegendTextColor); plot->legend->setBorderPen(prefs.colorScheme.Dark_PlotLegendBorderPen); plot->legend->setBrush(prefs.colorScheme.Dark_PlotLegendBrush); plot->xAxis->setTickLabelColor(prefs.colorScheme.Dark_PlotTickLabel); plot->xAxis->setLabelColor(prefs.colorScheme.Dark_PlotTickLabel); plot->yAxis->setTickLabelColor(prefs.colorScheme.Dark_PlotTickLabel); plot->yAxis->setLabelColor(prefs.colorScheme.Dark_PlotTickLabel); plot->xAxis->setBasePen(prefs.colorScheme.Dark_PlotBasePen); plot->xAxis->setTickPen(prefs.colorScheme.Dark_PlotTickPen); plot->yAxis->setBasePen(prefs.colorScheme.Dark_PlotBasePen); plot->yAxis->setTickPen(prefs.colorScheme.Dark_PlotTickPen); plot->graph(0)->setPen(prefs.colorScheme.Dark_PeakPlotLine); freqIndicatorLine->setPen(prefs.colorScheme.Dark_TuningLine); } else { //color = ui->groupBox->palette().color(QPalette::Button); plot->setBackground(prefs.colorScheme.Light_PlotBackground); plot->xAxis->grid()->setPen(prefs.colorScheme.Light_PlotAxisPen); plot->yAxis->grid()->setPen(prefs.colorScheme.Light_PlotAxisPen); plot->legend->setTextColor(prefs.colorScheme.Light_PlotLegendTextColor); plot->legend->setBorderPen(prefs.colorScheme.Light_PlotLegendBorderPen); plot->legend->setBrush(prefs.colorScheme.Light_PlotLegendBrush); plot->xAxis->setTickLabelColor(prefs.colorScheme.Light_PlotTickLabel); plot->xAxis->setLabelColor(prefs.colorScheme.Light_PlotTickLabel); plot->yAxis->setTickLabelColor(prefs.colorScheme.Light_PlotTickLabel); plot->yAxis->setLabelColor(prefs.colorScheme.Light_PlotTickLabel); plot->xAxis->setBasePen(prefs.colorScheme.Light_PlotBasePen); plot->xAxis->setTickPen(prefs.colorScheme.Light_PlotTickPen); plot->yAxis->setBasePen(prefs.colorScheme.Light_PlotBasePen); plot->yAxis->setTickPen(prefs.colorScheme.Light_PlotTickLabel); plot->graph(0)->setPen(prefs.colorScheme.Light_PeakPlotLine); freqIndicatorLine->setPen(prefs.colorScheme.Light_TuningLine); } } void wfmain::doCmd(commandtype cmddata) { cmds cmd = cmddata.cmd; std::shared_ptr data = cmddata.data; // This switch is for commands with parameters. // the "default" for non-parameter commands is to call doCmd(cmd). switch (cmd) { case cmdSetFreq: { lastFreqCmdTime_ms = QDateTime::currentMSecsSinceEpoch(); freqt f = (*std::static_pointer_cast(data)); emit setFrequency(0,f); break; } case cmdSetMode: { mode_info m = (*std::static_pointer_cast(data)); emit setMode(m); break; } case cmdSetTxPower: { unsigned char txpower = (*std::static_pointer_cast(data)); emit setTxPower(txpower); break; } case cmdSetMicGain: { unsigned char micgain = (*std::static_pointer_cast(data)); emit setTxPower(micgain); break; } case cmdSetRxRfGain: { unsigned char rfgain = (*std::static_pointer_cast(data)); emit setRfGain(rfgain); break; } case cmdSetModLevel: { unsigned char modlevel = (*std::static_pointer_cast(data)); rigInput currentIn; if(usingDataMode) { currentIn = currentModDataSrc; } else { currentIn = currentModSrc; } emit setModLevel(currentIn, modlevel); break; } case cmdSetAfGain: { unsigned char afgain = (*std::static_pointer_cast(data)); emit setAfGain(afgain); break; } case cmdSetSql: { unsigned char sqlLevel = (*std::static_pointer_cast(data)); emit setSql(sqlLevel); break; } case cmdSetIFShift: { unsigned char IFShiftLevel = (*std::static_pointer_cast(data)); emit setIFShift(IFShiftLevel); break; } case cmdSetTPBFInner: { unsigned char innterLevel = (*std::static_pointer_cast(data)); emit setTPBFInner(innterLevel); break; } case cmdSetTPBFOuter: { unsigned char outerLevel = (*std::static_pointer_cast(data)); emit setTPBFOuter(outerLevel); break; } case cmdSetPTT: { bool pttrequest = (*std::static_pointer_cast(data)); emit setPTT(pttrequest); ui->meter2Widget->clearMeterOnPTTtoggle(); if(pttrequest) { ui->meterSPoWidget->setMeterType(meterPower); } else { ui->meterSPoWidget->setMeterType(meterS); } break; } case cmdSetATU: { bool atuOn = (*std::static_pointer_cast(data)); emit setATU(atuOn); break; } case cmdSetUTCOffset: { timekind u = (*std::static_pointer_cast(data)); emit setUTCOffset(u); break; } case cmdSetTime: { timekind t = (*std::static_pointer_cast(data)); emit setTime(t); break; } case cmdSetDate: { datekind d = (*std::static_pointer_cast(data)); emit setDate(d); break; } default: doCmd(cmd); break; } } void wfmain::doCmd(cmds cmd) { // Use this function to take action upon a command. switch(cmd) { case cmdNone: //qInfo(logSystem()) << "NOOP"; break; case cmdGetRigID: emit getRigID(); break; case cmdGetRigCIV: // if(!know rig civ already) if(!haveRigCaps) { emit getRigCIV(); issueDelayedCommand(cmdGetRigCIV); // This way, we stay here until we get an answer. } break; case cmdGetFreq: emit getFrequency(); break; case cmdGetMode: emit getMode(); break; case cmdGetDataMode: if(rigCaps.hasDataModes) emit getDataMode(); break; case cmdSetModeFilter: emit setMode(setModeVal, setFilterVal); break; case cmdSetDataModeOff: emit setDataMode(false, (unsigned char)ui->modeFilterCombo->currentData().toInt()); break; case cmdSetDataModeOn: emit setDataMode(true, (unsigned char)ui->modeFilterCombo->currentData().toInt()); break; case cmdGetRitEnabled: emit getRitEnabled(); break; case cmdGetRitValue: emit getRitValue(); break; case cmdGetModInput: emit getModInput(false); break; case cmdGetModDataInput: emit getModInput(true); break; case cmdGetCurrentModLevel: // TODO: Add delay between these queries emit getModInputLevel(currentModSrc); emit getModInputLevel(currentModDataSrc); break; case cmdGetDuplexMode: emit getDuplexMode(); break; case cmdGetTone: emit getTone(); break; case cmdGetTSQL: emit getTSQL(); break; case cmdGetDTCS: emit getDTCS(); break; case cmdGetRptAccessMode: emit getRptAccessMode(); break; case cmdDispEnable: emit scopeDisplayEnable(); break; case cmdDispDisable: emit scopeDisplayDisable(); break; case cmdGetSpectrumMode: emit getScopeMode(); break; case cmdGetSpectrumSpan: emit getScopeSpan(); break; case cmdSpecOn: emit spectOutputEnable(); break; case cmdSpecOff: emit spectOutputDisable(); break; case cmdGetRxGain: emit getRfGain(); break; case cmdGetAfGain: emit getAfGain(); break; case cmdGetSql: emit getSql(); break; case cmdGetIFShift: emit getIfShift(); break; case cmdGetTPBFInner: emit getTPBFInner(); break; case cmdGetTPBFOuter: emit getTPBFOuter(); break; case cmdGetTxPower: emit getTxPower(); break; case cmdGetMicGain: emit getMicGain(); break; case cmdGetSpectrumRefLevel: emit getSpectrumRefLevel(); break; case cmdGetATUStatus: if(rigCaps.hasATU) emit getATUStatus(); break; case cmdStartATU: if(rigCaps.hasATU) emit startATU(); break; case cmdGetAttenuator: emit getAttenuator(); break; case cmdGetPreamp: emit getPreamp(); break; case cmdGetAntenna: emit getAntenna(); break; case cmdScopeCenterMode: emit setScopeMode(spectModeCenter); break; case cmdScopeFixedMode: emit setScopeMode(spectModeFixed); break; case cmdGetPTT: emit getPTT(); break; case cmdGetTxRxMeter: if(amTransmitting) emit getMeters(meterPower); else emit getMeters(meterS); break; case cmdGetSMeter: if(!amTransmitting) emit getMeters(meterS); break; case cmdGetCenterMeter: if(!amTransmitting) emit getMeters(meterCenter); break; case cmdGetPowerMeter: if(amTransmitting) emit getMeters(meterPower); break; case cmdGetSWRMeter: if(amTransmitting) emit getMeters(meterSWR); break; case cmdGetIdMeter: emit getMeters(meterCurrent); break; case cmdGetVdMeter: emit getMeters(meterVoltage); break; case cmdGetALCMeter: if(amTransmitting) emit getMeters(meterALC); break; case cmdGetCompMeter: if(amTransmitting) emit getMeters(meterComp); break; case cmdStartRegularPolling: runPeriodicCommands = true; break; case cmdStopRegularPolling: runPeriodicCommands = false; break; case cmdQueNormalSpeed: if(usingLAN) { delayedCommand->setInterval(delayedCmdIntervalLAN_ms); } else { delayedCommand->setInterval(delayedCmdIntervalSerial_ms); } break; default: qInfo(logSystem()) << __PRETTY_FUNCTION__ << "WARNING: Command fell through of type: " << (unsigned int)cmd; break; } } void wfmain::sendRadioCommandLoop() { // Called by the periodicPollingTimer, see setInitialTiming() if(!(loopTickCounter % 2)) { // if ther's a command waiting, run it. if(!delayedCmdQue.empty()) { commandtype cmddata = delayedCmdQue.front(); delayedCmdQue.pop_front(); doCmd(cmddata); } else if(!(loopTickCounter % 10)) { // pick from useful queries to make now and then if(haveRigCaps && !slowPollCmdQueue.empty()) { int nCmds = slowPollCmdQueue.size(); cmds sCmd = slowPollCmdQueue[(slowCmdNum++)%nCmds]; doCmd(sCmd); } } } else { // odd-number ticks: // s-meter or other metering if(haveRigCaps && !periodicCmdQueue.empty()) { int nCmds = periodicCmdQueue.size(); cmds pcmd = periodicCmdQueue[ (pCmdNum++)%nCmds ]; doCmd(pcmd); } } loopTickCounter++; } void wfmain::issueDelayedCommand(cmds cmd) { // Append to end of command queue commandtype cmddata; cmddata.cmd = cmd; cmddata.data = NULL; delayedCmdQue.push_back(cmddata); } void wfmain::issueDelayedCommandPriority(cmds cmd) { // Places the new command at the top of the queue // Use only when needed. commandtype cmddata; cmddata.cmd = cmd; cmddata.data = NULL; delayedCmdQue.push_front(cmddata); } void wfmain::issueDelayedCommandUnique(cmds cmd) { // Use this function to insert commands where // multiple (redundant) commands don't make sense. commandtype cmddata; cmddata.cmd = cmd; cmddata.data = NULL; // The following is both expensive and not that great, // since it does not check if the arguments are the same. bool found = false; for(unsigned int i=0; i < delayedCmdQue.size(); i++) { if(delayedCmdQue.at(i).cmd == cmd) { found = true; break; } } if(!found) { delayedCmdQue.push_front(cmddata); } // if( std::find(delayedCmdQue.begin(), delayedCmdQue.end(), cmddata ) == delayedCmdQue.end()) // { // delayedCmdQue.push_front(cmddata); // } } void wfmain::issueCmd(cmds cmd, mode_info m) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new mode_info(m)); delayedCmdQue.push_back(cmddata); } void wfmain::issueCmd(cmds cmd, freqt f) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new freqt(f)); delayedCmdQue.push_back(cmddata); } void wfmain::issueCmd(cmds cmd, timekind t) { qDebug(logSystem()) << "Issuing timekind command with data: " << t.hours << " hours, " << t.minutes << " minutes, " << t.isMinus << " isMinus"; commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new timekind(t)); delayedCmdQue.push_front(cmddata); } void wfmain::issueCmd(cmds cmd, datekind d) { qDebug(logSystem()) << "Issuing datekind command with data: " << d.day << " day, " << d.month << " month, " << d.year << " year."; commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new datekind(d)); delayedCmdQue.push_front(cmddata); } void wfmain::issueCmd(cmds cmd, int i) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new int(i)); delayedCmdQue.push_back(cmddata); } void wfmain::issueCmd(cmds cmd, char c) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new char(c)); delayedCmdQue.push_back(cmddata); } void wfmain::issueCmd(cmds cmd, bool b) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new bool(b)); delayedCmdQue.push_back(cmddata); } void wfmain::issueCmd(cmds cmd, unsigned char c) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new unsigned char(c)); delayedCmdQue.push_back(cmddata); } void wfmain::issueCmdUniquePriority(cmds cmd, bool b) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new bool(b)); delayedCmdQue.push_front(cmddata); removeSimilarCommand(cmd); } void wfmain::issueCmdUniquePriority(cmds cmd, unsigned char c) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new unsigned char(c)); delayedCmdQue.push_front(cmddata); removeSimilarCommand(cmd); } void wfmain::issueCmdUniquePriority(cmds cmd, char c) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new char(c)); delayedCmdQue.push_front(cmddata); removeSimilarCommand(cmd); } void wfmain::issueCmdUniquePriority(cmds cmd, freqt f) { commandtype cmddata; cmddata.cmd = cmd; cmddata.data = std::shared_ptr(new freqt(f)); delayedCmdQue.push_front(cmddata); removeSimilarCommand(cmd); } void wfmain::removeSimilarCommand(cmds cmd) { // pop anything out that is of the same kind of command: // pop anything out that is of the same kind of command: // Start at 1 since we put one in at zero that we want to keep. for(unsigned int i=1; i < delayedCmdQue.size(); i++) { if(delayedCmdQue.at(i).cmd == cmd) { //delayedCmdQue[i].cmd = cmdNone; delayedCmdQue.erase(delayedCmdQue.begin()+i); // i -= 1; } } } void wfmain::receiveRigID(rigCapabilities rigCaps) { // Note: We intentionally request rigID several times // because without rigID, we can't do anything with the waterfall. if(haveRigCaps) { return; } else { showStatusBarText(QString("Found radio at address 0x%1 of name %2 and model ID %3.").arg(rigCaps.civ,2,16).arg(rigCaps.modelName).arg(rigCaps.modelID)); qDebug(logSystem()) << "Rig name: " << rigCaps.modelName; qDebug(logSystem()) << "Has LAN capabilities: " << rigCaps.hasLan; qDebug(logSystem()) << "Rig ID received into wfmain: spectLenMax: " << rigCaps.spectLenMax; qDebug(logSystem()) << "Rig ID received into wfmain: spectAmpMax: " << rigCaps.spectAmpMax; qDebug(logSystem()) << "Rig ID received into wfmain: spectSeqMax: " << rigCaps.spectSeqMax; qDebug(logSystem()) << "Rig ID received into wfmain: hasSpectrum: " << rigCaps.hasSpectrum; this->rigCaps = rigCaps; rigName->setText(rigCaps.modelName); setWindowTitle(rigCaps.modelName); this->spectWidth = rigCaps.spectLenMax; // used once haveRigCaps is true. haveRigCaps = true; // Added so that server receives rig capabilities. emit sendRigCaps(rigCaps); rpt->setRig(rigCaps); trxadj->setRig(rigCaps); // Set the mode combo box up: ui->modeSelectCombo->blockSignals(true); ui->modeSelectCombo->clear(); for(unsigned int i=0; i < rigCaps.modes.size(); i++) { ui->modeSelectCombo->addItem(rigCaps.modes.at(i).name, rigCaps.modes.at(i).reg); } ui->modeSelectCombo->blockSignals(false); if(rigCaps.model == model9700) { ui->satOpsBtn->setDisabled(false); ui->adjRefBtn->setDisabled(false); } else { ui->satOpsBtn->setDisabled(true); ui->adjRefBtn->setDisabled(true); } QString inName; // Clear input combos before adding known inputs. ui->modInputCombo->clear(); ui->modInputDataCombo->clear(); for(int i=0; i < rigCaps.inputs.length(); i++) { switch(rigCaps.inputs.at(i)) { case inputMic: inName = "Mic"; break; case inputLAN: inName = "LAN"; break; case inputUSB: inName = "USB"; break; case inputACC: inName = "ACC"; break; case inputACCA: inName = "ACCA"; break; case inputACCB: inName = "ACCB"; break; default: inName = "Unknown"; break; } ui->modInputCombo->addItem(inName, rigCaps.inputs.at(i)); ui->modInputDataCombo->addItem(inName, rigCaps.inputs.at(i)); } if(rigCaps.inputs.length() == 0) { ui->modInputCombo->addItem("None", inputNone); ui->modInputDataCombo->addItem("None", inputNone); } ui->attSelCombo->clear(); if(rigCaps.hasAttenuator) { ui->attSelCombo->setDisabled(false); for(unsigned int i=0; i < rigCaps.attenuators.size(); i++) { inName = (i==0)?QString("0dB"):QString("-%1 dB").arg(rigCaps.attenuators.at(i), 0, 16); ui->attSelCombo->addItem(inName, rigCaps.attenuators.at(i)); } } else { ui->attSelCombo->setDisabled(true); } ui->preampSelCombo->clear(); if(rigCaps.hasPreamp) { ui->preampSelCombo->setDisabled(false); for(unsigned int i=0; i < rigCaps.preamps.size(); i++) { inName = (i==0)?QString("Disabled"):QString("Pre #%1").arg(rigCaps.preamps.at(i), 0, 16); ui->preampSelCombo->addItem(inName, rigCaps.preamps.at(i)); } } else { ui->preampSelCombo->setDisabled(true); } ui->antennaSelCombo->clear(); if(rigCaps.hasAntennaSel) { ui->antennaSelCombo->setDisabled(false); for(unsigned int i=0; i < rigCaps.antennas.size(); i++) { inName = QString("%1").arg(rigCaps.antennas.at(i)+1, 0, 16); // adding 1 to have the combobox start with ant 1 insted of 0 ui->antennaSelCombo->addItem(inName, rigCaps.antennas.at(i)); } } else { ui->antennaSelCombo->setDisabled(true); } ui->rxAntennaCheck->setEnabled(rigCaps.hasRXAntenna); ui->rxAntennaCheck->setChecked(false); ui->scopeBWCombo->blockSignals(true); ui->scopeBWCombo->clear(); if(rigCaps.hasSpectrum) { ui->scopeBWCombo->setHidden(false); for(unsigned int i=0; i < rigCaps.scopeCenterSpans.size(); i++) { ui->scopeBWCombo->addItem(rigCaps.scopeCenterSpans.at(i).name, (int)rigCaps.scopeCenterSpans.at(i).cstype); } } else { ui->scopeBWCombo->setHidden(true); } ui->scopeBWCombo->blockSignals(false); setBandButtons(); ui->tuneEnableChk->setEnabled(rigCaps.hasATU); ui->tuneNowBtn->setEnabled(rigCaps.hasATU); ui->connectBtn->setText("Disconnect"); // We must be connected now. prepareWf(ui->wfLengthSlider->value()); if(usingLAN) { ui->afGainSlider->setValue(prefs.localAFgain); } // Adding these here because clearly at this point we have valid // rig comms. In the future, we should establish comms and then // do all the initial grabs. For now, this hack of adding them here and there: issueDelayedCommand(cmdGetFreq); issueDelayedCommand(cmdGetMode); // recalculate command timing now that we know the rig better: calculateTimingParameters(); initPeriodicCommands(); // Set the second meter here as I suspect we need to be connected for it to work? for (int i = 0; i < ui->meter2selectionCombo->count(); i++) { if (static_cast(ui->meter2selectionCombo->itemData(i).toInt()) == prefs.meter2Type) { // I thought that setCurrentIndex() would call the activated() function for the combobox // but it doesn't, so call it manually. ui->meter2selectionCombo->setCurrentIndex(i); on_meter2selectionCombo_activated(i); } } } } void wfmain::initPeriodicCommands() { // This function places periodic polling commands into a queue. // The commands are run using a timer, // and the timer is started by the delayed command cmdStartPeriodicTimer. insertPeriodicCommand(cmdGetTxRxMeter, 128); insertSlowPeriodicCommand(cmdGetFreq, 128); insertSlowPeriodicCommand(cmdGetMode, 128); if(rigCaps.hasTransmit) insertSlowPeriodicCommand(cmdGetPTT, 128); insertSlowPeriodicCommand(cmdGetTxPower, 128); insertSlowPeriodicCommand(cmdGetRxGain, 128); if(rigCaps.hasAttenuator) insertSlowPeriodicCommand(cmdGetAttenuator, 128); if(rigCaps.hasTransmit) insertSlowPeriodicCommand(cmdGetPTT, 128); if(rigCaps.hasPreamp) insertSlowPeriodicCommand(cmdGetPreamp, 128); if (rigCaps.hasRXAntenna) { insertSlowPeriodicCommand(cmdGetAntenna, 128); } insertSlowPeriodicCommand(cmdGetDuplexMode, 128); } void wfmain::insertPeriodicCommand(cmds cmd, unsigned char priority) { // TODO: meaningful priority // These commands get run at the fastest pace possible // Typically just metering. if(priority < 10) { periodicCmdQueue.push_front(cmd); } else { periodicCmdQueue.push_back(cmd); } } void wfmain::insertPeriodicCommandUnique(cmds cmd) { // Use this function to insert a non-duplicate command // into the fast periodic polling queue, typically // meter commands where high refresh rates are desirable. removePeriodicCommand(cmd); periodicCmdQueue.push_front(cmd); } void wfmain::removePeriodicCommand(cmds cmd) { while(true) { auto it = std::find(this->periodicCmdQueue.begin(), this->periodicCmdQueue.end(), cmd); if(it != periodicCmdQueue.end()) { periodicCmdQueue.erase(it); } else { break; } } } void wfmain::insertSlowPeriodicCommand(cmds cmd, unsigned char priority) { // TODO: meaningful priority // These commands are run every 20 "ticks" of the primary radio command loop // Basically 20 times less often than the standard peridic command if(priority < 10) { slowPollCmdQueue.push_front(cmd); } else { slowPollCmdQueue.push_back(cmd); } } void wfmain::receiveFreq(freqt freqStruct) { qint64 tnow_ms = QDateTime::currentMSecsSinceEpoch(); if(tnow_ms - lastFreqCmdTime_ms > delayedCommand->interval() * 2) { ui->freqLabel->setText(QString("%1").arg(freqStruct.MHzDouble, 0, 'f')); freq = freqStruct; } else { qDebug(logSystem()) << "Rejecting stale frequency: " << freqStruct.Hz << " Hz, delta time ms = " << tnow_ms - lastFreqCmdTime_ms\ << ", tnow_ms " << tnow_ms << ", last: " << lastFreqCmdTime_ms; } } void wfmain::receivePTTstatus(bool pttOn) { // This is the only place where amTransmitting and the transmit button text should be changed: //qInfo(logSystem()) << "PTT status: " << pttOn; if (pttOn && !amTransmitting) { pttLed->setState(QLedLabel::State::StateError); } else if (!pttOn && amTransmitting) { pttLed->setState(QLedLabel::State::StateOk); } amTransmitting = pttOn; changeTxBtn(); } void wfmain::changeTxBtn() { if(amTransmitting) { ui->transmitBtn->setText("Receive"); } else { ui->transmitBtn->setText("Transmit"); } } void wfmain::receiveSpectrumData(QByteArray spectrum, double startFreq, double endFreq) { if(!haveRigCaps) { qDebug(logSystem()) << "Spectrum received, but RigID incomplete."; return; } if((startFreq != oldLowerFreq) || (endFreq != oldUpperFreq)) { // If the frequency changed and we were drawing peaks, now is the time to clearn them if(drawPeaks) { // TODO: create non-button function to do this // This will break if the button is ever moved or renamed. on_clearPeakBtn_clicked(); } } oldLowerFreq = startFreq; oldUpperFreq = endFreq; //qInfo(logSystem()) << "start: " << startFreq << " end: " << endFreq; quint16 specLen = spectrum.length(); //qInfo(logSystem()) << "Spectrum data received at UI! Length: " << specLen; //if( (specLen != 475) || (specLen!=689) ) if( specLen != rigCaps.spectLenMax ) { qDebug(logSystem()) << "-------------------------------------------"; qDebug(logSystem()) << "------ Unusual spectrum received, length: " << specLen; qDebug(logSystem()) << "------ Expected spectrum length: " << rigCaps.spectLenMax; qDebug(logSystem()) << "------ This should happen once at most. "; return; // safe. Using these unusual length things is a problem. } QVector x(spectWidth), y(spectWidth), y2(spectWidth); for(int i=0; i < spectWidth; i++) { x[i] = (i * (endFreq-startFreq)/spectWidth) + startFreq; } for(int i=0; i (unsigned char)spectrumPeaks.at(i)) { spectrumPeaks[i] = spectrum.at(i); } y2[i] = (unsigned char)spectrumPeaks.at(i); } } if(!spectrumDrawLock) { //ui->qcp->addGraph(); plot->graph(0)->setData(x,y); if((freq.MHzDouble < endFreq) && (freq.MHzDouble > startFreq)) { freqIndicatorLine->start->setCoords(freq.MHzDouble,0); freqIndicatorLine->end->setCoords(freq.MHzDouble,160); } if(drawPeaks) { plot->graph(1)->setData(x,y2); // peaks } plot->yAxis->setRange(0, rigCaps.spectAmpMax+1); plot->xAxis->setRange(startFreq, endFreq); plot->replot(); if(specLen == spectWidth) { wfimage.prepend(spectrum); wfimage.resize(wfLengthMax); wfimage.squeeze(); // Waterfall: for(int row = 0; row < wfLength; row++) { for(int col = 0; col < spectWidth; col++) { colorMap->data()->setCell( col, row, wfimage.at(row).at(col)); } } wf->yAxis->setRange(0,wfLength - 1); wf->xAxis->setRange(0, spectWidth-1); wf->replot(); } } } void wfmain::receiveSpectrumMode(spectrumMode spectMode) { for (int i = 0; i < ui->spectrumModeCombo->count(); i++) { if (static_cast(ui->spectrumModeCombo->itemData(i).toInt()) == spectMode) { ui->spectrumModeCombo->blockSignals(true); ui->spectrumModeCombo->setCurrentIndex(i); ui->spectrumModeCombo->blockSignals(false); } } } void wfmain::handlePlotDoubleClick(QMouseEvent *me) { double x; freqt freqGo; //double y; //double px; if(!freqLock) { //y = plot->yAxis->pixelToCoord(me->pos().y()); x = plot->xAxis->pixelToCoord(me->pos().x()); freqGo.Hz = x*1E6; freqGo.Hz = roundFrequency(freqGo.Hz, tsWfScrollHz); freqGo.MHzDouble = (float)freqGo.Hz / 1E6; //emit setFrequency(0,freq); issueCmd(cmdSetFreq, freqGo); freq = freqGo; setUIFreq(); //issueDelayedCommand(cmdGetFreq); showStatusBarText(QString("Going to %1 MHz").arg(x)); } } void wfmain::handleWFDoubleClick(QMouseEvent *me) { double x; freqt freqGo; //double y; //x = wf->xAxis->pixelToCoord(me->pos().x()); //y = wf->yAxis->pixelToCoord(me->pos().y()); // cheap trick until I figure out how the axis works on the WF: if(!freqLock) { x = plot->xAxis->pixelToCoord(me->pos().x()); freqGo.Hz = x*1E6; freqGo.Hz = roundFrequency(freqGo.Hz, tsWfScrollHz); freqGo.MHzDouble = (float)freqGo.Hz / 1E6; //emit setFrequency(0,freq); issueCmd(cmdSetFreq, freqGo); freq = freqGo; setUIFreq(); showStatusBarText(QString("Going to %1 MHz").arg(x)); } } void wfmain::handlePlotClick(QMouseEvent *me) { double x = plot->xAxis->pixelToCoord(me->pos().x()); showStatusBarText(QString("Selected %1 MHz").arg(x)); } void wfmain::handleWFClick(QMouseEvent *me) { double x = plot->xAxis->pixelToCoord(me->pos().x()); showStatusBarText(QString("Selected %1 MHz").arg(x)); } void wfmain::handleWFScroll(QWheelEvent *we) { // The wheel event is typically // .y() and is +/- 120. // We will click the dial once for every 120 received. //QPoint delta = we->angleDelta(); if(freqLock) return; freqt f; f.Hz = 0; f.MHzDouble = 0; int clicks = we->angleDelta().y() / 120; if(!clicks) return; unsigned int stepsHz = tsWfScrollHz; Qt::KeyboardModifiers key= we->modifiers(); if ((key == Qt::ShiftModifier) && (stepsHz !=1)) { stepsHz /= 10; } else if (key == Qt::ControlModifier) { stepsHz *= 10; } f.Hz = roundFrequencyWithStep(freq.Hz, clicks, stepsHz); f.MHzDouble = f.Hz / (double)1E6; freq = f; //emit setFrequency(0,f); issueCmdUniquePriority(cmdSetFreq, f); ui->freqLabel->setText(QString("%1").arg(f.MHzDouble, 0, 'f')); //issueDelayedCommandUnique(cmdGetFreq); } void wfmain::handlePlotScroll(QWheelEvent *we) { handleWFScroll(we); } void wfmain::on_scopeEnableWFBtn_clicked(bool checked) { if(checked) { emit spectOutputEnable(); } else { emit spectOutputDisable(); } } void wfmain::receiveMode(unsigned char mode, unsigned char filter) { //qInfo(logSystem()) << __func__ << "Received mode " << mode << " current mode: " << currentModeIndex; bool found=false; if(mode < 0x23) { for(int i=0; i < ui->modeSelectCombo->count(); i++) { if(ui->modeSelectCombo->itemData(i).toInt() == mode) { ui->modeSelectCombo->blockSignals(true); ui->modeSelectCombo->setCurrentIndex(i); ui->modeSelectCombo->blockSignals(false); found = true; } } currentModeIndex = mode; } else { qInfo(logSystem()) << __func__ << "Invalid mode " << mode << " received. "; } if(!found) { qInfo(logSystem()) << __func__ << "Received mode " << mode << " but could not match to any index within the modeSelectCombo. "; } if( (filter) && (filter < 4)){ ui->modeFilterCombo->blockSignals(true); ui->modeFilterCombo->setCurrentIndex(filter-1); ui->modeFilterCombo->blockSignals(false); } (void)filter; // Note: we need to know if the DATA mode is active to reach mode-D // some kind of queued query: if (rigCaps.hasDataModes && rigCaps.hasTransmit) { issueDelayedCommand(cmdGetDataMode); } } void wfmain::receiveDataModeStatus(bool dataEnabled) { ui->dataModeBtn->blockSignals(true); ui->dataModeBtn->setChecked(dataEnabled); ui->dataModeBtn->blockSignals(false); usingDataMode = dataEnabled; } void wfmain::on_clearPeakBtn_clicked() { if(haveRigCaps) { spectrumPeaks = QByteArray( (int)spectWidth, '\x01' ); } return; } void wfmain::on_drawPeakChk_clicked(bool checked) { if(checked) { on_clearPeakBtn_clicked(); // clear drawPeaks = true; } else { drawPeaks = false; #if QCUSTOMPLOT_VERSION >= 0x020000 plot->graph(1)->data()->clear(); #else plot->graph(1)->clearData(); #endif } prefs.drawPeaks = checked; } void wfmain::on_fullScreenChk_clicked(bool checked) { if(checked) { this->showFullScreen(); onFullscreen = true; } else { this->showNormal(); onFullscreen = false; } prefs.useFullScreen = checked; } void wfmain::on_goFreqBtn_clicked() { freqt f; bool ok = false; double freqDbl = 0; int KHz = 0; if(ui->freqMhzLineEdit->text().contains(".")) { freqDbl = ui->freqMhzLineEdit->text().toDouble(&ok); if(ok) { f.Hz = freqDbl*1E6; issueCmd(cmdSetFreq, f); } } else { KHz = ui->freqMhzLineEdit->text().toInt(&ok); if(ok) { f.Hz = KHz*1E3; issueCmd(cmdSetFreq, f); } } if(ok) { f.MHzDouble = (float)f.Hz / 1E6; freq = f; setUIFreq(); } ui->freqMhzLineEdit->selectAll(); freqTextSelected = true; ui->tabWidget->setCurrentIndex(0); } void wfmain::checkFreqSel() { if(freqTextSelected) { freqTextSelected = false; ui->freqMhzLineEdit->clear(); } } void wfmain::on_f0btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("0")); } void wfmain::on_f1btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("1")); } void wfmain::on_f2btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("2")); } void wfmain::on_f3btn_clicked() { ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("3")); } void wfmain::on_f4btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("4")); } void wfmain::on_f5btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("5")); } void wfmain::on_f6btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("6")); } void wfmain::on_f7btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("7")); } void wfmain::on_f8btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("8")); } void wfmain::on_f9btn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append("9")); } void wfmain::on_fDotbtn_clicked() { checkFreqSel(); ui->freqMhzLineEdit->setText(ui->freqMhzLineEdit->text().append(".")); } void wfmain::on_fBackbtn_clicked() { QString currentFreq = ui->freqMhzLineEdit->text(); currentFreq.chop(1); ui->freqMhzLineEdit->setText(currentFreq); } void wfmain::on_fCEbtn_clicked() { ui->freqMhzLineEdit->clear(); freqTextSelected = false; } void wfmain::on_spectrumModeCombo_currentIndexChanged(int index) { emit setScopeMode(static_cast(ui->spectrumModeCombo->itemData(index).toInt())); } void wfmain::on_fEnterBtn_clicked() { // TODO: do not jump to main tab on enter, only on return // or something. // Maybe this should be an option in settings-> on_goFreqBtn_clicked(); } void wfmain::on_scopeBWCombo_currentIndexChanged(int index) { emit setScopeSpan((char)index); } void wfmain::on_scopeEdgeCombo_currentIndexChanged(int index) { emit setScopeEdge((char)index+1); } void wfmain::changeMode(mode_kind mode) { bool dataOn = false; if(((unsigned char) mode >> 4) == 0x08) { dataOn = true; mode = (mode_kind)((int)mode & 0x0f); } changeMode(mode, dataOn); } void wfmain::changeMode(mode_kind mode, bool dataOn) { int filter = ui->modeFilterCombo->currentData().toInt(); emit setMode((unsigned char)mode, (unsigned char)filter); currentMode = mode; if(dataOn) { issueDelayedCommand(cmdSetDataModeOn); ui->dataModeBtn->blockSignals(true); ui->dataModeBtn->setChecked(true); ui->dataModeBtn->blockSignals(false); } else { issueDelayedCommand(cmdSetDataModeOff); ui->dataModeBtn->blockSignals(true); ui->dataModeBtn->setChecked(false); ui->dataModeBtn->blockSignals(false); } issueDelayedCommand(cmdGetMode); } void wfmain::on_modeSelectCombo_activated(int index) { // The "acticvated" signal means the user initiated a mode change. // This function is not called if code initated the change. mode_info mode; unsigned char newMode = static_cast(ui->modeSelectCombo->itemData(index).toUInt()); currentModeIndex = newMode; mode.reg = newMode; int filterSelection = ui->modeFilterCombo->currentData().toInt(); if(filterSelection == 99) { // oops, we forgot to reset the combo box return; } else { //qInfo(logSystem()) << __func__ << " at index " << index << " has newMode: " << newMode; currentMode = (mode_kind)newMode; mode.filter = filterSelection; mode.name = ui->modeSelectCombo->currentText(); // for debug for(unsigned int i=0; i < rigCaps.modes.size(); i++) { if(rigCaps.modes.at(i).reg == newMode) { mode.mk = rigCaps.modes.at(i).mk; break; } } issueCmd(cmdSetMode, mode); currentModeInfo = mode; //emit setMode(newMode, filterSelection); } } void wfmain::on_freqDial_valueChanged(int value) { int maxVal = ui->freqDial->maximum(); freqt f; f.Hz = 0; f.MHzDouble = 0; volatile int delta = 0; int directPath = 0; int crossingPath = 0; int distToMaxNew = 0; int distToMaxOld = 0; if(freqLock) { ui->freqDial->blockSignals(true); ui->freqDial->setValue(oldFreqDialVal); ui->freqDial->blockSignals(false); return; } if(value == 0) { distToMaxNew = 0; } else { distToMaxNew = maxVal - value; } if(oldFreqDialVal != 0) { distToMaxOld = maxVal - oldFreqDialVal; } else { distToMaxOld = 0; } directPath = abs(value - oldFreqDialVal); if(value < maxVal / 2) { crossingPath = value + distToMaxOld; } else { crossingPath = distToMaxNew + oldFreqDialVal; } if(directPath > crossingPath) { // use crossing path, it is shorter delta = crossingPath; // now calculate the direction: if( value > oldFreqDialVal) { // CW delta = delta; } else { // CCW delta *= -1; } } else { // use direct path // crossing path is larger than direct path, use direct path //delta = directPath; // now calculate the direction delta = value - oldFreqDialVal; } // With the number of steps and direction of steps established, // we can now adjust the frequeny: f.Hz = roundFrequencyWithStep(freq.Hz, delta, tsKnobHz); f.MHzDouble = f.Hz / (double)1E6; if(f.Hz > 0) { freq = f; oldFreqDialVal = value; ui->freqLabel->setText(QString("%1").arg(f.MHzDouble, 0, 'f')); //emit setFrequency(0,f); issueCmdUniquePriority(cmdSetFreq, f); } else { ui->freqDial->blockSignals(true); ui->freqDial->setValue(oldFreqDialVal); ui->freqDial->blockSignals(false); return; } } void wfmain::receiveBandStackReg(freqt freqGo, char mode, char filter, bool dataOn) { // read the band stack and apply by sending out commands qInfo(logSystem()) << __func__ << "BSR received into main: Freq: " << freqGo.Hz << ", mode: " << (unsigned int)mode << ", filter: " << (unsigned int)filter << ", data mode: " << dataOn; //emit setFrequency(0,freq); issueCmd(cmdSetFreq, freqGo); setModeVal = (unsigned char) mode; setFilterVal = (unsigned char) filter; issueDelayedCommand(cmdSetModeFilter); freq = freqGo; setUIFreq(); if(dataOn) { issueDelayedCommand(cmdSetDataModeOn); } else { issueDelayedCommand(cmdSetDataModeOff); } //issueDelayedCommand(cmdGetFreq); //issueDelayedCommand(cmdGetMode); ui->tabWidget->setCurrentIndex(0); receiveMode((unsigned char) mode, (unsigned char) filter); // update UI } void wfmain::bandStackBtnClick() { bandStkRegCode = ui->bandStkPopdown->currentIndex() + 1; waitingForBandStackRtn = true; // so that when the return is parsed we jump to this frequency/mode info emit getBandStackReg(bandStkBand, bandStkRegCode); } void wfmain::on_band23cmbtn_clicked() { bandStkBand = rigCaps.bsr[band23cm]; // 23cm bandStackBtnClick(); } void wfmain::on_band70cmbtn_clicked() { bandStkBand = rigCaps.bsr[band70cm]; // 70cm bandStackBtnClick(); } void wfmain::on_band2mbtn_clicked() { bandStkBand = rigCaps.bsr[band2m]; // 2m bandStackBtnClick(); } void wfmain::on_bandAirbtn_clicked() { bandStkBand = rigCaps.bsr[bandAir]; // VHF Aircraft bandStackBtnClick(); } void wfmain::on_bandWFMbtn_clicked() { bandStkBand = rigCaps.bsr[bandWFM]; // Broadcast FM bandStackBtnClick(); } void wfmain::on_band4mbtn_clicked() { // There isn't a BSR for this one: freqt f; if((currentMode == modeAM) || (currentMode == modeFM)) { f.Hz = (70.260) * 1E6; } else { f.Hz = (70.200) * 1E6; } issueCmd(cmdSetFreq, f); //emit setFrequency(0,f); issueDelayedCommandUnique(cmdGetFreq); ui->tabWidget->setCurrentIndex(0); } void wfmain::on_band6mbtn_clicked() { bandStkBand = 0x10; // 6 meters bandStackBtnClick(); } void wfmain::on_band10mbtn_clicked() { bandStkBand = 0x09; // 10 meters bandStackBtnClick(); } void wfmain::on_band12mbtn_clicked() { bandStkBand = 0x08; // 12 meters bandStackBtnClick(); } void wfmain::on_band15mbtn_clicked() { bandStkBand = 0x07; // 15 meters bandStackBtnClick(); } void wfmain::on_band17mbtn_clicked() { bandStkBand = 0x06; // 17 meters bandStackBtnClick(); } void wfmain::on_band20mbtn_clicked() { bandStkBand = 0x05; // 20 meters bandStackBtnClick(); } void wfmain::on_band30mbtn_clicked() { bandStkBand = 0x04; // 30 meters bandStackBtnClick(); } void wfmain::on_band40mbtn_clicked() { bandStkBand = 0x03; // 40 meters bandStackBtnClick(); } void wfmain::on_band60mbtn_clicked() { // This one is tricky. There isn't a band stack register on the // 7300 for 60 meters, so we just drop to the middle of the band: // Channel 1: 5330.5 kHz // Channel 2: 5346.5 kHz // Channel 3: 5357.0 kHz // Channel 4: 5371.5 kHz // Channel 5: 5403.5 kHz // Really not sure what the best strategy here is, don't want to // clutter the UI with 60M channel buttons... freqt f; f.Hz = (5.3305) * 1E6; issueCmd(cmdSetFreq, f); //emit setFrequency(0,f); issueDelayedCommandUnique(cmdGetFreq); ui->tabWidget->setCurrentIndex(0); } void wfmain::on_band80mbtn_clicked() { bandStkBand = 0x02; // 80 meters bandStackBtnClick(); } void wfmain::on_band160mbtn_clicked() { bandStkBand = 0x01; // 160 meters bandStackBtnClick(); } void wfmain::on_band630mbtn_clicked() { freqt f; f.Hz = 475 * 1E3; //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); ui->tabWidget->setCurrentIndex(0); } void wfmain::on_band2200mbtn_clicked() { freqt f; f.Hz = 136 * 1E3; //emit setFrequency(0,f); issueCmd(cmdSetFreq, f); issueDelayedCommandUnique(cmdGetFreq); ui->tabWidget->setCurrentIndex(0); } void wfmain::on_bandGenbtn_clicked() { // "GENE" general coverage frequency outside the ham bands // which does probably include any 60 meter frequencies used. bandStkBand = rigCaps.bsr[bandGen]; // GEN bandStackBtnClick(); } void wfmain::on_aboutBtn_clicked() { abtBox->show(); } void wfmain::on_fStoBtn_clicked() { // sequence: // type frequency // press Enter or Go // change mode if desired // type in index number 0 through 99 // press STO bool ok; int preset_number = ui->freqMhzLineEdit->text().toInt(&ok); if(ok && (preset_number >= 0) && (preset_number < 100)) { // TODO: keep an enum around with the current mode mem.setPreset(preset_number, freq.MHzDouble, (mode_kind)ui->modeSelectCombo->currentData().toInt() ); showStatusBarText( QString("Storing frequency %1 to memory location %2").arg( freq.MHzDouble ).arg(preset_number) ); } else { showStatusBarText(QString("Could not store preset to %1. Valid preset numbers are 0 to 99").arg(preset_number)); } } void wfmain::on_fRclBtn_clicked() { // Sequence: // type memory location 0 through 99 // press RCL // Program recalls data stored in vector at position specified // drop contents into text box, press go button // add delayed command for mode and data mode preset_kind temp; bool ok; QString freqString; int preset_number = ui->freqMhzLineEdit->text().toInt(&ok); if(ok && (preset_number >= 0) && (preset_number < 100)) { temp = mem.getPreset(preset_number); // TODO: change to int hz // TODO: store filter setting as well. freqString = QString("%1").arg(temp.frequency); ui->freqMhzLineEdit->setText( freqString ); ui->goFreqBtn->click(); setModeVal = temp.mode; setFilterVal = ui->modeFilterCombo->currentIndex()+1; // TODO, add to memory issueDelayedCommand(cmdSetModeFilter); issueDelayedCommand(cmdGetMode); } else { qInfo(logSystem()) << "Could not recall preset. Valid presets are 0 through 99."; } } void wfmain::on_rfGainSlider_valueChanged(int value) { issueCmdUniquePriority(cmdSetRxRfGain, (unsigned char) value); } void wfmain::on_afGainSlider_valueChanged(int value) { issueCmdUniquePriority(cmdSetAfGain, (unsigned char)value); if(usingLAN) { rxSetup.localAFgain = (unsigned char)(value); prefs.localAFgain = (unsigned char)(value); } } void wfmain::receiveRfGain(unsigned char level) { // qInfo(logSystem()) << "Receive RF level of" << (int)level << " = " << 100*level/255.0 << "%"; ui->rfGainSlider->blockSignals(true); ui->rfGainSlider->setValue(level); ui->rfGainSlider->blockSignals(false); } void wfmain::receiveAfGain(unsigned char level) { // qInfo(logSystem()) << "Receive AF level of" << (int)level << " = " << 100*level/255.0 << "%"; ui->afGainSlider->blockSignals(true); ui->afGainSlider->setValue(level); ui->afGainSlider->blockSignals(false); } void wfmain::receiveSql(unsigned char level) { ui->sqlSlider->setValue(level); } void wfmain::receiveIFShift(unsigned char level) { trxadj->updateIFShift(level); } void wfmain::receiveTBPFInner(unsigned char level) { trxadj->updateTPBFInner(level); } void wfmain::receiveTBPFOuter(unsigned char level) { trxadj->updateTPBFOuter(level); } void wfmain::on_tuneNowBtn_clicked() { issueDelayedCommand(cmdStartATU); showStatusBarText("Starting ATU tuning cycle..."); issueDelayedCommand(cmdGetATUStatus); } void wfmain::on_tuneEnableChk_clicked(bool checked) { issueCmd(cmdSetATU, checked); if(checked) { showStatusBarText("Turning on ATU"); } else { showStatusBarText("Turning off ATU"); } } void wfmain::on_exitBtn_clicked() { // Are you sure? QApplication::exit(); } void wfmain::on_pttOnBtn_clicked() { // is it enabled? if(!ui->pttEnableChk->isChecked()) { showStatusBarText("PTT is disabled, not sending command. Change under Settings tab."); return; } // Are we already PTT? Not a big deal, just send again anyway. showStatusBarText("Sending PTT ON command. Use Control-R to receive."); issueCmdUniquePriority(cmdSetPTT, true); // send PTT // Start 3 minute timer pttTimer->start(); issueDelayedCommand(cmdGetPTT); } void wfmain::on_pttOffBtn_clicked() { // Send the PTT OFF command (more than once?) showStatusBarText("Sending PTT OFF command"); issueCmdUniquePriority(cmdSetPTT, false); // Stop the 3 min timer pttTimer->stop(); issueDelayedCommand(cmdGetPTT); } void wfmain::handlePttLimit() { // transmission time exceeded! showStatusBarText("Transmit timeout at 3 minutes. Sending PTT OFF command now."); issueCmdUniquePriority(cmdSetPTT, false); issueDelayedCommand(cmdGetPTT); } void wfmain::on_saveSettingsBtn_clicked() { saveSettings(); // save memory, UI, and radio settings } void wfmain::receiveATUStatus(unsigned char atustatus) { // qInfo(logSystem()) << "Received ATU status update: " << (unsigned int) atustatus; switch(atustatus) { case 0x00: // ATU not active ui->tuneEnableChk->blockSignals(true); ui->tuneEnableChk->setChecked(false); ui->tuneEnableChk->blockSignals(false); showStatusBarText("ATU not enabled."); break; case 0x01: // ATU enabled ui->tuneEnableChk->blockSignals(true); ui->tuneEnableChk->setChecked(true); ui->tuneEnableChk->blockSignals(false); showStatusBarText("ATU enabled."); break; case 0x02: // ATU tuning in-progress. // Add command queue to check again and update status bar // qInfo(logSystem()) << "Received ATU status update that *tuning* is taking place"; showStatusBarText("ATU is Tuning..."); issueDelayedCommand(cmdGetATUStatus); // Sometimes the first hit seems to be missed. issueDelayedCommand(cmdGetATUStatus); break; default: qInfo(logSystem()) << "Did not understand ATU status: " << (unsigned int) atustatus; break; } } void wfmain::on_pttEnableChk_clicked(bool checked) { prefs.enablePTT = checked; } void wfmain::on_serialEnableBtn_clicked(bool checked) { prefs.enableLAN = !checked; ui->serialDeviceListCombo->setEnabled(checked); ui->connectBtn->setEnabled(true); ui->ipAddressTxt->setEnabled(!checked); ui->controlPortTxt->setEnabled(!checked); ui->usernameTxt->setEnabled(!checked); ui->passwordTxt->setEnabled(!checked); ui->audioRXCodecCombo->setEnabled(!checked); ui->audioTXCodecCombo->setEnabled(!checked); ui->audioSampleRateCombo->setEnabled(!checked); ui->rxLatencySlider->setEnabled(!checked); ui->txLatencySlider->setEnabled(!checked); ui->rxLatencyValue->setEnabled(!checked); ui->txLatencyValue->setEnabled(!checked); ui->baudRateCombo->setEnabled(checked); ui->serialDeviceListCombo->setEnabled(checked); //ui->udpServerSetupBtn->setEnabled(true); } void wfmain::on_lanEnableBtn_clicked(bool checked) { prefs.enableLAN = checked; ui->connectBtn->setEnabled(true); ui->ipAddressTxt->setEnabled(checked); ui->controlPortTxt->setEnabled(checked); ui->usernameTxt->setEnabled(checked); ui->passwordTxt->setEnabled(checked); ui->baudRateCombo->setEnabled(!checked); ui->serialDeviceListCombo->setEnabled(!checked); //ui->udpServerSetupBtn->setEnabled(false); if(checked) { showStatusBarText("After filling in values, press Save Settings."); } } void wfmain::on_ipAddressTxt_textChanged(QString text) { udpPrefs.ipAddress = text; } void wfmain::on_controlPortTxt_textChanged(QString text) { udpPrefs.controlLANPort = text.toUInt(); } void wfmain::on_usernameTxt_textChanged(QString text) { udpPrefs.username = text; } void wfmain::on_passwordTxt_textChanged(QString text) { udpPrefs.password = text; } void wfmain::on_audioOutputCombo_currentIndexChanged(int value) { #if defined(RTAUDIO) rxSetup.port = ui->audioOutputCombo->itemData(value).toInt(); #elif defined(PORTAUDIO) rxSetup.port = ui->audioOutputCombo->itemData(value).toInt(); #else QVariant v = ui->audioOutputCombo->itemData(value); rxSetup.port = v.value(); #endif rxSetup.name = ui->audioOutputCombo->itemText(value); qDebug(logGui()) << "Changed default audio output to:" << rxSetup.name; } void wfmain::on_audioInputCombo_currentIndexChanged(int value) { #if defined(RTAUDIO) txSetup.port = ui->audioInputCombo->itemData(value).toInt(); #elif defined(PORTAUDIO) txSetup.port = ui->audioInputCombo->itemData(value).toInt(); #else QVariant v = ui->audioInputCombo->itemData(value); txSetup.port = v.value(); #endif txSetup.name = ui->audioInputCombo->itemText(value); qDebug(logGui()) << "Changed default audio input to:" << txSetup.name; } void wfmain::on_audioSampleRateCombo_currentIndexChanged(QString text) { //udpPrefs.audioRXSampleRate = text.toInt(); //udpPrefs.audioTXSampleRate = text.toInt(); rxSetup.samplerate = text.toInt(); txSetup.samplerate = text.toInt(); } void wfmain::on_audioRXCodecCombo_currentIndexChanged(int value) { rxSetup.codec = ui->audioRXCodecCombo->itemData(value).toInt(); } void wfmain::on_audioTXCodecCombo_currentIndexChanged(int value) { txSetup.codec = ui->audioTXCodecCombo->itemData(value).toInt(); } void wfmain::on_rxLatencySlider_valueChanged(int value) { rxSetup.latency = value; ui->rxLatencyValue->setText(QString::number(value)); emit sendChangeLatency(value); } void wfmain::on_txLatencySlider_valueChanged(int value) { txSetup.latency = value; ui->txLatencyValue->setText(QString::number(value)); } void wfmain::on_vspCombo_currentIndexChanged(int value) { Q_UNUSED(value); prefs.virtualSerialPort = ui->vspCombo->currentText(); } void wfmain::on_toFixedBtn_clicked() { emit setScopeFixedEdge(oldLowerFreq, oldUpperFreq, ui->scopeEdgeCombo->currentIndex()+1); emit setScopeEdge(ui->scopeEdgeCombo->currentIndex()+1); issueDelayedCommand(cmdScopeFixedMode); } void wfmain::on_connectBtn_clicked() { this->rigStatus->setText(""); // Clear status if (haveRigCaps) { emit sendCloseComm(); ui->connectBtn->setText("Connect"); haveRigCaps = false; rigName->setText("NONE"); } else { emit sendCloseComm(); // Just in case there is a failed connection open. openRig(); } } void wfmain::on_udpServerSetupBtn_clicked() { srv->show(); } void wfmain::on_sqlSlider_valueChanged(int value) { issueCmd(cmdSetSql, (unsigned char)value); //emit setSql((unsigned char)value); } // These three are from the transceiver adjustment window: void wfmain::changeIFShift(unsigned char level) { //issueCmd(cmdSetIFShift, level); issueCmdUniquePriority(cmdSetIFShift, level); } void wfmain::changeTPBFInner(unsigned char level) { issueCmdUniquePriority(cmdSetTPBFInner, level); } void wfmain::changeTPBFOuter(unsigned char level) { issueCmdUniquePriority(cmdSetTPBFOuter, level); } void wfmain::on_modeFilterCombo_activated(int index) { int filterSelection = ui->modeFilterCombo->itemData(index).toInt(); if(filterSelection == 99) { // TODO: // Bump the filter selected back to F1, F2, or F3 // possibly track the filter in the class. Would make this easier. // filterSetup.show(); // } else { unsigned char newMode = static_cast(ui->modeSelectCombo->currentData().toUInt()); currentModeIndex = newMode; // we track this for other functions if(ui->dataModeBtn->isChecked()) { emit setDataMode(true, (unsigned char)filterSelection); } else { emit setMode(newMode, (unsigned char)filterSelection); } } } void wfmain::on_dataModeBtn_toggled(bool checked) { emit setDataMode(checked, (unsigned char)ui->modeFilterCombo->currentData().toInt()); usingDataMode = checked; if(usingDataMode) { changeModLabelAndSlider(currentModDataSrc); } else { changeModLabelAndSlider(currentModSrc); } } void wfmain::on_transmitBtn_clicked() { if(!amTransmitting) { // Currently receiving if(!ui->pttEnableChk->isChecked()) { showStatusBarText("PTT is disabled, not sending command. Change under Settings tab."); return; } // Are we already PTT? Not a big deal, just send again anyway. showStatusBarText("Sending PTT ON command. Use Control-R to receive."); issueCmdUniquePriority(cmdSetPTT, true); // send PTT // Start 3 minute timer pttTimer->start(); issueDelayedCommand(cmdGetPTT); //changeTxBtn(); } else { // Currently transmitting issueCmdUniquePriority(cmdSetPTT, false); pttTimer->stop(); issueDelayedCommand(cmdGetPTT); } } void wfmain::on_adjRefBtn_clicked() { cal->show(); } void wfmain::on_satOpsBtn_clicked() { sat->show(); } void wfmain::setRadioTimeDatePrep() { if(!waitingToSetTimeDate) { // 1: Find the current time and date QDateTime now = QDateTime::currentDateTime(); now.setTime(QTime::currentTime()); int second = now.time().second(); // 2: Find how many mseconds until next minute int msecdelay = QTime::currentTime().msecsTo( QTime::currentTime().addSecs(60-second) ); // 3: Compute time and date at one minute later QDateTime setpoint = now.addMSecs(msecdelay); // at HMS or posibly HMS + some ms. Never under though. // 4: Prepare data structs for the time at one minute later timesetpoint.hours = (unsigned char)setpoint.time().hour(); timesetpoint.minutes = (unsigned char)setpoint.time().minute(); datesetpoint.day = (unsigned char)setpoint.date().day(); datesetpoint.month = (unsigned char)setpoint.date().month(); datesetpoint.year = (uint16_t)setpoint.date().year(); unsigned int utcOffsetSeconds = (unsigned int)abs(setpoint.offsetFromUtc()); bool isMinus = setpoint.offsetFromUtc() < 0; utcsetting.hours = utcOffsetSeconds / 60 / 60; utcsetting.minutes = (utcOffsetSeconds - (utcsetting.hours*60*60) ) / 60; utcsetting.isMinus = isMinus; timeSync->setInterval(msecdelay); timeSync->setSingleShot(true); // 5: start one-shot timer for the delta computed in #2. timeSync->start(); waitingToSetTimeDate = true; showStatusBarText(QString("Setting time, date, and UTC offset for radio in %1 seconds.").arg(msecdelay/1000)); } } void wfmain::setRadioTimeDateSend() { // Issue priority commands for UTC offset, date, and time // UTC offset must come first, otherwise the radio may "help" and correct for any changes. showStatusBarText(QString("Setting time, date, and UTC offset for radio now.")); issueCmd(cmdSetTime, timesetpoint); issueCmd(cmdSetDate, datesetpoint); issueCmd(cmdSetUTCOffset, utcsetting); waitingToSetTimeDate = false; } void wfmain::changeSliderQuietly(QSlider *slider, int value) { slider->blockSignals(true); slider->setValue(value); slider->blockSignals(false); } void wfmain::statusFromSliderRaw(QString name, int rawValue) { showStatusBarText(name + QString(": %1").arg(rawValue)); } void wfmain::statusFromSliderPercent(QString name, int rawValue) { showStatusBarText(name + QString(": %1%").arg((int)(100*rawValue/255.0))); } void wfmain::receiveTxPower(unsigned char power) { changeSliderQuietly(ui->txPowerSlider, power); } void wfmain::receiveMicGain(unsigned char gain) { processModLevel(inputMic, gain); } void wfmain::processModLevel(rigInput source, unsigned char level) { rigInput currentIn; if(usingDataMode) { currentIn = currentModDataSrc; } else { currentIn = currentModSrc; } switch(source) { case inputMic: micGain = level; break; case inputACC: accGain = level; break; case inputACCA: accAGain = level; break; case inputACCB: accBGain = level; break; case inputUSB: usbGain = level; break; case inputLAN: lanGain = level; break; default: break; } if(currentIn == source) { changeSliderQuietly(ui->micGainSlider, level); } } void wfmain::receiveModInput(rigInput input, bool dataOn) { QComboBox *box; QString inputName; bool found; bool foundCurrent = false; if(dataOn) { box = ui->modInputDataCombo; currentModDataSrc = input; if(usingDataMode) foundCurrent = true; } else { box = ui->modInputCombo; currentModSrc = input; if(!usingDataMode) foundCurrent = true; } for(int i=0; i < box->count(); i++) { if(box->itemData(i).toInt() == (int)input) { box->blockSignals(true); box->setCurrentIndex(i); box->blockSignals(false); found = true; } } if(foundCurrent) { changeModLabel(input); } if(!found) qInfo(logSystem()) << "Could not find modulation input: " << (int)input; } void wfmain::receiveACCGain(unsigned char level, unsigned char ab) { if(ab==1) { processModLevel(inputACCB, level); } else { if(rigCaps.model == model7850) { processModLevel(inputACCA, level); } else { processModLevel(inputACC, level); } } } void wfmain::receiveUSBGain(unsigned char level) { processModLevel(inputUSB, level); } void wfmain::receiveLANGain(unsigned char level) { processModLevel(inputLAN, level); } void wfmain::receiveMeter(meterKind inMeter, unsigned char level) { switch(inMeter) { case meterS: ui->meterSPoWidget->setMeterType(meterS); ui->meterSPoWidget->setLevel(level); ui->meterSPoWidget->repaint(); break; case meterPower: ui->meterSPoWidget->setMeterType(meterPower); ui->meterSPoWidget->setLevel(level); ui->meterSPoWidget->update(); break; default: if(ui->meter2Widget->getMeterType() == inMeter) { ui->meter2Widget->setLevel(level); } break; } } void wfmain::receiveCompLevel(unsigned char compLevel) { (void)compLevel; } void wfmain::receiveMonitorGain(unsigned char monitorGain) { (void)monitorGain; } void wfmain::receiveVoxGain(unsigned char voxGain) { (void)voxGain; } void wfmain::receiveAntiVoxGain(unsigned char antiVoxGain) { (void)antiVoxGain; } void wfmain::on_txPowerSlider_valueChanged(int value) { issueCmdUniquePriority(cmdSetTxPower, (unsigned char)value); //emit setTxPower(value); } void wfmain::on_micGainSlider_valueChanged(int value) { processChangingCurrentModLevel((unsigned char) value); } void wfmain::on_scopeRefLevelSlider_valueChanged(int value) { value = (value/5) * 5; // rounded to "nearest 5" emit setSpectrumRefLevel(value); } void wfmain::receiveSpectrumRefLevel(int level) { changeSliderQuietly(ui->scopeRefLevelSlider, level); } // Slot to send/receive server config. // If store is true then write to config otherwise send current config by signal void wfmain::serverConfigRequested(SERVERCONFIG conf, bool store) { if (!store) { emit sendServerConfig(serverConfig); } else { // Store config in file! qInfo(logSystem()) << "Storing server config"; serverConfig = conf; } } void wfmain::on_modInputCombo_activated(int index) { emit setModInput( (rigInput)ui->modInputCombo->currentData().toInt(), false ); currentModSrc = (rigInput)ui->modInputCombo->currentData().toInt(); issueDelayedCommand(cmdGetCurrentModLevel); if(!usingDataMode) { changeModLabel(currentModSrc); } (void)index; } void wfmain::on_modInputDataCombo_activated(int index) { emit setModInput( (rigInput)ui->modInputDataCombo->currentData().toInt(), true ); currentModDataSrc = (rigInput)ui->modInputDataCombo->currentData().toInt(); issueDelayedCommand(cmdGetCurrentModLevel); if(usingDataMode) { changeModLabel(currentModDataSrc); } (void)index; } void wfmain::changeModLabelAndSlider(rigInput source) { changeModLabel(source, true); } void wfmain::changeModLabel(rigInput input) { changeModLabel(input, false); } void wfmain::changeModLabel(rigInput input, bool updateLevel) { QString inputName; unsigned char gain = 0; switch(input) { case inputMic: inputName = "Mic"; gain = micGain; break; case inputACC: inputName = "ACC"; gain = accGain; break; case inputACCA: inputName = "ACCA"; gain = accAGain; break; case inputACCB: inputName = "ACCB"; gain = accBGain; break; case inputUSB: inputName = "USB"; gain = usbGain; break; case inputLAN: inputName = "LAN"; gain = lanGain; break; default: inputName = "UNK"; gain=0; break; } ui->modSliderLbl->setText(inputName); if(updateLevel) { changeSliderQuietly(ui->micGainSlider, gain); } } void wfmain::processChangingCurrentModLevel(unsigned char level) { // slider moved, so find the current mod and issue the level set command. issueCmd(cmdSetModLevel, level); } void wfmain::on_tuneLockChk_clicked(bool checked) { freqLock = checked; } void wfmain::on_serialDeviceListCombo_activated(const QString &arg1) { QString manualPort; bool ok; if(arg1==QString("Manual...")) { manualPort = QInputDialog::getText(this, tr("Manual port assignment"), tr("Enter serial port assignment:"), QLineEdit::Normal, tr("/dev/device"), &ok); if(manualPort.isEmpty() || !ok) { ui->serialDeviceListCombo->blockSignals(true); ui->serialDeviceListCombo->setCurrentIndex(0); ui->serialDeviceListCombo->blockSignals(false); return; } else { prefs.serialPortRadio = manualPort; showStatusBarText("Setting preferences to use manually-assigned serial port: " + manualPort); ui->serialEnableBtn->setChecked(true); return; } } if(arg1==QString("Auto")) { prefs.serialPortRadio = "auto"; showStatusBarText("Setting preferences to automatically find rig serial port."); ui->serialEnableBtn->setChecked(true); return; } prefs.serialPortRadio = arg1; showStatusBarText("Setting preferences to use manually-assigned serial port: " + arg1); ui->serialEnableBtn->setChecked(true); } void wfmain::on_rptSetupBtn_clicked() { rpt->show(); } void wfmain::on_attSelCombo_activated(int index) { unsigned char att = (unsigned char)ui->attSelCombo->itemData(index).toInt(); emit setAttenuator(att); issueDelayedCommand(cmdGetPreamp); } void wfmain::on_preampSelCombo_activated(int index) { unsigned char pre = (unsigned char)ui->preampSelCombo->itemData(index).toInt(); emit setPreamp(pre); issueDelayedCommand(cmdGetAttenuator); } void wfmain::on_antennaSelCombo_activated(int index) { unsigned char ant = (unsigned char)ui->antennaSelCombo->itemData(index).toInt(); emit setAntenna(ant,ui->rxAntennaCheck->isChecked()); } void wfmain::on_rxAntennaCheck_clicked(bool value) { unsigned char ant = (unsigned char)ui->antennaSelCombo->itemData(ui->antennaSelCombo->currentIndex()).toInt(); emit setAntenna(ant, value); } void wfmain::on_wfthemeCombo_activated(int index) { colorMap->setGradient(static_cast(ui->wfthemeCombo->itemData(index).toInt())); prefs.wftheme = ui->wfthemeCombo->itemData(index).toInt(); } void wfmain::receivePreamp(unsigned char pre) { int preindex = ui->preampSelCombo->findData(pre); ui->preampSelCombo->setCurrentIndex(preindex); } void wfmain::receiveAttenuator(unsigned char att) { int attindex = ui->attSelCombo->findData(att); ui->attSelCombo->setCurrentIndex(attindex); } void wfmain::receiveAntennaSel(unsigned char ant, bool rx) { ui->antennaSelCombo->setCurrentIndex(ant); ui->rxAntennaCheck->setChecked(rx); } void wfmain::receiveSpectrumSpan(freqt freqspan, bool isSub) { if(!isSub) { switch((int)(freqspan.MHzDouble * 1000000.0)) { case(2500): ui->scopeBWCombo->setCurrentIndex(0); break; case(5000): ui->scopeBWCombo->setCurrentIndex(1); break; case(10000): ui->scopeBWCombo->setCurrentIndex(2); break; case(25000): ui->scopeBWCombo->setCurrentIndex(3); break; case(50000): ui->scopeBWCombo->setCurrentIndex(4); break; case(100000): ui->scopeBWCombo->setCurrentIndex(5); break; case(250000): ui->scopeBWCombo->setCurrentIndex(6); break; case(500000): ui->scopeBWCombo->setCurrentIndex(7); break; case(1000000): ui->scopeBWCombo->setCurrentIndex(8); break; case(2500000): ui->scopeBWCombo->setCurrentIndex(9); break; default: qInfo(logSystem()) << __func__ << "Could not match: " << freqspan.MHzDouble << " to anything like: " << (int)(freqspan.MHzDouble*1E6); break; } } } void wfmain::calculateTimingParameters() { // Function for calculating polling parameters. // Requires that we know the "baud rate" of the actual // radio connection. // baud on the serial port reflects the actual rig connection, // even if a client-server connection is being used. // Computed time for a 10 byte message, with a safety factor of 2. if (prefs.serialPortBaud == 0) { prefs.serialPortBaud = 9600; qInfo(logSystem()) << "WARNING: baud rate received was zero. Assuming 9600 baud, performance may suffer."; } unsigned int usPerByte = 9600*1000 / prefs.serialPortBaud; unsigned int msMinTiming=usPerByte * 10*2/1000; if(msMinTiming < 25) msMinTiming = 25; if(haveRigCaps && rigCaps.hasFDcomms) { delayedCommand->setInterval( msMinTiming); // 20 byte message } else { delayedCommand->setInterval( msMinTiming * 3); // 20 byte message } qInfo(logSystem()) << "Delay command interval timing: " << delayedCommand->interval() << "ms"; // Normal: delayedCmdIntervalLAN_ms = delayedCommand->interval(); delayedCmdIntervalSerial_ms = delayedCommand->interval(); // startup initial state: delayedCmdStartupInterval_ms = delayedCommand->interval() * 3; } void wfmain::receiveBaudRate(quint32 baud) { qInfo() << "Received serial port baud rate from remote server:" << baud; prefs.serialPortBaud = baud; calculateTimingParameters(); } void wfmain::on_rigPowerOnBtn_clicked() { powerRigOn(); } void wfmain::on_rigPowerOffBtn_clicked() { // Are you sure? if (!prefs.confirmPowerOff) { powerRigOff(); return; } QCheckBox* cb = new QCheckBox("Don't ask me again"); QMessageBox msgbox; msgbox.setWindowTitle("Power"); msgbox.setText("Power down the radio?\n"); msgbox.setIcon(QMessageBox::Icon::Question); QAbstractButton* yesButton = msgbox.addButton(QMessageBox::Yes); msgbox.addButton(QMessageBox::No); msgbox.setDefaultButton(QMessageBox::Yes); msgbox.setCheckBox(cb); QObject::connect(cb, &QCheckBox::stateChanged, [this](int state) { if (static_cast(state) == Qt::CheckState::Checked) { prefs.confirmPowerOff = false; } else { prefs.confirmPowerOff = true; } settings->beginGroup("Interface"); settings->setValue("ConfirmPowerOff", this->prefs.confirmPowerOff); settings->endGroup(); settings->sync(); }); msgbox.exec(); if (msgbox.clickedButton() == yesButton) { powerRigOff(); } } void wfmain::powerRigOn() { emit sendPowerOn(); delayedCommand->setInterval(3000); // 3 seconds if(ui->scopeEnableWFBtn->isChecked()) { issueDelayedCommand(cmdDispEnable); issueDelayedCommand(cmdQueNormalSpeed); issueDelayedCommand(cmdSpecOn); issueDelayedCommand(cmdStartRegularPolling); // s-meter, etc } else { issueDelayedCommand(cmdQueNormalSpeed); issueDelayedCommand(cmdStartRegularPolling); // s-meter, etc } delayedCommand->start(); } void wfmain::powerRigOff() { delayedCommand->stop(); delayedCmdQue.clear(); emit sendPowerOff(); } void wfmain::on_ritTuneDial_valueChanged(int value) { emit setRitValue(value); } void wfmain::on_ritEnableChk_clicked(bool checked) { emit setRitEnable(checked); } void wfmain::receiveRITStatus(bool ritEnabled) { ui->ritEnableChk->blockSignals(true); ui->ritEnableChk->setChecked(ritEnabled); ui->ritEnableChk->blockSignals(false); } void wfmain::receiveRITValue(int ritValHz) { if((ritValHz > -500) && (ritValHz < 500)) { ui->ritTuneDial->blockSignals(true); ui->ritTuneDial->setValue(ritValHz); ui->ritTuneDial->blockSignals(false); } else { qInfo(logSystem()) << "Warning: out of range RIT value received: " << ritValHz << " Hz"; } } void wfmain::showButton(QPushButton *btn) { btn->setHidden(false); } void wfmain::hideButton(QPushButton *btn) { btn->setHidden(true); } void wfmain::setBandButtons() { // Turn off each button first: hideButton(ui->band23cmbtn); hideButton(ui->band70cmbtn); hideButton(ui->band2mbtn); hideButton(ui->bandAirbtn); hideButton(ui->bandWFMbtn); hideButton(ui->band4mbtn); hideButton(ui->band6mbtn); hideButton(ui->band10mbtn); hideButton(ui->band12mbtn); hideButton(ui->band15mbtn); hideButton(ui->band17mbtn); hideButton(ui->band20mbtn); hideButton(ui->band30mbtn); hideButton(ui->band40mbtn); hideButton(ui->band60mbtn); hideButton(ui->band80mbtn); hideButton(ui->band160mbtn); hideButton(ui->band630mbtn); hideButton(ui->band2200mbtn); hideButton(ui->bandGenbtn); bandType bandSel; //for (auto band = rigCaps.bands.begin(); band != rigCaps.bands.end(); ++band) // no worky for(unsigned int i=0; i < rigCaps.bands.size(); i++) { bandSel = rigCaps.bands.at(i); switch(bandSel) { case(band23cm): showButton(ui->band23cmbtn); break; case(band70cm): showButton(ui->band70cmbtn); break; case(band2m): showButton(ui->band2mbtn); break; case(bandAir): showButton(ui->bandAirbtn); break; case(bandWFM): showButton(ui->bandWFMbtn); break; case(band4m): showButton(ui->band4mbtn); break; case(band6m): showButton(ui->band6mbtn); break; case(band10m): showButton(ui->band10mbtn); break; case(band12m): showButton(ui->band12mbtn); break; case(band15m): showButton(ui->band15mbtn); break; case(band17m): showButton(ui->band17mbtn); break; case(band20m): showButton(ui->band20mbtn); break; case(band30m): showButton(ui->band30mbtn); break; case(band40m): showButton(ui->band40mbtn); break; case(band60m): showButton(ui->band60mbtn); break; case(band80m): showButton(ui->band80mbtn); break; case(band160m): showButton(ui->band160mbtn); break; case(band630m): showButton(ui->band630mbtn); break; case(band2200m): showButton(ui->band2200mbtn); break; case(bandGen): showButton(ui->bandGenbtn); break; default: break; } } } void wfmain::on_rigCIVManualAddrChk_clicked(bool checked) { if(checked) { ui->rigCIVaddrHexLine->setEnabled(true); ui->rigCIVaddrHexLine->setText(QString("%1").arg(prefs.radioCIVAddr, 2, 16)); } else { ui->rigCIVaddrHexLine->setText("auto"); ui->rigCIVaddrHexLine->setEnabled(false); prefs.radioCIVAddr = 0; // auto showStatusBarText("Setting radio CI-V address to: 'auto'. Make sure CI-V Transceive is enabled on the radio."); } } void wfmain::on_rigCIVaddrHexLine_editingFinished() { bool okconvert=false; unsigned char propCIVAddr = (unsigned char) ui->rigCIVaddrHexLine->text().toUInt(&okconvert, 16); if(okconvert && (propCIVAddr < 0xe0) && (propCIVAddr != 0)) { prefs.radioCIVAddr = propCIVAddr; emit setCIVAddr(propCIVAddr); showStatusBarText(QString("Setting radio CI-V address to: 0x%1. Press Save Settings to retain.").arg(propCIVAddr, 2, 16)); } else { showStatusBarText(QString("Could not use provided CI-V address. Address must be < 0xE0")); } } void wfmain::on_baudRateCombo_activated(int index) { bool ok = false; quint32 baud = ui->baudRateCombo->currentData().toUInt(&ok); if(ok) { prefs.serialPortBaud = baud; serverConfig.baudRate = baud; showStatusBarText(QString("Changed baud rate to %1 bps. Press Save Settings to retain.").arg(baud)); } (void)index; } void wfmain::on_wfLengthSlider_valueChanged(int value) { prefs.wflength = (unsigned int)(value); prepareWf(value); } void wfmain::on_pollingBtn_clicked() { bool ok; int timing = 0; timing = QInputDialog::getInt(this, "wfview Radio Polling Setup", "Poll Timing Interval (ms)", delayedCommand->interval(), 1, 200, 1, &ok ); if(ok && timing) { delayedCommand->setInterval( timing ); qInfo(logSystem()) << "User changed radio polling interval to " << timing << "ms."; showStatusBarText("User changed radio polling interval to " + QString("%1").arg(timing) + "ms."); } } void wfmain::on_wfAntiAliasChk_clicked(bool checked) { colorMap->setAntialiased(checked); prefs.wfAntiAlias = checked; } void wfmain::on_wfInterpolateChk_clicked(bool checked) { colorMap->setInterpolate(checked); prefs.wfInterpolate = checked; } wfmain::cmds wfmain::meterKindToMeterCommand(meterKind m) { cmds c; switch(m) { case meterNone: c = cmdNone; break; case meterS: c = cmdGetSMeter; break; case meterCenter: c = cmdGetCenterMeter; break; case meterPower: c = cmdGetPowerMeter; break; case meterSWR: c = cmdGetSWRMeter; break; case meterALC: c = cmdGetALCMeter; break; case meterComp: c = cmdGetCompMeter; break; case meterCurrent: c = cmdGetIdMeter; break; case meterVoltage: c = cmdGetVdMeter; break; default: c = cmdNone; break; } return c; } void wfmain::on_meter2selectionCombo_activated(int index) { meterKind newMeterType; meterKind oldMeterType; newMeterType = static_cast(ui->meter2selectionCombo->currentData().toInt()); oldMeterType = ui->meter2Widget->getMeterType(); if(newMeterType == oldMeterType) return; cmds newCmd = meterKindToMeterCommand(newMeterType); cmds oldCmd = meterKindToMeterCommand(oldMeterType); removePeriodicCommand(oldCmd); if(newMeterType==meterNone) { ui->meter2Widget->hide(); ui->meter2Widget->setMeterType(newMeterType); } else { ui->meter2Widget->show(); ui->meter2Widget->setMeterType(newMeterType); insertPeriodicCommandUnique(newCmd); } prefs.meter2Type = newMeterType; (void)index; } void wfmain::on_enableRigctldChk_clicked(bool checked) { if (rigCtl != Q_NULLPTR) { rigCtl->disconnect(); delete rigCtl; rigCtl = Q_NULLPTR; } if (checked) { // Start rigctld rigCtl = new rigCtlD(this); rigCtl->startServer(prefs.rigCtlPort); connect(this, SIGNAL(sendRigCaps(rigCapabilities)), rigCtl, SLOT(receiveRigCaps(rigCapabilities))); if (rig != Q_NULLPTR) { // We are already connected to a rig. connect(rig, SIGNAL(stateInfo(rigStateStruct*)), rigCtl, SLOT(receiveStateInfo(rigStateStruct*))); connect(rigCtl, SIGNAL(setFrequency(unsigned char, freqt)), rig, SLOT(setFrequency(unsigned char, freqt))); connect(rigCtl, SIGNAL(setMode(unsigned char, unsigned char)), rig, SLOT(setMode(unsigned char, unsigned char))); connect(rigCtl, SIGNAL(setDataMode(bool, unsigned char)), rig, SLOT(setDataMode(bool, unsigned char))); connect(rigCtl, SIGNAL(setPTT(bool)), rig, SLOT(setPTT(bool))); connect(rigCtl, SIGNAL(sendPowerOn()), rig, SLOT(powerOn())); connect(rigCtl, SIGNAL(sendPowerOff()), rig, SLOT(powerOff())); connect(rigCtl, SIGNAL(setAttenuator(unsigned char)), rig, SLOT(setAttenuator(unsigned char))); connect(rigCtl, SIGNAL(setPreamp(unsigned char)), rig, SLOT(setPreamp(unsigned char))); connect(rigCtl, SIGNAL(setDuplexMode(duplexMode)), rig, SLOT(setDuplexMode(duplexMode))); // Levels: Set: connect(rigCtl, SIGNAL(setRfGain(unsigned char)), rig, SLOT(setRfGain(unsigned char))); connect(rigCtl, SIGNAL(setAfGain(unsigned char)), rig, SLOT(setAfGain(unsigned char))); connect(rigCtl, SIGNAL(setSql(unsigned char)), rig, SLOT(setSquelch(unsigned char))); connect(rigCtl, SIGNAL(setTxPower(unsigned char)), rig, SLOT(setTxPower(unsigned char))); connect(rigCtl, SIGNAL(setMicGain(unsigned char)), rig, SLOT(setMicGain(unsigned char))); connect(rigCtl, SIGNAL(setMonitorLevel(unsigned char)), rig, SLOT(setMonitorLevel(unsigned char))); connect(rigCtl, SIGNAL(setVoxGain(unsigned char)), rig, SLOT(setVoxGain(unsigned char))); connect(rigCtl, SIGNAL(setAntiVoxGain(unsigned char)), rig, SLOT(setAntiVoxGain(unsigned char))); connect(rigCtl, SIGNAL(setSpectrumRefLevel(int)), rig, SLOT(setSpectrumRefLevel(int))); emit sendRigCaps(rigCaps); emit requestRigState(); } } prefs.enableRigCtlD = checked; } void wfmain::on_rigctldPortTxt_editingFinished() { bool okconvert = false; unsigned int port = ui->rigctldPortTxt->text().toUInt(&okconvert); if (okconvert) { prefs.rigCtlPort = port; } } void wfmain::on_moreControlsBtn_clicked() { trxadj->show(); } void wfmain::on_useCIVasRigIDChk_clicked(bool checked) { prefs.CIVisRadioModel = checked; } // --- DEBUG FUNCTION --- void wfmain::on_debugBtn_clicked() { qInfo(logSystem()) << "Debug button pressed."; // issueDelayedCommand(cmdGetRigID); //emit getRigCIV(); trxadj->show(); //setRadioTimeDatePrep(); //wf->setInteraction(QCP::iRangeZoom, true); //wf->setInteraction(QCP::iRangeDrag, true); } wfview-1.2d/wfmain.h000066400000000000000000000567301415164626400145050ustar00rootroot00000000000000#ifndef WFMAIN_H #define WFMAIN_H #include #include #include #include #include #include #include #include #include #include "logcategories.h" #include "commhandler.h" #include "rigcommander.h" #include "freqmemory.h" #include "rigidentities.h" #include "repeaterattributes.h" #include "calibrationwindow.h" #include "repeatersetup.h" #include "satellitesetup.h" #include "transceiveradjustments.h" #include "udpserversetup.h" #include "udpserver.h" #include "qledlabel.h" #include "rigctld.h" #include "aboutbox.h" #include #include #include #include namespace Ui { class wfmain; } class wfmain : public QMainWindow { Q_OBJECT public: explicit wfmain(const QString serialPortCL, const QString hostCL, const QString settingsFile, QWidget *parent = 0); QString serialPortCL; QString hostCL; ~wfmain(); signals: // Basic to rig: void setCIVAddr(unsigned char newRigCIVAddr); void setRigID(unsigned char rigID); // Power void sendPowerOn(); void sendPowerOff(); // Frequency, mode, band: void getFrequency(); void setFrequency(unsigned char vfo, freqt freq); void getMode(); void setMode(unsigned char modeIndex, unsigned char modeFilter); void setMode(mode_info); void setDataMode(bool dataOn, unsigned char filter); void getDataMode(); void getModInput(bool dataOn); void setModInput(rigInput input, bool dataOn); void getBandStackReg(char band, char regCode); void getDebug(); void getRitEnabled(); void getRitValue(); void setRitValue(int ritValue); void setRitEnable(bool ritEnabled); // Repeater: void getDuplexMode(); void getTone(); void getTSQL(); void getDTCS(); void getRptAccessMode(); // Level get: void getLevels(); // get all levels void getRfGain(); void getAfGain(); void getSql(); void getIfShift(); void getTPBFInner(); void getTPBFOuter(); void getTxPower(); void getMicGain(); void getSpectrumRefLevel(); void getModInputLevel(rigInput input); // Level set: void setRfGain(unsigned char level); void setAfGain(unsigned char level); void setSql(unsigned char level); void setIFShift(unsigned char level); void setTPBFInner(unsigned char level); void setTPBFOuter(unsigned char level); void setIFShiftWindow(unsigned char level); void setTPBFInnerWindow(unsigned char level); void setTPBFOuterWindow(unsigned char level); void setMicGain(unsigned char); void setCompLevel(unsigned char); void setTxPower(unsigned char); void setMonitorLevel(unsigned char); void setVoxGain(unsigned char); void setAntiVoxGain(unsigned char); void setSpectrumRefLevel(int); void setModLevel(rigInput input, unsigned char level); void setACCGain(unsigned char level); void setACCAGain(unsigned char level); void setACCBGain(unsigned char level); void setUSBGain(unsigned char level); void setLANGain(unsigned char level); void getMeters(meterKind meter); // PTT, ATU, ATT, Antenna, Preamp: void getPTT(); void setPTT(bool pttOn); void getAttenuator(); void getPreamp(); void getAntenna(); void setAttenuator(unsigned char att); void setPreamp(unsigned char pre); void setAntenna(unsigned char ant, bool rx); void startATU(); void setATU(bool atuEnabled); void getATUStatus(); // Time and date: void setTime(timekind t); void setDate(datekind d); void setUTCOffset(timekind t); void getRigID(); // this is the model of the rig void getRigCIV(); // get the rig's CIV addr void spectOutputEnable(); void spectOutputDisable(); void scopeDisplayEnable(); void scopeDisplayDisable(); void setScopeMode(spectrumMode spectMode); void setScopeSpan(char span); void setScopeEdge(char edge); void setScopeFixedEdge(double startFreq, double endFreq, unsigned char edgeNumber); void getScopeMode(); void getScopeEdge(); void getScopeSpan(); void sayFrequency(); void sayMode(); void sayAll(); void sendCommSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate,QString vsp); void sendCommSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp); void sendCloseComm(); void sendChangeLatency(quint16 latency); void initServer(); void sendServerConfig(SERVERCONFIG conf); void sendRigCaps(rigCapabilities caps); void requestRigState(); private slots: void updateSizes(int tabIndex); void shortcutF1(); void shortcutF2(); void shortcutF3(); void shortcutF4(); void shortcutF5(); void shortcutF6(); void shortcutF7(); void shortcutF8(); void shortcutF9(); void shortcutF10(); void shortcutF11(); void shortcutF12(); void shortcutControlT(); void shortcutControlR(); void shortcutControlI(); void shortcutControlU(); void shortcutStar(); void shortcutSlash(); void shortcutMinus(); void shortcutPlus(); void shortcutShiftMinus(); void shortcutShiftPlus(); void shortcutControlMinus(); void shortcutControlPlus(); void shortcutPageUp(); void shortcutPageDown(); void shortcutF(); void shortcutM(); void handlePttLimit(); // hit at 3 min transmit length void receiveCommReady(); void receiveFreq(freqt); void receiveMode(unsigned char mode, unsigned char filter); void receiveSpectrumData(QByteArray spectrum, double startFreq, double endFreq); void receiveSpectrumMode(spectrumMode spectMode); void receiveSpectrumSpan(freqt freqspan, bool isSub); void receivePTTstatus(bool pttOn); void receiveDataModeStatus(bool dataOn); void receiveBandStackReg(freqt f, char mode, char filter, bool dataOn); // freq, mode, (filter,) datamode void receiveRITStatus(bool ritEnabled); void receiveRITValue(int ritValHz); void receiveModInput(rigInput input, bool dataOn); //void receiveDuplexMode(duplexMode dm); // Levels: void receiveRfGain(unsigned char level); void receiveAfGain(unsigned char level); void receiveSql(unsigned char level); void receiveIFShift(unsigned char level); void receiveTBPFInner(unsigned char level); void receiveTBPFOuter(unsigned char level); // 'change' from data in transceiver controls window: void changeIFShift(unsigned char level); void changeTPBFInner(unsigned char level); void changeTPBFOuter(unsigned char level); void receiveTxPower(unsigned char power); void receiveMicGain(unsigned char gain); void receiveCompLevel(unsigned char compLevel); void receiveMonitorGain(unsigned char monitorGain); void receiveVoxGain(unsigned char voxGain); void receiveAntiVoxGain(unsigned char antiVoxGain); void receiveSpectrumRefLevel(int level); void receiveACCGain(unsigned char level, unsigned char ab); void receiveUSBGain(unsigned char level); void receiveLANGain(unsigned char level); // Meters: void receiveMeter(meterKind meter, unsigned char level); // void receiveSMeter(unsigned char level); // void receivePowerMeter(unsigned char level); // void receiveALCMeter(unsigned char level); // void receiveCompMeter(unsigned char level); void receiveATUStatus(unsigned char atustatus); void receivePreamp(unsigned char pre); void receiveAttenuator(unsigned char att); void receiveAntennaSel(unsigned char ant, bool rx); void receiveRigID(rigCapabilities rigCaps); void receiveFoundRigID(rigCapabilities rigCaps); void receiveSerialPortError(QString port, QString errorText); void receiveStatusUpdate(QString errorText); void handlePlotClick(QMouseEvent *); void handlePlotDoubleClick(QMouseEvent *); void handleWFClick(QMouseEvent *); void handleWFDoubleClick(QMouseEvent *); void handleWFScroll(QWheelEvent *); void handlePlotScroll(QWheelEvent *); void sendRadioCommandLoop(); void showStatusBarText(QString text); void serverConfigRequested(SERVERCONFIG conf, bool store); void receiveBaudRate(quint32 baudrate); void setRadioTimeDateSend(); // void on_getFreqBtn_clicked(); // void on_getModeBtn_clicked(); // void on_debugBtn_clicked(); void on_clearPeakBtn_clicked(); void on_drawPeakChk_clicked(bool checked); void on_fullScreenChk_clicked(bool checked); void on_goFreqBtn_clicked(); void on_f0btn_clicked(); void on_f1btn_clicked(); void on_f2btn_clicked(); void on_f3btn_clicked(); void on_f4btn_clicked(); void on_f5btn_clicked(); void on_f6btn_clicked(); void on_f7btn_clicked(); void on_f8btn_clicked(); void on_f9btn_clicked(); void on_fDotbtn_clicked(); void on_fBackbtn_clicked(); void on_fCEbtn_clicked(); void on_fEnterBtn_clicked(); void on_scopeBWCombo_currentIndexChanged(int index); void on_scopeEdgeCombo_currentIndexChanged(int index); // void on_modeSelectCombo_currentIndexChanged(int index); void on_useDarkThemeChk_clicked(bool checked); void on_modeSelectCombo_activated(int index); // void on_freqDial_actionTriggered(int action); void on_freqDial_valueChanged(int value); void on_band6mbtn_clicked(); void on_band10mbtn_clicked(); void on_band12mbtn_clicked(); void on_band15mbtn_clicked(); void on_band17mbtn_clicked(); void on_band20mbtn_clicked(); void on_band30mbtn_clicked(); void on_band40mbtn_clicked(); void on_band60mbtn_clicked(); void on_band80mbtn_clicked(); void on_band160mbtn_clicked(); void on_bandGenbtn_clicked(); void on_aboutBtn_clicked(); void on_fStoBtn_clicked(); void on_fRclBtn_clicked(); void on_rfGainSlider_valueChanged(int value); void on_afGainSlider_valueChanged(int value); void on_tuneNowBtn_clicked(); void on_tuneEnableChk_clicked(bool checked); void on_exitBtn_clicked(); void on_pttOnBtn_clicked(); void on_pttOffBtn_clicked(); void on_saveSettingsBtn_clicked(); void on_debugBtn_clicked(); void on_pttEnableChk_clicked(bool checked); void on_lanEnableBtn_clicked(bool checked); void on_ipAddressTxt_textChanged(QString text); void on_controlPortTxt_textChanged(QString text); void on_usernameTxt_textChanged(QString text); void on_passwordTxt_textChanged(QString text); void on_audioOutputCombo_currentIndexChanged(int value); void on_audioInputCombo_currentIndexChanged(int value); void on_toFixedBtn_clicked(); void on_connectBtn_clicked(); void on_rxLatencySlider_valueChanged(int value); void on_txLatencySlider_valueChanged(int value); void on_audioRXCodecCombo_currentIndexChanged(int value); void on_audioTXCodecCombo_currentIndexChanged(int value); void on_audioSampleRateCombo_currentIndexChanged(QString text); void on_vspCombo_currentIndexChanged(int value); void on_scopeEnableWFBtn_clicked(bool checked); void on_sqlSlider_valueChanged(int value); void on_modeFilterCombo_activated(int index); void on_dataModeBtn_toggled(bool checked); void on_udpServerSetupBtn_clicked(); void on_transmitBtn_clicked(); void on_adjRefBtn_clicked(); void on_satOpsBtn_clicked(); void on_txPowerSlider_valueChanged(int value); void on_micGainSlider_valueChanged(int value); void on_scopeRefLevelSlider_valueChanged(int value); void on_useSystemThemeChk_clicked(bool checked); void on_modInputCombo_activated(int index); void on_modInputDataCombo_activated(int index); void on_tuneLockChk_clicked(bool checked); void on_spectrumModeCombo_currentIndexChanged(int index); void on_serialEnableBtn_clicked(bool checked); void on_tuningStepCombo_currentIndexChanged(int index); void on_serialDeviceListCombo_activated(const QString &arg1); void on_rptSetupBtn_clicked(); void on_attSelCombo_activated(int index); void on_preampSelCombo_activated(int index); void on_antennaSelCombo_activated(int index); void on_rxAntennaCheck_clicked(bool value); void on_wfthemeCombo_activated(int index); void on_rigPowerOnBtn_clicked(); void on_rigPowerOffBtn_clicked(); void on_ritTuneDial_valueChanged(int value); void on_ritEnableChk_clicked(bool checked); void on_band23cmbtn_clicked(); void on_band70cmbtn_clicked(); void on_band2mbtn_clicked(); void on_band4mbtn_clicked(); void on_band630mbtn_clicked(); void on_band2200mbtn_clicked(); void on_bandAirbtn_clicked(); void on_bandWFMbtn_clicked(); void on_rigCIVManualAddrChk_clicked(bool checked); void on_rigCIVaddrHexLine_editingFinished(); void on_baudRateCombo_activated(int); void on_wfLengthSlider_valueChanged(int value); void on_pollingBtn_clicked(); void on_wfAntiAliasChk_clicked(bool checked); void on_wfInterpolateChk_clicked(bool checked); void on_meter2selectionCombo_activated(int index); void on_enableRigctldChk_clicked(bool checked); void on_rigctldPortTxt_editingFinished(); void on_moreControlsBtn_clicked(); void on_useCIVasRigIDChk_clicked(bool checked); private: Ui::wfmain *ui; void closeEvent(QCloseEvent *event); QSettings *settings=Q_NULLPTR; void loadSettings(); void saveSettings(); QCustomPlot *plot; // line plot QCustomPlot *wf; // waterfall image QCPItemLine * freqIndicatorLine; //commHandler *comm; void setAppTheme(bool isCustom); void setPlotTheme(QCustomPlot *plot, bool isDark); void prepareWf(); void prepareWf(unsigned int wfLength); void showHideSpectrum(bool show); void getInitialRigState(); void setBandButtons(); void showButton(QPushButton *btn); void hideButton(QPushButton *btn); void openRig(); void powerRigOff(); void powerRigOn(); QStringList portList; QString serialPortRig; QShortcut *keyF1; QShortcut *keyF2; QShortcut *keyF3; QShortcut *keyF4; QShortcut *keyF5; QShortcut *keyF6; QShortcut *keyF7; QShortcut *keyF8; QShortcut *keyF9; QShortcut *keyF10; QShortcut *keyF11; QShortcut *keyF12; QShortcut *keyControlT; QShortcut *keyControlR; QShortcut *keyControlI; QShortcut *keyControlU; QShortcut *keyStar; QShortcut *keySlash; QShortcut *keyMinus; QShortcut *keyPlus; QShortcut *keyShiftMinus; QShortcut *keyShiftPlus; QShortcut *keyControlMinus; QShortcut *keyControlPlus; QShortcut *keyQuit; QShortcut *keyPageUp; QShortcut *keyPageDown; QShortcut *keyF; QShortcut *keyM; QShortcut *keyDebug; rigCommander * rig=Q_NULLPTR; QThread* rigThread = Q_NULLPTR; QCPColorMap * colorMap; QCPColorMapData * colorMapData; QCPColorScale * colorScale; QTimer * delayedCommand; QTimer * pttTimer; uint16_t loopTickCounter; uint16_t slowCmdNum; void setupPlots(); void makeRig(); void rigConnections(); void removeRig(); void findSerialPort(); void setupKeyShortcuts(); void setupMainUI(); void setUIToPrefs(); void setSerialDevicesUI(); void setAudioDevicesUI(); void setServerToPrefs(); void setInitialTiming(); void getSettingsFilePath(QString settingsFile); QStringList modes; int currentModeIndex; QStringList spans; QStringList edges; QStringList commPorts; QLabel* rigStatus; QLabel* rigName; QLedLabel* pttLed; QLedLabel* connectedLed; quint16 spectWidth; quint16 wfLength; bool spectrumDrawLock; QByteArray spectrumPeaks; QVector wfimage; unsigned int wfLengthMax; bool onFullscreen; bool drawPeaks; bool freqTextSelected; void checkFreqSel(); double oldLowerFreq; double oldUpperFreq; freqt freq; float tsKnobMHz; unsigned char setModeVal=0; unsigned char setFilterVal=0; enum cmds {cmdNone, cmdGetRigID, cmdGetRigCIV, cmdGetFreq, cmdSetFreq, cmdGetMode, cmdSetMode, cmdGetDataMode, cmdSetModeFilter, cmdSetDataModeOn, cmdSetDataModeOff, cmdGetRitEnabled, cmdGetRitValue, cmdSpecOn, cmdSpecOff, cmdDispEnable, cmdDispDisable, cmdGetRxGain, cmdSetRxRfGain, cmdGetAfGain, cmdSetAfGain, cmdGetSql, cmdSetSql, cmdGetIFShift, cmdSetIFShift, cmdGetTPBFInner, cmdSetTPBFInner, cmdGetTPBFOuter, cmdSetTPBFOuter, cmdGetATUStatus, cmdSetATU, cmdStartATU, cmdGetSpectrumMode, cmdGetSpectrumSpan, cmdScopeCenterMode, cmdScopeFixedMode, cmdGetPTT, cmdSetPTT, cmdGetTxPower, cmdSetTxPower, cmdGetMicGain, cmdSetMicGain, cmdSetModLevel, cmdGetSpectrumRefLevel, cmdGetDuplexMode, cmdGetModInput, cmdGetModDataInput, cmdGetCurrentModLevel, cmdStartRegularPolling, cmdStopRegularPolling, cmdQueNormalSpeed, cmdGetVdMeter, cmdGetIdMeter, cmdGetSMeter, cmdGetCenterMeter, cmdGetPowerMeter, cmdGetSWRMeter, cmdGetALCMeter, cmdGetCompMeter, cmdGetTxRxMeter, cmdGetTone, cmdGetTSQL, cmdGetDTCS, cmdGetRptAccessMode, cmdGetPreamp, cmdGetAttenuator, cmdGetAntenna, cmdSetTime, cmdSetDate, cmdSetUTCOffset}; struct commandtype { cmds cmd; std::shared_ptr data; }; std::deque delayedCmdQue; // rapid que for commands to the radio std::deque periodicCmdQueue; // rapid que for metering std::deque slowPollCmdQueue; // slow, regular checking for UI sync void doCmd(cmds cmd); void doCmd(commandtype cmddata); void issueCmd(cmds cmd, freqt f); void issueCmd(cmds cmd, mode_info m); void issueCmd(cmds cmd, timekind t); void issueCmd(cmds cmd, datekind d); void issueCmd(cmds cmd, int i); void issueCmd(cmds cmd, unsigned char c); void issueCmd(cmds cmd, char c); void issueCmd(cmds cmd, bool b); // These commands pop_front and remove similar commands: void issueCmdUniquePriority(cmds cmd, bool b); void issueCmdUniquePriority(cmds cmd, unsigned char c); void issueCmdUniquePriority(cmds cmd, char c); void issueCmdUniquePriority(cmds cmd, freqt f); void removeSimilarCommand(cmds cmd); qint64 lastFreqCmdTime_ms; int pCmdNum = 0; int delayedCmdIntervalLAN_ms = 100; int delayedCmdIntervalSerial_ms = 100; int delayedCmdStartupInterval_ms = 100; bool runPeriodicCommands; bool usingLAN = false; // Radio time sync: QTimer *timeSync; bool waitingToSetTimeDate; void setRadioTimeDatePrep(); timekind timesetpoint; timekind utcsetting; datekind datesetpoint; freqMemory mem; struct colors { QColor Dark_PlotBackground; QColor Dark_PlotAxisPen; QColor Dark_PlotLegendTextColor; QColor Dark_PlotLegendBorderPen; QColor Dark_PlotLegendBrush; QColor Dark_PlotTickLabel; QColor Dark_PlotBasePen; QColor Dark_PlotTickPen; QColor Dark_PeakPlotLine; QColor Dark_TuningLine; QColor Light_PlotBackground; QColor Light_PlotAxisPen; QColor Light_PlotLegendTextColor; QColor Light_PlotLegendBorderPen; QColor Light_PlotLegendBrush; QColor Light_PlotTickLabel; QColor Light_PlotBasePen; QColor Light_PlotTickPen; QColor Light_PeakPlotLine; QColor Light_TuningLine; } colorScheme; struct preferences { bool useFullScreen; bool useDarkMode; bool useSystemTheme; bool drawPeaks; bool wfAntiAlias; bool wfInterpolate; QString stylesheetPath; unsigned char radioCIVAddr; bool CIVisRadioModel; QString serialPortRadio; quint32 serialPortBaud; bool enablePTT; bool niceTS; bool enableLAN; bool enableRigCtlD; quint16 rigCtlPort; colors colorScheme; QString virtualSerialPort; unsigned char localAFgain; unsigned int wflength; int wftheme; bool confirmExit; bool confirmPowerOff; meterKind meter2Type; // plot scheme } prefs; preferences defPrefs; udpPreferences udpPrefs; udpPreferences udpDefPrefs; // Configuration for audio output and input. audioSetup rxSetup; audioSetup txSetup; colors defaultColors; void setDefaultColors(); // populate with default values void useColors(); // set the plot up void setDefPrefs(); // populate default values to default prefs void setTuningSteps(); quint64 roundFrequency(quint64 frequency, unsigned int tsHz); quint64 roundFrequencyWithStep(quint64 oldFreq, int steps,\ unsigned int tsHz); void setUIFreq(double frequency); void setUIFreq(); void changeTxBtn(); void issueDelayedCommand(cmds cmd); void issueDelayedCommandPriority(cmds cmd); void issueDelayedCommandUnique(cmds cmd); void changeSliderQuietly(QSlider *slider, int value); void statusFromSliderPercent(QString name, int percentValue); void statusFromSliderRaw(QString name, int rawValue); void processModLevel(rigInput source, unsigned char level); void processChangingCurrentModLevel(unsigned char level); void changeModLabel(rigInput source); void changeModLabel(rigInput source, bool updateLevel); void changeModLabelAndSlider(rigInput source); // Fast command queue: void initPeriodicCommands(); void insertPeriodicCommand(cmds cmd, unsigned char priority); void insertPeriodicCommandUnique(cmds cmd); void removePeriodicCommand(cmds cmd); void insertSlowPeriodicCommand(cmds cmd, unsigned char priority); void calculateTimingParameters(); void changeMode(mode_kind mode); void changeMode(mode_kind mode, bool dataOn); cmds meterKindToMeterCommand(meterKind m); int oldFreqDialVal; rigCapabilities rigCaps; rigInput currentModSrc = inputUnknown; rigInput currentModDataSrc = inputUnknown; mode_kind currentMode = modeUSB; mode_info currentModeInfo; bool haveRigCaps; bool amTransmitting; bool usingDataMode = false; unsigned char micGain=0; unsigned char accAGain=0; unsigned char accBGain=0; unsigned char accGain=0; unsigned char usbGain=0; unsigned char lanGain=0; calibrationWindow *cal; repeaterSetup *rpt; satelliteSetup *sat; transceiverAdjustments *trxadj; udpServerSetup *srv; aboutbox *abtBox; udpServer* udp = Q_NULLPTR; rigCtlD* rigCtl = Q_NULLPTR; QThread* serverThread = Q_NULLPTR; void bandStackBtnClick(); bool waitingForBandStackRtn; char bandStkBand; char bandStkRegCode; bool freqLock; float tsPlus; float tsPlusShift; float tsPlusControl; float tsPage; float tsPageShift; float tsWfScroll; unsigned int tsPlusHz; unsigned int tsPlusShiftHz; unsigned int tsPlusControlHz; unsigned int tsPageHz; unsigned int tsPageShiftHz; unsigned int tsWfScrollHz; unsigned int tsKnobHz; SERVERCONFIG serverConfig; }; Q_DECLARE_METATYPE(struct rigCapabilities) Q_DECLARE_METATYPE(struct freqt) Q_DECLARE_METATYPE(struct mode_info) Q_DECLARE_METATYPE(struct udpPreferences) Q_DECLARE_METATYPE(struct rigStateStruct) Q_DECLARE_METATYPE(struct audioPacket) Q_DECLARE_METATYPE(struct audioSetup) Q_DECLARE_METATYPE(struct timekind) Q_DECLARE_METATYPE(struct datekind) Q_DECLARE_METATYPE(enum rigInput) Q_DECLARE_METATYPE(enum meterKind) Q_DECLARE_METATYPE(enum spectrumMode) #endif // WFMAIN_H wfview-1.2d/wfmain.ui000066400000000000000000003077031415164626400146720ustar00rootroot00000000000000 wfmain 0 0 948 554 wfmain 3 View Spectrum Qt::Vertical 0 Spectrum Mode: Spectrum Mode Spectrum Mode Span: Spectrum Span QComboBox::AdjustToContents Edge Spectrum Edge QComboBox::AdjustToContents <html><head/><body><p>Press button to convert center mode spectrum to fixed mode, preserving the range. This allows you to tune without the spectrum moving, in the same currently-visible range that you see now. </p><p><br/></p><p>The currently-selected edge slot will be overriden.</p></body></html> ToFixed Clear Peaks Enable WF true Theme: Waterfall color theme Waterfall display color theme Selects the theme for the color waterfall dispaly Qt::Horizontal 40 20 0 0 0 0 0 0 190 0 145 30 DejaVu Sans 20 Current Frequency in MHZ 0000.000000 16777215 30 DejaVu Sans 20 MHz 0 10 10 0 0 16777215 80 <html><head/><body><p>Turns the radio on</p></body></html> Power On <html><head/><body><p>Turns the radio off</p></body></html> Power Off 0 0 0 60 60 60 60 Tuning Dial true true Tuning Step Selection possibly. Or not... Tuning Step Selection QComboBox::AdjustToContents Frequency Lock F Lock 30 30 R I T Dial -500 500 10 50 false 10.000000000000000 true R I T Enable RIT 0 16777215 30 Mode: Mode Selector QComboBox::AdjustToContents Data Mode Enable Data Receive Filter Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft Receive Filter Selection QComboBox::AdjustToContents Show additional controls Show More 0 0 70 16777215 80 RX RF Gain RF Gain Receiver RF Gain 255 Qt::Vertical 16777215 15 RF 0 0 70 16777215 80 RX AF Gain AF Gain Receive Audio Level. Sets rig volume on USB rigs, and sets PC volume on LAN rigs. 255 Qt::Vertical 16777215 15 AF 0 0 70 16777215 80 <html><head/><body><p>Squelch</p></body></html> Squelch Squelch control. Top is fully-muted, bottom is wide open. 255 Qt::Vertical 16777215 15 SQ 0 0 70 16777215 80 Transmit Power Transmit Power Transmit power level 255 Qt::Vertical 16777215 15 TX 0 0 70 16777215 80 Mic Gain Transmit Audio Gain Sets the gain for the transmit audio source, for example mic gain or accessory port gain 255 Qt::Vertical 16777215 15 Mic 0 0 70 16777215 80 Spectrum Reference Level Adjust Reference Level for Waterfall Reference level for the waterfall display -200 200 1 10 Qt::Vertical 16777215 15 Ref 0 0 70 16777215 80 Waterfall Length Waterfall Length 100 1024 160 Qt::Vertical 16777215 15 Len 0 0 0 50 Transmit and Receive button Transmit Enable the Automatic Antenna Tuner Enable ATU Enable or disable the automatic antenna tuner Enable ATU Start the automatic antenna tuner cycle Tune Show the repeater tone and offset window Repeater 0 0 0 0 Preamp/Att Preamp: Preamp selector QComboBox::AdjustToContents 0 0 Attenuator: Attenuator selector QComboBox::AdjustToContents 0 0 0 0 0 0 0 Antenna: Antenna port selector QComboBox::AdjustToContents false RX Qt::Horizontal 40 20 Band Band 0 0 0 0 16777215 128 23cm 0 0 16777215 128 70cm U 0 0 16777215 128 2m V 0 0 16777215 128 Air A 0 0 16777215 128 WFM W 0 0 16777215 128 4m $ 0 0 16777215 128 6m 6 0 0 16777215 128 10m 1 0 0 16777215 128 12m T 0 0 16777215 128 15m 5 0 0 16777215 128 17m 7 0 0 16777215 128 20m 2 0 0 16777215 128 30m 3 0 0 16777215 128 40m 4 0 0 16777215 128 60m S 0 0 16777215 128 80m 8 0 0 16777215 128 160m L 0 0 16777215 128 630m 0 0 16777215 128 2200m 0 0 16777215 128 Gen G 0 0 Segment &Last Used 16 16 true Band Stack Selection: 1 - Latest Used 2 - Older 3 - Oldest Used Voice Data &CW Qt::Horizontal 40 20 Frequency Frequency: DejaVu Sans Mono 14 75 true Go Return true Entry 0 0 30 30 5 5 0 0 0 30 <html><head/><body><p>To recall a preset memory:</p><p>1. Type in the preset number (0 through 99)</p><p>2. Press RCL (or use hotkey &quot;R&quot;)</p></body></html> &RCL R 0 0 30 30 6 6 0 0 30 30 3 3 false 0 0 30 30 &CE C 0 0 30 30 4 4 0 0 0 30 <html><head/><body><p>To store a preset:</p><p>1. Set the desired frequency and mode per normal methods</p><p>2. Type the index to to store to (0 through 99)</p><p>3. Press STO (or use hotkey &quot;S&quot;)</p></body></html> &STO S 0 0 30 30 9 9 0 0 0 30 Back Backspace 0 0 0 30 Enter Enter 0 0 30 30 0 0 0 0 30 30 . . 0 0 30 30 1 1 0 0 30 30 2 2 0 0 30 30 7 7 0 0 30 30 8 8 Settings Draw Peaks When tuning, set lower digits to zero true Qt::Horizontal 40 20 0 Use System Theme Waterfall Dark Theme Anti-Alias Waterfall Interpolate Waterfall true Show full screen Qt::Horizontal 40 20 0 <html><head/><body><p>Click here to adjust the frequency reference on the IC-9700.</p></body></html> Adjust Reference Satellite Ops Modulation Input: Modulation Input Transmit modulation source QComboBox::AdjustToContents Data Mod Input: Data Modulation Input Transmit Data-mode modulation input source QComboBox::AdjustToContents Qt::Horizontal 40 20 0 PTT On Ctrl+S PTT Off Enable PTT Controls Secondary Meter Selection: Qt::Horizontal 40 20 0 <html><head/><body><p>Select this option if the rig is pluged into the computer using a USB cable or a serial connection. </p></body></html> Connect over USB (serial) radioConnectionSerialNetworkGrp Serial Device: <html><head/><body><p>Select a serial port here. </p><p>Once selected, check &quot;Enable USB(serial), press &quot;Save Settings&quot;, exit, and re-start wfview. </p></body></html> Serial Device Selector Baud: <html><head/><body><p>Baud rate selection menu. </p><p>For the IC-7300 select 115200. </p><p>Older rigs may require other settings. </p><p>Be sure to match what baud rate the rig is set to. Using the highese supported baud rate for the radio is recommended. </p><p>Please press &quot;Save Settings&quot; and re-launc wfview for this to take effect.</p></body></html> baud rate baud rate selection menu <html><head/><body><p>Press here to set up the built-in rig server. The built-in server is intended to allow access over the network to a serial or USB-connected radio. </p></body></html> Server Setup <html><head/><body><p>If you are using an older (year 2010) radio, you may need to enable this option to manually specify the CI-V address. This option is also useful for radios that do not have CI-V Transceive enabled and thus will not answer our broadcast query for connected rigs on the CI-V bus.</p><p>If you have a modern radio with CI-V Transceive enabled, you should not need to check this box. </p><p>You will need to Save Settings and re-launch wfview for this to take effect. </p></body></html> Manual Radio CI-V Address: false 50 0 50 16777215 <html><head/><body><p>Enter the address in as hexidecimal, without any prefix, just as the radio presents the address in the menu. </p><p>Here are some common examples:</p> <p>IC-706: 58 <br/>IC-756: 50 <br/>IC-756 Pro: 5C <br/>IC-756 Pro II: 64 <br/>IC-756 Pro III: 6E <br/>IC-7000: 70 <br/>IC-7100: 88 <br/>IC-7200: 76 <br/>IC-7300: 94 </p><p>This setting is typically needed for older radios and for radios that do not have CI-V Transceive enabled. </p> <p>After changing, press Save Settings and re-launch wfview.</p></body></html> auto <html><head/><body><p>Only check for older radios!</p><p>This checkbox forces wfview to trust that the CI-V address is also the model number of the radio. This is only useful for older radios that do not reply to our Rig ID requests (0x19 0x00). Do not check this box unless you have an older radio. </p></body></html> Use as Model too Qt::Horizontal 40 20 <html><head/><body><p>Connection to the radio is via network. </p><p>This means you are connecting to a radio with native ethernet or wifi, such as the IC-705, IC-7610, IC-7850, IC-R8600, or IC-9700</p><p>You should also select this option if you are connecting to another instance of wfview over a network. </p></body></html> Connect over LAN radioConnectionSerialNetworkGrp Press here to initiate the network connection to the rig. Connect Qt::Horizontal 40 20 Qt::RightToLeft Enable RigCtld Port Qt::Horizontal 40 20 Virtual Serial Port <html><head/><body><p>Use this to define a virtual serial port. </p><p><br/></p><p>On Windows, the virtual serial port can be used to connect to a serial port loopback device, through which other programs can connect to the radio. </p><p><br/></p><p>On Linux and macOS, the port defined here is a pseudo-terminal device, which may be connected to directly by any program designed for a serial connection. </p></body></html> Virtual Serial Port Selector Radio IP Address Radio Control Port 50001 Username Password Qt::ImhNoAutoUppercase|Qt::ImhNoPredictiveText|Qt::ImhSensitiveData QLineEdit::PasswordEchoOnEdit RX Latency (ms) 0 0 30 500 Qt::Horizontal 0 TX Latency (ms) 30 500 Qt::Horizontal 0 RX Codec Receive Audio Codec Selector TX Codec Transmit Audio Codec Selector Sample Rate Audio Sample Rate Selector 48000 24000 16000 8000 Audio Output 300 16777215 Audio Output Selector Audio Input 300 16777215 Audio Input Selector Qt::Horizontal 40 20 0 <html><head/><body><p>This button runs debug functions, and is provided as a convenience for programmers. The functions executed are under:</p><p><span style=" color:#ffff55;">void</span><span style=" color:#55ff55;">wfmain</span><span style=" color:#aaaaaa;">::</span><span style=" font-weight:600;">on_debugBtn_clicked</span><span style=" color:#aaaaaa;">()</span></p><p>in wfmain.cpp.</p></body></html> Debug Ctrl+Alt+D Set up radio polling. The radio's meter is polled every-other interval. Polling Polling Qt::Horizontal 40 20 About Save Settings 75 true Exit Program 0 Please note: Changing the built-in network server requires pressing "Save Settings", closing wfview, and re-opening. 0 <html><head></head><body><p>Please see the <a href="https://wfview.org/wfview-user-manual/settings-tab/">User Manual</a> for more information. </p></body></html> Qt::RichText true Qt::Vertical 20 40 0 0 948 22 QCustomPlot 1 meter QWidget
meter.h
1
wfview-1.2d/wfview.code-workspace000066400000000000000000000001531415164626400171760ustar00rootroot00000000000000{ "folders": [ { "path": "." } ] } wfview-1.2d/wfview.pro000066400000000000000000000125041415164626400150730ustar00rootroot00000000000000#------------------------------------------------- # # Project created by QtCreator 2018-05-26T16:57:32 # #------------------------------------------------- QT += core gui serialport network multimedia greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport TARGET = wfview TEMPLATE = app DEFINES += WFVIEW_VERSION=\\\"1.2d\\\" CONFIG(debug, release|debug) { # For Debug builds only: QMAKE_CXXFLAGS += -faligned-new } else { # For Release builds only: linux:QMAKE_CXXFLAGS += -s QMAKE_CXXFLAGS += -fvisibility=hidden QMAKE_CXXFLAGS += -fvisibility-inlines-hidden QMAKE_CXXFLAGS += -faligned-new linux:QMAKE_LFLAGS += -O2 -s } # The following define makes your compiler emit warnings if you use # any feature of Qt which as been marked as deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the # deprecated API in order to know how to port your code away from it. DEFINES += QT_DEPRECATED_WARNINGS DEFINES += QCUSTOMPLOT_COMPILE_LIBRARY # These defines are used for the resampler equals(QT_ARCH, i386): DEFINES += USE_SSE equals(QT_ARCH, i386): DEFINES += USE_SSE2 equals(QT_ARCH, arm): DEFINES += USE_NEON DEFINES += OUTSIDE_SPEEX DEFINES += RANDOM_PREFIX=wf isEmpty(PREFIX) { PREFIX = /usr/local } DEFINES += PREFIX=\\\"$$PREFIX\\\" # Choose audio system, uses QTMultimedia if both are commented out. # DEFINES += RTAUDIO # DEFINES += PORTAUDIO contains(DEFINES, RTAUDIO) { # RTAudio defines win32:DEFINES += __WINDOWS_WASAPI__ #win32:DEFINES += __WINDOWS_DS__ # Requires DirectSound libraries linux:DEFINES += __LINUX_ALSA__ #linux:DEFINES += __LINUX_OSS__ #linux:DEFINES += __LINUX_PULSE__ macx:DEFINES += __MACOSX_CORE__ win32:SOURCES += ../rtaudio/RTAudio.cpp win32:HEADERS += ../rtaudio/RTAUdio.h !linux:INCLUDEPATH += ../rtaudio linux:LIBS += -lpulse -lpulse-simple -lrtaudio -lpthread } contains(DEFINES, PORTAUDIO) { CONFIG(debug, release|debug) { win32:LIBS += -L../portaudio/msvc/Win32/Debug/ -lportaudio_x86 } else { win32:LIBS += -L../portaudio/msvc/Win32/Release/ -lportaudio_x86 } win32:INCLUDEPATH += ../portaudio/include !win32:LIBS += -lportaudio } macx:INCLUDEPATH += /usr/local/include /opt/local/include macx:LIBS += -L/usr/local/lib -L/opt/local/lib macx:ICON = ../wfview/resources/wfview.icns win32:RC_ICONS = ../wfview/resources/wfview.ico QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.13 QMAKE_TARGET_BUNDLE_PREFIX = org.wfview MY_ENTITLEMENTS.name = CODE_SIGN_ENTITLEMENTS MY_ENTITLEMENTS.value = ../wfview/resources/wfview.entitlements QMAKE_MAC_XCODE_SETTINGS += MY_ENTITLEMENTS QMAKE_INFO_PLIST = ../wfview/resources/Info.plist !win32:DEFINES += HOST=\\\"`hostname`\\\" UNAME=\\\"`whoami`\\\" !win32:DEFINES += GITSHORT="\\\"$(shell git -C $$PWD rev-parse --short HEAD)\\\"" win32:DEFINES += GITSHORT=\\\"$$system(git -C $$PWD rev-parse --short HEAD)\\\" win32:DEFINES += HOST=\\\"wfview.org\\\" win32:DEFINES += UNAME=\\\"build\\\" RESOURCES += qdarkstyle/style.qrc \ resources/resources.qrc unix:target.path = $$PREFIX/bin INSTALLS += target # Why doesn't this seem to do anything? DISTFILES += resources/wfview.png \ resources/install.sh DISTFILES += resources/wfview.desktop unix:applications.files = resources/wfview.desktop unix:applications.path = $$PREFIX/share/applications INSTALLS += applications unix:pixmaps.files = resources/wfview.png unix:pixmaps.path = $$PREFIX/share/pixmaps INSTALLS += pixmaps unix:stylesheets.files = qdarkstyle unix:stylesheets.path = $$PREFIX/share/wfview INSTALLS += stylesheets # Do not do this, it will hang on start: # CONFIG(release, debug|release):DEFINES += QT_NO_DEBUG_OUTPUT CONFIG(debug, release|debug) { linux: QCPLIB = qcustomplotd win32:LIBS += -L../opus/win32/VS2015/Win32/Debug/ -lopus } else { linux: QCPLIB = qcustomplot win32:LIBS += -L../opus/win32/VS2015/Win32/Release/ -lopus } linux:LIBS += -L./ -l$$QCPLIB -lopus macx:LIBS += -framework CoreAudio -framework CoreFoundation -lpthread -lopus !linux:SOURCES += ../qcustomplot/qcustomplot.cpp !linux:HEADERS += ../qcustomplot/qcustomplot.h !linux:INCLUDEPATH += ../qcustomplot !linux:INCLUDEPATH += ../opus/include INCLUDEPATH += resampler SOURCES += main.cpp\ wfmain.cpp \ commhandler.cpp \ rigcommander.cpp \ freqmemory.cpp \ rigidentities.cpp \ udphandler.cpp \ logcategories.cpp \ audiohandler.cpp \ calibrationwindow.cpp \ satellitesetup.cpp \ udpserversetup.cpp \ udpserver.cpp \ meter.cpp \ qledlabel.cpp \ pttyhandler.cpp \ resampler/resample.c \ repeatersetup.cpp \ rigctld.cpp \ ring/ring.cpp \ transceiveradjustments.cpp \ aboutbox.cpp HEADERS += wfmain.h \ commhandler.h \ rigcommander.h \ freqmemory.h \ rigidentities.h \ udphandler.h \ logcategories.h \ audiohandler.h \ calibrationwindow.h \ satellitesetup.h \ udpserversetup.h \ udpserver.h \ packettypes.h \ meter.h \ qledlabel.h \ pttyhandler.h \ resampler/speex_resampler.h \ resampler/arch.h \ resampler/resample_sse.h \ repeatersetup.h \ repeaterattributes.h \ rigctld.h \ ulaw.h \ ring/ring.h \ transceiveradjustments.h \ audiotaper.h \ aboutbox.h FORMS += wfmain.ui \ calibrationwindow.ui \ satellitesetup.ui \ udpserversetup.ui \ repeatersetup.ui \ transceiveradjustments.ui \ aboutbox.ui wfview-1.2d/wfview.sln000066400000000000000000000042021415164626400150630ustar00rootroot00000000000000 Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30804.86 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wfview", "wfview.vcxproj", "{326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Win32 = Release|Win32 Release|x64 = Release|x64 Release|x86 = Release|x86 Template|Win32 = Template|Win32 Template|x64 = Template|x64 Template|x86 = Template|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Debug|Win32.ActiveCfg = Debug|Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Debug|Win32.Build.0 = Debug|Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Debug|x64.ActiveCfg = Debug|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Debug|x64.Build.0 = Debug|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Debug|x86.ActiveCfg = Debug|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Release|Win32.ActiveCfg = Release|Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Release|Win32.Build.0 = Release|Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Release|x64.ActiveCfg = Release|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Release|x64.Build.0 = Release|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Release|x86.ActiveCfg = Release|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Template|Win32.ActiveCfg = Debug|Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Template|Win32.Build.0 = Debug|Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Template|x64.ActiveCfg = Release|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Template|x64.Build.0 = Release|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Template|x86.ActiveCfg = Release|x64 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Template|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DED7FD3A-2DD6-4C9E-B809-BE03F4C13F69} EndGlobalSection EndGlobal wfview-1.2d/wfview.vcxproj000066400000000000000000000532551415164626400157760ustar00rootroot00000000000000 Release Win32 Debug Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2} wfview QtVS_v304 10.0.19041.0 10.0.19041.0 $(MSBuildProjectDirectory)\QtMsBuild v142 release\ false NotSet Application release\ wfview v142 debug\ false NotSet Application debug\ wfview debug\debug\wfviewtruerelease\release\wfviewtruefalsemsvc2019core;network;gui;multimedia;widgets;serialport;printsupportmsvc2019core;network;gui;multimedia;widgets;serialport;printsupport .;..\qcustomplot;..\opus\include;resampler;rtaudio;release;/include;%(AdditionalIncludeDirectories) -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) release\ false None 4577;4467;%(DisableSpecificWarnings) Sync release\ MaxSpeed _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;PREFIX="/usr/local";__WINDOWS_WASAPI__;GITSHORT="66912e1";HOST="wfview.org";UNAME="build";NDEBUG;QT_NO_DEBUG;%(PreprocessorDefinitions) false MultiThreadedDLL true true Level3 true ..\opus\win32\VS2015\Win32\Release\opus.lib;shell32.lib;%(AdditionalDependencies) ..\opus\win32\VS2015\Win32\Release;C:\opensslx86\lib;C:\Utils\my_sql\mysql-5.7.25-win32\lib;C:\Utils\postgresqlx86\pgsql\lib;%(AdditionalLibraryDirectories) "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions) true false true false true $(OutDir)\wfview.exe true Windows true Unsigned None 0 _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;PREFIX=\"/usr/local\";__WINDOWS_WASAPI__;GITSHORT=\"66912e1\";HOST=\"wfview.org\";UNAME=\"build\";NDEBUG;QT_NO_DEBUG;QT_MULTIMEDIA_LIB;QT_PRINTSUPPORT_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_SERIALPORT_LIB;QT_NETWORK_LIB;QT_CORE_LIB;%(PreprocessorDefinitions) msvc./$(Configuration)/moc_predefs.hMoc'ing %(Identity)...output$(Configuration)moc_%(Filename).cppdefaultRcc'ing %(Identity)...$(Configuration)qrc_%(Filename).cppUic'ing %(Identity)...$(ProjectDir)ui_%(Filename).h .;..\qcustomplot;..\opus\include;resampler;rtaudio;debug;/include;%(AdditionalIncludeDirectories) -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) debug\ false ProgramDatabase 4577;4467;%(DisableSpecificWarnings) Sync debug\ Disabled _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;PREFIX="/usr/local";__WINDOWS_WASAPI__;GITSHORT="66912e1";HOST="wfview.org";UNAME="build";%(PreprocessorDefinitions) false MultiThreadedDebugDLL true true Level3 true ..\opus\win32\VS2015\Win32\Debug\opus.lib;shell32.lib;%(AdditionalDependencies) ..\opus\win32\VS2015\Win32\Debug;C:\opensslx86\lib;C:\Utils\my_sql\mysql-5.7.25-win32\lib;C:\Utils\postgresqlx86\pgsql\lib;%(AdditionalLibraryDirectories) "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions) true true true $(OutDir)\wfview.exe true Windows true Unsigned None 0 _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;PREFIX=\"/usr/local\";__WINDOWS_WASAPI__;GITSHORT=\"66912e1\";HOST=\"wfview.org\";UNAME=\"build\";QT_MULTIMEDIA_LIB;QT_PRINTSUPPORT_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_SERIALPORT_LIB;QT_NETWORK_LIB;QT_CORE_LIB;_DEBUG;%(PreprocessorDefinitions) msvc./$(Configuration)/moc_predefs.hMoc'ing %(Identity)...output$(Configuration)moc_%(Filename).cppdefaultRcc'ing %(Identity)...$(Configuration)qrc_%(Filename).cppUic'ing %(Identity)...$(ProjectDir)ui_%(Filename).h Document true $(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs) cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -faligned-new -Zi -MDd -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2>NUL >debug\moc_predefs.h Generate moc_predefs.h debug\moc_predefs.h;%(Outputs) Document $(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs) cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -fvisibility=hidden -fvisibility-inlines-hidden -faligned-new -O2 -MD -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2>NUL >release\moc_predefs.h Generate moc_predefs.h release\moc_predefs.h;%(Outputs) true resourcesresources stylestyle wfview-1.2d/wfview.vcxproj.filters000066400000000000000000000321631415164626400174400ustar00rootroot00000000000000 {99349809-55BA-4b9d-BF79-8FDBB0286EB3} ui false {99349809-55BA-4b9d-BF79-8FDBB0286EB3} ui false {71ED8ED8-ACB9-4CE9-BBE1-E00B30144E11} cpp;c;cxx;moc;h;def;odl;idl;res; {71ED8ED8-ACB9-4CE9-BBE1-E00B30144E11} cpp;c;cxx;moc;h;def;odl;idl;res; {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hpp;hxx;hm;inl;inc;xsd {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hpp;hxx;hm;inl;inc;xsd {D9D6E242-F8AF-46E4-B9FD-80ECBC20BA3E} qrc;* false {D9D6E242-F8AF-46E4-B9FD-80ECBC20BA3E} qrc;* false {4FC737F1-C7A5-4376-A066-2A32D752A2FF} cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx {4FC737F1-C7A5-4376-A066-2A32D752A2FF} cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx {B83CAF91-C7BF-462F-B76C-EA11631F866C} * false {B83CAF91-C7BF-462F-B76C-EA11631F866C} * false Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Source Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Generated Files Generated Files Form Files Form Files Form Files Form Files Form Files Form Files Form Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files Distribution Files Distribution Files Distribution Files wfview-1.2d/wfview.vcxproj.user000066400000000000000000000012231415164626400167370ustar00rootroot00000000000000 PATH=$(QTDIR)\bin%3bC:\QT\5.15.2\MSVC2019\bin%3b$(QTDIR)\bin%3bC:\QT\5.15.2\MSVC2019\bin%3b$(PATH) PATH=$(QTDIR)\bin%3bC:\QT\5.15.2\MSVC2019\bin%3b$(QTDIR)\bin%3bC:\QT\5.15.2\MSVC2019\bin%3b$(PATH) wfview-1.2d/wfview_resource.rc000066400000000000000000000014071415164626400166060ustar00rootroot00000000000000#include IDI_ICON1 ICON DISCARDABLE "C:\\Users\\Phil\\source\\repos\\wfview\\resources\\wfview.ico" VS_VERSION_INFO VERSIONINFO FILEVERSION 0,0,0,0 PRODUCTVERSION 0,0,0,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_DLL FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "\0" VALUE "FileDescription", "\0" VALUE "FileVersion", "0.0.0.0\0" VALUE "LegalCopyright", "\0" VALUE "OriginalFilename", "wfview.exe\0" VALUE "ProductName", "wfview\0" VALUE "ProductVersion", "0.0.0.0\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x0409, 1200 END END /* End of Version info */