init: intial commit

This commit is contained in:
Panic
2025-11-27 22:14:01 -07:00
commit fd93e7619c
6 changed files with 332 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.vscode/tasks.json

125
devices/N64.go Normal file
View File

@@ -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}
}

96
devices/coin_acceptor.go Normal file
View File

@@ -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
}

27
go.mod Normal file
View File

@@ -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
)

45
go.sum Normal file
View File

@@ -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=

38
main.go Normal file
View File

@@ -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)})
}
}