├── LICENSE ├── README.md ├── common.pm ├── heatmap.pl ├── img ├── heatmap-example1.png └── rangeview-example1.png ├── install.pl ├── rangeview.pl ├── socket30003.cfg └── socket30003.pl /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dump1090.socket30003 2 | 3 | ## Table of contents 4 | 5 | * [dump1090.socket30003](#dump1090socket30003) 6 | * [Collect flight data for heatmap and rangview](#collect-flight-data-for-heatmap-and-rangview) 7 | * [Screenshots and video](#screenshots-and-video) 8 | * [The scripts](#the-scripts) 9 | * [Help page socket30003.pl](#help-page-socket30003pl) 10 | * [Output socket30003.pl](#output-socket30003pl) 11 | * [Help page heatmap.pl](#help-page-heatmappl) 12 | * [Output heatmap.pl](#output-heatmappl) 13 | * [Help page rangeview.pl](#help-page-rangeviewpl) 14 | * [Output rangeview.pl](#output-rangeviewpl) 15 | * [Help page install.pl](#help-page-installpl) 16 | * [Installation](#installation) 17 | * [Clone this repo](#clone-this-repo) 18 | * [Edit config file](#edit-config-file) 19 | * [Run installer](#run-installer) 20 | * [Add socket30003.pl as a crontab job](#add-socket30003pl-as-a-crontab-job) 21 | * [Check the log and output file](#check-the-log-and-output-file) 22 | * [Run heatmap.pl](#run-heatmappl) 23 | * [Run rangview.pl](#run-rangviewpl) 24 | * [View the heatmap](#view-the-heatmap) 25 | * [View the rangeview](#view-the-rangeview) 26 | * [More info](#more-info) 27 | 28 | ## Collect flight data for heatmap and rangview 29 | 30 | Use your dump1090 to collect flight data and create a heatmap and rangeview. 31 | 32 | ## Screenshots and video 33 | 34 | Heatmap. What are the flight paths through the air. 35 | [![Dump1090 Heatmap](https://raw.githubusercontent.com/tedsluis/dump1090.socket30003/master/img/heatmap-example1.png)](https://raw.githubusercontent.com/tedsluis/dump1090.socket30003/master/img/heatmap-example1.png) 36 | 37 | Rangeview. How far is the reach of your antenna. 38 | [![Dump1090 rangeview](https://raw.githubusercontent.com/tedsluis/dump1090.socket30003/master/img/rangeview-example1.png)](https://raw.githubusercontent.com/tedsluis/dump1090.socket30003/master/img/rangeview-example1.png) 39 | 40 | Youtube video: 41 | [![Dump1090 rangeview](https://raw.githubusercontent.com/tedsluis/dump1090/master/img/youtube16.png)](https://www.youtube.com/watch?v=Qz4XSFRjLTI) 42 | 43 | ## The scripts 44 | 45 | socket30003.pl 46 | * Collects dump1090 flight positions (ADB-S format) using a tcp socket 30003 stream 47 | and save them in csv format. 48 | 49 | heatmap.pl 50 | * Reads the flight positions from files in csv format and creates points for a heatmap. 51 | * The heatmap shows where planes come very often. It makes common routes visable. 52 | * Output in csv format (Google maps format). 53 | 54 | rangeview.pl 55 | * Reads the flight positions from files in csv format and creates a range/altitude view map. 56 | * The range/altitude view shows the maximum range of your antenna for every altitude zone. 57 | * KML output support (Google maps format). 58 | 59 | install.pl 60 | * Simpel installer script. 61 | 62 | socket30003.cfg 63 | * config file for socket30003.pl, heatmap.pl, rangeview.pl and install.pl 64 | 65 | The output heatmapdata.csv and rangeview.kml can be displayed in a my modified variant 66 | of dump1090-mutability: https://github.com/tedsluis/dump1090 67 | 68 | Read more about this at: 69 | http://discussions.flightaware.com/topic35844.html 70 | 71 | ## Help page socket30003.pl 72 | ```` 73 | This socket30003.pl script can retrieve flight data (like lat, lon and alt) 74 | from a dump1090 host using port 30003 and calcutates the distance and 75 | angle between the antenna and the plane. It will store these values in an 76 | output file in csv format (seperated by commas) together with other flight 77 | data. 78 | 79 | This script can run several times simultaneously on one host retrieving 80 | data from multiple dump1090 instances on different hosts. Each instance 81 | can use the same directories, but they all have their own data, log and 82 | pid files. And every day the script will create a new data and log file. 83 | 84 | A data files contain column headers (with the names of the columns). 85 | Columns headers like 'altitude', 'distance' and 'ground_speed' also contain 86 | their unit between parentheses, for example '3520(feet)' or '12,3(kilometer)'. 87 | This makes it more easy to parse the columns when using this data in other 88 | scripts. Every time the script is (re)started a header wiil be written 89 | in to the data file. This way it is possible to switch a unit, for 90 | example from 'meter' to 'kilometer', and other scripts will still be able 91 | to determine the correct unit type. 92 | 93 | By default the position data, log files and pid file(s) will be stored in this format: 94 | dump1090--.txt 95 | dump1090--.log 96 | dump1090-.pid 97 | 98 | The script can be lauched as a background process. It can be stopped by 99 | using the -stop parameter or by removing the pid file. When it not 100 | running as a background process, it can also be stopped by pressing 101 | CTRL-C. The script will write the current data and log entries to the 102 | filesystem before exiting... 103 | 104 | More info at: 105 | http://discussions.flightaware.com/post180185.html#p180185 106 | 107 | Syntax: socket30003.pl 108 | 109 | Optional parameters: 110 | -peer A dump1090 hostname or IP address. 111 | De default is the localhost, 127.0.0.1. 112 | -restart Restart the script. 113 | -stop Stop a running script. 114 | -status Display status. 115 | -data The data files are stored in /tmp by default. 116 | -log The log file is stored in /tmp by default. 117 | -pid The pid file is stored in /tmp by default. 118 | -msgmargin The max message margin. The default is 10 ms. 119 | -lon Location of your antenna. 120 | -lat 121 | -distanceunit Type of unit for distance: kilometer, 122 | nauticalmile, mile or meter 123 | Default distance unit is kilometer. 124 | -altitudeunit Type of unit for altitude: meter or feet. 125 | Default altitude unit is meter. 126 | -speedunit Type of unit for ground speed. 127 | Default speed unit is kilometerph. 128 | -nopositions Does not display the number of position while 129 | running interactive (launched from commandline). 130 | -debug Displays raw socket messages. 131 | -verbose Displays verbose log messages. 132 | -help This help page. 133 | 134 | Notes: 135 | - To launch it as a background process, add '&' or run it from crontab: 136 | 0 * * * * /home/tedsluis/git/dump1090.socket30003/socket30003.pl 137 | (This command checks if it ran every hour and relauch it if nessesary.) 138 | - The default values can be changed within the config file 'socket30003.cfg', 139 | section [common] and/or [socket30003]. 140 | 141 | Examples: 142 | socket30003.pl 143 | socket30003.pl -log /var/log -data /home/pi -pid /var/run -restart & 144 | socket30003.pl -peer 192.168.1.10 -nopositions -distanceunit nauticalmile -altitudeunit feet & 145 | socket30003.pl -peer 192.168.1.10 -stop 146 | 147 | Pay attention: to stop an instance: Don't forget to specify the same peer host. 148 | ```` 149 | ### Output socket30003.pl 150 | * Default outputfile: /tmp/dump1090-192_168_11_34-150830.txt (dump1090--.txt) 151 | ```` 152 | hex_ident,altitude(meter),latitude,longitude,date,time,angle,distance(kilometer),squawk,ground_speed(kilometerph),track,callsign 153 | 484CB8,3906,52.24399,5.25500,2017/01/09,16:35:02.113,45.11,20.93,0141,659,93,KLM1833 154 | 406D77,11575,51.09984,7.73237,2017/01/09,16:35:02.129,111.12,212.94,,,,BAW256 155 | 4CA1D4,11270,53.11666,6.02148,2017/01/09,16:35:03.464,40.85,130.79,,842,81,RYR89VN 156 | 4B1A1B,3426,51.86971,4.14556,2017/01/09,16:35:03.489,-103.38,68.93,1000,548,352,EZS85TP 157 | 4CA79D,11575,51.95681,4.17119,2017/01/09,16:35:03.489,-98.28,64.41,1366,775,263,RYR43FH 158 | hex_ident,altitude(feet),latitude,longitude,date,time,angle,distance(mile),squawk,ground_speed(mileph),track,callsign 159 | 48500,1416,52.53923,4.95834,2017/01/09,16:41:40.885,-15.42,51.21,1000,377,279,TRA5802 160 | 478690,11141,50.66931,3.43764,2017/01/09,16:41:40.886,-131.71,194.76,5325,542,37,SAS22K 161 | 34260E,10966,51.77884,5.07965,2017/01/09,16:41:40.888,-178.31,34.11,1114,522,214,IBE32HP 162 | 484558,5071,52.48020,4.22715,2017/01/09,16:41:40.892,-64.42,73.22,6260,459,303,KLM55U 163 | 4951B7,11270,52.78214,2.43583,2017/01/09,16:41:40.901,-74.55,195.81,2375,442,40,TAP766 164 | ```` 165 | note: As you can see it is possible to switch over to different type units for 'altitude', 'distance' and 'ground speed'! 166 | 167 | ## Help page heatmap.pl 168 | ```` 169 | This heatmap.pl script creates heatmap data 170 | which can be displated in a modified variant of dump1090-mutobility. 171 | 172 | It creates an output file with location data in csv format, which can 173 | be imported using the dump1090 GUI. 174 | 175 | Please read this post for more info: 176 | http://discussions.flightaware.com/post180185.html#p180185 177 | 178 | This script uses the data file(s) created by the 'socket30003.pl' 179 | script, which are by default stored in '/tmp' in this format: 180 | dump1090--YYMMDD.txt 181 | 182 | The script will automaticly use the correct units (feet, meter, 183 | kilometer, mile, natical mile) for 'altitude' and 'distance' when 184 | the input files contain column headers with the unit type between 185 | parentheses. When the input files doesn't contain column headers 186 | (as produced by older versions of 'socket30003.pl' script) 187 | you can specify the units using startup parameters or in the config 188 | file. Otherwise this script will use the default units. 189 | 190 | This script will create a heatmap of a square area around your 191 | antenna. You can change the default range by specifing the number 192 | of degrees -/+ to your antenna locations. (The default values will 193 | probably satisfy.) This area will be devided in to small squares. 194 | The default heatmap has a resolution of 1000 x 1000 squares. 195 | The script will read all the flight position data from the source 196 | file(s) and count the times they match with a square on the heatmap. 197 | 198 | The more positions match with a particular square on the heatmap, 199 | the more the 'weight' that heatmap position gets. We use only the 200 | squares with the most matches (most 'weight) to create the heatmap. 201 | This is because the map in the browser gets to slow when you use 202 | too much positions in the heatmap. Of cource this also depends on 203 | the amount of memory of your system. You can change the default 204 | number of heatmap positions. You can also set the maximum of 205 | 'weight' per heatmap position. 206 | 207 | Syntax: heatmap.pl 208 | 209 | Optional parameters: 210 | -data The data files are stored in 211 | '/tmp' by default. 212 | -log The data files are stored in 213 | '/tmp' by default. 214 | -output '/tmp' by default. 216 | -file The output file name. 217 | 'heatmapdata.csv' by default. 218 | -filemask Specify a filemask for the source data. 219 | The default filemask is 'dump*.txt'. 220 | -override Override output file if exists. 221 | Default is 'no'. 222 | -timestamp Add timestamp to output file name. 223 | Default is 'no'. 224 | -sequencenumber Add sequence number to output file name. 225 | Default is 'no'. 226 | -lon Location of your antenna. 227 | -lat 228 | -maxpositions '100000' positions. 230 | -maxweight Maximum position weight. The default is 231 | '1000'. 232 | -resolution Number of horizontal and vertical positions 233 | in output file. Default is '1000', 234 | which means '1000x1000' positions. 235 | -degrees To determine boundaries of area around the 236 | antenna. (lat-degree <--> lat+degree) x 237 | (lon-degree <--> lon+degree) 238 | De default is '5' degree. 239 | -debug Displays raw socket messages. 240 | -verbose Displays verbose log messages. 241 | -help This help page. 242 | 243 | note: 244 | The default values can be changed within the config file 245 | 'socket30003.cfg', section [common] and section [heatmap]. 246 | 247 | Examples: 248 | heatmap.pl 249 | heatmap.pl -data /home/pi -log /var/log 250 | heatmap.pl -lat 52.1 -lon 4.1 -maxposition 50000 251 | ```` 252 | ### Output heatmap.pl 253 | * Default output file: /tmp/heatmapdata.csv 254 | ```` 255 | "weight";"lat";"lon" 256 | "1000";"52.397";"4.721" 257 | "919";"52.389";"4.721" 258 | "841";"52.405";"4.721" 259 | "753";"52.413";"4.721" 260 | "750";"52.517";"5.297" 261 | "743";"52.317";"5.177" 262 | "679";"51.925";"2.849" 263 | "641";"51.853";"6.065" 264 | "609";"51.229";"3.649" 265 | ```` 266 | ## Help page rangeview.pl 267 | ```` 268 | This rangeview.pl script creates location data 269 | for a range/altitude view which can be displated in a modified 270 | fork of dump1090-mutobility. 271 | 272 | The script creates two output files: 273 | rangeview.csv) A file with location data in csv format can be 274 | imported in to tools like http://www.gpsvisualizer.com. 275 | rangeview.kml) A file with location data in kml format, which 276 | can be imported into a modified dum1090-mutability. 277 | 278 | Please read this post for more info: 279 | http://discussions.flightaware.com/post180185.html#p180185 280 | 281 | This script uses the output file(s) of the 'socket30003.pl' 282 | script, which are by default stored in /tmp in this format: 283 | dump1090--YYMMDD.txt 284 | 285 | It will read the files one by one and it will automaticly use 286 | the correct units (feet, meter, mile, nautical mile of kilometer) 287 | for 'altitude' and 'distance' when the input files contain 288 | column headers with the unit type between parentheses. When 289 | the input files doesn't contain column headers (as produced 290 | by older versions of 'socket30003.pl' script) you can specify 291 | the units.Otherwise this script will use the default units. 292 | 293 | The flight position data is sorted in to altitude zones. For 294 | each zone and for each direction the most remote location is 295 | saved. The most remote locations per altitude zone will be 296 | written to a file as a track. 297 | 298 | Syntax: rangeview.pl 299 | 300 | Optional parameters: 301 | -data The data files are stored in 302 | '/tmp' by default. 303 | -log The log files are stored in 304 | '/tmp' by default. 305 | -output '/tmp' by default. 307 | -file The output file name. The extention 308 | (.kml or .csv) determines the 309 | file structure! 310 | 'rangeview.kml' by default. 311 | -filemask Specify a filemask. 312 | The default filemask is 'dump*.txt'. 313 | -override Override output file if exists. 314 | Default is 'no'. 315 | -timestamp Add timestamp to output file name. 316 | Default is 'no'. 317 | -sequencenumber Add sequence number to output file name. 318 | Default is 'no'. 319 | -max Upper limit. Default is '12000 meter'. 320 | Higher values in the input data will be skipped. 321 | -min Lower limit. Default is '0 meter'. 322 | Lower values in the input data will be skipped. 323 | -directions Number of compass direction (pie slices). 324 | Minimal 8, maximal 7200. Default = '1440'. 325 | -zones Number of altitude zones. 326 | Minimal 1, maximum 99. 327 | Default = '24'. 328 | -lon Location of your antenna. 329 | -lat 330 | -distanceunit ,[] 331 | Type of unit: kilometer, nauticalmile, 332 | mile or meter. First unit is for the 333 | incoming source, the file(s) with flight 334 | positions. The second unit is for the 335 | output file. No unit means it is the 336 | same as incoming. 337 | Default distance unit's are: 338 | 'kilometer,kilometer'. 339 | -altitudeunit [,] 340 | Type of unit: feet or meter. First unit 341 | is for the incoming source, the file(s) 342 | with flight positions. The second unit 343 | is for the output file. No unit means it 344 | is the same as incoming. 345 | Default altitude unit's are: 346 | 'meter,meter'. 347 | -debug Displays raw socket messages. 348 | -verbose Displays verbose log messages. 349 | -help This help page. 350 | 351 | notes: 352 | - The default values can be changed within the config file 'socket30003.cfg'. 353 | - The source units will be overruled in case the input file header contains unit information. 354 | 355 | Examples: 356 | rangeview.pl 357 | rangeview.pl -distanceunit kilometer,nauticalmile -altitudeunit meter,feet 358 | rangeview.pl -data /home/pi/data -log /home/pi/log -output /home/pi/result 359 | 360 | ```` 361 | ### Output rangeview.pl 362 | * Default output file: /tmp/rangeview.csv 363 | ```` 364 | type,new_track,name,color,trackpoint,altitudezone,destination,hex_ident,Altitude(meter),latitude,longitude,date,time,angle,distance(kilometer) 365 | T,1,Altitude zone 1: 00000- 500,7fffff00,1, 0,-718,484646,357,52.00493,5.08865,2017/01/10,10:46:15.738,-179.72,8 366 | T,0,Altitude zone 1: 00000- 500,7fffff00,2, 0,-717,484646,357,52.00616,5.08808,2017/01/10,10:46:17.164,-179.32,8 367 | T,0,Altitude zone 1: 00000- 500,7fffff00,3, 0,-714,484646,357,52.00788,5.08722,2017/01/10,10:46:19.740,-178.7,8 368 | T,0,Altitude zone 1: 00000- 500,7fffff00,4, 0,-713,484646,357,52.00914,5.08667,2017/01/10,10:46:21.041,-178.28,8 369 | T,0,Altitude zone 1: 00000- 500,7fffff00,5, 0,-711,484646,357,52.01039,5.08604,2017/01/10,10:46:22.622,-177.79,8 370 | T,0,Altitude zone 1: 00000- 500,7fffff00,6, 0,-709,484646,357,52.01125,5.08560,2017/01/10,10:46:23.892,-177.44,8 371 | T,0,Altitude zone 1: 00000- 500,7fffff00,7, 0,-708,484646,357,52.01230,5.08518,2017/01/10,10:46:25.244,-177.09,8 372 | T,0,Altitude zone 1: 00000- 500,7fffff00,8, 0,-706,484646,357,52.01335,5.08461,2017/01/10,10:46:26.625,-176.62,8 373 | T,0,Altitude zone 1: 00000- 500,7fffff00,9, 0,-704,484646,357,52.01463,5.08400,2017/01/10,10:46:28.031,-176.09,7 374 | T,0,Altitude zone 1: 00000- 500,7fffff00,10, 0,-702,484646,357,52.01579,5.08345,2017/01/10,10:46:29.475,-175.59,7 375 | T,0,Altitude zone 1: 00000- 500,7fffff00,11, 0,-700,484646,357,52.01683,5.08293,2017/01/10,10:46:30.940,-175.11,7 376 | ```` 377 | 378 | * Default output file: /tmp/rangeview.kml 379 | ```` 380 | 381 | 382 | 383 | Paths 384 | Example 385 | 394 | 395 | 1 396 | 00000- 500 397 | #track-1 398 | 399 | absolute 400 | 401 | 5.08865,52.00493,357 402 | 5.08808,52.00616,357 403 | 5.08722,52.00788,357 404 | 5.08667,52.00914,357 405 | 5.08604,52.01039,357 406 | 5.08560,52.01125,357 407 | 5.08518,52.01230,357 408 | 5.08461,52.01335,357 409 | 5.08400,52.01463,357 410 | 5.08345,52.01579,357 411 | 5.08293,52.01683,357 412 | etc.. 413 | ```` 414 | ## Help page install.pl 415 | 416 | ```` 417 | This install.pl script installs the socket30003 scripts. 418 | It will create the directories, copy the files, check and set the permissions. 419 | In case of an update, it will backup the original config file and add new 420 | parameters if applicable. 421 | 422 | Please read this post for more info: 423 | http://discussions.flightaware.com/post180185.html#p180185 424 | 425 | Syntax: install.pl 426 | 427 | Optional parameters: 428 | -install '/home/pi/socket30003' by default. 430 | -data The data files will be stored in 431 | '/tmp' by default. 432 | -log The log files will be stored in 433 | '/tmp' by default. 434 | -output '' by default. 436 | -pid The pid files will be stored in 437 | '/tmp'. 438 | -debug Displays raw socket messages. 439 | -verbose Displays verbose log messages. 440 | -help This help page. 441 | 442 | Examples: 443 | install.pl 444 | install.pl -install /user/share/socket30003 445 | install.pl -data /home/pi/data -log /home/pi/log -output /home/pi/result 446 | 447 | ```` 448 | ## Installation 449 | 450 | Follow these steps for the use of the scripts. 451 | 452 | ### Clone this repo 453 | ```` 454 | $ cd 455 | $ mkdir git 456 | $ cd git 457 | $ git clone https://github.com/tedsluis/dump1090.socket30003.git 458 | ```` 459 | note: be sure 'git' is installed (sudo apt-get install git) 460 | 461 | ### Edit config file 462 | 463 | The best thing is to leave it as much as it is. May change some directories of the unit types (meters, kilometers, miles, feets, nautical miles etc). 464 | ```` 465 | $ cd dump1090.socket30003 466 | $ vi socket.cfg (or use an other editor) 467 | ```` 468 | 469 | ### Run installer 470 | 471 | Files wil be copied to the install directory. 472 | 473 | ```` 474 | $ ./install.pl 475 | ```` 476 | The files are now installed in the install directory from where you should use the scripts. 477 | 478 | ### Add socket30003.pl as a crontab job 479 | 480 | Of cource you can run 'socket30003.pl' from teh commandline, but if you want to leave it running for days or weeks you should add it as a crontab job. 481 | ```` 482 | $ sudo crontab -e 483 | 484 | */5 * * * * sudo /home/pi/socket30003/socket30003.pl 485 | ```` 486 | note: script will start every 5 minutes if it is not running anymore. 487 | 488 | ### Check the log and output file 489 | 490 | Wait at least a view minutes and then: 491 | ```` 492 | $ cat /tmp/*dump1090*.log | less 493 | $ cat /tmp/*dump1090*.txt | less 494 | ```` 495 | 496 | ### Run heatmap.pl 497 | 498 | Process the flight data and create a heatmap. 499 | 500 | Wait a couple of days to be sure you have enough data. 501 | ```` 502 | $ cd 503 | $ cd socket30003 504 | $ ./heatmap 505 | 10Jan17 17:20:32 pid=6403 I pi Log file: '/tmp/heatmap-170110.log' 506 | 10Jan17 17:20:32 pid=6403 I pi There will be no more then '50000' positions in the output file. 507 | 10Jan17 17:20:32 pid=6403 I pi The maximum position weight will be not more then '1000'. 508 | 10Jan17 17:20:32 pid=6403 I pi Output file: '/tmp/heatmapdata.csv' 509 | 10Jan17 17:20:32 pid=6403 I pi The resolution op the heatmap will be 1000x1000. 510 | 10Jan17 17:20:32 pid=6403 I pi The antenna latitude & longitude are: '52.01','5.01'. 511 | 10Jan17 17:20:32 pid=6403 I pi The heatmap will cover the area of 5 degree around the antenna, which is between latitude 47 - 57 and longitude 0 - 10. 512 | 10Jan17 17:20:32 pid=6403 I pi The following files in directory '/tmp' fit with the filemask '*dump*.txt*': 513 | 10Jan17 17:20:32 pid=6403 I pi /tmp/dump1090-ted1090-5-170110.txt 514 | 10Jan17 17:20:32 pid=6403 I pi /tmp/dump1090-ted1090-5-170109.txt 515 | 10Jan17 17:20:32 pid=6403 I pi Processing file '/tmp/dump1090-ted1090-5-170110.txt': 516 | 10Jan17 17:22:12 pid=6403 I pi -header units:altitude=meter,distance=kilometer,ground_speed=kilometerph, position 1-1369896 processed. 339 positions were out side the specified area. 517 | 10Jan17 17:22:12 pid=6403 I pi Processing file '/tmp/dump1090-ted1090-5-170109.txt': 518 | 10Jan17 17:22:55 pid=6403 I pi -header units:altitude=meter,distance=kilometer,ground_speed=kilometerph, position 1-580075 processed. 118 positions were out side the specified area. 519 | 10Jan17 17:22:57 pid=6403 I pi Number of sorted heatmap positions: 208056 520 | 10Jan17 17:23:02 pid=6403 I pi The highest weight is '00001259' and the lowest weight is '00000010'. 521 | 10Jan17 17:23:02 pid=6403 I pi Since the highest weight is more the the max weight '1000' the weight of all points will be multiplied with a factor 0.79428117553614. 522 | 10Jan17 17:23:04 pid=6403 I pi 50000 rows with heatmap position data processed! 523 | ```` 524 | You can find the result in '/tmp/heatmap.csv'. 525 | 526 | ### Run rangview.pl 527 | 528 | Process the flight data and create a rangeview. 529 | 530 | Be sure you have have collected data for a couple of days! 531 | 532 | ```` 533 | $ cd 534 | $ cd socket30003 535 | $ ./rangview.pl 536 | 10Jan17 20:30:38 pid=8562 I pi Log file: '/tmp/rangeview-170110.log' 537 | The altitude will be converted from 'meter' to 'meter'. 538 | The distance will be converted from 'kilometer' to 'kilometer. 539 | 10Jan17 20:30:38 pid=8562 I pi Output file: '/tmp/rangeview.csv' 540 | 10Jan17 20:30:38 pid=8562 I pi The maximum altitude is 12000 meter. 541 | 10Jan17 20:30:38 pid=8562 I pi The minimal altitude is 0 meter. 542 | 10Jan17 20:30:38 pid=8562 I pi The number of compass directions (pie slices) is 1440. 543 | 10Jan17 20:30:38 pid=8562 I pi The number of altitude zones is 24. 544 | 10Jan17 20:30:38 pid=8562 I pi The latitude/longitude location of the antenna is: 52.085624,5.0890591. 545 | 10Jan17 20:30:38 pid=8562 I pi An altitude zone is 500 meter. 546 | 10Jan17 20:30:38 pid=8562 I pi The following files fit with the filemask '*dump*.txt*': 547 | 10Jan17 20:30:38 pid=8562 I pi /tmp/dump1090-ted1090-5-170110.txt 548 | 10Jan17 20:30:38 pid=8562 I pi /tmp/dump1090-ted1090-5-170109.txt 549 | 10Jan17 20:30:38 pid=8562 I pi processing '/tmp/dump1090-ted1090-5-170110.txt': 550 | 10Jan17 20:32:47 pid=8562 I pi -header units:altitude=meter,distance=kilometer,ground_speed=kilometerph, position 1-1591682. processed. 551 | 10Jan17 20:32:47 pid=8562 I pi processing '/tmp/dump1090-ted1090-5-170109.txt': 552 | 10Jan17 20:33:33 pid=8562 I pi -header units:altitude=meter,distance=kilometer,ground_speed=kilometerph, position 1-580075. processed. 553 | 10Jan17 20:33:33 pid=8562 I pi Number of files read: 2 554 | 10Jan17 20:33:33 pid=8562 I pi Number of position processed: 2171757 and positions within range processed: 2060038 555 | 10Jan17 20:33:35 pid=8562 I pi 1,Altitude zone: 0- 499,Directions: 406/ 1440,Positions processed: 8128,Positions processed per direction: min: 1,max: 545,avg: 5,real avg: 20 556 | 10Jan17 20:33:35 pid=8562 I pi 2,Altitude zone: 500- 999,Directions: 524/ 1440,Positions processed: 33499,Positions processed per direction: min: 0,max: 778,avg: 23,real avg: 64 557 | 10Jan17 20:33:35 pid=8562 I pi 3,Altitude zone: 1000- 1499,Directions: 734/ 1440,Positions processed: 37874,Positions processed per direction: min: 0,max: 766,avg: 26,real avg: 51 558 | 10Jan17 20:33:35 pid=8562 I pi 4,Altitude zone: 1500- 1999,Directions: 1114/ 1440,Positions processed: 40865,Positions processed per direction: min: 2,max: 627,avg: 28,real avg: 36 559 | 10Jan17 20:33:35 pid=8562 I pi 5,Altitude zone: 2000- 2499,Directions: 1241/ 1440,Positions processed: 45281,Positions processed per direction: min: 23,max: 618,avg: 31,real avg: 36 560 | 10Jan17 20:33:35 pid=8562 I pi 6,Altitude zone: 2500- 2999,Directions: 1299/ 1440,Positions processed: 40231,Positions processed per direction: min: 29,max: 670,avg: 27,real avg: 30 561 | 10Jan17 20:33:35 pid=8562 I pi 7,Altitude zone: 3000- 3499,Directions: 1373/ 1440,Positions processed: 55566,Positions processed per direction: min: 18,max: 469,avg: 38,real avg: 40 562 | 10Jan17 20:33:36 pid=8562 I pi 8,Altitude zone: 3500- 3999,Directions: 1410/ 1440,Positions processed: 42652,Positions processed per direction: min: 16,max: 292,avg: 29,real avg: 30 563 | 10Jan17 20:33:36 pid=8562 I pi 9,Altitude zone: 4000- 4499,Directions: 1387/ 1440,Positions processed: 42639,Positions processed per direction: min: 4,max: 222,avg: 29,real avg: 30 564 | 10Jan17 20:33:36 pid=8562 I pi 10,Altitude zone: 4500- 4999,Directions: 1374/ 1440,Positions processed: 45339,Positions processed per direction: min: 20,max: 267,avg: 31,real avg: 33 565 | 10Jan17 20:33:36 pid=8562 I pi 11,Altitude zone: 5000- 5499,Directions: 1356/ 1440,Positions processed: 43614,Positions processed per direction: min: 18,max: 322,avg: 30,real avg: 32 566 | 10Jan17 20:33:36 pid=8562 I pi 12,Altitude zone: 5500- 5999,Directions: 1322/ 1440,Positions processed: 42356,Positions processed per direction: min: 11,max: 330,avg: 29,real avg: 32 567 | 10Jan17 20:33:36 pid=8562 I pi 13,Altitude zone: 6000- 6499,Directions: 1388/ 1440,Positions processed: 45874,Positions processed per direction: min: 12,max: 623,avg: 31,real avg: 33 568 | 10Jan17 20:33:36 pid=8562 I pi 14,Altitude zone: 6500- 6999,Directions: 1275/ 1440,Positions processed: 47050,Positions processed per direction: min: 17,max: 686,avg: 32,real avg: 36 569 | 10Jan17 20:33:36 pid=8562 I pi 15,Altitude zone: 7000- 7499,Directions: 1301/ 1440,Positions processed: 55586,Positions processed per direction: min: 28,max: 686,avg: 38,real avg: 42 570 | 10Jan17 20:33:36 pid=8562 I pi 16,Altitude zone: 7500- 7999,Directions: 1411/ 1440,Positions processed: 45161,Positions processed per direction: min: 26,max: 514,avg: 31,real avg: 32 571 | 10Jan17 20:33:36 pid=8562 I pi 17,Altitude zone: 8000- 8499,Directions: 1416/ 1440,Positions processed: 44739,Positions processed per direction: min: 13,max: 567,avg: 31,real avg: 31 572 | 10Jan17 20:33:37 pid=8562 I pi 18,Altitude zone: 8500- 8999,Directions: 1415/ 1440,Positions processed: 56940,Positions processed per direction: min: 43,max: 829,avg: 39,real avg: 40 573 | 10Jan17 20:33:37 pid=8562 I pi 19,Altitude zone: 9000- 9499,Directions: 1440/ 1440,Positions processed: 101448,Positions processed per direction: min: 96,max: 479,avg: 70,real avg: 70 574 | 10Jan17 20:33:37 pid=8562 I pi 20,Altitude zone: 9500- 9999,Directions: 1422/ 1440,Positions processed: 69574,Positions processed per direction: min: 59,max: 428,avg: 48,real avg: 48 575 | 10Jan17 20:33:37 pid=8562 I pi 21,Altitude zone: 10000- 10499,Directions: 1440/ 1440,Positions processed: 181891,Positions processed per direction: min: 112,max: 748,avg: 126,real avg: 126 576 | 10Jan17 20:33:37 pid=8562 I pi 22,Altitude zone: 10500- 10999,Directions: 1440/ 1440,Positions processed: 363296,Positions processed per direction: min: 177,max: 1431,avg: 252,real avg: 252 577 | 10Jan17 20:33:37 pid=8562 I pi 23,Altitude zone: 11000- 11499,Directions: 1440/ 1440,Positions processed: 226620,Positions processed per direction: min: 178,max: 2577,avg: 157,real avg: 157 578 | 10Jan17 20:33:37 pid=8562 I pi 24,Altitude zone: 11500- 11999,Directions: 1440/ 1440,Positions processed: 343815,Positions processed per direction: min: 240,max: 975,avg: 238,real avg: 238 579 | ```` 580 | You can find the result in '/tmp/rangview.kml'. 581 | 582 | ### View the heatmap 583 | 584 | Be sure you have my version of dump1090 mutability installed: https://github.com/tedsluis/dump1090 585 | 586 | Copy the 'heatmap.csv' to '/usr/share/dump1090-mutability/html/' 587 | ```` 588 | $ sudo cp /tmp/heatmap.csv /usr/share/dump1090-mutability/html/heatmapdata.csv 589 | ```` 590 | Optional: change the name and the path of the heatmap within '/usr/share/dump1090-mutability/html/config.js'. 591 | 592 | Refresh the dump1090 web GUI and toggle the [heatmap] button. 593 | 594 | ### View the rangeview 595 | 596 | Be sure you have my version of dump1090 mutability installed: https://github.com/tedsluis/dump1090 597 | 598 | Copy the 'rangview.kml' to a webserver where it can be publicly accessed (nessesary for the Google maps API). 599 | You can use Github, dropbox of Google drive to share the 'rangeview.kml' publicly. 600 | 601 | Edit and fill in the URL of the rangeview.kml after 'UserMap='. 602 | ```` 603 | $ sudo vi /usr/share/dump1090-mutability/html/config.js 604 | ```` 605 | 606 | Refresh the dump1090 web GUI and toggle the [rangeview] button. 607 | 608 | ## More info 609 | 610 | * https://github.com/tedsluis/dump1090 611 | * https://www.youtube.com/watch?v=Qz4XSFRjLTI 612 | * http://discussions.flightaware.com/ads-b-flight-tracking-f21/heatmap-range-altitude-view-for-dump1090-mutability-v1-15-t35844.html 613 | * ted.sluis@gmail.com 614 | -------------------------------------------------------------------------------- /common.pm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # Ted Sluis 2015-12-20 3 | # Filename : common.pm 4 | #=============================================================================== 5 | # common sub routines 6 | #=============================================================================== 7 | package common; 8 | use strict; 9 | use warnings; 10 | use POSIX qw(strftime); 11 | use Time::Local; 12 | use Getopt::Long; 13 | use File::Basename; 14 | use Cwd; 15 | use Term::ANSIColor qw(:constants); 16 | #=============================================================================== 17 | my %setting; 18 | my $myDebug; 19 | my $interactive = 0; 20 | my $account; 21 | my $verbose; 22 | my $logfile; 23 | my @errorlog; 24 | my @tmplog; 25 | #=============================================================================== 26 | sub InteractiveShellCheck(@) { 27 | # 28 | # Do we run this script interactive or not? 29 | # If so, turn on verbose logging. 30 | # 31 | # Input: none. You can force interactive or non interactive mode using 0 or 1. 32 | # Return: 1=interactive, 0=not interactive. 33 | # 34 | if ( defined ($_[0]) && $_[0] eq "common" ) { shift; } 35 | if ( defined ($_[0]) ){ 36 | # Force interactive or non interactive mode using 0 or 1. 37 | if ($_[0]){ 38 | $interactive = 0; 39 | LOG($logfile,"Forcing Interactive to OFF","D"); 40 | return 0; 41 | } else { 42 | $interactive = 1; 43 | LOG($logfile,"Forcing Interactive to ON","D"); 44 | return 1; 45 | } 46 | } 47 | if ($myDebug){ 48 | if (-t STDIN){ 49 | LOG($logfile,"Interactivity test -> STDIN = 1","D"); 50 | } else { 51 | LOG($logfile,"Interactivity test -> STDIN = 0","D"); 52 | } 53 | if (-t STDOUT){ 54 | LOG($logfile,"Interactivity test -> STDOUT = 1","D"); 55 | } else { 56 | LOG($logfile,"Interactivity test -> STDOUT = 0","D"); 57 | } 58 | } 59 | if (-t STDIN && -t STDOUT){ 60 | # Running interactive 61 | $interactive = 1; 62 | return 1; 63 | } 64 | return 0; 65 | } 66 | #=============================================================================== 67 | sub Account(@){ 68 | # 69 | # Determine the account that is being used. 70 | # 71 | # Input: none 72 | # Return: hash with key; 73 | # user 74 | # 75 | if ( defined ($_[0]) && $_[0] eq "common" ) { shift; } 76 | $account = `id -u --name`; 77 | chop($account); 78 | LOG($logfile,"Account=$account","D"); 79 | return $account; 80 | } 81 | $account = Account(); 82 | #=============================================================================== 83 | # Read configfile 84 | sub READCONFIG(@) { 85 | # 86 | # This routine will read the config file. 87 | # It looks for the sections and ignor those which do not apply. 88 | # It checks the INI file format and removes comments. 89 | # 90 | if ( defined($_[0]) && $_[0] eq "common" ) { shift; } 91 | # config file name: 92 | my $config = shift; 93 | # full scriptname: 94 | my $fullscriptname = shift; 95 | my %config; 96 | my $scriptname = basename($fullscriptname); 97 | my $directoryname = dirname($fullscriptname); 98 | # path to config file 99 | $config = $directoryname.'/'.$config; 100 | LOG($logfile,"Reading parameters and values from '$config' config file:","L"); 101 | if (!-e $config) { 102 | LOG($logfile,"Can not read config! Config file '$config' does not exists!","W"); 103 | return 0; 104 | } elsif (!-r $config) { 105 | LOG($logfile,"Can not read config! Config file '$config' is not readable!","W"); 106 | return 0; 107 | } else { 108 | my @cmd = `cat $config`; 109 | my $section; 110 | foreach my $line (@cmd) { 111 | chomp($line); 112 | # skip lines with comments: 113 | next if ($line =~ /^\s*#\s$|^\s*#\s*[^=]*$|^\s*#\s*[^=]*$|^\s*#\s*[^=]*[=]{2,}[^=]*$/); 114 | # skip blank lines: 115 | next if ($line =~ /^\s*$/); 116 | # Get section: 117 | if ($line =~ /^\s*\[([^\]]+)\]\s*(#.*)?$/) { 118 | $section = $1; 119 | LOG($logfile,"Section: [$section]","L") if (($section =~ /common/) || ($scriptname =~ /$section|install/)); 120 | next; 121 | } elsif ($line =~ /^(#?[^=]+)=([^\#]*)(#.*)?$/) { 122 | # Get paramter & value 123 | my $parameter = $1; 124 | my $value = $2; 125 | # remove any white spaces at the begin and the end: 126 | $parameter =~ s/^\s*|\s*$//g; 127 | $value =~ s/^\s*|\s*$//g; 128 | # Skip invalid lines: 129 | if ((!$parameter) || ($parameter =~ /^\s*$/)) { 130 | LOG($logfile,"The line '$line' in config file '$config' is invalid! No parameter specified!","W"); 131 | next; 132 | } 133 | if ((!$section) || ($section =~ /^\s*$/)) { 134 | LOG($logfile,"The line '$line' in config file '$config' is invalid! No section specified jet!","W"); 135 | next; 136 | } 137 | if (($parameter !~ /^#/) && (exists $config{$section}) && (exists $config{$section}{$parameter})) { 138 | LOG($logfile,"The line '$line' in section '$section' in config file '$config' does already exists! It has value '$config{$section}{$parameter}'. This line will be skipped.","W"); 139 | next; 140 | } 141 | # save section, parameter & value 142 | next unless (($section =~ /common/) || ($scriptname =~ /$section|install/)); 143 | $config{$section}{$parameter} = $value; 144 | LOG($logfile," $parameter = $value","L"); 145 | } else { 146 | # Invalid line: 147 | LOG($logfile,"The line '$line' in config file '$config' is invalid!","W"); 148 | LOG($logfile,"Valid lines looks like:","I"); 149 | LOG($logfile,"# comment line","I"); 150 | LOG($logfile,"[some_section_name]","I"); 151 | LOG($logfile,"parameter=value","I"); 152 | LOG($logfile,"Comment text (started with #) behind a section or parameter=value is allowed!","I"); 153 | next; 154 | } 155 | } 156 | } 157 | %setting = %config; 158 | return %config; 159 | } 160 | #=============================================================================== 161 | sub LOGset(@){ 162 | # 163 | # LOGset routine sets the log path. If the log file does 164 | # not exists, it will be created and the permissions will 165 | # be set. 166 | # 167 | # Input: path to log file. 168 | # Input: log file name. 169 | # Return: full path and log file name. 170 | # 171 | if ( defined($_[0]) && $_[0] eq "common" ) { shift; } 172 | my $logpath = shift; 173 | my $name = shift; 174 | if (!-d $logpath) { 175 | LOG($logfile,"The log file directory '$logpath' does not exists!","E"); 176 | exit 1; 177 | } 178 | if (!-x $logpath) { 179 | LOG($logfile,"The log file directory '$logpath' is not writeable!","E"); 180 | exit 1; 181 | } 182 | $logfile = "$logpath/$name"; 183 | system("touch $logfile") unless (-f $logfile); 184 | chmod(0666,$logfile); 185 | LOG($logfile,"=== NEW RUN MARKER ============================","H"); 186 | LOG($logfile,"Log file: '$logfile'","I"); 187 | return $logfile; 188 | } 189 | #=============================================================================== 190 | sub setdebug(@){ 191 | # 192 | # Sets debug mode and triggers verbose logging 193 | # 194 | # Input: none 195 | # Return: none 196 | # 197 | if ( $_[0] eq "common" ) { shift; } 198 | $myDebug += 1; 199 | LOG($logfile,"Setting debug ON!","I"); 200 | LOGverbose(); 201 | } 202 | #=============================================================================== 203 | sub LOGverbose(@){ 204 | # 205 | # Sets verbose logging mode. 206 | # 207 | # Input: none or 'off' to disable verbose logging. 208 | # Return: none 209 | # 210 | if ( defined($_[0]) && $_[0] eq "common" ) { shift; } 211 | my $level = shift || 0; 212 | if ($level eq "off"){ 213 | $verbose = 0; 214 | LOG($logfile,"Setting verbose OFF due to off switch.","D"); 215 | return; 216 | } 217 | $verbose = 1; 218 | LOG($logfile,"Setting verbose ON.","D"); 219 | } 220 | #=============================================================================== 221 | sub LOG(@) { 222 | # 223 | # Write messages to log file and display. 224 | # Adds alert color to Debug messages, Errors and Warnings. 225 | # 226 | # first field must be log file. 227 | # second field must be message. 228 | # third field informational or error. 229 | # 230 | if ( defined($_[0]) && $_[0] eq "common" ) { shift; } 231 | my $LOG = shift || ""; 232 | my $text = shift || ""; 233 | my $type = shift || "I"; 234 | # 235 | # D = Debug message. 236 | # E = Error message. 237 | # H = Log file only, reservered for === NEW RUN MARKER ===. 238 | # I = Informational log message. 239 | # L = Log file only, but can be displayed using verbose. 240 | # W = Warning message. 241 | # Lower case e, i or w are equel to E, I or W, but will be displayed on STDOUT without newline. 242 | # 243 | # Test type 244 | if ( $type eq "D" ){ 245 | # Displays debug info whenever debug is on. 246 | return unless $myDebug; 247 | $text = sprintf("pid=%-5s ",$$).(sprintf MAGENTA). "$type **DEBUG** $account $text" .sprintf RESET; 248 | } elsif ( $type =~ /H/ ) { 249 | # Writes only to log file: 250 | $text = sprintf("pid=%-5s ",$$)."- $account $text"; 251 | } elsif ( $type =~ /I|i/ ) { 252 | # Informational log message for log file & display. 253 | $text = sprintf("pid=%-5s ",$$).uc($type)." $account $text"; 254 | } elsif ( $type =~ /L/ ) { 255 | # Informational log message for log file only. 256 | $text = sprintf("pid=%-5s ",$$)."$type $account $text"; 257 | } elsif ( $type =~ /E|e/ ) { 258 | # Error message 259 | $text = sprintf("pid=%-5s ",$$).(sprintf RED).uc($type)." $account $text" .sprintf RESET; 260 | } elsif ( $type =~ /W|w/ ) { 261 | # Warning message 262 | $text = sprintf("pid=%-5s ",$$).(sprintf YELLOW).uc($type)." $account $text" .sprintf RESET; 263 | } else { 264 | # Every other type of message 265 | $text = sprintf("pid=%-5s ",$$)."I $account $text (onbekend logtype=$type)"; 266 | } 267 | $text = strftime("%d%b%y %H:%M:%S", localtime())." $text"; 268 | # Added to message queue 269 | if (($type =~ /H/) && ($text) && ($text =~ /NEW\sRUN\sMARKER/)) { 270 | # Add on start 271 | unshift(@tmplog,$text); 272 | } else { 273 | # Add on end 274 | push(@tmplog,$text); 275 | } 276 | # Do we have a log file jet? 277 | if (($LOG) && ($LOG !~ /^\s*$/) && (-w $LOG)) { 278 | # Write all messages in queue to log file. 279 | open (OUT,">>$LOG") or die "Cannot open logfile '$LOG' for output; Reason $! ! \n"; 280 | foreach my $line (@tmplog) { 281 | # remove ansi codes before writing to log file. 282 | $line =~ s/\x1b\[[0-9;]*m//g; 283 | print OUT $line."\n"; 284 | } 285 | # empty message queue 286 | @tmplog=(); 287 | close OUT; 288 | } 289 | # 290 | return if $type eq "H"; 291 | # Write message to display 292 | if (($interactive) && ((($type =~ /L/) && ($verbose)) || ($type =~ /[EeIiDWw]/))) { 293 | print $text; 294 | print "\n" unless ($type =~ /[eiw]/); 295 | } 296 | # Store Errors & Warnings 297 | if ( $type =~ /[EW]{1}/ ){ 298 | # remove ansi codes before writing to log file. 299 | $text =~ s/\x1b\[[0-9;]*m//g; 300 | push(@errorlog,"$type;$text"); 301 | } 302 | } # Einde LOG 303 | #=============================================================================== 304 | sub SetOutput(@) { 305 | # 306 | # Set output file name 307 | # 308 | # first field must be the path 309 | # second field must be filename+extention, for example data.csv 310 | # third field: override file: yes or no 311 | # fourth field: add timestamp to file name: yes or no 312 | # fifth field: add sequence number to file name: yes or no 313 | # 314 | if ( defined($_[0]) && $_[0] eq "common" ) { shift; } 315 | my $outputdirectory = shift || "/tmp"; 316 | my $outputdatafile = shift || "output.csv"; 317 | my $override = shift || "no"; 318 | my $timestamp = shift || "yes"; 319 | my $sequencenumber = shift || "yes"; 320 | # 321 | my ($name,$extention) = $outputdatafile =~ /^([^\.]+)(\.[^\.]*)?$/; 322 | $name = $name || "output"; 323 | $extention = $extention || ".csv"; 324 | LOG($logfile,"outputdirectory=$outputdirectory,outputdatafile=$outputdatafile(name=$name,extention=$extention),override=$override,timestamp=$timestamp,sequencenumber=$sequencenumber.","L"); 325 | # Add timestamp 326 | my ($second,$day,$month,$year,$minute,$hour) = (localtime)[0,3,4,5,1,2]; 327 | my $date = sprintf '%02d%02d%02d-%02d%02d%02d', $year-100,($month+1),$day,$hour,$minute,$second; 328 | $name .= '-'.$date if ($timestamp eq "yes"); 329 | # Add sequence number. 330 | if ($sequencenumber eq "yes") { 331 | my $mask = "'$name*$extention'"; 332 | LOG($logfile,"search for file with mask '$mask' in '$outputdirectory'.","L"); 333 | my @files = `find $outputdirectory -name $mask | sort`; 334 | my %files; 335 | foreach my $file (@files) { 336 | chomp($file); 337 | # Get sequence number from file name: 338 | if ($file =~ /\Q${name}\E-(\d+)\Q${extention}\E/) { 339 | $files{$1} = $file; 340 | LOG($logfile,"$1 = $files{$1}","L"); 341 | } 342 | } 343 | my $seqnum; 344 | if ((keys %files) == 0) { 345 | $seqnum = 0; 346 | } else { 347 | my @junk; 348 | # Get last sequence number: 349 | ($seqnum,@junk) = reverse sort keys %files; 350 | } 351 | # Next sequence number: 352 | $seqnum++; 353 | $name .= '-'.sprintf '%03d',$seqnum; 354 | } 355 | # Set file name: 356 | $outputdatafile = $outputdirectory.'/'.$name.$extention; 357 | if (($override eq "no") && (-e $outputdatafile)) { 358 | LOG($logfile,"The outputfile '$outputdatafile' already exists!","E"); 359 | LOG($logfile,"Use the option '-override' to override the file.","E"); 360 | LOG($logfile,"Or use the option '-timestamp' or '-sequencenumber' to add a timestamp or sequence number to the file name.","E"); 361 | LOG($logfile,"Or better: set one of these options in the 'socket30003.cfg' config file.","E"); 362 | exit 1; 363 | } elsif (($override eq "yes") && (-e $outputdatafile) && (!-w $outputdatafile)) { 364 | LOG($logfile,"Can not write to '$outputdatafile'! Check the permissions!","E"); 365 | exit 1; 366 | } 367 | LOG($logfile,"Output file: '$outputdatafile'","I"); 368 | return $outputdatafile; 369 | } 370 | #=============================================================================== 371 | sub GetSourceData(@){ 372 | # 373 | # Get the file names of the source data 374 | # 375 | # 376 | if ( defined($_[0]) && $_[0] eq "common" ) { shift; } 377 | my $datadirectory = shift || "/tmp"; 378 | my $filemask = shift || "'dump*.txt'"; 379 | # 380 | # Is the specified directory for the source data files readable? 381 | if (!-r $datadirectory) { 382 | LOG($logfile, "The data directory does not exists or you have no right permissions in '$datadirectory'!","E"); 383 | exit 1; 384 | } 385 | # Set default filemask 386 | if (!$filemask) { 387 | $filemask = "'dump*.txt'" ; 388 | } else { 389 | $filemask ="'*$filemask*'"; 390 | } 391 | # Find files 392 | my @files =`find $datadirectory -name $filemask`; 393 | if (@files == 0) { 394 | LOG($logfile,"No files were found in '$datadirectory' that matches with the $filemask filemask!","W"); 395 | exit 1; 396 | } else { 397 | LOG($logfile, "The following files fit with the filemask $filemask:","I"); 398 | my @tmp; 399 | foreach my $file (@files) { 400 | chomp($file); 401 | # skip log en pid files.... 402 | next if ($file =~ /log$|pid$/i); 403 | LOG($logfile," $file","I"); 404 | push(@tmp,$file); 405 | } 406 | @files = @tmp; 407 | if (@files == 0) { 408 | LOG($logfile,"No files were found in '$datadirectory' that matches with the $filemask filemask!","W"); 409 | exit 1; 410 | } 411 | } 412 | return @files; 413 | } 414 | # 415 | #================================================================================= 416 | # 417 | sub CheckDirectory(@) { 418 | # 419 | # field 1 = directory type (log, output, data, pid) 420 | # field 2 = directory 421 | # field 3 = modify (yes or no) 422 | # 423 | if ( defined($_[0]) && $_[0] eq "common" ) { shift; } 424 | my $directorytype = shift || "directory"; 425 | my $directory = shift; 426 | my $modify = shift || "yes"; 427 | # 428 | my $answer; 429 | if ((!$directory) || ($directory =~ /^\s*$/)) { 430 | LOG($logfile,"There is no $directorytype directory path specified!","W"); 431 | $directory = common->ReadInput("Specify the $directorytype directory","directory",$modify,"/tmp"); 432 | } 433 | # covert relative directory path to absolute path: 434 | $directory = getcwd().'/'.$directory if ($directory !~ /^\//); 435 | $directory =~ s/\/$//; 436 | if ($modify eq 'yes') { 437 | $answer = common->ReadInput("Do you want to use '$directory' as $directorytype directory?","multiplechoice",$modify,"y","n"); 438 | if ($answer =~ /n/i) { 439 | $directory = common->ReadInput("Specify the $directorytype directory","directory",$modify,$directory); 440 | } 441 | } 442 | if (!-e $directory) { 443 | LOG($logfile,"The $directorytype directory '$directory' does not exists!","I"); 444 | my @path = split(/\//,$directory); 445 | my $path = ""; 446 | foreach my $dir (@path) { 447 | next if ($dir =~ /^\s*$/); 448 | $path .= '/'.$dir; 449 | if (-e $path) { 450 | next; 451 | } else { 452 | mkdir($path); 453 | chmod(0775,$path) if ((!-r $path) || (!-w $path)); 454 | if (!-e $path) { 455 | LOG($logfile,"Unable to create $directorytype directory '$path'!","E"); 456 | exit 1; 457 | } else { 458 | LOG($logfile,"The $directorytype directory '$path' has been created.","L"); 459 | } 460 | } 461 | } 462 | } else { 463 | LOG($logfile,"The $directorytype directory '$directory' does exists.","L"); 464 | } 465 | if ((!-r $directory) || (!-w $directory)) { 466 | LOG($logfile,"The $directorytype directory '$directory' is not readable and/or writable!","I"); 467 | chmod($directory,0775); 468 | if ((!-r $directory) || (!-w $directory)) { 469 | LOG($logfile,"Unable to change the permissions for the $directorytype directory '$directory'!","E"); 470 | exit 1; 471 | } else { 472 | LOG($logfile,"The permissions for the $directorytype directory '$directory' have been set.","I"); 473 | } 474 | } else { 475 | LOG($logfile,"The $directorytype directory '$directory' is readable and writeable.","L"); 476 | } 477 | LOG($logfile,"Selected $directorytype directory: '$directory'.","I"); 478 | $directory =~ s/\/$//; 479 | return $directory; 480 | } 481 | # 482 | #================================================================================= 483 | # 484 | sub ReadInput(@) { 485 | # 486 | # field 1 = Question. 487 | # fiels 2 = type: multiplechoice, directory, file, filemask, numeric, ipaddress 488 | # field 3 = modify (yes or no) 489 | # field 4... = possible ansers 490 | # 491 | if ( defined($_[0]) && $_[0] eq "common" ) { shift; } 492 | my $question = shift; 493 | my $type = shift; 494 | my $modify = shift; 495 | my @answers = $_[0]; 496 | foreach my $a (@_) { 497 | push(@answers,$a) unless ($a eq $_[0]); 498 | } 499 | # 500 | my $answer; 501 | if ($type =~ /multiplechoice/) { 502 | my $answers = '^'.join('$|^',@answers).'$'; 503 | if ($modify =~ /y/) { 504 | LOG($logfile,"$question (".join('/',@answers)."): [$answers[0]]","i"); 505 | $answer = ; 506 | } 507 | chomp($answer) if ($answer); 508 | $answer ||= $answers[0]; 509 | while ($answer !~ /$answers/i) { 510 | LOG($logfile,"The specified answer '$answer' is invalid! Try again (".join('/',@answers)."): [$answers[0]]","w"); 511 | $answer = ; 512 | chomp($answer) if ($answer); 513 | $answer ||= $answers[0]; 514 | } 515 | } elsif ($type =~ /directory/) { 516 | if ($modify =~ /y/) { 517 | LOG($logfile,"$question: [$answers[0]]","i"); 518 | $answer = ; 519 | } 520 | chomp($answer) if ($answer); 521 | $answer ||= $answers[0]; 522 | while ($answer !~ /^\/?([a-z0-9\._\-]+\/?)+/i) { 523 | LOG($logfile,"The specified directory '$answer' is invalid! Try again: [$answers[0]","w"); 524 | $answer = ; 525 | chomp($answer) if ($answer); 526 | $answer ||= $answers[0]; 527 | } 528 | } elsif ($type =~ /^file$/) { 529 | if ($modify =~ /y/){ 530 | LOG($logfile,"$question: [$answers[0]]","i"); 531 | $answer = ; 532 | } 533 | chomp($answer) if ($answer); 534 | $answer ||= $answers[0]; 535 | while ($answer !~ /^[a-z0-9\.\-_]+$/i) { 536 | LOG($logfile,"The specified file '$answer' is invalid! Try again (without path): [$answers[0]","w"); 537 | $answer = ; 538 | chomp($answer) if ($answer); 539 | $answer ||= $answers[0]; 540 | } 541 | } elsif ($type =~ /^filemask$/) { 542 | if ($modify =~ /y/){ 543 | LOG($logfile,"$question: [$answers[0]]","i"); 544 | $answer = ; 545 | } 546 | chomp($answer) if ($answer); 547 | $answer ||= $answers[0]; 548 | while ($answer !~ /^[\w\d\.\-_\*\?]+$/i) { 549 | LOG($logfile,"The specified filemask '$answer' is invalid! Try again: [$answers[0]","w"); 550 | $answer = ; 551 | chomp($answer) if ($answer); 552 | $answer ||= $answers[0]; 553 | } 554 | } elsif ($type =~ /^numeric$/) { 555 | if ($modify =~ /y/){ 556 | LOG($logfile,"$question: [$answers[0]]","i"); 557 | $answer = ; 558 | } 559 | chomp($answer) if ($answer); 560 | $answer ||= $answers[0]; 561 | while ($answer !~ /^[\+\-]?\d+(\.\d+)?$/i) { 562 | LOG($logfile,"The specified numeric value '$answer' is invalid! Try again: [$answers[0]","w"); 563 | $answer = ; 564 | chomp($answer) if ($answer); 565 | $answer ||= $answers[0]; 566 | } 567 | } elsif ($type =~ /^ipaddress$/) { 568 | if ($modify =~ /y/){ 569 | LOG($logfile,"$question: [$answers[0]]","i"); 570 | $answer = ; 571 | } 572 | chomp($answer) if ($answer); 573 | $answer ||= $answers[0]; 574 | while ($answer !~ /^\d+\.\d+\.\d+\.\d+$/i) { 575 | LOG($logfile,"The specified filemask '$answer' is invalid! Try again: [$answers[0]","w"); 576 | $answer = ; 577 | chomp($answer) if ($answer); 578 | $answer ||= $answers[0]; 579 | } 580 | } 581 | return $answer; 582 | } 583 | 1; 584 | -------------------------------------------------------------------------------- /heatmap.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # ted.sluis@gmail.com 4 | # heatmap.pl 5 | # 6 | #=============================================================================== 7 | BEGIN { 8 | use strict; 9 | use POSIX qw(strftime); 10 | use Time::Local; 11 | use Getopt::Long; 12 | use File::Basename; 13 | use Cwd 'abs_path'; 14 | our $scriptname = basename($0); 15 | our $fullscriptname = abs_path($0); 16 | use lib dirname (__FILE__); 17 | use common; 18 | } 19 | # 20 | #=============================================================================== 21 | my $logfile =""; 22 | #=============================================================================== 23 | # Ctrl-C interupt handler 24 | $SIG{'INT'} = \&intHandler; 25 | 26 | sub intHandler { 27 | # Someone pressed Ctrl-C 28 | LOG("CTRL-C was pressed. Do you want to exit '$scriptname'? (y/n)","W"); 29 | my $answer = ; 30 | if ($answer =~ /^y$/i) { 31 | LOG("Exiting '$scriptname'.....","I"); 32 | exit 1; 33 | } else { 34 | LOG("'$scriptname' is continuing.......","I"); 35 | } 36 | } 37 | #=============================================================================== 38 | # Get options 39 | my $help; 40 | my $datadirectory; 41 | my $logdirectory; 42 | my $outputdirectory; 43 | my $outputdatafile; 44 | my $override; 45 | my $timestamp; 46 | my $sequencenumber, 47 | my $filemask; 48 | my $longitude; 49 | my $latitude; 50 | my $max_positions; 51 | my $resolution; 52 | my $degrees; 53 | my $max_weight; 54 | my $debug = 0; 55 | my $verbose = 0; 56 | 57 | GetOptions( 58 | "help!"=>\$help, 59 | "filemask=s"=>\$filemask, 60 | "data=s"=>\$datadirectory, 61 | "log=s"=>\$logdirectory, 62 | "output=s"=>\$outputdirectory, 63 | "file=s"=>\$outputdatafile, 64 | "override!"=>\$override, 65 | "timestamp!"=>\$timestamp, 66 | "sequencenumber!"=>\$sequencenumber, 67 | "longitude=s"=>\$longitude, 68 | "latitude=s"=>\$latitude, 69 | "maxpositions=s"=>\$max_positions, 70 | "resolution=s"=>\$resolution, 71 | "degrees=s"=>\$degrees, 72 | "maxweight=s"=>\$max_weight, 73 | "debug!"=>\$debug, 74 | "verbose!"=>\$verbose 75 | ) or exit(1); 76 | # 77 | $override = "yes" if ($override); 78 | $timestamp = "yes" if ($timestamp); 79 | $sequencenumber = "yes" if ($sequencenumber); 80 | # 81 | #=============================================================================== 82 | # if '-debug' parameter is used, set debug mode: 83 | common->setdebug if ($debug); 84 | # 85 | #=============================================================================== 86 | # if '-verbose' parameter is used, set verbose mode: 87 | common->LOGverbose if ($verbose); 88 | # 89 | #=============================================================================== 90 | # Checks if script runs interactive. 91 | my $interactive = common->InteractiveShellCheck; 92 | # 93 | #=============================================================================== 94 | # Log routine 95 | sub LOG(@){ 96 | common->LOG($logfile,@_); 97 | } 98 | # 99 | #=============================================================================== 100 | # Read settings from config file 101 | my %setting = common->READCONFIG('socket30003.cfg',$fullscriptname); 102 | # Use parameters & values from the 'heatmap' section. If empty or not-exists, then use from the 'common' section, otherwise script defaults. 103 | $datadirectory = $datadirectory || $setting{'heatmap'}{'datadirectory'} || $setting{'common'}{'datadirectory'} || "/tmp"; 104 | $logdirectory = $logdirectory || $setting{'heatmap'}{'logdirectory'} || $setting{'common'}{'logdirectory'} || "/tmp"; 105 | $outputdirectory= $outputdirectory || $setting{'heatmap'}{'outputdirectory'} || $setting{'common'}{'outputdirectory'} || "/tmp"; 106 | $latitude = $latitude || $setting{'heatmap'}{'latitude'} || $setting{'common'}{'latitude'} || 52.085624; # Antenna location 107 | $longitude = $longitude || $setting{'heatmap'}{'longitude'} || $setting{'common'}{'longitude'} || 5.0890591; # 108 | $filemask = $filemask || $setting{'heatmap'}{'filemask'} || $setting{'common'}{'filemask'} || "dump*txt"; 109 | $override = $override || $setting{'heatmap'}{'override'} || $setting{'common'}{'override'} || "no"; # override output file if exists. 110 | $timestamp = $timestamp || $setting{'heatmap'}{'timestamp'} || $setting{'common'}{'timestamp'} || "no"; # add timestamp to output file name. 111 | $sequencenumber = $sequencenumber || $setting{'heatmap'}{'sequencenumber'} || $setting{'common'}{'sequencenumber'} || "no"; # add sequence number to output file name. 112 | $outputdatafile = $outputdatafile || $setting{'heatmap'}{'outputdatafile'} || "heatmapdata.csv"; 113 | $degrees = $degrees || $setting{'heatmap'}{'degrees'} || 5; # used to determine boundary of area around antenne. 114 | $resolution = $resolution || $setting{'heatmap'}{'resolution'} || 1000; # number of horizontal and vertical positions in output file. 115 | $max_positions = $max_positions || $setting{'heatmap'}{'max_positions'} || 100000; # maximum number of positions in the outputfile. 116 | $max_weight = $max_weight || $setting{'heatmap'}{'max_weight'} || 1000; # maximum position weight on the heatmap. 117 | # 118 | #=============================================================================== 119 | # Check options: 120 | if ($help) { 121 | print "\nThis $scriptname script creates heatmap data 122 | which can be displated in a modified variant of dump1090-mutobility. 123 | 124 | It creates an output file with location data in csv format, which can 125 | be imported using the dump1090 GUI. 126 | 127 | Please read this post for more info: 128 | http://discussions.flightaware.com/post180185.html#p180185 129 | 130 | This script uses the data file(s) created by the 'socket30003.pl' 131 | script, which are by default stored in '$outputdirectory' in this format: 132 | dump1090--YYMMDD.txt 133 | 134 | The script will automaticly use the correct units (feet, meter, 135 | kilometer, mile, natical mile) for 'altitude' and 'distance' when 136 | the input files contain column headers with the unit type between 137 | parentheses. When the input files doesn't contain column headers 138 | (as produced by older versions of 'socket30003.pl' script) 139 | you can specify the units using startup parameters or in the config 140 | file. Otherwise this script will use the default units. 141 | 142 | This script will create a heatmap of a square area around your 143 | antenna. You can change the default range by specifing the number 144 | of degrees -/+ to your antenna locations. (The default values will 145 | probably satisfy.) This area will be devided in to small squares. 146 | The default heatmap has a resolution of 1000 x 1000 squares. 147 | The script will read all the flight position data from the source 148 | file(s) and count the times they match with a square on the heatmap. 149 | 150 | The more positions match with a particular square on the heatmap, 151 | the more the 'weight' that heatmap position gets. We use only the 152 | squares with the most matches (most 'weight) to create the heatmap. 153 | This is because the map in the browser gets to slow when you use 154 | too much positions in the heatmap. Of cource this also depends on 155 | the amount of memory of your system. You can change the default 156 | number of heatmap positions. You can also set the maximum of 157 | 'weight' per heatmap position. 158 | 159 | CSV output format: 160 | \"weight\";\"lat\";\"lon\" 161 | \"1001\";\"52.489\";\"4.729\" 162 | \"883\";\"52.37\";\"4.72\" 163 | \"868\";\"52.19\";\"4.81\" 164 | \"862\";\"51.9\";\"4.75\" 165 | \"791\";\"52.12\";\"4.8\" 166 | \"759\";\"52.01\";\"4.779\" 167 | 168 | Syntax: $scriptname 169 | 170 | Optional parameters: 171 | -data The data files are stored in 172 | '$datadirectory' by default. 173 | -log The data files are stored in 174 | '$logdirectory' by default. 175 | -output '$outputdirectory' by default. 177 | -file The output file name. 178 | '$outputdatafile' by default. 179 | -filemask Specify a filemask for the source data. 180 | The default filemask is '$filemask'. 181 | -override Override output file if exists. 182 | Default is '$override'. 183 | -timestamp Add timestamp to output file name. 184 | Default is '$timestamp'. 185 | -sequencenumber Add sequence number to output file name. 186 | Default is '$sequencenumber'. 187 | -lon Location of your antenna. 188 | -lat 189 | -maxpositions '$max_positions' positions. 191 | -maxweight Maximum position weight. The default is 192 | '$max_weight'. 193 | -resolution Number of horizontal and vertical positions 194 | in output file. Default is '$resolution', 195 | which means '${resolution}x${resolution}' positions. 196 | -degrees To determine boundaries of area around the 197 | antenna. (lat-degree <--> lat+degree) x 198 | (lon-degree <--> lon+degree) 199 | De default is '$degrees' degree. 200 | -debug Displays raw socket messages. 201 | -verbose Displays verbose log messages. 202 | -help This help page. 203 | 204 | note: 205 | The default values can be changed within the config file 206 | 'socket30003.cfg', section [common] and section [heatmap]. 207 | 208 | Examples: 209 | $scriptname 210 | $scriptname -data /home/pi -log /var/log 211 | $scriptname -lat 52.1 -lon 4.1 -maxposition 50000\n\n"; 212 | exit 0; 213 | } 214 | #=============================================================================== 215 | # Is the log directory writeable? 216 | if (!-w $logdirectory) { 217 | LOG("The directory does not exists or you have no write permissions in log directory '$logdirectory'!","E"); 218 | exit 1; 219 | } 220 | # Set log file path and name 221 | my ($second,$day,$month,$year,$minute,$hour) = (localtime)[0,3,4,5,1,2]; 222 | my $filedate = 'heatmap-'.sprintf '%02d%02d%02d', $year-100,($month+1),$day; 223 | $logfile = common->LOGset($logdirectory,"$filedate.log",$verbose); 224 | # 225 | #=============================================================================== 226 | # Resolution, Degrees & Factor 227 | if ($resolution) { 228 | if ($resolution !~ /^\d{2,5}$/) { 229 | LOG("The resolution '$resolution' is invalid!","E"); 230 | LOG("It should be between 10 and 99999.","E"); 231 | exit; 232 | } 233 | } else { 234 | $resolution = 1000; 235 | } 236 | if ($degrees) { 237 | if ($degrees !~ /^\d{1,2}(\.\d{1,4})?$/) { 238 | LOG("The given number of degrees '$degrees' is invalid!","E"); 239 | LOG("It should be between 0.0001 and 99.9999 degrees.","E"); 240 | exit; 241 | } 242 | } else { 243 | $degrees = 5; 244 | } 245 | my $factor = int($resolution / ($degrees * 2)); 246 | #=============================================================================== 247 | # Max positions 248 | if ($max_positions) { 249 | if (($max_positions !~ /^\d{3,6}$/) && ($max_positions > 99) && ($max_positions < 1000000)) { 250 | LOG("The maximum number of positions '$max_positions' is invalid!","E"); 251 | LOG("It should be between 100 and 999999.","E"); 252 | exit; 253 | } 254 | } else { 255 | $max_positions = 100000; 256 | } 257 | LOG("There will be no more then '$max_positions' positions in the output file.","I"); 258 | #=============================================================================== 259 | if ($max_weight) { 260 | if (($max_weight !~ /^\d{2,4}$/) && ($max_weight > 9) && ($max_weight < 10000)) { 261 | LOG("The maximum position weight '$max_weight' is invalid!","E"); 262 | LOG("It should be between 10 and 9999.","E"); 263 | exit; 264 | } 265 | } else { 266 | $max_weight = 1000; 267 | } 268 | LOG("The maximum position weight will be not more then '$max_weight'.","I"); 269 | #=============================================================================== 270 | # Is the specified directories for the output file writeable? 271 | if (!-w $outputdirectory) { 272 | LOG("The directory does not exists or you have no write permissions in output directory '$outputdirectory'!","E"); 273 | exit 1; 274 | } 275 | # Set output file 276 | $outputdatafile = common->SetOutput($outputdirectory,$outputdatafile,$override,$timestamp,$sequencenumber); 277 | # 278 | #=============================================================================== 279 | # longitude & latitude 280 | $longitude =~ s/,/\./ if ($longitude); 281 | if ($longitude !~ /^[-+]?\d+(\.\d+)?$/) { 282 | LOG("The specified longitude '$longitude' is invalid!","E"); 283 | exit 1; 284 | } 285 | $latitude =~ s/,/\./ if ($latitude); 286 | if ($latitude !~ /^[-+]?\d+(\.\d+)?$/) { 287 | LOG("The specified latitude '$latitude' is invalid!","E"); 288 | exit 1; 289 | } 290 | # area around antenna 291 | $latitude = int($latitude * 1000) / 1000; 292 | $longitude = int($longitude * 1000) / 1000; 293 | my $lat1 = int(($latitude - $degrees) * 10) / 10; # most westerly latitude 294 | my $lat2 = int(($latitude + $degrees) * 10) / 10; # most easterly latitude 295 | my $lon1 = int(($longitude - $degrees) * 10) / 10; # most northerly longitude 296 | my $lon2 = int(($longitude + $degrees) * 10) / 10; # most southerly longitude 297 | LOG("The resolution op the heatmap will be ${resolution}x${resolution}.","I"); 298 | LOG("The antenna latitude & longitude are: '$latitude','$longitude'.","I"); 299 | LOG("The heatmap will cover the area of $degrees degree around the antenna, which is between latitude $lat1 - $lat2 and longitude $lon1 - $lon2.","I"); 300 | #=============================================================================== 301 | # Get source data from data directory 302 | my %data; 303 | # Is the source directory readable? 304 | if (!-r $datadirectory) { 305 | LOG("The directory does not exists or you have no read permissions in data directory '$datadirectory'!","E"); 306 | exit 1; 307 | } 308 | # Set default filemask 309 | if (!$filemask) { 310 | $filemask = "'dump*.txt'" ; 311 | } else { 312 | $filemask ="'*$filemask*'"; 313 | } 314 | # Find files 315 | my @files =`find $datadirectory -name $filemask`; 316 | if (@files == 0) { 317 | LOG("No files were found in directory '$datadirectory' that matches with the $filemask filemask!","E"); 318 | exit 1; 319 | } else { 320 | LOG("The following files in directory '$datadirectory' fit with the filemask $filemask:","I"); 321 | my @tmp; 322 | foreach my $file (@files) { 323 | chomp($file); 324 | next if ($file =~ /log$|pid$/i); 325 | LOG(" $file","I"); 326 | push(@tmp,$file); 327 | } 328 | @files = @tmp; 329 | if (@files == 0) { 330 | LOG("No files were found in '$datadirectory' that matches with the '$filemask' filemask!","E"); 331 | exit 1; 332 | } 333 | } 334 | #=============================================================================== 335 | my %pos; 336 | my $lat; 337 | my $lon; 338 | open(my $outputdata, '>', "$outputdatafile") or die "Could not open file '$outputdatafile' $!"; 339 | print $outputdata "\"weight\";\"lat\";\"lon\""; 340 | # Read input files 341 | foreach my $filename (@files) { 342 | chomp($filename); 343 | # Read data file 344 | open(my $data_filehandle, '<', $filename) or die "Could not open file '$filename' $!"; 345 | LOG("Processing file '$filename':","I"); 346 | my $outside_area = 0; 347 | my $linecounter = 0; 348 | my @header; 349 | my %hdr; 350 | my $message=""; 351 | while (my $line = <$data_filehandle>) { 352 | chomp($line); 353 | $linecounter++; 354 | # Data Header 355 | # First line? 356 | if (($linecounter == 1) || ($line =~ /hex_ident/)){ 357 | if ($linecounter != 1) { 358 | $message .= "- ".($linecounter-1)." processed."; 359 | LOG($message,"I"); 360 | } 361 | # Reset fileunit: 362 | #%fileunit =(); 363 | # Does it contain header columns? 364 | if ($line =~ /hex_ident/) { 365 | @header = (); 366 | my @unit; 367 | # Header columns found! 368 | my @tmp = split(/,/,$line); 369 | foreach my $column (@tmp) { 370 | if ($column =~ /^\s*([^\(]+)\(([^\)]+)\)\s*$/) { 371 | # The column name includes a unit, for example: altitude(meter) 372 | push(@header,$1); 373 | #$fileunit{$1} = $2; 374 | push(@unit,"$1=$2"); 375 | } else { 376 | push(@header,$column); 377 | } 378 | } 379 | $message =" -header units:".join(",",@unit).", position $linecounter"; 380 | } else { 381 | # No header columns found. Use default! 382 | @header = ("hex_ident","altitude","latitude","longitude","date","time","angle","distance"); 383 | $message =" -default units, position $linecounter"; 384 | } 385 | # The file header unit information may be changed: set the units again. 386 | #setunits; 387 | my $columnnumber = 0; 388 | # Save column name with colomn number in hash. 389 | foreach my $header (@header) { 390 | $hdr{$header} = $columnnumber; 391 | $columnnumber++; 392 | } 393 | next if ($line =~ /hex_ident/); 394 | } 395 | # split columns into array values: 396 | my @col = split(/,/,$line); 397 | $lat = $col[$hdr{'latitude'}]; 398 | $lon = $col[$hdr{'longitude'}]; 399 | # remove lat/lon position that are to fare away. 400 | if (($lat < $lat1) || ($lat > $lat2) || ($lon < $lon1) || ($lon > $lon2)) { 401 | $outside_area++; 402 | next; 403 | } 404 | $lat = int((int(($lat - $latitude ) * $factor) / $factor + $latitude ) * 1000) / 1000; 405 | $lon = int((int(($lon - $longitude) * $factor) / $factor + $longitude) * 1000) / 1000; 406 | # count the number of time a lat/lon position was recorded: 407 | $pos{$lat}{$lon} = 0 if (!exists $pos{$lat}{$lon} ); 408 | $pos{$lat}{$lon} += 1; 409 | } 410 | LOG($message."-".($linecounter-1)." processed. $outside_area positions were out side the specified area.","I"); 411 | close($data_filehandle); 412 | } 413 | # Sort positions based on the number of times they occured in the flight position data. 414 | my %sort; 415 | foreach my $lat (keys %pos) { 416 | foreach my $lon (keys %{$pos{$lat}}) { 417 | my $number = sprintf("%08d",$pos{$lat}{$lon}); 418 | # Save lat/lon sorted by the number of times they were recorded 419 | $sort{"$number,$lat,$lon"} = 1; 420 | } 421 | } 422 | LOG("Number of sorted heatmap positions: ".(keys %sort),"I"); 423 | # Get the highest : 424 | my ($highest_weight,@rubbish)= reverse sort keys %sort; 425 | $highest_weight =~ s/,.+,.+$//; 426 | # Get lowest weight: 427 | my $counter = 0; 428 | my $lowest_weight; 429 | foreach my $sort (reverse sort keys %sort) { 430 | my ($weight,$lat,$lon) = split(/,/,$sort); 431 | $counter++; 432 | # stop after the maximum number of heatmap positions is reached or the weight to low: 433 | if (($counter >= $max_positions) || ($weight < 3)){ 434 | $lowest_weight = $weight; 435 | last; 436 | } 437 | } 438 | LOG("The highest weight is '$highest_weight' and the lowest weight is '$lowest_weight'.","I"); 439 | # Is the highest weight more then the maximum weight? 440 | if ($max_weight > $highest_weight){ 441 | $max_weight = $highest_weight; 442 | } else { 443 | LOG("Since the highest weight is more the the max weight '$max_weight' the weight of all points will be multiplied with a factor ".($max_weight / $highest_weight).".","I"); 444 | } 445 | # Proces the positions. Start with the positions that most occured in the flight position data. 446 | $counter = 0; 447 | foreach my $sort (reverse sort keys %sort) { 448 | my ($weight,$lat,$lon) = split(/,/,$sort); 449 | last if ($weight < 3); 450 | $weight = int(($max_weight / $highest_weight * $weight) + ($lowest_weight * $max_weight / $highest_weight * (($highest_weight - $weight) / $highest_weight)) + 1); 451 | $counter++; 452 | # stop after the maximum number of heatmap positions is reached: 453 | last if ($counter >= $max_positions); 454 | # print output to file: 455 | print $outputdata "\n\"$weight\";\"$lat\";\"$lon\""; 456 | } 457 | close($outputdata); 458 | chmod(0666,$outputdata); 459 | LOG("$counter rows with heatmap position data processed!","I"); 460 | -------------------------------------------------------------------------------- /img/heatmap-example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedsluis/dump1090.socket30003/5162816e1a32b84ae6251b47d33cb2e95dda3ea0/img/heatmap-example1.png -------------------------------------------------------------------------------- /img/rangeview-example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedsluis/dump1090.socket30003/5162816e1a32b84ae6251b47d33cb2e95dda3ea0/img/rangeview-example1.png -------------------------------------------------------------------------------- /install.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # ted.sluis@gmail.com 3 | # Filename : install.pl 4 | # 5 | #=============================================================================== 6 | BEGIN { 7 | use strict; 8 | use POSIX qw(strftime); 9 | use Time::Local; 10 | use Getopt::Long; 11 | use File::Basename; 12 | use File::Copy; 13 | use Math::Complex; 14 | use Cwd 'abs_path'; 15 | our $scriptname = basename($0); 16 | our $fullscriptname = abs_path($0); 17 | use lib dirname (__FILE__); 18 | use common; 19 | } 20 | # 21 | #=============================================================================== 22 | # Ctrl-C interupt handler 23 | $SIG{'INT'} = \&intHandler; 24 | 25 | sub intHandler { 26 | # Someone pressed Ctrl-C 27 | my $answer = common->ReadInput("CTRL-C was pressed. Do you want to exit '$scriptname'?","regular","y","n"); 28 | if ((!$answer) || ($answer =~ /^y$/i)) { 29 | LOG("Exiting '$scriptname'.....","I"); 30 | exit 1; 31 | } else { 32 | LOG("'$scriptname' is continuing.......","I"); 33 | } 34 | } 35 | #=============================================================================== 36 | # Get options 37 | my $help; 38 | my $datadirectory; 39 | my $installdirectory; 40 | my $logdirectory; 41 | my $outputdirectory; 42 | my $piddirectory; 43 | my $modify = "no"; 44 | GetOptions( 45 | "help!"=>\$help, 46 | "install=s"=>\$installdirectory, 47 | "data=s"=>\$datadirectory, 48 | "log=s"=>\$logdirectory, 49 | "pid=s"=>\$piddirectory, 50 | "output=s"=>\$outputdirectory, 51 | "modify!"=>\$modify, 52 | "debug!"=>\$debug, 53 | "verbose!"=>\$verbose 54 | ) or exit(1); 55 | $modify = "yes" if ($modify !~ /no/); 56 | print "modify=$modify\n"; 57 | # 58 | #=============================================================================== 59 | # if '-debug' parameter is used, set debug mode: 60 | common->setdebug if ($debug); 61 | # 62 | #=============================================================================== 63 | # if '-verbose' parameter is used, set verbose mode: 64 | common->LOGverbose if ($verbose); 65 | # 66 | #=============================================================================== 67 | # Checks if script runs interactive. 68 | my $interactive = common->InteractiveShellCheck; 69 | # 70 | #=============================================================================== 71 | # Log routine 72 | sub LOG(@){ 73 | common->LOG($logfile,@_); 74 | } 75 | # 76 | #=============================================================================== 77 | # Check options: 78 | if ($help) { 79 | print "\nThis $scriptname script installs the socket30003 scripts. 80 | It will create the directories, copy the files, check and set the permissions. 81 | In case of an update, it will backup the original config file and add new 82 | parameters if applicable. 83 | 84 | Please read this post for more info: 85 | http://discussions.flightaware.com/post180185.html#p180185 86 | 87 | Syntax: $scriptname 88 | 89 | Optional parameters: 90 | -install '/home/pi/socket30003' by default. 92 | -data The data files will be stored in 93 | '/tmp' by default. 94 | -log The log files will be stored in 95 | '/tmp' by default. 96 | -output '' by default. 98 | -pid The pid files will be stored in 99 | '/tmp'. 100 | -debug Displays raw socket messages. 101 | -verbose Displays verbose log messages. 102 | -help This help page. 103 | 104 | Examples: 105 | $scriptname 106 | $scriptname -install /user/share/socket30003 107 | $scriptname -data /home/pi/data -log /home/pi/log -output /home/pi/result \n\n"; 108 | exit 0; 109 | } 110 | 111 | # 112 | #=============================================================================== 113 | # Read settings from config file 114 | my %setting = common->READCONFIG('socket30003.cfg',$fullscriptname); 115 | # Use parameters & values from the 'install' section. If empty or not-exists, then use from the 'common' section, otherwise script defaults. 116 | $installdirectory = $installdirectory || $setting{'install'}{'installdirectory'} || "/home/pi/socket30003"; 117 | $datadirectory = $datadirectory || $setting{'install'}{'datadirectory'} || $setting{'common'}{'datadirectory'} || "/tmp"; 118 | $logdirectory = $logdirectory || $setting{'install'}{'logdirectory'} || $setting{'common'}{'logdirectory'} || "/tmp"; 119 | $piddirectory = $piddirectory || $setting{'install'}{'piddirectory'} || $setting{'common'}{'piddirectory'} || "/tmp"; 120 | $outputdirectory = $outputdirectory || $setting{'install'}{'outputdirectory'} || $setting{'common'}{'outputdirectory'} || "/tmp"; 121 | # 122 | #=============================================================================== 123 | # Does the directory exist? Is it readable and writable? 124 | $installdirectory = common->CheckDirectory('install',$installdirectory); 125 | #=============================================================================== 126 | # Look for old config file 127 | my %oldsetting; 128 | my $oldconfigfile = $installdirectory.'/socket30003.cfg'; 129 | if (-e $oldconfigfile) { 130 | LOG("Config file found in install directory! Reading config file....","I"); 131 | %oldsetting = common->READCONFIG('socket30003.cfg',$installdirectory.'/install.pl'); 132 | } 133 | #=============================================================================== 134 | # 135 | my $currentdirectory = dirname($fullscriptname); 136 | my $configfile = $currentdirectory.'/socket30003.cfg'; 137 | if (!-e $configfile) { 138 | LOG("The is no '$configfile' config file found in the source directory!","E"); 139 | exit 1; 140 | } 141 | my @cmd = `cat $configfile`; 142 | my $section; 143 | my @config; 144 | my %comment; 145 | my %checkconfigfile; 146 | foreach my $line (@cmd) { 147 | chomp($line); 148 | # save lines with comments: 149 | if ($line =~ /^\s*#\s$|^\s*#\s*[^=]*$|^\s*#\s*[^=]*$|^\s*#\s*[^=]*[=]{2,}[^=]*$/) { 150 | push(@config,$line); 151 | next; 152 | } 153 | # save blank lines: 154 | if ((!$line) || ($line =~ /^\s*$/)) { 155 | push(@config,""); 156 | next; 157 | } 158 | # Get section: 159 | if ($line =~ /^\s*\[([^\]]+)\]\s*(#.*)?$/) { 160 | $section = $1; 161 | push(@config,$line); 162 | LOG("Section: [$section]","I"); 163 | next; 164 | } elsif ($line =~ /^(#?[^=]+)=([^\#]*)(#.*)?$/) { 165 | # Get paramter & value 166 | my $parameter = $1; 167 | my $value = $2 || ""; 168 | my $comment = $3 || ""; 169 | # remove any white spaces at the begin and the end: 170 | $parameter =~ s/^\s*|\s*$//g; 171 | $value =~ s/^\s*|(\s*)$//g; 172 | # Add spaces to comment: 173 | $comment = ($1 || "").$comment; 174 | # Skip invalid lines: 175 | if ((!$parameter) || ($parameter =~ /^\s*$/)) { 176 | LOG("The line '$line' in config file '$configfile' is invalid! No parameter specified!","W"); 177 | next; 178 | } 179 | if ((!$section) || ($section =~ /^\s*$/)) { 180 | LOG("The line '$line' in config file '$configfile' is invalid! No section specified jet!","W"); 181 | next; 182 | } 183 | if ((exists $checkconfigfile{$section}) && ($checkconfigfile{$section}{$parameter})) { 184 | LOG("The line '$line' in config file '$configfile' already exists! It has value '$checkconfigfile{$section}{$parameter}'.","W"); 185 | } 186 | # Use value from existing config file if it is different: 187 | my $par = $parameter; 188 | my $param = $parameter; 189 | $par =~ s/^#//; 190 | # compare value existing config with source config. 191 | if ((exists $oldsetting{$section}) && ($oldsetting{$section}{$par}) && ($oldsetting{$section}{$par} !~ /^\Q$value\E$/)) { 192 | $val = $oldsetting{$section}{$par}; 193 | } else { 194 | $val = $value; 195 | } 196 | # compare parameter existing config with source config. 197 | if ((exists $oldsetting{$section}) && (exists $oldsetting{$section}{'#'.$par})) { 198 | $parameter = '#'.$par; 199 | } elsif ((exists $oldsetting{$section}) && (exists $oldsetting{$section}{$par})) { 200 | $parameter = $par; 201 | } 202 | $par = $param; 203 | # 204 | my $answer; 205 | if ($modify =~ /yes/) { 206 | if ($parameter =~ /^#/) { 207 | $answer = common->ReadInput("Do you want to enable this line: '$parameter=$value'?","multiplechoice",$modify,"n","y"); 208 | $parameter =~ s/^#// if ($answer =~ /y/); 209 | } else { 210 | $answer = common->ReadInput("Do you want to disable this line: '$parameter=$value'?","multiplechoice",$modify,"n","y"); 211 | $parameter = '#'.$parameter if ($answer =~ /y/); 212 | } 213 | } 214 | # 215 | if ($parameter !~ /^#/) { 216 | # save section, parameter & value 217 | $checkconfigfile{$section}{$parameter} = $value; 218 | # Ask what values should be used: 219 | if ($parameter =~ /filemask/) { 220 | $val = common->ReadInput("Which 'filemask' do you want to use?","filemask",$modify,"$val"); 221 | } elsif ($parameter =~ /^#?(\w+)directory$/) { 222 | print "VAL=$val\n"; 223 | $val = common->CheckDirectory($1,$val,$modify); 224 | } elsif ($parameter =~ /distanceunit/) { 225 | $val = common->ReadInput("Which 'distanceunit' do you want to use?","multiplechoice",$modify,"$val","kilometer","nauticalmile","mile","meter"); 226 | } elsif ($parameter =~ /altitudeunit/) { 227 | $val = common->ReadInput("Which 'altitudeunit' do you want to use?","multiplechoice",$modify,"$val","feet","meter"); 228 | } elsif ($parameter =~ /speedunit/) { 229 | $val = common->ReadInput("Which 'speedunit' do you want to use?","multiplechoice",$modify,"$val","kilometerph","knotph","mileph"); 230 | } elsif ($parameter =~ /(latitude|longitude|TIME_MESSAGE_MARGIN|degrees|resolution|max_positions|max_weight|maxaltitudemeter|maxaltitudefeet|minaltitude|numberofdirections|numberofaltitudezones)/) { 231 | $val = common->ReadInput("Which '$1' do you want to use?","numeric",$modify,"$val"); 232 | } elsif ($parameter =~ /(override|timestamp|sequencenumber|showpositions)/) { 233 | $val = common->ReadInput("Which value for '$1' do you want to use?","multiplechoice",$modify,$val,"yes","no"); 234 | } elsif ($parameter =~ /outputdatafile/) { 235 | $val = common->ReadInput("Which 'outputdatafile' name do you want to use?","file",$modify,"$val"); 236 | } elsif ($parameter =~ /PEER_HOST/) { 237 | $val = common->ReadInput("Which 'PEER_HOST' name do you want to use?","ipaddress",$modify,"$val"); 238 | } 239 | } 240 | # Value different as source config value? 241 | if ((($parameter !~ /^#/) && ($value !~ /\Q$val\E/)) || (($parameter !~ /\Q$par\E/))) { 242 | if ((!exists $comment{$section}{"$line"}) || (!exists $comment{$section}{"#$line"})) { 243 | $line = "#".$line if ($line !~ /^#/); 244 | $comment{$section}{"$line"} = ""; 245 | # Save config value as comment: 246 | push(@config,$line); 247 | LOG($line,"L"); 248 | } 249 | my $changepar = "(parameter:$par)"; 250 | $changepar = "(parameter:$par > $parameter)" if ($parameter !~ /\Q$par\E/); 251 | my $changeval = "(value:$value)"; 252 | $changeval= "(value:$value > $val)" if ($value !~ /\Q$val\E/); 253 | LOG("from existing config: $changepar, $changeval","I"); 254 | push(@config,"${parameter}=${val}${comment}"); 255 | LOG("${parameter}=${val}${comment}","L"); 256 | } else { 257 | push(@config,$line); 258 | LOG($line,"L"); 259 | } 260 | } else { 261 | # Invalid line: 262 | LOG("The line '$line' in config file '$configfile' is invalid!","W"); 263 | LOG("Valid lines looks like:","I"); 264 | LOG("# comment line","I"); 265 | LOG("[some_section_name]","I"); 266 | LOG("parameter=value","I"); 267 | LOG("Comment text (started with #) behind a section or parameter=value is allowed!","I"); 268 | next; 269 | } 270 | } 271 | if ($modify =~ /no/) { 272 | $logdirectory = common->CheckDirectory('log',$logdirectory,"no"); 273 | $piddirectory = common->CheckDirectory('pid',$piddirectory,"no"); 274 | $datadirectory = common->CheckDirectory('data',$datadirectory,"no"); 275 | $outputdirectory = common->CheckDirectory('output',$outputdirectory,"no"); 276 | } 277 | #=============================================================================== 278 | # Set log file path and name 279 | my ($second,$day,$month,$year,$minute,$hour) = (localtime)[0,3,4,5,1,2]; 280 | my $filedate = 'install-'.sprintf '%02d%02d%02d', $year-100,($month+1),$day; 281 | $logfile = common->LOGset($logdirectory,"$filedate.log",$verbose); 282 | # 283 | #=============================================================================== 284 | # 285 | LOG("Current directory: $currentdirectory","I"); 286 | if ($installdirectory =~ /^\Q$currentdirectory\E\/?$/i) { 287 | LOG("De source directory '$currentdirectory' is the same as the install direcory, so no script files will be copied.","I"); 288 | } else { 289 | my @cmd = `find . -name '*' | grep -P "socket30003.cfg|rangeview.pl|common.pm|heatmap.pl|install.pl|socket30003.pl"`; 290 | foreach my $filepath (@cmd) { 291 | chomp($filepath); 292 | my $file = basename($filepath); 293 | print "file=$file, $filepath, $installdirectory/$file\n"; 294 | copy($filepath,"$installdirectory/$file") or die "Copy failed: $!"; 295 | } 296 | } 297 | my $cfg="$installdirectory/socket30003.cfg"; 298 | @cmd=`sed -i "s|^outputdirectory=.*|outputdirectory=$outputdirectory|g" $cfg`; 299 | @cmd=`sed -i "s|^datadirectory=.*|datadirectory=$datadirectory|g" $cfg`; 300 | @cmd=`sed -i "s|^logdirectory=.*|logdirectory=$logdirectory|g" $cfg`; 301 | @cmd=`sed -i "s|^piddirectory=.*|piddirectory=$piddirectory|g" $cfg`; 302 | print "Finished!\n"; 303 | -------------------------------------------------------------------------------- /rangeview.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # ted.sluis@gmail.com 3 | # Filename : rangeview.pl 4 | # 5 | #=============================================================================== 6 | BEGIN { 7 | use strict; 8 | use POSIX qw(strftime); 9 | use Time::Local; 10 | use Getopt::Long; 11 | use File::Basename; 12 | use Math::Complex; 13 | use Cwd 'abs_path'; 14 | our $scriptname = basename($0); 15 | our $fullscriptname = abs_path($0); 16 | use lib dirname (__FILE__); 17 | use common; 18 | } 19 | # 20 | #=============================================================================== 21 | # Ctrl-C interupt handler 22 | $SIG{'INT'} = \&intHandler; 23 | 24 | sub intHandler { 25 | # Someone pressed Ctrl-C 26 | LOG("CTRL-C was pressed. Do you want to exit '$scriptname'? (y/n)","W"); 27 | my $answer = ; 28 | if ($answer =~ /^y$/i) { 29 | LOG("Exiting '$scriptname'.....","I"); 30 | exit 1; 31 | } else { 32 | LOG("'$scriptname' is continuing.......","I"); 33 | } 34 | } 35 | #=============================================================================== 36 | # Get options 37 | my $help; 38 | my $datadirectory; 39 | my $logdirectory; 40 | my $outputdirectory; 41 | my $outputdatafile; 42 | my $override; 43 | my $timestamp; 44 | my $sequencenumber, 45 | my $filemask; 46 | my $max_altitude; 47 | my $max_altitude_meter; 48 | my $max_altitude_feet; 49 | my $min_altitude; 50 | my $number_of_directions; 51 | my $number_of_altitudezones; 52 | my $distanceunit; 53 | my $altitudeunit; 54 | my $antenna_longitude; 55 | my $antenna_latitude; 56 | GetOptions( 57 | "help!"=>\$help, 58 | "filemask=s"=>\$filemask, 59 | "data=s"=>\$datadirectory, 60 | "log=s"=>\$logdirectory, 61 | "output=s"=>\$outputdirectory, 62 | "file=s"=>\$outputdatafile, 63 | "override!"=>\$override, 64 | "timestamp!"=>\$timestamp, 65 | "sequencenumber!"=>\$sequencenumber, 66 | "longitude=s"=>\$antenna_longitude, 67 | "latitude=s"=>\$antenna_latitude, 68 | "distanceunit=s"=>\$distanceunit, 69 | "altitudeunit=s"=>\$altitudeunit, 70 | "max=s"=>\$max_altitude, 71 | "min=s"=>\$min_altitude, 72 | "directions=s"=>\$number_of_directions, 73 | "zones=s"=>\$number_of_altitudezones, 74 | "debug!"=>\$debug, 75 | "verbose!"=>\$verbose 76 | ) or exit(1); 77 | # 78 | $override = "yes" if ($override); 79 | $timestamp = "yes" if ($timestamp); 80 | $sequencenumber = "yes" if ($sequencenumber); 81 | # 82 | # 83 | #=============================================================================== 84 | # if '-debug' parameter is used, set debug mode: 85 | common->setdebug if ($debug); 86 | # 87 | #=============================================================================== 88 | # if '-verbose' parameter is used, set verbose mode: 89 | common->LOGverbose if ($verbose); 90 | # 91 | #=============================================================================== 92 | # Checks if script runs interactive. 93 | my $interactive = common->InteractiveShellCheck; 94 | # 95 | #=============================================================================== 96 | # Log routine 97 | sub LOG(@){ 98 | common->LOG($logfile,@_); 99 | } 100 | # 101 | #=============================================================================== 102 | # Read settings from config file 103 | my %setting = common->READCONFIG('socket30003.cfg',$fullscriptname); 104 | # Use parameters & values from the 'rangeview' section. If empty or not-exists, then use from the 'common' section, otherwise script defaults. 105 | $number_of_directions = $number_of_directions || $setting{'rangeview'}{'numberofdirections'} || 1440; # 106 | $number_of_altitudezones = $number_of_altitudezones || $setting{'rangeview'}{'numberofaltitudezones'} || 24; 107 | $max_altitude_meter = $max_altitude || $setting{'rangeview'}{'maxaltitudemeter'} || 12000; # specified in meter 108 | $max_altitude_feet = $max_altitude || $setting{'rangeview'}{'maxaltitudefeet'} || 36000; # specified in feet 109 | $min_altitude = $min_altitude || $setting{'rangeview'}{'minaltitude'} || "0"; # specified in the output unit 110 | $outputdatafile = $outputdatafile || $setting{'rangeview'}{'outputdatafile'} || "rangeview.kml"; # KML or CSV extention 111 | $datadirectory = $datadirectory || $setting{'rangeview'}{'datadirectory'} || $setting{'common'}{'datadirectory'} || "/tmp"; 112 | $logdirectory = $logdirectory || $setting{'rangeview'}{'logdirectory'} || $setting{'common'}{'logdirectory'} || "/tmp"; 113 | $outputdirectory = $outputdirectory || $setting{'rangeview'}{'outputdirectory'} || $setting{'common'}{'outputdirectory'} || "/tmp"; 114 | $filemask = $filemask || $setting{'rangeview'}{'filemask'} || $setting{'common'}{'filemask'} || "dump*txt"; 115 | $override = $override || $setting{'rangeview'}{'override'} || $setting{'common'}{'override'} || "no"; # override output file if exists. 116 | $timestamp = $timestamp || $setting{'rangeview'}{'timestamp'} || $setting{'common'}{'timestamp'} || "no"; # add timestamp to output file name. 117 | $sequencenumber = $sequencenumber || $setting{'rangeview'}{'sequencenumber'} || $setting{'common'}{'sequencenumber'} || "no"; # add sequence number to output file name. 118 | $antenna_latitude = $antenna_latitude || $setting{'rangeview'}{'latitude'} || $setting{'common'}{'latitude'} || 52.085624; # Home location, default (Utrecht, The Netherlands) 119 | $antenna_longitude= $antenna_longitude || $setting{'rangeview'}{'longitude'} || $setting{'common'}{'longitude'} || 5.0890591; 120 | $distanceunit =($distanceunit || $setting{'rangeview'}{'distanceunit'} || $setting{'common'}{'distanceunit'} || "kilometer").','. 121 | ($distanceunit || $setting{'rangeview'}{'distanceunit'} || $setting{'common'}{'distanceunit'} || "kilometer"); # specify input & output unit! kilometer, nauticalmile, mile or meter 122 | $altitudeunit =($altitudeunit || $setting{'rangeview'}{'altitudeunit'} || $setting{'common'}{'altitudeunit'} || "meter").','. 123 | ($altitudeunit || $setting{'rangeview'}{'altitudeunit'} || $setting{'common'}{'altitudeunit'} || "meter"); # specify input & output unit! meter or feet 124 | # 125 | #=============================================================================== 126 | # Is the log directory writeable? 127 | if (!-w $logdirectory) { 128 | LOG("The log directory does not exists or you have no write permissions in '$logdirectory'!","E"); 129 | exit 1; 130 | } 131 | # Set log file path and name 132 | my ($second,$day,$month,$year,$minute,$hour) = (localtime)[0,3,4,5,1,2]; 133 | my $filedate = 'rangeview-'.sprintf '%02d%02d%02d', $year-100,($month+1),$day; 134 | $logfile = common->LOGset($logdirectory,"$filedate.log",$verbose); 135 | # 136 | #=============================================================================== 137 | # 138 | my %fileunit; 139 | # defaultdistanceunit 140 | my %distanceunit; 141 | my $error = 0; 142 | if ($distanceunit) { 143 | my @defaultdistanceunit = split(/,/,$distanceunit); 144 | if ($defaultdistanceunit[0] =~ /^kilometer$|^nauticalmile$|^mile$|^meter$/i) { 145 | $distanceunit{'in'} = lc($defaultdistanceunit[0]); 146 | if (defined $defaultdistanceunit[1]) { 147 | if ($defaultdistanceunit[1] =~ /^kilometer$|^nauticalmile$|^mile$|^meter$/i) { 148 | $distanceunit{'out'} = lc($defaultdistanceunit[1]); 149 | } else { 150 | $error++; 151 | } 152 | } else { 153 | $distanceunit{'out'} = lc($defaultdistanceunit[0]); 154 | } 155 | } else { 156 | $error++; 157 | } 158 | } else { 159 | $distanceunit{'in'} = "kilometer"; 160 | $distanceunit{'out'} = "kilometer"; 161 | } 162 | if ($error) { 163 | LOG("The default distance unit '$distanceunit' is invalid! It should be one of these: kilometer, nauticalmile, mile or meter.","E"); 164 | LOG("If you specify two units (seperated by a comma) then the first is for incomming flight position data and the second is for the range/altitude view output file.","E"); 165 | LOG("for example: '-distanceunit kilometer' or '-distanceunit kilometer,nauticalmile'","E"); 166 | exit 1; 167 | } 168 | # defaultaltitudeunit 169 | my %altitudeunit; 170 | $error = 0; 171 | if ($altitudeunit) { 172 | my @defaultaltitudeunit = split(/,/,$altitudeunit); 173 | if ($defaultaltitudeunit[0] =~ /^meter$|^feet$/i) { 174 | $altitudeunit{'in'} = lc($defaultaltitudeunit[0]); 175 | if (defined $defaultaltitudeunit[1]) { 176 | if ($defaultaltitudeunit[1] =~ /^meter$|^feet$/i) { 177 | $altitudeunit{'out'} = lc($defaultaltitudeunit[1]); 178 | } else { 179 | $error++; 180 | } 181 | } else { 182 | $altitudeunit{'out'} = lc($defaultaltitudeunit[0]); 183 | } 184 | } else { 185 | $error++; 186 | } 187 | } else { 188 | $altitudeunit{'in'} = "meter"; 189 | $altitudeunit{'out'} = "meter"; 190 | } 191 | if ($error) { 192 | LOG("The default altitude unit '$altitudeunit' is invalid! It should be one of these: meter or feet.","E"); 193 | LOG("If you specify two units (seperated by a comma) then the first is for incomming flight position data and the second is for the range/altitude view output file.","E"); 194 | LOG("for example: '-distanceunit meter' or '-distanceunit feet,meter'","E"); 195 | exit 1; 196 | } 197 | # Get correct max altitude: 198 | if ($altitudeunit{'out'} =~ /feet/) { 199 | $max_altitude = $max_altitude_feet; 200 | } else { 201 | $max_altitude = $max_altitude_meter; 202 | } 203 | 204 | # 205 | #=============================================================================== 206 | # Check options: 207 | if ($help) { 208 | print "\nThis $scriptname script creates location data 209 | for a range/altitude view which can be displated in a modified 210 | fork of dump1090-mutobility. 211 | 212 | The script creates two output files: 213 | rangeview.csv) A file with location data in csv format can be 214 | imported in to tools like http://www.gpsvisualizer.com. 215 | rangeview.kml) A file with location data in kml format, which 216 | can be imported into a modified dum1090-mutability. 217 | 218 | Please read this post for more info: 219 | http://discussions.flightaware.com/post180185.html#p180185 220 | 221 | This script uses the output file(s) of the 'socket30003.pl' 222 | script, which are by default stored in /tmp in this format: 223 | dump1090--YYMMDD.txt 224 | 225 | It will read the files one by one and it will automaticly use 226 | the correct units (feet, meter, mile, nautical mile of kilometer) 227 | for 'altitude' and 'distance' when the input files contain 228 | column headers with the unit type between parentheses. When 229 | the input files doesn't contain column headers (as produced 230 | by older versions of 'socket30003.pl' script) you can specify 231 | the units.Otherwise this script will use the default units. 232 | 233 | The flight position data is sorted in to altitude zones. For 234 | each zone and for each direction the most remote location is 235 | saved. The most remote locations per altitude zone will be 236 | written to a file as a track. 237 | 238 | Default .kml output format: 239 | 240 | 241 | 242 | Paths 243 | Example 244 | 253 | 254 | 1 255 | 00000- 500 256 | #track-1 257 | 258 | absolute 259 | 260 | 5.08865,52.00493,357 261 | 5.08808,52.00616,357 262 | 5.08722,52.00788,357 263 | 5.08667,52.00914,357 264 | 5.08604,52.01039,357 265 | 5.08560,52.01125,357 266 | 5.08518,52.01230,357 267 | 5.08461,52.01335,357 268 | 5.08400,52.01463,357 269 | 5.08345,52.01579,357 270 | 5.08293,52.01683,357 271 | 272 | Optional CSV output format: 273 | type,new_track,name,color,trackpoint,altitudezone,destination,hex_ident,Altitude(meter),latitude,longitude,date,time,angle,distance(kilometer) 274 | T,1,Altitude zone 1: 00000- 500,7fffff00,1, 0,-718,484646,357,52.00493,5.08865,2017/01/10,10:46:15.738,-179.72,8 275 | T,0,Altitude zone 1: 00000- 500,7fffff00,2, 0,-717,484646,357,52.00616,5.08808,2017/01/10,10:46:17.164,-179.32,8 276 | T,0,Altitude zone 1: 00000- 500,7fffff00,3, 0,-714,484646,357,52.00788,5.08722,2017/01/10,10:46:19.740,-178.7,8 277 | T,0,Altitude zone 1: 00000- 500,7fffff00,4, 0,-713,484646,357,52.00914,5.08667,2017/01/10,10:46:21.041,-178.28,8 278 | T,0,Altitude zone 1: 00000- 500,7fffff00,5, 0,-711,484646,357,52.01039,5.08604,2017/01/10,10:46:22.622,-177.79,8 279 | T,0,Altitude zone 1: 00000- 500,7fffff00,6, 0,-709,484646,357,52.01125,5.08560,2017/01/10,10:46:23.892,-177.44,8 280 | T,0,Altitude zone 1: 00000- 500,7fffff00,7, 0,-708,484646,357,52.01230,5.08518,2017/01/10,10:46:25.244,-177.09,8 281 | T,0,Altitude zone 1: 00000- 500,7fffff00,8, 0,-706,484646,357,52.01335,5.08461,2017/01/10,10:46:26.625,-176.62,8 282 | T,0,Altitude zone 1: 00000- 500,7fffff00,9, 0,-704,484646,357,52.01463,5.08400,2017/01/10,10:46:28.031,-176.09,7 283 | T,0,Altitude zone 1: 00000- 500,7fffff00,10, 0,-702,484646,357,52.01579,5.08345,2017/01/10,10:46:29.475,-175.59,7 284 | T,0,Altitude zone 1: 00000- 500,7fffff00,11, 0,-700,484646,357,52.01683,5.08293,2017/01/10,10:46:30.940,-175.11,7 285 | 286 | Syntax: $scriptname 287 | 288 | Optional parameters: 289 | -data The data files are stored in 290 | '$datadirectory' by default. 291 | -log The log files are stored in 292 | '$logdirectory' by default. 293 | -output '$outputdirectory' by default. 295 | -file The output file name. The extention 296 | (.kml or .csv) determines the 297 | file structure! 298 | '$outputdatafile' by default. 299 | -filemask Specify a filemask. 300 | The default filemask is '$filemask'. 301 | -override Override output file if exists. 302 | Default is '$override'. 303 | -timestamp Add timestamp to output file name. 304 | Default is '$timestamp'. 305 | -sequencenumber Add sequence number to output file name. 306 | Default is '$sequencenumber'. 307 | -max Upper limit. Default is '$max_altitude $altitudeunit{'out'}'. 308 | Higher values in the input data will be skipped. 309 | -min Lower limit. Default is '$min_altitude $altitudeunit{'out'}'. 310 | Lower values in the input data will be skipped. 311 | -directions Number of compass direction (pie slices). 312 | Minimal 8, maximal 7200. Default = '$number_of_directions'. 313 | -zones Number of altitude zones. 314 | Minimal 1, maximum 99. 315 | Default = '$number_of_altitudezones'. 316 | -lon Location of your antenna. 317 | -lat 318 | -distanceunit ,[] 319 | Type of unit: kilometer, nauticalmile, 320 | mile or meter. First unit is for the 321 | incoming source, the file(s) with flight 322 | positions. The second unit is for the 323 | output file. No unit means it is the 324 | same as incoming. 325 | Default distance unit's are: 326 | '$distanceunit'. 327 | -altitudeunit [,] 328 | Type of unit: feet or meter. First unit 329 | is for the incoming source, the file(s) 330 | with flight positions. The second unit 331 | is for the output file. No unit means it 332 | is the same as incoming. 333 | Default altitude unit's are: 334 | '$altitudeunit'. 335 | -debug Displays raw socket messages. 336 | -verbose Displays verbose log messages. 337 | -help This help page. 338 | 339 | notes: 340 | - The default values can be changed within the config file 'socket30003.cfg'. 341 | - The source units will be overruled in case the input file header contains unit information. 342 | 343 | Examples: 344 | $scriptname 345 | $scriptname -distanceunit kilometer,nauticalmile -altitudeunit meter,feet 346 | $scriptname -data /home/pi/data -log /home/pi/log -output /home/pi/result \n\n"; 347 | exit 0; 348 | } 349 | #=============================================================================== 350 | print "The altitude will be converted from '$altitudeunit{'in'}' to '$altitudeunit{'out'}'.\n"; 351 | print "The distance will be converted from '$distanceunit{'in'}' to '$distanceunit{'out'}.\n"; 352 | my %convertalt; 353 | my %convertdis; 354 | #=============================================================================== 355 | # Set unit for altitude and distance 356 | sub setunits(@) { 357 | # altitude unit: 358 | $convertalt{'in'} = 1 if ($altitudeunit{'in'} eq "meter"); 359 | $convertalt{'out'} = 1 if ($altitudeunit{'out'} eq "meter"); 360 | $convertalt{'in'} = 0.3048 if ($altitudeunit{'in'} eq "feet"); 361 | $convertalt{'out'} = 3.2808399 if ($altitudeunit{'out'} eq "feet"); 362 | # altitude unit is overruled in case the input file header contains unit information: 363 | $convertalt{'in'} = 1 if ((exists $fileunit{'altitude'}) && ($fileunit{'altitude'} eq "meter")); 364 | $convertalt{'in'} = 0.3048 if ((exists $fileunit{'altitude'}) && ($fileunit{'altitude'} eq "feet")); 365 | # distance 366 | $convertdis{'in'} = 1 if ($distanceunit{'in'} eq "meter"); 367 | $convertdis{'out'} = 1 if ($distanceunit{'out'} eq "meter"); 368 | $convertdis{'in'} = 1609.344 if ($distanceunit{'in'} eq "mile"); 369 | $convertdis{'out'} = 0.000621371192 if ($distanceunit{'out'} eq "mile"); 370 | $convertdis{'in'} = 1852 if ($distanceunit{'in'} eq "nauticalmile"); 371 | $convertdis{'out'} = 0.000539956803 if ($distanceunit{'out'} eq "nauticalmile"); 372 | $convertdis{'in'} = 1000 if ($distanceunit{'in'} eq "kilometer"); 373 | $convertdis{'out'} = 0.001 if ($distanceunit{'out'} eq "kilometer"); 374 | # distance unit is overruled in case the input file header contains unit information: 375 | $convertdis{'in'} = 1 if ((exists $fileunit{'distance'}) && ($fileunit{'distance'} eq "meter")); 376 | $convertdis{'in'} = 1609.344 if ((exists $fileunit{'distance'}) && ($fileunit{'distance'} eq "mile")); 377 | $convertdis{'in'} = 1852 if ((exists $fileunit{'distance'}) && ($fileunit{'distance'} eq "nauticalmile")); 378 | $convertdis{'in'} = 1000 if ((exists $fileunit{'distance'}) && ($fileunit{'distance'} eq "kilometer")); 379 | } 380 | setunits; 381 | # convert altitude to the correct unit: 382 | sub alt(@) { 383 | my $altitude = shift; 384 | my $altitude_in_meters = $convertalt{'in'} * $altitude; 385 | my $result = int($convertalt{'out'} * $altitude_in_meters); 386 | return $result; 387 | } 388 | # convert distance to the correct unit: 389 | sub dis(@) { 390 | my $distance = shift; 391 | my $distance_in_meters = $convertdis{'in'} * $distance; 392 | my $result = int($convertdis{'out'} * $distance_in_meters); 393 | } 394 | # 395 | #=============================================================================== 396 | # Is the specified directories for the output file writeable? 397 | if (!-w $outputdirectory) { 398 | LOG("The output directory does not exists or you have no write permissions in '$datadirectory'!","E"); 399 | exit 1; 400 | } 401 | # check file name extention 402 | if ($outputdatafile =~ /\.csv$|\.kml/i) { 403 | # Set output file 404 | $outputdatafile = common->SetOutput($outputdirectory,$outputdatafile,$override,$timestamp,$sequencenumber); 405 | } else { 406 | LOG("The output file name '$outputdatafile' is invalid! It should have a .kml or .csv extention!","E"); 407 | exit 1; 408 | } 409 | #=============================================================================== 410 | $error=0; 411 | if ((($max_altitude) && ($max_altitude !~ /^\d+$/)) || ($max_altitude > (20000 * $convertalt{'out'})) || ($max_altitude <= $min_altitude)) { 412 | LOG("The maximum altitude ($max_altitude $altitudeunit{'out'}) is not valid! It should be at least as high as the minium altitude ($min_altitude $altitudeunit{'out'}), but not higher than ".(20000 * $convertalt{'out'})." $altitudeunit{'out'}!","E"); 413 | $error++; 414 | } else { 415 | LOG("The maximum altitude is $max_altitude $altitudeunit{'out'}.","I"); 416 | } 417 | if ((($min_altitude) && ($min_altitude !~ /^\d+$/)) || ($min_altitude < 0) || ($min_altitude >= $max_altitude)) { 418 | LOG("The minium altitude ($min_altitude $altitudeunit{'out'}) is not valid! It should be less than the maximum altitude ($max_altitude $altitudeunit{'out'}), but not less than 0 $altitudeunit{'out'}!","E"); 419 | $error++; 420 | } else { 421 | LOG("The minimal altitude is $min_altitude $altitudeunit{'out'}.","I"); 422 | } 423 | if ((($number_of_directions) && ($number_of_directions !~ /^\d+$/)) || ($number_of_directions < 8) || ($number_of_directions > 7200)) { 424 | LOG("The number of compass directions ($number_of_directions) is invalid! It should be at least 8 and less then 7200.","E"); 425 | $error++; 426 | } else { 427 | LOG("The number of compass directions (pie slices) is $number_of_directions.","I"); 428 | } 429 | if ((($number_of_altitudezones) &&($number_of_altitudezones !~ /^\d+$/)) || ($number_of_altitudezones < 1) || ($number_of_altitudezones > 99)) { 430 | LOG("The number of altitude zones ($number_of_altitudezones) is invalid! It should be at least 1 and less than 100.","E"); 431 | } else { 432 | LOG("The number of altitude zones is $number_of_altitudezones.","I"); 433 | } 434 | if ($error > 0) { 435 | exit 1; 436 | } 437 | # longitude & latitude 438 | $antenna_longitude =~ s/,/\./ if ($antenna_longitude); 439 | if ($antenna_longitude !~ /^[-+]?\d+(\.\d+)?$/) { 440 | LOG("The specified longitude '$antenna_longitude' is invalid!","E"); 441 | exit 1; 442 | } 443 | $antenna_latitude =~ s/,/\./ if ($antenna_latitude); 444 | if ($antenna_latitude !~ /^[-+]?\d+(\.\d+)?$/) { 445 | LOG("The specified latitude '$antenna_latitude' is invalid!","E"); 446 | exit 1; 447 | } 448 | LOG("The latitude/longitude location of the antenna is: $antenna_latitude,$antenna_longitude.","I"); 449 | # 450 | #=============================================================================== 451 | my $diff_altitude = $max_altitude - $min_altitude; 452 | my $zone_altitude = int($diff_altitude / $number_of_altitudezones); 453 | LOG("An altitude zone is $zone_altitude $altitudeunit{'out'}.","I"); 454 | # 455 | #=============================================================================== 456 | # Get source file names 457 | my @files = common->GetSourceData($datadirectory,$filemask); 458 | #=============================================================================== 459 | my %data; 460 | my $filecounter=0; 461 | my $positioncounter=0; 462 | my %positionperzonecounter; 463 | my %positionperdirectioncounter; 464 | my $position; 465 | # Read input files 466 | foreach my $filename (@files) { 467 | LOG("processing '$filename':","I"); 468 | $filecounter++; 469 | chomp($filename); 470 | # Read data file 471 | open(my $data_filehandle, '<', $filename) or die "Could not open file '$filename' $!"; 472 | my $linecounter = 0; 473 | my @header; 474 | my %hdr; 475 | my $message; 476 | while (my $line = <$data_filehandle>) { 477 | chomp($line); 478 | $linecounter++; 479 | # Data Header 480 | # First line? 481 | if (($linecounter == 1) || ($line =~ /hex_ident/)){ 482 | if ($linecounter != 1){ 483 | $message .= "- ".($linecounter-1)." processed."; 484 | LOG($message,"I"); 485 | } 486 | # Reset fileunit: 487 | %fileunit =(); 488 | # Does it contain header columns? 489 | if ($line =~ /hex_ident/) { 490 | @header = (); 491 | my @unit; 492 | # Header columns found! 493 | my @tmp = split(/,/,$line); 494 | foreach my $column (@tmp) { 495 | if ($column =~ /^\s*([^\(]+)\(([^\)]+)\)\s*$/) { 496 | # The column name includes a unit, for example: altitude(meter) 497 | push(@header,$1); 498 | $fileunit{$1} = $2; 499 | push(@unit,"$1=$2"); 500 | } else { 501 | push(@header,$column); 502 | } 503 | } 504 | $message = " -header units:".join(",",@unit).", position $linecounter"; 505 | } else { 506 | # No header columns found. Use default! 507 | @header = ("hex_ident","altitude","latitude","longitude","date","time","angle","distance"); 508 | $message = " -default units:altitude=$altitudeunit{'in'},distance=$distanceunit{'in'}, position $linecounter"; 509 | } 510 | # The file header unit information may be changed: set the units again. 511 | setunits; 512 | my $columnnumber = 0; 513 | # Save column name with colomn number in hash. 514 | foreach my $header (@header) { 515 | $hdr{$header} = $columnnumber; 516 | $columnnumber++; 517 | } 518 | next if ($line =~ /hex_ident/); 519 | } 520 | # split line in to columns. 521 | my @col = split(/,/,$line); 522 | $position++; 523 | my $altitude = alt($col[$hdr{'altitude'}]); 524 | my $distance = dis($col[$hdr{'distance'}]); 525 | # Remove any invalid position bigger than 600km 526 | next if ($distance > (600000 * $convertdis{'out'})); 527 | # Skip lower then min_altitude. 528 | next if ($altitude < $min_altitude); 529 | # Skip higher then max_altitude is the highest zone: 530 | next if ($altitude > $max_altitude); 531 | # Calculate the altitude zone and direction zone 532 | my $altitude_zone = sprintf("% 6d",int($altitude / $zone_altitude) * $zone_altitude); 533 | my $direction_zone = sprintf("% 4d",int($col[$hdr{'angle'}] / 360 * $number_of_directions)); 534 | # Update the counters for statictics 535 | $positioncounter++; 536 | $positionperzonecounter{$altitude_zone} = 0 if (!exists $positionperzonecounter{$altitude_zone}); 537 | $positionperzonecounter{$altitude_zone}++; 538 | $positionperdirectioncounter{$altitude_zone}{$direction_zone} = 0 if (! exists $positionperdirectioncounter{$altitude_zone}{$direction_zone}); 539 | $positionperdirectioncounter{$altitude_zone}{$direction_zone}++; 540 | # Save position if it is the most fare away location for it's altitude zone and direction zoe: 541 | if ((!exists $data{$altitude_zone}||(!exists $data{$altitude_zone}{$direction_zone})||($data{$altitude_zone}{$direction_zone}{'distance'} < $col[$hdr{'distance'}]))) { 542 | $data{$altitude_zone}{$direction_zone}{'distance'} = int($distance * 100) / 100; 543 | $data{$altitude_zone}{$direction_zone}{'hex_ident'} = $col[$hdr{'hex_ident'}]; 544 | $data{$altitude_zone}{$direction_zone}{'altitude'} = int($altitude); 545 | $data{$altitude_zone}{$direction_zone}{'latitude'} = $col[$hdr{'latitude'}]; 546 | $data{$altitude_zone}{$direction_zone}{'longitude'} = $col[$hdr{'longitude'}]; 547 | $data{$altitude_zone}{$direction_zone}{'date'} = $col[$hdr{'date'}]; 548 | $data{$altitude_zone}{$direction_zone}{'time'} = $col[$hdr{'time'}]; 549 | $data{$altitude_zone}{$direction_zone}{'angle'} = int($col[$hdr{'angle'}] * 100) / 100; 550 | 551 | } 552 | } 553 | close($data_filehandle); 554 | $message .= "-".($linecounter-1).". processed."; 555 | LOG($message,"I"); 556 | } 557 | LOG("Number of files read: $filecounter","I"); 558 | LOG("Number of position processed: $position and positions within range processed: $positioncounter","I"); 559 | #=============================================================================== 560 | # convert hsl colors to bgr colors 561 | sub hsl_to_bgr(@) { 562 | my ($h, $s, $l) = @_; 563 | my ($r, $g, $b); 564 | if ($s == 0){ 565 | $r = $g = $b = $l; 566 | } else { 567 | sub hue2rgb(@){ 568 | my ($p, $q, $t) = @_; 569 | while ($t < 0) { $t += 1; } 570 | while ($t > 1) { $t -= 1; } 571 | if ($t < (1/6)) { return $p + ($q - $p) * 6 * $t; } 572 | if ($t < (1/2)) { return $q; } 573 | if ($t < (2/3)) { return $p + ($q - $p) * (2/3 - $t) * 6; } 574 | return $p; 575 | } 576 | my $q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s; 577 | my $p = 2 * $l - $q; 578 | $r = hue2rgb($p, $q, $h + 1/3); 579 | $g = hue2rgb($p, $q, $h); 580 | $b = hue2rgb($p, $q, $h - 1/3); 581 | } 582 | $r = sprintf("%x",int($r * 255)); 583 | $g = sprintf("%x",int($g * 255)); 584 | $b = sprintf("%x",int($b * 255)); 585 | return $b.$g.$r; 586 | } 587 | #================================================================================ 588 | # 589 | my @zone; 590 | foreach my $altitude_zone (sort {$a<=>$b} keys %data) { 591 | foreach my $dz (0..$number_of_directions) { 592 | $direction_zone = sprintf("% 4d",$dz); 593 | foreach my $previous_altitude_zone (@zone) { 594 | # Higher altitude zones reache atleast as far as the lower altitude zones. 595 | if ((exists $data{$previous_altitude_zone}) && (exists $data{$previous_altitude_zone}{$direction_zone}) && 596 | ((!exists $data{$altitude_zone}{$direction_zone}) || 597 | ($data{$previous_altitude_zone}{$direction_zone}{'distance'} > $data{$altitude_zone}{$direction_zone}{'distance'}))) { 598 | foreach my $header ("hex_ident","altitude","latitude","longitude","date","time","angle","distance") { 599 | $data{$altitude_zone}{$direction_zone}{$header} = $data{$previous_altitude_zone}{$direction_zone}{$header}; 600 | } 601 | 602 | } 603 | } 604 | } 605 | # Save previous zones 606 | push(@zone,$altitude_zone); 607 | } 608 | #================================================================================ 609 | my @color = ("7f0000ff","7fffff00","7fff0033","7f00cc00","7fff00ff","7fff6600","7f660099","7f00ffff"); 610 | my $data_filehandle; 611 | my $kml_filehandle; 612 | my $trackpoint=0; 613 | my $track=0; 614 | my $newtrack; 615 | if ($outputdatafile =~ /csv$/i) { 616 | open($data_filehandle, '>',"$outputdatafile") or die "Unable to open '$outputdatafile'!\n"; 617 | print $data_filehandle "type,new_track,name,color,trackpoint,altitudezone,destination,hex_ident,Altitude($altitudeunit{'out'}),latitude,longitude,date,time,angle,distance($distanceunit{'out'})\n"; 618 | } else { 619 | open($kml_filehandle, '>',"$outputdatafile") or die "Unable to open '$outputdatafile'!\n"; 620 | print $kml_filehandle " 621 | 622 | 623 | Paths 624 | Example\n"; 625 | } 626 | foreach my $altitude_zone (sort {$a<=>$b} keys %data) { 627 | $track++; 628 | # convert altitude to feet: 629 | my $altitude_feet = $altitude_zone / $convertalt{'out'} * 3.2808399 / 1.8; 630 | my $s = 85; 631 | my $l = 50; 632 | my $h = 20; 633 | my @val = (20,140,300); 634 | my @alt = (2000,10000,40000); 635 | foreach my $index (0..$#alt) { 636 | if ($altitude_feet > $alt[$index]) { 637 | if ($index == 2) { 638 | $h = $val[$index]; 639 | } else { 640 | $h = ($val[$index] + ($val[$index+1] - $val[$index]) * ($altitude_feet - $alt[$index]) / ($alt[$index+1] - $alt[$index])); 641 | } 642 | last; 643 | } 644 | } 645 | if ($h < 0) {$h = ($h % 360) + 360;} elsif ($h >= 360) {$h = $h % 360;} 646 | if ($s < 5) {$s = 5;} elsif ($s > 95) {$s = 95;} 647 | if ($l < 5) {$l = 5;} elsif ($l > 95) {$l = 95;} 648 | my $kml_color = "ff".hsl_to_bgr($h/360,$s/100,$l/100); 649 | # Determine color 650 | my $colornumber = $track; 651 | while ($colornumber > 7) { 652 | $colornumber = $colornumber - 8; 653 | } 654 | my $alt_zone_name = sprintf("%05d-%5d",$altitude_zone,($altitude_zone + $zone_altitude)); 655 | my $positionperzonecounter = sprintf("% 9d",$positionperzonecounter{$altitude_zone}); 656 | my $tracknumber = sprintf("% 2d",$track); 657 | $newtrack = 1; 658 | my $min_positions_per_direction =0; 659 | my $max_positions_per_direction =0; 660 | print $kml_filehandle " 669 | 670 | $track 671 | $alt_zone_name 672 | #track-$track 673 | 674 | absolute 675 | \n" if ($outputdatafile =~ /kml$/i); 676 | foreach my $direction_zone (sort {$a<=>$b} keys %{$data{$altitude_zone}}) { 677 | my @row; 678 | my @kml; 679 | foreach my $header ("hex_ident","altitude","latitude","longitude","date","time","angle","distance") { 680 | push(@row,$data{$altitude_zone}{$direction_zone}{$header}); 681 | } 682 | $trackpoint++; 683 | if ($outputdatafile =~ /kml$/i) { 684 | print $kml_filehandle "$data{$altitude_zone}{$direction_zone}{'longitude'},$data{$altitude_zone}{$direction_zone}{'latitude'},$data{$altitude_zone}{$direction_zone}{'altitude'}\n"; 685 | } else { 686 | print $data_filehandle "T,$newtrack,Altitude zone $track: $alt_zone_name,$color[$colornumber],$trackpoint,$altitude_zone,$direction_zone,".join(",",@row)."\n"; 687 | } 688 | $newtrack = 0; 689 | $positionperdirectioncounter{$altitude_zone}{$direction_zone} = 0 if (! exists $positionperdirectioncounter{$altitude_zone}{$direction_zone}); 690 | $min_positions_per_direction = $positionperdirectioncounter{$altitude_zone}{$direction_zone} if ($positionperdirectioncounter{$altitude_zone}{$direction_zone} < $max_positions_per_direction); 691 | $max_positions_per_direction = $positionperdirectioncounter{$altitude_zone}{$direction_zone} if ($positionperdirectioncounter{$altitude_zone}{$direction_zone} > $max_positions_per_direction); 692 | } 693 | print $kml_filehandle " 694 | 695 | \n" if ($outputdatafile =~ /kml$/i); 696 | my $real_number_of_directions = scalar keys %{$positionperdirectioncounter{$altitude_zone}}; 697 | my $avarage_positions_per_direction = sprintf("% 6d",($positionperzonecounter{$altitude_zone} / $number_of_directions)); 698 | my $avarage_positions_per_real_direction =sprintf("% 6d",($positionperzonecounter{$altitude_zone} / $real_number_of_directions)); 699 | my $line = sprintf("% 3d,Altitude zone:% 6d-% 6d,Directions:% 5d/% 5d,Positions processed:% 10d,Positions processed per direction: min:% 6d,max:% 6d,avg:% 6d,real avg:% 6d",$tracknumber,$altitude_zone,($altitude_zone + $zone_altitude-1),($real_number_of_directions+1),$number_of_directions,$positionperzonecounter{$altitude_zone},$min_positions_per_direction,$max_positions_per_direction,$avarage_positions_per_direction,$avarage_positions_per_real_direction); 700 | LOG($line,"I"); 701 | } 702 | print $kml_filehandle " 703 | \n" if ($outputdatafile =~ /kml$/i); 704 | close $outputdatafile; 705 | 706 | -------------------------------------------------------------------------------- /socket30003.cfg: -------------------------------------------------------------------------------- 1 | # ted.sluis@gmail.com 2 | # Filename : socket30003.cfg 3 | #================================================================ 4 | # config file. 5 | #================================================================ 6 | [common] 7 | # The default settings for all scripts. 8 | # They can be override by settings in one of the other sections, but only when those settings have a value. 9 | # If you comment out these lines below or remove the values, the script will use the default values from one of the other sections or from the script: 10 | # 11 | filemask=dump*.txt # default file mask. 12 | datadirectory=/tmp # default data directory. 13 | logdirectory=/tmp # default log file directory. 14 | piddirectory=/tmp # default pid file directory. 15 | outputdirectory=/tmp # default output directory. 16 | distanceunit=kilometer # default distance unit: kilometer, nauticalmile, mile or meter. 17 | altitudeunit=meter # default altitude unit: meter or feet. 18 | speedunit=kilometerph # default speed unit: kilometerph, knotph or mileph (ph = per hour). 19 | latitude=52.085624 # antenna location, default (Utrecht, The Netherlands). 20 | longitude=5.0890591 21 | override=no # override output file if exists. 22 | timestamp=no # add timestamp to output file name. 23 | sequencenumber=no # add sequence number to output file name, when file allready exists. 24 | 25 | #================================================================ 26 | [socket30003] 27 | # The default settings for socket30003.pl script: 28 | # 29 | # If you comment out these lines below or remove the values, the script will use the default values from the script: 30 | # 31 | PEER_HOST=127.0.0.1 # default IP address or hostname of the DUMP1090 host. A Dump1090 on a local host can be addressed with 127.0.0.1 32 | TIME_MESSAGE_MARGIN=10 # default max acceptable margin between messages in milliseconds 33 | showpositions=no # Show positions processed: yes or no 34 | # 35 | # Override the settings in the [common] section: 36 | # If you comment out these lines below or remove the values, the script will use the default values from the [common] section. 37 | # Remove the '#' at the start of the line to override the setting used in the [common] section: 38 | # 39 | #datadirectory=/tmp # default data directory. 40 | #logdirectory=/tmp # default log file directory. 41 | #piddirectory=/tmp # default pid file directory. 42 | #distanceunit=kilometer # default distance unit: kilometer, nauticalmile, mile or meter. 43 | #altitudeunit=meter # default altitude unit: meter or feet. 44 | #speedunit=kilometerph # default speed unit: kilometerph, knotph or mileph (ph = per hour). 45 | #latitude=52.085624 # antenna location, default (Utrecht, The Netherlands). 46 | #longitude=5.0890591 47 | 48 | #================================================================ 49 | [heatmap] 50 | # Default settings for the heatmap.pl script: 51 | # 52 | # If you comment out these lines below or remove the values, the script will use the default values from the script: 53 | # 54 | degrees=5 # used to determine boundary of area around antenne. 55 | resolution=1000 # number of horizontal and vertical positions in output file. 56 | max_positions=100000 # maximum number of positions in the outputfile. 57 | max_weight=1000 # maximum position weight on the heatmap. 58 | outputdatafile=heatmapdata.csv # heatmap data file name. 59 | # 60 | # Override the settings in the [common] section: 61 | # If you comment out these lines below or remove the values, the script will use the default values from the [common] section. 62 | # Remove the '#' at the start of the line to override the setting used in the [common] section: 63 | # 64 | #filemask=dump*.txt # default file mask. 65 | #datadirectory=/tmp # default datadirectory, usually equel to the datadirectory in the [common] section. 66 | #logdirectory=/tmp # default log file directory. 67 | #outputdirectory=/tmp # default output directory. 68 | #latitude=52.085624 # center of the heatmap, usually equel to the antenna location in the [common] section. 69 | #longitude=5.0890591 # 70 | #override=no # override output file if exists. 71 | #timestamp=yes # add timestamp to output file name. 72 | #sequencenumber=yes # add sequence number to output file name, when file allready exists. 73 | 74 | #================================================================ 75 | [rangeview] 76 | # Default settings for the rangeview.pl script: 77 | # 78 | # If you comment out these lines below or remove the values, the script will use the default values from the script: 79 | # 80 | maxaltitudemeter=12000 # maximum altitude, specified in meter. 81 | maxaltitudefeet=36000 # maximum altitude, specified in feet. 82 | minaltitude=0 # minimum altitude, specified in the output unit. 83 | numberofdirections=1440 # number of directions. 84 | numberofaltitudezones=24 # number of altitude zones. 85 | outputdatafile=rangeview.kml # rangeview data file name: rangeview.kml (kml extention=xml structure) or rangeview.csv (csv extention=csv structure). 86 | # 87 | # Override the settings in the [common] section: 88 | # If you comment out these lines below or remove the values, the script will use the default values from the [common] section. 89 | # Remove the '#' at the start of the line to override the setting used in the [common] section: 90 | # 91 | #distanceunit= kilometer,kilometer # default distance unit: specify input & output unit! kilometer, nauticalmile, mile or meter 92 | #altitudeunit=meter,meter # default altitude unit: specify input & output unit! meter or feet 93 | #filemask=dump*.txt # default file mask. 94 | #datadirectory=/tmp # default datadirectory, usually equel to the datadirectory in the [common] section. 95 | #logdirectory=/tmp # default log file directory. 96 | #outputdirectory=/tmp # default output directory. 97 | #latitude=52.085624 # center of the heatmap, usually equel to the antenna location in the [common] section. 98 | #longitude=5.0890591 99 | #override=no # override output file if exists. 100 | #timestamp=yes # add timestamp to output file name. 101 | #sequencenumber=yes # add sequence number to output file name, when file allready exists. 102 | 103 | #================================================================ 104 | [install] 105 | # Default settings for the install.pl script: 106 | # 107 | # If you comment out these lines below or remove the values, the script will use the default values from the script: 108 | # 109 | installdirectory=/home/pi/socket30003 # default install directory. 110 | # 111 | # Override the settings in the [common] section: 112 | # If you comment out these lines below or remove the values, the script will use the default values from the [common] section. 113 | # Remove the '#' at the start of the line to override the setting used in the [common] section: 114 | # 115 | #datadirectory=/tmp # default datadirectory, usually equel to the datadirectory in the [common] section. 116 | #logdirectory=/tmp # default log file directory. 117 | #outputdirectory=/tmp # default output directory. 118 | #piddirectory=/tmp # default pid directory. 119 | 120 | 121 | -------------------------------------------------------------------------------- /socket30003.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # ted.sluis@gmail.com 3 | # Filename : socket30003.pl 4 | # 5 | #=============================================================================== 6 | # This script reads data from a dump1090 instance using TCP 30003 port stream and writes 7 | # longitude, latitude, altitude, hex_indent, flight number, ground speed, squawk, 8 | # direction, date and time to a text file (comma serperated). 9 | # The script also calculates the angle and distance relative to location of the antenna. 10 | #=============================================================================== 11 | # Down here are the fields that are served in the messages by dump1090 over port 30003: 12 | # Note: not all fields are filled with data. 13 | # 14 | # Field Description 15 | # message_type See MessageType. 16 | # transmission_type See TransmissionType. 17 | # session_id String. Database session record number. 18 | # aircraft_id String. Database aircraft record number. 19 | # hex_ident String. 24-bit ICACO ID, in hex. 20 | # flight_id String. Database flight record number. 21 | # generated_date String. Date the message was generated. 22 | # generated_time String. Time the message was generated. 23 | # logged_date String. Date the message was logged. 24 | # logged_time String. Time the message was logged. 25 | # callsign String. Eight character flight ID or callsign. 26 | # altitude Integer. Mode C Altitude relative to 1013 mb (29.92" Hg). 27 | # ground_speed Integer. Speed over ground. 28 | # track Integer. Ground track angle. 29 | # lat Float. Latitude. 30 | # lon Float. Longitude 31 | # vertical_rate Integer. Climb rate. 32 | # squawk String. Assigned Mode A squawk code. 33 | # alert Boolean. Flag to indicate that squawk has changed. 34 | # emergency Boolean. Flag to indicate emergency code has been set. 35 | # spi Boolean. Flag to indicate Special Position Indicator has been set. 36 | # is_on_ground Boolean. Flag to indicate ground squat switch is active. 37 | # 38 | # MessageType 39 | # There are 6 types of SBS-1 messages represented by the MessageType enum: 40 | # Enum Value 41 | # SELECTION_CHANGE "SEL" 42 | # NEW_ID "ID" 43 | # NEW_AIRCRAFT "AIR" 44 | # STATUS_AIRCRAFT "STA" 45 | # CLICK "CLK" 46 | # TRANSMISSION "MSG" 47 | # SELECTION_CHANGE, NEW_ID, NEW_AIRCRAFT, STATUS_CHANGE, and CLK indicate changes in the state of the SBS-1 software and aren't typically used by other systems. 48 | # 49 | # TRANSMISSION messages contain information sent by aircraft. 50 | # TransmissionType 51 | # There are 8 subtypes of transmission messages, specified by the TransmissionType enum: 52 | # Enum Value Description Spec 53 | # ES_IDENT_AND_CATEGORY 1 ES identification and category DF17 BDS 0,8 54 | # ES_SURFACE_POS 2 ES surface position message DF17 BDS 0,6 55 | # ES_AIRBORNE_POS 3 ES airborne position message DF17 BDS 0,5 56 | # ES_AIRBORNE_VEL 4 ES airborne velocity message DF17 BDS 0,9 57 | # SURVEILLANCE_ALT 5 Surveillance alt message DF4, DF20 58 | # SURVEILLANCE_ID 6 Surveillance ID message DF5, DF21 59 | # AIR_TO_AIR 7 Air-to-air message DF16 60 | # ALL_CALL_REPLY 8 All call reply DF11 61 | # Only ES_SURFACE_POS and ES_AIRBORNE_POS transmissions will have position (latitude and longitude) information. 62 | # 63 | #=============================================================================== 64 | BEGIN { 65 | use strict; 66 | use POSIX qw(strftime); 67 | use Time::Local; 68 | use Socket; # For constants like AF_INET and SOCK_STREAM 69 | use Getopt::Long; 70 | use File::Basename; 71 | use Cwd 'abs_path'; 72 | our $scriptname = basename($0); 73 | our $fullscriptname = abs_path($0); 74 | use lib dirname (__FILE__); 75 | use common; 76 | } 77 | #=============================================================================== 78 | my $logfile =""; 79 | my $message; 80 | my $epochtime = time(); 81 | my $defaultdistanceunit; 82 | my $defaultaltitudeunit; 83 | my $defaultspeedunit; 84 | #=============================================================================== 85 | # Get options 86 | my $restart; 87 | my $stop; 88 | my $status; 89 | my $help; 90 | my $datadirectory; 91 | my $logdirectory; 92 | my $piddirectory; 93 | my $peerhost; 94 | my $time_message_margin; 95 | my $lon; 96 | my $lat; 97 | my $nopositions; 98 | my $debug = 0; 99 | my $verbose = 0; 100 | GetOptions( 101 | "restart!"=>\$restart, 102 | "stop!"=>\$stop, 103 | "status!"=>\$status, 104 | "help!"=>\$help, 105 | "distanceunit=s"=>\$defaultdistanceunit, 106 | "altitudeunit=s"=>\$defaultaltitudeunit, 107 | "speedunit=s"=>\$defaultspeedunit, 108 | "nopositions!"=>\$nopositions, 109 | "data=s"=>\$datadirectory, 110 | "log=s"=>\$logdirectory, 111 | "pid=s"=>\$piddirectory, 112 | "peer=s"=>\$peerhost, 113 | "msgmargin=s"=>\$time_message_margin, 114 | "longitude=s"=>\$lon, 115 | "latitude=s"=>\$lat, 116 | "debug!"=>\$debug, 117 | "verbose!"=>\$verbose 118 | ) or exit(1); 119 | # 120 | #=============================================================================== 121 | # if '-debug' parameter is used, set debug mode: 122 | common->setdebug if ($debug); 123 | # 124 | #=============================================================================== 125 | # if '-verbose' parameter is used, set verbose mode: 126 | common->LOGverbose if ($verbose); 127 | # 128 | #=============================================================================== 129 | # Checks if script runs interactive. 130 | my $interactive = common->InteractiveShellCheck; 131 | # 132 | #=============================================================================== 133 | # Log routine 134 | sub LOG(@){ 135 | common->LOG($logfile,@_); 136 | } 137 | # 138 | #=============================================================================== 139 | # Ctrl-C interupt handler 140 | my $interrupted = 0; 141 | $SIG{'INT'} = \&intHandler; 142 | # 143 | sub intHandler { 144 | # Someone pressed Ctrl-C 145 | if (($message)||(($epochtime+2) >time)) { 146 | LOG("You pressed CTRL-C. Do you want to exit? (y/n)","W"); 147 | my $answer = ; 148 | if ($answer =~ /^y$/i) { 149 | $interrupted = "The script was interrupted by CTRL-C!"; 150 | } else { 151 | LOG("'$scriptname' continues...","I"); 152 | } 153 | } else { 154 | LOG("You pressed CTRL-C. $scriptname' is interrupted!","W"); 155 | exit 1; 156 | } 157 | } 158 | # 159 | #=============================================================================== 160 | # Read settings from config file 161 | my %setting = common->READCONFIG('socket30003.cfg',$fullscriptname); 162 | my $PEER_HOST = $setting{'socket30003'}{'PEER_HOST'} || '127.0.0.1'; # The IP address or hostname of the DUMP1090 host. A Dump1090 on a local host can be addressed with 127.0.0.1 163 | my $TIME_MESSAGE_MARGIN = $setting{'socket30003'}{'TIME_MESSAGE_MARGIN'} || 10; # max acceptable margin between messages in milliseconds. 164 | my $defaultdatadirectory = $setting{'socket30003'}{'datadirectory'} || $setting{'common'}{'datadirectory'} || "/tmp"; 165 | my $defaultlogdirectory = $setting{'socket30003'}{'logdirectory'} || $setting{'common'}{'logdirectory'} || "/tmp"; 166 | my $defaultpiddirectory = $setting{'socket30003'}{'piddirectory'} || $setting{'common'}{'piddirectory'} || "/tmp"; 167 | my $latitude = $setting{'socket30003'}{'latitude'} || $setting{'common'}{'latitude'} || 52.085624; # Home location, default (Utrecht, The Netherlands) 168 | my $longitude = $setting{'socket30003'}{'longitude'} || $setting{'common'}{'longitude'} || 5.0890591; 169 | $defaultdistanceunit = $defaultdistanceunit || $setting{'socket30003'}{'distanceunit'} || $setting{'common'}{'distanceunit'} || "kilometer"; # kilometer, nauticalmile, mile or meter. 170 | $defaultaltitudeunit = $defaultaltitudeunit || $setting{'socket30003'}{'altitudeunit'} || $setting{'common'}{'altitudeunit'} || "meter"; # meter or feet. 171 | $defaultspeedunit = $defaultspeedunit || $setting{'socket30003'}{'speedunit'} || $setting{'common'}{'speedunit'} || "kilometerph"; # kilometerph, knotph or mileph (ph = per hour). 172 | # 173 | #=============================================================================== 174 | # Check options: 175 | if ($help) { 176 | print " 177 | This $scriptname script can retrieve flight data (like lat, lon and alt) 178 | from a dump1090 host using port 30003 and calcutates the distance and 179 | angle between the antenna and the plane. It will store these values in an 180 | output file in csv format (seperated by commas). 181 | 182 | This script can run several times simultaneously on one host retrieving 183 | data from multiple dump1090 instances on different hosts. Each instance 184 | can use the same directories, but they all have their own data, log and 185 | pid files. And every day the script will create a new data and log file. 186 | 187 | A data files contain column headers (with the names of the columns). 188 | Columns headers like 'altitude', 'distance' and 'ground_speed' also contain 189 | their unit between parentheses, for example '3520(feet)' or '12,3(kilometer)'. 190 | This makes it more easy to parse the columns when using this data in other 191 | scripts. Every time the script is (re)started a header wiil be written 192 | in to the data file. This way it is possible to switch a unit, for 193 | example from 'meter' to 'kilometer', and other scripts will still be able 194 | to determine the correct unit type. 195 | 196 | By default the position data, log files and pid file(s) will be stored in this format: 197 | dump1090--.txt 198 | dump1090--.log 199 | dump1090-.pid 200 | 201 | CSV output format: 202 | hex_ident,altitude(meter),latitude,longitude,date,time,angle,distance(kilometer),squawk,ground_speed(kilometerph),track,callsign 203 | 484CB8,3906,52.24399,5.25500,2017/01/09,16:35:02.113,45.11,20.93,0141,659,93,KLM1833 204 | 406D77,11575,51.09984,7.73237,2017/01/09,16:35:02.129,111.12,212.94,,,,BAW256 205 | 4CA1D4,11270,53.11666,6.02148,2017/01/09,16:35:03.464,40.85,130.79,,842,81,RYR89VN 206 | 4B1A1B,3426,51.86971,4.14556,2017/01/09,16:35:03.489,-103.38,68.93,1000,548,352,EZS85TP 207 | 4CA79D,11575,51.95681,4.17119,2017/01/09,16:35:03.489,-98.28,64.41,1366,775,263,RYR43FH 208 | 209 | The script can be lauched as a background process. It can be stopped by 210 | using the -stop parameter or by removing the pid file. When it not 211 | running as a background process, it can also be stopped by pressing 212 | CTRL-C. The script will write the current data and log entries to the 213 | filesystem before exiting... 214 | 215 | More info at: 216 | http://discussions.flightaware.com/post180185.html#p180185 217 | 218 | Syntax: $scriptname 219 | 220 | Optional parameters: 221 | -peer A dump1090 hostname or IP address. 222 | De default is the localhost, $PEER_HOST. 223 | -restart Restart the script. 224 | -stop Stop a running script. 225 | -status Display status. 226 | -data The data files are stored in $defaultdatadirectory by default. 227 | -log The log file is stored in $defaultlogdirectory by default. 228 | -pid The pid file is stored in $defaultpiddirectory by default. 229 | -msgmargin The max message margin. The default is $TIME_MESSAGE_MARGIN ms. 230 | -lon Location of your antenna. 231 | -lat 232 | -distanceunit Type of unit for distance: kilometer, 233 | nauticalmile, mile or meter 234 | Default distance unit is $defaultdistanceunit. 235 | -altitudeunit Type of unit for altitude: meter or feet. 236 | Default altitude unit is $defaultaltitudeunit. 237 | -speedunit Type of unit for ground speed. 238 | Default speed unit is $defaultspeedunit. 239 | -nopositions Does not display the number of position while 240 | running interactive (launched from commandline). 241 | -debug Displays raw socket messages. 242 | -verbose Displays verbose log messages. 243 | -help This help page. 244 | 245 | Notes: 246 | - To launch it as a background process, add '&' or run it from crontab: 247 | 0 * * * * $fullscriptname 248 | (This command checks if it ran every hour and relauch it if nessesary.) 249 | - The default values can be changed within the config file 'socket30003.cfg', 250 | section [common] and/or [socker30003]. 251 | 252 | Examples: 253 | $scriptname 254 | $scriptname -log /var/log -data /home/pi -pid /var/run -restart & 255 | $scriptname -peer 192.168.1.10 -nopositions -distanceunit nauticalmile -altitudeunit feet & 256 | $scriptname -peer 192.168.1.10 -stop 257 | 258 | Pay attention: to stop an instance: Don't forget to specify the same peer host.\n\n"; 259 | exit; 260 | } 261 | # defaultdestinationunit 262 | if ($defaultdistanceunit) { 263 | if ($defaultdistanceunit =~ /^kilometer$|^nauticalmile$|^mile$|^meter$/i) { 264 | $defaultdistanceunit = lc($defaultdistanceunit); 265 | } else { 266 | LOG("The default distance unit '$defaultdistanceunit' is invalid! It should be one of these: kilometer, nauticalmile, mile or meter.","E"); 267 | exit 1; 268 | } 269 | } else { 270 | $defaultdistanceunit = "kilometer"; 271 | } 272 | # defaultaltitudeunit 273 | if ($defaultaltitudeunit) { 274 | if ($defaultaltitudeunit =~ /^meter$|^feet$/i) { 275 | $defaultaltitudeunit = lc($defaultaltitudeunit); 276 | } else { 277 | LOG("The default altitude unit '$defaultaltitudeunit' is invalid! It should be one of these: meter or feet.","E"); 278 | exit 1; 279 | } 280 | } else { 281 | $defaultaltitudeunit = "meter"; 282 | } 283 | # defaultspeedunit 284 | if ($defaultspeedunit) { 285 | if ($defaultspeedunit =~ /^kilometerph$|^mileph$|^knotph$/i) { 286 | $defaultspeedunit = lc($defaultspeedunit); 287 | } else { 288 | LOG("The default speed unit '$defaultspeedunit' is invalid! It should be one of these: kilometerph, mileph or knotph (ph = per hour).","E"); 289 | exit 1; 290 | } 291 | } else { 292 | $defaultspeedunit = "kilometer"; 293 | } 294 | LOG("Using the unit '$defaultdistanceunit' for the distance, '$defaultaltitudeunit' for the altitude and '$defaultspeedunit' for the speed.","I"); 295 | # 296 | # Compose filedate 297 | sub filedate(@) { 298 | my $hostalias = shift; 299 | my ($second,$day,$month,$year,$minute) = (localtime)[0,3,4,5,1]; 300 | my $filedate = 'dump1090-'.$hostalias.'-'.sprintf '%02d%02d%02d', $year-100,($month+1),$day; 301 | } 302 | # 303 | # Are the specified directories for data, log and pid file writeable? 304 | $datadirectory = $defaultdatadirectory if (!$datadirectory); 305 | if (!-w $datadirectory) { 306 | LOG("You have no write permissions in data directory '$datadirectory'!","E"); 307 | exit 1; 308 | } 309 | $logdirectory = $defaultlogdirectory if (!$logdirectory); 310 | if (!-w $logdirectory) { 311 | LOG("You have no write permissions in log directory '$logdirectory'!","E"); 312 | exit 1; 313 | } 314 | $piddirectory = $defaultpiddirectory if (!$piddirectory); 315 | if (!-w $logdirectory) { 316 | LOG("You have no write permissions in pid directory '$piddirectory'!","E"); 317 | exit 1; 318 | } 319 | # Was a hostname specified? 320 | $PEER_HOST = $peerhost if ($peerhost); 321 | # Test peer host: 322 | my @ping =`ping -w 4 -c 1 $PEER_HOST`; 323 | my $result; 324 | foreach my $output (@ping) { 325 | # rtt min/avg/max/mdev = 162.207/162.207/162.207/0.000 ms 326 | if ($output =~ /=\s*\d{1,4}\.\d{1,4}\/\d{1,4}\.\d{1,4}\/\d{1,4}\.\d{1,4}\/\d{1,4}\.\d{1,4}\s*ms/) { 327 | $result = "ok"; 328 | } 329 | } 330 | if (!$result) { 331 | LOG("Unable to connect to peer host '$PEER_HOST'!","E"); 332 | exit 1; 333 | } else { 334 | LOG("Trying to connect to peer host '$PEER_HOST'...","I"); 335 | } 336 | # Was a time message margin specified? 337 | $TIME_MESSAGE_MARGIN = $time_message_margin if ($time_message_margin); 338 | if (($TIME_MESSAGE_MARGIN < 1) || ($TIME_MESSAGE_MARGIN > 2000)) { 339 | LOG("The specified 'message margin' ($TIME_MESSAGE_MARGIN) is out of range!","E"); 340 | LOG("Try something between '1' and '2000' milliseconds! The default is 10ms","E"); 341 | exit 1; 342 | } 343 | # longitude & latitude 344 | $longitude = $lon if ($lon); 345 | $longitude =~ s/,/\./ if ($longitude); 346 | if ($longitude !~ /^[-+]?\d+(\.\d+)?$/) { 347 | LOG("The specified longitude '$longitude' is invalid!","E"); 348 | exit 1; 349 | } 350 | $latitude = $lat if ($lat); 351 | $latitude =~ s/,/\./ if ($latitude); 352 | if ($latitude !~ /^[-+]?\d+(\.\d+)?$/) { 353 | LOG("The specified latitude '$latitude' is invalid!","E"); 354 | exit 1; 355 | } 356 | LOG("The antenna latitude & longitude are: '$latitude','$longitude'","I"); 357 | # 358 | #=============================================================================== 359 | # Convert epoch time to YYYY-MM-DD/HH:MM:SS format 360 | sub epoch2date (@) { 361 | my $epoch = shift; 362 | my $datestring = strftime "%Y-%m-%d/%H:%M:%S", localtime($epoch); 363 | return $datestring; 364 | } 365 | # 366 | #=============================================================================== 367 | # Calculate angle and distance between two coordinates 368 | # 369 | # Calculate pi 370 | my $pi = atan2(1,1) * 4; 371 | # 372 | # Calculate angle between 2 coordinates relative to the north pole. 373 | # 0 = north, -90 = west, 90 = east, (-/+)180 = south 374 | sub angle(@) { 375 | my ($lat1,$lon1,$lat2,$lon2) = @_; 376 | my $dlat = $lat2 - $lat1; 377 | my $dlon = $lon2 - $lon1; 378 | my $y = sin($dlon / 180) * cos($lat2 / 180); 379 | my $x = cos($lat1 / 180) * sin($lat2 / 180) - sin($lat1 / 180) * cos($lat2 / 180) * cos($dlon / 180); 380 | my $angle = atan2( $y, $x ) * 57.2957795; 381 | return $angle; 382 | } 383 | # 384 | # arccos(rad) 385 | sub acos(@) { 386 | my $rad = shift; 387 | my $arccos = atan2(sqrt(1 - $rad**2),$rad); 388 | return $arccos; 389 | } 390 | # 391 | # decimal degrees to radians 392 | sub deg2rad(@){ 393 | my $deg = shift; 394 | my $rad = $deg * $pi / 180; 395 | return $rad; 396 | } 397 | # 398 | # Radians to decimal degrees 399 | sub rad2deg(@){ 400 | my $rad = shift; 401 | my $deg = $rad * 180 / $pi; 402 | return $deg; 403 | } 404 | # 405 | # Calculate distance between two coordinates. 406 | sub distance(@) { 407 | my ($lat1,$lon1,$lat2,$lon2) = @_; 408 | my $theta = $lon1 - $lon2; 409 | my $dist = rad2deg(acos(sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta)))); 410 | my $distance; 411 | # calculate the distance using the required unit: 412 | if ($defaultdistanceunit =~ /^mile$/) { 413 | $distance = int($dist * 69.09 * 100) / 100; # mile 414 | } elsif ($defaultdistanceunit =~ /^meter$/) { 415 | $distance = int($dist * 111189.57696); # meter 416 | } elsif ($defaultdistanceunit =~ /^nauticalmile$/) { 417 | $distance = int($dist * 59.997756 * 100) / 100; # nautical mile 418 | } else { 419 | $distance = int($dist * 111.18957696 * 100) / 100; # kilometer 420 | } 421 | return $distance; 422 | } 423 | # 424 | #=============================================================================== 425 | # Handle the process using a pid (process id) file. 426 | sub Check_pid(@){ 427 | my $pidfile = shift; 428 | my $pid; 429 | # return if pidfile does not exists.. 430 | return 0 if (! -e $pidfile); 431 | my @cat =`cat $pidfile`; 432 | foreach my $line (@cat) { 433 | chomp($line); 434 | # get pid from pidfile... 435 | $pid =$1 if ($line =~ /^(\d+)$/); 436 | } 437 | if (!$pid) { 438 | # pidfile without pid 439 | unlink($pidfile); 440 | return 0; 441 | } 442 | # check if process still exists. 443 | my @process =`ps -ef | grep $pid | grep -v grep`; 444 | my $result; 445 | foreach my $line (@process) { 446 | chomp($line); 447 | # pi 2773 2160 29 22:04 pts/0 00:00:09 /usr/bin/perl -w ./client-0.2.pl 448 | $result =$1 if ($result = $line =~ /^\w+\s+($pid)\s+\d+/); 449 | } 450 | # return if pid pidfile is a running process 451 | return $pid if ($result); 452 | # remove pidfile if process is no longer running... 453 | unlink($pidfile); 454 | return 0; 455 | } 456 | # Compose pid file 457 | my $hostalias = $PEER_HOST; 458 | $hostalias =~ s/\./_/g if ($hostalias =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/); 459 | LOG("The data directory/file is: $datadirectory/".filedate($hostalias).".txt","I"); 460 | LOG("The log directory/file is: $logdirectory/".filedate($hostalias).".log","I"); 461 | my $pidfile = "$piddirectory/dump1090-$hostalias.pid"; 462 | # 463 | if (-e $pidfile) { 464 | # pid file already exists 465 | my $pid = Check_pid($pidfile); 466 | if ($status) { 467 | if ($pid) { 468 | LOG("'$scriptname' ($pid) is running!","I"); 469 | } else { 470 | LOG("'$scriptname' is not running!","I"); 471 | } 472 | exit; 473 | } elsif ($restart) { 474 | if ($pid) { 475 | LOG("'$scriptname' ($pid) is running!","I"); 476 | unlink($pidfile); 477 | if (-e $pidfile) { 478 | LOG("Unable to remove '$pidfile' ($pid)! '$scriptname' is not restarting....","E"); 479 | exit 1; 480 | } 481 | LOG("Stopping '$scriptname' ($pid)....","I"); 482 | sleep 2; 483 | } else { 484 | LOG("'$scriptname' is not running!","W"); 485 | } 486 | LOG("Starting '$scriptname'....","I"); 487 | } elsif ($stop) { 488 | if ($pid) { 489 | LOG("'$scriptname' ($pid) is running!"."I"); 490 | } else { 491 | LOG("'$scriptname' is not running!","W"); 492 | exit 1; 493 | } 494 | unlink($pidfile); 495 | if (-e $pidfile) { 496 | LOG("Unable to remove '$pidfile' ($pid)! '$scriptname' is not stopping.....","E"); 497 | exit 1; 498 | } 499 | LOG("Stopping '$scriptname' ($pid)....","I"); 500 | sleep 2; 501 | exit 0; 502 | } else { 503 | if ($pid) { 504 | LOG("Unable to start '$scriptname'. '$scriptname' ($pid) is already running!","W"); 505 | exit 1; 506 | } else { 507 | LOG("'$scriptname' will be started!","I"); 508 | } 509 | } 510 | } else { 511 | # There is no pid file 512 | if ($status) { 513 | LOG("'$scriptname' is not running!","I"); 514 | exit 0; 515 | } elsif ($restart) { 516 | LOG("'$scriptname' was not running, but it is starting now!","W"); 517 | } elsif ($stop) { 518 | LOG("'$scriptname' was not running....","W"); 519 | exit 1; 520 | } else { 521 | LOG("Starting '$scriptname'....","I"); 522 | } 523 | } 524 | # Create pid file with pid number inside. 525 | my $pid =$$; 526 | my @cmd =`echo $pid > $pidfile`; 527 | if (! -e $pidfile) { 528 | LOG("Unable to create '$pidfile'! '$scriptname' ($pid) is not starting....","W"); 529 | exit 1; 530 | } else { 531 | LOG("'$scriptname' ($pid) is started! Using pidfile $pidfile.","I"); 532 | } 533 | # 534 | #=============================================================================== 535 | # Main program 536 | my $previous_date =""; 537 | my $previous_minute = 0; 538 | my $previous_second = 0; 539 | my $data_filehandle; 540 | my $message_count = 0; 541 | my $position_count = 0; 542 | my $flight_count = 0; 543 | my %flight; 544 | # Data Header 545 | my @header = ("message_type","transmission_type","session_id","aircraft_id","hex_ident","flight_id","generated_date","generated_time","logged_date","logged_time","callsign","altitude","ground_speed","track","lat","lon","vertical_rate","squawk","alert","emergency","spi","is_on_ground"); 546 | my %hdr; 547 | my $columnnumber = 0; 548 | # Save colum name with colomn number in hash. 549 | foreach my $header (@header) { 550 | $hdr{$header} = $columnnumber; 551 | $columnnumber++; 552 | } 553 | # 554 | #=============================================================================== 555 | # Socket that reads data from the PEER_HOST over port 30003. 556 | $proto = getprotobyname('tcp'); #get the tcp protocol 557 | $connectioncount=0; 558 | my ($SOCKET); 559 | while (1) { 560 | # create a socket handle (descriptor) 561 | socket($SOCKET, AF_INET, SOCK_STREAM, $proto) or die "could not create socket : $!"; 562 | # connect to remote server 563 | $iaddr = inet_aton($PEER_HOST) or die "Unable to resolve hostname : $PEER_HOST"; 564 | $paddr = sockaddr_in("30003", $iaddr); #socket address structure 565 | connect($SOCKET , $paddr) or die "connect failed : $!"; 566 | LOG("Connected to $PEER_HOST on port 30003","D"); 567 | $connectioncount++; 568 | # 569 | # Read messages from the 30003 socket in a continuous loop: 570 | my $errorcount = 0; 571 | while ($message = <$SOCKET>){ 572 | $message_count++; 573 | chomp($message); 574 | if ($debug) { 575 | if ($message) { 576 | LOG("messagecount=$message_count,message='$message'","D"); 577 | } else { 578 | LOG("messagecount=$message_count,message=''","W"); 579 | } 580 | } 581 | # Split line into colomns: 582 | my @col = split /,/,$message; 583 | my $hex_ident = $col[$hdr{'hex_ident'}]; 584 | # Check whether if has enough columns and a hex_ident: 585 | if ((@col > 20) && ($hex_ident) && ($hex_ident =~ /^[0-9A-F]+$/i)){ 586 | $errorcount = 0; 587 | } else { 588 | $errorcount++; 589 | if (($errorcount == 100) || ($errorcount == 1000)) { 590 | # write an error message to the log file after 100 or 1000 incomplete messages in a row: 591 | LOG("messagecount=$message_count, incomplete messages in a row: $errorcount, last message='$message'","L"); 592 | } elsif ($errorcount > 10000) { 593 | # Exits the script after 10000 incomplete messages in a row: 594 | LOG("messagecount=$message_count, incomplete messages in a row: $errorcount, last message='$message'. Exit script......","E"); 595 | LOG("Not able to read proper data from the socket! Check whether your dump1090 is running on '$PEER_HOST' port 30003 (tcp).","E"); 596 | exit; 597 | } 598 | next; 599 | } 600 | $epochtime = time; 601 | # Flight first time seen: 602 | if (! exists $flight{$hex_ident}{'lastseen'}) { 603 | # Save time in epoch when the flight was first seen. 604 | $flight{$hex_ident}{'firstseen'} = $epochtime; 605 | # Overall flight count (per day): 606 | $flight_count++; 607 | # Position count per flight: 608 | $flight{$hex_ident}{'position_count'} = 0; 609 | $flight{$hex_ident}{'message_count'} = 0; 610 | } 611 | # Save time when flight was last seen: 612 | $flight{$hex_ident}{'lastseen'} = $epochtime; 613 | # Count messages per flight: 614 | $flight{$hex_ident}{'message_count'}++; 615 | # Compose filedate 616 | my ($second,$day,$month,$year,$minute) = (localtime)[0,3,4,5,1]; 617 | my $filedate = 'dump1090-'.$hostalias.'-'.sprintf '%02d%02d%02d', $year-100,($month+1),$day; 618 | # Every second we want to check whether the pid file is still there. 619 | if($previous_second ne $second) { 620 | $previous_second = $second; 621 | if (!-e $pidfile) { 622 | # The PID file was removed (by an outside process). 623 | # This means it is time to exit..... 624 | $interrupted = "The '$scriptname' ($pid) was interrupted. The pidfile $pidfile was removed by an outside process...!"; 625 | } 626 | } 627 | # Handle data and log file: 628 | if($filedate ne $previous_date){ 629 | # Close files if they were open: 630 | if ($previous_date ne "") { 631 | close $data_filehandle; 632 | } 633 | # Set newfile date: 634 | $previous_date=$filedate; 635 | # Open files 636 | open($data_filehandle, '>>',"$datadirectory/$filedate.txt") or die "Unable to open '$datadirectory/$filedate.txt'!\n"; 637 | $logfile = common->LOGset($logdirectory,"$filedate.log",$verbose); 638 | $data_filehandle->autoflush; 639 | # write header: 640 | print $data_filehandle "hex_ident,altitude($defaultaltitudeunit),latitude,longitude,date,time,angle,distance($defaultdistanceunit),squawk,ground_speed($defaultspeedunit),track,callsign\n"; 641 | # reset counters for a new day: 642 | $message_count = 1; 643 | $position_count = keys %{$flight{$hex_ident}}; 644 | $flight_count = keys %flight; 645 | } 646 | # Check every minute for hex_ident's that can be retiered: 647 | if (($minute ne $previous_minute) || ($interrupted)) { 648 | if (-e $pidfile) { 649 | my @cmd=`cat $pidfile`; 650 | $pidfilestatus=""; 651 | for $line (@cmd) { 652 | chomp($line); 653 | if ($line =~ /^$pid$/){ 654 | $pidfilestatus="ok"; 655 | } 656 | } 657 | if ($pidfilestatus !~ /ok/) { 658 | # The PID file was changed (by an outside process). 659 | # This means it is time to exit..... 660 | $interrupted = "The '$scriptname' ($pid) was interrupted. The pid with the pidfile $pidfile was changed by an outside process...!"; 661 | } 662 | } 663 | $previous_minute = $minute; 664 | # Log overall statistics: 665 | LOG("current number of flights=".scalar(keys %flight).",epoch=".epoch2date($epochtime).",msg_count=$message_count,pos_count=$position_count,flight_count=$flight_count,con_lost=$connectioncount.","L"); 666 | foreach my $hex_ident (keys %flight) { 667 | # check if flight was not seen for longer than 120 secondes: 668 | next unless ((($flight{$hex_ident}{'lastseen'} + 120) < $epochtime) || ($interrupted)); 669 | # Set position_count zero if there are no positions for this flight. 670 | $flight{$hex_ident}{'position_count'} = 0 if (! exists $flight{$hex_ident}{'position_count'}); 671 | # Log flight statistics: 672 | LOG("removed:$hex_ident,first seen=".epoch2date($flight{$hex_ident}{'firstseen'}).",last seen=".epoch2date($flight{$hex_ident}{'lastseen'}).",message_count=$flight{$hex_ident}{'message_count'},position_count=$flight{$hex_ident}{'position_count'}.","L"); 673 | # remove flight information (and prevent unnessesary memory usage). 674 | delete $flight{$hex_ident}; 675 | } 676 | if ($interrupted) { 677 | LOG("Exit: $interrupted","I"); 678 | exit; 679 | } 680 | } 681 | # Get logged date & time: 682 | # 2015/04/06,19:14:29.596 683 | my $loggeddatetime = $col[$hdr{'logged_date'}].",".$col[$hdr{'logged_time'}]; 684 | my ($hour,$millisecond); 685 | if (($year,$month,$day,$hour,$minute,$second,$millisecond) = $loggeddatetime =~ /^(\d{4})\/(\d{2})\/(\d{2}),(\d{2}):(\d{2}):(\d{2})\.(\d{1,3})$/){ 686 | # change date & time into epoch time in milliseconds: 687 | $loggeddatetime = (timelocal($second,$minute,$hour,$day,($month-1),$year).$millisecond) * 1000; 688 | } else { 689 | # No valid date & time format 690 | next; 691 | } 692 | # Save callsign 693 | if ($col[$hdr{'callsign'}] =~ /[a-z0-9]+/i) { 694 | $flight{$hex_ident}{'callsign'} = $col[$hdr{'callsign'}]; 695 | } 696 | # Save longitude and datetime 697 | if ($col[$hdr{'lon'}] =~ /\./) { 698 | $flight{$hex_ident}{'lon'} = $col[$hdr{'lon'}]; 699 | $flight{$hex_ident}{'lon_loggedtime'} = $loggeddatetime; 700 | } 701 | # Save latitude and datetime 702 | if ($col[$hdr{'lat'}] =~ /\./) { 703 | $flight{$hex_ident}{'lat'} = $col[$hdr{'lat'}]; 704 | $flight{$hex_ident}{'lat_loggedtime'} = $loggeddatetime; 705 | } 706 | # Save Altitude and datetime 707 | if ($col[$hdr{'altitude'}] =~ /^\d*[123456789]\d*\.?\d*$/) { 708 | my $altitude = $col[$hdr{'altitude'}]; 709 | if ($defaultaltitudeunit =~ /^meter$/) { 710 | # save feet as meters: 711 | $flight{$hex_ident}{'altitude'} = int($altitude / 3.2828); 712 | } else { 713 | # save as feet: 714 | $flight{$hex_ident}{'altitude'} = int($altitude); 715 | } 716 | $flight{$hex_ident}{'altitude_loggedtime'} = $loggeddatetime; 717 | } 718 | # Save squawk 719 | if ($col[$hdr{'squawk'}] =~ /^\d+$/) { 720 | $flight{$hex_ident}{'squawk'} = $col[$hdr{'squawk'}]; 721 | } 722 | # Save track 723 | if ($col[$hdr{'track'}] =~ /^\d+\.?\d*$/) { 724 | $flight{$hex_ident}{'track'} = $col[$hdr{'track'}]; 725 | } 726 | # Save speed 727 | if ($col[$hdr{'ground_speed'}] =~ /^\d*[123456789]\d*\.?\d*$/) { 728 | my $speed = $col[$hdr{'ground_speed'}]; 729 | if ($defaultspeedunit =~ /^mileph$/) { 730 | # save knots as miles: 731 | $flight{$hex_ident}{'ground_speed'} = int($speed * 1.150779); 732 | } elsif ($defaultspeedunit =~ /^kilometerph$/) { 733 | # save knots as kilometers 734 | $flight{$hex_ident}{'ground_speed'} = int($speed * 1.852); 735 | } else { 736 | # save as knots as knots 737 | $flight{$hex_ident}{'ground_speed'} = int($speed); 738 | } 739 | } 740 | # Be sure that the requiered fields (longitude, latitude and altitude) for this flight are captured: 741 | next unless ((exists $flight{$hex_ident}{'lon'}) && (exists $flight{$hex_ident}{'lat'}) && (exists $flight{$hex_ident}{'altitude'})); 742 | # If there is a time difference, calculate the time differences between messages: 743 | my $diff1 = abs($flight{$hex_ident}{'lon_loggedtime'} - $flight{$hex_ident}{'lat_loggedtime'}); 744 | my $diff2 = abs($flight{$hex_ident}{'lon_loggedtime'} - $flight{$hex_ident}{'altitude_loggedtime'}); 745 | my $diff3 = abs($flight{$hex_ident}{'lat_loggedtime'} - $flight{$hex_ident}{'altitude_loggedtime'}); 746 | # Be sure that the time differance between the messages is less than $TIME_MESSAGE_MARGIN. 747 | next unless (($diff1 < $TIME_MESSAGE_MARGIN) && ($diff2 < $TIME_MESSAGE_MARGIN) && ($diff3 < $TIME_MESSAGE_MARGIN)); 748 | # Skip this one. All the values need to be different... 749 | next if ((exists $flight{$hex_ident}{'prev_lon'}) && ($flight{$hex_ident}{'lon'} eq $flight{$hex_ident}{'prev_lon'}) && 750 | (exists $flight{$hex_ident}{'Prev_lat'}) && ($flight{$hex_ident}{'lat'} eq $flight{$hex_ident}{'Prev_lat'}) && 751 | (exists $flight{$hex_ident}{'prev_altitude'}) && ($flight{$hex_ident}{'altitude'} eq $flight{$hex_ident}{'prev_altitude'})); 752 | # Skip this one. All the values need to be from a new moment... 753 | next if ((exists $flight{$hex_ident}{'prev_lon_loggedtime'}) && ($flight{$hex_ident}{'lon_loggedtime'} eq $flight{$hex_ident}{'prev_lon_loggedtime'}) && 754 | (exists $flight{$hex_ident}{'Prev_lat_loggedtime'}) && ($flight{$hex_ident}{'lat_loggedtime'} eq $flight{$hex_ident}{'Prev_lat_loggedtime'}) && 755 | (exists $flight{$hex_ident}{'prev_altitude_loggedtime'}) && ($flight{$hex_ident}{'altitude_loggedtime'} eq $flight{$hex_ident}{'prev_altitude_loggedtime'})); 756 | 757 | # Count the positions per flight and overall: 758 | $flight{$hex_ident}{'position_count'}++; 759 | $position_count++; 760 | # Get angle and distance 761 | my $angle = int(angle($latitude,$longitude,$flight{$hex_ident}{'lat'},$flight{$hex_ident}{'lon'}) * 100) / 100; 762 | my $distance = distance($latitude,$longitude,$flight{$hex_ident}{'lat'},$flight{$hex_ident}{'lon'}); 763 | # Write the data to the data file: 764 | print $data_filehandle "$hex_ident,$flight{$hex_ident}{'altitude'},$flight{$hex_ident}{'lat'},$flight{$hex_ident}{'lon'},$col[$hdr{'logged_date'}],$col[$hdr{'logged_time'}],$angle,$distance,".($flight{$hex_ident}{'squawk'}||"").",".($flight{$hex_ident}{'ground_speed'}||"").",".($flight{$hex_ident}{'track'}||"").",".($flight{$hex_ident}{'callsign'}||"")."\n"; 765 | # Save the values per flight to examine the next position. 766 | $flight{$hex_ident}{'prev_lon'} = $flight{$hex_ident}{'lon'}; 767 | $flight{$hex_ident}{'Prev_lat'} = $flight{$hex_ident}{'lat'}; 768 | $flight{$hex_ident}{'prev_altitude'} = $flight{$hex_ident}{'altitude'}; 769 | $flight{$hex_ident}{'prev_lon_loggedtime'} = $flight{$hex_ident}{'lon_loggedtime'}; 770 | $flight{$hex_ident}{'Prev_lat_loggedtime'} = $flight{$hex_ident}{'lat_loggedtime'}; 771 | $flight{$hex_ident}{'prev_altitude_loggedtime'} = $flight{$hex_ident}{'altitude_loggedtime'}; 772 | # Display statistics when running interactive: 773 | if (($interactive) && (!$nopositions)) { 774 | my $back = length "positions:".$position_count; 775 | print "positions:".$position_count, substr "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b", 0, $back; 776 | } 777 | } 778 | LOG("lost TCP connection","D"); 779 | } 780 | 781 | # This cleanup routine will be executed even when the script is stopped by an 'exit' of CTRL-C. 782 | END { 783 | # Clean up pidfile (if exists) 784 | if (($pidfile) && (-e $pidfile)) { 785 | my @cat =`cat $pidfile`; 786 | foreach my $line (@cat) { 787 | chomp($line); 788 | # clean only when it matches the PID 789 | my $pid =$$; 790 | unlink($pidfile) if ($line =~ /^$pid$/); 791 | } 792 | } 793 | } 794 | 1; 795 | --------------------------------------------------------------------------------