From 36294b9ecd2d2554506030b73cb35df0787dfe7a Mon Sep 17 00:00:00 2001 From: Walter Francis Date: Thu, 14 Jul 2016 11:05:43 -0400 Subject: [PATCH 1/4] Add support for using iCal as a data source --- README.md | 63 ++++++++++++++- aprs.pl | 216 +++++++++++++++++++++++++++++++++++++++++++------ aprsobjects.pm | 27 +++---- 3 files changed, 260 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 209da58..48a81be 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,29 @@ APRS Objects Generator for Direwolf This script makes it possible to schedule objects to be advertised with Direwolf. The output of this script is the currently active objects for use in a Direwolf configuration file. Suggested setup is to create a Direwolf config file with no emphermeral objects in it. Then have a cron job which runs every N minutes (5 is a good starting point) which copies the current Direwolf configuration file to another filename, then concatenate the output of your template Direwolf config and the output of this script to a new Direwolf config. Compare the two files; if they are different restart Direwolf so it picks up the new settings. -A very simple example script to do this is included which you can update and include in your setup. +A very simple example script to execute Direwolf on a cron job is included which you can update and include in your setup. + +Objects can be defined using the @objects array in the aprsobjects.pm module or they can be pulled from an iCal source. These two data sources are exclusve from one another, so you may only use one or the other. This script is based on the ideas from Bob Bruninga (WB4APR) in http://www.aprs.org/info/netsked.txt ### aprsobjects.pm -You will need to update the objects in aprsobjects.pm with the objects in your area which you wish to advertise. +You will need to update aprsobjects.pm with the settings and method you wish to use to define objects. If you want to use iCal you must set up an iCal calendar and set the public URL for it, or you can define the objects using the @objects array. To set up iCal see the second below specific to that topic. #### Default options There are several defaults to set in the get_defaults function +* $debug = 0; + * If set to 1, enable extra debugging which will output the steps being executed and objects evaluated. All debug lines are commented out so they should not affect Direwolf processing + +* $moduleversion = 'x.x'; + * To ensure our module fields matches our program fields, this must match the version in the main program. This just indicates the need to ensure any fields you have defined still meet the fields the program needs. + +* $ical = "https://calendar.google.com/calendar/ical/....../basic.ics"; + * iCal url when using iCal as a data source for objects. Disables the use of using the @objects array in aprsobjects.pm + * $startinterval = '0:30'; * How long to delay our initial beacon advertisement, in direwolf format (min:sec). In this example, it means Direwolf will wait 30 seconds before it advertises the first object. @@ -32,8 +43,11 @@ There are several defaults to set in the get_defaults function * Suggestion: All day or every day events at the end so emphemeral events are advertised sooner * Object data description and order - * DAY - Sun = 0, Mon = 1, Tue = 2, Wed = 3, Thur = 4, Fri = 5, Sat = 6, Every Day = -1 + * DOW - Sun = 0, Mon = 1, Tue = 2, Wed = 3, Thur = 4, Fri = 5, Sat = 6, Every Day = -1 * ENABLED - 0 entry is disabled, 1 enabled + * MONTH - Month for a dated event (1-12) + * DAY - Day of the month for a dated event (1-31) + * YEAR - Year for a dated event (2016) * STARTTIME/ENDTIME - Start/End = 0 implies broadcast all day, HH:MM format * TIMEBEFORE - Number of minutes to start advertising object before STARTTIME * OBJNAME - Name of APRS Object, max 9 characters @@ -47,6 +61,47 @@ There are several defaults to set in the get_defaults function * SYMBOL - APRS Symbol to use for object * COMMENT (OPTIONAL) - Description of object +### iCal data + +Setting up APRS objects in the @object array can be a bit tedious and requires someone editing the file. Using an iCal URL as a data source is potentially an easier way to define objects, and this also allows you to delegate the adding and maintaining of objects to other people as you can share your calendar with other users. I have tested this with Google Calendar but it should work with other calendars as well. Be aware that this data is pulled down every run, and can take several seconds to download and parse. If you need your data to not require an internet connection it is better to use the @objects array. + +I have found that a dozen APRS object events with recurring settings can take a long time to parse, so the script limits the days evaluated to today and tomorrow (as events are dated as UTC, events in the evening may be dated tomorrow in the raw iCal data that is parsed). This speeds up the parsing considerably and since we're only generally advertising for events in immediate future this is fine. When limited to this timeframe I generally see the script take about 11s to run on a busy Raspberry Pi 2 on a day with 7 APRS objects defined but takes less than 1s on a modern desktop machine. + +#### Calendar Setup + +Nothing really special to do for the Google Calendar setup, you can name it anything you want. But to get the URL, you'll need to go to your APRS calendar's settings and either use the Public address if you have made this calendar public, or use the Private address. Copy the URL and put it into the $ical variable in the get_defaults function in aprsobjects.pm Just click the appropriate ICAL button in Google Calendar settings and copy the link it gives you. You can see an example URL that I have set up in the file to sanity check that your URL looks right. + +#### APRS Object calendar entries + +When you create calendar entries for your APRS objects you can set the event title to anything you want, this value is not used by this script. If the event repeats or is an all day event (ie: A repeater, etc) select those options. The Description is where the required fields are defined and must be set up correctly or the object won't be properly parsed. The order of the options does not matter but they must be in the format of FIELD:DATA and separated by a new line. These are the same fields used in the objects array, so for more details on exactly what the fields mean please see that section. Example: + +TIMEBEFORE:15 +OBJNAME:IRLP-4945 +MHZ:146.940 +LAT:38^02.856N +LON:84^29.347W +FREQ:5:00 +OFFSET:-0.600 +TONE:88.5 +HEIGHT:500 +POWER:50 +SYMBOL:I0 +COMMENT:R45m KY4K IRLP node 4945 in Lexington, KY + +If you want to disable an object temporarily you can set it as "Available" under the "Show me as" setting near the bottom of the event entry. Available implies that the object is disabled (not active), Busy implies it is enabled (active). I use this to adjust recurring events, such as club meetings. You can also delete the event if that is more appropriate. + ### aprs.pl -The script which does the logic and prints the output of the matching objects. Unless there is a need to tweak some of the logic there is likely no reason to update this file. \ No newline at end of file +The script which does the logic and prints the output of the matching objects. Unless there is a need to tweak some of the logic there is likely no reason to update this file. + +### Todo + +#### Send packets directly, not using direwolf + +Being able to send packets directly to a TNC would be good, such as a KISS interface. I need to investigate the best way to do this, but I have a few ideas + + * beacon -d "APZ101 WIDE2-1" -t 60 0 '!DDMM.mmN/DDDMM.mmW$Comment' + * aprx maybe? + * Or directly in Perl using KISS + * http://search.cpan.org/~rbdavison/Device-TNC-0.03/lib/Device/TNC/KISS.pm + * http://search.cpan.org/dist/Ham-APRS-FAP/IS.pm \ No newline at end of file diff --git a/aprs.pl b/aprs.pl index 8ab25e4..c317e42 100644 --- a/aprs.pl +++ b/aprs.pl @@ -2,8 +2,9 @@ use strict; use warnings; +use Data::Dumper; -our $VERSION = '1.1'; +our $VERSION = '1.2'; # APRS Schedule generator for Direwolf # Based on http://www.aprs.org/info/netsked.txt @@ -13,22 +14,12 @@ our $VERSION = '1.1'; # that template and this output and use that as the final configuration. Repeat this every N minutes, # checking to see if the configuration has changed and if so restart direwolf so it picks up the new info. -# Todo -## Send packets directly, not using direwolf -### beacon -d "APZ101 WIDE2-1" -t 60 0 '!DDMM.mmN/DDDMM.mmW$Comment' -### aprx maybe? -### Or directly in Perl using KISS -### http://search.cpan.org/~rbdavison/Device-TNC-0.03/lib/Device/TNC/KISS.pm -### http://search.cpan.org/dist/Ham-APRS-FAP/IS.pm - -## Improve object date options -### Perhaps read a public iCal - # This perl program is generic, so we use our module for station-specific settings use aprsobjects; # Get default settings from our aprsobjects module -my ( $moduleversion, $startinterval, $delayinterval, @outputs ) +my ( $debug, $moduleversion, $icalurl, $startinterval, $delayinterval, + @outputs ) = get_defaults(); # Test to make sure our versions match so we know our fields are going to be correct @@ -39,12 +30,45 @@ if ( $VERSION ne $moduleversion ) { . $moduleversion . "\n"; } -# Get our objects from our aprsobjects module -my (@objects) = get_objects(); - # Get our initial delay for advertisements which will be incremented for each additional object my $delay = $startinterval; +# Create our empty objects variable to populate from our module or ical +my @objects; + +# See if we're using iCal and if so, make sure iCal::Parser and LWP::Simple are available +my $ical; + +if ($icalurl) { + $ical = eval { + require iCal::Parser; + iCal::Parser->import; + + require LWP::Simple; + LWP::Simple->import(qw($ua get)); + 1; + }; +} + +# Get the current time and set up variables to use later +my ($lt_sec, $lt_min, $lt_hour, $lt_mday, $lt_mon, + $lt_year, $lt_wday, $lt_yday, $lt_isdst +) = localtime(); +$lt_year += 1900; +$lt_mon += 1; + +if ($ical) { + + # Get our objects from our iCal URL + print "\n### Objects generated from iCal data\n"; + (@objects) = ical_parse($icalurl); +} +else { + # Get our objects from our aprsobjects module + print "\n### Objects generated using module data\n"; + (@objects) = get_objects(); +} + # Iterate through each entry in the @objects array foreach my $entry (@objects) { my ($DOW, $ENABLED, $MONTH, $DAY, $YEAR, @@ -53,6 +77,8 @@ foreach my $entry (@objects) { $HEIGHT, $POWER, $SYMBOL, $COMMENT ) = split( /\|/xsm, $entry ); + if ($debug) { print "### Object: $entry\n"; } + # Determine the time at which we start advertising the object before the official start time my $start; @@ -69,32 +95,27 @@ foreach my $entry (@objects) { $start = $STARTTIME; } + my $time = sprintf( "%02d%02d", $lt_hour, $lt_min ); + # We compare start/end as simple numbers, so strip out the colon $start =~ s/://g; my $end = $ENDTIME; $end =~ s/://g; - # Get the current time and build a numeric string to compare - my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) - = localtime(); - my $time = sprintf( "%02d%02d", $hour, $min ); - $year += 1900; - $mon += 1; - # Determine if this object matches the day, is an every-day object, or is dated with todays date # If the object is enabled make sure that dated events are only matched on their proper date regardless # of DOW settings, so make sure daily vs single day vs dated events are tested separately. if ($ENABLED # This object matches a single-day event, not a dated event - && (( ( $DOW eq $wday && $DAY !~ /\d+/ ) + && (( ( $DOW eq $lt_wday && $DAY !~ /\d+/ ) # This object matches a daily event, not a dated event || ( $DOW eq -1 && $DAY !~ /\d+/ ) ) # If it's not a single or daily event, test to see if this object matches todays date - || ( $MONTH eq $mon && $DAY eq $mday && $YEAR eq $year ) + || ( $MONTH eq $lt_mon && $DAY eq $lt_mday && $YEAR eq $lt_year ) ) ) { @@ -181,9 +202,154 @@ foreach my $entry (@objects) { } } +# Parse our iCal URL and return the same array we use in the module +sub ical_parse { + my ($url) = @_; + + if ($debug) { print "### ical_parse\n"; } + + # Set our LWP defaults, maybe + #$ua->timeout(15); + + my $file = get($url); + if (!defined($file)) + { + print "\n## iCal download failed (check URL?)\n"; + return; + } + my @returnobj; + +# Restrict our iCal event parsing to today and tomorrow, as the iCal data is in UTC +# We restrict the results because it takes forever to process lots of events and we only care about today anyway + my $start_time = sprintf( "%d%02d%02d", $lt_year, $lt_mon, $lt_mday ); + my $end_time = sprintf( "%d%02d%02d", $lt_year, $lt_mon, $lt_mday + 1 ); + my %defaults = ( start => $start_time, end => $end_time ); + my $parser = iCal::Parser->new(%defaults); + my $hash = $parser->parse_strings($file); + + # Get the events for today (specifically; ignore the rest of the iCal data) + my $todayhash = $hash->{events}->{$lt_year}->{$lt_mon}->{$lt_mday}; + my @todayarray = $todayhash; + + # Iterate through each calendar UID (event) + foreach my $uid (@todayarray) { + my @uidkeys = keys %$uid; + + # Iterate through each calendar object in this UID + foreach my $object (@uidkeys) { + my $DTSTART = $todayhash->{$object}->{DTSTART}; + my $DTEND = $todayhash->{$object}->{DTEND}; + my $allday = $todayhash->{$object}->{allday}; + my $DESCRIPTION = $todayhash->{$object}->{DESCRIPTION}; + my $TRANSP = $todayhash->{$object}->{TRANSP}; + # Make sure our hours and minutes are padded to two digits + my $start = sprintf( "%02d:%02d", + $DTSTART->{local_c}->{hour}, + $DTSTART->{local_c}->{minute} ); + my $end = sprintf( "%02d:%02d", + $DTEND->{local_c}->{hour}, + $DTEND->{local_c}->{minute} ); + my $month = $DTSTART->{local_c}->{month}; + my $day = $DTSTART->{local_c}->{day}; + my $year = $DTSTART->{local_c}->{year}; + my $dow = $DTSTART->{local_c}->{day_of_week}; + + # If allday isn't defined, set it to 0 + unless ( defined($allday) ) { + $allday = 0; + } + + # Clean up values the way we expect them to be for all-day events + #if ( $start eq "0:0" ) { + if ($allday) { + $dow = "-1"; + $start = "0"; + $end = "0"; + $day = ''; + $month = ''; + $year = ''; + } + +# Create a string like we'd use in the aprsobjects module so we can parse it the same + my $object = build_object( + $start, $end, $allday, $dow, $month, + $day, $year, $TRANSP, $DESCRIPTION + ); + push( @returnobj, $object ); + } + } + return @returnobj; +} + +# Build a string of this objects fields +sub build_object { + my ($start, $end, $allday, $dow, $month, + $day, $year, $TRANSP, $DESCRIPTION + ) = @_; + + if ($debug) { print "### build_object\n"; } + + # Split up DESCRIPTION into the fields we expect + my $objectparts = split_description($DESCRIPTION); + +# Use the "TRANSP" attribute to determine if this object is active or not based on Available/Busy in GCal + my $active = 1; + if ( $TRANSP eq "TRANSPARENT" ) { + $active = 0; + } + + my $object = join( "|", + $dow, $active, $month, $day, $year, $start, $end, $objectparts ); + return $object; +} + +# Split up the description line and return a joined string +sub split_description { + my ($description) = @_; + +# For now, disable uninitialized warnings here so we don't get noise from optional fields + no warnings 'uninitialized'; + + if ($debug) { print "### split_description\n"; } + + # We literally get the string \n for carriage returns here. Yuck? + my (@description) = split( /\\n/s, $description ); + + my ($timebefore, $objname, $mhz, $lat, $lon, $freq, + $offset, $tone, $height, $power, $symbol, $comment + ); + + foreach my $desc (@description) { + my ( $key, $value ) = split( /:/, $desc, 2 ); + if ( $key eq "TIMEBEFORE" ) { $timebefore = $value; } + if ( $key eq "OBJNAME" ) { $objname = $value; } + if ( $key eq "MHZ" ) { $mhz = $value; } + if ( $key eq "LAT" ) { $lat = $value; } + if ( $key eq "LON" ) { $lon = $value; } + if ( $key eq "FREQ" ) { $freq = $value; } + if ( $key eq "OFFSET" ) { $offset = $value; } + if ( $key eq "TONE" ) { $tone = $value; } + if ( $key eq "HEIGHT" ) { $height = $value; } + if ( $key eq "POWER" ) { $power = $value; } + if ( $key eq "SYMBOL" ) { $symbol = $value; } + +# We sometimes get backslashes back from iCal, so we need to strip them back out of the comment + if ( $key eq "COMMENT" ) { + $comment = $value; + $comment =~ s/\\//g; + } + } + + my $objectparts = join( "|", + $timebefore, $objname, $mhz, $lat, $lon, $freq, + $offset, $tone, $height, $power, $symbol, $comment ); + return $objectparts; +} + # Update time reference to reflect desired additional delay # Used for HH:MM and MM:SS but the calculations are the same. Pass in negative numbers to get an earlier time. sub update_delay { + if ($debug) { print "### update_delay\n"; } my ( $delaytmp, $delayintervaltmp ) = @_; my ( $sec_min, $sec_sec ) = split( /:/xsm, $delaytmp ); my $sec_seconds = $sec_min * 60 + $sec_sec + $delayintervaltmp; diff --git a/aprsobjects.pm b/aprsobjects.pm index 8d80b99..dbca230 100644 --- a/aprsobjects.pm +++ b/aprsobjects.pm @@ -7,8 +7,14 @@ require Exporter; # Set default options sub get_defaults { + # Debug mode + my $debug = 0; + # Version control our module to make sure our field expections are correct - my $moduleversion = '1.1'; + my $moduleversion = '1.2'; + + # If using iCal for our source, put the URL here + my $ical = "https://calendar.google.com/calendar/ical/vmk180ahs4stmati3go5dek4c8%40group.calendar.google.com/private-dbfa0949d58f94bf76f763f75bc8b107/basic.ics"; # How long to delay our initial beacon advertisement, in direwolf format (min:sec) my $startinterval = '0:30'; @@ -19,7 +25,7 @@ sub get_defaults { # Define our output paths for every object my @outputs = ( 'sendto=IG', 'via="WIDE1-1,WIDE2-1"' ); - return ( $moduleversion, $startinterval, $delayinterval, @outputs ); + return ( $debug, $moduleversion, $ical, $startinterval, $delayinterval, @outputs ); } # Objects array is a pipe (|) separated list of fields @@ -34,7 +40,7 @@ sub get_defaults { # YEAR - Year for a dated event (2016) # STARTTIME/ENDTIME - Start/End = 0 implies broadcast all day, HH:MM format # TIMEBEFORE - Number of minutes to start advertising object before STARTTIME -# OBJNAME - Name of APRS Object, ax 9 characters +# OBJNAME - Name of APRS Object, max 9 characters # MHZ - Frequency associated with this object # LAT/LON - Latitude/Longitude of object # FREQ - How often to beacon object @@ -49,20 +55,7 @@ sub get_objects { my @objects = ( # DOW ENABLED MONTH DAY YEAR STARTTIME ENDTIME TIMEBEFORE OBJNAME MHZ LAT LON FREQ OFFSET TONE HEIGHT POWER SYMBOL COMMENT '0|1||||21:00|22:00|15|NETATVCOM|146.760|38^02.380N|84^24.170W|5:00|-0.600||750|50|/N|9pm R30m AVT and Specialized Communications Net Sun', - '1|1||||19:00|20:00|15|NET-ARES|146.865|37^43.778N|84^19.012W|5:00|-0.600|192.8|200|50|/N|7pm Madison County ARES Net Mon', - '2|1||||20:00|21:00|15|NET-SWAP|145.370|37^43.778N|84^19.012W|5:00|-0.600|192.8|200|50|/N|8pm Amateur Swap Net Tue', - '3|1||||20:30|21:30|15|NET-EMERG|146.715|37^43.778N|84^19.012W|5:00|-0.600|100.0|200|50|/N|8:30pm Wilderness Trail Emergency Net Wed', - '3|1||||21:00|22:00|15|NET-ARES|146.760|38^02.380N|84^24.170W|5:00|-0.600||750|50|/N|9pm Fayette County ARES Net Wed', - '4|1||||19:00|21:00|60|MEET-TECH|146.760|38^05.152N|84^29.347W|5:00|-0.600||750|50|/E|7pm BARS Technical Group Thur Basement Red Cross Building', - '4|1||||19:30|20:30|15|NET-JAWS|145.490|37^53.077N|84^34.430W|5:00|-0.600||150|50|/N|7:30pm JAWS Rag Chew Net Thur', - '4|1||||20:00|21:00|15|NET-ARES|145.330|38^02.973N|84^45.157W|5:00|-0.600||200|50|/N|8pm Woodford County ARES Net Thurs', - '4|1||||20:45|21:45|15|NET-PRC|145.430|38^00.389N|84^13.606W|5:00|-0.600|203.5|200|50|/N|8:45pm Pioneer Amateur Radio Club Net Thur', - '4|1||||21:00|22:00|15|NET-IRLP|146.940|38^02.856N|84^29.943W|5:00|-0.600|88.5|500|50|/N|9pm IRLP Wide Area Net Thur 9pm', - '6|1||||11:00|13:00|60|BARSSHACK|146.760|38^05.152N|84^29.347W|5:00|-0.600||750|50|/E|11am BARS Shack Open Sat 11-1pm Red Cross Building', - '6|1||||13:00|15:00|60|WORKSHOP|146.760|38^05.152N|84^29.347W|5:00|-0.600||750|50|/E|1pm BARS Radio Theory and Construction Workshop Sat 1-3pm Basement Red Cross Building', - '-1|1|8|14|2016|8:00|14:00|60|BARSHAMFS|146.760|38^01.382N|84^54.372W|5:00|-0.600||750|50|/N|8am to 2pm Today BARS Hamfest', - '-1|1|9|24|2016|8:00|14:00|60|CK-HAMFST|146.865|37^43.778N|84^19.012W|5:00|-0.600|192.8|200|50|/N|8am to 2pm Today Central Kentucky Hamfest', - '-1|1||||0|0|15|IRLP-4945|146.940|38^02.856N|84^29.943W|10:00|-0.600|88.5|500|50|I0|R45m KY4K IRLP node 4945 in Lexington, KY', + '-1|1|9|24|2016|8:00|14:00|60|CK-HAMFST|146.865|37^43.778N|84^19.012W|5:00|-0.600|192.8|200|50|/E|8am to 2pm Today Central Kentucky Hamfest', '-1|1||||0|0|15|146.940/R|146.940|38^02.856N|84^29.943W|10:00|-0.600|88.5|500|50|/r|R45m KY4K IRLP node 4945 in Lexington, KY', ); return (@objects); -- GitLab From 9874de46f0db103f9a378e52a0a42a0ceb4db8b8 Mon Sep 17 00:00:00 2001 From: Walter Francis Date: Thu, 14 Jul 2016 11:44:55 -0400 Subject: [PATCH 2/4] Little perlcritic cleanup and fix README iCal field list --- README.md | 28 +++++++++++++++------------- aprs.pl | 32 ++++++++++++++++---------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 48a81be..eb2533b 100644 --- a/README.md +++ b/README.md @@ -75,18 +75,18 @@ Nothing really special to do for the Google Calendar setup, you can name it anyt When you create calendar entries for your APRS objects you can set the event title to anything you want, this value is not used by this script. If the event repeats or is an all day event (ie: A repeater, etc) select those options. The Description is where the required fields are defined and must be set up correctly or the object won't be properly parsed. The order of the options does not matter but they must be in the format of FIELD:DATA and separated by a new line. These are the same fields used in the objects array, so for more details on exactly what the fields mean please see that section. Example: -TIMEBEFORE:15 -OBJNAME:IRLP-4945 -MHZ:146.940 -LAT:38^02.856N -LON:84^29.347W -FREQ:5:00 -OFFSET:-0.600 -TONE:88.5 -HEIGHT:500 -POWER:50 -SYMBOL:I0 -COMMENT:R45m KY4K IRLP node 4945 in Lexington, KY +TIMEBEFORE:15 +OBJNAME:IRLP-4945 +MHZ:146.940 +LAT:38^02.856N +LON:84^29.347W +FREQ:5:00 +OFFSET:-0.600 +TONE:88.5 +HEIGHT:500 +POWER:50 +SYMBOL:I0 +COMMENT:R45m KY4K IRLP node 4945 in Lexington, KY If you want to disable an object temporarily you can set it as "Available" under the "Show me as" setting near the bottom of the event entry. Available implies that the object is disabled (not active), Busy implies it is enabled (active). I use this to adjust recurring events, such as club meetings. You can also delete the event if that is more appropriate. @@ -104,4 +104,6 @@ Being able to send packets directly to a TNC would be good, such as a KISS inter * aprx maybe? * Or directly in Perl using KISS * http://search.cpan.org/~rbdavison/Device-TNC-0.03/lib/Device/TNC/KISS.pm - * http://search.cpan.org/dist/Ham-APRS-FAP/IS.pm \ No newline at end of file + * http://search.cpan.org/dist/Ham-APRS-FAP/IS.pm + +#### Clean up ugly event date check nested if statement \ No newline at end of file diff --git a/aprs.pl b/aprs.pl index c317e42..e7fa1fe 100644 --- a/aprs.pl +++ b/aprs.pl @@ -2,7 +2,6 @@ use strict; use warnings; -use Data::Dumper; our $VERSION = '1.2'; @@ -83,7 +82,7 @@ foreach my $entry (@objects) { my $start; # Only modify the start time if we're a timed object - if ( $STARTTIME ne 0 && $ENDTIME ne 0 ) { + if ( $STARTTIME ne "0" && $ENDTIME ne "0" ) { # Set our time before to a negative value so we subtract time from our official start time my $timebefore = $TIMEBEFORE * -1; @@ -98,9 +97,9 @@ foreach my $entry (@objects) { my $time = sprintf( "%02d%02d", $lt_hour, $lt_min ); # We compare start/end as simple numbers, so strip out the colon - $start =~ s/://g; + $start =~ s/://xg; my $end = $ENDTIME; - $end =~ s/://g; + $end =~ s/://xg; # Determine if this object matches the day, is an every-day object, or is dated with todays date # If the object is enabled make sure that dated events are only matched on their proper date regardless @@ -108,10 +107,10 @@ foreach my $entry (@objects) { if ($ENABLED # This object matches a single-day event, not a dated event - && (( ( $DOW eq $lt_wday && $DAY !~ /\d+/ ) + && (( ( $DOW eq $lt_wday && $DAY !~ /\d+/x ) # This object matches a daily event, not a dated event - || ( $DOW eq -1 && $DAY !~ /\d+/ ) + || ( $DOW eq "-1" && $DAY !~ /\d+/x ) ) # If it's not a single or daily event, test to see if this object matches todays date @@ -121,7 +120,7 @@ foreach my $entry (@objects) { { # Determine if the object should be advertised based on comparing our adjusted start time or always if start/end are 0 - if ( ( $STARTTIME eq 0 && $ENDTIME eq 0 ) + if ( ( $STARTTIME eq "0" && $ENDTIME eq "0" ) || ( ( $start <= $time ) && ( $time <= $end ) ) ) { # Handle fields which are optional and may be empty @@ -155,12 +154,12 @@ foreach my $entry (@objects) { my $datestring = ''; # If we have a start/end time, and we're not a dated event - if ( $STARTTIME && $DAY !~ /\d+/ ) { + if ( $STARTTIME && $DAY !~ /\d+/x ) { $datestring = ' (' . $STARTTIME . ' -' . $ENDTIME . ')'; } # If we're a dated event - if ( $DAY =~ /\d+/ ) { + if ( $DAY =~ /\d+/x ) { $datestring = ' (' . $MONTH . '/' @@ -212,11 +211,11 @@ sub ical_parse { #$ua->timeout(15); my $file = get($url); - if (!defined($file)) - { + if ( !defined($file) ) { print "\n## iCal download failed (check URL?)\n"; return; } + my @returnobj; # Restrict our iCal event parsing to today and tomorrow, as the iCal data is in UTC @@ -242,8 +241,9 @@ sub ical_parse { my $allday = $todayhash->{$object}->{allday}; my $DESCRIPTION = $todayhash->{$object}->{DESCRIPTION}; my $TRANSP = $todayhash->{$object}->{TRANSP}; + # Make sure our hours and minutes are padded to two digits - my $start = sprintf( "%02d:%02d", + my $start = sprintf( "%02d:%02d", $DTSTART->{local_c}->{hour}, $DTSTART->{local_c}->{minute} ); my $end = sprintf( "%02d:%02d", @@ -313,14 +313,14 @@ sub split_description { if ($debug) { print "### split_description\n"; } # We literally get the string \n for carriage returns here. Yuck? - my (@description) = split( /\\n/s, $description ); + my (@description) = split( /\\n/xs, $description ); my ($timebefore, $objname, $mhz, $lat, $lon, $freq, $offset, $tone, $height, $power, $symbol, $comment ); foreach my $desc (@description) { - my ( $key, $value ) = split( /:/, $desc, 2 ); + my ( $key, $value ) = split( /:/x, $desc, 2 ); if ( $key eq "TIMEBEFORE" ) { $timebefore = $value; } if ( $key eq "OBJNAME" ) { $objname = $value; } if ( $key eq "MHZ" ) { $mhz = $value; } @@ -336,7 +336,7 @@ sub split_description { # We sometimes get backslashes back from iCal, so we need to strip them back out of the comment if ( $key eq "COMMENT" ) { $comment = $value; - $comment =~ s/\\//g; + $comment =~ s/\\//xg; } } @@ -349,8 +349,8 @@ sub split_description { # Update time reference to reflect desired additional delay # Used for HH:MM and MM:SS but the calculations are the same. Pass in negative numbers to get an earlier time. sub update_delay { - if ($debug) { print "### update_delay\n"; } my ( $delaytmp, $delayintervaltmp ) = @_; + if ($debug) { print "### update_delay\n"; } my ( $sec_min, $sec_sec ) = split( /:/xsm, $delaytmp ); my $sec_seconds = $sec_min * 60 + $sec_sec + $delayintervaltmp; my $min_decmin = $sec_seconds % 60; -- GitLab From 49d5e94a243893294be9a9eda1bf1d1b2aca1212 Mon Sep 17 00:00:00 2001 From: Walter Francis Date: Sun, 17 Jul 2016 23:05:31 -0400 Subject: [PATCH 3/4] Have main script die with error in case we fail so we can capture this in our start-up script and handle it, such as reverting to a known-good config file, such as an iCal query failed or some other temporary issue. Updated dw-start example script to match. Cleaned up README file a little. --- README.md | 10 ++++++---- aprs.pl | 3 +-- dw-start.sh | 11 ++++++++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index eb2533b..9953821 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This script is based on the ideas from Bob Bruninga (WB4APR) in http://www.aprs. ### aprsobjects.pm -You will need to update aprsobjects.pm with the settings and method you wish to use to define objects. If you want to use iCal you must set up an iCal calendar and set the public URL for it, or you can define the objects using the @objects array. To set up iCal see the second below specific to that topic. +You will need to update aprsobjects.pm with the settings and method you wish to use to define objects. If you want to use iCal you must set up an iCal calendar and set the public URL for it, or you can define the objects using the @objects array. To set up iCal see the section below specific to that topic. #### Default options @@ -24,7 +24,7 @@ There are several defaults to set in the get_defaults function * To ensure our module fields matches our program fields, this must match the version in the main program. This just indicates the need to ensure any fields you have defined still meet the fields the program needs. * $ical = "https://calendar.google.com/calendar/ical/....../basic.ics"; - * iCal url when using iCal as a data source for objects. Disables the use of using the @objects array in aprsobjects.pm + * iCal url when using iCal as a data source for objects. Disables the use of using the @objects array in aprsobjects.pm If you do not want to use iCal as a source, set this to an empty string or to undef * $startinterval = '0:30'; * How long to delay our initial beacon advertisement, in direwolf format (min:sec). In this example, it means Direwolf will wait 30 seconds before it advertises the first object. @@ -37,6 +37,8 @@ There are several defaults to set in the get_defaults function #### Objects array +The @objects array is how you define the APRS objects you want to advertise. If you're using iCal there is no need to populate anything in this object. + * Objects array is a pipe (|) separated list of fields * Leave empty for unused field, ie: |data|data||data|data * General Formatting is whatever Direwolf requires for the given field, see examples, Direwolf docs, etc @@ -65,7 +67,7 @@ There are several defaults to set in the get_defaults function Setting up APRS objects in the @object array can be a bit tedious and requires someone editing the file. Using an iCal URL as a data source is potentially an easier way to define objects, and this also allows you to delegate the adding and maintaining of objects to other people as you can share your calendar with other users. I have tested this with Google Calendar but it should work with other calendars as well. Be aware that this data is pulled down every run, and can take several seconds to download and parse. If you need your data to not require an internet connection it is better to use the @objects array. -I have found that a dozen APRS object events with recurring settings can take a long time to parse, so the script limits the days evaluated to today and tomorrow (as events are dated as UTC, events in the evening may be dated tomorrow in the raw iCal data that is parsed). This speeds up the parsing considerably and since we're only generally advertising for events in immediate future this is fine. When limited to this timeframe I generally see the script take about 11s to run on a busy Raspberry Pi 2 on a day with 7 APRS objects defined but takes less than 1s on a modern desktop machine. +I have found that a dozen APRS object events with recurring settings can take a long time to parse, so the script limits the days evaluated to today and tomorrow (as events are dated as UTC, events in the evening may be dated tomorrow in the raw iCal data that is parsed). This speeds up the parsing considerably and since we're only generally advertising for events in the immediate future this is fine. When limited to this timeframe I generally see the script take about 11s to run on a busy Raspberry Pi 2 on a day with 7 APRS objects defined but takes less than 1s on a modern desktop machine. #### Calendar Setup @@ -73,7 +75,7 @@ Nothing really special to do for the Google Calendar setup, you can name it anyt #### APRS Object calendar entries -When you create calendar entries for your APRS objects you can set the event title to anything you want, this value is not used by this script. If the event repeats or is an all day event (ie: A repeater, etc) select those options. The Description is where the required fields are defined and must be set up correctly or the object won't be properly parsed. The order of the options does not matter but they must be in the format of FIELD:DATA and separated by a new line. These are the same fields used in the objects array, so for more details on exactly what the fields mean please see that section. Example: +When you create calendar entries for your APRS objects you can set the event title to anything you want, this value is not used by this script. If the event repeats or is an all day event (ie: a repeater, etc) select those options. The Description is where the required fields are defined and must be set up correctly or the object won't be properly parsed. The order of the options does not matter but they must be in the format of FIELD:DATA and separated by a new line. These are the same fields used in the objects array, so for more details on exactly what the fields mean please see that section. Example: TIMEBEFORE:15 OBJNAME:IRLP-4945 diff --git a/aprs.pl b/aprs.pl index e7fa1fe..14beee0 100644 --- a/aprs.pl +++ b/aprs.pl @@ -212,8 +212,7 @@ sub ical_parse { my $file = get($url); if ( !defined($file) ) { - print "\n## iCal download failed (check URL?)\n"; - return; + die "\n## iCal download failed (check URL?)\n"; } my @returnobj; diff --git a/dw-start.sh b/dw-start.sh index e02b289..e58b289 100755 --- a/dw-start.sh +++ b/dw-start.sh @@ -26,7 +26,7 @@ if [ $pid -gt 0 ]; then killall direwolf sleep 5 else - echo "Config files match, exiting" + echo "Config files match, exiting dw-start script" exit fi fi @@ -35,5 +35,14 @@ fi mv $config ${config}.bak cat $template >$config perl aprs.pl >>$config + +# Check our return code; if it's non-zero let's revert to our previous config file +rc=$? + +if [ $rc != 0 ]; then + echo "APRS Script failed, reverting" + mv ${config}.bak $config +fi + # Start Direwolf /usr/local/bin/direwolf -t 0 -c $config >/opt/direwolf/logs/direwolf.log -- GitLab From fc6534979e33715a6c726d4ff7685fec8c201400 Mon Sep 17 00:00:00 2001 From: Walter Francis Date: Thu, 4 Aug 2016 15:10:11 -0400 Subject: [PATCH 4/4] Cleaned up use of module, did some more linting --- aprs.pl | 96 +++++++++++++++++++++++++++----------------------- aprsobjects.pm | 45 ++++++++++------------- 2 files changed, 71 insertions(+), 70 deletions(-) diff --git a/aprs.pl b/aprs.pl index 14beee0..017be8a 100644 --- a/aprs.pl +++ b/aprs.pl @@ -2,6 +2,8 @@ use strict; use warnings; +use FindBin; +use lib $FindBin::Bin; our $VERSION = '1.2'; @@ -17,24 +19,25 @@ our $VERSION = '1.2'; use aprsobjects; # Get default settings from our aprsobjects module -my ( $debug, $moduleversion, $icalurl, $startinterval, $delayinterval, - @outputs ) - = get_defaults(); +my $debug = $aprsobjects::debug; +my $moduleversion = $aprsobjects::moduleversion; +my $icalurl = $aprsobjects::icalurl; +my $startinterval = $aprsobjects::startinterval; +my $delayinterval = $aprsobjects::delayinterval; +my @outputs = @aprsobjects::outputs; +my @objects = @aprsobjects::objects; # Test to make sure our versions match so we know our fields are going to be correct if ( $VERSION ne $moduleversion ) { die 'Our program version number ' - . $VERSION - . ' does not match our module version ' - . $moduleversion . "\n"; + . $VERSION + . ' does not match our module version ' + . $moduleversion . "\n"; } # Get our initial delay for advertisements which will be incremented for each additional object my $delay = $startinterval; -# Create our empty objects variable to populate from our module or ical -my @objects; - # See if we're using iCal and if so, make sure iCal::Parser and LWP::Simple are available my $ical; @@ -50,7 +53,8 @@ if ($icalurl) { } # Get the current time and set up variables to use later -my ($lt_sec, $lt_min, $lt_hour, $lt_mday, $lt_mon, +my ( + $lt_sec, $lt_min, $lt_hour, $lt_mday, $lt_mon, $lt_year, $lt_wday, $lt_yday, $lt_isdst ) = localtime(); $lt_year += 1900; @@ -65,12 +69,12 @@ if ($ical) { else { # Get our objects from our aprsobjects module print "\n### Objects generated using module data\n"; - (@objects) = get_objects(); } # Iterate through each entry in the @objects array foreach my $entry (@objects) { - my ($DOW, $ENABLED, $MONTH, $DAY, $YEAR, + my ( + $DOW, $ENABLED, $MONTH, $DAY, $YEAR, $STARTTIME, $ENDTIME, $TIMEBEFORE, $OBJNAME, $MHZ, $LAT, $LON, $FREQ, $OFFSET, $TONE, $HEIGHT, $POWER, $SYMBOL, $COMMENT @@ -104,10 +108,13 @@ foreach my $entry (@objects) { # Determine if this object matches the day, is an every-day object, or is dated with todays date # If the object is enabled make sure that dated events are only matched on their proper date regardless # of DOW settings, so make sure daily vs single day vs dated events are tested separately. - if ($ENABLED + if ( + $ENABLED # This object matches a single-day event, not a dated event - && (( ( $DOW eq $lt_wday && $DAY !~ /\d+/x ) + && ( + ( + ( $DOW eq $lt_wday && $DAY !~ /\d+/x ) # This object matches a daily event, not a dated event || ( $DOW eq "-1" && $DAY !~ /\d+/x ) @@ -116,7 +123,7 @@ foreach my $entry (@objects) { # If it's not a single or daily event, test to see if this object matches todays date || ( $MONTH eq $lt_mon && $DAY eq $lt_mday && $YEAR eq $lt_year ) ) - ) + ) { # Determine if the object should be advertised based on comparing our adjusted start time or always if start/end are 0 @@ -160,13 +167,12 @@ foreach my $entry (@objects) { # If we're a dated event if ( $DAY =~ /\d+/x ) { - $datestring - = ' (' - . $MONTH . '/' - . $DAY . '/' - . $YEAR . ') (' - . $STARTTIME . ' - ' - . $ENDTIME . ') '; + $datestring = ' (' + . $MONTH . '/' + . $DAY . '/' + . $YEAR . ') (' + . $STARTTIME . ' - ' + . $ENDTIME . ') '; } @@ -174,25 +180,25 @@ foreach my $entry (@objects) { foreach my $output (@outputs) { print 'OBEACON ' - . $output - . ' DELAY=' - . $delay - . ' EVERY=' - . $FREQ - . ' OBJNAME=' - . $OBJNAME . ' LAT=' - . $LAT - . ' LONG=' - . $LON - . ' SYMBOL=' - . $SYMBOL - . ' FREQ=' - . $MHZ - . $offset_string - . $tone_string - . $height_string - . $power_string - . $comment_string . "\n"; + . $output + . ' DELAY=' + . $delay + . ' EVERY=' + . $FREQ + . ' OBJNAME=' + . $OBJNAME . ' LAT=' + . $LAT + . ' LONG=' + . $LON + . ' SYMBOL=' + . $SYMBOL + . ' FREQ=' + . $MHZ + . $offset_string + . $tone_string + . $height_string + . $power_string + . $comment_string . "\n"; # Update the delay value so each object advertises $delayinterval seconds after the previous one $delay = update_delay( $delay, $delayinterval ); @@ -225,7 +231,7 @@ sub ical_parse { my $parser = iCal::Parser->new(%defaults); my $hash = $parser->parse_strings($file); - # Get the events for today (specifically; ignore the rest of the iCal data) + # Get the events for today (specifically; ignore the rest of the iCal data) my $todayhash = $hash->{events}->{$lt_year}->{$lt_mon}->{$lt_mday}; my @todayarray = $todayhash; @@ -282,7 +288,8 @@ sub ical_parse { # Build a string of this objects fields sub build_object { - my ($start, $end, $allday, $dow, $month, + my ( + $start, $end, $allday, $dow, $month, $day, $year, $TRANSP, $DESCRIPTION ) = @_; @@ -314,7 +321,8 @@ sub split_description { # We literally get the string \n for carriage returns here. Yuck? my (@description) = split( /\\n/xs, $description ); - my ($timebefore, $objname, $mhz, $lat, $lon, $freq, + my ( + $timebefore, $objname, $mhz, $lat, $lon, $freq, $offset, $tone, $height, $power, $symbol, $comment ); diff --git a/aprsobjects.pm b/aprsobjects.pm index dbca230..dd16811 100644 --- a/aprsobjects.pm +++ b/aprsobjects.pm @@ -2,31 +2,26 @@ package aprsobjects; require Exporter; @ISA = ("Exporter"); -@EXPORT = ( "get_objects", "get_defaults" ); # Set default options -sub get_defaults { - # Debug mode - my $debug = 0; +# Debug mode +our $debug = 0; - # Version control our module to make sure our field expections are correct - my $moduleversion = '1.2'; +# Version control our module to make sure our field expections are correct +our $moduleversion = '1.2'; - # If using iCal for our source, put the URL here - my $ical = "https://calendar.google.com/calendar/ical/vmk180ahs4stmati3go5dek4c8%40group.calendar.google.com/private-dbfa0949d58f94bf76f763f75bc8b107/basic.ics"; +# If using iCal for our source, put the URL here +our $icalurl = "https://calendar.google.com/calendar/ical/vmk180ahs4stmati3go5dek4c8%40group.calendar.google.com/private-dbfa0949d58f94bf76f763f75bc8b107/basic.ics"; - # How long to delay our initial beacon advertisement, in direwolf format (min:sec) - my $startinterval = '0:30'; +# How long to delay our initial beacon advertisement, in direwolf format (min:sec) +our $startinterval = '0:30'; - # How many more seconds of delay to add between each additional beacon so advertisements are spaced out - my $delayinterval = '15'; +# How many more seconds of delay to add between each additional beacon so advertisements are spaced out +our $delayinterval = '15'; - # Define our output paths for every object - my @outputs = ( 'sendto=IG', 'via="WIDE1-1,WIDE2-1"' ); - - return ( $debug, $moduleversion, $ical, $startinterval, $delayinterval, @outputs ); -} +# Define our output paths for every object +our @outputs = ( 'sendto=IG', 'via="WIDE1-1,WIDE2-1"' ); # Objects array is a pipe (|) separated list of fields ## Leave empty for unused field, ie: |data|data||data|data @@ -51,14 +46,12 @@ sub get_defaults { # SYMBOL - APRS Symbol to use for object # COMMENT (OPTIONAL) - Description of object -sub get_objects { - my @objects = ( - # DOW ENABLED MONTH DAY YEAR STARTTIME ENDTIME TIMEBEFORE OBJNAME MHZ LAT LON FREQ OFFSET TONE HEIGHT POWER SYMBOL COMMENT - '0|1||||21:00|22:00|15|NETATVCOM|146.760|38^02.380N|84^24.170W|5:00|-0.600||750|50|/N|9pm R30m AVT and Specialized Communications Net Sun', - '-1|1|9|24|2016|8:00|14:00|60|CK-HAMFST|146.865|37^43.778N|84^19.012W|5:00|-0.600|192.8|200|50|/E|8am to 2pm Today Central Kentucky Hamfest', - '-1|1||||0|0|15|146.940/R|146.940|38^02.856N|84^29.943W|10:00|-0.600|88.5|500|50|/r|R45m KY4K IRLP node 4945 in Lexington, KY', - ); - return (@objects); -} +our @objects = ( + +# DOW ENABLED MONTH DAY YEAR STARTTIME ENDTIME TIMEBEFORE OBJNAME MHZ LAT LON FREQ OFFSET TONE HEIGHT POWER SYMBOL COMMENT + '0|1||||21:00|22:00|15|NETATVCOM|146.760|38^02.380N|84^24.170W|5:00|-0.600||750|50|/N|9pm R30m AVT and Specialized Communications Net Sun', + '-1|1|9|24|2016|8:00|14:00|60|CK-HAMFST|146.865|37^43.778N|84^19.012W|5:00|-0.600|192.8|200|50|/E|8am to 2pm Today Central Kentucky Hamfest', + '-1|1||||0|0|15|146.940/R|146.940|38^02.856N|84^29.943W|10:00|-0.600|88.5|500|50|/r|R45m KY4K IRLP node 4945 in Lexington, KY', +); 1; -- GitLab