| #!/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 |