commit 01b50f47d84b59fecc6daaf9a9d5e05e56f64537
Author: Brian C. Lane <bcl@brianlane.com>
Date: Fri, 25 Nov 2022 13:57:45 -0800
Parse loglines and POST patterns to Life server
Diffstat:
A | go.mod | | | 3 | +++ |
A | main.go | | | 179 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 182 insertions(+), 0 deletions(-)
diff --git a/go.mod b/go.mod
@@ -0,0 +1,3 @@
+module github.com/bcl/log2life
+
+go 1.19
diff --git a/main.go b/main.go
@@ -0,0 +1,179 @@
+package main
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "strings"
+)
+
+/* commandline flags */
+type cmdlineArgs struct {
+ Logfile string // Logfile to read
+ Speed float64 // Playback speed factor 1.0 == realtime
+ Width int // Width of Life world in cells
+ Height int // Height of Life world in cells
+ Port int // Port to connect to
+ Host string // Host IP to connect to
+}
+
+/* commandline defaults */
+var cfg = cmdlineArgs{
+ Logfile: "",
+ Speed: 1.0,
+ Width: 100,
+ Height: 100,
+ Port: 3051,
+ Host: "127.0.0.1",
+}
+
+/* parseArgs handles parsing the cmdline args and setting values in the global cfg struct */
+func init() {
+ flag.Float64Var(&cfg.Speed, "speed", cfg.Speed, "Playback speed. 1.0 is realtime")
+ flag.IntVar(&cfg.Width, "width", cfg.Width, "Width of Life world in cells")
+ flag.IntVar(&cfg.Height, "height", cfg.Height, "Height of Life world in cells")
+ flag.IntVar(&cfg.Port, "port", cfg.Port, "Port to listen to")
+ flag.StringVar(&cfg.Host, "host", cfg.Host, "Host IP to bind to")
+
+ // first non flag argument is the logfile name
+ flag.Usage = func() {
+ fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [options] logfile:\n", os.Args[0])
+ flag.PrintDefaults()
+ }
+
+ flag.Parse()
+}
+
+func main() {
+ if flag.NArg() == 0 {
+ flag.Usage()
+ os.Exit(1)
+ }
+ filename := flag.Arg(0)
+
+ var f *os.File
+ var err error
+ if filename == "-" {
+ f = os.Stdin
+ } else {
+ _, err = os.Stat(filename)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("Playback of %s to %s:%d at %0.1fx speed\n", filename, cfg.Host, cfg.Port, cfg.Speed)
+
+ // Read logfile line by line
+ f, err = os.Open(filename)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ pattern, err := LineToPattern(scanner.Text(), cfg.Width, cfg.Height)
+ if err != nil {
+ log.Print(err)
+ continue
+ }
+
+ fmt.Printf("%s\n", strings.Join(pattern, "\n"))
+
+ err = SendPattern(cfg.Host, cfg.Port, pattern)
+ if err != nil {
+ fmt.Printf("ERROR: %s\n", err)
+ }
+
+ }
+ if err = scanner.Err(); err != nil {
+ log.Fatal(err)
+ }
+}
+
+// LineToPattern converts a log line to a Life 1.05 pattern with position based on the client IP
+func LineToPattern(line string, width, height int) ([]string, error) {
+
+ // Get the IP and convert to x, y coordinated, scaled by columns, rows and 0, 0 at the center
+ fields := strings.SplitN(line, " ", 4)
+ if fields[0] == "-" || strings.TrimSpace(fields[0]) == "" {
+ return []string{}, fmt.Errorf("No client IP address")
+ }
+ x, y := IPToXY(fields[0], width, height)
+
+ // Get the timestamp (will eventually return this and use it for timing)
+ fields = strings.SplitN(fields[3], "]", 2)
+ // timestamp := fields[0][1:]
+
+ // XOR the data into an 8x8 bitpattern
+ var data [8]byte
+ var idx int
+ for _, b := range []byte(fields[1]) {
+ // Skip quotes
+ if b == byte('"') {
+ continue
+ }
+ data[idx] = data[idx] ^ b
+ idx = (idx + 1) % 8
+ }
+
+ // Convert the data to a Life 1.05 pattern
+ return MakeLife105(x, y, data), nil
+}
+
+// IPToXY convert an IPv4 dotted quad into an X, Y coordinate
+func IPToXY(addr string, width, height int) (x, y int) {
+ ip := net.ParseIP(addr)
+ if ip == nil {
+ return 0, 0
+ }
+
+ // Only using IPv4 right now so 4 bytes from the ip which are at the end
+ // because it converts it to a IPv6 encoded IPv4
+ x = int(float64(int(ip[12])<<8+int(ip[13]))/0xffff*float64(width)) - width/2
+ y = int(float64(int(ip[14])<<8+int(ip[15]))/0xffff*float64(height)) - height/2
+
+ return x, y
+}
+
+// SendPattern POSTs a pattern to the life server and returns any errors
+func SendPattern(host string, port int, pattern []string) error {
+ data := strings.NewReader(strings.Join(pattern, "\n"))
+ resp, err := http.Post(fmt.Sprintf("http://%s:%d", host, port), "text/plain", data)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ _, err = io.ReadAll(resp.Body)
+ return err
+}
+
+// MakeLife105 converts an array of 8 bytes into a life 1.05 pattern string
+func MakeLife105(x, y int, data [8]byte) []string {
+ var pattern []string
+
+ pattern = append(pattern, "#Life 1.05")
+ pattern = append(pattern, "#D log2life ouput")
+ pattern = append(pattern, "#N")
+ pattern = append(pattern, fmt.Sprintf("#P %d %d", x, y))
+
+ for _, b := range data {
+ var line string
+ for i := 0; i < 8; i++ {
+ if b&0x80 == 0x80 {
+ line = line + "*"
+ } else {
+ line = line + "."
+ }
+
+ b = b << 1
+ }
+ pattern = append(pattern, line)
+ }
+
+ return pattern
+}