App-ArduinoBuilder

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

 - Implement a workaround to convert command lines to using Windows escaping
   when needed.
 - Improve the logging. Convert it to using Log::Log4perl through Log::Any.
 - Replace the custom CommandRunner with Parallel::TaskExecutor (the same code
   forked to a new separate distribution).
 - Fix a bug with an inverted logic when recursing in the src directories.
 - Use IPC::Run for the JsonTool module.
 - Reduce the default verbosity of the output by not logging entire command
   lines on failures.
 - Add a way to force a port for the system, even if the discovery fails.
 - Improve the discovery and the matching of the upload and monitor ports.

0.07 - 2023-06-05

 - Implement a "monitor" tool to talk to the board being programmed.
 - Improve the matchin of the --port option.
 - Refactor the command runner.

0.06 - 2023-05-02

 - Add support to upload the firmware to the board.
 - Make the command line more powerful (allow to execute more than one command).
 - Display the binary and data size of the compiled program.
 - Use the builtin tools of the Arduino GUI when we can find them.

0.05 - 2023-04-30

 - Bump required Perl to 5.26 for conveniance (indented here-docs).
 - The configuration can be overriden on the command line.
 - Parallelize the compilation
 - Many bug fixes.

Makefile.PL  view on Meta::CPAN

);

sub MY::postamble {
  my ($self) = @_;

  my @postamble;
  push @postamble, ::postamble() if *::postamble{CODE};

  # Solaris has a weird (?) make that does not support our `export` statements.
  push @postamble, <<"MAKE_FRAGMENT" unless $^O eq 'solaris';
distupload: distcheck disttest
\t\$(MAKE) tardist
\tcpan-upload --directory Dist-Setup \$(DISTVNAME).tar\$(SUFFIX)

cover:
\tcover -test

critic: export EXTENDED_TESTING = 1
critic: all
\tperl -Ilib t/001-perlcritic.t 2>&1 | less

rawcritic:
\tperlcritic lib script

lib/App/ArduinoBuilder.pm  view on Meta::CPAN

      'project-dir|project|p=s' => sub { $config->set('builder.project_dir' => $_[1], allow_override => 1) },
      'build-dir|build|b=s' => sub { $config->set('builder.internal.build_dir' => $_[1], allow_override => 1) },
      'log-level|l=s' => sub { set_log_level($_[1]) },
      'config|c=s%' => sub { $config->set($_[1] => $_[2], allow_override => 1) },
      'menu=s%' => sub { $config->set('builder.menu.'.$_[1] => $_[2], allow_override => 1) },
      'skip=s@' => sub { push @skip, split /,/, $_[1] },  # skip this step
      'force=s@' => sub { push @force, split /,/, $_[1] },  # even if it would be skipped by the dependency checker
      'only=s@' => sub { push @only, split /,/, $_[1] },  # run only these steps (skip all others)
      'stack-trace-on-error|stack' => sub { Log::Any::Simple::die_with_stack_trace('long') },
      'parallelize|j=i' => sub { $config->set('builder.parallelize' => $_[1], allow_override => 1) },
      'target-port|port=s' => sub { $config->append('builder.upload.port' => $_[1], ',')},
      'force-port=s' => sub {
        my ($protocol, $address) = split(/:/, $_[1]);
        $config->set('builder.forced_port.protocol' => $protocol);
        $config->set('builder.forced_port.address' => $address);
        $config->set('builder.forced_port.forced' => 1);
      },
    ) or pod2usage(-exitval => 2, -verbose =>0);

  if (my @unknown = grep { !/^(clean|build|discover|upload|monitor)$/ } @ARGV) {
    fatal "Unknown command%s: %s", (@unknown > 1 ? 's' : ''), join(', ', @unknown);
  }

  generate_project_config($config);

  push @ARGV, 'build' unless @ARGV;

  trace "Executing the following command: %s", sub { join(', ', @ARGV) };

  if (grep { /^clean$/ } @ARGV) {
    clean($config);
  }
  if (grep { /^build$/ } @ARGV) {
    build($config, \@skip, \@force, \@only);
  }
  if (grep { /^discover$/ } @ARGV and not grep { /^(upload|monitor)$/ } @ARGV) {
    discover($config);
  }
  if (grep { /^upload$/ } @ARGV) {
    discover($config);
    upload($config);
  }
  if (grep { /^monitor$/ } @ARGV) {
    # The upload process potentially modifies the board port, so we run the
    # discovery here even if it was run on upload.
    discover($config);
    monitor($config);
  }
}

sub generate_project_config {
  my ($config) = @_;

  my $project_dir_is_cwd = 0;
  my $project_dir;

lib/App/ArduinoBuilder.pm  view on Meta::CPAN

  my ($config) = @_;

  if ($config->get('builder.forced_port.forced', default => 0)) {
    info 'Skipping board discovery';
    return;
  }

  info 'Running board discovery...';
  my @ports = App::ArduinoBuilder::Discovery::discover($config);
  # Discovery can be run more than once, as the port of a board can be changed
  # after upload. So we override any previous discovered ports.
  $config->set('builder.internal.ports' => \@ports, allow_override =>1);
  if (@ports) {
    debug 'Found port%s: %s', (@ports > 1 ? 's' : ''), join(', ', map { $_->get('upload.port.label') } @ports);
  } else {
    warning 'No port found.';
  }
}

sub select_port {
  my ($config) = @_;

  if ($config->get('builder.forced_port.forced', default => 0)) {
    # A forced port does not match a found port (as there are none), so we can’t
    # just set selected_port here.
    return $config->filter('builder.forced_port')->prefix('upload.port');
  }

  {
    my $port = $config->get('builder.internal.selected_port', default => undef);
    if (defined $port) {
      debug 'Using previously selected port: %s', $port->get('upload.port.label');
      return $port;
    }
  }

  my @ports = @{$config->get('builder.internal.ports')};
  # TODO: implement an exact match selection and an interactive selection.
  fatal "You must pass the --target-port option to select the upload target" unless $config->exists('builder.upload.port');
  my @targets = map { qr/^$_$/i } split(/\s*,\s*/, $config->get('builder.upload.port'));
  @ports = grep { my $port = $_; any { $port->get('upload.port.lc_label') =~ m/$_/ || $port->get('upload.port.lc_address') =~ m/$_/ } @targets } @ports;
  unless (@ports) {
    fatal "None of the specified ports (%s) can be found, can your target be found by the 'discover' command?", join(', ', @targets);
  }
  warn "More than one found port match with builder.upload.port. Picking the firt one." if @ports > 1;
  my $port = $ports[0];
  info 'Using the first match port from the configuration: %s', $port->get('upload.port.address');

  $config->set('builder.internal.selected_port' => $port);
  return $port;
}

sub upload {
  my ($config) = @_;

  info 'Uploading binary to the board...';

  my $port = select_port($config);
  my $protocol = $port->get('upload.port.protocol');
  my $tool = $config->get("upload.tool.${protocol}", default => $config->get('upload.tool.default', default => $config->get('upload.tool')));
  my $tool_config = $config->filter("tools.${tool}");

  # TODO: add a way to set the verbose mode, in which case the upload.params.verbose
  # property should be copied, instead of upload.params.quiet.
  # Reference: https://arduino.github.io/arduino-cli/0.32/platform-specification/#verbose-parameter
  $tool_config->set('upload.verbose' => $tool_config->get('upload.params.quiet'), allow_override => 1);

  my $upload_config = $config->filter("upload.${protocol}")->prefix('upload');
  $upload_config->merge($tool_config);
  $upload_config->merge($port);
  # Note: $config is in the recursive base in $upload_config.

  # TODO: Before executing the command, some boards require that we manually
  # reset them through their Serial port.
  # See the code: https://github.com/arduino/arduino-cli/blob/ad9ddb882016c2af10e0db3785a46122bc9cfb1f/commands/upload/upload.go#L370
  # And the doc: https://arduino.github.io/arduino-cli/0.32/platform-specification/#1200-bps-bootloader-reset

  my $cmd = $upload_config->get('upload.pattern');
  debug "Upload configuration:\n%s", sub { $upload_config->dump('  ') };
  default_executor()->run_now(sub {
        close STDIN;
        execute_cmd($cmd);
      });

  info 'Success!';

}

sub monitor {

lib/App/ArduinoBuilder/Builder.pm  view on Meta::CPAN

  $this->_run_recipe_pattern('size', capture_output => \$output, is_size => 1);
  # TODO: There is a variant using the 'advanced_size' recipe that can be
  # implemented for more complex scenario and that we are not yet supporting.

  my $bin_size_re = $this->{config}->get('recipe.size.regex', default => undef);
  if ($bin_size_re) {
    my $bin_size = 0;
    while ($output =~ m/${bin_size_re}/mg) {
      $bin_size += $1;
    }
    my $max_bin_size = $this->{config}->get('upload.maximum_size', default => undef);
    if ($max_bin_size) {
      info '  Sketch uses %d bytes (%d%%) of program space. Maximum is %d bytes.', $bin_size, ($bin_size * 100 / $max_bin_size), $max_bin_size;
      fatal 'Sketch is too large' if $bin_size > $max_bin_size;
    } else {
      info '  Sketch uses %d bytes of program space.', $bin_size;
    }
  }

  my $data_size_re =$this->{config}->get('recipe.size.regex.data', default => undef);
  if ($data_size_re) {
    my $data_size = 0;
    while ($output =~ m/${data_size_re}/mg) {
      $data_size += $1;
    }
    my $max_data_size = $this->{config}->get('upload.maximum_data_size', default => undef);
    if ($max_data_size) {
      info '  Global variables use %d bytes (%d%%) of dynamic memory, leaving %d bytes for local variables. Maximum is %d bytes.', $data_size, ($data_size * 100 / $max_data_size), ($max_data_size - $data_size), $max_data_size;
      fatal 'Too much memory used' if $data_size > $max_data_size;
    } else {
      info '  Global variables use %d bytes of dynamic memory.', $data_size;
    }
  }

  return;
}

lib/App/ArduinoBuilder/Discovery.pm  view on Meta::CPAN

  fatal "Pluggable discovery returned an error for ${toolname}: %s", $res if $res->{error} && $res->{error} eq 'true';
  debug "Pluggable discovery for ${toolname} found: %s", $res->{ports};
  return @{$res->{ports}};
}

# See: https://arduino.github.io/arduino-cli/0.32/platform-specification/#properties-from-pluggable-discovery
sub _port_to_config {
  my ($config, $port) = @_;

  my $port_config = App::ArduinoBuilder::Config->new(base => $config);
  $port_config->parse_perl($port, prefix => 'upload.port');
  if ($port_config->exists('upload.port.address')) {
    $port_config->set('serial.port' => $port_config->get('upload.port.address'));
  }
  if ($port_config->get('upload.port.protocol', default => '') eq 'serial') {
    $port_config->set('serial.port.file' => $port_config->get('upload.port.label'));
  }

  # Case folded versions, later used to compare to the content of the --port
  # option.
  $port_config->set('upload.port.lc_label', lc($port_config->get('upload.port.label')));
  $port_config->set('upload.port.lc_address', lc($port_config->get('upload.port.address')));

  return $port_config;
}

# For _some_ documentation, see:
# https://arduino.github.io/arduino-cli/0.32/platform-specification/#pluggable-discovery
sub discover {
  my ($config) = @_;
  my $discovery_config = $config->filter('pluggable_discovery');
  if ($discovery_config->filter('required')->empty()) {

lib/App/ArduinoBuilder/Discovery.pm  view on Meta::CPAN


  return () unless @discovered_ports;

  # Something that we don’t implement is that the discovery could be used to
  # automatically detect the board being used, as well as some of its "menu"
  # properties.
  # However, the whole thing is very buggy. For example the Feather 2040 board
  # will have a match on the vid property but not the pid property, so we accept
  # partial match (theoretically we should accept full matches).

  # Some boards have the upload ports properties defined without the
  # upload_ports prefix (in addition to also having them with the prefix), we
  # are ignoring that.
  my $defined_ports = $config->filter('upload_port');
  my @property_sets_keys = $defined_ports->top_level_keys();
  my @property_sets;
  if (all { m/^\d+$/ } @property_sets_keys) {
    @property_sets = map { { $defined_ports->filter($_)->get_hash() } } @property_sets_keys;
  } else {
    @property_sets = { $defined_ports->get_hash() };
  }
  
  # We compute a "match strength" for all discovered ports, corresponding to how
  # many properties (from a single defined set) the port matches.

lib/App/ArduinoBuilder/Discovery.pm  view on Meta::CPAN

    for my $s (@property_sets) {
      my $match = 0;
      while (my ($k, $v) = each %{$s}) {
        if (fc($p->{properties}{$k} // '') eq fc($v)) {
          $match++;
        }
      }
      $max_match = max($max_match, $match);
    }
    $all_max_match = max($all_max_match, $max_match);
    $p->{upload_port_match_strength} = $max_match;
    trace "Port %s: match strength == %d", $p->{label}, $max_match;
  }

  # Now, we keep all the found ports that have the highest 
  @discovered_ports = grep { $_->{upload_port_match_strength} == $all_max_match } @discovered_ports;

  return map { _port_to_config($config, $_) } @discovered_ports;
}

1;

lib/App/ArduinoBuilder/Monitor.pm  view on Meta::CPAN


use App::ArduinoBuilder::JsonTool;
use File::Spec::Functions 'catfile';
use IO::Select;
use IO::Socket::INET;
use Log::Any::Simple ':default';

sub monitor {
  my ($config, $port) = @_;

  my $protocol = $port->get('upload.port.protocol');
  my $board_port = $port->get('upload.port.address');

  # Some documentation for these properties is at:
  # https://arduino.github.io/arduino-cli/0.32/platform-specification/#pluggable-monitor
  my $cmd;
  if ($config->exists("pluggable_monitor.required.${protocol}")) {
    my $tooldef = $config->get("pluggable_monitor.required.${protocol}");
    if ($tooldef =~ m/^([^:]+):(.*)$/) {
      # Note: for now we’re ignoring the vendor ID part.
      my $tool = $2;
      my $tool_key = "runtime.tools.${tool}.path";

script/arduino_builder  view on Meta::CPAN

Build the project and all its dependencies (Arduino core and libraries).

The command tries to not rebuild unecessary parts of the binary and of its
dependencies if their sources have not changed. In some circumstance we may not
detect correctly all dependencies so it may be useful to run the B<clean>
command (in particular if you have deleted a source file).

=item B<discover>

Run the board discovery to detect your board. This command is implied by the
B<upload> and/or B<monitor> commands.

=item B<upload>

Upload the compiled binary to your board. If the B<build> command is not
executed, the binary must already exist and no new version will be compiled
before the upload.

Note that the end of the compilation phase will report whether your binary can
hold in your board memory (and will abort the program if not) but the B<upload>
command itself does not perform such a check.

=item B<monitor>

Open a connection to the board that allows to interract with a running program.
This is usually, but not necessarily, done through a serial port.

=back

=head1 OPTIONS

script/arduino_builder  view on Meta::CPAN

=item C<builder.source.is_recursive>

=item C<builder.menu.XXX>

=item C<builder.library.XXX>

=item C<builder.parallelize>*

=item C<builder.config.append.XXX>

=item C<builder.upload.port>*

The port to use to upload and/or monitor the board. The passed value should be
the label of a port found by the B<monitor> command (typically the name of the
serial port to use). You can run C<arduino_builder monitor -l debug> to see
which ports are found by the command.

You can also use a comma separated list for this configuration value in case
your board can appear under different ports.

The values passed in this list are treated as regex, so you can specify things
like C<com\d+> to match any COM ports (on Windows).

script/arduino_builder  view on Meta::CPAN


=item Automatic board detection

There are some mechanism in the Arduino software to identify a connected board.
For now we require that the user specifies the port used to communicate with the
board.

=item Upload verification

The verification step, documented
L<here|https://arduino.github.io/arduino-cli/0.32/platform-specification/#upload-verification>,
is not yet implemented.

Similarly uploads using external programmers and bootloader burning are not
handled.

=item 1200bps bootloader reset

If the reset to bootloader is not performed by the upload tools used, Arduino
Builder cannot yet perform this reset itself (documented
L<here|https://arduino.github.io/arduino-cli/0.32/platform-specification/#1200-bps-bootloader-reset>).

=back

=head1 AUTHOR

This program has been written by L<Mathias Kende|mailto:mathias@cpan.org>.

=head1 COPYRIGHT AND LICENSE



( run in 2.009 seconds using v1.01-cache-2.11-cpan-b16cb0d3907 )