mirror of
https://github.com/mineracks/seedhammer-v1-companion.git
synced 2026-06-26 22:01:05 +10:00
Twelve packages lifted from seedhammer/seedhammer @ v1.3.0
(commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e):
address/ — bitcoin address parsing
backup/ — v1 plate dimensions + UR-coded multi-plate backup
bc/ — Blockchain Commons: ur, fountain, bytewords, urtypes,
xoshiro256 (5 subpkgs)
bip32/ — HD-key derivation
bip39/ — mnemonic seed phrases + 2048-word wordlist
engrave/ — text/QR → MoveTo/LineTo command stream conversion
seedqr/ — SeedQR / CompactSeedQR encoders
image/ — paletted, rgb565, alpha4, ninepatch image formats
nonstandard/ — bitcoin descriptor + script parsing
font/ — bitmap + vector font runtime
font/{comfortaa,poppins,constant,bitmap,vector}/ — actual fonts
driver/mjolnir/ — MarkingWay USB-serial engraver driver
Plus an earlier-aside backup_test.go restored (its deps are now lifted).
Import paths globally rewritten seedhammer.com → mineracks namespace
via single sed pass; verified no orphan refs remain. go.mod adopts
upstream's full dep set plus the replace-directive for the patched
kortschak/qr fork.
go build ./... clean (all 27 packages)
go test ./... clean (12 packages with tests, all passing)
NOT lifted in this commit:
- driver/{wshat,drm,libcamera} (hardware-specific GPIO/LCD/camera —
will be platform-v1/-shaped abstractions instead)
- gui/ (depends on the above; lifts in Phase 2)
- cmd/{controller,...} (Pi binary entrypoints — not needed for the
companion repo)
- zbar/ (QR scanner — needs libcamera)
Next:
- Write the SH1E reference encoder/decoder in engrave/wire/sh1e/
- Lift Gangleri42's cmd/webnfc/ shell + retune to v1 plates
- First buildable composer WASM with a working preview
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
389 lines
7.9 KiB
Go
Executable File
389 lines
7.9 KiB
Go
Executable File
// package mjolnir implements a driver for the MarkingWay engraving
|
|
// machine.
|
|
package mjolnir
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"runtime"
|
|
|
|
"github.com/tarm/serial"
|
|
"github.com/mineracks/seedhammer-v1-companion/engrave"
|
|
)
|
|
|
|
type program struct {
|
|
cmds chan [cmdSize]byte
|
|
count int
|
|
sent int
|
|
}
|
|
|
|
var Params = engrave.Params{
|
|
StrokeWidth: 38,
|
|
Millimeter: 126,
|
|
}
|
|
|
|
type Options struct {
|
|
MoveSpeed float32
|
|
PrintSpeed float32
|
|
End image.Point
|
|
}
|
|
|
|
var safePoint = image.Pt(119, 43)
|
|
|
|
const (
|
|
cmdSize = 10
|
|
|
|
defaultMoveSpeed = .5
|
|
defaultPrintSpeed = .1
|
|
)
|
|
|
|
func Open(dev string) (io.ReadWriteCloser, error) {
|
|
// Hardware parameters.
|
|
const (
|
|
baudRate = 115200
|
|
stopBits = 1
|
|
parity = false
|
|
wordLen = 8
|
|
controlHandshake = 0
|
|
flowReplace = 0
|
|
xonLimit = 2048
|
|
xoffLimit = 512
|
|
)
|
|
|
|
var devices []string
|
|
if dev != "" {
|
|
devices = append(devices, dev)
|
|
} else {
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
devices = append(devices, "COM3")
|
|
case "linux":
|
|
devices = append(devices, "/dev/ttyUSB0", "/dev/ttyUSB1")
|
|
}
|
|
}
|
|
if len(devices) == 0 {
|
|
return nil, errors.New("no device specified")
|
|
}
|
|
var firstErr error
|
|
for _, dev := range devices {
|
|
c := &serial.Config{Name: dev, Baud: baudRate}
|
|
s, err := serial.OpenPort(c)
|
|
if err == nil {
|
|
return s, nil
|
|
}
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
return nil, firstErr
|
|
}
|
|
|
|
const (
|
|
initCmd = 0x00
|
|
cancelCmd = 0xaf
|
|
setSpeedCmd = 0x30
|
|
setDelaysCmd = 0x31
|
|
moveToOriginCmd = 0x21
|
|
moveToOriginCmdExtra = 0x50
|
|
moveToOriginCmdResponse = 0x00
|
|
initProgramCmd = 0x60
|
|
moveCmd = 0x80
|
|
lineCmd = 0x00
|
|
nopCmd = 0xff
|
|
)
|
|
|
|
const (
|
|
initializedStatus = 0x00
|
|
cancellingStatus = 0x62
|
|
cancelledStatus = 0x65
|
|
bufferProgramStatus = 0x60
|
|
programStepStatus = 0x6f
|
|
programCompleteStatus = 0x6a
|
|
)
|
|
|
|
// The engraver expects program commands in batches.
|
|
const progBatchSize = 80
|
|
|
|
func Engrave(dev io.ReadWriter, opts Options, plan engrave.Plan, quit <-chan struct{}) (eerr error) {
|
|
bufw := bufio.NewWriterSize(dev, progBatchSize*cmdSize)
|
|
writeMut := make(chan struct{}, 1)
|
|
writeMut <- struct{}{}
|
|
flush := func() {
|
|
<-writeMut
|
|
defer func() { writeMut <- struct{}{} }()
|
|
if eerr != nil {
|
|
return
|
|
}
|
|
eerr = bufw.Flush()
|
|
}
|
|
defer flush()
|
|
wr := func(data ...byte) {
|
|
<-writeMut
|
|
defer func() { writeMut <- struct{}{} }()
|
|
if eerr != nil {
|
|
return
|
|
}
|
|
_, eerr = bufw.Write(data)
|
|
}
|
|
done := make(chan struct{})
|
|
defer close(done)
|
|
go func() {
|
|
select {
|
|
case <-quit:
|
|
select {
|
|
case <-writeMut:
|
|
case <-done:
|
|
return
|
|
}
|
|
dev.Write([]byte{cancelCmd})
|
|
writeMut <- struct{}{}
|
|
<-done
|
|
case <-done:
|
|
}
|
|
}()
|
|
bufr := bufio.NewReaderSize(dev, 100)
|
|
r := func(c int) []byte {
|
|
flush()
|
|
if eerr != nil {
|
|
return nil
|
|
}
|
|
data := make([]byte, c)
|
|
n, err := bufr.Read(data)
|
|
eerr = err
|
|
data = data[:n]
|
|
return data
|
|
}
|
|
expect := func(exp ...byte) {
|
|
for len(exp) > 0 && eerr == nil {
|
|
got := r(len(exp))
|
|
n := len(got)
|
|
if !bytes.Equal(exp[:n], got) {
|
|
eerr = fmt.Errorf("unexpected reply\nexp: %#x\ngot: %#x", exp, got)
|
|
return
|
|
}
|
|
exp = exp[n:]
|
|
}
|
|
}
|
|
atleast := func(n int) []byte {
|
|
var res []byte
|
|
for n > 0 {
|
|
data := r(n)
|
|
res = append(res, data...)
|
|
n -= len(data)
|
|
}
|
|
return res
|
|
}
|
|
origin := func() {
|
|
wr(moveToOriginCmd, moveToOriginCmdExtra)
|
|
expect(moveToOriginCmd, moveToOriginCmdResponse)
|
|
}
|
|
cancel := func() {
|
|
wr(cancelCmd)
|
|
}
|
|
initialize := func() {
|
|
cancel()
|
|
wr(initCmd)
|
|
for {
|
|
status := r(1)
|
|
if eerr != nil {
|
|
break
|
|
}
|
|
switch status[0] {
|
|
case initializedStatus:
|
|
return
|
|
case cancelledStatus:
|
|
// Re-initialize.
|
|
wr(initCmd)
|
|
}
|
|
}
|
|
}
|
|
parseCoords := func(coords []byte) (x int, y int, z int) {
|
|
x = int(coords[0]) | int(coords[1])<<8 | int(coords[2])<<16
|
|
y = int(coords[3]) | int(coords[4])<<8 | int(coords[5])<<16
|
|
z = int(coords[6]) | int(coords[7])<<8 | int(coords[8])<<16
|
|
return
|
|
}
|
|
queryPos := func() (x int, y int, z int) {
|
|
wr(0x16)
|
|
expect(0x16)
|
|
x, y, z = parseCoords(atleast(9))
|
|
return
|
|
}
|
|
_, _ = atleast, queryPos
|
|
|
|
initialize()
|
|
|
|
// Speed range: [1000,30].
|
|
setSpeeds := func(print, move, xxx int) {
|
|
wr(setSpeedCmd, byte(print), byte(print>>8), byte(move), byte(move>>8), byte(xxx), byte(xxx>>8))
|
|
expect(setSpeedCmd)
|
|
}
|
|
|
|
// Delay range: 0-255.
|
|
setDelays := func(penDown, penUp int) {
|
|
wr(setDelaysCmd, byte(penDown), byte(penUp))
|
|
expect(setDelaysCmd)
|
|
}
|
|
setDelays(0x14, 0x14)
|
|
|
|
// Init done.
|
|
|
|
runProgram := func(plan engrave.Plan) {
|
|
p := &program{}
|
|
plan(p.Command)
|
|
p.Prepare()
|
|
defer func() {
|
|
for i := p.sent; i < p.count; i++ {
|
|
<-p.cmds
|
|
}
|
|
}()
|
|
go plan(p.Command)
|
|
p.sent = 0
|
|
// Round up to nearest batch size. Note that the rounding
|
|
// adds another, empty, batch in case we fill up the last one.
|
|
// Otherwise, the engraver won't send a completed status.
|
|
nbatches := (p.count + progBatchSize) / progBatchSize
|
|
if nbatches > 0xffff {
|
|
eerr = errors.New("engrave: program too large")
|
|
return
|
|
}
|
|
wr(initProgramCmd, byte(nbatches), byte(nbatches>>8))
|
|
done:
|
|
for {
|
|
status := r(1)
|
|
if eerr != nil {
|
|
return
|
|
}
|
|
paddedCount := nbatches * progBatchSize
|
|
switch status[0] {
|
|
case bufferProgramStatus:
|
|
if p.sent == paddedCount {
|
|
break
|
|
}
|
|
ncmd := progBatchSize
|
|
if rem := p.count - p.sent; ncmd > rem {
|
|
ncmd = rem
|
|
}
|
|
for i := 0; i < ncmd; i++ {
|
|
cmd := <-p.cmds
|
|
p.sent++
|
|
wr(cmd[:]...)
|
|
}
|
|
// Pad with 0xff.
|
|
pad := [cmdSize]byte{}
|
|
for i := range pad {
|
|
pad[i] = nopCmd
|
|
}
|
|
for i := ncmd; i < progBatchSize; i++ {
|
|
p.sent++
|
|
wr(pad[:]...)
|
|
}
|
|
case programStepStatus:
|
|
case programCompleteStatus:
|
|
break done
|
|
case cancellingStatus:
|
|
case cancelledStatus:
|
|
if eerr == nil {
|
|
eerr = ErrCancelled
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
moveTo := func(p image.Point) {
|
|
runProgram(func(yield func(engrave.Command)) {
|
|
yield(engrave.Move(p))
|
|
})
|
|
}
|
|
|
|
setSpeeds(300, 300, 0xe6)
|
|
|
|
// Prepare the machine: (1) reset the origin and
|
|
// (2) move to safe point. The first is necessary because
|
|
// the absolute position of the needle is not known at startup.
|
|
// The second is to avoid needle collision with the tightening
|
|
// nuts.
|
|
origin()
|
|
// Avoid a false home by moving out and re-homing.
|
|
falseHome := 5 * Params.Millimeter
|
|
moveTo(image.Pt(falseHome, falseHome))
|
|
origin()
|
|
sp := image.Point{
|
|
X: safePoint.X * Params.Millimeter,
|
|
Y: safePoint.Y * Params.Millimeter,
|
|
}
|
|
moveTo(sp)
|
|
|
|
// 0 lowest, 1 highest.
|
|
moveSpeed := opts.MoveSpeed
|
|
printSpeed := opts.PrintSpeed
|
|
if moveSpeed == 0 {
|
|
moveSpeed = defaultMoveSpeed
|
|
}
|
|
if printSpeed == 0 {
|
|
printSpeed = defaultPrintSpeed
|
|
}
|
|
mms := int(moveSpeed*float32(30) + (1.-moveSpeed)*float32(1000))
|
|
mps := int(printSpeed*float32(30) + (1.-printSpeed)*float32(1000))
|
|
setSpeeds(mps, mms, 0xe6)
|
|
runProgram(plan)
|
|
if eerr == nil || eerr == ErrCancelled {
|
|
setSpeeds(300, 300, 0xe6)
|
|
if opts.End != (image.Point{}) {
|
|
moveTo(opts.End)
|
|
} else {
|
|
moveTo(sp)
|
|
origin()
|
|
}
|
|
}
|
|
|
|
return eerr
|
|
}
|
|
|
|
var ErrCancelled = errors.New("cancelled")
|
|
|
|
func mkcoords(p image.Point) [9]byte {
|
|
x, y := p.X, p.Y
|
|
if x < 0 || x > 0xffffff || y < 0 || y > 0xffffff {
|
|
panic(fmt.Errorf("(%d,%d) out of range", x, y))
|
|
}
|
|
return [...]byte{
|
|
byte(x), byte(x >> 8), byte(x >> 16),
|
|
byte(y), byte(y >> 8), byte(y >> 16),
|
|
0x00, 0x00, 0x00, // Z = 0.
|
|
}
|
|
}
|
|
|
|
func (p *program) cmd(c [cmdSize]byte) {
|
|
if p.cmds != nil {
|
|
p.cmds <- c
|
|
} else {
|
|
p.count++
|
|
}
|
|
}
|
|
|
|
func (p *program) Prepare() {
|
|
p.cmds = make(chan [cmdSize]byte)
|
|
}
|
|
|
|
func (p *program) Command(c engrave.Command) {
|
|
var cmd [cmdSize]byte
|
|
coords := mkcoords(c.Coord)
|
|
copy(cmd[1:], coords[:])
|
|
if c.Line {
|
|
cmd[0] = lineCmd
|
|
} else {
|
|
cmd[0] = moveCmd
|
|
}
|
|
p.cmd(cmd)
|
|
p.pause()
|
|
}
|
|
|
|
func (p *program) pause() {
|
|
// p.cmd([...]byte{0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
|
}
|