App-ArduinoBuilder
view release on metacpan or search on metacpan
lib/App/ArduinoBuilder/Discovery.pm view on Meta::CPAN
package App::ArduinoBuilder::Discovery;
use 5.026;
use strict;
use warnings;
use utf8;
use App::ArduinoBuilder::Config;
use App::ArduinoBuilder::JsonTool;
use File::Spec::Functions;
use List::Util 'all', 'max';
use Log::Any::Simple ':default';
use Time::HiRes 'usleep';
sub _test_command_response {
my ($res, $cmd) = @_;
return 0 if $res->{eventType} ne $cmd;
return 0 if $res->{message} ne 'OK';
return 1;
}
sub _fail_invalid_response {
my ($res, $cmd, $tool) = @_;
return if _test_command_response($res, $cmd);
fatal "Invalid response for ${tool}:\n%s", $res;
}
# Specification of the discovery protocol.
# https://arduino.github.io/arduino-cli/0.32/pluggable-discovery-specification/
sub _run_one_discovery {
my ($toolname, $cmd) = @_;
my $tool = App::ArduinoBuilder::JsonTool->new($cmd);
# TODO: we could test that the tool accept to speak the protocol version 1.
# But, as there are no other versions for know, itâs somehow an overkill.
_fail_invalid_response($tool->send("HELLO 1 ${App::ArduinoBuilder::TOOLS_USER_AGENT}\n"), 'hello', $toolname);
_fail_invalid_response($tool->send("START\n"), 'start', $toolname);
# A small delay here, otherwise the LIST command returns empty results :(
usleep(50000);
my $res = $tool->send("LIST\n");
if (!@{$res->{ports}}) {
# mitigation: if the list was empty initially, letâs try again, maybe the tool has initialized now.
trace "Retrying due to empty list";
$res = $tool->send("LIST\n");
}
_fail_invalid_response($tool->send("QUIT\n"), 'quit', $toolname);
fatal "Invalid pluggable discovery data (eventType ne 'list') for ${toolname}: %s", $res unless $res->{eventType} eq 'list';
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()) {
$discovery_config->set('required.0' => 'builtin:serial-discovery');
$discovery_config->set('required.1' => 'builtin:mdns-discovery');
}
# There is no real documentation of how to use the 'VENDOR_ID:DISCOVERY_NAME'
# references. For now, we just assume that there is a tool with that discovery
# name (we ignore the vendor ID) and we expect a binary of the same name in
# the tool directory (this is the format used by the builtin tools).
# In general, that syntax could be used to automaticaly download the required
# tools if they are not already present.
my @discovered_ports;
for my $k ($discovery_config->keys()) {
if ($k =~ m/^(.*)\.pattern$/) {
push @discovered_ports, _run_one_discovery($1, $discovery_config->get($k));
} elsif ($k =~ m/required(?:\.\d+)?/) {
if ($discovery_config->get($k) =~ m/^([^:]+):(.*)$/) {
# Note: for now weâre ignoring the vendor ID part.
my $tool = $2;
my $tool_key = "runtime.tools.${tool}.path";
if (!$config->exists($tool_key)) {
error "Pluggable discovery references unknown tool: ${tool}";
next;
}
my $tool_dir = $config->get($tool_key);
my $cmd = catfile($tool_dir, $tool);
$cmd .= '.exe' if $^O eq 'MSWin32';
push @discovered_ports, _run_one_discovery($tool, $cmd);
} else {
error "Invalid pluggable discovery reference format: %s", $discovery_config->get($k);
}
} else {
error "Invalid pluggable discovery key: %s => %s", $k, $discovery_config->get($k);
}
}
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.
my $all_max_match = 0;
for my $p (@discovered_ports) {
my $max_match = 0;
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;
( run in 0.885 second using v1.01-cache-2.11-cpan-b16cb0d3907 )