log2life

Feed log lines to sdl2-life as Life 1.05 patterns
git clone https://www.brianlane.com/git/log2life
Log | Files | Refs | README

main.go (5436B)


      1 // log2life
      2 // by Brian C. Lane <bcl@brianlane.com>
      3 package main
      4 
      5 import (
      6 	"bufio"
      7 	"flag"
      8 	"fmt"
      9 	"io"
     10 	"log"
     11 	"net"
     12 	"net/http"
     13 	"os"
     14 	"strings"
     15 	"time"
     16 )
     17 
     18 /* commandline flags */
     19 type cmdlineArgs struct {
     20 	Logfile string  // Logfile to read
     21 	Speed   float64 // Playback speed factor 1.0 == realtime
     22 	Columns int     // Columns of Life world in cells
     23 	Rows    int     // Rows of Life world in cells
     24 	Port    int     // Port to connect to
     25 	Host    string  // Host IP to connect to
     26 }
     27 
     28 /* commandline defaults */
     29 var cfg = cmdlineArgs{
     30 	Logfile: "",
     31 	Speed:   1.0,
     32 	Columns: 100,
     33 	Rows:    100,
     34 	Port:    3051,
     35 	Host:    "127.0.0.1",
     36 }
     37 
     38 /* parseArgs handles parsing the cmdline args and setting values in the global cfg struct */
     39 func init() {
     40 	flag.Float64Var(&cfg.Speed, "speed", cfg.Speed, "Playback speed. 1.0 is realtime")
     41 	flag.IntVar(&cfg.Columns, "columns", cfg.Columns, "Width of Life world in cells")
     42 	flag.IntVar(&cfg.Rows, "rows", cfg.Rows, "Height of Life world in cells")
     43 	flag.IntVar(&cfg.Port, "port", cfg.Port, "Port to listen to")
     44 	flag.StringVar(&cfg.Host, "host", cfg.Host, "Host IP to bind to")
     45 
     46 	// first non flag argument is the logfile name
     47 	flag.Usage = func() {
     48 		fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [options] logfile:\n", os.Args[0])
     49 		flag.PrintDefaults()
     50 	}
     51 
     52 	flag.Parse()
     53 }
     54 
     55 func main() {
     56 	if flag.NArg() == 0 {
     57 		flag.Usage()
     58 		os.Exit(1)
     59 	}
     60 	filename := flag.Arg(0)
     61 
     62 	var f *os.File
     63 	var err error
     64 	if filename == "-" {
     65 		f = os.Stdin
     66 
     67 		// When feeding log lines we don't want to delay
     68 		cfg.Speed = 0
     69 		fmt.Printf("Playback of <stdin> to %s:%d in realtime\n", cfg.Host, cfg.Port)
     70 	} else {
     71 
     72 		// Read logfile line by line
     73 		f, err = os.Open(filename)
     74 		if err != nil {
     75 			log.Fatal(err)
     76 		}
     77 		fmt.Printf("Playback of %s to %s:%d at %0.1fx speed\n", filename, cfg.Host, cfg.Port, cfg.Speed)
     78 	}
     79 
     80 	lastTime := time.Time{}
     81 	scanner := bufio.NewScanner(f)
     82 	for scanner.Scan() {
     83 		pattern, timestamp, err := LineToPattern(scanner.Text(), cfg.Columns, cfg.Rows)
     84 		if err != nil {
     85 			log.Print(err)
     86 			continue
     87 		}
     88 
     89 		// When reading from stdin speed is set to 0 for no delay. When replaying a
     90 		// log file it will use the timestamps to replay it in realtime unless -speed is passed
     91 		// with a different value.
     92 		noTime := time.Time{}
     93 		if lastTime != noTime {
     94 			delay := time.Duration(float64(timestamp.Sub(lastTime).Microseconds())*1/cfg.Speed) * time.Microsecond
     95 			fmt.Printf("delaying %s\n", delay)
     96 			time.Sleep(delay)
     97 		}
     98 		lastTime = timestamp
     99 
    100 		fmt.Printf("%s\n", strings.Join(pattern, "\n"))
    101 		err = SendPattern(cfg.Host, cfg.Port, pattern)
    102 		if err != nil {
    103 			fmt.Printf("ERROR: %s\n", err)
    104 		}
    105 
    106 	}
    107 	if err = scanner.Err(); err != nil {
    108 		log.Fatal(err)
    109 	}
    110 }
    111 
    112 // LineToPattern converts a log line to a Life 1.05 pattern with position based on the client IP
    113 func LineToPattern(line string, width, height int) ([]string, time.Time, error) {
    114 
    115 	// Get the IP and convert to x, y coordinated, scaled by columns, rows and 0, 0 at the center
    116 	fields := strings.SplitN(line, " ", 4)
    117 	if fields[0] == "-" || strings.TrimSpace(fields[0]) == "" {
    118 		return []string{}, time.Time{}, fmt.Errorf("No client IP address")
    119 	}
    120 	x, y := IPToXY(fields[0], width, height)
    121 
    122 	// Get the timestamp (will eventually return this and use it for timing)
    123 	// [20/Nov/2022:02:27:49 +0000]
    124 	fields = strings.SplitN(fields[3], "]", 2)
    125 	//	timestamp := fields[0][1:]
    126 	timestamp, err := time.Parse("02/Jan/2006:15:04:05 -0700", fields[0][1:])
    127 	if err != nil {
    128 		return []string{}, time.Time{}, err
    129 	}
    130 
    131 	// XOR the data into an 8x8 bitpattern
    132 	// TODO Scramble this a bit more, all the log data is 7 bit
    133 	var data [8]byte
    134 	var idx int
    135 	for _, b := range []byte(fields[1]) {
    136 		// Skip quotes
    137 		if b == byte('"') {
    138 			continue
    139 		}
    140 		data[idx] = data[idx] ^ b
    141 		idx = (idx + 1) % 8
    142 	}
    143 
    144 	// Convert the data to a Life 1.05 pattern
    145 	return MakeLife105(x, y, data), timestamp, nil
    146 }
    147 
    148 // IPToXY convert an IPv4 dotted quad into an X, Y coordinate
    149 func IPToXY(addr string, width, height int) (x, y int) {
    150 	ip := net.ParseIP(addr)
    151 	if ip == nil {
    152 		return 0, 0
    153 	}
    154 
    155 	// Only using IPv4 right now so 4 bytes from the ip which are at the end
    156 	// because it converts it to a IPv6 encoded IPv4
    157 	// Use the upper 16 bits as x and lower 16 as y, scaled to the life world size
    158 	// and with 0,0 at the center
    159 	x = int(float64(int(ip[12])<<8+int(ip[13]))/0xffff*float64(width)) - width/2
    160 	y = int(float64(int(ip[14])<<8+int(ip[15]))/0xffff*float64(height)) - height/2
    161 
    162 	return x, y
    163 }
    164 
    165 // SendPattern POSTs a pattern to the life server and returns any errors
    166 func SendPattern(host string, port int, pattern []string) error {
    167 	data := strings.NewReader(strings.Join(pattern, "\n"))
    168 	resp, err := http.Post(fmt.Sprintf("http://%s:%d", host, port), "text/plain", data)
    169 	if err != nil {
    170 		return err
    171 	}
    172 	defer resp.Body.Close()
    173 
    174 	_, err = io.ReadAll(resp.Body)
    175 	return err
    176 }
    177 
    178 // MakeLife105 converts an array of 8 bytes into a life 1.05 pattern string
    179 func MakeLife105(x, y int, data [8]byte) []string {
    180 	var pattern []string
    181 
    182 	pattern = append(pattern, "#Life 1.05")
    183 	pattern = append(pattern, "#D log2life ouput")
    184 	pattern = append(pattern, "#N")
    185 	pattern = append(pattern, fmt.Sprintf("#P %d %d", x, y))
    186 
    187 	for _, b := range data {
    188 		var line string
    189 		for i := 0; i < 8; i++ {
    190 			if b&0x80 == 0x80 {
    191 				line = line + "*"
    192 			} else {
    193 				line = line + "."
    194 			}
    195 
    196 			b = b << 1
    197 		}
    198 		pattern = append(pattern, line)
    199 	}
    200 
    201 	return pattern
    202 }