Skip to content

Commit 0ec66b4

Browse files
committed
Generate commands for boards
1 parent 43fe5d5 commit 0ec66b4

File tree

7 files changed

+1077
-0
lines changed

7 files changed

+1077
-0
lines changed

boards.go

Lines changed: 158 additions & 0 deletions
Large diffs are not rendered by default.

boards/boards.go

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
/*
2+
* This file is part of arduino-create-agent.
3+
*
4+
* arduino-create-agent is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17+
*
18+
* As a special exception, you may use this file as part of a free software
19+
* library without restriction. Specifically, if other files instantiate
20+
* templates or use macros or inline functions from this file, or you compile
21+
* this file and link it with other files to produce an executable, this
22+
* file does not by itself cause the resulting executable to be covered by
23+
* the GNU General Public License. This exception does not however
24+
* invalidate any other reasons why the executable file might be covered by
25+
* the GNU General Public License.
26+
*
27+
* Copyright 2017 BCMI LABS SA (http://www.arduino.cc/)
28+
*/
29+
package boards
30+
31+
import (
32+
"encoding/json"
33+
"io/ioutil"
34+
"path/filepath"
35+
"regexp"
36+
"strings"
37+
38+
"github.com/bcmi-labs/arduino-modules/fs"
39+
properties "github.com/dmotylev/goproperties"
40+
"github.com/juju/errors"
41+
)
42+
43+
// Board is a physical board belonging to a certain architecture in a package.
44+
// The most obvious package is arduino, which contains architectures avr, sam
45+
// and samd
46+
// It can contain multiple variants, but at least one that it's the default
47+
type Board struct {
48+
ID string `json:"id"`
49+
Name string `json:"name"`
50+
Vid []string `json:"vid"`
51+
Pid []string `json:"pid"`
52+
Package string `json:"package"`
53+
Architecture string `json:"architecture"`
54+
Fqbn string `json:"fqbn"`
55+
Variants map[string]*Variant `json:"variants"`
56+
DefaultVariant string `json:"default_variant"`
57+
}
58+
59+
// Boards is a map of Boards
60+
type Boards map[string]*Board
61+
62+
// Variant is a board that differ slightly from the others with the same model
63+
type Variant struct {
64+
Name string `json:"name"`
65+
Fqbn string `json:"fqbn"`
66+
Actions Actions `json:"actions"`
67+
}
68+
69+
// Action is a command that a tool can execute on a board
70+
type Action struct {
71+
Tool string `json:"tool"`
72+
ToolVersion string `json:"tool_version"`
73+
Ext string `json:"ext"`
74+
Command string `json:"command"`
75+
Params options `json:"params"`
76+
Options options `json:"options"`
77+
Files []fs.File `json:"files,omitempty"`
78+
}
79+
80+
// Actions is a map of Actions
81+
type Actions map[string]*Action
82+
83+
// Lister contains methods to retrieve a slice of boards
84+
type Lister interface {
85+
List()
86+
}
87+
88+
// Retriever contains methods to retrieve a single board
89+
type Retriever interface {
90+
ById()
91+
ByVidPid()
92+
}
93+
94+
// Client parses the boards.txt, platform.txt and platform.json files to build a
95+
// representation of the boards
96+
type Client struct {
97+
}
98+
99+
// New returns a new client by parsing the files contained in the given folder
100+
func New() *Client {
101+
return nil
102+
}
103+
104+
// ByID returns the board with the given id, or nil if it doesn't exists
105+
func (list Boards) ByID(id string) *Board {
106+
return list[id]
107+
}
108+
109+
// ByVidPid return the board with the correct combination of vid and pid, or nil
110+
// if it doesn't exists
111+
func (list Boards) ByVidPid(vid, pid string) *Board {
112+
for _, board := range list {
113+
if in(vid, board.Vid) && in(pid, board.Pid) {
114+
return board
115+
}
116+
}
117+
118+
return nil
119+
}
120+
121+
// New returns a new board with the given id
122+
func (list Boards) New(id string) *Board {
123+
board := &Board{
124+
Fqbn: id,
125+
Vid: []string{},
126+
Pid: []string{},
127+
DefaultVariant: "default",
128+
Variants: map[string]*Variant{
129+
"default": &Variant{
130+
Fqbn: id,
131+
Actions: Actions{},
132+
},
133+
},
134+
}
135+
list[id] = board
136+
return board
137+
}
138+
139+
// ParseBoardsTXT parses a bords.txt adding the boards to itself
140+
func (list Boards) ParseBoardsTXT(path string) error {
141+
arch := filepath.Base(filepath.Dir(path))
142+
pack := filepath.Base(filepath.Dir(filepath.Dir(path)))
143+
144+
props, err := properties.Load(path)
145+
if err != nil {
146+
return errors.Annotatef(err, "parse properties of %s", path)
147+
}
148+
149+
menu := findMenu(props)
150+
151+
temp := Boards{}
152+
153+
// discover which boards are present
154+
for key, value := range props {
155+
parts := strings.Split(key, ".")
156+
157+
// Discard menus
158+
if parts[0] == "menu" {
159+
continue
160+
}
161+
162+
// The first part is always the id
163+
id := parts[0]
164+
if id == "" {
165+
continue
166+
}
167+
168+
fqbn := pack + ":" + arch + ":" + id
169+
170+
// Get or create the board
171+
var board *Board
172+
if board = temp.ByID(fqbn); board == nil {
173+
board = temp.New(fqbn)
174+
board.ID = id
175+
board.Package = pack
176+
board.Architecture = arch
177+
}
178+
179+
if len(parts) < 2 {
180+
continue
181+
}
182+
183+
// Populate fields
184+
populate(parts, board, menu, value)
185+
}
186+
187+
// Upgrade the variants with the common options
188+
for fqbn, board := range temp {
189+
defVariant := board.Variants["default"]
190+
delete(board.Variants, "default")
191+
if len(board.Variants) == 0 {
192+
board.Variants["default"] = defVariant
193+
} else {
194+
for _, variant := range board.Variants {
195+
for name, action := range defVariant.Actions {
196+
populateAction(variant, name, "tool", action.Tool)
197+
for opt, value := range action.Options {
198+
populateAction(variant, name, opt, value)
199+
}
200+
}
201+
}
202+
}
203+
204+
// Set the default variant
205+
normalize(board)
206+
207+
// Append the board
208+
list[fqbn] = board
209+
}
210+
211+
return nil
212+
}
213+
214+
// Find parses all subfolders of a location, computing the results
215+
func Find(location string) (Boards, error) {
216+
folders, err := ioutil.ReadDir(location)
217+
if err != nil {
218+
return nil, errors.Annotatef(err, "while reading the contents of folder %s", location)
219+
}
220+
221+
list := Boards{}
222+
plats := Platforms{}
223+
224+
for _, folder := range folders {
225+
if !folder.IsDir() {
226+
continue
227+
}
228+
files, err := ioutil.ReadDir(filepath.Join(location, folder.Name()))
229+
if err != nil {
230+
return nil, errors.Annotatef(err, "while reading the contents of folder %s", folder)
231+
}
232+
233+
for _, file := range files {
234+
path := filepath.Join(location, folder.Name(), file.Name())
235+
236+
if file.IsDir() {
237+
// Parse boards.txt
238+
list.ParseBoardsTXT(filepath.Join(path, "boards.txt"))
239+
240+
// Parse platform.json
241+
f, _ := ioutil.ReadFile(filepath.Join(path, "platform.json"))
242+
var plat Platform
243+
json.Unmarshal(f, &plat)
244+
245+
// Parse platform.txt
246+
plat.ParsePlatformTXT(filepath.Join(path, "platform.txt"))
247+
plats[plat.Architecture+":"+plat.Packager] = &plat
248+
}
249+
}
250+
}
251+
252+
Compute(list, plats)
253+
254+
return list, nil
255+
}
256+
257+
// Compute fills the fields of the boards that need to be calculated from the platform info, such as the fully expanded commandline or the tools versions
258+
func Compute(brds Boards, plats Platforms) {
259+
extRe := regexp.MustCompile(`{build.project_name}(\.bin|\.hex|\.bin)`)
260+
261+
for _, board := range brds {
262+
for _, variant := range board.Variants {
263+
for name, action := range variant.Actions {
264+
tool := plats.Tool(board.Package, board.Architecture, action.Tool)
265+
if tool == nil {
266+
continue
267+
}
268+
269+
if tool.Patterns[name] == nil {
270+
continue
271+
}
272+
273+
action.ToolVersion = tool.Version
274+
275+
// Find the expected extension
276+
action.Command = expand(tool, variant, name)
277+
278+
// Arduino Zero Debug port is strange, so it's handled as a special case
279+
if variant.Fqbn == "arduino:samd:arduino_zero_edbg" && name == "upload" {
280+
action.Command = `"{runtime.tools.openocd.path}/bin/openocd" {upload.verbose} -s "{runtime.tools.openocd.path}/share/openocd/scripts/" -f "{build.path}/arduino_zero.cfg" -c "telnet_port disabled; program {build.path}/{build.project_name}.bin verify reset 0x00002000; shutdown"`
281+
}
282+
283+
// Arduino M0 Debug port is strange, so it's handled as a special case
284+
if variant.Fqbn == "arduino:samd:mzero_pro_bl_dbg" && name == "upload" {
285+
action.Tool = "openocd"
286+
action.Command = `"{runtime.tools.openocd.path}/bin/openocd" {upload.verbose} -s "{runtime.tools.openocd.path}/share/openocd/scripts/" -f "{build.path}/arduino_zero.cfg" -c "telnet_port disabled; program {build.path}/{build.project_name}.bin verify reset 0x00004000; shutdown"`
287+
}
288+
289+
match := extRe.FindString(action.Command)
290+
action.Ext = filepath.Ext(match)
291+
for param, value := range tool.Patterns[name].Params {
292+
action.Params[param] = value
293+
}
294+
295+
// Find the files to include
296+
findFiles(action, plats[board.Architecture+":"+board.Package])
297+
}
298+
}
299+
}
300+
301+
}
302+
303+
func findFiles(action *Action, plat *Platform) {
304+
action.Files = []fs.File{}
305+
re := regexp.MustCompile(`{runtime.platform.path}[\w\/\.\-]*`)
306+
307+
action.Command = re.ReplaceAllStringFunc(action.Command, func(file string) string {
308+
filename := strings.Replace(file, `{runtime.platform.path}`, plat.Path, -1)
309+
data, err := ioutil.ReadFile(filename)
310+
if err != nil {
311+
return file
312+
}
313+
314+
filename = filepath.Base(filename)
315+
316+
action.Files = append(action.Files, fs.File{Name: filename, Data: data})
317+
318+
return "{runtime.platform.path}/" + filename
319+
})
320+
321+
}
322+
323+
func expand(tool *Tool, variant *Variant, pattern string) string {
324+
re := regexp.MustCompile(`{([\S{}]*?)}`)
325+
326+
if tool.Patterns[pattern] == nil {
327+
return ""
328+
}
329+
330+
command := tool.Patterns[pattern].Command
331+
oldCommand := ""
332+
333+
for command != oldCommand {
334+
oldCommand = command
335+
command = re.ReplaceAllStringFunc(command, replace(tool, variant, pattern))
336+
}
337+
return command
338+
}
339+
340+
func replace(tool *Tool, variant *Variant, pattern string) func(string) string {
341+
return func(value string) string {
342+
// Remove parenthesis
343+
key := strings.Replace(value, "{", "", 1)
344+
key = strings.Replace(key, "}", "", 1)
345+
346+
// Update path
347+
if key == "path" {
348+
return tool.Path
349+
}
350+
351+
// Search in tool options
352+
if prop, ok := tool.Options[key]; ok {
353+
return prop
354+
}
355+
356+
for name, action := range variant.Actions {
357+
actionkey := strings.Replace(key, name+".", "", 1)
358+
if prop, ok := action.Options[actionkey]; ok {
359+
return prop
360+
}
361+
}
362+
363+
return value
364+
}
365+
}

0 commit comments

Comments
 (0)