commit fd93e7619cda97b36a5e7e91fd60583acfcd9e45 Author: Panic Date: Thu Nov 27 22:14:01 2025 -0700 init: intial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..890da3a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/tasks.json \ No newline at end of file diff --git a/devices/N64.go b/devices/N64.go new file mode 100644 index 0000000..fd88469 --- /dev/null +++ b/devices/N64.go @@ -0,0 +1,125 @@ +package devices + +import ( + "encoding/binary" + "fmt" + "log" + + "go.bug.st/serial" +) + +const ( + MAX_DATA_SIZE = 512 +) + +type N64 interface { + Start() error + Close() error + SendData(data []byte) error + ReceiveData() ([]byte, error) +} + +type EverDrive struct { + PortName string + port serial.Port + running bool +} + +func (e *EverDrive) Start() error { + mode := &serial.Mode{ + BaudRate: 9600, + DataBits: 8, + Parity: serial.NoParity, // Changed from EvenParity + StopBits: serial.OneStopBit, // Changed from TwoStopBits + } + port, err := serial.Open(e.PortName, mode) + if err != nil { + return err + } + + e.port = port + e.running = true + return nil +} + +func (e *EverDrive) Close() error { + return nil +} + +func (e *EverDrive) SendData(data []byte) error { + if !e.running { + return fmt.Errorf("N64 is not running") + } + + // --- 1. CONSTRUCT THE UNFLOADER PACKET --- + + // Header (4 Bytes) + packet := []byte{'D', 'M', 'A', '@'} + + // Data Type (1 Byte) + // using 0x01 (DataTypeText) is fine, or define your own. + packet = append(packet, 0x01) + + // Size (3 Bytes, Big Endian) + // This tells usb.c how many REAL bytes of payload to expect. + lengthBuf := make([]byte, 4) + binary.BigEndian.PutUint32(lengthBuf, uint32(len(data))) + packet = append(packet, lengthBuf[1:]...) // Skip first byte, append last 3 + + // Payload (The Coin) + packet = append(packet, data...) + + // PAYLOAD ALIGNMENT (Crucial per usb.c line 640) + // len = ALIGN(usb_datasize, 2); + // If we send 1 byte (odd), usb.c will read 2 bytes. + // We must provide that padding byte or it will eat the 'C' of the footer. + if len(data)%2 != 0 { + packet = append(packet, 0x00) + } + + // Footer (4 Bytes) + packet = append(packet, 'C', 'M', 'P', 'H') + + // --- 2. HARDWARE PADDING (The Fix) --- + // The EverDrive needs a 512-byte transfer to wake up. + // We pad the rest of the buffer with zeros. + // The N64 will read the packet above, then fail 'DMA@' checks + // on the zeros until the buffer is empty. This is expected behavior. + + blockSize := 512 + totalLen := len(packet) + + if totalLen < blockSize { + padding := make([]byte, blockSize-totalLen) + packet = append(packet, padding...) + } + + // Write exactly 512 bytes + _, err := e.port.Write(packet) + if err != nil { + log.Printf("❌ N64 Write Error: %v", err) + return err + } + + log.Printf("📤 Sent Packet (Payload: %v | Total Wire: %d)", data, len(packet)) + return nil +} +func (e *EverDrive) ReceiveData() ([]byte, error) { + if !e.running { + return nil, fmt.Errorf("N64 is not running") + } + + data := make([]byte, MAX_DATA_SIZE) + _, err := e.port.Read(data) + if err != nil { + log.Printf("❌ N64 Read Error: %v", err) + return nil, err + } + + log.Printf("📥 Received %s from N64", data) + return data, nil +} + +func NewEverDrive(portName string) *EverDrive { + return &EverDrive{PortName: portName} +} diff --git a/devices/coin_acceptor.go b/devices/coin_acceptor.go new file mode 100644 index 0000000..fa924ec --- /dev/null +++ b/devices/coin_acceptor.go @@ -0,0 +1,96 @@ +package devices + +import ( + "fmt" + "log" + + "go.bug.st/serial" +) + +type Coin int + +func (c Coin) Value() int { + return int(c) +} + +func (c Coin) String() string { + switch c { + case Quarter: + return "Quarter (25c)" + case Dime: + return "Dime (10c)" + case Nickel: + return "Nickel (5c)" + case Penny: + return "Penny (1c)" + default: + return fmt.Sprintf("Unknown Coin (%d)", c) + } +} + +const ( + Quarter Coin = 25 + Dime Coin = 10 + Nickel Coin = 5 + Penny Coin = 1 +) + +type CoinAcceptor interface { + Start() error + Close() error + Coins() <-chan Coin +} + +type DG600F struct { + PortName string + BaudRate int + coins chan Coin + port serial.Port +} + +func (c *DG600F) Start() error { + mode := &serial.Mode{ + BaudRate: c.BaudRate, + DataBits: 8, + Parity: serial.EvenParity, + StopBits: serial.TwoStopBits, + } + + port, err := serial.Open(c.PortName, mode) + if err != nil { + return err + } + + c.port = port + c.coins = make(chan Coin, 10) + log.Println("Coin acceptor started") + + go c.listen() + + return nil +} + +func (c *DG600F) listen() { + for { + data := make([]byte, 1) + _, err := c.port.Read(data) + if err != nil { + log.Println("Error reading from port:", err) + return + } + + if data[0] == 0x00 { + continue + } + + c.coins <- Coin(data[0]) + } +} + +func (c *DG600F) Close() error { + return c.port.Close() +} + +func (c *DG600F) Coins() <-chan Coin { + return c.coins +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8ba3bc8 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module github.com/PrintAndPanic/microtransactions64-server + +go 1.25.3 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/creack/goselect v0.1.2 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.bug.st/serial v1.6.4 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a787956 --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7b169c1 --- /dev/null +++ b/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "flag" + "log" + + "github.com/PrintAndPanic/microtransactions64-server/devices" +) + +func main() { + // "n64" defaults to /dev/ttyUSB0 (The Everdrive) + n64Port := flag.String("n64", "/dev/ttyUSB0", "Serial port for the N64 Everdrive") + + // 2. Parse Flags (Critical: Must be called before accessing the variables) + flag.Parse() + + log.Printf("🚀 STARTING: N64 on %s", *n64Port) + // --- CONFIGURATION --- + coinPortName := "/dev/serial0" + coinPortBaud := 9600 + // --------------------- + + // 1. Configure the Serial Port + coinAcceptor := devices.DG600F{PortName: coinPortName, BaudRate: coinPortBaud} + coinAcceptor.Start() + defer coinAcceptor.Close() + + n64 := devices.NewEverDrive(*n64Port) + n64.Start() + defer n64.Close() + + // // 2. Listen for coins + for coin := range coinAcceptor.Coins() { + log.Println("Coin accepted:", coin) + n64.SendData([]byte{byte(coin)}) + } + +}