Skip to content

Commit

Permalink
First part of moving du-stream to separate repo.
Browse files Browse the repository at this point in the history
  • Loading branch information
PerMalmberg committed Jul 23, 2023
1 parent 6278ee8 commit 26ed6a6
Show file tree
Hide file tree
Showing 18 changed files with 615 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .busted
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
return {
_all = {
coverage = true
},
default = {
verbose = true
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ luac.out
*.x86_64
*.hex

**/luacov-html/*
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "external/du-serializer"]
path = external/du-serializer
url = [email protected]:PerMalmberg/du-serializer.git
[submodule "external/du-unit-testing"]
path = external/du-unit-testing
url = [email protected]:PerMalmberg/du-unit-testing.git
5 changes: 5 additions & 0 deletions .luacov
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
reporter = "html"
include = {
"src$",
"src%/.+$"
}
37 changes: 37 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.PHONY: clean test dev release
CLEAN_COV=if [ -e luacov.report.out ]; then rm luacov.report.out; fi; if [ -e luacov.stats.out ]; then rm luacov.stats.out; fi
PWD=$(shell pwd)

LUA_PATH := ./src/?.lua
LUA_PATH := $(LUA_PATH);$(PWD)/external/du-serializer/?.lua
LUA_PATH := $(LUA_PATH);$(PWD)/external/du-unit-testing/src/?.lua
LUA_PATH := $(LUA_PATH);$(PWD)/external/du-unit-testing/external/du-lua-examples/?.lua
LUA_PATH := $(LUA_PATH);$(PWD)/external/du-unit-testing/external/du-lua-examples/api-mockup/?.lua
LUA_PATH := $(LUA_PATH);$(PWD)/external/du-unit-testing/external/du-luac/lua/?.lua


all: release

lua_path:
@echo "$(LUA_PATH)"

clean_cov:
@$(CLEAN_COV)

clean_report:
@if [ -d ./luacov-html ]; then rm -rf ./luacov-html; fi

clean: clean_cov clean_report
@rm -rf out

test: clean
@echo Runnings unit tests on du-render
@LUA_PATH="$(LUA_PATH)" busted . --exclude-pattern=".*serializer.*"
@luacov
@$(CLEAN_COV)

dev: test
@LUA_PATH="$(LUA_PATH)" du-lua build --copy=development/main
@# Modify file inline. Actual regex is '/^\s*---.*$/d' but $ must be doubled in make file
@sed -i '/^\s*---.*$$/d' "./out/development/example/stream/screen.lua"
@sed -i '/^\s*---.*$$/d' "./out/development/example/render/main.lua"
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# du-stream
A data stream library for Dual Universe screens and emitter/receivers

A stream library for Dual Universe

This file was automatically generated by [DU-LuaC](https://github.com/wolfe-labs/DU-LuaC)'s interactive CLI.
21 changes: 21 additions & 0 deletions du-stream.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"runOnSave.commands": [
{
"match": ".*\\.lua",
"command": "make test",
"runIn": "terminal"
}
],
"Lua.workspace.library": [
"external/du-libs/",
"${3rd}/luassert/library",
"${3rd}/busted/library"
]
}
}
1 change: 1 addition & 0 deletions external/du-serializer
Submodule du-serializer added at 337226
1 change: 1 addition & 0 deletions external/du-unit-testing
Submodule du-unit-testing added at 6df3fe
30 changes: 30 additions & 0 deletions project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"cli": {
"fmtVersion": 3
},
"name": "du-stream",
"description": "A stream library for Dual Universe",
"sourcePath": "src",
"outputPath": "out",
"libs": [],
"builds": {
"main": {
"name": "main",
"type": "control",
"slots": {}
}
},
"targets": {
"development": {
"name": "development",
"minify": false,
"handleErrors": false
}
},
"internalPaths": [
"autoconf/",
"cpml/",
"pl/",
"utils/event"
]
}
Empty file added src/CommQueue.lua
Empty file.
241 changes: 241 additions & 0 deletions src/Stream.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
require("serializer") --QQQ from du-libs???
local serializer = {
Serialize = serialize,
Deserialize = deserialize
}
---@module "interface.Device"

---@alias CommQueue { queue:string[], waitingForReply:boolean, seq:integer }
---@alias ScreenLink {setScriptInput:fun(string), clearScriptOutput:fun(), getScriptOutput:fun():string}
---@alias Renderer {setOutput:fun(string), getInput:fun():string}
---@alias StreamData {output:CommQueue, input:CommQueue, lastReceived:number}

---@class Stream
---@field New fun(device:Device, parent:DataReceiver, timeout:number):Stream
---@field Tick fun()
---@field Write fun(data:table|string)
---@field WaitingToSend fun():boolean

--[[
Data format:
#remaining_chucks|seq|cmd|payload
Where:
- remaining_chunks is a 2-digit integer indicating how many chunks remains to complete the message. 0 means the last chuck.
- seq is a single digit seqence number, used to ensure we don't read the same data twice. It wraps around at 9.
- cmd is a two digit integer indicating what to do with the data
- payload is the actual payload, if any
]]
local headerSize = 1 -- #
+ 2 -- remaining_chucks
+ 1 -- |
+ 1 -- seq
+ 1 -- |
+ 2 -- cmd
+ 1 -- |

---@enum StreamCommand
local Command = {
Reset = 0,
Poll = 1,
Ack = 2,
Data = 3,
}

---Represents a stream between two entities.
local Stream = {}
Stream.__index = Stream

---Create a new Stream
---@param device Device
---@param parent DataReceiver
---@param timeout number The amount of time to wait for a reply before considering the connection broken.
---@return Stream
function Stream.New(device, parent, timeout) -- QQQ Block size as argument or enum
local s = {}
local blockSize = 1024 - headerSize -- Game allows max 1024 bytes in buffers

---@diagnostic disable-next-line: undefined-global
local getTime = getTime or system.getUtcTime

device.Clear()

---@type StreamData
local streamData = {
input = { queue = {}, waitingForReply = false, seq = 0 },
output = { queue = {}, waitingForReply = false, seq = 0 },
lastReceived = getTime()
}

local input = streamData.input
local output = streamData.output

---Assembles the package
---@param payload string
local function assemblePackage(payload)
local queue = input.queue

if #queue == 0 then
table.insert(queue, "")
end
queue[#queue] = queue[#queue] .. payload
end

---Completes a transmission
---@param count number
local function completeTransmission(count)
if count == 0 then
local queue = input.queue

local deserialized = serializer.Deserialize(queue[#queue])

parent.OnData(deserialized)
-- Last part, begin new data
queue[1] = ""
end
end

local function sameInput(commQueue, seq)
if seq == commQueue.seq then
return true
end

commQueue.seq = seq
return false
end

---Creates a block
---@param blockCount integer
---@param commQueue CommQueue
---@param cmd StreamCommand
---@param payload string?
---@return string
local function createBlock(blockCount, commQueue, cmd, payload)
commQueue.seq = (commQueue.seq + 1)
if commQueue.seq > 9 then
commQueue.seq = 0
end

payload = payload or ""
local b = string.format("#%0.2d|%0.1d|%0.2d|%s", blockCount, commQueue.seq, cmd, payload)
return b
end

---Reads incoming data
---@return StreamCommand|nil #Command
---@return number #Packet count
---@return string #Payload
local function readData()
local r = device.Read()

local count, seq, cmd, payload = r:match("^#(%d+)|(%d)|(%d+)|(.*)$")

payload = payload or ""
local validPacket = count and cmd
if validPacket then
cmd = tonumber(cmd)
count = tonumber(count) or 0
validPacket = validPacket and cmd and count
end

if not validPacket then
return nil, 0, ""
end

-- Since we can't clear the input when running in RenderScript, we have to rely on the sequence number to prevent duplicate data.
if sameInput(input, seq) then
return nil, 0, ""
end

return cmd, count, payload
end

---Call this function in OnUpdate
function s.Tick()
local cmd, count, payload = readData()

-- Did we get any input?
if cmd then
parent.OnTimeout(false, s)
streamData.lastReceived = getTime()

if device.IsController() then
if cmd == Command.Data then
assemblePackage(payload)
completeTransmission(count)
end
-- No need to handle ACK, it's just a trigger to move on.
output.waitingForReply = false
else
local sendAck = false

if cmd == Command.Poll or cmd == Command.Data then
if cmd == Command.Data then
assemblePackage(payload)
completeTransmission(count)
end

-- Send either ACK or actual data as a reply
if #output.queue > 0 then
device.Send(table.remove(output.queue, 1))
else
sendAck = true
end
elseif cmd == Command.Reset then
output.queue = {}
output.waitingForReply = false
input.queue = {}
input.waitingForReply = false
sendAck = true
end

if sendAck then
device.Send(createBlock(0, output, Command.Ack))
end
end
end

if getTime() - streamData.lastReceived >= timeout then
parent.OnTimeout(true, s)
streamData.lastReceived = getTime() -- Reset to trigger again
output.queue = {}
output.waitingForReply = false
end

if device.IsController() and not output.waitingForReply then
if #output.queue == 0 then
device.Send(createBlock(0, output, Command.Poll))
output.waitingForReply = true
else
device.Send(table.remove(output.queue, 1))
output.waitingForReply = true
end
end
end

---Write the data to the stream
---@param dataToSend table|string
function s.Write(dataToSend)
local data = serializer.Serialize(dataToSend)
local blockCount = math.ceil(data:len() / blockSize) - 1

while data:len() > blockSize - headerSize do
local part = data:sub(1, blockSize)
data = data:sub(blockSize + 1)
table.insert(output.queue, createBlock(blockCount, output, Command.Data, part))
blockCount = blockCount - 1
end

if data:len() > 0 then
table.insert(output.queue, createBlock(blockCount, output, Command.Data, data))
end
end

---Returns true if there is data waiting to be sent. Good for holding off additional write.
---@return boolean
function s.WaitingToSend() return #output.queue > 0 end

return setmetatable(s, Stream)
end

return Stream
Loading

0 comments on commit 26ed6a6

Please sign in to comment.