diff --git a/README.md b/README.md index 933f17b..021bd1b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,48 @@ A Go client library to interact with your MakerBot printer via the network. Documentation, examples and more at [GoDoc](https://godoc.org/github.com/tjhorner/makerbot-rpc). -**This is currently in beta and does not support many functions that MakerBot printers make available.** ~~Most notably, it does not yet support sending print files.~~ Also, some responses are not yet modelled. +Since this is currently mid-development, things will probably change _very, very often._ No stable API is guaranteed until the first stable version of this project. + +## Example + +```shell +go get github.com/tjhorner/makerbot-rpc +``` + +```golang +// WARNING: This example may fail to work at any time. +// This library is still in development. This example +// is only provided to give a sense of what the library can do. +// Errors are ignored for brevity. + +client := makerbot.Client{} +defer client.Close() + +// React when the printer's state changes +client.HandleStateChange(func(olsd new, *makerbot.PrinterMetadata) { + if new.CurrentProcess == nil { + return + } + + // Log the thing the printer is currently doing + log.Printf("Current process: %s, %v%% done\n", new.CurrentProcess.Name, new.CurrentProcess.Progress) +}) + +// Make initial TCP connection w/ printer +client.ConnectLocal("192.168.1.2", "9999") // most MakerBot printers listen on 9999 + +log.Printf("Connected to MakerBot printer: %s\n", client.Printer.MachineName) + +// Authenticate with Thingiverse +client.AuthenticateWithThingiverse("my_access_token", "my_username") + +log.Println("Queuing file for printing...") + +// Print a file named `print.makerbot` in the same directory +client.PrintFile("print.makerbot") + +log.Println("Done! Bye bye.") +``` ## Features and TODO @@ -29,4 +70,8 @@ Documentation, examples and more at [GoDoc](https://godoc.org/github.com/tjhorne - [ ] Get machine config (low priority; isn't very useful) - [ ] Write tests (will need to make a mock MakerBot RPC server) - [ ] Write examples -- [ ] Better errors \ No newline at end of file +- [ ] Better errors + +## License + +TBD, but probably MIT later. \ No newline at end of file diff --git a/client.go b/client.go index b115da7..7d10288 100644 --- a/client.go +++ b/client.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/tjhorner/makerbot-rpc/printfile" "github.com/tjhorner/makerbot-rpc/reflector" "github.com/tjhorner/makerbot-rpc/jsonrpc" @@ -127,7 +128,7 @@ func (c *Client) handshake() error { go c.endCameraStream() } - metadata := parseCameraFrameMetadata(c.rpc.GetRawData(16)) + metadata := unpackCameraFrameMetadata(c.rpc.GetRawData(16)) data := c.rpc.GetRawData(int(metadata.FileSize)) @@ -336,8 +337,6 @@ type rpcPutTermParams struct { Length int `json:"length"` } -// TODO this could probably be done better with an io reader - // Print will synchronously print a .makerbot file with the provided // `filename` (can be anything). `data` should be the contents of the // .makerbot file. The function returns when it is done sending the entire @@ -393,6 +392,8 @@ func (c *Client) Print(filename string, data []byte) error { // `filename` and automatically reading from it then // feeding it to Print. func (c *Client) PrintFile(filename string) error { + // TODO support streaming files in so we don't need to + // load the entire thing into memory data, err := ioutil.ReadFile(filename) if err != nil { return err @@ -400,3 +401,19 @@ func (c *Client) PrintFile(filename string) error { return c.Print(filepath.Base(filename), data) } + +// PrintFileVerify is exactly like PrintFile except it errors +// if the print file is not designed for the printer that this +// Client is connected to. +func (c *Client) PrintFileVerify(filename string) error { + metadata, err := printfile.GetFileMetadata(filename) + if err != nil { + return err + } + + if metadata.BotType != c.Printer.BotType { + return errors.New("print file is not designed for this MakerBot printer") + } + + return c.PrintFile(filename) +} diff --git a/jsonrpc/client.go b/jsonrpc/client.go index f41e9ed..07760ef 100644 --- a/jsonrpc/client.go +++ b/jsonrpc/client.go @@ -36,7 +36,9 @@ type rpcError struct { } `json:"data"` } -func (e *rpcError) Error() string { return fmt.Sprintf("rpc error: %s: %s", e.Data.Name, e.Message) } +func (e *rpcError) Error() string { + return fmt.Sprintf("rpc error (remote): %s: %s", e.Data.Name, e.Message) +} type rpcResponse struct { ID *string `json:"id"` @@ -81,7 +83,6 @@ func (c *Client) Connect() error { json.Unmarshal(j, &req) if sub, ok := c.subs[req.Method]; ok { - go sub(req.Params) } } else if resp.ID != nil { diff --git a/jsonrpc/jsonreader.go b/jsonrpc/jsonreader.go index 7735c0e..a316584 100644 --- a/jsonrpc/jsonreader.go +++ b/jsonrpc/jsonreader.go @@ -112,7 +112,6 @@ func (r *JSONReader) transition(b byte) { // FeedByte feeds the JSONReader a single byte func (r *JSONReader) FeedByte(b byte) { r.buffer = append(r.buffer, b) - r.transition(b) } @@ -127,6 +126,7 @@ func (r *JSONReader) FeedBytes(bs []byte) { // `length` is reached. The captured data is returned as an // array of bytes. func (r *JSONReader) GetRawData(length int) []byte { + // TODO should we mutex lock reading from TCP socket? ch := make(chan []byte) r.rawCh = &ch r.state = state4 diff --git a/jsonrpc/jsonreader_test.go b/jsonrpc/jsonreader_test.go new file mode 100644 index 0000000..7c42ecc --- /dev/null +++ b/jsonrpc/jsonreader_test.go @@ -0,0 +1,106 @@ +package jsonrpc_test + +import ( + "crypto/rand" + "errors" + "reflect" + "sync" + "testing" + + "github.com/tjhorner/makerbot-rpc/jsonrpc" +) + +func TestJSONReader(t *testing.T) { + testJson := `[ + { + "_id": "5cc4918e5ffc07a556e7a467", + "index": 0, + "guid": "ccaaa030-db12-4e41-901a-71db5eddd1a5", + "isActive": false, + "balance": "$2,952.59", + "picture": "http://placehold.it/32x32", + "age": 36, + "eyeColor": "green", + "name": { + "first": "Rush", + "last": "Best" + }, + "company": "AQUASURE", + "email": "rush.best@aquasure.us", + "phone": "+1 (963) 588-3601", + "address": "438 Murdock Court, Winchester, New Mexico, 1957", + "about": "Proident exercitation et Lorem est. Deserunt occaecat culpa aute fugiat. Ea adipisicing culpa veniam est et qui anim ut sit tempor ut laboris est dolore. Cillum nulla incididunt eiusmod et cillum sit incididunt ullamco incididunt ex quis deserunt excepteur. Laboris anim esse duis duis Lorem in elit adipisicing laboris sit cupidatat esse tempor incididunt. Cillum elit laborum voluptate sint commodo exercitation laboris adipisicing non ipsum.", + "registered": "Monday, August 1, 2016 11:54 PM", + "latitude": "-2.77938", + "longitude": "-177.303158", + "tags": [ + "fugiat", + "elit", + "qui", + "in", + "officia" + ], + "range": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "friends": [ + { + "id": 0, + "name": "Dorothea Maxwell" + }, + { + "id": 1, + "name": "Fry Blair" + }, + { + "id": 2, + "name": "Jessie Ware" + } + ], + "greeting": "Hello, Rush! You have 5 unread messages.", + "favoriteFruit": "banana" + } + ]` + + var wg sync.WaitGroup + + wg.Add(1) + reader := jsonrpc.NewJSONReader(func(data []byte) error { + if string(data) != testJson { + t.Errorf("JSONReader's json was incorrect, got: %s, want: [long test string]\n", string(data)) + } + + wg.Done() + return nil + }) + + reader.FeedBytes([]byte(testJson)) + + wg.Wait() +} + +func TestJSONReader_GetRawData(t *testing.T) { + randBytes := make([]byte, 32) // 32 random bytes + rand.Read(randBytes) + + reader := jsonrpc.NewJSONReader(func(d []byte) error { return errors.New("") }) + + go func() { + reader.FeedBytes(randBytes) + }() + + result := reader.GetRawData(32) + + if !reflect.DeepEqual(randBytes, result) { + t.Errorf("JSONReader did not return randBytes\n") + } +} diff --git a/printfile/printfile.go b/printfile/printfile.go new file mode 100644 index 0000000..3ade42b --- /dev/null +++ b/printfile/printfile.go @@ -0,0 +1,216 @@ +/* +Package printfile is a library for parsing .makerbot print files. +*/ +package printfile + +import ( + "archive/zip" + "encoding/json" + "errors" + "image/png" + "io/ioutil" + "regexp" + "strconv" + "strings" +) + +var thumbnailRegex = regexp.MustCompile(`thumbnail_([0-9]+)x([0-9]+)\.png`) + +// Parse parses everything (metadata, thumbnails, toolpath) from a +// .makerbot file given a zip.ReadCloser +func Parse(r *zip.ReadCloser) (*MakerBotFile, error) { + metadata, err := parseMetadata(r) + if err != nil { + return nil, err + } + + toolpath, err := parseToolpath(r) + if err != nil { + return nil, err + } + + thumbnails := parseThumbnails(r) + if err != nil { + return nil, err + } + + return &MakerBotFile{ + ThumbnailSizes: thumbnails, + Metadata: metadata, + Toolpath: toolpath, + }, nil +} + +// GetMetadata grabs just the Metadata of a .makerbot file given a zip.ReadCloser +func GetMetadata(r *zip.ReadCloser) (*Metadata, error) { + return parseMetadata(r) +} + +// GetToolpath grabs just the Toolpath of a .makerbot file given a zip.ReadCloser +func GetToolpath(r *zip.ReadCloser) (*Toolpath, error) { + return parseToolpath(r) +} + +// GetThumbnails grabs just the []Thumbnail of a .makerbot file given a zip.ReadCloser +func GetThumbnails(r *zip.ReadCloser) *[]Thumbnail { + return parseThumbnails(r) +} + +// ParseFile parses everything (metadata, thumbnails, toolpath) from a +// .makerbot file given a filepath +func ParseFile(filename string) (*MakerBotFile, error) { + rc, err := zip.OpenReader(filename) + if err != nil { + return nil, err + } + + return Parse(rc) +} + +// GetFileMetadata grabs just the Metadata of a .makerbot file given a filepath +func GetFileMetadata(filename string) (*Metadata, error) { + rc, err := zip.OpenReader(filename) + if err != nil { + return nil, err + } + + return parseMetadata(rc) +} + +// GetFileToolpath grabs just the Toolpath of a .makerbot file given a filepath +func GetFileToolpath(filename string) (*Toolpath, error) { + rc, err := zip.OpenReader(filename) + if err != nil { + return nil, err + } + + return parseToolpath(rc) +} + +// GetFileThumbnails grabs just the []Thumbnail of a .makerbot file given a filepath +func GetFileThumbnails(filename string) (*[]Thumbnail, error) { + rc, err := zip.OpenReader(filename) + if err != nil { + return nil, err + } + + return parseThumbnails(rc), nil +} + +func parseMetadata(zr *zip.ReadCloser) (*Metadata, error) { + var meta Metadata + found := false + + for _, f := range zr.File { + if f.Name != "meta.json" { + continue + } + + found = true + + fc, err := f.Open() + if err != nil { + return nil, err + } + + dec := json.NewDecoder(fc) + dec.Decode(&meta) + break + } + + if !found { + return nil, errors.New("parseMetadata: malformed .makerbot file; does not have metadata") + } + + return &meta, nil +} + +func parseToolpath(zr *zip.ReadCloser) (*Toolpath, error) { + var tp Toolpath + found := false + + for _, f := range zr.File { + if f.Name != "print.jsontoolpath" { + continue + } + + found = true + + fc, err := f.Open() + if err != nil { + return nil, err + } + + dec := json.NewDecoder(fc) + dec.Decode(&tp) + break + } + + if !found { + return nil, errors.New("parseToolpath: malformed .makerbot file; does not have toolpath") + } + + return &tp, nil +} + +func parseThumbnail(f *zip.File) (*Thumbnail, error) { + fc, err := f.Open() + if err != nil { + // Errors are negligible here + return nil, err + } + + matches := thumbnailRegex.FindStringSubmatch(f.Name) + if len(matches) < 3 { + return nil, err + } + + width, err := strconv.Atoi(matches[1]) + if err != nil { + return nil, err + } + + height, err := strconv.Atoi(matches[2]) + if err != nil { + return nil, err + } + + img, err := png.DecodeConfig(fc) + if err != nil { + return nil, err + } + + data, err := ioutil.ReadAll(fc) + if err != nil { + return nil, err + } + + thumb := Thumbnail{ + TargetHeight: height, + TargetWidth: width, + ActualHeight: img.Height, + ActualWidth: img.Width, + Data: data, + } + + return &thumb, nil +} + +func parseThumbnails(zr *zip.ReadCloser) *[]Thumbnail { + var thumbnails []Thumbnail + + for _, f := range zr.File { + if !strings.HasPrefix(f.Name, "thumbnail_") || !strings.HasSuffix(f.Name, ".png") { + continue + } + + thumb, err := parseThumbnail(f) + if err != nil { + continue + } + + thumbnails = append(thumbnails, *thumb) + } + + return &thumbnails +} diff --git a/printfile/printfile_test.go b/printfile/printfile_test.go new file mode 100644 index 0000000..a67ee8b --- /dev/null +++ b/printfile/printfile_test.go @@ -0,0 +1,69 @@ +package printfile_test + +import ( + "testing" + + "github.com/tjhorner/makerbot-rpc/printfile" +) + +const file = "test/box.makerbot" + +const expectedUUID = "3c997805-959b-414f-85b5-c45872a11b78" +const expectedThumbnails = 3 +const expectedToolpathCommands = 9158 + +func TestParseFile(t *testing.T) { + file, err := printfile.ParseFile(file) + if err != nil { + t.Error(err) + } + + if file.Metadata.UUID != expectedUUID { + t.Errorf("metadata UUID is wrong; wanted: %s, got: %s\n", expectedUUID, file.Metadata.UUID) + } + + if len(*file.ThumbnailSizes) != expectedThumbnails { + t.Errorf("print thumbnails size is wrong; wanted: %d, got: %d\n", expectedThumbnails, len(*file.ThumbnailSizes)) + } + + if len(*file.Toolpath) != expectedToolpathCommands { + t.Errorf("toolpath commands size is wrong; wanted: %d, got: %d\n", expectedToolpathCommands, len(*file.Toolpath)) + } + + if len(*file.Toolpath) != file.Metadata.TotalCommands { + t.Errorf("toolpath commands size is inconsistent; wanted: %d, got: %d\n", file.Metadata.TotalCommands, len(*file.Toolpath)) + } +} + +func TestGetFileMetadata(t *testing.T) { + metadata, err := printfile.GetFileMetadata(file) + if err != nil { + t.Error(err) + } + + if metadata.UUID != expectedUUID { + t.Errorf("metadata UUID is wrong; wanted: %s, got: %s\n", expectedUUID, metadata.UUID) + } +} + +func TestGetFileThumbnails(t *testing.T) { + thumbs, err := printfile.GetFileThumbnails(file) + if err != nil { + t.Error(err) + } + + if len(*thumbs) != expectedThumbnails { + t.Errorf("print thumbnails size is wrong; wanted: %d, got: %d\n", expectedThumbnails, len(*thumbs)) + } +} + +func TestGetFileToolpath(t *testing.T) { + tp, err := printfile.GetFileToolpath(file) + if err != nil { + t.Error(err) + } + + if len(*tp) != expectedToolpathCommands { + t.Errorf("toolpath commands size is wrong; wanted: %d, got: %d\n", expectedToolpathCommands, len(*tp)) + } +} diff --git a/printfile/test/box.makerbot b/printfile/test/box.makerbot new file mode 100644 index 0000000..3f729ec Binary files /dev/null and b/printfile/test/box.makerbot differ diff --git a/printfile/types.go b/printfile/types.go new file mode 100644 index 0000000..8f610ba --- /dev/null +++ b/printfile/types.go @@ -0,0 +1,606 @@ +package printfile + +// MakerBotFile is a representation of a .makerbot file +type MakerBotFile struct { + ThumbnailSizes *[]Thumbnail + Toolpath *Toolpath + Metadata *Metadata +} + +// Thumbnail represents a `thumbnail_*.jpg` file embedded +// within a .makerbot file. +// +// TargetWidth and TargetHeight exist because of a really funny +// bug with MakerBot Print: since it takes the thumbnail photos +// on the device that is slicing the print and they didn't take +// into account the screen resolution/density, sometimes (e.g. on +// a MacBook Pro) it can be different than the target width that's +// stated on the file itself. On my MacBook, it's actually double +// the size lol. +// +// So ActualWidth and ActualHeight hold the actual dimensions of the +// image. +type Thumbnail struct { + Data []byte + TargetWidth int + TargetHeight int + ActualWidth int + ActualHeight int +} + +// Toolpath is a set of ToolpathInstructions +type Toolpath []ToolpathInstruction + +// ToolpathInstruction represents something +type ToolpathInstruction struct { + Command ToolpathCommand `json:"command"` +} + +// ToolpathCommand is a command +type ToolpathCommand struct { + Function string `json:"function"` + Metadata struct { + Relative struct { + A bool `json:"a"` + X bool `json:"x"` + Y bool `json:"y"` + Z bool `json:"z"` + } `json:"relative"` + } `json:"metadata"` + Parameters struct { + A float64 `json:"a"` + FeedRate float64 `json:"feedrate"` + X float64 `json:"x"` + Y float64 `json:"y"` + Z float64 `json:"z"` + } `json:"parameters"` + Tags []string `json:"tags"` +} + +// Metadata is a representation of the meta.json +// file inside of .makerbot print files. +// +// I have no clue what most of these fields do. +// I just threw meta.json into https://mholt.github.io/json-to-go/ +// because I was NOT doing all of this by hand. +type Metadata struct { + BotType string `json:"bot_type"` + BoundingBox struct { + XMax float64 `json:"x_max"` + XMin float64 `json:"x_min"` + YMax float64 `json:"y_max"` + YMin float64 `json:"y_min"` + ZMax float64 `json:"z_max"` + ZMin float64 `json:"z_min"` + } `json:"bounding_box"` + ChamberTemperature float64 `json:"chamber_temperature"` + CommandedDurationSeconds float64 `json:"commanded_duration_s"` + DurationSeconds float64 `json:"duration_s"` + ExtruderTemperature int `json:"extruder_temperature"` + ExtruderTemperatures []int `json:"extruder_temperatures"` + ExtrusionDistanceMm float64 `json:"extrusion_distance_mm"` + ExtrusionDistancesMm []float64 `json:"extrusion_distances_mm"` + ExtrusionMassGrams float64 `json:"extrusion_mass_g"` + ExtrusionMassesGrams []float64 `json:"extrusion_masses_g"` + GrueVersion string `json:"grue_version"` + MachineConfig struct { + Acceleration struct { + BufferSize int `json:"buffer_size"` + ImpulseSpeedLimitMmPerS struct { + X int `json:"x"` + Y int `json:"y"` + Z int `json:"z"` + } `json:"impulse_speed_limit_mm_per_s"` + MaxSpeedChangeMmPerS struct { + X int `json:"x"` + Y int `json:"y"` + Z int `json:"z"` + } `json:"max_speed_change_mm_per_s"` + MinSpeedChangeMmPerS struct { + X float64 `json:"x"` + Y float64 `json:"y"` + Z int `json:"z"` + } `json:"min_speed_change_mm_per_s"` + RateMmPerSSq struct { + X int `json:"x"` + Y int `json:"y"` + Z int `json:"z"` + } `json:"rate_mm_per_s_sq"` + SplitMoveDistanceMm float64 `json:"split_move_distance_mm"` + SplitMoveRecursionCount int `json:"split_move_recursion_count"` + } `json:"acceleration"` + BotType string `json:"bot_type"` + BuildVolume struct { + X int `json:"x"` + Y int `json:"y"` + Z int `json:"z"` + } `json:"build_volume"` + ExtraSlicerSettings struct { + PlateVariability float64 `json:"plate_variability"` + } `json:"extra_slicer_settings"` + ExtruderProfiles struct { + AttachedExtruders []struct { + Calibrated string `json:"calibrated"` + ID int `json:"id"` + } `json:"attached_extruders"` + Mk12 struct { + Materials struct { + Pla struct { + Acceleration struct { + ImpulseSpeedLimitMmPerS struct { + A int `json:"a"` + } `json:"impulse_speed_limit_mm_per_s"` + MaxSpeedChangeMmPerS struct { + A float64 `json:"a"` + } `json:"max_speed_change_mm_per_s"` + MinSpeedChangeMmPerS struct { + A float64 `json:"a"` + } `json:"min_speed_change_mm_per_s"` + RateMmPerSSq struct { + A int `json:"a"` + } `json:"rate_mm_per_s_sq"` + SlipCompensationTable [][]int `json:"slip_compensation_table"` + } `json:"acceleration"` + FeedDiameter float64 `json:"feed_diameter"` + MaxFlowRate float64 `json:"max_flow_rate"` + OozeFeedstockDistance float64 `json:"ooze_feedstock_distance"` + RestartRate int `json:"restart_rate"` + RetractDistance float64 `json:"retract_distance"` + RetractRate int `json:"retract_rate"` + Temperature int `json:"temperature"` + } `json:"pla"` + } `json:"materials"` + MaxSpeedMmPerSecond struct { + A float64 `json:"a"` + } `json:"max_speed_mm_per_second"` + NozzleDiameter float64 `json:"nozzle_diameter"` + StepsPerMm struct { + A float64 `json:"a"` + } `json:"steps_per_mm"` + } `json:"mk12"` + Mk13 struct { + Materials struct { + Pla struct { + Acceleration struct { + ImpulseSpeedLimitMmPerS struct { + A float64 `json:"a"` + } `json:"impulse_speed_limit_mm_per_s"` + MaxSpeedChangeMmPerS struct { + A float64 `json:"a"` + } `json:"max_speed_change_mm_per_s"` + MinSpeedChangeMmPerS struct { + A float64 `json:"a"` + } `json:"min_speed_change_mm_per_s"` + RateMmPerSSq struct { + A float64 `json:"a"` + } `json:"rate_mm_per_s_sq"` + SlipCompensationTable [][]int `json:"slip_compensation_table"` + } `json:"acceleration"` + FeedDiameter float64 `json:"feed_diameter"` + MaxFlowRate float64 `json:"max_flow_rate"` + OozeFeedstockDistance float64 `json:"ooze_feedstock_distance"` + RestartRate int `json:"restart_rate"` + RetractDistance float64 `json:"retract_distance"` + RetractRate int `json:"retract_rate"` + Temperature int `json:"temperature"` + } `json:"pla"` + } `json:"materials"` + MaxSpeedMmPerSecond struct { + A float64 `json:"a"` + } `json:"max_speed_mm_per_second"` + NozzleDiameter float64 `json:"nozzle_diameter"` + StepsPerMm struct { + A float64 `json:"a"` + } `json:"steps_per_mm"` + } `json:"mk13"` + Mk13Impla struct { + Materials struct { + ImPla struct { + Acceleration struct { + ImpulseSpeedLimitMmPerS struct { + A float64 `json:"a"` + } `json:"impulse_speed_limit_mm_per_s"` + MaxSpeedChangeMmPerS struct { + A float64 `json:"a"` + } `json:"max_speed_change_mm_per_s"` + MinSpeedChangeMmPerS struct { + A float64 `json:"a"` + } `json:"min_speed_change_mm_per_s"` + RateMmPerSSq struct { + A float64 `json:"a"` + } `json:"rate_mm_per_s_sq"` + SlipCompensationTable [][]int `json:"slip_compensation_table"` + } `json:"acceleration"` + FeedDiameter float64 `json:"feed_diameter"` + MaxFlowRate float64 `json:"max_flow_rate"` + OozeFeedstockDistance float64 `json:"ooze_feedstock_distance"` + RestartRate int `json:"restart_rate"` + RetractDistance float64 `json:"retract_distance"` + RetractRate int `json:"retract_rate"` + Temperature int `json:"temperature"` + } `json:"im-pla"` + } `json:"materials"` + MaxSpeedMmPerSecond struct { + A float64 `json:"a"` + } `json:"max_speed_mm_per_second"` + NozzleDiameter float64 `json:"nozzle_diameter"` + StepsPerMm struct { + A float64 `json:"a"` + } `json:"steps_per_mm"` + } `json:"mk13_impla"` + SupportedExtruders struct { + Num0 interface{} `json:"0"` + Num1 string `json:"1"` + Num2 string `json:"2"` + Num3 string `json:"3"` + Num4 string `json:"4"` + Num5 string `json:"5"` + Num6 string `json:"6"` + Num7 string `json:"7"` + Num8 string `json:"8"` + Num9 string `json:"9"` + Num10 string `json:"10"` + Num11 string `json:"11"` + Num12 string `json:"12"` + Num13 string `json:"13"` + Num14 string `json:"14"` + Num99 string `json:"99"` + } `json:"supported_extruders"` + } `json:"extruder_profiles"` + GantryConfiguration struct { + MaxFillSpeed int `json:"max_fill_speed"` + MaxInnerShellSpeed int `json:"max_inner_shell_speed"` + MaxOuterShellSpeed int `json:"max_outer_shell_speed"` + TravelSpeedXy int `json:"travel_speed_xy"` + TravelSpeedZ int `json:"travel_speed_z"` + } `json:"gantry_configuration"` + MakerbotGeneration int `json:"makerbot_generation"` + MaxSpeedMmPerSecond struct { + X int `json:"x"` + Y int `json:"y"` + Z int `json:"z"` + } `json:"max_speed_mm_per_second"` + StartPosition struct { + X int `json:"x"` + Y int `json:"y"` + Z float64 `json:"z"` + } `json:"start_position"` + StepsPerMm struct { + X float64 `json:"x"` + Y float64 `json:"y"` + Z int `json:"z"` + } `json:"steps_per_mm"` + Version string `json:"version"` + } `json:"machine_config"` + Material string `json:"material"` + Materials []string `json:"materials"` + MiracleConfig struct { + Bot string `json:"_bot"` + Extruders []string `json:"_extruders"` + Materials []string `json:"_materials"` + DoRaft bool `json:"doRaft"` + Gaggles struct { + Default struct { + AdjacentFillLeakyConnections bool `json:"adjacentFillLeakyConnections"` + AdjacentFillLeakyDistanceRatio float64 `json:"adjacentFillLeakyDistanceRatio"` + BacklashEpsilon float64 `json:"backlashEpsilon"` + BacklashFeedback float64 `json:"backlashFeedback"` + BacklashX float64 `json:"backlashX"` + BacklashY float64 `json:"backlashY"` + BaseInsetDistanceMultiplier float64 `json:"baseInsetDistanceMultiplier"` + BaseLayerHeight float64 `json:"baseLayerHeight"` + BaseLayerWidth float64 `json:"baseLayerWidth"` + BaseNumberOfShells int `json:"baseNumberOfShells"` + BedZOffset int `json:"bedZOffset"` + BridgeAnchorMinimumLength float64 `json:"bridgeAnchorMinimumLength"` + BridgeAnchorWidth float64 `json:"bridgeAnchorWidth"` + BridgeMaximumLength float64 `json:"bridgeMaximumLength"` + BrimsBaseWidth float64 `json:"brimsBaseWidth"` + BrimsModelOffset float64 `json:"brimsModelOffset"` + BrimsOverlapWidth float64 `json:"brimsOverlapWidth"` + Coarseness float64 `json:"coarseness"` + ComputeVolumeLike210 bool `json:"computeVolumeLike2_1_0"` + DefaultExtruder int `json:"defaultExtruder"` + DefaultSupportMaterial int `json:"defaultSupportMaterial"` + Description string `json:"description"` + DoBacklashCompensation bool `json:"doBacklashCompensation"` + DoBreakawaySupport bool `json:"doBreakawaySupport"` + DoBridging bool `json:"doBridging"` + DoBrims bool `json:"doBrims"` + DoExponentialDeceleration bool `json:"doExponentialDeceleration"` + DoExternalSpurs bool `json:"doExternalSpurs"` + DoFanCommand bool `json:"doFanCommand"` + DoFanModulation bool `json:"doFanModulation"` + DoFixedLayerStart bool `json:"doFixedLayerStart"` + DoFixedShellStart bool `json:"doFixedShellStart"` + DoInternalSpurs bool `json:"doInternalSpurs"` + DoMinfill bool `json:"doMinfill"` + DoMixedRaft bool `json:"doMixedRaft"` + DoMixedSupport bool `json:"doMixedSupport"` + DoNewPathPlanning bool `json:"doNewPathPlanning"` + DoPaddedBase bool `json:"doPaddedBase"` + DoRaft bool `json:"doRaft"` + DoRateLimit bool `json:"doRateLimit"` + DoSplitLongMoves bool `json:"doSplitLongMoves"` + DoSupport bool `json:"doSupport"` + DoSupportUnderBridges bool `json:"doSupportUnderBridges"` + ExponentialDecelerationMinSpeed float64 `json:"exponentialDecelerationMinSpeed"` + ExponentialDecelerationRatio float64 `json:"exponentialDecelerationRatio"` + ExponentialDecelerationSegmentCount int `json:"exponentialDecelerationSegmentCount"` + ExtruderProfiles []struct { + DefaultTemperature int `json:"defaultTemperature"` + ExtrusionProfiles struct { + Bridges struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate float64 `json:"feedrate"` + } `json:"bridges"` + Brims struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate float64 `json:"feedrate"` + } `json:"brims"` + FirstModelLayer struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate float64 `json:"feedrate"` + } `json:"firstModelLayer"` + FloorSurfaceFills struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate int `json:"feedrate"` + } `json:"floorSurfaceFills"` + Infill struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate int `json:"feedrate"` + } `json:"infill"` + Insets struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate int `json:"feedrate"` + } `json:"insets"` + Outlines struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate int `json:"feedrate"` + } `json:"outlines"` + Purge struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate int `json:"feedrate"` + } `json:"purge"` + Raft struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate float64 `json:"feedrate"` + } `json:"raft"` + RaftBase struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate float64 `json:"feedrate"` + } `json:"raftBase"` + RoofSurfaceFills struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate int `json:"feedrate"` + } `json:"roofSurfaceFills"` + SparseRoofSurfaceFills struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate int `json:"feedrate"` + } `json:"sparseRoofSurfaceFills"` + Spurs struct { + FanSpeed float64 `json:"fanSpeed"` + Feedrate int `json:"feedrate"` + } `json:"spurs"` + } `json:"extrusionProfiles"` + ExtrusionVolumeMultiplier float64 `json:"extrusionVolumeMultiplier"` + FeedDiameter float64 `json:"feedDiameter"` + IdleTemperature int `json:"idleTemperature"` + NozzleDiameter float64 `json:"nozzleDiameter"` + OozeFeedstockDistance float64 `json:"oozeFeedstockDistance"` + PreOozeFeedstockDistance float64 `json:"preOozeFeedstockDistance"` + RestartExtraDistance float64 `json:"restartExtraDistance"` + RestartRate int `json:"restartRate"` + RetractDistance float64 `json:"retractDistance"` + RetractRate int `json:"retractRate"` + ToolchangeRestartDistance float64 `json:"toolchangeRestartDistance"` + ToolchangeRestartRate float64 `json:"toolchangeRestartRate"` + ToolchangeRetractDistance float64 `json:"toolchangeRetractDistance"` + ToolchangeRetractRate float64 `json:"toolchangeRetractRate"` + } `json:"extruderProfiles"` + FanDefaultSpeed float64 `json:"fanDefaultSpeed"` + FanLayer int `json:"fanLayer"` + FanModulationThreshold float64 `json:"fanModulationThreshold"` + FanModulationWindow float64 `json:"fanModulationWindow"` + FixedLayerStartX int `json:"fixedLayerStartX"` + FixedLayerStartY int `json:"fixedLayerStartY"` + FixedShellStartDirection int `json:"fixedShellStartDirection"` + FloorSolidThickness int `json:"floorSolidThickness"` + FloorSurfaceThickness float64 `json:"floorSurfaceThickness"` + FloorThickness float64 `json:"floorThickness"` + HorizontalInset int `json:"horizontalInset"` + InfillDensity float64 `json:"infillDensity"` + InfillShellSpacingMultiplier float64 `json:"infillShellSpacingMultiplier"` + InsetDistanceMultiplier float64 `json:"insetDistanceMultiplier"` + LayerHeight float64 `json:"layerHeight"` + LeakyConnectionsAdjacentDistance float64 `json:"leakyConnectionsAdjacentDistance"` + MaxConnectionLength float64 `json:"maxConnectionLength"` + MaxSparseFillThickness float64 `json:"maxSparseFillThickness"` + MaxSpurWidth float64 `json:"maxSpurWidth"` + MinLayerDuration float64 `json:"minLayerDuration"` + MinLayerHeight float64 `json:"minLayerHeight"` + MinRaftBaseGap float64 `json:"minRaftBaseGap"` + MinSpeedMultiplier float64 `json:"minSpeedMultiplier"` + MinSpurLength float64 `json:"minSpurLength"` + MinSpurWidth float64 `json:"minSpurWidth"` + MinThickInfillImprovement float64 `json:"minThickInfillImprovement"` + MinimumMoveDistance float64 `json:"minimumMoveDistance"` + ModelFillProfiles struct { + Bridge struct { + Density float64 `json:"density"` + OrientationInterval int `json:"orientationInterval"` + OrientationOffset int `json:"orientationOffset"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"bridge"` + FloorSurface struct { + Density float64 `json:"density"` + OrientationInterval int `json:"orientationInterval"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"floor_surface"` + RoofSurface struct { + Density float64 `json:"density"` + OrientationInterval int `json:"orientationInterval"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"roof_surface"` + Solid struct { + Density float64 `json:"density"` + OrientationInterval int `json:"orientationInterval"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"solid"` + Sparse struct { + Density float64 `json:"density"` + DiamondFillTurnDistance float64 `json:"diamondFillTurnDistance"` + DiamondFillZigZagOverlap float64 `json:"diamondFillZigZagOverlap"` + OrientationInterval int `json:"orientationInterval"` + OrientationOffset int `json:"orientationOffset"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"sparse"` + SparseRoofSurface struct { + Density float64 `json:"density"` + DiamondFillTurnDistance float64 `json:"diamondFillTurnDistance"` + DiamondFillZigZagOverlap float64 `json:"diamondFillZigZagOverlap"` + OrientationInterval int `json:"orientationInterval"` + OrientationOffset int `json:"orientationOffset"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"sparse_roof_surface"` + } `json:"modelFillProfiles"` + NumberOfBrims int `json:"numberOfBrims"` + NumberOfExtentShells int `json:"numberOfExtentShells"` + NumberOfInternalBrims int `json:"numberOfInternalBrims"` + NumberOfShells int `json:"numberOfShells"` + NumberOfSparseShells int `json:"numberOfSparseShells"` + NumberOfSupportShells int `json:"numberOfSupportShells"` + PaddedBaseOutlineOffset float64 `json:"paddedBaseOutlineOffset"` + PauseHeights []interface{} `json:"pauseHeights"` + PurgeBaseRotation int `json:"purgeBaseRotation"` + PurgeBucketSide float64 `json:"purgeBucketSide"` + PurgeWallBaseFilamentWidth float64 `json:"purgeWallBaseFilamentWidth"` + PurgeWallBasePatternLength float64 `json:"purgeWallBasePatternLength"` + PurgeWallBasePatternWidth float64 `json:"purgeWallBasePatternWidth"` + PurgeWallModelOffset float64 `json:"purgeWallModelOffset"` + PurgeWallPatternWidth float64 `json:"purgeWallPatternWidth"` + PurgeWallSpacing float64 `json:"purgeWallSpacing"` + PurgeWallWidth float64 `json:"purgeWallWidth"` + PurgeWallXLength int `json:"purgeWallXLength"` + RaftBaseInfillShellSpacingMultiplier float64 `json:"raftBaseInfillShellSpacingMultiplier"` + RaftBaseInsetDistanceMultiplier float64 `json:"raftBaseInsetDistanceMultiplier"` + RaftBaseLayers int `json:"raftBaseLayers"` + RaftBaseOutset int `json:"raftBaseOutset"` + RaftBaseShells int `json:"raftBaseShells"` + RaftBaseThickness float64 `json:"raftBaseThickness"` + RaftBaseWidth float64 `json:"raftBaseWidth"` + RaftBrimsSpacing float64 `json:"raftBrimsSpacing"` + RaftExtraOffset float64 `json:"raftExtraOffset"` + RaftFillProfiles struct { + Base struct { + Density float64 `json:"density"` + LinearFillGroupDensity float64 `json:"linearFillGroupDensity"` + LinearFillGroupSize int `json:"linearFillGroupSize"` + OrientationInterval int `json:"orientationInterval"` + OrientationOffset int `json:"orientationOffset"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"base"` + Interface struct { + Density float64 `json:"density"` + OrientationInterval int `json:"orientationInterval"` + OrientationOffset int `json:"orientationOffset"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"interface"` + Surface struct { + Density float64 `json:"density"` + OrientationInterval int `json:"orientationInterval"` + OrientationOffset int `json:"orientationOffset"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"surface"` + } `json:"raftFillProfiles"` + RaftInterfaceLayers int `json:"raftInterfaceLayers"` + RaftInterfaceShells int `json:"raftInterfaceShells"` + RaftInterfaceThickness float64 `json:"raftInterfaceThickness"` + RaftInterfaceWidth float64 `json:"raftInterfaceWidth"` + RaftInterfaceZOffset float64 `json:"raftInterfaceZOffset"` + RaftModelShellsSpacing float64 `json:"raftModelShellsSpacing"` + RaftModelSpacing float64 `json:"raftModelSpacing"` + RaftSurfaceInsetDistanceMultiplier float64 `json:"raftSurfaceInsetDistanceMultiplier"` + RaftSurfaceLayers int `json:"raftSurfaceLayers"` + RaftSurfaceOutset int `json:"raftSurfaceOutset"` + RaftSurfaceShellSpacingMultiplier float64 `json:"raftSurfaceShellSpacingMultiplier"` + RaftSurfaceShells int `json:"raftSurfaceShells"` + RaftSurfaceThickness float64 `json:"raftSurfaceThickness"` + RaftSurfaceZOffset float64 `json:"raftSurfaceZOffset"` + RateLimitBufferSize int `json:"rateLimitBufferSize"` + RateLimitMinSpeed int `json:"rateLimitMinSpeed"` + RateLimitSpeedRatio float64 `json:"rateLimitSpeedRatio"` + RateLimitSplitBias int `json:"rateLimitSplitBias"` + RateLimitSplitMoveDistance float64 `json:"rateLimitSplitMoveDistance"` + RateLimitSplitRecursionDepth int `json:"rateLimitSplitRecursionDepth"` + RateLimitTransmissionRate int `json:"rateLimitTransmissionRate"` + RoofAnchorMargin float64 `json:"roofAnchorMargin"` + RoofSolidThickness int `json:"roofSolidThickness"` + RoofSurfaceThickness float64 `json:"roofSurfaceThickness"` + RoofThickness float64 `json:"roofThickness"` + ShellsLeakyConnections bool `json:"shellsLeakyConnections"` + SplitMinimumDistance float64 `json:"splitMinimumDistance"` + StartPosition struct { + X int `json:"x"` + Y int `json:"y"` + Z float64 `json:"z"` + } `json:"startPosition"` + SupportAngle float64 `json:"supportAngle"` + SupportExtraDistance float64 `json:"supportExtraDistance"` + SupportFillProfiles struct { + Sparse struct { + ConsistentOrder bool `json:"consistentOrder"` + Density float64 `json:"density"` + OrientationInterval int `json:"orientationInterval"` + OrientationOffset int `json:"orientationOffset"` + OrientationRange int `json:"orientationRange"` + Pattern string `json:"pattern"` + } `json:"sparse"` + } `json:"supportFillProfiles"` + SupportInsetDistanceMultiplier float64 `json:"supportInsetDistanceMultiplier"` + SupportInteriorExtruder int `json:"supportInteriorExtruder"` + SupportLayerHeight float64 `json:"supportLayerHeight"` + SupportLeakyConnections bool `json:"supportLeakyConnections"` + SupportModelSpacing float64 `json:"supportModelSpacing"` + SupportRoofModelSpacing float64 `json:"supportRoofModelSpacing"` + SupportShellSpacingMultiplier float64 `json:"supportShellSpacingMultiplier"` + ThickLayerThreshold int `json:"thickLayerThreshold"` + ThickLayerVolumeMultiplier int `json:"thickLayerVolumeMultiplier"` + TravelSpeedXY int `json:"travelSpeedXY"` + TravelSpeedZ int `json:"travelSpeedZ"` + UseRelativeExtruderPositions bool `json:"useRelativeExtruderPositions"` + } `json:"default"` + } `json:"gaggles"` + Version string `json:"version"` + } `json:"miracle_config"` + ModelCounts []struct { + Count int `json:"count"` + Name string `json:"name"` + } `json:"model_counts"` + NumZLayers int `json:"num_z_layers"` + NumZTransitions int `json:"num_z_transitions"` + PlatformTemperature int `json:"platform_temperature"` + Preferences struct { + Default struct { + Overrides struct { + DefaultSupportMaterial int `json:"defaultSupportMaterial"` + } `json:"overrides"` + PrintMode string `json:"print_mode"` + } `json:"default"` + } `json:"preferences"` + ThingID interface{} `json:"thing_id"` + ToolType string `json:"tool_type"` + ToolTypes []string `json:"tool_types"` + TotalCommands int `json:"total_commands"` + UUID string `json:"uuid"` + Version string `json:"version"` +} diff --git a/reflector/client.go b/reflector/client.go index e628371..7accf51 100644 --- a/reflector/client.go +++ b/reflector/client.go @@ -76,14 +76,17 @@ func (c *Client) httpPost(endpoint string, params map[string]string) (*json.RawM return &jr, nil } +// GetPrinters gets a list of printers connected to the Thingiverse account func (c *Client) GetPrinters() (*json.RawMessage, error) { return c.httpGet("/printers") } +// GetPrinter gets a printer with `id` func (c *Client) GetPrinter(id string) (*json.RawMessage, error) { return c.httpGet(fmt.Sprintf("/printers/%s", id)) } +// CallPrinter returns a relay on which you can attach a makerbot.Client func (c *Client) CallPrinter(id string) (*CallPrinterResponse, error) { resp, err := c.httpPost("/call", map[string]string{"printer_id": id}) if err != nil { diff --git a/reflector/reflector.go b/reflector/reflector.go index 9870cf5..8bf71c4 100644 --- a/reflector/reflector.go +++ b/reflector/reflector.go @@ -5,6 +5,8 @@ package reflector import "net/http" +// NewClient returns a Client with the specified access +// token func NewClient(accessToken string) Client { return Client{ BaseURL: "https://reflector.makerbot.com", diff --git a/reflector/types.go b/reflector/types.go index 8ee5ba9..887c82a 100644 --- a/reflector/types.go +++ b/reflector/types.go @@ -2,6 +2,8 @@ package reflector import "net" +// CallPrinterResponse represents a response from the +// Client.CallPrinter method type CallPrinterResponse struct { Call struct { ID string `json:"id"` @@ -10,6 +12,7 @@ type CallPrinterResponse struct { } `json:"call"` } +// RelayAddr resolves the relay's address func (r *CallPrinterResponse) RelayAddr() (*net.TCPAddr, error) { return net.ResolveTCPAddr("tcp", r.Call.Relay) } diff --git a/utils.go b/utils.go index 3dfdc44..c648dff 100644 --- a/utils.go +++ b/utils.go @@ -54,7 +54,7 @@ type CameraFrameMetadata struct { Format CameraFrameFormat // Format that the frame is in (invalid, YUYV, JPEG) } -func parseCameraFrameMetadata(packed []byte) CameraFrameMetadata { +func unpackCameraFrameMetadata(packed []byte) CameraFrameMetadata { return CameraFrameMetadata{ FileSize: binary.BigEndian.Uint32(packed[0:4]) - 16, Width: binary.BigEndian.Uint32(packed[4:8]),