├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README.zh-cn.md ├── connector.go ├── connector_http.go ├── connector_sacp.go ├── discover.go ├── doc.go ├── go.mod ├── go.sum ├── localstorage.go ├── main.go ├── octoprint.go ├── printer.go ├── sacp.go ├── start-octoprint.bat └── utils.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 2 | name: Build 3 | 4 | on: 5 | push: 6 | tags: 7 | - "v*.*" 8 | 9 | jobs: 10 | build: 11 | name: Go Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: "1.20" 21 | check-latest: true 22 | 23 | - name: Build 24 | run: make all-zip 25 | 26 | - name: Create Release 27 | uses: softprops/action-gh-release@v1 28 | with: 29 | draft: true 30 | prerelease: false 31 | files: | 32 | dist/*.zip 33 | 34 | - name: Login to Docker Hub 35 | uses: docker/login-action@v3 36 | with: 37 | username: ${{ secrets.DOCKERHUB_USERNAME }} 38 | password: ${{ secrets.DOCKERHUB_TOKEN }} 39 | 40 | - name: Build and push Docker image 41 | uses: docker/build-push-action@v3 42 | with: 43 | context: . 44 | file: ./Dockerfile 45 | push: true 46 | tags: | 47 | macdylan/sm2uploader:latest 48 | macdylan/sm2uploader:${{ github.ref_name }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py 2 | dist/ 3 | .vscode/ 4 | go.work* 5 | docker-compose.yml 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | COPY dist/sm2uploader-linux-amd64 /usr/local/bin/sm2uploader 4 | 5 | ENV HOST=$HOST 6 | 7 | ENV TIMEOUT=0.1s 8 | ENV OCTOPRINT=:8888 9 | EXPOSE 8888 10 | 11 | ENTRYPOINT [ "/usr/local/bin/sm2uploader" ] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Anton Skorochod 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DIST = dist/ 2 | NAME = sm2uploader 3 | ifeq "$(GITHUB_REF_NAME)" "" 4 | VERSION := -X 'main.Version=$(shell git rev-parse --short HEAD)' 5 | else 6 | VERSION := -X 'main.Version=$(GITHUB_REF_NAME)' 7 | endif 8 | FLAGS = -ldflags="-w -s $(VERSION)" 9 | CMD = go build -trimpath $(FLAGS) 10 | SRC = $(shell ls *.go | grep -v _test.go) 11 | EXT_FILES = README.md README.zh-cn.md LICENSE 12 | 13 | .PHONY: all clean dep darwin-arm64 darwin-amd64 linux-amd64 linux-arm7 linux-arm6 win64 win32 14 | 15 | darwin-arm64: $(SRC) 16 | GOOS=darwin GOARCH=arm64 $(CMD) -o $(DIST)$(NAME)-$@ $^ 17 | 18 | darwin-amd64: $(SRC) 19 | GOOS=darwin GOARCH=amd64 $(CMD) -o $(DIST)$(NAME)-$@ $^ 20 | 21 | linux-amd64: $(SRC) 22 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(CMD) -o $(DIST)$(NAME)-$@ $^ 23 | 24 | linux-arm7: $(SRC) 25 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(CMD) -o $(DIST)$(NAME)-$@ $^ 26 | 27 | linux-arm6: $(SRC) 28 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 $(CMD) -o $(DIST)$(NAME)-$@ $^ 29 | 30 | win64: $(SRC) 31 | GOOS=windows GOARCH=amd64 $(CMD) -o $(DIST)$(NAME)-$@.exe $^ 32 | 33 | win32: $(SRC) 34 | GOOS=windows GOARCH=386 $(CMD) -o $(DIST)$(NAME)-$@.exe $^ 35 | 36 | dep: # Get the dependencies 37 | go mod download 38 | 39 | all: dep darwin-arm64 win64 win32 darwin-amd64 linux-amd64 linux-arm7 linux-arm6 40 | @true 41 | 42 | all-zip: all 43 | for p in darwin-arm64 win64.exe win32.exe darwin-amd64 linux-amd64 linux-arm7 linux-arm6; do \ 44 | if [ "$$p" = "win64.exe" -o "$$p" = "win32.exe" ]; then \ 45 | zip -j $(DIST)$(NAME)-$$p.zip $(DIST)$(NAME)-$$p $(EXT_FILES) *.bat; \ 46 | else \ 47 | zip -j $(DIST)$(NAME)-$$p.zip $(DIST)$(NAME)-$$p $(EXT_FILES); \ 48 | fi \ 49 | done 50 | 51 | clean: 52 | rm -f $(DIST)$(NAME)-* 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文说明|Chinese Readme](README.zh-cn.md) 2 | 3 | # sm2uploader 4 | A command-line tool for send the gcode file to Snapmaker Printers via WiFi connection. 5 | 6 | ## Features: 7 | - Auto discover printers (UDP broadcast, same as Snapmaker Luban) 8 | - Upload any type of file does not depend on the head/module limit 9 | - Simulated a OctoPrint server, so that it can be in any slicing software such as Cura/PrusaSlicer/SuperSlicer/OrcaSlicer send gcode to the printer 10 | - Smart pre-heat for switch tools, shutoff nozzles that are no longer in use, and other optimization features for multi-extruders. 11 | - Reinforce the prime tower to avoid it collapse for multi-filament printing 12 | - No need to click Yes button on the touch screen every time for authorization connect 13 | - Support Snapmaker 2 A150/250/350, J1, Artisan 14 | - Support for multiple platforms including win/macOS/Linux/RaspberryPi 15 | 16 | ## Usage: 17 | Download [sm2uploader](https://github.com/macdylan/sm2uploader/releases) 18 | - Linux/macOS: `chmod +x sm2uploader` 19 | 20 | for Windows: 21 | - locate to the sm2uploader folder, and double-click `start-octoprint.bat` 22 | - type a port number for octoprint that you wish to listen 23 | - when `Server started ...` message appears, the startup was successful, do not close the cmd window, and go to the slicer software to setup a OctoPrint printer 24 | - use `http://127.0.0.1:(PORT NUM)` as url, click the Test Connect button, all configuration will be finished if successful. 25 | 26 | ```bash 27 | ## Discover mode 28 | $ sm2uploader /path/to/code-file1 /path/to/code-file2 29 | Discovering ... 30 | Use the arrow keys to navigate: ↓ ↑ → ← 31 | ? Found 3 machines: 32 | ▸ A350-3DP@192.168.1.20 - Snapmaker A350 33 | A250-CNC@192.168.1.18 - Snapmaker A250 34 | J1V19@192.168.1.19 - Snapmaker-J1 35 | Printer IP: 192.168.1.19 36 | Printer Model: Snapmaker J1 37 | Uploading file 'code-file1' [1.2 MB]... 38 | - SACP sending 100% 39 | Upload finished. 40 | Uploading file 'code-file2' [1.0 MB]... 41 | - SACP sending 100% 42 | Upload finished. 43 | 44 | ## Use printer id 45 | $ sm2uploader -host J1V19 /path/to/code-file1 46 | Discovering ... 47 | Printer IP: 192.168.1.19 48 | Printer Model: Snapmaker J1 49 | Uploading file 'code-file1' [1.2 MB]... 50 | - SACP sending 100% 51 | Upload finished. 52 | 53 | ## OctoPrint server (CTRL-C to stop) 54 | $ sm2uploader -octoprint 127.0.0.1:8844 -host A350 55 | Printer IP: 192.168.1.20 56 | Printer Model: Snapmaker 2 Model A350 57 | Starting OctoPrint server on :8844 ... 58 | Server started, now you can upload files to http://127.0.0.1:8844 59 | Request GET /api/version completed in 6.334µs 60 | - HTTP sending 100.0% 61 | Upload finished: model.gcode [382.2 KB] 62 | Request POST /api/files/local completed in 951.080458ms 63 | ``` 64 | 65 | If UDP Discover can not work, use `sm2uploader -host 192.168.1.20 /file.gcode` to directly upload to printer. 66 | 67 | If `host` in `knownhosts`, `-host printer-id` is very convenient. 68 | 69 | Get help: `sm2uploader -h` 70 | 71 | ## Fix the "can not be opened because it is from an unidentified developer" 72 | 73 | Solution: https://osxdaily.com/2012/07/27/app-cant-be-opened-because-it-is-from-an-unidentified-developer/ 74 | 75 | or: 76 | `xattr -d com.apple.quarantine sm2uploader-darwin` 77 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | [English Readme](README.md) 2 | 3 | # sm2uploader 4 | Luban 和 Cura with SnapmakerPlugin 对于新手很友好,但是我的大部分配置文件在 PrusaSlicer 中,切片后再使用 Luban 上传到打印机是非常低效的。 5 | 这个工具提供了一步上传的能力,你可以通过命令行一次上传多个 gcode/cnc/bin固件 等文件。 6 | 7 | ## 功能 8 | - 自动发现局域网内所有的 Snapmaker 打印机(和 Luban 相同的协议,使用 UDP 广播) 9 | - 模拟 OctoPrint Server,这样就可以在各种切片软件,比如 Cura/PrusaSlicer/SuperSlicer/OrcaSlicer 中向 Snapmaker 打印机发送文件 10 | - 为多挤出机提供智能预热、关闭不再使用的喷头等优化功能 11 | - 强化擦料塔,避免多材料打印时因不粘合而倒塌,例如在 PETG+PLA 混合打印时 12 | - Snapmaker 2 A-Series 第一次连接时需要授权,之后可以直接一步上传 13 | - 支持 Snapmaker 2 A/J1/Artisan 全系列打印机 14 | - 支持 macOS/Windows/Linux/RaspberryPi 多个平台 15 | 16 | ## 使用方法 17 | 下载适用的[程序文件](https://github.com/macdylan/sm2uploader/releases) 18 | - Linux/macOS 下,可能需要赋予可执行权限 `chmod +x sm2uploader` 19 | 20 | Windows 使用方法: 21 | - 在解压目录中双击 `start-octoprint.bat` 批处理程序 22 | - 按照提示输入端口号,不输入直接回车则使用默认的 `8899` 端口 23 | - 当出现 `Server started ...` 信息时表示服务启动成功,此时不要关闭命令行窗口 24 | - 打开切片软件,设置物理打印机,输入命令行窗口中提示的 `http://127.0.0.1:端口号`,测试连接成功即可 25 | 26 | ```bash 27 | ## 自动查找模式 28 | $ sm2uploader /path/to/code-file1 /path/to/code-file2 29 | Discovering ... 30 | Use the arrow keys to navigate: ↓ ↑ → ← 31 | ? Found 3 machines: 32 | ▸ A350-3DP@192.168.1.20 - Snapmaker A350 33 | A250-CNC@192.168.1.18 - Snapmaker A250 34 | J1V19@192.168.1.19 - Snapmaker-J1 35 | Printer IP: 192.168.1.19 36 | Printer Model: Snapmaker J1 37 | Uploading file 'code-file1' [1.2 MB]... 38 | - SACP sending 100% 39 | Upload finished. 40 | Uploading file 'code-file2' [1.0 MB]... 41 | - SACP sending 100% 42 | Upload finished. 43 | 44 | ## 指定打印机名字进行连接 45 | $ sm2uploader -host J1V19 /path/to/code-file1 46 | Discovering ... 47 | Printer IP: 192.168.1.19 48 | Printer Model: Snapmaker J1 49 | Uploading file 'code-file1' [1.2 MB]... 50 | - SACP sending 100% 51 | Upload finished. 52 | 53 | ## 模拟 OctoPrint (CTRL-C 终止运行) 54 | $ sm2uploader -octoprint 127.0.0.1:8844 -host A350 55 | Printer IP: 192.168.1.20 56 | Printer Model: Snapmaker 2 Model A350 57 | Starting OctoPrint server on :8844 ... 58 | Server started, now you can upload files to http://127.0.0.1:8844 59 | Request GET /api/version completed in 6.334µs 60 | - HTTP sending 100.0% 61 | Upload finished: model.gcode [382.2 KB] 62 | Request POST /api/files/local completed in 951.080458ms 63 | ``` 64 | 65 | 打印机的 UDP 应答服务有时会挂掉,通常需要重启打印机来解决。或者你可以直接指定目标IP: `sm2uploader -host 192.168.1.20 /file.gcode` 66 | 67 | 如果 `host` 被发现过或者连接过,它会存在于 `knownhosts` 中,直接使用 id 进行连接会更加简洁: `sm2uploader -host A350-3DP /file.gcode` 68 | 69 | 更多参数:`sm2uploader -h` 70 | 71 | ## 在 macOS 系统提示文件无法打开的解决方法 72 | macOS 不允许直接打开未经数字签名的程序,参考解决方案: https://osxdaily.com/2012/07/27/app-cant-be-opened-because-it-is-from-an-unidentified-developer/ 73 | 74 | 也可以直接在终端执行 `xattr -d com.apple.quarantine sm2uploader-darwin` 75 | -------------------------------------------------------------------------------- /connector.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net" 7 | "time" 8 | ) 9 | 10 | const ( 11 | FILE_SIZE_MIN = 1 12 | FILE_SIZE_MAX = 2 << 30 // 2GB 13 | ) 14 | 15 | var ( 16 | errFileEmpty = errors.New("File is empty.") 17 | errFileTooLarge = errors.New("File is too large.") 18 | ) 19 | 20 | type Payload struct { 21 | File io.Reader 22 | Name string 23 | Size int64 24 | } 25 | 26 | func (p *Payload) SetName(name string) { 27 | p.Name = normalizedFilename(name) 28 | } 29 | 30 | func (p *Payload) ReadableSize() string { 31 | return humanReadableSize(p.Size) 32 | } 33 | 34 | func (p *Payload) GetContent(nofix bool) (cont []byte, err error) { 35 | if nofix || !p.ShouldBeFix() { 36 | cont, err = io.ReadAll(p.File) 37 | } else { 38 | cont, err = postProcess(p.File) 39 | p.Size = int64(len(cont)) 40 | } 41 | return cont, err 42 | } 43 | 44 | func (p *Payload) ShouldBeFix() bool { 45 | return shouldBeFix(p.Name) 46 | } 47 | 48 | func NewPayload(file io.Reader, name string, size int64) *Payload { 49 | return &Payload{ 50 | File: file, 51 | Name: normalizedFilename(name), 52 | Size: size, 53 | } 54 | } 55 | 56 | type connector struct { 57 | handlers []Handler 58 | } 59 | 60 | type Handler interface { 61 | Ping(*Printer) bool 62 | Connect() error 63 | Disconnect() error 64 | Upload(*Payload) error 65 | SetToolTemperature(int, int) error 66 | SetBedTemperature(int, int) error 67 | Home() error 68 | } 69 | 70 | func (c *connector) RegisterHandler(h Handler) { 71 | c.handlers = append(c.handlers, h) 72 | } 73 | 74 | // Upload to upload a file to a printer 75 | func (c *connector) Upload(printer *Printer, payload *Payload) error { 76 | // Iterate through all handlers 77 | for _, h := range c.handlers { 78 | // Check if handler can ping the printer 79 | if h.Ping(printer) { 80 | // Connect to the printer 81 | if err := h.Connect(); err != nil { 82 | return err 83 | } 84 | defer h.Disconnect() 85 | 86 | if payload.Size > FILE_SIZE_MAX { 87 | return errFileTooLarge 88 | } 89 | if payload.Size < FILE_SIZE_MIN { 90 | return errFileEmpty 91 | } 92 | // Upload the file to the printer 93 | if err := h.Upload(payload); err != nil { 94 | return err 95 | } 96 | 97 | // Return nil if successful 98 | return nil 99 | } 100 | } 101 | // Return error if printer is not available 102 | return errors.New("Printer " + printer.IP + " is not available.") 103 | } 104 | 105 | func (c *connector) PreHeatCommands(printer *Printer, tool_1_temperature int, tool_2_temperature int, bed_temperature int, home bool) error { 106 | // Iterate through all handlers 107 | for _, h := range c.handlers { 108 | // Check if handler can ping the printer 109 | if h.Ping(printer) { 110 | // Connect to the printer 111 | if err := h.Connect(); err != nil { 112 | return err 113 | } 114 | defer h.Disconnect() 115 | 116 | // Send the GCode command to the printer 117 | if tool_1_temperature > 0 { 118 | if err := h.SetToolTemperature(0, tool_1_temperature); err != nil { 119 | return err 120 | } 121 | } 122 | if tool_2_temperature > 0 { 123 | if err := h.SetToolTemperature(1, tool_2_temperature); err != nil { 124 | return err 125 | } 126 | } 127 | if bed_temperature > 0 { 128 | if err := h.SetBedTemperature(0, bed_temperature); err != nil { 129 | return err 130 | } 131 | if err := h.SetBedTemperature(1, bed_temperature); err != nil { 132 | return err 133 | } 134 | } 135 | if home { 136 | if err := h.Home(); err != nil { 137 | return err 138 | } 139 | } 140 | // Return nil if successful 141 | return nil 142 | } 143 | } 144 | // Return error if printer is not available 145 | return errors.New("Printer " + printer.IP + " is not available.") 146 | } 147 | 148 | var Connector = &connector{} 149 | 150 | // ping the printer to see if it is available 151 | func ping(ip string, port string, timeout int) bool { 152 | if timeout <= 0 { 153 | timeout = 2 154 | } 155 | conn, err := net.DialTimeout("tcp", net.JoinHostPort(ip, port), time.Second*time.Duration(timeout)) 156 | if err != nil { 157 | return false 158 | } 159 | defer conn.Close() 160 | return true 161 | } 162 | -------------------------------------------------------------------------------- /connector_http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/gosuri/uilive" 11 | "github.com/imroc/req/v3" 12 | ) 13 | 14 | const ( 15 | HTTPPort = "8080" 16 | HTTPTimeout = 5 17 | ) 18 | 19 | const ( 20 | AuthStatusApproved = 1 + iota 21 | AuthStatusDenied 22 | AuthStatusWaiting 23 | ) 24 | 25 | type HTTPConnector struct { 26 | client *req.Client 27 | printer *Printer 28 | } 29 | 30 | func (hc *HTTPConnector) Ping(p *Printer) bool { 31 | if p.Sacp { 32 | return false 33 | } 34 | if ping(p.IP, HTTPPort, 3) { 35 | hc.printer = p 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | func (hc *HTTPConnector) Connect() error { 42 | result := struct { 43 | Token string `json:"token"` 44 | }{} 45 | 46 | req := hc.request(). 47 | SetResult(&result). 48 | SetRetryCount(3). 49 | SetRetryFixedInterval(1 * time.Second). 50 | SetRetryCondition(func(r *req.Response, err error) bool { 51 | if Debug { 52 | log.Printf("-- retrying %s -> %d, token %s", r.Request.URL, r.StatusCode, hc.printer.Token) 53 | } 54 | 55 | // token expired 56 | if r.StatusCode == 403 && hc.printer.Token != "" { 57 | hc.printer.Token = "" 58 | // reconnect with no token to get new one 59 | return true 60 | } 61 | return false 62 | }) 63 | 64 | resp, err := req.Post(hc.URL("/connect")) 65 | if err != nil { 66 | return err 67 | } 68 | if resp.StatusCode == 200 { 69 | if hc.printer.Token != result.Token { 70 | hc.printer.Token = result.Token 71 | } 72 | tip := false 73 | for { 74 | switch hc.checkStatus() { 75 | case AuthStatusApproved: 76 | return nil 77 | case AuthStatusWaiting: 78 | if !tip { 79 | tip = true 80 | log.Println(">>> Please tap Yes on Snapmaker touchscreen to continue <<<") 81 | } 82 | // wait for auth on HMI 83 | <-time.After(2 * time.Second) 84 | case AuthStatusDenied: 85 | return fmt.Errorf("access denied") 86 | } 87 | } 88 | /* 89 | } else if resp.StatusCode == 403 && hc.printer.Token != "" { 90 | // token expired 91 | hc.printer.Token = "" 92 | // reconnect with no token to get new one 93 | return hc.Connect() 94 | */ 95 | } 96 | 97 | return fmt.Errorf("connect error %d", resp.StatusCode) 98 | } 99 | 100 | func (hc *HTTPConnector) Disconnect() (err error) { 101 | if hc.client != nil && hc.printer.Token != "" { 102 | _, err = hc.request().Post(hc.URL("/disconnect")) 103 | } 104 | return 105 | } 106 | 107 | func (hc *HTTPConnector) SetToolTemperature(tool int, temperature int) (err error) { 108 | // *** NOT IMPLEMENTED *** 109 | err = fmt.Errorf("not implemented") 110 | return 111 | } 112 | 113 | func (hc *HTTPConnector) SetBedTemperature(tool int, temperature int) (err error) { 114 | // *** NOT IMPLEMENTED *** 115 | err = fmt.Errorf("not implemented") 116 | return 117 | } 118 | 119 | func (hc *HTTPConnector) Home() (err error) { 120 | // *** NOT IMPLEMENTED *** 121 | err = fmt.Errorf("not implemented") 122 | return 123 | } 124 | 125 | func (hc *HTTPConnector) Upload(payload *Payload) (err error) { 126 | finished := make(chan empty, 1) 127 | defer func() { 128 | finished <- empty{} 129 | }() 130 | go func() { 131 | ticker := time.NewTicker(2 * time.Second) 132 | for { 133 | select { 134 | case <-ticker.C: 135 | hc.checkStatus() 136 | case <-finished: 137 | if Debug { 138 | log.Printf("-- heartbeat stopped") 139 | } 140 | ticker.Stop() 141 | return 142 | } 143 | } 144 | }() 145 | 146 | w := uilive.New() 147 | w.Start() 148 | log.SetOutput(w) 149 | defer func() { 150 | w.Stop() 151 | log.SetOutput(os.Stderr) 152 | }() 153 | 154 | file := req.FileUpload{ 155 | ParamName: "file", 156 | FileName: payload.Name, 157 | GetFileContent: func() (io.ReadCloser, error) { 158 | pr, pw := io.Pipe() 159 | go func() { 160 | defer pw.Close() 161 | content, err := payload.GetContent(NoFix) 162 | if !NoFix { 163 | log.SetOutput(os.Stderr) 164 | if err != nil { 165 | log.Printf("G-Code fix error(ignored): %s", err) 166 | } else if payload.ShouldBeFix() { 167 | log.Printf("G-Code fixed") 168 | } 169 | log.SetOutput(w) 170 | } 171 | pw.Write(content) 172 | }() 173 | return pr, nil 174 | }, 175 | FileSize: payload.Size, 176 | // ContentType: "application/octet-stream", 177 | } 178 | r := hc.request(0) 179 | r.SetFileUpload(file) 180 | r.SetUploadCallbackWithInterval(func(info req.UploadInfo) { 181 | if info.FileSize > 0 { 182 | perc := float64(info.UploadedSize) / float64(info.FileSize) * 100.0 183 | log.Printf(" - HTTP sending %.1f%%", perc) 184 | } else { 185 | log.Printf(" - HTTP sending %s...", humanReadableSize(info.UploadedSize)) 186 | } 187 | }, 35*time.Millisecond) 188 | 189 | _, err = r.Post(hc.URL("/upload")) 190 | return 191 | } 192 | 193 | func (hc *HTTPConnector) request(timeout ...int) *req.Request { 194 | to := HTTPTimeout 195 | if len(timeout) > 0 { 196 | to = timeout[0] 197 | } 198 | 199 | if hc.client == nil { 200 | hc.client = req.C() 201 | hc.client.DisableAllowGetMethodPayload() 202 | if Debug { 203 | hc.client.EnableDumpAllWithoutRequestBody() 204 | } 205 | } 206 | 207 | req := hc.client.SetTimeout(time.Second * time.Duration(to)).R() 208 | // for GET 209 | req.SetQueryParam("token", hc.printer.Token) 210 | // for POST 211 | req.SetFormData(map[string]string{"token": hc.printer.Token}) 212 | 213 | return req 214 | } 215 | 216 | func (hc *HTTPConnector) checkStatus() (status int) { 217 | r, err := hc.request().Get(hc.URL("/status")) 218 | if Debug { 219 | log.Printf("-- heartbeat: %d, err(%s)", r.StatusCode, err) 220 | } 221 | if err == nil { 222 | switch r.StatusCode { 223 | case 200: 224 | return AuthStatusApproved 225 | case 204: 226 | return AuthStatusWaiting 227 | // case 401: 228 | // return AuthStatusDenied 229 | // case 403: 230 | // if hc.printer.Token != "" { hc.printer.Token = ""} 231 | // return AuthStatusExpired 232 | } 233 | } 234 | return AuthStatusDenied 235 | } 236 | 237 | /* 238 | URL to make url with path 239 | */ 240 | func (hc *HTTPConnector) URL(path string) string { 241 | return fmt.Sprintf("http://%s:%s/api/v1%s", hc.printer.IP, HTTPPort, path) 242 | } 243 | 244 | func init() { 245 | Connector.RegisterHandler(&HTTPConnector{}) 246 | } 247 | -------------------------------------------------------------------------------- /connector_sacp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "time" 8 | 9 | "github.com/gosuri/uilive" 10 | ) 11 | 12 | const ( 13 | SACPPort = "8888" 14 | SACPTimeout = 5 15 | ) 16 | 17 | type SACPConnector struct { 18 | printer *Printer 19 | conn net.Conn 20 | } 21 | 22 | func (sc *SACPConnector) Ping(p *Printer) bool { 23 | // if !p.Sacp { 24 | // return false 25 | // } 26 | if ping(p.IP, SACPPort, 3) { 27 | sc.printer = p 28 | return true 29 | 30 | } 31 | return false 32 | } 33 | 34 | func (sc *SACPConnector) Connect() (err error) { 35 | conn, err := SACP_connect(sc.printer.IP, SACPTimeout*time.Second) 36 | if conn != nil { 37 | sc.conn = conn 38 | } 39 | return err 40 | } 41 | 42 | func (sc *SACPConnector) Disconnect() error { 43 | if sc.conn != nil { 44 | SACP_disconnect(sc.conn, SACPTimeout*time.Second) 45 | sc.conn.Close() 46 | } 47 | return nil 48 | } 49 | 50 | func (sc *SACPConnector) Upload(payload *Payload) (err error) { 51 | content, err := payload.GetContent(NoFix) 52 | if !NoFix { 53 | if err != nil { 54 | log.Printf("G-Code fix error(ignored): %s", err) 55 | } else if payload.ShouldBeFix() { 56 | log.Printf("G-Code fixed") 57 | } 58 | } 59 | 60 | w := uilive.New() 61 | w.Start() 62 | log.SetOutput(w) 63 | defer func() { 64 | w.Stop() 65 | log.SetOutput(os.Stderr) 66 | }() 67 | 68 | err = SACP_start_upload(sc.conn, payload.Name, content, SACPTimeout*time.Second) 69 | return 70 | } 71 | 72 | func (sc *SACPConnector) SetToolTemperature(tool_id int, temperature int) (err error) { 73 | err = SACP_set_tool_temperature(sc.conn, uint8(tool_id), uint16(temperature), SACPTimeout*time.Second) 74 | return 75 | } 76 | 77 | func (sc *SACPConnector) SetBedTemperature(tool_id int, temperature int) (err error) { 78 | err = SACP_set_bed_temperature(sc.conn, uint8(tool_id), uint16(temperature), SACPTimeout*time.Second) 79 | return 80 | } 81 | 82 | func (sc *SACPConnector) Home() (err error) { 83 | err = SACP_home(sc.conn, SACPTimeout*time.Second) 84 | return 85 | } 86 | 87 | func init() { 88 | Connector.RegisterHandler(&SACPConnector{}) 89 | } 90 | -------------------------------------------------------------------------------- /discover.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "log" 7 | "net" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | /* Discover discovers printers on the network. It returns a slice of 13 | * pointers to Printer objects. If no printers are found, it returns 14 | * an empty slice. If an error occurs, it returns nil. 15 | */ 16 | func Discover(timeout time.Duration) ([]*Printer, error) { 17 | var ( 18 | mu = sync.Mutex{} 19 | // Create a slice to hold the printers 20 | printers = []*Printer{} 21 | ) 22 | 23 | addrs, err := getBroadcastAddresses() 24 | if err != nil { 25 | return printers, err 26 | } 27 | 28 | discoverPrinter := func(addr string) error { 29 | // Create a new UDP broadcast address 30 | broadcastAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", addr, 20054)) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | // Create a new UDP connection 36 | conn, err := net.ListenUDP("udp4", nil) 37 | if err != nil { 38 | return err 39 | } 40 | defer conn.Close() 41 | 42 | if Debug { 43 | log.Printf("-- Discovering on %s", broadcastAddr) 44 | } 45 | 46 | // Set a timeout for the connection 47 | conn.SetDeadline(time.Now().Add(timeout)) 48 | 49 | // Send the discover message 50 | _, err = conn.WriteTo([]byte("discover"), broadcastAddr) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // Create a buffer to hold the response 56 | buf := make([]byte, 1500) 57 | 58 | // Loop until the timeout is reached 59 | for { 60 | // Read the response 61 | n, _, err := conn.ReadFromUDP(buf) 62 | if err != nil { 63 | // If the error is a timeout, break out of the loop 64 | if err, ok := err.(net.Error); ok && err.Timeout() { 65 | break 66 | } 67 | return err 68 | } 69 | 70 | if Debug { 71 | log.Printf("-- Discover got %d bytes %s", n, buf[:n]) 72 | } 73 | 74 | // Parse the response into a Printer object 75 | printer, err := NewPrinter(buf[:n]) 76 | if err != nil { 77 | continue 78 | } 79 | 80 | // Add the printer to the slice 81 | mu.Lock() 82 | printers = append(printers, printer) 83 | mu.Unlock() 84 | } 85 | return nil 86 | } 87 | 88 | var wg sync.WaitGroup 89 | for _, addr := range addrs { 90 | wg.Add(1) 91 | go func(addr string) { 92 | defer wg.Done() 93 | err := discoverPrinter(addr) 94 | if err != nil { 95 | log.Printf("Error discovering on %s: %v", addr, err) 96 | } 97 | }(addr) 98 | } 99 | wg.Wait() 100 | 101 | // Return the slice of printers 102 | return printers, nil 103 | } 104 | 105 | func getBroadcastAddresses() ([]string, error) { 106 | ifs, err := net.Interfaces() 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | addrMap := map[string]bool{} 112 | for _, iface := range ifs { 113 | addrs, err := iface.Addrs() 114 | if err != nil { 115 | continue 116 | } 117 | for _, addr := range addrs { 118 | if n, ok := addr.(*net.IPNet); ok && !n.IP.IsLoopback() { 119 | if v4addr := n.IP.To4(); v4addr != nil { 120 | // convert all parts of the masked bits to its maximum value 121 | // by converting the address into a 32 bit integer and then 122 | // ORing it with the inverted mask 123 | baddr := make(net.IP, len(v4addr)) 124 | binary.BigEndian.PutUint32(baddr, binary.BigEndian.Uint32(v4addr)|^binary.BigEndian.Uint32(n.IP.DefaultMask())) 125 | if s := baddr.String(); !addrMap[s] { 126 | addrMap[s] = true 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | addrs := make([]string, 0, len(addrMap)) 134 | for addr := range addrMap { 135 | addrs = append(addrs, addr) 136 | } 137 | return addrs, nil 138 | } 139 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | var ( 11 | Version = "dev" 12 | ) 13 | 14 | func flag_usage() { 15 | ex, _ := os.Executable() 16 | usage := `%s [options] file1.gcode file2.nc ... 17 | 18 | %s 19 | 20 | Options: 21 | ` 22 | fmt.Printf(usage, filepath.Base(ex), Version) 23 | flag.PrintDefaults() 24 | os.Exit(1) 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/macdylan/sm2uploader 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gosuri/uilive v0.0.4 7 | github.com/imroc/req/v3 v3.11.0 8 | github.com/macdylan/SMFix/fix v0.0.0-20240823141528-a02aee6e72f0 9 | github.com/manifoldco/promptui v0.9.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/chzyer/readline v1.5.1 // indirect 15 | github.com/hashicorp/errwrap v1.1.0 // indirect 16 | github.com/hashicorp/go-multierror v1.1.1 // indirect 17 | github.com/kr/pretty v0.3.1 // indirect 18 | github.com/mattn/go-isatty v0.0.19 // indirect 19 | golang.org/x/net v0.11.0 // indirect 20 | golang.org/x/sys v0.10.0 // indirect 21 | golang.org/x/text v0.10.0 // indirect 22 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 3 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 6 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 8 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 9 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= 12 | github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI= 13 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 14 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 15 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 16 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 17 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 18 | github.com/imroc/req/v3 v3.11.0 h1:s2+GYIdzd20i4bB1ZWncsRx+x7wcy3f6cpDIbR1P6ro= 19 | github.com/imroc/req/v3 v3.11.0/go.mod h1:G6fkq27P+JcTcgRVxecxY+amHN1xFl8W81eLCfJ151M= 20 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 21 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 22 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 23 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 24 | github.com/macdylan/SMFix/fix v0.0.0-20240823141528-a02aee6e72f0 h1:AwXODRLB2f2TA5k2z//F3Zyln+x65gBFPL3iEku1oDA= 25 | github.com/macdylan/SMFix/fix v0.0.0-20240823141528-a02aee6e72f0/go.mod h1:dnB1MevhW7tICqBpQ2aHpVClwLdmSBUNmfV7jpRmiWw= 26 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 27 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 28 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 29 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 30 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 31 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 32 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 33 | golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 34 | golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= 35 | golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= 36 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 42 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 44 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 45 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 46 | golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= 47 | golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 48 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 50 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 51 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 53 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /localstorage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | type LocalStorage struct { 10 | Printers []*Printer `yaml:"printers"` 11 | savePath string 12 | } 13 | 14 | func NewLocalStorage(savePath string) *LocalStorage { 15 | s := &LocalStorage{ 16 | Printers: []*Printer{}, 17 | savePath: savePath, 18 | } 19 | 20 | if b, err := os.ReadFile(savePath); err == nil { 21 | yaml.Unmarshal(b, &s) 22 | } 23 | 24 | return s 25 | } 26 | 27 | // Add to add printers to LocalStorage 28 | func (ls *LocalStorage) Add(printers ...*Printer) { 29 | // Iterate over each printer 30 | for _, p := range printers { 31 | // Skip if printer ID is empty 32 | if p.ID == "" { 33 | continue 34 | } 35 | // Iterate over each printer in LocalStorage 36 | for idx, x := range ls.Printers { 37 | // If printer ID matches, update IP and Token if necessary 38 | if x.ID == p.ID { 39 | if x.IP != p.IP { 40 | ls.Printers[idx].IP = p.IP 41 | } 42 | if p.Token != "" && x.Token != p.Token { 43 | ls.Printers[idx].Token = p.Token 44 | } 45 | // Go to exists label 46 | goto exists 47 | } 48 | } 49 | // Append printer to LocalStorage 50 | ls.Printers = append(ls.Printers, p) 51 | // Label for when printer already exists 52 | exists: 53 | } 54 | } 55 | 56 | func (ls *LocalStorage) Save() (err error) { 57 | if b, err := yaml.Marshal(ls); err == nil { 58 | return os.WriteFile(ls.savePath, b, 0644) 59 | } 60 | return 61 | } 62 | 63 | func (ls *LocalStorage) Find(host string) *Printer { 64 | for _, p := range ls.Printers { 65 | if p.ID == host || p.IP == host { 66 | return p 67 | } 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/manifoldco/promptui" 13 | ) 14 | 15 | var ( 16 | Host string 17 | KnownHosts string 18 | DiscoverTimeout time.Duration 19 | OctoPrintListenAddr string 20 | Tool1Temperature int 21 | Tool2Temperature int 22 | BedTemperature int 23 | Home bool 24 | NoFix bool 25 | Debug bool 26 | 27 | _Payloads []*Payload 28 | SmFixExtensions = map[string]bool{ 29 | ".gcode": true, 30 | ".nc": false, 31 | ".cnc": false, 32 | ".bin": false, 33 | } 34 | ) 35 | 36 | func main() { 37 | defer func() { 38 | if r := recover(); r != nil { 39 | os.Exit(2) 40 | } 41 | }() 42 | 43 | // 获取程序所在目录 - Get the directory where the program is located 44 | ex, _ := os.Executable() 45 | dir, err := filepath.Abs(filepath.Dir(ex)) 46 | if err != nil { 47 | log.Panicln(err) 48 | } 49 | defaultKnownHosts := filepath.Join(dir, "hosts.yaml") 50 | if envKnownhosts := os.Getenv("KNOWN_HOSTS"); envKnownhosts != "" { 51 | defaultKnownHosts = envKnownhosts 52 | } 53 | 54 | flag.StringVar(&Host, "host", os.Getenv("HOST"), "upload to host(id/ip/hostname), not required.") 55 | flag.StringVar(&KnownHosts, "knownhosts", defaultKnownHosts, "known hosts") 56 | flag.StringVar(&OctoPrintListenAddr, "octoprint", os.Getenv("OCTOPRINT"), "octoprint listen address, e.g. '-octoprint :8844' then you can upload files to printer by http://localhost:8844") 57 | flag.IntVar(&Tool1Temperature, "tool1", parseIntEnv("TOOL1", 0), "set the temperature (preheat) of tool 1") 58 | flag.IntVar(&Tool2Temperature, "tool2", parseIntEnv("TOOL2", 0), "set the temperature (preheat) of tool 2") 59 | flag.IntVar(&BedTemperature, "bed", parseIntEnv("BED", 0), "set the temperature (preheat) of bed") 60 | flag.BoolVar(&Home, "home", parseBoolEnv("HOME", false), "home the printer") 61 | flag.DurationVar(&DiscoverTimeout, "timeout", parseDurationEnv("TIMEOUT", 4*time.Second), "printer discovery timeout") 62 | flag.BoolVar(&NoFix, "nofix", parseBoolEnv("NOFIX", false), "disable SMFix(built-in)") 63 | flag.BoolVar(&Debug, "debug", parseBoolEnv("DEBUG", false), "debug mode") 64 | 65 | flag.Usage = flag_usage 66 | flag.Parse() 67 | 68 | if Debug { 69 | log.Printf("-- Debug mode: %s", Version) 70 | } 71 | 72 | if NoFix { 73 | log.Println("smfix disabled") 74 | } 75 | 76 | var printer *Printer 77 | ls := NewLocalStorage(KnownHosts) 78 | defer func() { 79 | if printer != nil { 80 | // update printer's token 81 | ls.Add(printer) 82 | if Debug { 83 | log.Printf("-- Updated printer: %s", printer.String()) 84 | } 85 | } 86 | if err := ls.Save(); err == nil && Debug { 87 | log.Printf("-- Saved known hosts: %s", KnownHosts) 88 | } 89 | }() 90 | 91 | // Check if host is specified 92 | printer = ls.Find(Host) 93 | if printer != nil { 94 | log.Println("Found printer in " + KnownHosts) 95 | } 96 | 97 | // Discover printers 98 | if printer == nil { 99 | log.Println("Discovering ...") 100 | if printers, err := Discover(DiscoverTimeout); err == nil { 101 | if Debug { 102 | log.Printf("-- Discovered %d printers", len(printers)) 103 | } 104 | ls.Add(printers...) 105 | } else if Debug { 106 | log.Printf("-- Discover error: %s", err.Error()) 107 | } 108 | printer = ls.Find(Host) 109 | if printer != nil { 110 | log.Printf("Found printer: %s", printer.String()) 111 | } 112 | } 113 | 114 | if printer == nil { 115 | if Host == "" { 116 | // Prompt user to select a printer 117 | printers := ls.Printers 118 | if len(printers) == 0 { 119 | log.Panicln("No printers found") 120 | } 121 | if len(printers) > 1 { 122 | prompt := promptui.Select{ 123 | Label: "Select a printer", 124 | Items: printers, 125 | } 126 | idx, _, err := prompt.Run() 127 | if err != nil { 128 | log.Panicln(err) 129 | } 130 | printer = printers[idx] 131 | } else { 132 | printer = printers[0] 133 | } 134 | } else { 135 | // directly to printer using ip/hostname 136 | printer = &Printer{IP: Host} 137 | } 138 | } 139 | 140 | log.Println("Printer IP:", printer.IP) 141 | if printer.Model != "" { 142 | log.Println("Printer Model:", printer.Model) 143 | } 144 | 145 | // Create a channel to listen for signals 146 | sc := make(chan os.Signal, 1) 147 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 148 | go func() { 149 | sig := <-sc 150 | log.Printf("Received signal: %s", sig) 151 | // update printer's token 152 | if printer != nil { 153 | ls.Add(printer) 154 | if Debug { 155 | log.Printf("-- Updated printer: %s", printer.String()) 156 | } 157 | } 158 | if err := ls.Save(); err == nil && Debug { 159 | log.Printf("-- Saved known hosts: %s", KnownHosts) 160 | } 161 | os.Exit(0) 162 | }() 163 | 164 | if OctoPrintListenAddr != "" { 165 | // listen for octoprint uploads 166 | if err := startOctoPrintServer(OctoPrintListenAddr, printer); err != nil { 167 | log.Panic(err) 168 | } 169 | return 170 | } 171 | 172 | preheating := Tool1Temperature != 0 || Tool2Temperature != 0 || BedTemperature != 0 || Home 173 | if preheating { 174 | log.Println("Preheating...") 175 | if err := Connector.PreHeatCommands(printer, Tool1Temperature, Tool2Temperature, BedTemperature, Home); err != nil { 176 | log.Panic(err) 177 | } 178 | } 179 | 180 | // 检查文件参数是否存在 - Check if the file parameter exists 181 | for _, file := range flag.Args() { 182 | if st, err := os.Stat(file); os.IsNotExist(err) { 183 | log.Panicf("File %s does not exist\n", file) 184 | } else { 185 | f, _ := os.Open(file) 186 | _Payloads = append(_Payloads, NewPayload(f, st.Name(), st.Size())) 187 | } 188 | } 189 | 190 | // 检查是否有传入的文件 - Check if a file has been passed in 191 | if len(_Payloads) == 0 { 192 | if !preheating { 193 | log.Panicln("No input files") 194 | } 195 | } 196 | 197 | // 从 slic3r 环境变量中获取文件名 198 | envFilename := os.Getenv("SLIC3R_PP_OUTPUT_NAME") 199 | 200 | // Upload files to host 201 | for _, p := range _Payloads { 202 | if envFilename != "" { 203 | p.SetName(filepath.Base(envFilename)) 204 | } 205 | 206 | log.Printf("Uploading file '%s' [%s]...", p.Name, p.ReadableSize()) 207 | if err := Connector.Upload(printer, p); err != nil { 208 | log.Panicln(err) 209 | } else { 210 | log.Println("Upload finished.") 211 | <-time.After(time.Second * 1) // HMI needs some time to refresh 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /octoprint.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | "runtime" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const ( 15 | maxMemory = 64 << 20 // 64MB 16 | ) 17 | 18 | var ( 19 | noTrim = false 20 | noShutoff = false 21 | // noPreheat = false 22 | // noReinforceTower = false 23 | noReplaceTool = false 24 | ) 25 | 26 | type stats struct { 27 | start time.Time 28 | memory uint64 29 | success uint 30 | failure uint 31 | lastSuccess *last 32 | lastFailure *last 33 | } 34 | 35 | type last struct { 36 | filaname string 37 | size int64 38 | time time.Time 39 | } 40 | 41 | func (s *stats) addSuccess(filaname string, size int64) { 42 | s.success++ 43 | s.lastSuccess = &last{ 44 | filaname: normalizedFilename(filaname), 45 | size: size, 46 | time: time.Now(), 47 | } 48 | } 49 | 50 | func (s *stats) addFailure(filaname string, size int64) { 51 | s.failure++ 52 | s.lastFailure = &last{ 53 | filaname: normalizedFilename(filaname), 54 | size: size, 55 | time: time.Now(), 56 | } 57 | } 58 | 59 | func (s *stats) String() string { 60 | var mem runtime.MemStats 61 | runtime.ReadMemStats(&mem) 62 | s.memory = mem.Alloc 63 | 64 | buf := bytes.Buffer{} 65 | buf.WriteString("memory alloc: " + humanReadableSize(int64(s.memory)) + "\n") 66 | buf.WriteString("uptime: " + time.Since(s.start).String() + "\n") 67 | buf.WriteString(fmt.Sprintf("success: %d, failure: %d\n", s.success, s.failure)) 68 | buf.WriteString(fmt.Sprintf("last success: %s\n - %s (%s)\n", s.lastSuccess.time.Format(time.RFC3339), s.lastSuccess.filaname, humanReadableSize(s.lastSuccess.size))) 69 | buf.WriteString(fmt.Sprintf("last failure: %s\n - %s (%s)\n", s.lastFailure.time.Format(time.RFC3339), s.lastFailure.filaname, humanReadableSize(s.lastFailure.size))) 70 | return buf.String() 71 | } 72 | 73 | func LoggingMiddleware(next http.Handler) http.Handler { 74 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 75 | start := time.Now() 76 | defer func() { 77 | log.Printf("Request %s %s completed in %v", r.Method, r.URL.Path, time.Since(start)) 78 | }() 79 | next.ServeHTTP(w, r) 80 | }) 81 | } 82 | 83 | func startOctoPrintServer(listenAddr string, printer *Printer) error { 84 | var ( 85 | _stats *stats 86 | mux = http.NewServeMux() 87 | ) 88 | 89 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 90 | protocol := "HTTP" 91 | if printer.Sacp { 92 | protocol = "SACP" 93 | } 94 | resp := `sm2uploader ` + Version + ` - https://github.com/macdylan/sm2uploader` + "\n\n" + 95 | ` printer id: ` + printer.ID + "\n" + 96 | ` printer ip: ` + printer.IP + "\n" + 97 | ` protocol: ` + protocol + "\n\n" + 98 | _stats.String() 99 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 100 | writeResponse(w, http.StatusOK, resp) 101 | }) 102 | 103 | mux.HandleFunc("/api/version", func(w http.ResponseWriter, r *http.Request) { 104 | respVersion := `{"api": "0.1", "server": "1.2.3", "text": "OctoPrint 1.2.3/Dummy"}` 105 | writeResponse(w, http.StatusOK, respVersion) 106 | }) 107 | 108 | mux.HandleFunc("/api/files/local", func(w http.ResponseWriter, r *http.Request) { 109 | // Check if request is a POST request 110 | if r.Method != http.MethodPost { 111 | methodNotAllowedResponse(w, r.Method) 112 | return 113 | } 114 | 115 | err := r.ParseMultipartForm(maxMemory) 116 | if err != nil { 117 | internalServerErrorResponse(w, err.Error()) 118 | return 119 | } 120 | 121 | // Retrieve the uploaded file 122 | file, fd, err := r.FormFile("file") 123 | if err != nil { 124 | bedRequestResponse(w, err.Error()) 125 | return 126 | } 127 | defer file.Close() 128 | 129 | // read X-Api-Key header 130 | apiKey := r.Header.Get("X-Api-Key") 131 | if len(apiKey) > 5 { 132 | argumentsFromApi(apiKey) 133 | } 134 | 135 | // Send the stream to the printer 136 | payload := NewPayload(file, fd.Filename, fd.Size) 137 | if err := Connector.Upload(printer, payload); err != nil { 138 | _stats.addFailure(payload.Name, payload.Size) 139 | internalServerErrorResponse(w, err.Error()) 140 | return 141 | } 142 | 143 | _stats.addSuccess(payload.Name, payload.Size) 144 | 145 | log.Printf("Upload finished: %s [%s]", fd.Filename, payload.ReadableSize()) 146 | 147 | // Return success response 148 | writeResponse(w, http.StatusOK, `{"done": true}`) 149 | }) 150 | 151 | handler := LoggingMiddleware(mux) 152 | log.Printf("Starting OctoPrint server on %s ...", listenAddr) 153 | 154 | // Create a listener 155 | listener, err := net.Listen("tcp", listenAddr) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | _stats = &stats{ 161 | start: time.Now(), 162 | success: 0, 163 | failure: 0, 164 | lastSuccess: &last{ 165 | filaname: "", 166 | size: 0, 167 | time: time.Now(), 168 | }, 169 | lastFailure: &last{ 170 | filaname: "", 171 | size: 0, 172 | time: time.Now(), 173 | }, 174 | } 175 | 176 | log.Printf("Server started, now you can upload files to http://%s", listener.Addr().String()) 177 | // Start the server 178 | return http.Serve(listener, handler) 179 | } 180 | 181 | func writeResponse(w http.ResponseWriter, status int, body string) { 182 | if has := w.Header().Get("Content-Type"); has == "" { 183 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 184 | } 185 | w.WriteHeader(status) 186 | w.Write([]byte(body)) 187 | } 188 | 189 | func methodNotAllowedResponse(w http.ResponseWriter, method string) { 190 | log.Print("Method not allowed: ", method) 191 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 192 | } 193 | 194 | func internalServerErrorResponse(w http.ResponseWriter, err string) { 195 | log.Print("Internal server error: ", err) 196 | http.Error(w, err, http.StatusInternalServerError) 197 | } 198 | 199 | func bedRequestResponse(w http.ResponseWriter, err string) { 200 | log.Print("Bad request: ", err) 201 | http.Error(w, err, http.StatusBadRequest) 202 | } 203 | 204 | func argumentsFromApi(str string) { 205 | noTrim = strings.Contains(str, "notrim") 206 | // noPreheat = strings.Contains(str, "nopreheat") 207 | noShutoff = strings.Contains(str, "noshutoff") 208 | // noReinforceTower = strings.Contains(str, "noreinforcetower") 209 | noReplaceTool = strings.Contains(str, "noreplacetool") 210 | msg := []string{} 211 | if noTrim { 212 | msg = append(msg, "-notrim") 213 | } 214 | // if noPreheat { 215 | // msg = append(msg, "-nopreheat") 216 | // } 217 | if noShutoff { 218 | msg = append(msg, "-noshutoff") 219 | } 220 | // if noReinforceTower { 221 | // msg = append(msg, "-noreinforcetower") 222 | // } 223 | if noReplaceTool { 224 | msg = append(msg, "-noreplacetool") 225 | } 226 | if len(msg) > 0 { 227 | log.Printf("SMFix with args: %s", strings.Join(msg, " ")) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /printer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type Printer struct { 10 | IP string `yaml:"ip"` 11 | ID string `yaml:"id"` 12 | Model string `yaml:"model"` 13 | Token string `yaml:"token"` 14 | Sacp bool `yaml:"sacp"` 15 | } 16 | 17 | /* 18 | NewPrinter create new printer from response 19 | Snapmaker J1X123P@192.168.1.201|model:Snapmaker J1|status:IDLE|SACP:1 20 | */ 21 | func NewPrinter(resp []byte) (*Printer, error) { 22 | msg := string(resp) 23 | if !strings.Contains(msg, "|model:") || !strings.Contains(msg, "@") { 24 | return nil, errors.New("invalid response") 25 | } 26 | var ( 27 | parts = strings.Split(msg, "|") 28 | id = parts[0][:strings.LastIndex(parts[0], "@")] 29 | ip = parts[0][strings.LastIndex(parts[0], "@")+1:] 30 | model = parts[1][strings.Index(parts[1], ":")+1:] 31 | sacp = strings.Contains(msg, "SACP:1") 32 | ) 33 | 34 | return &Printer{ 35 | IP: ip, 36 | ID: id, 37 | Model: model, 38 | Token: "", 39 | Sacp: sacp, 40 | }, nil 41 | } 42 | 43 | /* Name for promptui */ 44 | func (p *Printer) String() string { 45 | return fmt.Sprintf("%s@%s - %s", p.ID, p.IP, p.Model) 46 | } 47 | -------------------------------------------------------------------------------- /sacp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Author: https://github.com/kanocz 3 | */ 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "crypto/md5" 9 | "encoding/binary" 10 | "encoding/hex" 11 | "errors" 12 | "io" 13 | "log" 14 | "net" 15 | "time" 16 | ) 17 | 18 | const ( 19 | SACP_data_len = 60 * 1024 // just as defined in original python code 20 | ) 21 | 22 | var ( 23 | errInvalidSACP = errors.New("data doesn't look like SACP packet") 24 | errInvalidSACPVer = errors.New("SACP version missmatch") 25 | errInvalidChksum = errors.New("SACP checksum doesn't match data") 26 | errInvalidSize = errors.New("SACP package is too short") 27 | ) 28 | 29 | type SACP_pack struct { 30 | // 0xAA byte 31 | // 0x55 byte 32 | // DataLength uint16 33 | // 0x01 (SACP version) 34 | ReceiverID byte 35 | // head_chksum byte 36 | SenderID byte 37 | Attribute byte 38 | Sequence uint16 39 | CommandSet byte 40 | CommandID byte 41 | Data []byte 42 | // data_checksum uint16 43 | } 44 | 45 | func (sacp SACP_pack) Encode() []byte { 46 | result := make([]byte, 15+len(sacp.Data)) 47 | 48 | result[0] = 0xAA 49 | result[1] = 0x55 50 | binary.LittleEndian.PutUint16(result[2:4], uint16(len(sacp.Data)+6+2)) 51 | result[4] = 0x01 52 | result[5] = sacp.ReceiverID 53 | result[6] = sacp.headChksum(result[:6]) 54 | result[7] = sacp.SenderID 55 | result[8] = sacp.Attribute 56 | binary.LittleEndian.PutUint16(result[9:11], sacp.Sequence) 57 | result[11] = sacp.CommandSet 58 | result[12] = sacp.CommandID 59 | 60 | if len(sacp.Data) > 0 { // this also include check on nil 61 | copy(result[13:], sacp.Data) 62 | } 63 | 64 | binary.LittleEndian.PutUint16(result[len(result)-2:], sacp.U16Chksum(result[7:], uint16(len(sacp.Data))+6)) 65 | 66 | return result[:] 67 | } 68 | 69 | func (sacp *SACP_pack) Decode(data []byte) error { 70 | if len(data) < 13 { 71 | return errInvalidSize 72 | } 73 | if data[0] != 0xAA && data[1] != 0x55 { 74 | return errInvalidSACP 75 | } 76 | dataLen := binary.LittleEndian.Uint16(data[2:4]) 77 | if dataLen != uint16(len(data)-7) { 78 | return errInvalidSize 79 | } 80 | if data[4] != 0x01 { 81 | return errInvalidSACPVer 82 | } 83 | if sacp.headChksum(data[:6]) != data[6] { 84 | return errInvalidChksum 85 | } 86 | if binary.LittleEndian.Uint16(data[len(data)-2:]) != sacp.U16Chksum(data[7:], dataLen-2) { 87 | return errInvalidChksum 88 | } 89 | 90 | sacp.ReceiverID = data[5] 91 | sacp.SenderID = data[7] 92 | sacp.Attribute = data[8] 93 | sacp.Sequence = binary.LittleEndian.Uint16(data[9:11]) 94 | sacp.CommandSet = data[11] 95 | sacp.CommandID = data[12] 96 | sacp.Data = data[13 : len(data)-2] 97 | 98 | return nil 99 | } 100 | 101 | func (sacp *SACP_pack) headChksum(data []byte) byte { 102 | crc := byte(0) 103 | poly := byte(7) 104 | for i := 0; i < len(data); i++ { 105 | for j := 0; j < 8; j++ { 106 | bit := ((data[i] & 0xff) >> (7 - j) & 0x01) == 1 107 | c07 := (crc >> 7 & 0x01) == 1 108 | crc = crc << 1 109 | if (!c07 && bit) || (c07 && !bit) { 110 | crc ^= poly 111 | } 112 | } 113 | } 114 | crc = crc & 0xff 115 | return crc 116 | } 117 | 118 | func (sacp *SACP_pack) U16Chksum(package_data []byte, length uint16) uint16 { 119 | check_num := uint32(0) 120 | if length > 0 { 121 | for i := 0; i < int(length-1); i += 2 { 122 | check_num += uint32((uint32(package_data[i])&0xff)<<8 | uint32(package_data[i+1])&0xff) 123 | check_num &= 0xffffffff 124 | } 125 | if length%2 != 0 { 126 | check_num += uint32(package_data[length-1]) 127 | } 128 | } 129 | for check_num > 0xFFFF { 130 | check_num = ((check_num >> 16) & 0xFFFF) + (check_num & 0xFFFF) 131 | } 132 | check_num = ^check_num 133 | return uint16(check_num & 0xFFFF) 134 | } 135 | 136 | func writeSACPstring(w io.Writer, s string) { 137 | binary.Write(w, binary.LittleEndian, uint16(len(s))) 138 | w.Write([]byte(s)) 139 | } 140 | 141 | func writeSACPbytes(w io.Writer, s []byte) { 142 | binary.Write(w, binary.LittleEndian, uint16(len(s))) 143 | w.Write(s) 144 | } 145 | 146 | func writeLE[T any](w io.Writer, u T) { 147 | binary.Write(w, binary.LittleEndian, u) 148 | } 149 | 150 | func SACP_connect(ip string, timeout time.Duration) (net.Conn, error) { 151 | conn, err := net.Dial("tcp4", ip+":8888") 152 | if err != nil { 153 | // log.Printf("Error connecting to %s: %v", ip, err) 154 | return nil, err 155 | } 156 | 157 | conn.SetWriteDeadline(time.Now().Add(timeout)) 158 | _, err = conn.Write(SACP_pack{ 159 | ReceiverID: 2, 160 | SenderID: 0, 161 | Attribute: 0, 162 | Sequence: 1, 163 | CommandSet: 0x01, 164 | CommandID: 0x05, 165 | Data: []byte{ 166 | 11, 0, 's', 'm', '2', 'u', 'p', 'l', 'o', 'a', 'd', 'e', 'r', 167 | 0, 0, 168 | 0, 0, 169 | }, 170 | }.Encode()) 171 | 172 | if err != nil { 173 | // log.Println("Error writing \"hello\": ", err) 174 | conn.Close() 175 | return nil, err 176 | } 177 | 178 | for { 179 | p, err := SACP_read(conn, timeout) 180 | if err != nil || p == nil { 181 | // log.Println("Error reading \"hello\" responce: ", err) 182 | conn.Close() 183 | return nil, err 184 | } 185 | 186 | if Debug { 187 | log.Printf("-- SACP_connect got:\n%v", p) 188 | } 189 | 190 | if p.CommandSet == 1 && p.CommandID == 5 { 191 | break 192 | } 193 | } 194 | 195 | if Debug { 196 | log.Println("-- Connected to printer") 197 | } 198 | 199 | return conn, nil 200 | } 201 | 202 | func SACP_read(conn net.Conn, timeout time.Duration) (*SACP_pack, error) { 203 | var buf [SACP_data_len + 15]byte 204 | 205 | deadline := time.Now().Add(timeout) 206 | conn.SetReadDeadline(deadline) 207 | 208 | n, err := conn.Read(buf[:4]) 209 | if err != nil { 210 | return nil, err 211 | } 212 | if n != 4 { 213 | return nil, errInvalidSize 214 | } 215 | 216 | dataLen := binary.LittleEndian.Uint16(buf[2:4]) 217 | n, err = conn.Read(buf[4 : dataLen+7]) 218 | if err != nil { 219 | return nil, err 220 | } 221 | if n != int(dataLen+3) { 222 | return nil, errInvalidSize 223 | } 224 | 225 | var sacp SACP_pack 226 | err = sacp.Decode(buf[:dataLen+7]) 227 | 228 | return &sacp, err 229 | } 230 | 231 | var sequence uint16 = 2 232 | 233 | func SACP_set_tool_temperature(conn net.Conn, tool_id uint8, temperature uint16, timeout time.Duration) error { 234 | data := bytes.Buffer{} 235 | 236 | data.WriteByte(0x08) 237 | 238 | // Tool ID, starting at 0x00 239 | data.WriteByte(tool_id) 240 | 241 | // Temperature 242 | writeLE(&data, uint16(temperature)) 243 | 244 | return SACP_send_command(conn, 0x10, 0x02, data, timeout) 245 | } 246 | 247 | func SACP_set_bed_temperature(conn net.Conn, tool_id uint8, temperature uint16, timeout time.Duration) error { 248 | data := bytes.Buffer{} 249 | 250 | data.WriteByte(0x05) 251 | 252 | // Tool ID, starting at 0x00 253 | data.WriteByte(tool_id) 254 | 255 | // Temperature 256 | writeLE(&data, uint16(temperature)) 257 | 258 | return SACP_send_command(conn, 0x14, 0x02, data, timeout) 259 | } 260 | 261 | func SACP_home(conn net.Conn, timeout time.Duration) error { 262 | data := bytes.Buffer{} 263 | data.WriteByte(0x00) 264 | 265 | // 0x31 is also used when homing in Luban??? 266 | // 0x35 homes everything 267 | return SACP_send_command(conn, 0x01, 0x35, data, timeout) 268 | } 269 | 270 | func SACP_send_command(conn net.Conn, command_set uint8, command_id uint8, data bytes.Buffer, timeout time.Duration) error { 271 | 272 | sequence++ 273 | 274 | conn.SetWriteDeadline(time.Now().Add(timeout)) 275 | _, err := conn.Write(SACP_pack{ 276 | ReceiverID: 1, 277 | SenderID: 0, 278 | Attribute: 0, 279 | Sequence: sequence, 280 | CommandSet: command_set, 281 | CommandID: command_id, 282 | Data: data.Bytes(), 283 | }.Encode()) 284 | 285 | if err != nil { 286 | return err 287 | } 288 | 289 | if Debug { 290 | log.Printf("-- Sequence: %d Sent GCode: %x", sequence, data.Bytes()) 291 | } 292 | 293 | for { 294 | conn.SetReadDeadline(time.Now().Add(timeout)) 295 | p, err := SACP_read(conn, timeout) 296 | if err != nil { 297 | return err 298 | } 299 | 300 | if Debug { 301 | log.Printf("-- Got reply from printer: %v", p) 302 | } 303 | 304 | if p.Sequence == sequence && p.CommandSet == command_set && p.CommandID == command_id { 305 | if len(p.Data) == 1 && p.Data[0] == 0 { 306 | return nil 307 | } 308 | } 309 | } 310 | } 311 | 312 | func SACP_start_upload(conn net.Conn, filename string, gcode []byte, timeout time.Duration) error { 313 | // prepare data for upload begin packet 314 | package_count := uint16((len(gcode) / SACP_data_len) + 1) 315 | md5hash := md5.Sum(gcode) 316 | 317 | data := bytes.Buffer{} 318 | 319 | writeSACPstring(&data, filename) 320 | writeLE(&data, uint32(len(gcode))) 321 | writeLE(&data, package_count) 322 | writeSACPstring(&data, hex.EncodeToString(md5hash[:])) 323 | 324 | if Debug { 325 | log.Println("-- Starting upload ...") 326 | } 327 | 328 | conn.SetWriteDeadline(time.Now().Add(timeout)) 329 | _, err := conn.Write(SACP_pack{ 330 | ReceiverID: 2, 331 | SenderID: 0, 332 | Attribute: 0, 333 | Sequence: 1, 334 | CommandSet: 0xb0, 335 | CommandID: 0x00, 336 | Data: data.Bytes(), 337 | }.Encode()) 338 | 339 | if err != nil { 340 | return err 341 | } 342 | 343 | for { 344 | // always receive packet, then send responce 345 | conn.SetReadDeadline(time.Now().Add(timeout)) 346 | p, err := SACP_read(conn, time.Second*10) 347 | if err != nil { 348 | return err 349 | } 350 | 351 | if p == nil { 352 | return errInvalidSize 353 | } 354 | 355 | if Debug { 356 | log.Printf("-- Got reply from printer: %v", p) 357 | } 358 | 359 | switch { 360 | case p.CommandSet == 0xb0 && p.CommandID == 0: 361 | // just ignore, don't know that this message means :) 362 | case p.CommandSet == 0xb0 && p.CommandID == 1: 363 | // sending next chunk 364 | if len(p.Data) < 4 { 365 | return errInvalidSize 366 | } 367 | md5_len := binary.LittleEndian.Uint16(p.Data[:2]) 368 | if len(p.Data) < 2+int(md5_len)+2 { 369 | return errInvalidSize 370 | } 371 | 372 | pkgRequested := binary.LittleEndian.Uint16(p.Data[2+md5_len : 2+md5_len+2]) 373 | var pkgData []byte 374 | 375 | if pkgRequested == package_count-1 { // last package 376 | pkgData = gcode[SACP_data_len*int(pkgRequested):] 377 | } else { // regular package 378 | pkgData = gcode[SACP_data_len*int(pkgRequested) : SACP_data_len*int(pkgRequested+1)] 379 | } 380 | 381 | data := bytes.Buffer{} 382 | data.WriteByte(0) 383 | writeSACPstring(&data, hex.EncodeToString(md5hash[:])) 384 | writeLE(&data, pkgRequested) 385 | writeSACPbytes(&data, pkgData) 386 | 387 | // log.Printf(" sending package %d of %d", pkgRequested+1, package_count) 388 | perc := float64(pkgRequested+1) / float64(package_count) * 100.0 389 | log.Printf(" - SACP sending %.1f%%", perc) 390 | 391 | conn.SetWriteDeadline(time.Now().Add(timeout)) 392 | _, err := conn.Write(SACP_pack{ 393 | ReceiverID: 2, 394 | SenderID: 0, 395 | Attribute: 1, 396 | Sequence: p.Sequence, 397 | CommandSet: 0xb0, 398 | CommandID: 0x01, 399 | Data: data.Bytes(), 400 | }.Encode()) 401 | 402 | if err != nil { 403 | return err 404 | } 405 | 406 | case p.CommandSet == 0xb0 && p.CommandID == 2: 407 | // send finished!!! 408 | if len(p.Data) == 1 && p.Data[0] == 0 { 409 | 410 | if Debug { 411 | log.Print("-- Upload finished") 412 | } 413 | 414 | if err := SACP_disconnect(conn, timeout); err != nil { 415 | return err 416 | } 417 | 418 | return nil // everything is ok! 419 | } 420 | 421 | log.Print("Unable to process b0/02 with invalid data", p.Data) 422 | 423 | default: 424 | continue 425 | } 426 | 427 | } 428 | } 429 | 430 | func SACP_disconnect(conn net.Conn, timeout time.Duration) (err error) { 431 | conn.SetWriteDeadline(time.Now().Add(timeout)) 432 | _, err = conn.Write(SACP_pack{ 433 | ReceiverID: 2, 434 | SenderID: 0, 435 | Attribute: 0, 436 | Sequence: 1, 437 | CommandSet: 0x01, 438 | CommandID: 0x06, 439 | Data: []byte{}, 440 | }.Encode()) 441 | 442 | return err 443 | } 444 | -------------------------------------------------------------------------------- /start-octoprint.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Will start the sm2uploader in OctoPrint mode. press Ctrl+C to exit. 3 | 4 | rem set host=-host A350 5 | set host= 6 | 7 | set /p port=Enter a local port num (default is 8899) 8 | if "%port%"=="" set "port=8899" 9 | if %port% LSS 1024 ( 10 | echo Port number must be greater than 1024 11 | pause 12 | exit /b 1 13 | ) 14 | 15 | set wdir=%~dp0 16 | cd /d %wdir% 17 | 18 | set w64=sm2uploader-win64.exe 19 | set w32=sm2uploader-win32.exe 20 | set cmd= 21 | 22 | where /q %w32% && set "cmd=%w32%" 23 | where /q %w64% && set "cmd=%w64%" 24 | 25 | if "%cmd%"=="" ( 26 | echo Can not find %w64% or %w32% 27 | pause 28 | exit /b 1 29 | ) 30 | 31 | echo %cmd% %host% -octoprint 127.0.0.1:%port% 32 | %cmd% %host% -octoprint 127.0.0.1:%port% 33 | pause 34 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/macdylan/SMFix/fix" 16 | ) 17 | 18 | type empty struct{} 19 | 20 | func humanReadableSize(size int64) string { 21 | const unit = 1024 22 | if size < unit { 23 | return fmt.Sprintf("%d B", size) 24 | } 25 | div, exp := int64(unit), 0 26 | for n := size / unit; n >= unit; n /= unit { 27 | div *= unit 28 | exp++ 29 | } 30 | return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp]) 31 | } 32 | 33 | var reFilename = regexp.MustCompile(`^[\.\/\\~]+`) 34 | 35 | func normalizedFilename(filename string) string { 36 | return reFilename.ReplaceAllString(filename, "") 37 | } 38 | 39 | /* 40 | func postProcessFile(file_path string) (out []byte, err error) { 41 | var r *os.File 42 | if r, err = os.Open(file_path); err != nil { 43 | return 44 | } 45 | defer r.Close() 46 | return postProcess(r) 47 | } 48 | */ 49 | 50 | func postProcess(r io.Reader) (out []byte, err error) { 51 | var ( 52 | isFixed = false 53 | nl = []byte("\n") 54 | headers = [][]byte{} 55 | gcodes = []*fix.GcodeBlock{} 56 | sc = bufio.NewScanner(r) 57 | ) 58 | for sc.Scan() { 59 | line := sc.Text() 60 | if !isFixed && strings.HasPrefix(line, "; Postprocessed by smfix") { 61 | isFixed = true 62 | } 63 | 64 | g, err := fix.ParseGcodeBlock(line) 65 | if err == nil { 66 | if g.Is("G4") { 67 | var s int 68 | if err := g.GetParam('S', &s); err == nil && s == 0 { 69 | continue 70 | } 71 | } 72 | gcodes = append(gcodes, g) 73 | continue 74 | } 75 | if err != fix.ErrEmptyString { 76 | return nil, err 77 | } 78 | } 79 | 80 | if !isFixed { 81 | funcs := []fix.GcodeModifier{} 82 | 83 | if !noTrim { 84 | // funcs = append(funcs, fix.GcodeTrimLines) 85 | } 86 | if !noShutoff { 87 | funcs = append(funcs, fix.GcodeFixShutoff) 88 | } 89 | // if !noPreheat { 90 | // funcs = append(funcs, fix.GcodeFixPreheat) 91 | // } 92 | if !noReplaceTool { 93 | funcs = append(funcs, fix.GcodeReplaceToolNum) 94 | } 95 | // if !noReinforceTower { 96 | // funcs = append(funcs, fix.GcodeReinforceTower) 97 | // } 98 | 99 | funcs = append(funcs, fix.GcodeFixOrcaToolUnload) 100 | 101 | for _, fn := range funcs { 102 | gcodes = fn(gcodes) 103 | } 104 | 105 | if headers, err = fix.ExtractHeader(gcodes); err != nil { 106 | return nil, err 107 | } 108 | } 109 | 110 | var buf bytes.Buffer 111 | 112 | for _, h := range headers { 113 | buf.Write(h) 114 | buf.Write(nl) 115 | } 116 | 117 | for _, gcode := range gcodes { 118 | buf.WriteString(gcode.String()) 119 | buf.Write(nl) 120 | } 121 | return buf.Bytes(), nil 122 | } 123 | 124 | func shouldBeFix(fpath string) bool { 125 | ext := strings.ToLower(filepath.Ext(fpath)) 126 | return SmFixExtensions[ext] 127 | } 128 | 129 | func parseIntEnv(key string, defaultValue int) int { 130 | if value, ok := os.LookupEnv(key); ok { 131 | if v, err := strconv.Atoi(value); err == nil { 132 | return v 133 | } 134 | } 135 | return defaultValue 136 | } 137 | 138 | func parseBoolEnv(key string, defaultValue bool) bool { 139 | if value, ok := os.LookupEnv(key); ok { 140 | if v, err := strconv.ParseBool(value); err == nil { 141 | return v 142 | } 143 | } 144 | return defaultValue 145 | } 146 | 147 | func parseDurationEnv(key string, defaultValue time.Duration) time.Duration { 148 | if value, ok := os.LookupEnv(key); ok { 149 | if v, err := time.ParseDuration(value); err == nil { 150 | return v 151 | } 152 | } 153 | return defaultValue 154 | } 155 | --------------------------------------------------------------------------------