mirror of https://github.com/ariejan/i6502.git
Working websockets
This commit is contained in:
parent
12f64bf7d3
commit
b692c8a7b1
|
@ -0,0 +1,108 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
pongWait = 60 * time.Second
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
type connection struct {
|
||||
// The i6502 machine
|
||||
machine *Machine
|
||||
|
||||
// Websocket connection
|
||||
ws *websocket.Conn
|
||||
|
||||
// Outgoing data channel
|
||||
send chan []byte
|
||||
}
|
||||
|
||||
var (
|
||||
upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
)
|
||||
|
||||
// Pump received websocket messages into the machine
|
||||
func (c *connection) readPump() {
|
||||
defer func() {
|
||||
c.ws.Close()
|
||||
}()
|
||||
|
||||
c.ws.SetReadLimit(maxMessageSize)
|
||||
c.ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, message, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
c.writeBytesToMachine(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Pump serial output from the machine into the socket
|
||||
func (c *connection) writePump() {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
|
||||
defer func() {
|
||||
c.ws.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case data, ok := <-c.machine.SerialTx:
|
||||
if !ok {
|
||||
c.write(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
if err := c.write(websocket.TextMessage, []byte{data}); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) write(messageType int, payload []byte) error {
|
||||
c.ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
return c.ws.WriteMessage(messageType, payload)
|
||||
}
|
||||
|
||||
func (c *connection) writeBytesToMachine(data []byte) {
|
||||
for _, b := range data {
|
||||
log.Printf("%c", b)
|
||||
c.machine.SerialRx <- b
|
||||
}
|
||||
}
|
||||
|
||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
c := &connection{machine: CreateMachine(), send: make(chan []byte, 256), ws: ws}
|
||||
|
||||
go c.writePump()
|
||||
|
||||
c.machine.Reset()
|
||||
c.readPump()
|
||||
}
|
|
@ -30,12 +30,16 @@ type Acia6551 struct {
|
|||
rxFull bool
|
||||
txEmpty bool
|
||||
|
||||
rxInterruptEnabled bool
|
||||
txInterruptEnabled bool
|
||||
|
||||
InterruptChan chan bool
|
||||
|
||||
RxChan chan byte
|
||||
TxChan chan byte
|
||||
}
|
||||
|
||||
func NewAcia6551() *Acia6551 {
|
||||
fmt.Println("Resetting the Acia6551")
|
||||
func NewAcia6551(cpu *Cpu) *Acia6551 {
|
||||
acia := &Acia6551{}
|
||||
acia.Reset()
|
||||
return acia
|
||||
|
@ -54,6 +58,11 @@ func (a *Acia6551) Reset() {
|
|||
a.lastTxWrite = 0
|
||||
a.lastRxRead = 0
|
||||
a.overrun = false
|
||||
|
||||
a.rxInterruptEnabled = false
|
||||
a.txInterruptEnabled = false
|
||||
|
||||
a.InterruptChan = make(chan bool, 0)
|
||||
}
|
||||
|
||||
func (a *Acia6551) Size() int {
|
||||
|
@ -91,7 +100,9 @@ func (a *Acia6551) RxWrite(data byte) {
|
|||
a.rx = data
|
||||
a.rxFull = true
|
||||
|
||||
// TODO: IRQs
|
||||
if a.rxInterruptEnabled {
|
||||
// getbus.assertIrq()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Acia6551) statusRegister() byte {
|
||||
|
@ -153,7 +164,6 @@ func (a *Acia6551) HasRx() bool {
|
|||
func (a *Acia6551) debugTxOutput() {
|
||||
if a.HasTx() {
|
||||
a.TxChan <- a.TxRead()
|
||||
fmt.Printf("%c", a.TxRead())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>i6502 Serial Terminal</title>
|
||||
<link href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
|
||||
var conn;
|
||||
var msg = $("#msg");
|
||||
var log = $("#log");
|
||||
|
||||
function appendLog(msg) {
|
||||
var d = log[0]
|
||||
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
|
||||
msg.appendTo(log)
|
||||
if (doScroll) {
|
||||
d.scrollTop = d.scrollHeight - d.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
$("#form").submit(function() {
|
||||
if (!conn) {
|
||||
return false;
|
||||
}
|
||||
if (!msg.val()) {
|
||||
return false;
|
||||
}
|
||||
conn.send(msg.val());
|
||||
msg.val("");
|
||||
return false
|
||||
});
|
||||
|
||||
if (window["WebSocket"]) {
|
||||
conn = new WebSocket("ws://{{$}}/ws");
|
||||
conn.onclose = function(evt) {
|
||||
appendLog($("<div><b>Connection closed.</b></div>"))
|
||||
}
|
||||
conn.onmessage = function(evt) {
|
||||
if (evt.data.indexOf("\n") == 0) {
|
||||
appendLog($("<br/>"))
|
||||
} else {
|
||||
appendLog($("<span/>").text(evt.data))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appendLog($("<div><b>Your browser does not support WebSockets.</b></div>"))
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
#log {
|
||||
background: black;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Droid Sans Mono', courier;
|
||||
margin: 0;
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
right: 0.5em;
|
||||
bottom: 3em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#form {
|
||||
padding: 0 0.5em 0 0.5em;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="log"></div>
|
||||
<form id="form">
|
||||
<input type="submit" value="Send" />
|
||||
<input type="text" id="msg" size="64"/>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,78 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/ariejan/i6502/bus"
|
||||
"github.com/ariejan/i6502/cpu"
|
||||
"github.com/ariejan/i6502/devices"
|
||||
"github.com/ariejan/i6502/memory"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Machine struct {
|
||||
// Outgoing bytes, using the serial interface
|
||||
SerialTx chan byte
|
||||
|
||||
// Incoming bytes, using the serial interface
|
||||
SerialRx chan byte
|
||||
|
||||
// The cpu, bus etc.
|
||||
cpu *cpu.Cpu
|
||||
bus *bus.Bus
|
||||
}
|
||||
|
||||
// Creates a new i6502 Machine instance
|
||||
func CreateMachine() *Machine {
|
||||
ram := memory.CreateRam()
|
||||
|
||||
rom, err := memory.LoadRomFromFile("rom/ehbasic.rom")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
acia6551 := devices.NewAcia6551()
|
||||
|
||||
bus, _ := bus.CreateBus()
|
||||
bus.Attach(ram, "32kB RAM", 0x0000)
|
||||
bus.Attach(rom, "16kB ROM", 0xC000)
|
||||
bus.Attach(acia6551, "ACIA 6551 Serial", 0x8800)
|
||||
|
||||
cpu := &cpu.Cpu{Bus: bus, ExitChan: make(chan int, 0)}
|
||||
|
||||
machine := &Machine{SerialTx: make(chan byte, 256), SerialRx: make(chan byte, 256), cpu: cpu, bus: bus}
|
||||
|
||||
// Run the CPU
|
||||
go func() {
|
||||
for {
|
||||
cpu.Step()
|
||||
}
|
||||
}()
|
||||
|
||||
// Connect acia6551 Tx to SerialTx
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data := <-acia6551.TxChan:
|
||||
machine.SerialTx <- data
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Connect SerialRx to acia6551 Rx
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data := <-machine.SerialRx:
|
||||
log.Printf("Rx: %c", data)
|
||||
acia6551.RxChan <- data
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
cpu.Reset()
|
||||
|
||||
return machine
|
||||
}
|
||||
|
||||
func (m *Machine) Reset() {
|
||||
m.cpu.Reset()
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":6123", "http service address")
|
||||
homeTmpl = template.Must(template.ParseFiles("home.html"))
|
||||
)
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found", 404)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
homeTmpl.Execute(w, r.Host)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", serveWs)
|
||||
|
||||
err := http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
BIN
rom/ehbasic.rom
BIN
rom/ehbasic.rom
Binary file not shown.
Loading…
Reference in New Issue