typedef struct { uint8_t mode; uint8_t hour; uint8_t minute; uint8_t second; uint16_t offset; uint8_t command; uint8_t datalength; uint8_t data[16]; } SCHEDULEENTRY; #### #runonce hour minute second command offset values 0 0 0 0 DEEPSLEEP 0 0 19 20 0 0 20 0 STREAM_TELEMETRY 0 0 0 0 20 30 SWITCHED_POWER 0 1 0 0 21 0 STREAM_TELEMETRY 0 0 0 0 22 0 WRITECOIL 0 2 4 1 0 0 23 0 STREAM_TELEMETRY 0 0 0 0 23 30 SWITCHED_POWER 0 0 0 0 24 0 DEEPSLEEP 0 0 59 0 #### #!/usr/bin/env perl use strict; use warnings; use Carp; use Data::Dumper; # Command map (text to command number). This should be parsed from radioduino/globals.h, # but for now it's just hardcoded my %commandmap = ( WRITECONFIG => 2, READCONFIG => 3, RECONFIGURE_RADIO => 5, WRITEFRAM => 6, READFRAM => 7, PING => 9, WRITE_RTC => 16, READ_RTC => 17, STREAM_TELEMETRY => 19, READINPUTREGISTERS => 20, READHOLDINGREGISTERS => 22, WRITEHOLDINGREGISTERS => 24, READCOIL => 26, WRITECOIL => 28, READDISCRETE => 30, '32KHZ' => 50, SWITCHED_POWER => 52, DEEPSLEEP => 54, DEBUG => 60, ); # Open the file handles open(my $ifh, '<', 'crontab') or croak($!); open(my $ofh, '>', 'crontab.bin') or croak($!); binmode $ofh; # Format of the plaintext crontab #runonce hour minute second command offset values my $cnt = 0; while((my $line = <$ifh>)) { $cnt++; chomp $line; # Ignore comments next if($line =~ /^\#/); # Make sure there is only one space between columns $line =~ s/\ +/ /g; # Split elements and do some very basic validation my ($runonce, $hour, $minute, $second, $command, $offset, @values) = split/\ /, $line; if(!scalar @values) { croak("Line $cnt: has not enough values: $line\n"); } # Turn the given command NAME into the correct command NUMBER if(!defined($commandmap{$command})) { croak("Line $cnt: Unknown command $command\n"); } my $numcommand = $commandmap{$command}; # Internally in the Radioduino, the "runonce" flag also serves as "invalid record" flag, so turn the plaintext 0/1 boolean into the correct number my $mode = 0; if($runonce == 1) { $mode = 2; } else { $mode = 1; } # Bulk up @values to the correct length of 16 bytes while((scalar @values) < 16) { push @values, 0; } # Turn everything into binary my $event = ''; $event .= chr($mode); $event .= chr($hour); $event .= chr($minute); $event .= chr($second); $event .= chr(($offset >> 8) & 0xff); $event .= chr($offset & 0xff); $event .= chr($numcommand); $event .= chr(scalar @values); foreach my $val (@values) { $event .= chr($val); } # Write binary entry to file print $ofh $event; print $line, "\n"; } # Fill in empty records, so we overwrite any old entries with empty/invalid ones while($cnt < 86) { my $event = chr(0) x 24; print $ofh $event; $cnt++; } # Fill the remaining 8 bytes with zeroes as well for(1..8) { print $ofh chr(0); } # close filehandles close $ifh; close $ofh #### #!/usr/bin/env perl use strict; use warnings; use Time::HiRes qw(sleep); use Data::Dumper; use Net::Clacks::Client; use XML::Simple; use Carp; # Load the temporary GSPSupport module (currently under redesign due to protocol change) # FIXME: This script uses all kinds of hardcoded values. This will need to change after the protocol upgrade BEGIN { unshift @INC, './'; }; use GSPSupport; my $project = GSPSupport->parse(); # Load local config (e.g. with radio device we are talking to, relay routing etc) my $config = GSPSupport::loadClacksConfig(); # Slurb the binary crontab file my $tmptab = GSPSupport::slurpBinFile('crontab.bin'); my @crontab = split//, $tmptab; my $offset = 0; my $lastpacket; my $resendtime = 0; # Connect to Net::Clacks and listen to packets from our target device my $clacks = Net::Clacks::Client->new($config->{host}, $config->{port}, $config->{user}, $config->{password}, 'GSPDecoder'); $clacks->listen('GSP::RECIEVE::' . $project->{textid}); my $nextping = 0; my $done = 0; # Initiate stream by sending the first chunk sendNextChunk(); while(!$done) { my $now = time; if($nextping < $now) { $clacks->ping(); $nextping = $now + 30; } # Check for timeout and resend packet if required if($resendtime > 0 && $resendtime < time) { $clacks->set('GSP::SEND', $lastpacket); $clacks->doNetwork(); $resendtime = time + 5; } $clacks->doNetwork(); while((my $message = $clacks->getNext())) { if($message->{type} eq 'disconnect') { $clacks->listen('GSP::RECIEVE::' . $project->{textid}); $clacks->ping(); $clacks->doNetwork(); $nextping = $now + 30; next; } next unless($message->{type} eq 'set'); next unless($message->{name} eq 'GSP::RECIEVE::' . $project->{textid}); # We got some packet from our target Radioduino, check if it's the right one (it may not be, it could be some other # telemetry stuff) decodeFrame($message->{data}); } sleep(0.01); } exit(0); my @frame; sub decodeFrame { my $line = shift; @frame = GSPSupport::packet2frame($line); if($frame[6] != $project->{id}) { # Not from this project return; } if($frame[8] == 255) { # ERROR FRAME. This happens if we did not disable the scheduler manually before starting to upload # a new crontab GSPSupport::decodeErrorFrame(@frame); $done = 1; return; } if($frame[8] != 8) { # Not a COMMAND_DOWN_READFRAM, probably some other automatic telemetry stream return; } if($offset == 2048) { # Seems we have send all data, so we are done print "All sent!\n"; $done = 1; return; } # Ok, send the next chunk of data sendNextChunk(); } sub sendNextChunk { print "Sending $offset...\n"; # Get an empty frame with default values like routing already set my @outframe = GSPSupport::emptyframe($project); # Set command number $outframe[8] = 6; # COMMAND_UP_WRITEFRAM # Set FRAM address and data length $outframe[9] = ($offset >> 8) & 0xff; $outframe[10] = $offset & 0xff; $outframe[11] = 16; # Copy the data chunk for(my $i = 0; $i < 16; $i++) { my $char = shift @crontab; $outframe[12 + $i] = ord($char); } # Turn this array into a proper nRF24-over-Clacks frame packet my $packet = GSPSupport::frame2packet(@outframe); # Remember the packet in case we need to resend it $lastpacket = $packet; # Send it via clacks and remember the new timeout time $clacks->set('GSP::SEND', $packet); $clacks->doNetwork(); $resendtime = time + 5; # Move offset $offset += 16; return; } #### package GSPSupport; use strict; use warnings; use XML::Simple; use Carp; use English; sub parse { open(my $ifh, '<', 'projectname.txt') or croak($!); my $projectname = <$ifh>; close $ifh; chomp $projectname; if(!length($projectname)) { croak("No projectname given!"); } my $devicesfile = '/home/cavac/src/gardenspaceagency/missioncontrol/devices.xml'; if(!-f $devicesfile) { $devicesfile = '../devices.xml'; } my $deviceconfig = XMLin($devicesfile); my $devices = $deviceconfig->{device}; if(!defined($devices->{$projectname})) { croak("Unknown project $projectname"); } my $config = $devices->{$projectname}; my @rf24routing; push @rf24routing, $config->{id}; while((scalar @rf24routing < 5)) { push @rf24routing, 0; } $config->{rf24routing} = \@rf24routing; return $config; } sub loadClacksConfig { my $configfile = '/home/cavac/src/clacksconfig.xml'; if(!-f $configfile) { $configfile = '../clacksconfig.xml'; } my $config = XMLin($configfile); return $config; } sub emptyframe { my $config = shift; my @outframe = (0) x 30; # Sender, reciever $outframe[0] = 0x01; # Linksender MODEM for(my $i = 0; $i < 5; $i++) { $outframe[$i + 1] = $config->{rf24routing}->[$i]; } $outframe[6] = 0x01; # Real sender MODEM $outframe[7] = $config->{id}; # Real reciever return @outframe; } sub frame2packet { my @frame = @_; my $packet = ''; foreach my $byte (@frame) { my $lowbyte = ($byte & 0x0f) + 65; my $highbyte = ($byte >> 4) + 65; $packet .= chr($highbyte); $packet .= chr($lowbyte); } return $packet; } sub packet2frame { my $packet = shift; my @chars = split//, $packet; my @frame = (); # Decode to bytes while(@chars) { my $high = shift @chars; my $low = shift @chars; my $val = ((ord($high) - 65) << 4) + (ord($low) - 65); push @frame, $val; } return @frame; } sub decodeErrorFrame { my @frame = @_; if($frame[12] == 1) { print "MODBUS ERROR\n"; } elsif($frame[12] == 2) { print "INVALID PAYLOAD_LENGTH ERROR\n"; } elsif($frame[12] == 3) { print "MODBUS_SLAVE_NOT_SET ERROR\n"; } elsif($frame[12] == 4) { print "REBOOT_DETECTED ERROR\n"; } elsif($frame[12] == 5) { print "REQUEST_REJECTED ERROR\n"; } elsif($frame[12] == 6) { print "MOSFET_TEMPERATURE ERROR\n"; } return; } sub slurpBinFile { my $fname = shift; # Read in file in binary mode, slurping it into a single scalar. # We have to make sure we use binmode *and* turn on the line termination variable completly # to work around the multiple idiosynchrasies of Perl on Windows open(my $fh, "<", $fname) or croak($ERRNO); local $INPUT_RECORD_SEPARATOR = undef; binmode($fh); my $data = <$fh>; close($fh); return $data; } 1;