Skip to content

Commit

Permalink
yote
Browse files Browse the repository at this point in the history
  • Loading branch information
tjhorner committed Apr 26, 2019
1 parent 4f21df1 commit 52f13da
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 76 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
ignore_me_*
5 changes: 3 additions & 2 deletions README.md
Expand Up @@ -11,16 +11,17 @@ Documentation, examples and more at [GoDoc](https://godoc.org/github.com/tjhorne
## Features and TODO

- [x] Connecting to printers (`Connect()`)
- [ ] Printer discovery via mDNS
- [x] Authenticating with local printers via Thingiverse (`AuthenticateWithThingiverse()`)
- [ ] Authenticating with local printers via local authentication (pushing the knob)
- [ ] Authenticating with remote printers via MakerBot Reflector
- [x] Authenticating with remote printers via MakerBot Reflector
- [x] Printer state updates (`HandleStateUpdate()`)
- [x] Load filament method (`LoadFilament()`)
- [x] Unload filament method (`UnloadFilament()`)
- [x] Cancel method (`Cancel()`)
- [x] Change machine name (`ChangeMachineName()`)
- [ ] Send print files
- [ ] Camera stream/snapshots
- [x] Camera stream/snapshots
- [ ] Get machine config
- [ ] Write tests (will need to make a mock MakerBot RPC server)
- [ ] Write examples
Expand Down
2 changes: 1 addition & 1 deletion auth.go
Expand Up @@ -18,7 +18,7 @@ func (c *Client) httpGet(endpoint string, qs map[string]string) (map[string]inte
}
req.URL.RawQuery = q.Encode()

r, err := c.http.Do(req)
r, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
Expand Down
174 changes: 148 additions & 26 deletions client.go
Expand Up @@ -3,8 +3,9 @@ package makerbot
import (
"encoding/json"
"errors"
"log"
"net/http"
"strings"

"github.com/tjhorner/makerbot-rpc/reflector"

"github.com/tjhorner/makerbot-rpc/jsonrpc"
)
Expand All @@ -17,53 +18,127 @@ type rpcSystemNotification struct {

// Client represents an RPC client that can connect to
// MakerBot 3D printers via the network.
//
// Calls to the printer (e.g. LoadFilament, Cancel, etc.)
// will block, so you may want to take this into consideration.
type Client struct {
IP string
Port string
Printer *Printer
stateCbs []func(old, new *PrinterMetadata)
http *http.Client
rpc *jsonrpc.Client
IP string
Port string
Printer *Printer
stateCbs []func(old, new *PrinterMetadata)
cameraCh *chan CameraFrame
cameraCbs []func(*CameraFrame)
rpc *jsonrpc.Client
}

// Connect connects to the printer and performs the initial handshake.
// ConnectLocal connects to a local printer and performs the initial handshake.
// If it is successful, the Printer field will be populated with information
// about the machine this client is connected to.
func (c *Client) Connect() error {
if c.IP == "" || c.Port == "" {
return errors.New("IP and Port are required fields for Client")
//
// After using ConnectLocal, you must use one of the AuthenticateWith* methods
// to authenticate with the printer.
func (c *Client) ConnectLocal(ip, port string) error {
c.IP = ip
c.Port = port

err := c.connectRPC()
if err != nil {
return err
}

return c.handshake()
}

// ConnectRemote uses MakerBot Reflector to remotely connect to a printer
// and performs the initial handshake. It will connect to printer with ID
// `id` and will authenticate using the Thingiverse token `accessToken`.
//
// Since authentication is already performed by Thingiverse, you do not need
// to perform any additional authentication after it is connected.
func (c *Client) ConnectRemote(id, accessToken string) error {
refl := reflector.NewClient(accessToken)

call, err := refl.CallPrinter(id)
if err != nil {
return err
}

split := strings.Split(call.Call.Relay, ":")
c.IP = split[0]
c.Port = split[1]

err = c.connectRPC()
if err != nil {
return err
}

ok, err := c.sendAuthPacket(id, call)
if err != nil {
return err
}

if !*ok {
return errors.New("could not authenticate with printer via Reflector call")
}

rpc := jsonrpc.NewClient(c.IP, c.Port)
err := rpc.Connect()
return c.handshake()
}

func (c *Client) connectRPC() error {
c.rpc = jsonrpc.NewClient(c.IP, c.Port)
return c.rpc.Connect()
}

func (c *Client) handshake() error {
printer, err := c.sendHandshake()
if err != nil {
log.Fatalln(err)
return err
}

c.Printer = printer

onStateChange := func(message json.RawMessage) {
oldState := c.Printer.Metadata
var oldState *PrinterMetadata
if c.Printer != nil {
oldState = c.Printer.Metadata
}

var newState rpcSystemNotification
json.Unmarshal(message, &newState)

c.Printer.Metadata = newState.Info

for _, cb := range c.stateCbs {
cb(oldState, newState.Info)
go cb(oldState, newState.Info) // Async so we don't block other callbacks
}
}

rpc.Subscribe("system_notification", onStateChange)
rpc.Subscribe("state_notification", onStateChange)
c.rpc.Subscribe("system_notification", onStateChange)
c.rpc.Subscribe("state_notification", onStateChange)

c.rpc = rpc
c.rpc.Subscribe("camera_frame", func(m json.RawMessage) {
if len(c.cameraCbs) == 0 {
go c.endCameraStream()
}

printer, err := c.handshake()
if err != nil {
return err
}
metadata := parseCameraFrameMetadata(c.rpc.GetRawData(16))

c.Printer = printer
data := c.rpc.GetRawData(int(metadata.FileSize))

frame := CameraFrame{
Data: data,
Metadata: &metadata,
}

if c.cameraCh != nil {
*c.cameraCh <- frame
c.cameraCh = nil
}

for _, cb := range c.cameraCbs {
go cb(&frame) // Async so we don't block other callbacks
}
})

return nil
}
Expand All @@ -88,6 +163,12 @@ func (c *Client) HandleStateChange(cb func(old, new *PrinterMetadata)) {
c.stateCbs = append(c.stateCbs, cb)
}

// HandleCameraFrame calls `cb` when the printer sends a camera frame.
func (c *Client) HandleCameraFrame(cb func(frame *CameraFrame)) {
c.cameraCbs = append(c.cameraCbs, cb)
go c.requestCameraStream()
}

func (c *Client) call(method string, args, result interface{}) error {
if c.rpc == nil {
return errors.New("client is not connected to printer")
Expand All @@ -96,7 +177,7 @@ func (c *Client) call(method string, args, result interface{}) error {
return c.rpc.Call(method, args, &result)
}

func (c *Client) handshake() (*Printer, error) {
func (c *Client) sendHandshake() (*Printer, error) {
var reply Printer
return &reply, c.call("handshake", rpcEmptyParams{}, &reply)
}
Expand All @@ -113,6 +194,23 @@ func (c *Client) authenticate(accessToken string) (*json.RawMessage, error) {
return &reply, c.call("authenticate", rpcAuthenticateParams{accessToken}, &reply)
}

type rpcAuthPacketParams struct {
CallID string `json:"call_id"`
ClientCode string `json:"client_code"`
PrinterID string `json:"printer_id"`
}

func (c *Client) sendAuthPacket(id string, pc *reflector.CallPrinterResponse) (*bool, error) {
params := rpcAuthPacketParams{
CallID: pc.Call.ID,
ClientCode: pc.Call.ClientCode,
PrinterID: id,
}

var reply bool
return &reply, c.call("auth_packet", params, &reply)
}

// AuthenticateWithThingiverse performs authentication with the printers
// by using a Thingiverse token:username pair.
//
Expand Down Expand Up @@ -166,3 +264,27 @@ func (c *Client) ChangeMachineName(name string) (*json.RawMessage, error) {
var reply json.RawMessage
return &reply, c.call("cancel", rpcChangeMachineNameParams{name}, &reply)
}

func (c *Client) requestCameraStream() error {
return c.call("request_camera_stream", rpcEmptyParams{}, nil)
}

func (c *Client) endCameraStream() error {
return c.call("end_camera_stream", rpcEmptyParams{}, nil)
}

// GetCameraFrame requests a single frame from the printer's camera
func (c *Client) GetCameraFrame() (*CameraFrame, error) {
ch := make(chan CameraFrame)
c.cameraCh = &ch

err := c.requestCameraStream()
if err != nil {
return nil, err
}

data := <-ch
close(ch)

return &data, nil
}
78 changes: 41 additions & 37 deletions jsonrpc/client.go
Expand Up @@ -51,6 +51,7 @@ type Client struct {
Port string
rsps map[string]chan rpcResponse
subs map[string]func(json.RawMessage)
jr JSONReader
conn *net.TCPConn
}

Expand All @@ -66,47 +67,42 @@ func (c *Client) Connect() error {
return err
}

jd := make(chan []byte)
jr := NewJSONReader(jd)
done := func(j []byte) error {
// need to determine if this is a request or a response
var resp rpcResponse
err := json.Unmarshal(j, &resp)
if err != nil {
return err
}

go func() {
for {
b := make([]byte, 1)
conn.Read(b)
if resp.Result == nil && resp.Error == nil && resp.ID == nil {
// Request
var req rpcServerRequest
json.Unmarshal(j, &req)

if sub, ok := c.subs[req.Method]; ok {

jr.FeedByte(b[0])
go sub(req.Params)
}
} else if resp.ID != nil {
// Response
if rsp, ok := c.rsps[*resp.ID]; ok {
go func() { rsp <- resp }()
delete(c.rsps, *resp.ID)
}
}
}()

return nil
}

c.jr = NewJSONReader(done)

go func() {
for {
j := <-jd
// log.Println(string(j))

// need to determine if this is a request or a response
var resp rpcResponse
json.Unmarshal(j, &resp)

// log.Printf("recv'd json: %+v\n", resp)

if resp.Result == nil && resp.Error == nil && resp.ID == nil {
// Request
var req rpcServerRequest
json.Unmarshal(j, &req)

if sub, ok := c.subs[req.Method]; ok {
// log.Printf("request: %+v\n", req)
sub(req.Params)
}
} else {
// Response
if rsp, ok := c.rsps[*resp.ID]; ok {
// log.Printf("response: %+v\n", resp)
rsp <- resp
delete(c.rsps, *resp.ID)
// log.Printf("%+v\n", c.rsps)
}
}
b := make([]byte, 1)
conn.Read(b)

c.jr.FeedByte(b[0])
}
}()

Expand Down Expand Up @@ -137,7 +133,7 @@ func (c *Client) Call(serviceMethod string, args, reply interface{}) error {
}

id := uuid.New().String()
// log.Printf("ID: %+v\n", id)

req := rpcClientRequest{
Params: args,
}
Expand All @@ -161,7 +157,7 @@ func (c *Client) Call(serviceMethod string, args, reply interface{}) error {

if reply != nil {
resp := <-msg
// log.Println("this is good " + serviceMethod)

if resp.Error != nil {
return resp.Error
}
Expand All @@ -171,6 +167,7 @@ func (c *Client) Call(serviceMethod string, args, reply interface{}) error {
}

json.Unmarshal(*resp.Result, &reply)
close(msg)
}

return nil
Expand Down Expand Up @@ -199,3 +196,10 @@ func (c *Client) Subscribe(namespace string, cb func(message json.RawMessage)) e
func (c *Client) Unsubscribe(namespace string) {
delete(c.subs, namespace)
}

// GetRawData grabs raw data from the TCP connection until
// `length` is reached. The captured data is returned as an
// array of bytes.
func (c *Client) GetRawData(length int) []byte {
return c.jr.GetRawData(length)
}

0 comments on commit 52f13da

Please sign in to comment.