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;