chiark / gitweb /
b1fe3de13f8a62bf562aed7999dfc814674775da
[trains.git] / hostside / gui
1 #!/usr/bin/wishx
2
3 load chiark_tcl_hbytes-1.so
4
5 #---------- general utilities ----------
6
7 set tk_strictMotif 1
8
9 tk_setPalette background black foreground white
10
11 source lib.tcl
12
13 set default_speedstep_list {0 1 10 20 35 50 65 80 95 110 126}
14
15 set picturepadx 10
16 set picturepady 10
17
18 proc pagew {page} { return ".picture-$page" }
19 proc debug {m} { puts $m }
20
21 proc sconn {m} {
22     global sconn
23     debug "=> $m"
24     puts $sconn $m
25 }
26
27 proc widgets-dgram {} {
28     global cpage geometry picturepadx picturepady
29     if {[string length $geometry]} { wm geometry . $geometry }
30     set sizes [exec ./gui-plan-$cpage --sizes]
31     set w [pagew $cpage]
32     frame $w
33     frame $w.picture -background {} \
34             -width [lindex $sizes 0] \
35             -height [lindex $sizes 1]
36     pack $w.picture -padx $picturepadx -pady $picturepady
37     pack $w
38 }
39
40 proc widgets-fullscreen-nowm {} {
41     pack propagate . false
42     foreach wh {width height} {
43         . configure -$wh [winfo screen$wh .]
44     }
45 }
46
47 proc bgerror {emsg} {
48     global errorCode errorInfo
49     catch {
50         puts stderr "UNEXPECTED BACKGROUND ERROR\n"
51         puts stderr "$errorCode\n$errorInfo\n$emsg"
52     }
53     exit 16
54 }
55
56 #---------- train set event registraton ----------
57
58 set event_dispatch_body {
59     append l " "
60 }
61 set event_selections {}
62
63 proc register-event {selections args re body} {
64     global event_dispatch_body event_selections
65
66     eval lappend event_selections $selections
67
68     foreach selection $selections {
69         if {[regexp {^\w} $selection]} {
70             error "selection $selection lacks context char"
71         }
72     }
73     if {[regexp {^\^\w} $re]} {
74         error "re $re never matches context char"
75     }
76
77     regsub -all {\W+} $re - proc
78     set proc "event/$proc/[join $args -]"
79     set suffix {}
80     set number 0
81     while {![catch { info args $proc$suffix }]} { set suffix [incr number] }
82     append proc $suffix
83
84     proc $proc [concat l $args] $body
85     set al ""
86     foreach a $args { append al " \$$a" }
87
88     append event_dispatch_body \
89             "    if {\[regexp [list $re] \$l dummy $args]} {\n" \
90             "        debug \"$proc$al\"\n" \
91             "        eval [list $proc] \[list \$l$al]\n" \
92             "        return\n" \
93             "    }\n"
94 }
95
96 #---------- handling of commands we issue ----------
97
98 proc scmd {onresult ctrlr commandstr args} {
99     # later, calls
100     #   eval [list $onresult-ok|nak|error $ackornakmessage] $args
101     global commands_queued
102     sconn $commandstr
103     lappend commands_queued [list $ctrlr $onresult $args]
104 }
105
106 proc scmd_result {oknakerr message reporterrmsg} {
107     global commands_queued
108     manyset [lindex $commands_queued 0] ctrlr onresult args
109     set commands_queued [lrange $commands_queued 1 end]
110     if {[string length $reporterrmsg]} {
111         report-problem "$ctrlr: $reporterrmsg"
112     }
113     set proc "$onresult-$oknakerr"
114     if {![string compare $oknakerr nak] && [catch { info args $proc }]} {
115         set proc "$onresult-err"
116     }
117     eval [list $proc $message] $args
118 }
119
120 register-event {} {} {^\+ack \S+ ok } { scmd_result ok $l "" }
121 register-event {} {train segment error} \
122         {^\+ack \S+ SignallingPredictedProblem (\S+) (\S+) \: (.*) $} {
123     set m $train
124     if {[string compare - $segment]} { append m " @$segment" }
125     append m ": $error"
126     scmd_result err $l $m
127 }
128 register-event {} {} {^\+ack } { scmd_result err $l $l }
129 register-event {} {} {^\+nack \S+ } { scmd_result nak $l $l }
130
131 proc routinecmd-nak {m args} { error "got nak to routine command: $m" }
132 proc routinecmd-err {m args} { }
133 proc routinecmd-ok {m args} { }
134
135 proc mustsucceed-err {m args} { error "unexpected error: $m" }
136 proc mustsucceed-ok {m args} { }
137
138 proc report-problem-report-stderr {m} { puts stderr "*** $m" }
139 set report_problem_report report-problem-report-stderr
140
141 proc report-problem {message} {
142     global report_problem_report
143     eval $report_problem_report [list $message]
144 }
145
146 proc widget-problem-report {} {
147     global problem_reports report_problem_report
148     set problem_reports "\n\n\n\n\n"
149     label .problem-report -anchor w -justify left -takefocus 0 \
150             -border 2 -relief sunken -width 80 -textvariable problem_reports
151     pack .problem-report -side top
152     set report_problem_report report-problem-report-widget
153 }
154
155 proc report-problem-report-widget {m} {
156     global problem_reports
157     set problem_reports [join [concat \
158                 [lrange [split $problem_reports "\n"] 1 end] \
159                 [list $m] \
160             ] "\n"]
161 }
162
163 #---------- movpos (overlay buttons, keybindings, execution) ----------
164
165 proc movpos-button-gvars {mid} {
166     upvar #0 mp_details($mid) details
167     if {![info exists details]} { return 0 }
168     uplevel 1 [list manyset $details cpage key seg feat poslocs]
169     uplevel 1 { set w [pagew $cpage].movpos-$mid }
170     upvar #0 mp_state($mid) state
171     uplevel 1 [list manyset $state posn]
172     return 1
173 }
174
175 proc movpos-button-sstate {mid posn} {
176     upvar #0 mp_state($mid) state
177     set state $posn
178     movpos-button-setdisplay $mid
179 }
180
181 proc movpos-all-unknown {} {
182     global mp_details
183     foreach mid [array names mp_details] {
184         movpos-button-sstate $mid ?
185     }
186 }
187
188 proc widgets-movpos {} {
189     global mp_details
190     foreach mid [array names mp_details] {
191         upvar #0 mp_state($mid) state
192         set state {?}
193         movpos-button-gvars $mid
194         set w [pagew $cpage].movpos-$mid
195         button $w -text $key -padx 0 -pady 0 -borderwidth 0 \
196                 -command [list movpos-invoked $mid "plan $cpage"]
197         movpos-button-setdisplay $mid
198     }
199 }
200
201 proc movpos-button-setdisplay {mid} {
202     # we want to display as much of these as possible:
203     #   position known ?   (actual position is done by button location)
204     #   moving or stable
205     #   whether a train's plan includes a different position
206     #   whether autopoint
207     movpos-button-gvars $mid
208     switch -exact $posn {
209         ?       { set poslocn 2;     set fg black; set bg white }
210         default { set poslocn $posn; set fg white; set bg black }
211     }
212     $w configure -background $bg -foreground $fg \
213             -activebackground $bg -activeforeground $fg
214     manyset [lindex $poslocs $poslocn] x y
215     place $w -anchor center -x $x -y $y
216 }
217
218 proc movpos-invoked {mid ctrlr} {
219     global movfeatcommand
220     movpos-button-gvars $mid
221     switch -exact $posn {
222         0 { set new_posn 1 }
223         default { set new_posn 0 }
224     }
225     scmd routinecmd $ctrlr "$movfeatcommand $seg $feat $new_posn"
226 }
227
228 register-event ?movpos_*_feat {seg feat posn_new} \
229         {^.movpos (\w+) feat (\w+) ([01]|\?) } {
230     set mid $seg/$feat
231     if {![movpos-button-gvars $mid]} return
232     set posn $posn_new
233     movpos-button-sstate $mid $posn
234 }
235
236 proc movpos-bindkey-1 {cpage key seg feat adj0x adj0y adj1x adj1y} {
237     global posdeviation picturepadx picturepady
238     manyset [subseg-end-get-centroid $cpage $seg $feat {}] mx my
239     set mid $seg/$feat
240     foreach posn {0 1} {
241         manyset [subseg-end-get-centroid $cpage $seg $feat $posn] x y
242         set dx [expr {$x-$mx}]; set dy [expr {$y-$my}]
243         set d [expr {sqrt($dx*$dx + $dy*$dy)}]
244         set mul [expr {$posdeviation / ($d + 1e-6)}]
245         set x [expr {$mx + $mul*$dx + $picturepadx + [set adj${posn}x]}]
246         set y [expr {$my + $mul*$dy + $picturepady + [set adj${posn}y]}]
247         lappend poslocs [list $x $y]
248     }
249     lappend poslocs [list [expr {$mx + $picturepadx + ($adj0x+$adj1x)*0.5}] \
250                           [expr {$my + $picturepady + ($adj0y+$adj1y)*0.5}]]
251     upvar #0 mp_details($mid) details
252     set details [list $cpage $key $seg $feat $poslocs]
253
254     bind . <Key-[string tolower $key]> [list movpos-invoked $mid "keyboard"]
255 }
256
257 #---------- computation of movpos button locations
258
259 proc layout-subseg-featmap {seg concfeatpos args} {
260     global subsegfeatmap
261     set subsegfeatmap($concfeatpos) $args
262 }
263     
264 proc layout-subseg-end {seg feat posn x y} {
265     global cpage
266     upvar #0 subsegfeatmap($feat$posn) mapped
267     if {[info exists mapped]} {
268         foreach {feat posn} $mapped {
269             layout-subseg-end $seg $feat $posn $x $y
270         }
271         return
272     }
273     upvar #0 ld_sse/${cpage}($seg/$feat$posn) sse
274     if {![info exists sse]} { set sse {{} {}} }
275     manyset $sse lx ly
276     lappend lx $x
277     lappend ly $y
278     set sse [list $lx $ly]
279     if {[string length $posn]} { layout-subseg-end $seg $feat {} $x $y }
280 }
281
282 proc subseg-end-get-centroid {cpage seg feat posn} {
283     upvar #0 ld_sse/${cpage}($seg/$feat$posn) sse
284     if {![info exists sse]} {
285         puts "skipping binding of unknown $seg/$feat$posn"
286         return -code return
287     }
288     manyset $sse lx ly
289     set sx 0; set sy 0; set n [llength $lx]
290     foreach x $lx y $ly { addexpr sx {$x}; addexpr sy {$y} }
291     return [list [expr {$sx * 1.0 / $n}] [expr {$sy * 1.0 / $n}]]
292 }
293
294 proc layout-data {} {
295     global cpage
296     upvar #0 ld_sse/$cpage sse
297     catch { unset sse }
298     source ../layout/ours.dgram.segmap-info
299     source ../layout/ours.dgram-$cpage.overlay-info
300
301     upvar #0 movpos_bindings($cpage) bindings
302     if {![info exists bindings]} {
303         puts "no movpos bindings for $cpage"
304         return
305     }
306     foreach binding $bindings {
307         if {[regexp \
308    {^([A-Z])([-+]\d*)?([-+]\d*)?([-+]\d*)?([-+]\d*)?\=(\w+)/([A-Z]+)$} \
309                  $binding dummy key adj0x adj0y adj1x adj1y seg feat]} {
310             if {![string length $adj1x]} { set adj1x $adj0x; set adj1y $adj0y }
311             movpos-bindkey-1 $cpage $key $seg $feat \
312                 $adj0x.0 $adj0y.0 $adj1x.0 $adj1y.0
313         } elseif {[regexp {^[A-Z]$} $binding] || [regexp {~} $binding]} {
314         } else {
315             error "incomprehensible binding $binding on page $cpage"
316         }
317     }
318 }
319
320 #---------- speed ----------
321
322 # variables:
323 #   $train_commanded($train)    $speed_step
324 #   $train_direction($train)    forwards|backwards or unset
325 #   $speedws                    [list $w ...]
326 #
327 # speed/${w}(...) aka s(...):
328 #   $s(ctrlr)                   controller
329 #   $s(train)                   train selected, or something not \w+
330 #   $s(optionmenu)              optionmenu widget name
331 #   $s(kind)                    abs or rel
332 #   $s(commanding)              step of command we have scmd'd, or unset
333 #   $s(queued)                  step of command we would like to queue
334 #                                or unset if n/a
335 #   $s(inhibit)   0   all is well, can command any speed
336 #                 1   train newly selected, only rel can command higher speed
337 #                 2   can only command same or lower speed
338 #
339 # We don't worry too much about races: in particular, we don't mind
340 # racing with other things trying to command the speed, and losing
341 # the odd increment/decrement.  But since we thread the requested
342 # speed via realtime, we do queue up our own increments/decrements
343 # while we're executing a speed command, to avoid loss of steps during
344 # quick motions.
345
346 # Interfaces for concrete controllers:
347 #   speedw-new $w $ctrlr
348 #   speedw-setstate $w disabled|normal        controller appears/disappears
349 #   speedw-userinput-abs $w $step
350 #   speedw-userinput-rel $w $stepmap
351 # where
352 #   eval {stepmap} [list $oldstep] => $newstep
353
354 set speedws {}
355
356 proc speedws-forall {command args} {
357     global speedws
358     foreach w $speedws { eval [list $command $w] $args }
359 }
360
361 proc speedws-fortrain {train command args} {
362     global speedws
363     foreach w $speedws {
364         upvar #0 speed/$w s
365         if {[string compare $s(train) $train]} continue
366         eval [list $command $w] $args
367     }
368 }
369
370 proc speedw-new {w ctrlr} {
371     upvar #0 speed/$w s
372     global speedws
373
374     lappend speedws $w
375     set s(ctrlr) $ctrlr
376     set s(inhibit) 0
377
378     frame $w -relief sunken -border 2
379     label $w.ctrlr -state disabled -text $s(ctrlr)
380     set s(optionmenu) [tk_optionMenu $w.train speed/${w}(train) {}]
381     $w.train configure -textvariable {} -width 15
382     label $w.speed -state disabled -width 4 \
383             -font -*-courier-bold-r-*-*-20-*-*-*-*-*-*-* \
384             -background black -foreground white
385     pack $w.ctrlr $w.train $w.speed -side left
386     
387     speedw-notrains $w "(starting)"
388 }
389 proc speedw-notrains {w whystr} {
390     $w.train configure -state disabled
391     speedw-train-noneselected $w $whystr
392 }
393 proc speedw-train-noneselected {w whystr} {
394     upvar #0 speed/$w s
395     set s(train) {}
396     $w.train configure -text $whystr
397     $w.speed configure -text -
398     speedw-inhibit $w
399 }
400
401 proc speedw-inhibit {w} {
402     upvar #0 speed/$w s
403     set s(inhibit) 2
404     $w.speed configure -foreground red
405 }
406 proc speedw-uninhibit {w max} {
407     upvar #0 speed/$w s
408     set r $s(inhibit)
409     if {$r>$max} { return -1 }
410     set s(inhibit) 0
411     $w.speed configure -foreground white
412     return $r
413 }
414
415 proc speedw-setstate {w disnorm} {
416     $w.ctrlr configure -state $disnorm
417     $w.speed configure -state $disnorm
418 }
419
420 proc speedw-train-selectnext {w} {
421     upvar #0 speed/$w s
422     set max [$s(optionmenu) index end]
423     for {set ix 0} {$ix <= $max} {incr ix} {
424         set v [$s(optionmenu) entrycget $ix -value]
425         if {![string compare $v $s(train)]} break
426     }
427     set activate [expr {($ix+1) % ($max+1)}]
428     $s(optionmenu) invoke $activate
429 }
430
431 proc speedw-train-selected {w t} {
432     upvar #0 speed/$w s
433     $w.train configure -text $t
434     set s(inhibit) 1
435     set s(train) $t
436     $w.speed configure -foreground white
437     speedw-redisplay-speed $w
438 }
439
440 proc speedw-redisplay-speed {w} {
441     upvar #0 speed/$w s
442     upvar #0 train_commanded($s(train)) gcommanded
443     upvar #0 train_direction($s(train)) gdirection
444     set t $gcommanded
445     if {[info exists gdirection]} {
446         switch -exact $gdirection {
447             forwards { set t "$t>" }
448             backwards { set t "<$t" }
449         }
450     }
451     $w.speed configure -text $t
452 }
453
454 proc speedw-train-direction {w dirchange} {
455     upvar #0 speed/$w s
456     if {![string length $s(train)]} return
457     scmd routinecmd $s(ctrlr) "direction $s(train) $dirchange"
458 }
459
460 proc speedw-trains-available {w l} {
461     upvar #0 speed/$w s
462     if {![llength $l]} { speedw-train-noneselected $w "(no trains)"; return }
463     $s(optionmenu) delete 0 end
464     $s(optionmenu) add radiobutton -label "(none)" -value {} \
465             -command [list speedw-train-noneselected $w "(no train selected)"]
466     set l [lsort $l]
467     foreach t $l {
468         $s(optionmenu) add radiobutton -label $t -value $t \
469                 -command [list speedw-train-selected $w $t]
470     }
471     $w.train configure -state normal
472     if {[llength $l]==1} {
473         $s(optionmenu) invoke 1
474     } elseif {[set ix [lsearch -exact $l $s(train)]] >= 0} {
475         $s(optionmenu) invoke [expr {$ix+1}]
476     } elseif {![string length $s(train)]} {
477         $s(optionmenu) invoke 0
478     } else {
479         $w.train configure -text "$s(train) (not present)"
480     }
481 }
482
483 proc speedw-userinput-abs {w speed} {
484     upvar #0 speed/$w s
485     if {![string length $s(train)]} return
486     set s(queued) $speed
487     speedw-check $w
488 }
489
490 proc speedw-check {w} {
491     upvar #0 speed/$w s
492     if {![string length $s(train)]} return
493     upvar #0 train_commanded($s(train)) gcommanded
494     upvar #0 train_direction($s(train)) gdirection
495     if {[info exists s(commanding)]} return
496     if {![info exists s(queued)]} return
497     set newspeed $s(queued)
498     unset s(queued)
499     if {$s(inhibit)} {
500         if {$newspeed > $gcommanded} return
501         speedw-uninhibit $w 2
502     }
503     set s(commanding) $newspeed
504     scmd speedw-commanded $s(ctrlr) "speed $s(train) $newspeed $gdirection" $w
505 }
506
507 proc speedw-commanded-nak {m args} { error "got nak from speed: $m" }
508 proc speedw-commanded-ok {m w} {
509     upvar #0 speed/$w s
510     unset s(commanding)
511     speedw-check $w
512 }
513 proc speedw-commanded-err {m w} {
514     upvar #0 speed/$w s
515     unset s(commanding)
516     speedw-inhibit $w
517     speedw-check $w
518 }
519
520 proc speedw-userinput-rel {w stepmap} {
521     upvar #0 speed/$w s
522     if {![string length $s(train)]} return
523     upvar #0 train_commanded($s(train)) gcommanded
524     if {[info exists s(queued)]} {
525         set oldspeed $s(queued)
526     } elseif {[info exists s(commanding)]} {
527         set oldspeed $s(commanding)
528     } else {
529         set oldspeed $gcommanded
530     }
531     set newspeed [eval $stepmap [list $oldspeed]]
532     speedw-userinput-abs $w $newspeed
533 }
534
535 proc speedw-userinput-rel-steps {w delta steplist} {
536     if {$delta<0} {
537         if {[speedw-uninhibit $w 2]>1} { incr delta 1 }
538         if {!$delta} return
539     } else {
540         speedw-uninhibit $w 1
541     }
542     speedw-userinput-rel $w [list speedw-stepmap-fromlist $steplist $delta]
543 }
544
545 proc speedws-train-problem {train} {
546     speedws-fortrain $train speedw-inhibit
547 }
548
549 register-event ?train_*_at {train direction} \
550         {^.train (\w+) at \S+ (forwards|backwards) } {
551     upvar #0 train_direction($train) dirn
552     set dirn $direction
553     speedws-fortrain $train speedw-redisplay-speed
554 }
555
556 register-event ?train_*_speed_commanding {train speed} \
557         {^.train (\w+) speed commanding (\d+) } {
558     upvar #0 train_commanded($train) cmd
559     set cmd $speed
560     speedws-fortrain $train speedw-redisplay-speed
561 }
562
563 proc speedws-stastate-hook {} {
564     global train_direction stastate
565     switch -exact -- $stastate {
566         Run {
567             set trains [array names train_direction]
568             speedws-forall speedw-trains-available $trains
569         }
570         Resolving {
571             movpos-all-unknown
572         }
573         Finalising {
574             speedws-forall speedw-notrains "($stastate)"
575         }
576         default {
577             catch { unset train_commanded }
578             speedws-forall speedw-notrains "($stastate)"
579         }
580     }
581 }
582
583 register-event &train_*_signalling-problem {train problem} \
584         {^\&train (\w+) signalling-problem (.*) $} {
585     global speedws
586     regsub {^(\S+) (\S+) \: } $problem {\1 @\2: } problem
587     report-problem "event: $problem"
588     speedws-train-problem $train
589 }
590
591 proc speedw-new-cooked {wunique desc} {
592     set w .inputs.$wunique
593     speedw-new $w $desc
594     pack $w -side left -padx 10
595     return $w
596 }
597
598 proc speedw-stepmap-fromlist {speedlist offset oldspeed} {
599     if {![llength $speedlist]} {
600         unset speedlist
601         upvar #0 default_speedstep_list speedlist
602     }
603     set ixabove 0
604     foreach entry $speedlist {
605         if {$entry==$oldspeed} { set ixbelow $ixabove; break }
606         if {$entry>$oldspeed} break
607         set ixbelow $ixabove
608         incr ixabove
609     }
610     set ix [expr {($offset>0 ? $ixbelow : $ixabove) + $offset}]
611     if {$ix<0} { return 0 }
612     if {$ix>=[llength $speedlist]} { return [lindex $speedlist end] }
613     return [lindex $speedlist $ix]
614 }
615
616 #---------- concrete input bindings ----------
617
618 proc ib-suppressions {args} {
619     set l {}
620     foreach supp $args {
621         set l [concat $l --redaction $supp --suppress]
622     }
623     return $l
624 }
625
626 proc ib-speedw-new {devid wunique desc} {
627     upvar #0 input/$devid in
628     set in(speedw) [speedw-new-cooked $wunique $desc]
629 }
630
631 #----- wheelmouse
632
633 proc ib-ev/wheelmouse/EV_REL/REL_WHEEL {devid value} {
634     upvar #0 input/$devid in
635     speedw-userinput-rel-steps $in(speedw) [expr {-$value}] {}
636 }
637
638 proc ib-selectnext {devid value} {
639     if {$value!=1} return
640     upvar #0 input/$devid in
641     speedw-train-selectnext $in(speedw)
642 }
643 proc ib-changedirection {devid value} {
644     upvar #0 input/$devid in
645     if {!$value} return
646     speedw-train-direction $in(speedw) change
647 }    
648
649 proc ib-ev/wheelmouse/EV_KEY/BTN_LEFT {devid value} {
650     ib-selectnext $devid $value
651 }
652 proc ib-ev/wheelmouse/EV_KEY/BTN_RIGHT {devid value} {
653     ib-changedirection $devid $value
654 }
655
656 proc ib-create/wheelmouse {devid wunique desc} {
657     ib-speedw-new $devid $wunique $desc
658 }
659
660 proc ib-wheelmouse-redactions {} {
661     return [ib-suppressions  \
662             {EV REL REL X} \
663             {EV REL REL Y}]
664 }
665
666 proc ib-evcmd/wheelmouse {devid target} {
667     return [ib-evcmd-construct $devid $target [concat \
668             [list --grab] [ib-wheelmouse-redactions]]]
669 }
670
671 #----- gamepad
672
673 proc ib-create/gamepad {devid wunique desc} {
674     ib-speedw-new $devid $wunique $desc
675 }
676
677 #----- ebuyer wireless keyboard
678
679 proc ib-create/ebwikeb {devid wunique} {
680     upvar #0 input/$devid in
681     set in(desc) "main keyboard"
682     ib-create/wheelmouse $devid $wunique $in(desc)
683     set in(modifiers) 0
684 }
685
686 proc ib-evcmd/ebwikeb {devid target} {
687     return [concat \
688             [list ./evdev-manip-ebwikeb --redact --stdin-monitor] \
689             [ib-wheelmouse-redactions] \
690             [ib-suppressions \
691                 {0x01 02} \
692                 {EV MSC} \
693                 {0x0c 01} \
694                 {0xffbc 88 0xffbc 00}]]
695 }
696
697 proc ib-ev/ebwikeb/EV_REL/REL_WHEEL {devid value} {
698     ib-ev/wheelmouse/EV_REL/REL_WHEEL $devid $value
699 }
700
701 proc ib-ebwikeb-modifier {devid value bitval} {
702     upvar #0 input/${devid}(modifiers) mod
703     if {$value} {
704         set mod [expr {$mod | $bitval}]
705     } else {
706         set mod [expr {$mod & ~$bitval}]
707     }
708 }
709 proc ib-ev/ebwikeb/EV_KEY/KEY_LEFTSHIFT {devid value} {
710     ib-ebwikeb-modifier $devid $value 0x0001
711 }
712 proc ib-ev/ebwikeb/EV_KEY/KEY_RIGHTSHIFT {devid value} {
713     ib-ebwikeb-modifier $devid $value 0x0002
714 }
715 proc ib-ev/ebwikeb/EV_KEY/KEY_LEFTCTRL {devid value} {
716     ib-ebwikeb-modifier $devid $value 0x0100
717 }
718 proc ib-ev/ebwikeb/EV_KEY/KEY_RIGHTCTRL {devid value} {
719     ib-ebwikeb-modifier $devid $value 0x0200
720 }
721 proc ib-ev/ebwikeb/EV_KEY/KEY_CAPSLOCK {devid value} {
722     ib-ebwikeb-modifier $devid $value 0x0400
723 }
724
725 proc ib-ev/ebwikeb/0xffbc_88/0xffbc_0d {devid value} {
726     upvar #0 input/$devid in
727     if {!$value} return
728     if {$in(modifiers) & 0x00ff} {
729         ib-selectnext $devid $value
730     } else {
731         ib-changedirection $devid $value
732     }
733 }
734
735 proc ib-ev/ebwikeb/EV_KEY/KEY_BOOKMARKS {devid value} {
736     upvar #0 input/$devid in
737     if {!$value} return
738     if {!($in(modifiers) & 0xff00)} return
739     if {$in(modifiers) & 0x00ff} {
740         set how stop
741     } else {
742         set how auto
743     }
744     scmd routinecmd $in(desc) "!realtime $how"
745 }
746
747 #----- static keybindings speed `controller'
748
749 proc bind-keyboard-speed {kslow kfast kseltrain kreverse desc} {
750     set wunique [get-unique keyboardspeed]
751     set w [speedw-new-cooked $wunique $desc]
752     foreach delta {-1 +1} sf {slow fast} {
753         bind . <Key-[set k$sf]> [list speedw-userinput-rel-steps $w $delta {}]
754     }
755     bind . <Key-$kseltrain> [list speedw-train-selectnext $w]
756     bind . <Key-$kreverse> [list speedw-train-direction $w change]
757     speedw-setstate $w normal
758 }
759
760 #----- Joytech "Neo S" USB PC gamepad
761
762 proc hidraw-descriptors/gamepad-neo-s {} {
763     return xx
764 }
765
766 #proc hidraw-readable/gamepad-neo-s {chan hidraw devid} {
767 #    upvar #0 hidraw/$hidraw raw
768 #    # In my tests with tcl8.3 and tcl8.5, "read chan numbytes"
769 #    # on a nonblocking binary channel does only one read(2)
770 #    # provided that read(2) returns less than requested
771 #    while 1 {
772 #       set msg [hbytes raw2h [read $chan 256]]
773 #       if {![hbytes 
774 #       switch -glob [bhtes $msg {
775 #           4a* { set want 8 }
776 #           {} { set want 64 }
777 #           * { error "unknown report number $raw(buf)" }
778 #       }
779 #       if {$want > $sofar} {
780 #           set got [read $chan [expr {$want - $sofar}]]
781 #           
782 #       }
783 #
784 #       set sofar [hbytes length $raw(buf)]
785 #       if {!$sofar} {
786 #           set got [read $chan 64]
787 #       } else {
788 #           
789 #       }
790         
791
792 #---------- input device evdev binding ----------
793
794 proc ib-evcmd-construct {devid target xargs} {
795     upvar #0 input/$devid in
796     if {[llength $target] > 1} {
797         debug "ib $devid - multiple devices, not supported"
798         return {}
799     }
800     manyset [lindex $target 0] ev sysfs
801     if {[regexp { } $ev]} { error "event device `$ev' contains space" }
802     return [concat \
803             [list ./evdev-manip --redact] $xargs \
804             [list --stdin-monitor \
805                   --expect-sysfs /sys$sysfs/$ev/dev \
806                   /dev/input/$ev]]
807 }
808
809 proc bind-input-core {devid devkind devinfo concrete concargs} {
810     global input_bindings
811     lappend input_bindings [list $devkind $devid $devinfo $concrete $concargs]
812 }
813
814 proc bind-input {bus vendor product version concrete args} {
815     bind-input-core evdev:$bus:$vendor:$product:$version \
816         evdev [list $bus $vendor $product $version] \
817         $concrete $args
818 }
819
820 proc bind-input-static {event sysfs concrete args} {
821     bind-input-core [get-unique static] \
822         static [list $event $sysfs] \
823         $concrete $args
824 }
825
826 proc bind-input-raw {devtype concrete args} {
827     bind-input-core hidraw:[get-unique $devtype] \
828         hidraw [list $devtype] \
829         $concrete $args
830 }
831
832 proc widgets-input-bindings {} {
833     global input_bindings
834     foreach binding $input_bindings {
835         manyset $binding devkind devid devinfo concrete concargs
836         set cid [get-unique $concrete]
837         upvar #0 input/$devid in
838         set in(laststart) 0
839         set in(concrete) $concrete
840         eval [list ib-create/$concrete $devid $cid] $concargs
841     }
842     pack .inputs -side top -fill x
843 }
844
845 # input/$devid becomes `in' via upvar:
846 #  $in(chan)        channel open onto evdev-manip;
847 #                   unset if none, or hidraw, or something
848 #  $in(laststart)   last start time, [clock seconds]
849 #                      at every event we set this the current time
850 #                      but we insist on adding at least 5s
851 #                      and if that would make it > current time +15s
852 #                      we don't start
853 #  $in(speedw)      optional, may be set by ib-create
854
855 # hidraw/hidrawN becomes `hr' via upvar:
856 #  $raw(devid)      $devid (see above)
857 #  $raw(chan)       channel open onto /dev/hidrawN
858
859 proc input-concrete-start-try {devid concrete} {
860     global inputretryadd inputretrymax
861     upvar #0 input/$devid in
862     set now [clock seconds]
863     set newlast [expr {$in(laststart) + $inputretryadd}]
864     if {$newlast > $now + $inputretrymax} { return 0 }
865     if {$newlast < $now} { set newlast $now }
866     set in(laststart) $newlast
867 }
868
869 proc input-bindings-list {devkind} {
870     global input_bindings
871     set o {}
872     foreach b $input_bindings {
873         manyset $b dk
874         if {[string compare $dk $devkind]} continue
875         lappend o $b
876     }
877     return $o
878 }
879
880 proc scan-input-bindings {} {
881     global errorInfo errorCode unmatched_notified old_hidraws
882     global input_bindings scaninputinterval
883     global input_rawbindings
884
885     after $scaninputinterval scan-input-bindings
886
887     # scan /proc/bus/input/devices for appropriate evdevs
888     # results go in $target($devid)
889     if {[catch {
890         set f [open /proc/bus/input/devices]
891     } emsg]} {
892         if {[string match {POSIX ENOENT *} $errorCode]} return
893         error $emsg $errorInfo $errorCode
894     }
895     while 1 {
896         set r [gets $f l]
897         if {$r <= 0} {
898             if {[info exists v(devid)] &&
899                 [info exists v(sysfs)] &&
900                 [info exists v(event)]} {
901                 lappend target($v(devid)) [list $v(event) $v(sysfs)]
902             }
903             catch { unset v }
904         }
905         if {$r < 0} {
906             break
907         }
908         append l "\n"
909         if {[regexp \
910  {^I: Bus=(\w+) Vendor=(\w+) Product=(\w+) Version=(\w+)\s} \
911                  $l dummy bus vendor product version]} {
912             set v(devid) $bus:$vendor:$product:$version
913         } elseif {[regexp {^S: Sysfs=(\S+)\s} $l dummy sysfs]} {
914             set v(sysfs) $sysfs
915         } elseif {[regexp {^H: Handlers=(?:.*\s)?(event\d+)\s} $l dummy ev]} {
916             set v(event) $ev
917         } else {
918             # ignored
919         }
920     }
921     close $f
922
923     # add as-if-scanned entries for static bindings to target
924     # also check to see if we want hidraw
925     foreach binding $input_bindings {
926         manyset $binding devkind devid devinfo concrete concargs
927         switch -exact $devkind static { } default continue
928         lappend target($devid) [list $event $sysfs]
929     }
930
931     # scan /dev/hidraw*
932     foreach binding [input-bindings-list hidraw] {
933         manyset $binding devkind devid devinfo concrete concargs
934         switch -exact $devkind hidraw { } default continue
935         manyset $devinfo devtype
936         set rawmap([hidraw-descriptors/$devtype]) \
937             [list $devid $devtype $concrete]
938     }
939     if {[array exists rawmap]} {
940         set new_hidraws [lsort [glob -nocomplain -directory /dev hidraw*]]
941         foreach hidraw $new_hidraws {
942             upvar #0 hidraw/$hidraw raw
943             if {[info exists raw(chan)]} {
944                 set found($raw(devid)) 1
945                 continue
946             }
947             if {[lsearch -exact $old_hidraws $hidraw] >= 0} continue
948             if {[catch {
949                 set chan [open $hidraw r+]
950                 set descriptors [exec ./hidraw-ioctl -d <@ $chan]
951                 if {![info exists rawmap($descriptors)]} {
952                     set m [exec ./hidraw-ioctl -i <@ $chan]
953                     error "unknown descriptors (unmatched device) $hidraw $m >$descriptors<"
954                 }
955             } emsg]} {
956                 upvar #0 hidraw_notified($hidraw) notified
957                 if {![info exists notified] ||
958                      [string compare $notified $emsg]} {
959                     debug "ir $hidraw $emsg"
960                     set notified $emsg
961                 }
962                 catch { close $chan }
963                 catch { unset chan }
964                 continue
965             }
966             manyset $rawmap($descriptors) devid devtype concrete
967             set found($devid) 1
968             if {![input-concrete-start-try $devid $concrete]} {
969                 catch { close $chan }
970                 continue
971             }
972             set raw(devid) $devid
973             set raw(chan) $chan
974             fconfigure $chan -buffering none -blocking no \
975                 -encoding binary -eofchar {} -translation binary
976             fileevent $chan readable \
977                 [list hidraw-readable/$devtype $chan $hidraw $devid]
978             input-binding-present $devid 1 "hidraw $hidraw"
979         }
980         set old_hidraws $new_hidraws
981     }
982
983     # try to start the input binding for all the unstarted found targets
984     foreach devid [array names target] {
985         upvar #0 input/$devid in
986         if {![info exists in(concrete)]} {
987             if {![info exists unmatched_notified($devid)]} {
988                 debug "ib $devid unmatched, ignored"
989                 set unmatched_notified($devid) 1
990             }
991             continue
992         }
993         set found($devid) 1
994         if {[info exists in(chan)]} continue
995         if {![input-concrete-start-try $devid $concrete]} continue
996
997         set cmdl [ib-evcmd/$in(concrete) $devid $target($devid)]
998         if {![llength $cmdl]} {
999             unset target($devid)
1000             continue
1001         }
1002         lappend cmdl 2>@ stderr
1003         catch-for-input-binding $devid {
1004             debug "ib $devid running $cmdl"
1005             set in(chan) [open |$cmdl r+]
1006             fconfigure $in(chan) -blocking 0 -buffering line
1007             fileevent $in(chan) readable [list catch-for-input-binding $devid \
1008                     [list readable input-binding $in(chan) $devid]]
1009         }
1010     }
1011
1012     # anything not found, not present
1013     foreach binding $input_bindings {
1014         manyset $binding devkind devid devinfo concrete concargs
1015         switch -exact $devkind evdev - raw { } default continue
1016         upvar #0 input/$devid in
1017         if {![info exists in(concrete)]} continue
1018         if {[info exists found($devid)]} continue
1019         input-binding-present $devid 0 absent
1020     }
1021 }
1022
1023 proc input-binding-present {devid yes why} {
1024     upvar #0 input/$devid in
1025     if {[info exists in(speedw)]} {
1026         speedw-setstate $in(speedw) [lindex {disabled normal} $yes]
1027     }
1028     set call "ib-[lindex {absent present} $yes]/$in(concrete)"
1029     if {![catch { info args $call }]} {
1030         $call $devid $why
1031     }
1032 }
1033
1034 proc input-binding-eof {chan devid} {
1035     upvar #0 input/$devid in
1036     fconfigure $in(chan) -blocking 1
1037     close $in(chan)
1038     error "evdev-manip exited" {} {CHILDSTATUS ? 0}
1039 }
1040
1041 proc input-binding-inputline {chan l devid} {
1042     global showunbound
1043     upvar #0 input/$devid in
1044     if {![catch { info args ib-inputline/$in(concrete) }]} {
1045         # give the input binding first dibs
1046         if {[ib-inputline/$in(concrete) $devid $l]} return
1047     }
1048     regsub {^[^ ]+ } $l {} lr
1049     switch -glob -- $lr {
1050         {opened *} {
1051             debug "ib $devid start << $l"
1052             input-binding-present 1 $devid "evdev open"
1053         }
1054         {[-0-9]*} {
1055             manyset [split $lr] value kindl kindr codel coder
1056             set proc ib-ev/$in(concrete)/${kindl}_${kindr}/${codel}_${coder}
1057             if {[catch { info args $proc }]} {
1058                 if {$showunbound} {
1059                     debug "ib $devid unbound $proc << $l"
1060                 }
1061                 return
1062             }
1063             $proc $devid $value
1064         }
1065         * {
1066             debug "ib $devid ignored << $l"
1067         }
1068     }
1069 }
1070
1071 proc catch-for-input-binding {devid body} {
1072     upvar #0 input/$devid in
1073     global errorInfo errorCode
1074     set r [catch { uplevel 1 $body } rv]
1075     if {$r!=1} { return -code $r $rv }
1076     switch -glob $errorCode {
1077         {CHILDSTATUS *} { set m "exited with status [lindex $errorCode 2]" }
1078         {CHILDKILLED *} { set m "killed by signal [lindex $errorCode 3]" }
1079         {POSIX *} { set m "communication error: [lindex $errorCode 1]" }
1080         * { error $rv $errorInfo $errorCode }
1081     }
1082     debug "ib $devid died $m"
1083     catch { close $in(chan) }
1084     catch { unset in(chan) }
1085
1086     input-binding-present $devid 0 "died $m"
1087 }
1088
1089 proc engage-input-bindings {} {
1090     scan-input-bindings
1091 }
1092
1093 #---------- plan background (gui-plan subprocess) ----------
1094
1095 proc gui-pipe-readable {args} {
1096     global gui_pipe
1097     while {[gets $gui_pipe l] >= 0} {
1098         debug "<gui-plan $l"
1099     }
1100     if {[eof $gui_pipe]} {
1101         close $gui_pipe
1102         error "gui-plan crashed"
1103     }
1104 }
1105
1106 #---------- train set events of general interest, and setup ----------
1107
1108 proc train-event-eof {args} {
1109     error "lost connection to train set"
1110 }
1111
1112 register-event ?stastate {ctxch state} {^(.)stastate (\w+|\-) } {
1113     global ctrain trains stastate
1114     set stastate $state
1115     report-problem "stastate: $state"
1116     if {[string compare $ctxch |]} speedws-stastate-hook
1117 }
1118
1119 register-event ?resolution {message} \
1120         {^.resolution (\S+ .*)$} {
1121     if {[string match "problems *" $message]} return
1122     report-problem "resolution: $message"
1123 }
1124
1125 register-event ?warning {message} {^.warning (\S+ .*)$} {
1126     report-problem "warning: $message"
1127 }
1128
1129 register-event {} {} {^=connected } {
1130     global pages gui_pipe server port event_selections
1131
1132     scmd replayed {} "select-replay [concat $event_selections]"
1133
1134     foreach page $pages {
1135         set w [pagew $page]
1136         tkwait visibility $w.picture
1137
1138         set cmdl [list ./gui-plan-$page [winfo id $w.picture] @$server,$port]
1139         lappend cmdl 2>@ stderr
1140         set gui_pipe [open |$cmdl r]
1141
1142         puts stderr "running $cmdl"
1143         fconfigure $gui_pipe -blocking no
1144         fileevent $gui_pipe readable gui-pipe-readable
1145     }
1146 }
1147 proc replayed-err {m args} { error "replay failed: $m" }
1148 proc replayed-ok {m args} {
1149     speedws-stastate-hook
1150 }
1151
1152 register-event {} {} {^=failed } { error "multiplexer failed: $l" }
1153 register-event {} {} {^=denied } { error "multiplexer denied us: $l" }
1154 register-event {} {} {^\+nack } { error "multiplexer does not understand" }
1155
1156 #---------- main program ----------
1157
1158 append event_dispatch_body {
1159     debug "ignored $l"
1160 }
1161 proc train-event-inputline {sconn l} $event_dispatch_body
1162 proc register-event {args} { error "too late!" }
1163
1164 proc engage-server {} {
1165     global server port sconn
1166
1167     set sconn [socket $server $port]
1168     fconfig-trainproto $sconn
1169     fileevent $sconn readable {readable train-event $sconn}
1170 }
1171
1172 proc main {} {
1173     global pages cpage configfile input_bindings old_hidraws
1174     setting server railway {[[0-9a-z:].*}
1175     setting geometry {} {[-+]\d+[-+]\d+}
1176     setting posdeviation 5 {\d+}
1177     setting movfeatcommand {movfeat+} {(?:!movfeat|movfeat\+?\+?)}
1178     setting problemdisplayms 1000 {\d+}
1179     setting inputretryadd 5 {\d+}
1180     setting inputretrymax 15 {\d+}
1181     setting scaninputinterval 500 {\d+}
1182     setting showunbound 0 {[01]}
1183
1184     set hostname [lindex [split [info hostname] .] 0]
1185     setting configfile gui-$hostname.config {.+}
1186     parse-argv {}
1187
1188     frame .inputs
1189     if {![info exists input_bindings]} { set input_bindings {} }
1190     set old_hidraws {}
1191
1192     uplevel #0 source gui-layout.config
1193     uplevel #0 source $configfile
1194     foreach cpage $pages {
1195         layout-data
1196         widgets-dgram
1197     }
1198     unset cpage
1199     widgets-movpos
1200     widgets-input-bindings
1201     widget-problem-report
1202     engage-server
1203     engage-input-bindings
1204     start_commandloop
1205 }
1206
1207 main