| #!/usr/bin/env tclsh | |
| ############################################################################### | |
| # BRLTTY - A background process providing access to the console screen (when in | |
| # text mode) for a blind person using a refreshable braille display. | |
| # | |
| # Copyright (C) 1995-2023 by The BRLTTY Developers. | |
| # | |
| # BRLTTY comes with ABSOLUTELY NO WARRANTY. | |
| # | |
| # This is free software, placed under the terms of the | |
| # GNU Lesser General Public License, as published by the Free Software | |
| # Foundation; either version 2.1 of the License, or (at your option) any | |
| # later version. Please see the file LICENSE-LGPL for details. | |
| # | |
| # Web Page: http://brltty.app/ | |
| # | |
| # This software is maintained by Dave Mielke <dave@mielke.cc>. | |
| ############################################################################### | |
| source [file join [file dirname [info script]] .. prologue.tcl] | |
| proc logSourceWarning {location message} { | |
| logWarning "$message: [dict get $location file]\[[dict get $location line]\]" | |
| } | |
| proc makeDecimalNumber {value} { | |
| return [expr {$value + 0}] | |
| } | |
| proc formatDeviceIdentifier {vendor product} { | |
| return [format "%04X:%04X" [makeDecimalNumber $vendor] [makeDecimalNumber $product]] | |
| } | |
| proc makeGenericDeviceTable {definitions} { | |
| set table [dict create] | |
| foreach definition $definitions { | |
| set entry [dict create] | |
| foreach {index property} { | |
| 2 vendor | |
| 3 product | |
| } { | |
| if {$index < [llength $definition]} { | |
| if {[string length [set value [lindex $definition $index]]] > 0} { | |
| dict set entry $property $value | |
| } | |
| } | |
| } | |
| dict set table [formatDeviceIdentifier [lindex $definition 0] [lindex $definition 1]] $entry | |
| } | |
| return $table | |
| } | |
| proc parseChannelDefinition {channelVariable lines location stringsArray} { | |
| upvar 1 $stringsArray strings | |
| set channel [dict create] | |
| set ok 1 | |
| foreach line $lines { | |
| if {[regexp {\{\s*/\*\s*(.*?)\s*\*/\s*$} $line x model]} { | |
| dict set channel model $model | |
| continue | |
| } | |
| foreach property {vendor product parentVendor parentProduct} { | |
| set prefix {\.} | |
| set suffix {\s*=\s*(\S*?)\s*,} | |
| set pattern "${prefix}${property}${suffix}" | |
| if {[regexp $pattern $line x identifier]} { | |
| dict set channel $property $identifier | |
| } | |
| } | |
| foreach property {manufacturers products} { | |
| set prefix {\.} | |
| set suffix {\s*=\s*(\S*?)\s*,} | |
| set pattern "${prefix}${property}${suffix}" | |
| if {[regexp $pattern $line x name]} { | |
| dict set channel $property $strings($name) | |
| } | |
| } | |
| } | |
| foreach property {model vendor product} { | |
| if {![dict exists $channel $property]} { | |
| logSourceWarning $location "property not defined: $property" | |
| set ok 0 | |
| } | |
| } | |
| if {$ok} { | |
| foreach property {vendor product} { | |
| if {[string is integer -strict [set value [dict get $channel $property]]]} { | |
| dict set channel $property [makeDecimalNumber $value] | |
| } else { | |
| logSourceWarning $location "$property is not numeric: $value" | |
| dict unset channel $property | |
| set ok 0 | |
| } | |
| } | |
| if {$ok} { | |
| uplevel 1 [list set $channelVariable $channel] | |
| return 1 | |
| } | |
| } | |
| return 0 | |
| } | |
| proc parseChannelDefinitions {channelsVariable lines location stringsArray} { | |
| upvar 1 $channelsVariable channels | |
| upvar 1 $stringsArray strings | |
| set lineNumber [dict get $location line] | |
| foreach lineText $lines { | |
| if {[info exists definition]} { | |
| if {[regexp {\}} $lineText]} { | |
| if {[parseChannelDefinition channel $definition $location strings]} { | |
| lappend channels $channel | |
| } | |
| unset definition | |
| } else { | |
| lappend definition $lineText | |
| } | |
| } elseif {[regexp {\{} $lineText]} { | |
| set definition [list $lineText] | |
| dict set location line $lineNumber | |
| } | |
| incr lineNumber | |
| } | |
| } | |
| proc parseSourceFile {channelsVariable file} { | |
| upvar 1 $channelsVariable channels | |
| if {[catch [list open $file {RDONLY}] stream] == 0} { | |
| set location [dict create] | |
| dict set location file $file | |
| set lineNumber 0 | |
| while {[gets $stream lineText] >= 0} { | |
| incr lineNumber | |
| if {[info exists definitions]} { | |
| if {[regexp {^\s*END_USB_CHANNEL_DEFINITIONS} $lineText]} { | |
| parseChannelDefinitions channels $definitions $location strings | |
| unset definitions | |
| } else { | |
| lappend definitions $lineText | |
| } | |
| } elseif {[info exists stringsList]} { | |
| if {[regexp {^\s*END_USB_STRING_LIST} $lineText]} { | |
| set strings($stringsName) $stringsList | |
| unset stringsList | |
| unset stringsName | |
| } elseif {[regexp {^\s*"(.*)"\s*,} $lineText x string]} { | |
| lappend stringsList $string | |
| } | |
| } elseif {[regexp {^\s*BEGIN_USB_CHANNEL_DEFINITIONS} $lineText]} { | |
| set definitions [list] | |
| dict set location line [expr {$lineNumber + 1}] | |
| } elseif {[regexp {^\s*BEGIN_USB_STRING_LIST\s*\(\s*(.*?)\s*\)} $lineText x stringsName]} { | |
| set stringsList [list] | |
| } | |
| } | |
| close $stream | |
| } else { | |
| writeProgramMessage $stream | |
| } | |
| } | |
| proc parseSourceFiles {channelsVariable driverDirectory} { | |
| upvar 1 $channelsVariable channels | |
| foreach file [glob -directory $driverDirectory -nocomplain {*.[ch]}] { | |
| parseSourceFile channels $file | |
| } | |
| return $channels | |
| } | |
| proc parseDriver {directory} { | |
| set driver [dict create] | |
| dict set driver name [set name [file tail $directory]] | |
| set channels [list] | |
| if {[string length [set code [getBrailleDriverCode $name]]] > 0} { | |
| dict set driver code $code | |
| parseSourceFiles channels $directory | |
| } else { | |
| logWarning "driver code not defined: $name" | |
| } | |
| dict set driver channels $channels | |
| return $driver | |
| } | |
| proc parseDrivers {} { | |
| global sourceRoot | |
| set drivers [list] | |
| foreach directory [lsort [glob -directory [file join $sourceRoot Drivers Braille] -types {d r x} -nocomplain *]] { | |
| if {[llength [dict get [set driver [parseDriver $directory]] channels]] > 0} { | |
| lappend drivers $driver | |
| } | |
| } | |
| return $drivers | |
| } | |
| proc formatDeviceDescription {device} { | |
| return "[dict get $device name] \[[dict get $device channel model]\]" | |
| } | |
| proc makeDeviceTable {drivers} { | |
| global genericDevices | |
| set deviceTable [dict create] | |
| dict set deviceTable vendors [dict create] | |
| foreach driver $drivers { | |
| dict with driver { | |
| foreach channel $channels { | |
| dict with channel { | |
| set identifier [formatDeviceIdentifier $vendor $product] | |
| set device [dict create code $code name $name channel $channel] | |
| if {[dict exists $deviceTable vendors $vendor products $product]} { | |
| set devices [dict get $deviceTable vendors $vendor products $product devices] | |
| logNote "device specified more than once: $identifier: [formatDeviceDescription [lindex $devices 0]] & [formatDeviceDescription $device]" | |
| } else { | |
| set devices [list] | |
| dict set deviceTable vendors $vendor products $product generic [dict exists $genericDevices $identifier] | |
| } | |
| lappend devices $device | |
| dict set deviceTable vendors $vendor products $product devices $devices | |
| if {[dict get $deviceTable vendors $vendor products $product generic]} { | |
| logNote "generic device identifier: $identifier: [formatDeviceDescription $device]" | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return $deviceTable | |
| } | |
| proc getSchemes {} { | |
| set schemes [list] | |
| set length [string length [set prefix makeLines_]] | |
| foreach command [info procs $prefix*] { | |
| lappend schemes [string range $command $length end] | |
| } | |
| return $schemes | |
| } | |
| proc parseFileArguments {filesArray schemes arguments} { | |
| upvar 1 $filesArray files | |
| foreach argument $arguments { | |
| if {[set index [string first : $argument]] < 0} { | |
| syntaxError "scheme not specified: $argument" | |
| } | |
| if {[string length [set scheme [string range $argument 0 $index-1]]] == 0} { | |
| syntaxError "scheme not specified: $argument" | |
| } | |
| if {![lcontain $schemes $scheme]} { | |
| syntaxError "unknown scheme: $scheme" | |
| } | |
| if {[string length [set path [string range $argument $index+1 end]]] == 0} { | |
| syntaxError "path not specified: $argument" | |
| } | |
| if {![file exists $path]} { | |
| semanticError "file not found: $path" | |
| } | |
| if {![file isfile $path]} { | |
| semanticError "not a file: $path" | |
| } | |
| if {![file readable $path]} { | |
| semanticError "file not readable: $path" | |
| } | |
| if {![file writable $path]} { | |
| semanticError "file not writable: $path" | |
| } | |
| lappend files($scheme) $path | |
| } | |
| } | |
| proc compareFilters {filter1 filter2} { | |
| set length1 [llength $filter1] | |
| set length2 [llength $filter2] | |
| if {$length1 > $length2} { | |
| return -1 | |
| } | |
| if {$length1 < $length2} { | |
| return 1 | |
| } | |
| return [string compare $filter1 $filter2] | |
| } | |
| proc xmlMakeComment {comment} { | |
| return "<!-- $comment -->" | |
| } | |
| proc makeComment_android {comment} { | |
| return [xmlMakeComment $comment] | |
| } | |
| proc makeLines_android {vendor product codes descriptions exclude channels} { | |
| set lines [list] | |
| foreach description $descriptions { | |
| lappend lines [makeComment_android $description] | |
| } | |
| set line "<usb-device vendor-id=\"$vendor\" product-id=\"$product\" />" | |
| if {$exclude} { | |
| set line [makeComment_android $line] | |
| } | |
| lappend lines $line | |
| return $lines | |
| } | |
| proc makeComment_c {comment} { | |
| return "// $comment" | |
| } | |
| proc makeLines_c {vendor product codes descriptions exclude channels} { | |
| set lines [list] | |
| foreach description $descriptions { | |
| lappend lines [makeComment_c $description] | |
| } | |
| set codes [lmap code $codes { | |
| set code "\"$code\"" | |
| }] | |
| lappend lines [format "USB_DEVICE_ENTRY(0X%04X, 0X%04X, [join $codes ", "])," $vendor $product] | |
| return $lines | |
| } | |
| proc makeComment_hotplug {comment} { | |
| return "# $comment" | |
| } | |
| proc makeLines_hotplug {vendor product codes descriptions exclude channels} { | |
| set lines [list] | |
| foreach description $descriptions { | |
| lappend lines [makeComment_hotplug $description] | |
| } | |
| set line [format "brltty 0x%04x 0x%04x 0x%04x" 3 $vendor $product] | |
| if {$exclude} { | |
| set line [makeComment_hotplug $line] | |
| } | |
| lappend lines $line | |
| return $lines | |
| } | |
| proc makeComment_inf {comment} { | |
| return "; $comment" | |
| } | |
| proc makeLines_inf {vendor product codes descriptions exclude channels} { | |
| set lines [list] | |
| set line [format "\"\$1: %s\"=\$2, USB\\VID_%04X&PID_%04X" [join $descriptions ", "] $vendor $product] | |
| if {$exclude} { | |
| set line [makeComment_inf $line] | |
| } | |
| lappend lines $line | |
| return $lines | |
| } | |
| proc makeComment_metainfo {comment} { | |
| return [xmlMakeComment $comment] | |
| } | |
| proc makeLines_metainfo {vendor product codes descriptions exclude channels} { | |
| set lines [list] | |
| foreach description $descriptions { | |
| lappend lines [makeComment_metainfo $description] | |
| } | |
| set line [format "<modalias>usb:v%04Xp%04X*</modalias>" $vendor $product] | |
| if {$exclude} { | |
| set line [makeComment_metainfo $line] | |
| } | |
| lappend lines $line | |
| return $lines | |
| } | |
| proc makeComment_udev {comment} { | |
| return "# $comment" | |
| } | |
| proc makeLines_udev {vendor product codes descriptions exclude channels} { | |
| set lines [list] | |
| foreach description $descriptions { | |
| lappend lines [makeComment_udev $description] | |
| } | |
| set baseFilter [list] | |
| lappend baseFilter [format "ENV{PRODUCT}==\"%x/%x/*\"" $vendor $product] | |
| set baseLength [llength $baseFilter] | |
| foreach channel $channels { | |
| set filter $baseFilter | |
| foreach {property attribute} { | |
| manufacturers manufacturer | |
| products product | |
| } { | |
| if {[dict exists $channel $property]} { | |
| foreach string [dict get $channel $property] { | |
| lappend filter "ATTR{$attribute}==\"$string\"" | |
| } | |
| } | |
| } | |
| foreach {property attribute} { | |
| parentVendor idVendor | |
| parentProduct idProduct | |
| } { | |
| if {[dict exists $channel $property]} { | |
| set identifier [format "%04x" [dict get $channel $property]] | |
| lappend filter "ATTRS{$attribute}==\"$identifier\"" | |
| } | |
| } | |
| lappend filters($filter) [dict get $channel code] | |
| } | |
| foreach filter [lsort -command compareFilters [array names filters]] { | |
| set filterLength [llength $filter] | |
| set codes [lsort -unique $filters($filter)] | |
| set rule $filter | |
| lappend rule "ENV{BRLTTY_BRAILLE_DRIVER}=\"[join $codes ,]\"" | |
| lappend rule "GOTO=\"brltty_usb_run\"" | |
| set line [join $rule ", "] | |
| if {$exclude} { | |
| set line [makeComment_udev $line] | |
| } | |
| lappend lines $line | |
| } | |
| return $lines | |
| } | |
| proc makeLines {linesArray schemes deviceTable} { | |
| global genericDevices | |
| upvar #0 optionValues(nogeneric) noGenericDevices | |
| upvar #0 optionValues(onlygeneric) onlyGenericDevices | |
| upvar #0 optionValues(keep) keepExcludedDevices | |
| foreach scheme $schemes { | |
| set makeLines makeLines_$scheme | |
| set makeComment makeComment_$scheme | |
| upvar 1 ${linesArray}($scheme) lines | |
| set lines [list ""] | |
| set vendors [dict get $deviceTable vendors] | |
| foreach vendor [lsort -integer [dict keys $vendors]] { | |
| set vendorEntry [dict get $vendors $vendor] | |
| set products [dict get $vendorEntry products] | |
| foreach product [lsort -integer [dict keys $products]] { | |
| set productEntry [dict get $products $product] | |
| set isGeneric [dict get $productEntry generic] | |
| set excludeDevice [expr {$isGeneric? $noGenericDevices: $onlyGenericDevices}] | |
| if {$excludeDevice && !$keepExcludedDevices} { | |
| continue | |
| } | |
| set identifier [formatDeviceIdentifier $vendor $product] | |
| lappend lines [$makeComment "Device: $identifier"] | |
| set codes [list] | |
| set descriptions [list] | |
| set channels [list] | |
| foreach deviceEntry [dict get $productEntry devices] { | |
| lappend codes [set code [dict get $deviceEntry code]] | |
| lappend descriptions [set description [formatDeviceDescription $deviceEntry]] | |
| set channel [dict get $deviceEntry channel] | |
| dict set channel code $code | |
| dict set channel description $description | |
| lappend channels $channel | |
| } | |
| set codes [lsort -unique $codes] | |
| set descriptions [lsort $descriptions] | |
| if {$isGeneric} { | |
| lappend lines [$makeComment "Generic Identifier"] | |
| set generic [dict get $genericDevices $identifier] | |
| foreach {property header} { | |
| vendor Vendor | |
| product Product | |
| } { | |
| if {[dict exists $generic $property]} { | |
| lappend lines [$makeComment "$header: [dict get $generic $property]"] | |
| } | |
| } | |
| } | |
| eval [list lappend lines] [$makeLines $vendor $product $codes $descriptions $excludeDevice $channels] | |
| lappend lines "" | |
| } | |
| } | |
| } | |
| } | |
| proc updateFile {file newLines} { | |
| upvar #0 optionValues(test) testMode | |
| set lines [readLines $file] | |
| set markerSuffix "_USB_BRAILLE_DEVICES" | |
| set beginMarker "BEGIN$markerSuffix" | |
| set endMarker "END$markerSuffix" | |
| set ranges [list] | |
| set range [list] | |
| set index 0 | |
| foreach line $lines { | |
| set location "$file\[[expr {$index + 1}]\]" | |
| if {[regexp "\\s${beginMarker}(?:\\s+(.*?)\\s*)?\$" $line x arguments]} { | |
| if {[llength $range] > 0} { | |
| writeProgramMessage "nested begin marker: $location" | |
| return 0 | |
| } | |
| lappend range $index [lindex [regexp -inline -- {^\s*} $line] 0] [regexp -inline -all {\S+} $arguments] | |
| } elseif {[regexp "\\s${endMarker}(?:\\s+.*)?\$" $line]} { | |
| if {[llength $range] == 0} { | |
| writeProgramMessage "missing begin marker: $location" | |
| return 0 | |
| } | |
| lappend range $index | |
| lappend ranges $range | |
| set range [list] | |
| } | |
| incr index | |
| } | |
| if {[llength $range] > 0} { | |
| writeProgramMessage "missing end marker: $file\[[lindex $range 0]\]" | |
| return 0 | |
| } | |
| if {[llength $ranges] == 0} { | |
| writeProgramMessage "no region(s) found: $file" | |
| return 0 | |
| } | |
| set hasChanged 0 | |
| foreach range [lreverse $ranges] { | |
| set first [expr {[lindex $range 0] + 1}] | |
| set prefix [lindex $range 1] | |
| set argumentCount [llength [set argumentList [lindex $range 2]]] | |
| set last [expr {[lindex $range 3] - 1}] | |
| set count [expr {$last - $first + 1}] | |
| set replacement [list] | |
| foreach line $newLines { | |
| foreach match [lreverse [regexp -inline -all -indices {\$[1-9][0-9]*} $line]] { | |
| set from [lindex $match 0] | |
| set to [lindex $match 1] | |
| set argumentIndex [string range $line $from+1 $to] | |
| if {[incr argumentIndex -1] < $argumentCount} { | |
| set line [string replace $line $from $to [lindex $argumentList $argumentIndex]] | |
| } | |
| } | |
| if {[string length $line] > 0} { | |
| set line "$prefix$line" | |
| } | |
| lappend replacement "$line" | |
| } | |
| if {!$hasChanged} { | |
| if {[llength $newLines] != $count} { | |
| set hasChanged 1 | |
| } else { | |
| set index 0 | |
| while {$index < $count} { | |
| if {[string compare [lindex $lines $first+$index] [lindex $replacement $index]] != 0} { | |
| set hasChanged 1 | |
| break | |
| } | |
| incr index | |
| } | |
| } | |
| } | |
| if {$hasChanged} { | |
| if {$count == 0} { | |
| set lines [eval [list linsert $lines $first] $replacement] | |
| } else { | |
| set lines [eval [list lreplace $lines $first $last] $replacement] | |
| } | |
| } | |
| } | |
| if {!$hasChanged} { | |
| return 0 | |
| } | |
| if {$testMode} { | |
| writeProgramMessage "test mode - file not updated: $file" | |
| } else { | |
| logMessage notice "updating file: $file" | |
| replaceFile $file "[join $lines \n]\n" | |
| } | |
| return 1 | |
| } | |
| proc updateFiles {filesArray linesArray} { | |
| upvar 1 $filesArray files | |
| upvar 1 $linesArray lines | |
| set updated 0 | |
| foreach scheme [array names files] { | |
| foreach file $files($scheme) { | |
| if {[updateFile $file $lines($scheme)]} { | |
| set updated 1 | |
| } | |
| } | |
| } | |
| return $updated | |
| } | |
| set genericDevices [makeGenericDeviceTable { | |
| { 0X0403 0X6001 | |
| "Future Technology Devices International, Ltd." | |
| "FT232 USB-Serial (UART) IC" | |
| } | |
| { 0X0403 0X6010 | |
| "Future Technology Devices International, Ltd" | |
| "FT2232C/D/H Dual UART/FIFO IC" | |
| } | |
| { 0X10C4 0XEA60 | |
| "Cygnal Integrated Products, Inc." | |
| "CP210x UART Bridge / myAVR mySmartUSB light" | |
| } | |
| { 0X10C4 0XEA80 | |
| "Cygnal Integrated Products, Inc." | |
| "CP210x UART Bridge" | |
| } | |
| { 0X1A86 0X7523 | |
| "Jiangsu QinHeng, Ltd." | |
| "CH341 USB Bridge Controller" | |
| } | |
| }] | |
| set optionDefinitions { | |
| {keep flag "keep commented out rules for excluded devices"} | |
| {nogeneric flag "don't include generic devices"} | |
| {onlygeneric flag "only include generic devices"} | |
| {test flag "don't update the files"} | |
| } | |
| processProgramArguments optionValues $optionDefinitions positionalArguments "\[scheme:file ...\]" | |
| if {[llength $positionalArguments] == 0} { | |
| lappend positionalArguments "android:[file join $sourceRoot Android Gradle app src main res xml usb_devices.xml]" | |
| lappend positionalArguments "c:[file join $sourceRoot Programs usb_devices.c]" | |
| lappend positionalArguments "hotplug:[file join $sourceRoot Autostart Hotplug brltty.usermap]" | |
| lappend positionalArguments "metainfo:[file join $sourceRoot Autostart AppStream org.a11y.brltty.metainfo.xml]" | |
| foreach file [glob -directory [file join $sourceRoot Windows] -nocomplain "*.inf"] { | |
| lappend positionalArguments "inf:$file" | |
| } | |
| } | |
| if {[llength $positionalArguments] == 0} { | |
| syntaxError "files not specified" | |
| } | |
| set schemes [getSchemes] | |
| parseFileArguments files $schemes $positionalArguments | |
| makeLines lines [array names files] [makeDeviceTable [parseDrivers]] | |
| if {![updateFiles files lines]} { | |
| logMessage task "no files updated" | |
| } | |
| exit 0 |