diff options
| author | Omar Rizwan <omar@omar.website> | 2023-06-30 18:11:12 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-06-30 18:11:12 +0000 |
| commit | 8a8a749eb2afbf6b0636c280554c32219fff1dd2 (patch) | |
| tree | a4fca4989a76d0e17c2d17ffe8ad1ad349111df2 | |
| parent | Left-align labels, watch for .folk.temp files (diff) | |
| parent | Fix load librt to work on folk0 also (diff) | |
| download | folk-8a8a749eb2afbf6b0636c280554c32219fff1dd2.tar.gz folk-8a8a749eb2afbf6b0636c280554c32219fff1dd2.zip | |
Merge pull request #42 from FolkComputer/osnr/camera-pipeline
Run Camera in subprocess
34 files changed, 1116 insertions, 737 deletions
@@ -29,7 +29,7 @@ ssh: ssh folk@$(FOLK_SHARE_NODE) flamegraph: - sudo perf record -F 997 --tid=$(shell pgrep tclsh8.6) -g -- sleep 30 + sudo perf record -F 997 --tid=$(shell pgrep tclsh8.6 | head -1) -g -- sleep 30 sudo perf script -f > out.perf ~/FlameGraph/stackcollapse-perf.pl out.perf > out.folded ~/FlameGraph/flamegraph.pl out.folded > out.svg @@ -194,17 +194,10 @@ Edit /boot/cmdline.txt https://github.com/raspberrypi/firmware/issues/1647#issue https://askubuntu.com/questions/1321443/very-long-startup-time-on-ubuntu-server-network-configuration (add `optional: true` to all netplan interfaces) -## Setup notes - -- get a separate computer (Raspberry Pi 4, probably). don't use your laptop. -- make as solid / permanent a mount as you can. you shouldn't be - scared of it falling and you shouldn't have to take it apart and put - it back together every time - ## License We intend to release this repo as open-source under an MIT, GPLv3, -Apache 2.0, or AGPLv3 license by June 2023 or earlier; by contributing +Apache 2.0, or AGPLv3 license in 2023; by contributing code, you're also agreeing to license your code under whichever license we end up choosing. @@ -1,25 +1,36 @@ -set wifi "Fios-LGTS3-5G" -catch { - if {$::tcl_platform(os) eq "Darwin"} { - set wifi [exec sh -c {/Sy*/L*/Priv*/Apple8*/V*/C*/R*/airport -I | sed -n "s/^.*SSID: \(.*\)$/\1/p"}] - } elseif {$::tcl_platform(os) eq "Linux"} { - set wifi [exec iwgetid -r] - } -} - -if {$wifi eq "cynosure"} { set ::shareNode "folk-omar.local" } \ -elseif {$wifi eq "Verizon_TWRHB4"} { set ::shareNode "folk-cwervo.local" } \ -elseif {$wifi eq "WONDERLAND"} { set ::shareNode "folk-haip.local" } \ -elseif {$wifi eq "GETNEAR"} { set ::shareNode "folk-ian.local" } \ -elseif {$wifi eq "Fios-LGTS3-5G" || $wifi eq "Fios-LGTS3"} { set ::shareNode "folk0.local" } \ -elseif {[string match "_onefact.org*" $wifi]} { set ::shareNode "folk-onefact.local" } \ -else { set ::shareNode "folk0.local" } - if {[info exists ::env(FOLK_SHARE_NODE)]} { set ::shareNode $::env(FOLK_SHARE_NODE) +} else { + try { + if {$::tcl_platform(os) eq "Darwin"} { + set wifi [exec sh -c {/Sy*/L*/Priv*/Apple8*/V*/C*/R*/airport -I | sed -n "s/^.*SSID: \(.*\)$/\1/p"}] + } elseif {$::tcl_platform(os) eq "Linux"} { + set wifi [exec iwgetid -r] + } + + if {$wifi eq "cynosure"} { + set ::shareNode "folk-omar.local" + } elseif {$wifi eq "Verizon_TWRHB4"} { + set ::shareNode "folk-cwervo.local" + } elseif {$wifi eq "WONDERLAND"} { + set ::shareNode "folk-haip.local" + } elseif {$wifi eq "GETNEAR"} { + set ::shareNode "folk-ian.local" + } elseif {$wifi eq "Fios-LGTS3-5G" || $wifi eq "Fios-LGTS3"} { + set ::shareNode "folk0.local" + } elseif {[string match "_onefact.org*" $wifi]} { + set ::shareNode "folk-onefact.local" + } else { + # there's no default. + } + } on error e { + set ::shareNode "none" + } } -if {$::shareNode eq "none"} { unset ::shareNode } + +if {[info exists ::shareNode] && $::shareNode eq "none"} { unset ::shareNode } if {[info exists ::argv] && $::argv eq "shareNode"} { - puts $::shareNode + if {[info exists ::shareNode]} { puts $::shareNode } \ + else { puts none } } @@ -1,5 +1,3 @@ -package require Tk - namespace eval Display { variable WIDTH 800 variable HEIGHT 600 @@ -9,12 +7,37 @@ namespace eval Display { variable green green variable red red - canvas .display -background black -width $Display::WIDTH -height $Display::HEIGHT - pack .display - wm title . $::nodename - wm geometry . [set Display::WIDTH]x[expr {$Display::HEIGHT + 40}]-0+0 ;# align to top-right of screen - - proc init {} {} + proc init {} { + package require Tk + + canvas .display -background black -width $Display::WIDTH -height $Display::HEIGHT + pack .display + wm title . $::thisProcess + wm geometry . [set Display::WIDTH]x[expr {$Display::HEIGHT + 40}]-0+0 ;# align to top-right of screen + + set ::chs [list] + bind . <KeyPress> {apply {{k} { + lappend ::chs $k + Retract keyboard claims the keyboard character log is /something/ + Assert keyboard claims the keyboard character log is $::chs + Step + }} %K} + + proc ::Display::commit {} { + .display delete all + + set displayList [list] + foreach match [Statements::findMatches {/someone/ wishes display runs /command/}] { + lappend displayList [dict get $match command] + } + + proc lcomp {a b} {expr {[lindex $a 2] == "text"}} + variable displayTime + set displayTime [time { + eval [join [lsort -command lcomp $displayList] "\n"] + }] + } + } proc fillRect {x0 y0 x1 y1 color} { uplevel [list Wish display runs [list .display create rectangle $x0 $y0 $x1 $y1 -fill $color]] @@ -30,37 +53,18 @@ namespace eval Display { } variable displayTime - proc commit {} { - .display delete all - - set displayList [list] - foreach match [Statements::findMatches {/someone/ wishes display runs /command/}] { - lappend displayList [dict get $match command] - } - proc lcomp {a b} {expr {[lindex $a 2] == "text"}} - variable displayTime - set displayTime [time { - eval [join [lsort -command lcomp $displayList] "\n"] - }] - } + # No-op until Display::init is called. + proc commit {} {} } -set ::chs [list] -proc handleKeyPress {k} { - lappend ::chs $k - Retract keyboard claims the keyboard character log is /something/ - Assert keyboard claims the keyboard character log is $::chs - Step -} -bind . <KeyPress> {handleKeyPress %K} - Assert when /program/ has error /err/ with info /info/ {{program err info} { puts stderr "Error: $program has error $err with info $info" }} source "hosts.tcl" if {[info exists ::shareNode]} { + puts "Will try to share with: $::shareNode" # copy to Pi if {[catch { # TODO: forward entry point @@ -69,18 +73,22 @@ if {[info exists ::shareNode]} { exec -ignorestderr ssh folk@$::shareNode -- sudo systemctl restart folk >@stdout & } err]} { puts "error syncing: $err" - puts "Proceeding without sharing to table." + puts "Proceeding without sharing." } else { source "lib/peer.tcl" peer $::shareNode - Assert "laptop.tcl" wishes $::nodename shares statements like \ - [list $::nodename is providing root virtual programs /rootVirtualPrograms/] + Assert "laptop.tcl" wishes $::thisProcess shares statements like \ + [list $::thisProcess is providing root virtual programs /rootVirtualPrograms/] } } -Display::init +try { + Display::init +} on error e { + puts stderr "Failed to init display: $e" +} loadVirtualPrograms Step @@ -43,6 +43,9 @@ namespace eval c { #include <inttypes.h> #include <stdint.h> #include <stdbool.h> + + #define __ENSURE(EXPR) if (!(EXPR)) { Tcl_SetResult(interp, "failed to convert argument from Tcl to C in: " #EXPR, NULL); return TCL_ERROR; } + #define __ENSURE_OK(EXPR) if ((EXPR) != TCL_OK) { return TCL_ERROR; } } variable code [list] variable objtypes [list] @@ -64,30 +67,37 @@ namespace eval c { } variable argtypes { - int { expr {{ int $argname; Tcl_GetIntFromObj(interp, $obj, &$argname); }}} - bool { expr {{ int $argname; Tcl_GetIntFromObj(interp, $obj, &$argname); }}} - int32_t { expr {{ int $argname; Tcl_GetIntFromObj(interp, $obj, &$argname); }}} - char { expr {{ char $argname = Tcl_GetString($obj)[0]; }}} - size_t { expr {{ size_t $argname; Tcl_GetLongFromObj(interp, $obj, (long *)&$argname); }}} - intptr_t { expr {{ intptr_t $argname; Tcl_GetLongFromObj(interp, $obj, (long *)&$argname); }}} - uint16_t { expr {{ uint16_t $argname; Tcl_GetIntFromObj(interp, $obj, (int *)&$argname); }}} - uint32_t { expr {{ uint32_t $argname; sscanf(Tcl_GetString($obj), "%"PRIu32, &$argname); }}} - uint64_t { expr {{ uint64_t $argname; sscanf(Tcl_GetString($obj), "%"PRIu64, &$argname); }}} + int { expr {{ int $argname; __ENSURE_OK(Tcl_GetIntFromObj(interp, $obj, &$argname)); }}} + double { expr {{ double $argname; __ENSURE_OK(Tcl_GetDoubleFromObj(interp, $obj, &$argname)); }}} + bool { expr {{ int $argname; __ENSURE_OK(Tcl_GetIntFromObj(interp, $obj, &$argname)); }}} + int32_t { expr {{ int $argname; __ENSURE_OK(Tcl_GetIntFromObj(interp, $obj, &$argname)); }}} + char { expr {{ + char $argname; + { + int _len_$argname; + char* _tmp_$argname = Tcl_GetStringFromObj($obj, &_len_$argname); + __ENSURE(_len_$argname >= 1); + $argname = _tmp_$argname[0]; + } + }}} + size_t { expr {{ size_t $argname; __ENSURE_OK(Tcl_GetLongFromObj(interp, $obj, (long *)&$argname)); }}} + intptr_t { expr {{ intptr_t $argname; __ENSURE_OK(Tcl_GetLongFromObj(interp, $obj, (long *)&$argname)); }}} + uint16_t { expr {{ uint16_t $argname; __ENSURE_OK(Tcl_GetIntFromObj(interp, $obj, (int *)&$argname)); }}} + uint32_t { expr {{ uint32_t $argname; __ENSURE(sscanf(Tcl_GetString($obj), "%"PRIu32, &$argname) == 1); }}} + uint64_t { expr {{ uint64_t $argname; __ENSURE(sscanf(Tcl_GetString($obj), "%"PRIu64, &$argname) == 1); }}} char* { expr {{ char* $argname = Tcl_GetString($obj); }} } Tcl_Obj* { expr {{ Tcl_Obj* $argname = $obj; }}} default { if {[string index $argtype end] == "*"} { expr {{ $argtype $argname; - if (sscanf(Tcl_GetString($obj), "($argtype) 0x%p", &$argname) != 1) { - return TCL_ERROR; - } + __ENSURE(sscanf(Tcl_GetString($obj), "($argtype) 0x%p", &$argname) == 1); }} } elseif {[regexp {([^\[]+)\[(\d*)\]$} $argtype -> basetype arraylen]} { # note: arraylen can be "" expr {{ int ${argname}_objc; Tcl_Obj** ${argname}_objv; - Tcl_ListObjGetElements(interp, $obj, &${argname}_objc, &${argname}_objv); + __ENSURE_OK(Tcl_ListObjGetElements(interp, $obj, &${argname}_objc, &${argname}_objv)); $basetype $argname\[${argname}_objc\]; { for (int i = 0; i < ${argname}_objc; i++) { @@ -113,6 +123,7 @@ namespace eval c { variable rtypes { int { expr {{ $robj = Tcl_NewIntObj($rvalue); }}} int32_t { expr {{ $robj = Tcl_NewIntObj($rvalue); }}} + double { expr {{ $robj = Tcl_NewDoubleObj($rvalue); }}} char { expr {{ $robj = Tcl_ObjPrintf("%c", $rvalue); }}} bool { expr {{ $robj = Tcl_NewIntObj($rvalue); }}} uint16_t { expr {{ $robj = Tcl_NewIntObj($rvalue); }}} diff --git a/lib/environment.tcl b/lib/environment.tcl index ae5a369b..28dd33fb 100644 --- a/lib/environment.tcl +++ b/lib/environment.tcl @@ -1,5 +1,3 @@ -namespace eval ::SerializableEnvironment {} - proc serializeEnvironment {} { set argnames [list] set argvalues [list] @@ -10,31 +8,9 @@ proc serializeEnvironment {} { lappend argvalues [uplevel [list set $name]] } } - # foreach importName [namespace eval ::SerializableEnvironment {namespace import}] { - # dict set env %$importName [namespace origin ::SerializableEnvironment::$importName] - # } - # foreach procName [info procs ::SerializableEnvironment::*] { - # if {![dict exists $env %[namespace tail $procName]]} { - # dict set env ^[namespace tail $procName] \ - # [list [info args $procName] [info body $procName]] - # } - # } list $argnames $argvalues } -# proc deserializeEnvironment {env} { -# dict for {name value} $env { -# if {[string index $name 0] eq "^"} { -# proc ::SerializableEnvironment::[string range $name 1 end] {*}$value -# } elseif {[string index $name 0] eq "%"} { -# namespace eval ::SerializableEnvironment \ -# [list namespace import -force $value] -# } else { -# set ::SerializableEnvironment::$name $value -# } -# } -# } - set ::Evaluator::totalTimesMap [dict create] set ::Evaluator::runsMap [dict create] diff --git a/lib/evaluator.tcl b/lib/evaluator.tcl index 8eacc659..6d462d09 100644 --- a/lib/evaluator.tcl +++ b/lib/evaluator.tcl @@ -6,8 +6,14 @@ namespace eval statement { $cc include <stdlib.h> $cc include <assert.h> - $cc struct statement_handle_t { int32_t idx; int32_t gen; } - $cc struct match_handle_t { int32_t idx; int32_t gen; } + $cc code { + typedef struct statement_handle_t { int32_t idx; int32_t gen; } statement_handle_t; + typedef struct match_handle_t { int32_t idx; int32_t gen; } match_handle_t; + } + $cc rtype statement_handle_t { $robj = Tcl_ObjPrintf("s%d:%d", $rvalue.idx, $rvalue.gen); } + $cc argtype statement_handle_t { statement_handle_t $argname; sscanf(Tcl_GetString($obj), "s%d:%d", &$argname.idx, &$argname.gen); } + $cc rtype match_handle_t { $robj = Tcl_ObjPrintf("m%d:%d", $rvalue.idx, $rvalue.gen); } + $cc argtype match_handle_t { match_handle_t $argname; sscanf(Tcl_GetString($obj), "m%d:%d", &$argname.idx, &$argname.gen); } $cc enum edge_type_t { EMPTY, PARENT, CHILD } @@ -562,7 +568,7 @@ namespace eval Statements { ;# singleton Statement store for (int i = 0; i < resultsCount; i++) { Tcl_Obj* matchObj = environmentToTclDict(results[i]); statement_handle_t id = results[i]->matchedStatementIds[0]; - Tcl_DictObjPut(NULL, matchObj, Tcl_ObjPrintf("__matcheeIds"), Tcl_ObjPrintf("{idx %d gen %d}", id.idx, id.gen)); + Tcl_DictObjPut(NULL, matchObj, Tcl_ObjPrintf("__matcheeIds"), Tcl_ObjPrintf("{s%d:%d}", id.idx, id.gen)); Tcl_ListObjAppendElement(NULL, ret, matchObj); ckfree((char *)results[i]); } @@ -666,7 +672,7 @@ namespace eval Statements { ;# singleton Statement store s->typePtr = &statement_t_ObjType; s->internalRep.otherValuePtr = &statements[i]; - Tcl_ListObjAppendElement(NULL, ret, Tcl_ObjPrintf("idx %d", i)); + Tcl_ListObjAppendElement(NULL, ret, Tcl_ObjPrintf("s%d:%d", i, statements[i].gen)); Tcl_ListObjAppendElement(NULL, ret, s); } return ret; @@ -674,9 +680,7 @@ namespace eval Statements { ;# singleton Statement store proc dot {} { set dot [list] dict for {id stmt} [all] { - set id [dict get $id idx] - - lappend dot "subgraph cluster_$id {" + lappend dot "subgraph <cluster_$id> {" lappend dot "color=lightgray;" set label [statement clause $stmt] @@ -684,23 +688,20 @@ namespace eval Statements { ;# singleton Statement store expr { [string length $line] > 80 ? "[string range $line 0 80]..." : $line } }] "\n"] set label [string map {"\"" "\\\""} [string map {"\\" "\\\\"} $label]] - lappend dot "s$id \[label=\"s$id: $label\"\];" + lappend dot "<$id> \[label=\"$id: $label\"\];" - dict for {matchId_ _} [statement parentMatchIds $stmt] { - set matchId [dict get $matchId_ idx] - if {$matchId == -1} continue - set parents [lmap edge [matchEdges $matchId_] {expr { - [dict get $edge type] == 1 ? "s[dict get $edge statement idx]" : [continue] + dict for {matchId _} [statement parentMatchIds $stmt] { + set parents [lmap edge [matchEdges $matchId] {expr { + [dict get $edge type] == 1 ? "[dict get $edge statement]" : [continue] }}] - lappend dot "m$matchId \[label=\"m$matchId <- $parents\"\];" - lappend dot "m$matchId -> s$id;" + lappend dot "<$matchId> \[label=\"$matchId <- $parents\"\];" + lappend dot "<$matchId> -> <$id>;" } lappend dot "}" - dict for {childId _} [statement childMatchIds $stmt] { - set childId [dict get $childId idx] - lappend dot "s$id -> m$childId;" + dict for {childMatchId _} [statement childMatchIds $stmt] { + lappend dot "<$id> -> <$childMatchId>;" } } return "digraph { rankdir=LR; [join $dot "\n"] }" @@ -826,7 +827,7 @@ namespace eval Evaluator { Tcl_ListObjAppendElement(interp, env, result->bindings[i].value); } - Tcl_ObjSetVar2(interp, Tcl_ObjPrintf("::matchId"), NULL, Tcl_ObjPrintf("idx %d gen %d", matchId.idx, matchId.gen), 0); + Tcl_ObjSetVar2(interp, Tcl_ObjPrintf("::matchId"), NULL, Tcl_ObjPrintf("m%d:%d", matchId.idx, matchId.gen), 0); tryRunInSerializedEnvironment(interp, lambda, env); } } @@ -994,20 +995,6 @@ namespace eval Evaluator { statement_t* collect = get(collectId); - // First, delete the existing match child. - { - for (size_t i = 0; i < collect->n_edges; i++) { - edge_to_match_t* edge = statementEdgeAt(collect, i); - if (edge->type == CHILD) { - match_handle_t childMatchId = edge->match; - matchGet(childMatchId)->recollectOnDestruction = false; - reactToMatchRemoval(interp, childMatchId); - matchRemove(childMatchId); - break; - } - } - } - Tcl_Obj* clause = collect->clause; int clauseLength; Tcl_Obj** clauseWords; Tcl_ListObjGetElements(interp, clause, &clauseLength, &clauseWords); @@ -1037,17 +1024,35 @@ namespace eval Evaluator { for (int j = 0; j < results[i]->matchedStatementIdsCount; j++) { parents[parentsCount++] = results[i]->matchedStatementIds[j]; } + ckfree((char *)results[i]); } + // Create a new match for the new collection. match_handle_t matchId = addMatchImpl(parentsCount, parents); match_t* match = matchGet(matchId); match->recollectOnDestruction = true; match->recollectCollectId = collectId; + // Run the When body within this new match. env = Tcl_DuplicateObj(env); Tcl_ListObjAppendElement(NULL, env, matches); - Tcl_ObjSetVar2(interp, Tcl_ObjPrintf("::matchId"), NULL, Tcl_ObjPrintf("idx %d gen %d", matchId.idx, matchId.gen), 0); + Tcl_ObjSetVar2(interp, Tcl_ObjPrintf("::matchId"), NULL, Tcl_ObjPrintf("m%d:%d", matchId.idx, matchId.gen), 0); tryRunInSerializedEnvironment(interp, lambda, env); + + // Finally, delete the old match child if any. + // (We do this last, _after_ adding the new match, because it helps with incrementality.) + { + for (size_t i = 0; i < collect->n_edges; i++) { + edge_to_match_t* edge = statementEdgeAt(collect, i); + if (edge->type == CHILD && !matchHandleIsEqual(edge->match, matchId)) { + match_handle_t childMatchId = edge->match; + matchGet(childMatchId)->recollectOnDestruction = false; + reactToMatchRemoval(interp, childMatchId); + matchRemove(childMatchId); + break; + } + } + } } $cc code { diff --git a/lib/language.tcl b/lib/language.tcl new file mode 100644 index 00000000..b706753a --- /dev/null +++ b/lib/language.tcl @@ -0,0 +1,67 @@ +# 'Language' utilities that extend and customize base Tcl. + +proc fn {name argNames body} { + uplevel [list set ^$name [list $argNames $body]] +} +rename unknown _original_unknown +# Trap resolution of commands so that they can call the lambda in +# lexical scope created by `fn`. +proc unknown {name args} { + if {[uplevel [list info exists ^$name]]} { + apply [uplevel [list set ^$name]] {*}$args + } else { + uplevel [list _original_unknown $name {*}$args] + } +} + +# Trim indentation in multiline quoted text. +proc undent {msg {whitespaceChars " "}} { + set msgLines [split $msg "\n"] + set maxLength [string length $msg] + + set regExp [subst -nocommands {([$whitespaceChars]*)[^$whitespaceChars]}] + + set indent [ + tcl::mathfunc::min {*}[ + lmap x $msgLines { + if {[regexp $regExp $x match whitespace]} { + string length $whitespace + } else { + lindex $maxLength + } + } + ] + ] + + join [ltrim [lmap x $msgLines {string range $x $indent end}]] "\n" +} +# Remove empty items at the beginning and the end of a list. +proc ltrim {list} { + set first [lsearch -not -exact $list {}] + set last [lsearch -not -exact [lreverse $list] {}] + return [ + if {$first == -1} { + list + } else { + lrange $list $first end-$last + } + ] +} + +proc python3 {args} { + exec python3 << [undent [join $args " "]] +} + +proc assert condition { + set s "{$condition}" + if {![uplevel 1 expr $s]} { + set errmsg "assertion failed: $condition" + if {[lindex $condition 1] eq "eq" && [string index [lindex $condition 0] 0] eq "$"} { + set errmsg "$errmsg\n[uplevel 1 [list set [string range [lindex $condition 0] 1 end]]] is not equal to [lindex $condition 2]" + } + return -code error $errmsg + } +} + +namespace import ::tcl::mathop::* +namespace import ::tcl::mathfunc::* diff --git a/lib/peer.tcl b/lib/peer.tcl index 5570f9a0..49f7bb26 100644 --- a/lib/peer.tcl +++ b/lib/peer.tcl @@ -3,51 +3,72 @@ lappend auto_path "./vendor" namespace eval clauseset { # only used for statement syndication - namespace export create add minus clauses + namespace export create add union difference clauses proc create {args} { set kvs [list] foreach k $args { lappend kvs $k true } dict create {*}$kvs } - proc add {sv k} { upvar $sv s; dict set s $k true } - proc minus {s t} { + proc add {sv stmt} { upvar $sv s; dict set s $stmt true } + + proc union {s t} { dict merge $s $t } + proc difference {s t} { dict filter $s script {k v} {expr {![dict exists $t $k]}} } + proc clauses {s} { dict keys $s } namespace ensemble create } -namespace eval Peers {} +namespace eval ::Peers {} -proc ::peer {node} { +proc ::peer {process} { package require websocket - namespace eval Peers::$node { + namespace eval ::Peers::$process { variable connected false variable prevShareStatements [clauseset create] + variable prevReceivedStatements [clauseset create] proc log {s} { - variable node - puts "$::nodename -> $node: $s" + variable process + puts "$::thisProcess -> $process: $s" } proc setupSock {} { - variable node - log "Trying to connect to: ws://$node:4273/ws" - variable sock [::websocket::open "ws://$node:4273/ws" [namespace code handleWs]] + variable process + log "Trying to connect to: ws://$process:4273/ws" + variable sock [::websocket::open "ws://$process:4273/ws" [namespace code handleWs]] } proc handleWs {sock type msg} { if {$type eq "connect"} { log "Connected" variable connected true + + # Establish a peering on their end, in the reverse + # direction, so they can send stuff back to us. + # It'll implicitly run in a ::Peers::X namespace on their end + # (because of how `run` is implemented above) + run { + variable chan [uplevel {set chan}] + variable connected true + variable prevShareStatements [clauseset create] + variable prevReceivedStatements [clauseset create] + proc run {msg} { + variable chan + ::websocket::send $chan text $msg + } + } } elseif {$type eq "disconnect"} { log "Disconnected" variable connected false variable prevShareStatements [clauseset create] + variable prevReceivedStatements [clauseset create] after 2000 [namespace code setupSock] } elseif {$type eq "error"} { log "WebSocket error: $type $msg" after 2000 [namespace code setupSock] - } elseif {$type eq "text" || $type eq "ping" || $type eq "pong"} { - # We don't handle responses yet. + } elseif {$type eq "text"} { + eval $msg + } elseif {$type eq "ping" || $type eq "pong"} { } else { error "Unknown WebSocket event: $type $msg" } @@ -55,10 +76,13 @@ proc ::peer {node} { proc run {msg} { variable sock - ::websocket::send $sock text $msg + ::websocket::send $sock text [list namespace eval ::Peers::$::thisProcess $msg] } - proc init {n} { variable node $n; setupSock } + proc init {n} { + variable process $n; setupSock + vwait ::Peers::${n}::connected + } init - } $node + } $process } diff --git a/lib/process.tcl b/lib/process.tcl index 67642dfa..b3aa9bf4 100644 --- a/lib/process.tcl +++ b/lib/process.tcl @@ -1,15 +1,59 @@ -set ::processPrelude { - source "main.tcl" - proc every {ms body} { - try $body - after $ms [list after idle [namespace code [info level 0]]] - } +namespace eval ::Zygote { + set cc [c create] + $cc include <unistd.h> + $cc proc ::Zygote::fork {} int { return fork(); } + # FIXME: waitpid + # FIXME: some kind of shared-memory log queue + $cc compile + + # The zygote is a process that's forked off during Folk + # startup. It can fork itself to create subprocesses on demand. - Assert $::nodename wishes $::nodename shares all claims + # Fork Folk to create the zygote process (= set the current state + # of Folk as the startup state for all subprocesses that will be + # spawned later) + proc init {} { + variable reader + variable writer + lassign [chan pipe] reader writer + set pid [fork] + if {$pid == 0} { + # We're in the child (the zygote). We will block waiting + # for commands from the parent (the original/main thread). + close $writer + fconfigure $reader -buffering line + zygote + + } else { + # We're still in the parent. The child (the zygote) is $pid. + close $reader + # We will send the zygote a message every time we want it to + # fork. + fconfigure $writer -buffering line + } + } + # Zygote's main loop. + proc zygote {} { + variable reader + set script "" + while {[gets $reader line] != -1} { + append script $line\n + if {[info complete $script]} { + set pid [fork] + if {$pid == 0} { + eval $script + exit 0 + } + set script "" + } + } + exit 0 + } - source "lib/peer.tcl" - peer "localhost" - vwait Peers::localhost::connected + proc spawn {code} { + variable writer + puts $writer $code + } } proc On-process {name body} { @@ -18,43 +62,37 @@ proc On-process {name body} { set ::Processes::${name}::body $body set ::Processes::${name}::this [uplevel {expr {[info exists this] ? $this : "<unknown>"}}] namespace eval ::Processes::$name { - variable tclfd [file tempfile tclfile tclfile.tcl] - # TODO: send it the serialized environment - set lambda [list {} $body] - set run [list Evaluator::runInSerializedEnvironment $lambda [list]] - puts $tclfd [join [list $::processPrelude $run] "\n"]; close $tclfd - - variable stdio [open "|tclsh8.6 $tclfile 2>@1" w+] - variable pid [pid $stdio] - - variable log [list] - proc handleReadable {} { - variable name - variable stdio - variable log - if {[gets $stdio line] >= 0} { - lappend log $line - puts "$name: $line" - Retract process $name has standard output log /l/ - Assert process $name has standard output log $log - Step - } elseif {[eof $stdio]} { close $stdio } - } - fconfigure $stdio -blocking 0 -buffering line - fileevent $stdio readable [namespace code handleReadable] + set processCode [list apply {{__name __body} { + set ::thisProcess $__name - if {$this ne "<unknown>"} { - Assert $this is running process $name - } + Assert <lib/process.tcl> wishes $::thisProcess shares all wishes + Assert <lib/process.tcl> wishes $::thisProcess shares all claims + + ::peer "localhost" + + Assert <lib/process.tcl> claims $::thisProcess has pid [pid] + Assert when $::thisProcess has pid /something/ [list {} $__body] + Step + vwait forever + }} $name $body] + + Zygote::spawn [list apply {{processCode} { + # A supervisor that wraps the subprocess. + set pid [Zygote::fork] + if {$pid == 0} { + eval $processCode + } else { + # TODO: Supervise the subprocess. + # waitpid $pid + # how to report outcomes to Folk? + # does it have an inbox? do we assert into Folk and let it retract? + } + }} $processCode] - proc handleUnmatch {} { - variable pid - variable name - catch {exec kill $pid} - Retract /someone/ is running process $name - Retract process $name has standard output log /something/ - namespace delete ::Processes::$name + When (non-capturing) $name has pid /pid/ { + On unmatch { + exec kill -9 $pid + } } - uplevel 2 [list On unmatch ::Processes::${name}::handleUnmatch] } } @@ -31,27 +31,17 @@ namespace eval Evaluator { } if {$this ne ""} { Say $this has error $err with info $::errorInfo - puts stderr "$::nodename: Error in $this, match $::matchId: $err\n$::errorInfo" + puts stderr "$::thisProcess: Error in $this, match $::matchId: $err\n$::errorInfo" } else { Say $::matchId has error $err with info $::errorInfo - puts stderr "$::nodename: Error in match $::matchId: $err\n$::errorInfo" + puts stderr "$::thisProcess: Error in match $::matchId: $err\n$::errorInfo" } } } } set ::logsize -1 ;# Hack to keep metrics working -proc fn {name argNames body} { - uplevel [list set ^$name [list $argNames $body]] -} -rename unknown _original_unknown -proc unknown {name args} { - if {[uplevel [list info exists ^$name]]} { - apply [uplevel [list set ^$name]] {*}$args - } else { - uplevel [list _original_unknown $name {*}$args] - } -} +source "lib/language.tcl" # invoke at top level, add/remove independent 'axioms' for the system proc Assert {args} { @@ -127,15 +117,22 @@ proc Every {event args} { uplevel [list When {*}$pattern "$body\nEvaluator::Unmatch $level"] } } + proc On {event args} { if {$event eq "process"} { if {[llength $args] == 2} { lassign $args name body } elseif {[llength $args] == 1} { - set name "${::matchId}-process" + # Generate a unique name. + set this [uplevel {expr {[info exists this] ? $this : "<unknown>"}}] + set subprocessId [uplevel {incr __subprocessId}] + set name "${this}-${::matchId}-${subprocessId}" set body [lindex $args 0] } - uplevel [list On-process $name $body] + # Serialize the lexical environment at the callsite so we can + # send that to the subprocess. + lassign [uplevel Evaluator::serializeEnvironment] argNames argValues + uplevel [list On-process $name [list apply [list $argNames $body] {*}$argValues]] } elseif {$event eq "unmatch"} { set body [lindex $args 0] @@ -158,9 +155,8 @@ proc After {n unit body} { } set ::committed [dict create] proc Commit {args} { - upvar this this set body [lindex $args end] - set key [list Commit [expr {[info exists this] ? $this : "<unknown>"}] {*}[lreplace $args end end]] + set key [list Commit [uplevel {expr {[info exists this] ? $this : "<unknown>"}}] {*}[lreplace $args end end]] lassign [uplevel Evaluator::serializeEnvironment] argNames argValues set lambda [list {this} [list apply [list $argNames $body] {*}$argValues]] Assert $key has program $lambda @@ -170,40 +166,36 @@ proc Commit {args} { dict set ::committed $key $lambda } -set ::nodename "[info hostname]-[pid]" - set ::stepCount 0 set ::stepTime "none" source "lib/peer.tcl" -proc Step {} { +proc StepImpl {} { incr ::stepCount - Assert $::nodename has step count $::stepCount - Retract $::nodename has step count [expr {$::stepCount - 1}] - set ::stepTime [time {Evaluator::Evaluate}] + Assert $::thisProcess has step count $::stepCount + Retract $::thisProcess has step count [expr {$::stepCount - 1}] + + Evaluator::Evaluate if {[namespace exists Display]} { Display::commit ;# TODO: this is weird, not right level } - foreach peerNs [namespace children Peers] { + foreach peerNs [namespace children ::Peers] { apply [list {peer} { variable connected if {!$connected} { return } set shareStatements [clauseset create] - if {[llength [Statements::findMatches [list /someone/ wishes $::nodename shares all statements]]] > 0} { - dict for {_ stmt} [Statements::all] { + set shareAllWishes [expr {[llength [Statements::findMatches [list /someone/ wishes $::thisProcess shares all wishes]]] > 0}] + set shareAllClaims [expr {[llength [Statements::findMatches [list /someone/ wishes $::thisProcess shares all claims]]] > 0}] + dict for {_ stmt} [Statements::all] { + if {($shareAllWishes && [lindex [statement clause $stmt] 1] eq "wishes") || + ($shareAllClaims && [lindex [statement clause $stmt] 1] eq "claims")} { clauseset add shareStatements [statement clause $stmt] } - } elseif {[llength [Statements::findMatches [list /someone/ wishes $::nodename shares all claims]]] > 0} { - dict for {_ stmt} [Statements::all] { - if {[lindex [statement clause $stmt] 1] eq "claims"} { - clauseset add shareStatements [statement clause $stmt] - } - } } - set matches [Statements::findMatches [list /someone/ wishes $::nodename shares statements like /pattern/]] + set matches [Statements::findMatches [list /someone/ wishes $::thisProcess shares statements like /pattern/]] lappend matches {*}[Statements::findMatches [list /someone/ wishes $peer receives statements like /pattern/]] foreach m $matches { set pattern [dict get $m pattern] @@ -215,12 +207,17 @@ proc Step {} { } variable prevShareStatements - set shareAssertStatements [clauseset clauses [clauseset minus $shareStatements $prevShareStatements]] - set shareRetractStatements [clauseset clauses [clauseset minus $prevShareStatements $shareStatements]] + set shareAssertStatements [clauseset difference $shareStatements $prevShareStatements] + set shareRetractStatements [clauseset difference $prevShareStatements $shareStatements] if {[llength $shareAssertStatements] > 0 || [llength $shareRetractStatements] > 0} { - run [list apply {{shareAssertStatements shareRetractStatements} { - foreach stmt $shareAssertStatements { Assert {*}$stmt } - foreach stmt $shareRetractStatements { Retract {*}$stmt } + run [list apply {{receivedAssertStatements receivedRetractStatements} { + upvar [uplevel {namespace current}]::prevReceivedStatements prevReceivedStatements + # TODO: Just track process provenance in the statements? + set prevReceivedStatements [clauseset union $prevReceivedStatements $receivedAssertStatements] + set prevReceivedStatements [clauseset difference $prevReceivedStatements $receivedRetractStatements] + + dict for {stmt _} $receivedAssertStatements { Assert {*}$stmt } + dict for {stmt _} $receivedRetractStatements { Retract {*}$stmt } Step }} $shareAssertStatements $shareRetractStatements] } @@ -228,6 +225,7 @@ proc Step {} { } $peerNs] [namespace tail $peerNs] } } +proc Step {} { set ::stepTime [time StepImpl] } source "lib/math.tcl" @@ -242,9 +240,66 @@ Assert when /__this/ has program code /__programCode/ {{__this __programCode} { Claim $__this has program [list {this} $__programCode] }} +set ::thisNode "[info hostname]" +set ::nodename $::thisNode ;# for backward compat + +namespace eval ::Heap { + # Folk has a shared heap among all processes on a given node + # (physical machine). + + # Memory allocated from the Folk heap should be accessible, at + # exactly the same virtual address, from any Folk process. + + proc init {} { + variable cc [c create] + $cc include <sys/mman.h> + $cc include <sys/stat.h> + $cc include <fcntl.h> + $cc include <unistd.h> + $cc include <stdlib.h> + $cc code { + size_t folkHeapSize = 100000000; // 100MB + uint8_t* folkHeapBase; + uint8_t* _Atomic folkHeapPointer; + } + # The memory mapping of the heap will be inherited by all + # subprocesses, since it's established before the creation of + # the zygote. + $cc proc folkHeapMount {} void { + int fd = shm_open("/folk-heap", O_RDWR | O_CREAT, S_IROTH | S_IWOTH | S_IRUSR | S_IWUSR); + ftruncate(fd, folkHeapSize); + folkHeapBase = (uint8_t*) mmap(0, folkHeapSize, + PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (folkHeapBase == NULL) { + fprintf(stderr, "heapMount: failed"); exit(1); + } + folkHeapPointer = folkHeapBase; + } + $cc proc folkHeapAlloc {size_t sz} void* { + if (folkHeapPointer + sz > folkHeapBase + folkHeapSize) { + fprintf(stderr, "heapAlloc: out of memory"); exit(1); + } + void* ptr = folkHeapPointer; + folkHeapPointer = folkHeapPointer + sz; + return (void*) ptr; + } + if {$::tcl_platform(os) eq "Linux"} { + $cc cflags -lrt + c loadlib [lindex [exec /usr/sbin/ldconfig -p | grep librt.so | head -1] end] + } + $cc compile + folkHeapMount + } +} +Heap::init + if {[info exists ::entry]} { - # This all only runs if we're in a primary Folk process; we don't - # want it to run in subprocesses (which also run main.tcl). + source "lib/process.tcl" + Zygote::init + + # Everything below here only runs if we're in the primary Folk + # process. + set ::thisProcess $::thisNode proc ::loadVirtualPrograms {} { set ::rootVirtualPrograms [dict create] @@ -258,7 +313,7 @@ if {[info exists ::entry]} { {*}[glob -nocomplain "user-programs/[info hostname]/*.folk"]] { loadProgram $programFilename } - Assert $::nodename is providing root virtual programs $::rootVirtualPrograms + Assert $::thisNode is providing root virtual programs $::rootVirtualPrograms # So we can retract them all at once if some other node connects and # wants to impose its root virtual programs: @@ -272,7 +327,7 @@ if {[info exists ::entry]} { # Are there foreign root virtual programs that should take priority over ours? foreach root $roots { - if {[dict get $root node] ne $::nodename} { + if {[dict get $root node] ne $::thisNode} { set chosenRoot $root break } @@ -318,12 +373,11 @@ if {[info exists ::entry]} { } dict set ::rootVirtualPrograms $programName $programCode - Assert $::nodename is providing root virtual programs $::rootVirtualPrograms - Retract $::nodename is providing root virtual programs $oldRootVirtualPrograms + Assert $::thisNode is providing root virtual programs $::rootVirtualPrograms + Retract $::thisNode is providing root virtual programs $oldRootVirtualPrograms Step } - source "lib/process.tcl" source "./web.tcl" source $::entry } diff --git a/pi/AprilTags.tcl b/pi/AprilTags.tcl new file mode 100644 index 00000000..a489a1f4 --- /dev/null +++ b/pi/AprilTags.tcl @@ -0,0 +1,63 @@ +source "pi/cUtils.tcl" + +namespace eval AprilTags { + rename [c create] apc + apc cflags -I$::env(HOME)/apriltag + apc include <apriltag.h> + apc include <tagStandard52h13.h> + apc include <math.h> + apc include <assert.h> + apc code { + apriltag_detector_t *td; + apriltag_family_t *tf; + } + defineImageType apc + + apc proc detectInit {} void { + td = apriltag_detector_create(); + tf = tagStandard52h13_create(); + apriltag_detector_add_family_bits(td, tf, 1); + td->nthreads = 2; + } + + apc proc detect {image_t gray} Tcl_Obj* { + assert(gray.components == 1); + image_u8_t im = (image_u8_t) { .width = gray.width, .height = gray.height, .stride = gray.width, .buf = gray.data }; + + zarray_t *detections = apriltag_detector_detect(td, &im); + int detectionCount = zarray_size(detections); + + Tcl_Obj* detectionObjs[detectionCount]; + for (int i = 0; i < detectionCount; i++) { + apriltag_detection_t *det; + zarray_get(detections, i, &det); + + int size = sqrt((det->p[0][0] - det->p[1][0])*(det->p[0][0] - det->p[1][0]) + (det->p[0][1] - det->p[1][1])*(det->p[0][1] - det->p[1][1])); + detectionObjs[i] = Tcl_ObjPrintf("id %d center {%f %f} corners {{%f %f} {%f %f} {%f %f} {%f %f}} size %d", + det->id, + det->c[0], det->c[1], + det->p[0][0], det->p[0][1], + det->p[1][0], det->p[1][1], + det->p[2][0], det->p[2][1], + det->p[3][0], det->p[3][1], + size); + } + + + zarray_destroy(detections); + Tcl_Obj* result = Tcl_NewListObj(detectionCount, detectionObjs); + return result; + } + + apc proc detectCleanup {} void { + tagStandard52h13_destroy(tf); + apriltag_detector_destroy(td); + } + + c loadlib $::env(HOME)/apriltag/libapriltag.so + apc compile + + proc init {} { + detectInit + } +} diff --git a/pi/Camera.tcl b/pi/Camera.tcl index 5cbc3dbb..963d1261 100644 --- a/pi/Camera.tcl +++ b/pi/Camera.tcl @@ -1,246 +1,258 @@ source "lib/c.tcl" source "pi/cUtils.tcl" -rename [c create] camc - -camc include <string.h> -camc include <math.h> +namespace eval Camera { + rename [c create] camc -camc include <errno.h> -camc include <fcntl.h> -camc include <sys/ioctl.h> -camc include <sys/mman.h> -camc include <asm/types.h> -camc include <linux/videodev2.h> + camc include <string.h> + camc include <math.h> -camc include <stdint.h> -camc include <stdlib.h> + camc include <errno.h> + camc include <fcntl.h> + camc include <sys/ioctl.h> + camc include <sys/mman.h> + camc include <asm/types.h> + camc include <linux/videodev2.h> -camc include <jpeglib.h> + camc include <stdint.h> + camc include <stdlib.h> -camc struct buffer_t { - uint8_t* start; - size_t length; -} -camc struct camera_t { - int fd; - uint32_t width; - uint32_t height; - size_t buffer_count; - buffer_t* buffers; - buffer_t head; -} + camc include <jpeglib.h> -camc code { - void quit(const char* msg) { - fprintf(stderr, "[%s] %d: %s\n", msg, errno, strerror(errno)); - exit(1); + camc struct buffer_t { + uint8_t* start; + size_t length; } + camc struct camera_t { + int fd; + uint32_t width; + uint32_t height; + size_t buffer_count; + buffer_t* buffers; + buffer_t head; + } + + camc code { + uint8_t* folkImagesBase; + + void quit(const char* msg) { + fprintf(stderr, "[%s] %d: %s\n", msg, errno, strerror(errno)); + exit(1); + } - int xioctl(int fd, int request, void* arg) { - for (int i = 0; i < 100; i++) { - int r = ioctl(fd, request, arg); - if (r != -1 || errno != EINTR) return r; - printf("[%x][%d] %s\n", request, i, strerror(errno)); + int xioctl(int fd, int request, void* arg) { + for (int i = 0; i < 100; i++) { + int r = ioctl(fd, request, arg); + if (r != -1 || errno != EINTR) return r; + printf("[%x][%d] %s\n", request, i, strerror(errno)); + } + return -1; } - return -1; } -} -defineImageType camc - -camc proc cameraOpen {char* device int width int height} camera_t* { - printf("device [%s]\n", device); - int fd = open(device, O_RDWR | O_NONBLOCK, 0); - if (fd == -1) quit("open"); - camera_t* camera = ckalloc(sizeof (camera_t)); - camera->fd = fd; - camera->width = width; - camera->height = height; - camera->buffer_count = 0; - camera->buffers = NULL; - camera->head.length = 0; - camera->head.start = NULL; - return camera; -} - -camc proc cameraInit {camera_t* camera} void { - struct v4l2_capability cap; - if (xioctl(camera->fd, VIDIOC_QUERYCAP, &cap) == -1) quit("VIDIOC_QUERYCAP"); - if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) quit("no capture"); - if (!(cap.capabilities & V4L2_CAP_STREAMING)) quit("no streaming"); - - struct v4l2_format format; - memset(&format, 0, sizeof format); - format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - format.fmt.pix.width = camera->width; - format.fmt.pix.height = camera->height; - format.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; - format.fmt.pix.field = V4L2_FIELD_NONE; - if (xioctl(camera->fd, VIDIOC_S_FMT, &format) == -1) quit("VIDIOC_S_FMT"); - - struct v4l2_requestbuffers req; - memset(&req, 0, sizeof req); - req.count = 4; - req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - req.memory = V4L2_MEMORY_MMAP; - if (xioctl(camera->fd, VIDIOC_REQBUFS, &req) == -1) quit("VIDIOC_REQBUFS"); - camera->buffer_count = req.count; - camera->buffers = calloc(req.count, sizeof (buffer_t)); - - size_t buf_max = 0; - for (size_t i = 0; i < camera->buffer_count; i++) { - struct v4l2_buffer buf; - memset(&buf, 0, sizeof buf); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_MMAP; - buf.index = i; - if (xioctl(camera->fd, VIDIOC_QUERYBUF, &buf) == -1) - quit("VIDIOC_QUERYBUF"); - if (buf.length > buf_max) buf_max = buf.length; - camera->buffers[i].length = buf.length; - camera->buffers[i].start = - mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, - camera->fd, buf.m.offset); - if (camera->buffers[i].start == MAP_FAILED) quit("mmap"); + defineImageType camc + + camc proc cameraOpen {char* device int width int height} camera_t* { + printf("device [%s]\n", device); + int fd = open(device, O_RDWR | O_NONBLOCK, 0); + if (fd == -1) quit("open"); + camera_t* camera = ckalloc(sizeof (camera_t)); + camera->fd = fd; + camera->width = width; + camera->height = height; + camera->buffer_count = 0; + camera->buffers = NULL; + camera->head.length = 0; + camera->head.start = NULL; + return camera; } - camera->head.start = ckalloc(buf_max); - printf("camera %d; bufcount %zu\n", camera->fd, camera->buffer_count); -} + camc proc cameraInit {camera_t* camera} void { + struct v4l2_capability cap; + if (xioctl(camera->fd, VIDIOC_QUERYCAP, &cap) == -1) quit("VIDIOC_QUERYCAP"); + if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) quit("no capture"); + if (!(cap.capabilities & V4L2_CAP_STREAMING)) quit("no streaming"); + + struct v4l2_format format; + memset(&format, 0, sizeof format); + format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + format.fmt.pix.width = camera->width; + format.fmt.pix.height = camera->height; + format.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; + format.fmt.pix.field = V4L2_FIELD_NONE; + if (xioctl(camera->fd, VIDIOC_S_FMT, &format) == -1) quit("VIDIOC_S_FMT"); + + struct v4l2_requestbuffers req; + memset(&req, 0, sizeof req); + req.count = 4; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; + if (xioctl(camera->fd, VIDIOC_REQBUFS, &req) == -1) quit("VIDIOC_REQBUFS"); + camera->buffer_count = req.count; + camera->buffers = calloc(req.count, sizeof (buffer_t)); + + size_t buf_max = 0; + for (size_t i = 0; i < camera->buffer_count; i++) { + struct v4l2_buffer buf; + memset(&buf, 0, sizeof buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + if (xioctl(camera->fd, VIDIOC_QUERYBUF, &buf) == -1) + quit("VIDIOC_QUERYBUF"); + if (buf.length > buf_max) buf_max = buf.length; + camera->buffers[i].length = buf.length; + camera->buffers[i].start = + mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, + camera->fd, buf.m.offset); + if (camera->buffers[i].start == MAP_FAILED) quit("mmap"); + } + camera->head.start = ckalloc(buf_max); -camc proc cameraStart {camera_t* camera} void { - for (size_t i = 0; i < camera->buffer_count; i++) { - struct v4l2_buffer buf; - memset(&buf, 0, sizeof buf); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_MMAP; - buf.index = i; - if (xioctl(camera->fd, VIDIOC_QBUF, &buf) == -1) quit("VIDIOC_QBUF"); - printf("camera_start(%zu): %s\n", i, strerror(errno)); + printf("camera %d; bufcount %zu\n", camera->fd, camera->buffer_count); } - enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (xioctl(camera->fd, VIDIOC_STREAMON, &type) == -1) + camc proc cameraStart {camera_t* camera} void { + for (size_t i = 0; i < camera->buffer_count; i++) { + struct v4l2_buffer buf; + memset(&buf, 0, sizeof buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + if (xioctl(camera->fd, VIDIOC_QBUF, &buf) == -1) quit("VIDIOC_QBUF"); + printf("camera_start(%zu): %s\n", i, strerror(errno)); + } + + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(camera->fd, VIDIOC_STREAMON, &type) == -1) quit("VIDIOC_STREAMON"); -} + } -camc code { -int camera_capture(camera_t* camera) { - struct v4l2_buffer buf; - memset(&buf, 0, sizeof buf); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_MMAP; - if (xioctl(camera->fd, VIDIOC_DQBUF, &buf) == -1) { - fprintf(stderr, "camera_capture: VIDIOC_DQBUF failed: %d: %s\n", errno, strerror(errno)); - return 0; + camc code { + int camera_capture(camera_t* camera) { + struct v4l2_buffer buf; + memset(&buf, 0, sizeof buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + if (xioctl(camera->fd, VIDIOC_DQBUF, &buf) == -1) { + fprintf(stderr, "camera_capture: VIDIOC_DQBUF failed: %d: %s\n", errno, strerror(errno)); + return 0; + } + memcpy(camera->head.start, camera->buffers[buf.index].start, buf.bytesused); + camera->head.length = buf.bytesused; + if (xioctl(camera->fd, VIDIOC_QBUF, &buf) == -1) { + fprintf(stderr, "camera_capture: VIDIOC_QBUF failed: %d: %s\n", errno, strerror(errno)); + return 0; + } + return 1; + } } - memcpy(camera->head.start, camera->buffers[buf.index].start, buf.bytesused); - camera->head.length = buf.bytesused; - if (xioctl(camera->fd, VIDIOC_QBUF, &buf) == -1) { - fprintf(stderr, "camera_capture: VIDIOC_QBUF failed: %d: %s\n", errno, strerror(errno)); - return 0; + + camc proc cameraFrame {camera_t* camera} int { + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + fd_set fds; + FD_ZERO(&fds); + FD_SET(camera->fd, &fds); + int r = select(camera->fd + 1, &fds, 0, 0, &timeout); + // printf("r: %d\n", r); + if (r == -1) quit("select"); + if (r == 0) { + printf("selection failed of fd %d\n", camera->fd); + return 0; + } + return camera_capture(camera); } - return 1; -} -} -camc proc cameraFrame {camera_t* camera} int { - struct timeval timeout; - timeout.tv_sec = 1; - timeout.tv_usec = 0; - - fd_set fds; - FD_ZERO(&fds); - FD_SET(camera->fd, &fds); - int r = select(camera->fd + 1, &fds, 0, 0, &timeout); - // printf("r: %d\n", r); - if (r == -1) quit("select"); - if (r == 0) { - printf("selection failed of fd %d\n", camera->fd); - return 0; + camc proc cameraDecompressRgb {camera_t* camera image_t dest} void { + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, camera->head.start, camera->head.length); + if (jpeg_read_header(&cinfo, TRUE) != 1) { + printf("Fail\n"); + exit(1); + } + jpeg_start_decompress(&cinfo); + + while (cinfo.output_scanline < cinfo.output_height) { + unsigned char *buffer_array[1]; + buffer_array[0] = dest.data + (cinfo.output_scanline) * dest.width * cinfo.output_components; + jpeg_read_scanlines(&cinfo, buffer_array, 1); + } + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); } - return camera_capture(camera); -} + camc proc cameraDecompressGray {camera_t* camera image_t dest} void { + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, camera->head.start, camera->head.length); + if (jpeg_read_header(&cinfo, TRUE) != 1) { + printf("Fail\n"); + exit(1); + } + cinfo.out_color_space = JCS_GRAYSCALE; + jpeg_start_decompress(&cinfo); -camc proc cameraDecompressRgb {camera_t* camera image_t dest} void { - struct jpeg_decompress_struct cinfo; - struct jpeg_error_mgr jerr; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_decompress(&cinfo); - jpeg_mem_src(&cinfo, camera->head.start, camera->head.length); - if (jpeg_read_header(&cinfo, TRUE) != 1) { - printf("Fail\n"); - exit(1); - } - jpeg_start_decompress(&cinfo); - - while (cinfo.output_scanline < cinfo.output_height) { - unsigned char *buffer_array[1]; - buffer_array[0] = dest.data + (cinfo.output_scanline) * dest.width * cinfo.output_components; - jpeg_read_scanlines(&cinfo, buffer_array, 1); - } - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); -} -camc proc cameraDecompressGray {camera_t* camera image_t dest} void { - struct jpeg_decompress_struct cinfo; - struct jpeg_error_mgr jerr; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_decompress(&cinfo); - jpeg_mem_src(&cinfo, camera->head.start, camera->head.length); - if (jpeg_read_header(&cinfo, TRUE) != 1) { - printf("Fail\n"); - exit(1); - } - cinfo.out_color_space = JCS_GRAYSCALE; - jpeg_start_decompress(&cinfo); - - while (cinfo.output_scanline < cinfo.output_height) { - unsigned char *buffer_array[1]; - buffer_array[0] = dest.data + (cinfo.output_scanline) * dest.width * cinfo.output_components; - jpeg_read_scanlines(&cinfo, buffer_array, 1); - } - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); -} -camc proc rgbToGray {image_t rgb} image_t { - uint8_t* gray = (uint8_t*) ckalloc(rgb.width * rgb.height * sizeof (uint8_t)); - for (int y = 0; y < rgb.height; y++) { - for (int x = 0; x < rgb.width; x++) { - // we're spending 10-20% of camera time here on Pi ... ?? - - int i = (y * rgb.width + x) * 3; - uint32_t r = rgb.data[i]; - uint32_t g = rgb.data[i + 1]; - uint32_t b = rgb.data[i + 2]; - // from https://mina86.com/2021/rgb-to-greyscale/ - uint32_t yy = 3567664 * r + 11998547 * g + 1211005 * b; - gray[y * rgb.width + x] = ((yy + (1 << 23)) >> 24); + while (cinfo.output_scanline < cinfo.output_height) { + unsigned char *buffer_array[1]; + buffer_array[0] = dest.data + (cinfo.output_scanline) * dest.width * cinfo.output_components; + jpeg_read_scanlines(&cinfo, buffer_array, 1); } + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + } + camc proc rgbToGray {image_t rgb} image_t { + uint8_t* gray = calloc(rgb.width * rgb.height, sizeof (uint8_t)); + for (int y = 0; y < rgb.height; y++) { + for (int x = 0; x < rgb.width; x++) { + // we're spending 10-20% of camera time here on Pi ... ?? + + int i = (y * rgb.width + x) * 3; + uint32_t r = rgb.data[i]; + uint32_t g = rgb.data[i + 1]; + uint32_t b = rgb.data[i + 2]; + // from https://mina86.com/2021/rgb-to-greyscale/ + uint32_t yy = 3567664 * r + 11998547 * g + 1211005 * b; + gray[y * rgb.width + x] = ((yy + (1 << 23)) >> 24); + } + } + return (image_t) { + .width = rgb.width, .height = rgb.height, + .bytesPerRow = rgb.width, + .data = gray + }; } - return (image_t) { - .width = rgb.width, .height = rgb.height, - .bytesPerRow = rgb.width, - .data = gray - }; -} -camc proc newImage {int width int height int components} image_t { - return (image_t) { - .width = width, .height = height, - .components = components, - .bytesPerRow = width*components, - .data = (uint8_t*) ckalloc(width*height*components) - }; -} -camc proc freeImage {image_t image} void { - ckfree(image.data); -} + camc import ::Heap::cc folkHeapAlloc as folkHeapAlloc + camc proc newImage {int width int height int components} image_t { + if (folkImagesBase == NULL) { + folkImagesBase = folkHeapAlloc(50000000); // 50MB + } + + // FIXME: This is a hack. + static int imageCount = 0; + imageCount = (imageCount + 1) % 20; + + uint8_t* data = folkImagesBase + imageCount * (width*components*height); + return (image_t) { + .width = width, + .height = height, + .components = components, + .bytesPerRow = width*components, + .data = data + }; + } + camc proc freeImage {image_t image} void { + // free(image.data); + } -c loadlib [expr {$tcl_platform(os) eq "Darwin" ? "/opt/homebrew/lib/libjpeg.dylib" : [lindex [exec /usr/sbin/ldconfig -p | grep libjpeg] end]}] -camc compile + c loadlib [expr {$tcl_platform(os) eq "Darwin" ? "/opt/homebrew/lib/libjpeg.dylib" : [lindex [exec /usr/sbin/ldconfig -p | grep libjpeg] end]}] + camc compile -namespace eval Camera { variable camera variable WIDTH @@ -251,7 +263,22 @@ namespace eval Camera { variable HEIGHT set WIDTH $width set HEIGHT $height - + + try { + while {1} { + set pid [exec lsof -t "/dev/video0"] + if {$pid eq ""} break + exec kill -9 $pid + } + } on error err { + puts "got an error when trying to claim the video input as our own: ${err}" + } + + # FIXME: This is bad. Something about the state of /dev/video0 makes the ioctl in cameraInit + # return "device busy" - sleeping helps... + exec sleep .5 + + variable camera set camera [cameraOpen "/dev/video0" $WIDTH $HEIGHT] cameraInit $camera cameraStart $camera @@ -260,7 +287,6 @@ namespace eval Camera { for {set i 0} {$i < 5} {incr i} { cameraFrame $camera } - set Camera::camera $camera } proc frame {} { @@ -304,70 +330,3 @@ if {([info exists ::argv0] && $::argv0 eq [info script]) || \ freeImage $rgb } } - - -namespace eval AprilTags { - rename [c create] apc - apc cflags -I$::env(HOME)/apriltag - apc include <apriltag.h> - apc include <tagStandard52h13.h> - apc include <math.h> - apc include <assert.h> - apc code { - apriltag_detector_t *td; - apriltag_family_t *tf; - } - defineImageType apc - - apc proc detectInit {} void { - td = apriltag_detector_create(); - tf = tagStandard52h13_create(); - apriltag_detector_add_family_bits(td, tf, 1); - td->nthreads = 2; - } - - apc proc detect {image_t gray} Tcl_Obj* { - assert(gray.components == 1); - image_u8_t im = (image_u8_t) { .width = gray.width, .height = gray.height, .stride = gray.width, .buf = gray.data }; - - zarray_t *detections = apriltag_detector_detect(td, &im); - int detectionCount = zarray_size(detections); - - Tcl_Obj* detectionObjs[detectionCount]; - for (int i = 0; i < detectionCount; i++) { - apriltag_detection_t *det; - zarray_get(detections, i, &det); - - int size = sqrt( - (det->p[0][0] - det->p[1][0]) * (det->p[0][0] - det->p[1][0]) + - (det->p[0][1] - det->p[1][1]) * (det->p[0][1] - det->p[1][1]) - ); - - detectionObjs[i] = Tcl_ObjPrintf("id %d center {%f %f} corners {{%f %f} {%f %f} {%f %f} {%f %f}} size %d", - det->id, - det->c[0], det->c[1], - det->p[0][0], det->p[0][1], - det->p[1][0], det->p[1][1], - det->p[2][0], det->p[2][1], - det->p[3][0], det->p[3][1], - size); - } - - - zarray_destroy(detections); - Tcl_Obj* result = Tcl_NewListObj(detectionCount, detectionObjs); - return result; - } - - apc proc detectCleanup {} void { - tagStandard52h13_destroy(tf); - apriltag_detector_destroy(td); - } - - c loadlib $::env(HOME)/apriltag/libapriltag.so - apc compile - - proc init {} { - detectInit - } -} @@ -73,53 +73,6 @@ namespace eval Display { } } -# Camera thread -namespace eval Camera { - variable WIDTH 1280 - variable HEIGHT 720 - variable statements [list] - - variable cameraThread [thread::create [format { - source pi/Camera.tcl - Camera::init %d %d - AprilTags::init - puts "Camera tid: [getTid]" - - set grayFrames [list] - while true { - # Hack: we free old images. Really this should be done on - # the main thread when it's actually done with them. - if {[llength $grayFrames] > 10} { - freeImage [lindex $grayFrames 0] - set grayFrames [lreplace $grayFrames 0 0] - } - set cameraTime [time { - set grayFrame [Camera::grayFrame] - set tags [AprilTags::detect $grayFrame] - lappend grayFrames $grayFrame - }] - set statements [list] - lappend statements [list camera claims the camera time is $cameraTime] - lappend statements [list camera claims the camera frame is $grayFrame] - foreach tag $tags { - lappend statements [list camera claims tag [dict get $tag id] has center [dict get $tag center] size [dict get $tag size]] - lappend statements [list camera claims tag [dict get $tag id] has corners [dict get $tag corners]] - } - - # send this script back to the main Folk thread - # puts "\n\nCommands\n-----\n[join $commands \"\n\"]" - thread::send -async "%s" [list set Camera::statements $statements] - } - } $WIDTH $HEIGHT [thread::id]]] - puts "Camera thread id: $cameraThread" - - Assert when $::nodename has step count /c/ {{c} { - foreach stmt $Camera::statements { - Say {*}$stmt - } - }} -} - try { set keyboardThread [thread::create [format { source "pi/Keyboard.tcl" diff --git a/play/fork-play.tcl b/play/fork-play.tcl new file mode 100644 index 00000000..920a8987 --- /dev/null +++ b/play/fork-play.tcl @@ -0,0 +1,23 @@ +source "lib/c.tcl" +set cc [c create] +$cc include <unistd.h> +$cc proc ::fork {} int { + return fork(); +} +$cc compile + +puts "In parent ([pid]). Forking" +set pid [fork] +if {$pid == 0} { + puts "In child ([pid]). Forking" + set pid2 [fork] + if {$pid2 == 0} { + puts "In grandchild ([pid]). Done" + exit 0 + } + puts "In child. Done" + exit 0 +} + +puts "In parent. Done" +while true {} diff --git a/play/zygote-play.tcl b/play/zygote-play.tcl new file mode 100644 index 00000000..358e59cd --- /dev/null +++ b/play/zygote-play.tcl @@ -0,0 +1,60 @@ +source "lib/c.tcl" +set cc [c create] +$cc include <unistd.h> +$cc proc ::fork {} int { + return fork(); +} +$cc compile + +namespace eval Zygote { + proc init {} { + variable writer + lassign [chan pipe] reader writer + set pid [fork] + if {$pid == 0} { + # We're in the child (the zygote). We will block waiting + # for commands from the parent (the original/main thread). + close $writer + + fconfigure $reader -buffering line + # Zygote's main loop: + set script "" + while {[gets $reader line] != -1} { + append script $line\n + if {[info complete $script]} { + # FIXME: This fork breaks it. + set pid [fork] + if {$pid == 0} { + eval $script + exit 0 + } + set script "" + } + } + exit 0 + + } else { + # We're still in the parent. The child (the zygote) is $pid. + close $reader + # We will send the zygote a message every time we want it to + # fork. + fconfigure $writer -buffering line + } + } + proc spawn {code} { + variable writer + puts $writer $code + } +} + +Zygote::init + +Zygote::spawn { + puts "hello from [pid]" +} +Zygote::spawn { + puts "wow from [pid]" +} + +after 3000 { puts done; set ::done true } +vwait ::done diff --git a/test/basic.tcl b/test/basic.tcl index af2bb16f..c39916dd 100644 --- a/test/basic.tcl +++ b/test/basic.tcl @@ -1,9 +1,3 @@ -proc assert condition { - set s "{$condition}" - if {![uplevel 1 expr $s]} { - return -code error "assertion failed: $condition" - } -} proc count condition { Statements::count $condition } @@ -11,7 +5,7 @@ proc count condition { Assert programOakland has program {{this} { Claim Omar lives in "Oakland" }} -Assert when $::nodename has step count /c/ {{c} { +Assert when $::thisProcess has step count /c/ {{c} { When Omar lives in /place/ { Claim $place is a place where Omar lives } diff --git a/test/commit.tcl b/test/commit.tcl index 2a8bdaed..1cf780b8 100644 --- a/test/commit.tcl +++ b/test/commit.tcl @@ -1,10 +1,3 @@ -proc assert condition { - set s "{$condition}" - if {![uplevel 1 expr $s]} { - return -code error "assertion failed: $condition" - } -} - Assert programBall has program {{this} { Commit { Claim $this has a ball at x 100 y 100 } diff --git a/test/cstructs.tcl b/test/cstructs.tcl index 17f0fb41..623cd53f 100644 --- a/test/cstructs.tcl +++ b/test/cstructs.tcl @@ -1,10 +1,3 @@ -proc assert condition { - set s "{$condition}" - if {![uplevel 1 expr $s]} { - return -code error "assertion failed: $condition" - } -} - set cc [c create] $cc struct Name { char* first; @@ -25,3 +18,19 @@ $cc compile puts [omar] assert {[dict get [omar] name last] eq "Rizwan"} + +set cc [c create] +$cc proc plusone {int a} int { + return a + 1; +} +$cc proc dostuff {void* v} int { + return 300; +} +$cc compile +assert {[plusone 3] eq 4} + +catch {plusone Wrong} err +assert {[string match {expected integer but got "Wrong"*} $err]} + +catch {dostuff hi} err +assert {[string match {failed to convert argument from Tcl to C*} $err]} diff --git a/test/joins.tcl b/test/joins.tcl index 799abe0c..57d80300 100644 --- a/test/joins.tcl +++ b/test/joins.tcl @@ -1,10 +1,3 @@ -proc assert condition { - set s "{$condition}" - if {![uplevel 1 expr $s]} { - return -code error "assertion failed: $condition" - } -} - Assert Omar is a person Assert Omar lives in "New York" Assert Elmo is a person diff --git a/test/negation.tcl b/test/negation.tcl index 0067783a..501d7295 100644 --- a/test/negation.tcl +++ b/test/negation.tcl @@ -1,9 +1,3 @@ -proc assert condition { - set s "{$condition}" - if {![uplevel 1 expr $s]} { - return -code error "assertion failed: $condition" - } -} Assert programNegation has program code { When /nobody/ is booping { set ::booping nope diff --git a/test/perf.tcl b/test/perf.tcl index 97ee79cc..186f997c 100644 --- a/test/perf.tcl +++ b/test/perf.tcl @@ -9,22 +9,22 @@ Assert when /name/ is a /animal/ {{name animal} { Assert when /node/ has step count /c/ {{node c} {}} Assert Bob is a cat -puts "$::nodename: No additional statements:" +puts "$::thisProcess: No additional statements:" puts " [run]" for {set i 0} {$i < 100} {incr i} { Assert $i } -puts "$::nodename: Asserted 100 statements:" +puts "$::thisProcess: Asserted 100 statements:" puts " [run]" Assert Omar is a human -puts "$::nodename: Asserted 100 statements + Omar is a human:" +puts "$::thisProcess: Asserted 100 statements + Omar is a human:" puts " [run]" -puts "$::nodename: Same:" +puts "$::thisProcess: Same:" puts " [run]" -puts "$::nodename: Same:" +puts "$::thisProcess: Same:" puts " [run]" -puts "$::nodename: Same:" +puts "$::thisProcess: Same:" puts " [run]" diff --git a/test/process.tcl b/test/process.tcl index 72dcb264..c69fe26c 100644 --- a/test/process.tcl +++ b/test/process.tcl @@ -8,8 +8,7 @@ Step Assert when we are running {{} { On process { - Assert <root> claims things are good - Step + Claim things are good } When things are good { @@ -17,24 +16,70 @@ Assert when we are running {{} { } }} Step -vwait good +vwait ::good Assert when we are running {{} { - puts "Core: $::nodename" On process { set n 0 while true { incr n Commit { Claim the counter is $n } - Step + if {$n > 10} { break } } } When the counter is /n/ { if {$n > 5} { - set ::done true + set ::ok true } } }} Step -vwait done +vwait ::ok + +Assert when we are running {{} { + On process { + Claim I am in a process + When I am in a process { + Commit { Claim we were in a process } + } + } + When we were in a process { + set ::wereinaprocess true + } +}} +Step +vwait ::wereinaprocess + +Assert when we are running {{} { + On process { + Wish $::thisProcess receives statements like [list /x/ claims the main process exists] + When the main process exists { + Commit { Claim the subprocess heard that the main process exists } + } + } + Claim the main process exists + When the subprocess heard that the main process exists { + set ::heard true + } +}} +Step +vwait ::heard + +Retract when we are running /anything/ +Step + +Assert when we are running {{} { + set x done + On process { + eval [python3 [subst { + print("Claim Python is $x") + }]] + } + When Python is done { + set ::pythondone true + } +}} +Step + +vwait ::pythondone diff --git a/test/shm.tcl b/test/shm.tcl new file mode 100644 index 00000000..c3d63fc7 --- /dev/null +++ b/test/shm.tcl @@ -0,0 +1,58 @@ +Assert we are running +Assert when we are running {{} { + On process { + set cc [c create] + $cc include <sys/mman.h> + $cc include <sys/stat.h> + $cc include <fcntl.h> + $cc include <unistd.h> + $cc include <stdlib.h> + $cc proc shmMount {char* name size_t size void* addr} void { + int fd = shm_open(name, O_RDWR | O_CREAT, S_IROTH | S_IWOTH | S_IRUSR | S_IWUSR); + ftruncate(fd, size); + void* ptr = mmap(addr, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0); + if (ptr == NULL || ptr != addr) { + fprintf(stderr, "shmMount: failed"); exit(1); + } + } + $cc proc blup {} void { + void* ptr = (void*)0x280000000; + shmMount("/folk-images", 1000000000, ptr); + + char* s = (char*)ptr; + snprintf(s, 100, "Hello!"); + } + $cc compile + blup + } + + On process { + set cc [c create] + $cc include <sys/mman.h> + $cc include <sys/stat.h> + $cc include <fcntl.h> + $cc include <unistd.h> + $cc include <stdlib.h> + $cc proc shmMount {char* name size_t size void* addr} void { + int fd = shm_open(name, O_RDWR | O_CREAT, S_IROTH | S_IWOTH | S_IRUSR | S_IWUSR); + ftruncate(fd, size); + void* ptr = mmap(addr, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0); + if (ptr == NULL || ptr != addr) { + fprintf(stderr, "shmMount: failed"); exit(1); + } + } + $cc proc blup {} void { + void* ptr = (void*)0x280000000; + shmMount("/folk-images", 1000000000, ptr); + + char* s = (char*)ptr; + printf("[%s]\n", s); + } + $cc compile + blup + } +}} +Step + +after 1000 {set done true} +vwait done diff --git a/test/trie.tcl b/test/trie.tcl index 9e353d58..92ad9efa 100644 --- a/test/trie.tcl +++ b/test/trie.tcl @@ -2,13 +2,6 @@ set t [trie create] trie add t {Omar is a person} 1 trie add t {Generic is a /y/} 2 -proc assert condition { - set s "{$condition}" - if {![uplevel 1 expr $s]} { - return -code error "assertion failed: $condition" - } -} - assert {[trie lookup $t {Omar is a person}] eq {1}} assert {[trie lookup $t {/p/ is a person}] eq {1 2}} assert {[trie lookup $t {Omar is a /x/}] eq {1}} diff --git a/test/with-all.tcl b/test/with-all.tcl index d628a9db..27e594bd 100644 --- a/test/with-all.tcl +++ b/test/with-all.tcl @@ -1,14 +1,3 @@ -proc assert condition { - set s "{$condition}" - if {![uplevel 1 expr $s]} { - set errmsg "assertion failed: $condition" - if {[lindex $condition 1] eq "eq" && [string index [lindex $condition 0] 0] eq "$"} { - set errmsg "$errmsg\n[uplevel 1 [list set [string range [lindex $condition 0] 1 end]]] is not equal to [lindex $condition 2]" - } - return -code error $errmsg - } -} - Assert programOakland has program code { Claim Omar lives in "Oakland" } diff --git a/user-programs/haippi7/laser-pi.tcl b/user-programs/haippi7/laser-pi.tcl index 601f2809..19b9702a 100644 --- a/user-programs/haippi7/laser-pi.tcl +++ b/user-programs/haippi7/laser-pi.tcl @@ -97,7 +97,7 @@ namespace eval Camera { } $WIDTH $HEIGHT [thread::id]]] puts "Camera thread id: $cameraThread" - Assert when $::nodename has step count /c/ { + Assert when $::thisProcess has step count /c/ { foreach stmt $Camera::statements { Say {*}$stmt } diff --git a/virtual-programs/camera.folk b/virtual-programs/camera.folk new file mode 100644 index 00000000..b865392e --- /dev/null +++ b/virtual-programs/camera.folk @@ -0,0 +1,37 @@ +if {$::isLaptop} return + +namespace eval ::Camera { + variable WIDTH 1280 + variable HEIGHT 720 +} + +On process { + source pi/Camera.tcl + source pi/AprilTags.tcl + Camera::init 1280 720 + AprilTags::init + + puts "Camera tid: [getTid]" + + while true { + set cameraTime [time { + set grayFrame [Camera::grayFrame] + }] + set aprilTime [time { + set tags [AprilTags::detect $grayFrame] + }] + + Commit { + Claim the camera frame is $grayFrame + + Claim the camera time is $cameraTime + Claim the AprilTag time is $aprilTime + + foreach tag $tags { + Claim tag [dict get $tag id] has center [dict get $tag center] size [dict get $tag size] + Claim tag [dict get $tag id] has corners [dict get $tag corners] + } + } + Step + } +} diff --git a/virtual-programs/new-program-web-editor.folk b/virtual-programs/new-program-web-editor.folk index 692b00e0..5797365b 100644 --- a/virtual-programs/new-program-web-editor.folk +++ b/virtual-programs/new-program-web-editor.folk @@ -113,9 +113,9 @@ Wish the web server handles route "/new" with handler { ws.close(); } ws.onmessage = (msg) => { - if (msg.data.startsWith("ERROR:")) { + if (msg.data.startsWith("Error:")) { const errorEl = document.getElementById("error"); - if (msg.data == "ERROR: {}") { + if (msg.data === "Error:") { errorEl.style.backgroundColor = ""; errorEl.innerText = ""; } else { @@ -153,7 +153,10 @@ Wish the web server handles route "/new" with handler { if {$::isLaptop} { Step } `); setTimeout(() => { - send(`list ERROR: [Statements::findMatches [list {${program}} has error /err/ with info /errorInfo/]]`); + send(` +set errors [Statements::findMatches [list {${program}} has error /err/ with info /errorInfo/]] +join [list "Error:" {*}[lmap e $errors {dict get $e errorInfo}]] "\n" +`); }, 500); } let jobid; diff --git a/virtual-programs/regions.folk b/virtual-programs/regions.folk index 1702bbfa..8f320b42 100644 --- a/virtual-programs/regions.folk +++ b/virtual-programs/regions.folk @@ -1,5 +1,4 @@ namespace eval ::vec2 { - namespace import ::tcl::mathop::+ ::tcl::mathop::- ::tcl::mathop::* proc add {a b} { list [+ [lindex $a 0] [lindex $b 0]] [+ [lindex $a 1] [lindex $b 1]] } @@ -21,8 +20,6 @@ namespace eval ::vec2 { proc dot {a b} { expr {[lindex $a 0]*[lindex $b 0] + [lindex $a 1]*[lindex $b 1]} } - namespace import ::tcl::mathfunc::max ::tcl::mathfunc::min - namespace import ::tcl::mathop::/ proc distanceToLineSegment {a v w} { set l2 [vec2 distance $v $w] if {$l2 == 0.0} { diff --git a/virtual-programs/shapes.folk b/virtual-programs/shapes.folk index 25c615c5..e5a67fe5 100644 --- a/virtual-programs/shapes.folk +++ b/virtual-programs/shapes.folk @@ -68,6 +68,8 @@ When /someone/ wishes /p/ draws a /color/ /shape/ offset /offsetVector/ & /p/ ha set y [expr {$y + $offsetY}] } + # puts "drawing $shape at $x $y" + set adjustedWidth [expr {$width * 0.25}] set x [expr { $x * 1.3}] set y [expr { $y * 1.25}] diff --git a/virtual-programs/tags-and-calibration.folk b/virtual-programs/tags-and-calibration.folk index f3155612..cf252de6 100644 --- a/virtual-programs/tags-and-calibration.folk +++ b/virtual-programs/tags-and-calibration.folk @@ -1,5 +1,10 @@ if {$::isLaptop} { return } +namespace eval ::Camera { + variable WIDTH 1280 + variable HEIGHT 720 +} + package require math::linearalgebra namespace import ::math::linearalgebra::add \ ::math::linearalgebra::sub \ diff --git a/virtual-programs/web-editor.folk b/virtual-programs/web-editor.folk index 6669d9fa..ae14f9b4 100644 --- a/virtual-programs/web-editor.folk +++ b/virtual-programs/web-editor.folk @@ -1,117 +1,121 @@ Wish the web server handles route {/page/(.*)$} with handler { if {[regexp -all {/page/(\d*)$} $path whole_match program_id]} { - set filename "../folk-printed-programs/$program_id.folk" + set filename "../folk-printed-programs/$program_id.folk" set fp [open $filename r] set file_data [read $fp] close $fp } elseif {[regexp -all {/page/(.*)$} $path whole_match program_id]} { - set filename "virtual-programs/$program_id.folk" + set filename "virtual-programs/$program_id.folk" set fp [open $filename r] set file_data [read $fp] close $fp } html [string map [list file_data [htmlEscape $file_data] program_id $program_id file_name $filename] { - <html> - <body> - <div> - <span id="status">Status</span> - <button onclick="handleSave()">Save</button> - <button id="print" onclick="handlePrint()">Print</button> - </div> - <textarea id="code" style="width: 100%;height: 95vh;">file_data</textarea> - <pre id="error"></pre> - <script> - const isVirtualProgram = 'file_name'.includes('virtual-programs'); + <html> + <body> + <div> + <span id="status">Status</span> + <button onclick="handleSave()">Save</button> + <button id="print" onclick="handlePrint()">Print</button> + </div> + <textarea id="code" style="width: 100%;height: 95vh;">file_data</textarea> + <pre id="error"></pre> + <script> + const isVirtualProgram = 'file_name'.includes('virtual-programs'); - if (isVirtualProgram) { - document.getElementById("print").disabled = true; - } + if (isVirtualProgram) { + document.getElementById("print").disabled = true; + } - const codeEle = document.getElementById("code"); - function uuidv4() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - ); - } + const codeEle = document.getElementById("code"); + function uuidv4() { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + } - // Cmd + S || Ctrl + S => Save - document.addEventListener('keydown', function(e) { - if ((window.navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey) && e.keyCode == 83) { - e.preventDefault(); - handleSave(); - } - }, false); - // Cmd + P || Ctrl + P => Print - document.addEventListener('keydown', function(e) { - if ((window.navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey) && e.keyCode == 80) { - e.preventDefault(); - handlePrint(); - } - }, false); + // Cmd + S || Ctrl + S => Save + document.addEventListener('keydown', function(e) { + if ((window.navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey) && e.keyCode == 83) { + e.preventDefault(); + handleSave(); + } + }, false); + // Cmd + P || Ctrl + P => Print + document.addEventListener('keydown', function(e) { + if ((window.navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey) && e.keyCode == 80) { + e.preventDefault(); + handlePrint(); + } + }, false); - let ws; - let send; - function wsConnect() { - ws = new WebSocket(window.location.origin.replace("http", "ws") + "/ws"); - send = function(s) { ws.send(s); } + let ws; + let send; + function wsConnect() { + ws = new WebSocket(window.location.origin.replace("http", "ws") + "/ws"); + send = function(s) { ws.send(s); } - ws.onopen = () => { - document.getElementById('status').innerHTML = "<span style=background-color:seagreen;color:white;>Connnected</span>"; - }; - ws.onclose = window.onbeforeunload = () => { - document.getElementById('status').innerHTML = "<span style=background-color:red;color:white;>Disconnnected</span>"; - setTimeout(() => { wsConnect(); }, 1000); - }; - ws.onerror = (err) => { - document.getElementById('status').innerText = "Error"; - console.error('Socket encountered error: ', err.message, 'Closing socket'); - ws.close(); - } - ws.onmessage = (msg) => { - if (msg.data.startsWith("ERROR:")) { - const errorEl = document.getElementById("error"); - if (msg.data == "ERROR: {}") { - errorEl.style.backgroundColor = ""; - errorEl.innerText = ""; - } else { - errorEl.style.backgroundColor = "#f55"; - errorEl.innerText = msg.data; - } - } - } - }; - wsConnect(); + ws.onopen = () => { + document.getElementById('status').innerHTML = "<span style=background-color:seagreen;color:white;>Connnected</span>"; + }; + ws.onclose = window.onbeforeunload = () => { + document.getElementById('status').innerHTML = "<span style=background-color:red;color:white;>Disconnnected</span>"; + setTimeout(() => { wsConnect(); }, 1000); + }; + ws.onerror = (err) => { + document.getElementById('status').innerText = "Error"; + console.error('Socket encountered error: ', err.message, 'Closing socket'); + ws.close(); + } + ws.onmessage = (msg) => { + if (msg.data.startsWith("Error:")) { + const errorEl = document.getElementById("error"); + if (msg.data === "Error:") { + errorEl.style.backgroundColor = ""; + errorEl.innerText = ""; + } else { + errorEl.style.backgroundColor = "#f55"; + errorEl.innerText = msg.data; + } + } + } + }; + wsConnect(); - function handleSave() { - const code = document.getElementById("code").value; - send(` - set fp [open file_name w] - puts -nonewline $fp {${code}} - close $fp - puts "Saved program_id.folk" - `); + function handleSave() { + const code = document.getElementById("code").value; + send(` + set fp [open file_name w] + puts -nonewline $fp {${code}} + close $fp + puts "Saved program_id.folk" + `); if (isVirtualProgram) { send(`EditVirtualProgram file_name {${code}}`) - } + } - setTimeout(() => { - send(`list ERROR: [Statements::findMatches [list program_id has error /err/ with info /errorInfo/]]`); - }, 500); - } - let jobid; - function handlePrint() { - const code = document.getElementById("code").value; - jobid = String(Math.random()); - send(`Assert web wishes to print program program_id with code {${code}} with job id {${jobid}}`); - setTimeout(500, () => { - send(`Retract web wishes to print program program_id with code {${code}} with job id {${jobid}}`); - }); - } - </script> - </body> - </html> + setTimeout(() => { + send(` +set errors [Statements::findMatches [list program_id has error /err/ with info /errorInfo/]] +join [list "Error:" {*}[lmap e $errors {dict get $e errorInfo}]] "\n" +`); + }, 500); + } + + let jobid; + function handlePrint() { + const code = document.getElementById("code").value; + jobid = String(Math.random()); + send(`Assert web wishes to print program program_id with code {${code}} with job id {${jobid}}`); + setTimeout(500, () => { + send(`Retract web wishes to print program program_id with code {${code}} with job id {${jobid}}`); + }); + } + </script> + </body> + </html> }] } @@ -32,7 +32,10 @@ proc handlePage {path contentTypeVar} { lappend l [subst { <li> <details> - <summary>$id: [htmlEscape [statement short $stmt]]</summary> + <summary style="[expr { + [lsearch -exact [statement clause $stmt] error] != -1 + ? "color: red" + : ""}]">$id: [htmlEscape [statement short $stmt]]</summary> <pre>[htmlEscape [statement clause $stmt]]</pre> </details> </li> @@ -101,6 +104,7 @@ proc handlePage {path contentTypeVar} { proc handleRead {chan addr port} { chan configure $chan -translation crlf gets $chan line; set firstline $line + # puts "Http: $chan $addr $port: $line" set headers [list] while {[gets $chan line] >= 0 && $line ne ""} { if {[regexp -expanded {^( [^\s:]+ ) \s* : \s* (.+)} $line -> k v]} { @@ -136,6 +140,7 @@ proc handleRead {chan addr port} { } close $chan } elseif {[::websocket::test $::serverSock $chan "/ws" $headers]} { + # puts "WS: $chan $addr $port" ::websocket::upgrade $chan # from now the handleWS will be called (not anymore handleRead). } else { puts "Closing: $chan $addr $port $headers"; close $chan } @@ -143,15 +148,28 @@ proc handleRead {chan addr port} { proc handleWS {chan type msg} { if {$type eq "connect" || $type eq "ping" || $type eq "pong"} { + # puts "Event $type from chan $chan" } elseif {$type eq "text"} { if {[catch {::websocket::send $chan text [eval $msg]} err] == 1} { if [catch { - puts stderr "$::nodename: Error on receipt: $err\n$::errorInfo" + puts stderr "$::thisProcess: Error on receipt: $err\n$::errorInfo" ::websocket::send $chan text $err - } err2] { puts "$::nodename: $err2" } + } err2] { puts "$::thisProcess: $err2" } + } + } elseif {$type eq "disconnect"} { + foreach peerNs [namespace children ::Peers] { + apply [list {disconnectedChan} { + variable chan + if {$chan eq $disconnectedChan} { + variable prevReceivedStatements + foreach stmt $prevReceivedStatements { + Retract {*}$stmt + } + } + } $peerNs] $chan } } else { - puts "$::nodename: Unhandled WS event $type $msg" + puts "$::thisProcess: Unhandled WS event $type on $chan ($msg)" } } |
