blob: 13f57bd42da7de55726012c558a9504d0ed591d9 [file] [log] [blame] [edit]
#!/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