Alternate screen size support.
This is a relatively big new feature, which introduces small breaking changes to the API: * NegotiateTelnet() now returns (DevInfo, error) instead of just error. The function signature for the Tx type now has one additional DevInfo parameter. * RunTransactions() requires one additional parameter, the DevInfo (which is allowed to be nil). Otherwise, existing behavior continues to work as-is and everything still defaults to operating on the default 24x80 terminal size. See example5 for a larger-terminal-aware application. Pending some time to make sure no new bugs cropped up in this release and that the alternate screen size support works in the wild, I anticipate this will become the v1.0.0 release of go3270.
This commit is contained in:
1 parent
3a38ce6e4a
commit
b6fa48f37f
19 files changed
+820
-126
No files matched your search
@@ -3,3 +3,4 @@ example1/example1
|
|||||||
example2/example2
|
example2/example2
|
||||||
example3/example3
|
example3/example3
|
||||||
example4/example4
|
example4/example4
|
||||||
|
example5/example5
|
||||||
@@ -10,7 +10,13 @@ This library allows you to write Go servers for tn3270 clients by building 3270
|
|||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|
||||||
See the example folders for quick demonstrations of using the library. example1 uses the lower-level function ShowScreenOpts(), and example2 uses a higher-level function HandleScreen(). example3 demonstrates updating the client's 3270 display while waiting for a response by using an update thread and a waiting response thread.
|
See the example folders for quick demonstrations of using the library:
|
||||||
|
|
||||||
|
* example1 uses the lower-level function ShowScreenOpts().
|
||||||
|
* example2 uses a higher-level convenience function HandleScreen().
|
||||||
|
* example3 demonstrates updating the client's 3270 display while waiting for a response by using an update thread and a waiting response thread.
|
||||||
|
* example4 demonstrates the RunTransactions() approach to handing control from one screen to the next. This is the recommended way to build applications using go3270.
|
||||||
|
* example5 demonstrates support for larger-than-default (24x80) terminal sizes.
|
||||||
|
|
||||||
**NEW**: For larger applications, I recommend using the `RunTransactions()` function to serve as the driver for your application. You can implement transaction functions which pass control from one transaction to another. example4 demonstrates a "larger" application that uses this approach.
|
**NEW**: For larger applications, I recommend using the `RunTransactions()` function to serve as the driver for your application. You can implement transaction functions which pass control from one transaction to another. example4 demonstrates a "larger" application that uses this approach.
|
||||||
|
|
||||||
@@ -18,22 +24,17 @@ Here's [a video introducing the library][introVideo] as well.
|
|||||||
|
|
||||||
[introVideo]: https://www.youtube.com/watch?v=h9XTjup5W5U
|
[introVideo]: https://www.youtube.com/watch?v=h9XTjup5W5U
|
||||||
|
|
||||||
Known Problems
|
|
||||||
--------------
|
|
||||||
|
|
||||||
- The telnet negotiation does not check for any errors or for any responses from the client. We just assume it goes well and we're actually talking to a tn3270 client.
|
|
||||||
- Screen size is limited to exactly 24x80. In the future, the terminal type could be interrogated and the library could support the screen sizes of other 3270 models.
|
|
||||||
|
|
||||||
3270 information
|
3270 information
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
Everything I know about 3270 data streams I learned from [Tommy Sprinkle's tutorial][sprinkle]. The tn3270 telnet negotiation is gleaned from [RFC 1576: TN3270 Current Practices][rfc1576], [RFC 1041: Telnet 3270 Regime Option][rfc1041], and [RFC 854: Telnet Protocol Specification][rfc854]. The IANA maintains a [useful reference of telnet option numbers][telnetOptions].
|
I started learning about 3270 data streams from [Tommy Sprinkle's tutorial][sprinkle]. The tn3270 telnet negotiation is gleaned from [RFC 1576: TN3270 Current Practices][rfc1576], [RFC 1041: Telnet 3270 Regime Option][rfc1041], and [RFC 854: Telnet Protocol Specification][rfc854]. The IANA maintains a [useful reference of telnet option numbers][telnetOptions]. The reference I use for 3270 data streams is [the 1981 version from IBM][ibmref].
|
||||||
|
|
||||||
[sprinkle]: http://www.tommysprinkle.com/mvs/P3270/
|
[sprinkle]: http://www.tommysprinkle.com/mvs/P3270/
|
||||||
[rfc1576]: https://tools.ietf.org/html/rfc1576
|
[rfc1576]: https://tools.ietf.org/html/rfc1576
|
||||||
[rfc1041]: https://tools.ietf.org/html/rfc1041
|
[rfc1041]: https://tools.ietf.org/html/rfc1041
|
||||||
[rfc854]: https://tools.ietf.org/html/rfc854
|
[rfc854]: https://tools.ietf.org/html/rfc854
|
||||||
[telnetOptions]: https://www.iana.org/assignments/telnet-options/telnet-options.xhtml
|
[telnetOptions]: https://www.iana.org/assignments/telnet-options/telnet-options.xhtml
|
||||||
|
[ibmref]: https://bitsavers.org/pdf/ibm/3270/GA23-0059-0_3270_Data_Stream_Programmers_Reference_Jan1981.pdf
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|||||||
+14
-2
@@ -9,15 +9,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestEncode(t *testing.T) {
|
func TestEncode(t *testing.T) {
|
||||||
encoded := getpos(0, 0)
|
encoded := getpos(0, 0, 80)
|
||||||
if encoded[0] != 0x40 || encoded[1] != 0x40 {
|
if encoded[0] != 0x40 || encoded[1] != 0x40 {
|
||||||
t.Error("Position (0, 0) not correctly encoded")
|
t.Error("Position (0, 0) not correctly encoded")
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded = getpos(11, 39)
|
encoded = getpos(11, 39, 80)
|
||||||
if encoded[0] != 0x4e || encoded[1] != 0xd7 {
|
if encoded[0] != 0x4e || encoded[1] != 0xd7 {
|
||||||
t.Error("Position (11, 39) not correctly encoded")
|
t.Error("Position (11, 39) not correctly encoded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Large screen, 14-bit addressing
|
||||||
|
encoded = getpos(100, 120, 130)
|
||||||
|
if encoded[0] != 0x33 || encoded[1] != 0x40 {
|
||||||
|
t.Errorf("Position (100, 120) on 130-col screen not correctly encoded")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecode(t *testing.T) {
|
func TestDecode(t *testing.T) {
|
||||||
@@ -30,4 +36,10 @@ func TestDecode(t *testing.T) {
|
|||||||
if decoded != 919 {
|
if decoded != 919 {
|
||||||
t.Error("Buffer address incorrectly decoded")
|
t.Error("Buffer address incorrectly decoded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Large screen, 14-bit addressing
|
||||||
|
decoded = decodeBufAddr([2]byte{0x33, 0x40})
|
||||||
|
if decoded != 13120 {
|
||||||
|
t.Error("14-bit buffer address incorrectly decoded")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,10 @@ func handle(conn net.Conn) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// Always begin new connection by negotiating the telnet options
|
// Always begin new connection by negotiating the telnet options
|
||||||
go3270.NegotiateTelnet(conn)
|
if _, err := go3270.NegotiateTelnet(conn); err != nil {
|
||||||
|
fmt.Printf("ERROR: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fieldValues := make(map[string]string)
|
fieldValues := make(map[string]string)
|
||||||
var response go3270.Response
|
var response go3270.Response
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ func handle(conn net.Conn) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// Always begin new connection by negotiating the telnet options
|
// Always begin new connection by negotiating the telnet options
|
||||||
go3270.NegotiateTelnet(conn)
|
if _, err := go3270.NegotiateTelnet(conn); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fieldValues := make(map[string]string)
|
fieldValues := make(map[string]string)
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ func handle(conn net.Conn) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// Always begin new connection by negotiating the telnet options
|
// Always begin new connection by negotiating the telnet options
|
||||||
go3270.NegotiateTelnet(conn)
|
if _, err := go3270.NegotiateTelnet(conn); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// First, let's send the initial screen and wait forever for the user to
|
// First, let's send the initial screen and wait forever for the user to
|
||||||
// press PF3, and when we get it, send a message on the done channel.
|
// press PF3, and when we get it, send a message on the done channel.
|
||||||
|
|||||||
@@ -75,8 +75,13 @@ func handle(conn net.Conn, db DB, gblstate *global) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Always begin new connection by negotiating the telnet options
|
// Always begin new connection by negotiating the telnet options
|
||||||
go3270.NegotiateTelnet(conn)
|
devinfo, err := go3270.NegotiateTelnet(conn)
|
||||||
err := go3270.RunTransactions(conn, state.login, nil)
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = go3270.RunTransactions(conn, devinfo, state.login, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -48,7 +48,8 @@ var helpScreen = go3270.Screen{
|
|||||||
// generator function for it is a stand-alone function and not a method on the
|
// generator function for it is a stand-alone function and not a method on the
|
||||||
// session.
|
// session.
|
||||||
func help(returnTransaction go3270.Tx) go3270.Tx {
|
func help(returnTransaction go3270.Tx) go3270.Tx {
|
||||||
return func(conn net.Conn, data any) (go3270.Tx, any, error) {
|
return func(conn net.Conn, _ go3270.DevInfo, data any) (
|
||||||
|
go3270.Tx, any, error) {
|
||||||
_, err := go3270.HandleScreen(
|
_, err := go3270.HandleScreen(
|
||||||
helpScreen, // the screen to display
|
helpScreen, // the screen to display
|
||||||
nil, // (no) rules to enforce
|
nil, // (no) rules to enforce
|
||||||
|
|||||||
+4
-2
@@ -58,7 +58,8 @@ var loginScreenRules = go3270.Rules{
|
|||||||
|
|
||||||
// login transaction accepts a string value in data if the login screen
|
// login transaction accepts a string value in data if the login screen
|
||||||
// should be initialized with an error message.
|
// should be initialized with an error message.
|
||||||
func (sess *session) login(conn net.Conn, data any) (go3270.Tx, any, error) {
|
func (sess *session) login(conn net.Conn, _ go3270.DevInfo,
|
||||||
|
data any) (go3270.Tx, any, error) {
|
||||||
|
|
||||||
fieldValues := make(map[string]string)
|
fieldValues := make(map[string]string)
|
||||||
|
|
||||||
@@ -169,7 +170,8 @@ type newuserData struct {
|
|||||||
errmsg string
|
errmsg string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sess *session) newuser(conn net.Conn, data any) (go3270.Tx, any, error) {
|
func (sess *session) newuser(conn net.Conn, _ go3270.DevInfo, data any) (
|
||||||
|
go3270.Tx, any, error) {
|
||||||
|
|
||||||
fieldValues := make(map[string]string)
|
fieldValues := make(map[string]string)
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ type mainmenuData struct {
|
|||||||
|
|
||||||
// mainmenu transaction accepts a mainmenuData struct as the data if the
|
// mainmenu transaction accepts a mainmenuData struct as the data if the
|
||||||
// option field or error message should be populated.
|
// option field or error message should be populated.
|
||||||
func (sess *session) mainmenu(conn net.Conn, data any) (go3270.Tx, any, error) {
|
func (sess *session) mainmenu(conn net.Conn, _ go3270.DevInfo, data any) (
|
||||||
|
go3270.Tx, any, error) {
|
||||||
|
|
||||||
fieldValues := make(map[string]string)
|
fieldValues := make(map[string]string)
|
||||||
|
|
||||||
@@ -182,8 +183,8 @@ var exampleScreen = go3270.Screen{
|
|||||||
// exampleFeature is a transaction that will act as a placeholder for real
|
// exampleFeature is a transaction that will act as a placeholder for real
|
||||||
// application functionality. It accepts a string in the data which will
|
// application functionality. It accepts a string in the data which will
|
||||||
// be displayed on the panel.
|
// be displayed on the panel.
|
||||||
func (sess *session) exampleFeature(conn net.Conn, data any) (go3270.Tx,
|
func (sess *session) exampleFeature(conn net.Conn, _ go3270.DevInfo,
|
||||||
any, error) {
|
data any) (go3270.Tx, any, error) {
|
||||||
|
|
||||||
fieldValues := make(map[string]string)
|
fieldValues := make(map[string]string)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// This file is part of https://github.com/racingmars/go3270/
|
||||||
|
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
|
||||||
|
// LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/racingmars/go3270"
|
||||||
|
)
|
||||||
|
|
||||||
|
var biglayout = go3270.Screen{
|
||||||
|
// Column will be calculated at runtime for the following field:
|
||||||
|
{Row: 0, Intense: true, Content: "3270 Screen Size Example"},
|
||||||
|
|
||||||
|
{Row: 2, Col: 0,
|
||||||
|
Content: "This screen is using the full size that your terminal supports."},
|
||||||
|
|
||||||
|
{Row: 4, Col: 0, Content: "Terminal Type . . ."},
|
||||||
|
{Row: 4, Col: 21, Name: "termtype", Intense: true},
|
||||||
|
|
||||||
|
{Row: 5, Col: 0, Content: "Rows . . . . . . . ."},
|
||||||
|
{Row: 5, Col: 21, Name: "rows", Intense: true},
|
||||||
|
|
||||||
|
{Row: 6, Col: 0, Content: "Columns . . . . . ."},
|
||||||
|
{Row: 6, Col: 21, Name: "cols", Intense: true},
|
||||||
|
|
||||||
|
{Row: 8, Col: 0, Content: "To visit a default sized screen, press"},
|
||||||
|
{Row: 8, Col: 39, Content: "PF1", Color: go3270.Yellow, Intense: true},
|
||||||
|
|
||||||
|
{Row: 9, Col: 0, Content: "To exit and disconnect, press"},
|
||||||
|
{Row: 9, Col: 30, Content: "PF3", Color: go3270.Yellow, Intense: true},
|
||||||
|
|
||||||
|
// a blank field for error messages
|
||||||
|
{Row: 11, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func bigscreen(conn net.Conn, devinfo go3270.DevInfo, data any) (
|
||||||
|
go3270.Tx, any, error) {
|
||||||
|
|
||||||
|
rows, cols := devinfo.AltDimensions()
|
||||||
|
termtype := devinfo.TerminalType()
|
||||||
|
|
||||||
|
// Make a local copy of the screen definition that we can append lines to.
|
||||||
|
screen := make(go3270.Screen, len(biglayout))
|
||||||
|
copy(screen, biglayout)
|
||||||
|
|
||||||
|
// Center the title on any screen width
|
||||||
|
screen[0].Col = (cols / 2) - (len(biglayout[0].Content) / 2)
|
||||||
|
|
||||||
|
// We'll start writing "data lines" at row 13 up to the penultimate row on
|
||||||
|
// the terminal
|
||||||
|
for i := 13; i < rows-1; i++ {
|
||||||
|
newfield := go3270.Field{Row: i, Col: 0,
|
||||||
|
Content: fmt.Sprintf("This is data row %d.", i-12)}
|
||||||
|
if i == rows-2 {
|
||||||
|
newfield.Content += " (The last.)"
|
||||||
|
}
|
||||||
|
screen = append(screen, newfield)
|
||||||
|
|
||||||
|
// And demonstrate that we can position to the full width, too.
|
||||||
|
newfield = go3270.Field{Row: i, Col: cols - 5, Content: "<**>"}
|
||||||
|
screen = append(screen, newfield)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And an input field on the last row, to make sure field buffer address
|
||||||
|
// decoding works on larger screens.
|
||||||
|
newfield := go3270.Field{Row: rows - 1, Col: 0,
|
||||||
|
Content: "Enter data here:", Color: go3270.Pink, Intense: true}
|
||||||
|
screen = append(screen, newfield)
|
||||||
|
newfield = go3270.Field{Row: rows - 1, Col: 17, Name: "inputdata",
|
||||||
|
Write: true}
|
||||||
|
screen = append(screen, newfield)
|
||||||
|
newfield = go3270.Field{Row: rows - 1, Col: cols - 1} // "stop" field
|
||||||
|
screen = append(screen, newfield)
|
||||||
|
|
||||||
|
fieldValues := map[string]string{
|
||||||
|
"termtype": termtype,
|
||||||
|
"rows": strconv.Itoa(rows),
|
||||||
|
"cols": strconv.Itoa(cols),
|
||||||
|
}
|
||||||
|
|
||||||
|
if data != nil {
|
||||||
|
fieldValues["errormsg"] = fmt.Sprintf("You said: %s", data.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := go3270.HandleScreenAlt(
|
||||||
|
screen, // the screen to display
|
||||||
|
nil, // (no) rules to enforce
|
||||||
|
fieldValues, // pre-populated values in fields
|
||||||
|
[]go3270.AID{ // keys we accept -- validating
|
||||||
|
go3270.AIDEnter,
|
||||||
|
},
|
||||||
|
[]go3270.AID{ // keys we accept -- non-validating
|
||||||
|
go3270.AIDPF1,
|
||||||
|
go3270.AIDPF3,
|
||||||
|
},
|
||||||
|
"errormsg", // name of field to put error messages in
|
||||||
|
rows-1, 18, // cursor coordinates
|
||||||
|
conn, // network connection
|
||||||
|
devinfo, // device info for alternate screen size support
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.AID {
|
||||||
|
case go3270.AIDEnter:
|
||||||
|
// Re-run current transaction, echoing back input
|
||||||
|
return bigscreen, resp.Values["inputdata"], err
|
||||||
|
case go3270.AIDPF1:
|
||||||
|
// Go to default screen size transaction
|
||||||
|
return normalscreen, nil, nil
|
||||||
|
case go3270.AIDPF3:
|
||||||
|
// Exit
|
||||||
|
return nil, nil, nil
|
||||||
|
default:
|
||||||
|
// re-run current transaction
|
||||||
|
return bigscreen, nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// This file is part of https://github.com/racingmars/go3270/
|
||||||
|
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
|
||||||
|
// LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
// Example 5 demonstrates support for larger-than-default alternate screen
|
||||||
|
// sizes in terminals that are larger than 24x80.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/racingmars/go3270"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ln, err := net.Listen("tcp", ":3270")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("LISTENING ON PORT 3270 FOR CONNECTIONS")
|
||||||
|
fmt.Println("Press Ctrl-C to end server.")
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
go handle(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle is the handler for individual user connections.
|
||||||
|
func handle(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Always begin new connection by negotiating the telnet options
|
||||||
|
devinfo, err := go3270.NegotiateTelnet(conn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = go3270.RunTransactions(conn, devinfo, bigscreen, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// This file is part of https://github.com/racingmars/go3270/
|
||||||
|
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
|
||||||
|
// LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/racingmars/go3270"
|
||||||
|
)
|
||||||
|
|
||||||
|
var normallayout = go3270.Screen{
|
||||||
|
{Row: 0, Col: 28, Intense: true, Content: "3270 Screen Size Example"},
|
||||||
|
|
||||||
|
{Row: 2, Col: 0,
|
||||||
|
Content: "This screen is using the default size that all terminals support, 24x80."},
|
||||||
|
{Row: 3, Col: 0,
|
||||||
|
Content: "But I know the following information about your particular terminal:"},
|
||||||
|
|
||||||
|
{Row: 4, Col: 0, Content: "Terminal Type . . ."},
|
||||||
|
{Row: 4, Col: 21, Name: "termtype", Intense: true},
|
||||||
|
|
||||||
|
{Row: 5, Col: 0, Content: "Rows . . . . . . . ."},
|
||||||
|
{Row: 5, Col: 21, Name: "rows", Intense: true},
|
||||||
|
{Row: 5, Col: 28, Content: "(but currently using 24)"},
|
||||||
|
|
||||||
|
{Row: 6, Col: 0, Content: "Columns . . . . . ."},
|
||||||
|
{Row: 6, Col: 21, Name: "cols", Intense: true},
|
||||||
|
{Row: 6, Col: 28, Content: "(but currently using 80)"},
|
||||||
|
|
||||||
|
{Row: 8, Col: 0, Content: "To visit a large sized screen, press"},
|
||||||
|
{Row: 8, Col: 37, Content: "PF1", Color: go3270.Yellow, Intense: true},
|
||||||
|
|
||||||
|
{Row: 9, Col: 0, Content: "To exit and disconnect, press"},
|
||||||
|
{Row: 9, Col: 30, Content: "PF3", Color: go3270.Yellow, Intense: true},
|
||||||
|
|
||||||
|
// a blank field for error messages
|
||||||
|
{Row: 11, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalscreen(conn net.Conn, devinfo go3270.DevInfo, data any) (
|
||||||
|
go3270.Tx, any, error) {
|
||||||
|
|
||||||
|
rows, cols := devinfo.AltDimensions()
|
||||||
|
termtype := devinfo.TerminalType()
|
||||||
|
|
||||||
|
// Make a local copy of the screen definition that we can append lines to.
|
||||||
|
screen := make(go3270.Screen, len(normallayout))
|
||||||
|
copy(screen, normallayout)
|
||||||
|
|
||||||
|
// We'll start writing "data lines" at row 13 up to 24
|
||||||
|
for i := 13; i < 24; i++ {
|
||||||
|
newfield := go3270.Field{Row: i, Col: 0,
|
||||||
|
Content: fmt.Sprintf("This is data row %d.", i-12)}
|
||||||
|
if i == 23 {
|
||||||
|
newfield.Content += " (The last.)"
|
||||||
|
}
|
||||||
|
screen = append(screen, newfield)
|
||||||
|
|
||||||
|
newfield = go3270.Field{Row: i, Col: 80 - 5, Content: "<**>"}
|
||||||
|
screen = append(screen, newfield)
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldValues := map[string]string{
|
||||||
|
"termtype": termtype,
|
||||||
|
"rows": strconv.Itoa(rows),
|
||||||
|
"cols": strconv.Itoa(cols),
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can call the old HandleScreen(), or we could have used the new
|
||||||
|
// HandleScreenAlt() and provided a nil DevInfo.
|
||||||
|
resp, err := go3270.HandleScreen(
|
||||||
|
screen, // the screen to display
|
||||||
|
nil, // (no) rules to enforce
|
||||||
|
fieldValues, // pre-populated values in fields
|
||||||
|
nil, // keys we accept -- validating
|
||||||
|
[]go3270.AID{ // keys we accept -- non-validating
|
||||||
|
go3270.AIDPF1,
|
||||||
|
go3270.AIDPF3,
|
||||||
|
},
|
||||||
|
"errormsg", // name of field to put error messages in
|
||||||
|
1, 1, // cursor coordinates
|
||||||
|
conn, // network connection
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.AID {
|
||||||
|
case go3270.AIDPF1:
|
||||||
|
// Go to big screen size transaction
|
||||||
|
return bigscreen, nil, nil
|
||||||
|
case go3270.AIDPF3:
|
||||||
|
// Exit
|
||||||
|
return nil, nil, nil
|
||||||
|
default:
|
||||||
|
// re-run current transaction
|
||||||
|
return normalscreen, nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,9 +86,23 @@ type FieldRules struct {
|
|||||||
// fields pass validation, OR 2) the user presses a key in exitkeys. In all
|
// fields pass validation, OR 2) the user presses a key in exitkeys. In all
|
||||||
// other cases, HandleScreen will re-present the screen to the user again,
|
// other cases, HandleScreen will re-present the screen to the user again,
|
||||||
// possibly with an error message set in the errorField field.
|
// possibly with an error message set in the errorField field.
|
||||||
|
//
|
||||||
|
// For alternate screen support (larger than 24x80), use HandleScreenAlt().
|
||||||
func HandleScreen(screen Screen, rules Rules, values map[string]string,
|
func HandleScreen(screen Screen, rules Rules, values map[string]string,
|
||||||
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
|
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
|
||||||
conn net.Conn) (Response, error) {
|
conn net.Conn) (Response, error) {
|
||||||
|
return HandleScreenAlt(screen, rules, values, pfkeys, exitkeys, errorField,
|
||||||
|
crow, ccol, conn, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleScreenAlt is identical to HandleScreen, but writes to the "alternate"
|
||||||
|
// screen size provided by dev. To write a non-24-by-80 screen, use this
|
||||||
|
// HandleScreenAlt function with a non-nil dev. If dev is nil, the behavior is
|
||||||
|
// identical to HandleScreen, which is limited to 24x80 and will set larger
|
||||||
|
// terminals to the default 24x80 mode.
|
||||||
|
func HandleScreenAlt(screen Screen, rules Rules, values map[string]string,
|
||||||
|
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
|
||||||
|
conn net.Conn, dev DevInfo) (Response, error) {
|
||||||
|
|
||||||
// Save the original field values for any named fields to support
|
// Save the original field values for any named fields to support
|
||||||
// the MustChange rule. Also build a map of named fields.
|
// the MustChange rule. Also build a map of named fields.
|
||||||
@@ -128,7 +142,7 @@ mainloop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := ShowScreenOpts(screen, myValues, conn,
|
resp, err := ShowScreenOpts(screen, myValues, conn,
|
||||||
ScreenOpts{CursorRow: crow, CursorCol: ccol})
|
ScreenOpts{CursorRow: crow, CursorCol: ccol, AltScreen: dev})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-23
@@ -6,9 +6,7 @@ package go3270
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response encapsulates data received from a 3270 client in response to the
|
// Response encapsulates data received from a 3270 client in response to the
|
||||||
@@ -61,9 +59,11 @@ const (
|
|||||||
AIDPA2 AID = 0x6E
|
AIDPA2 AID = 0x6E
|
||||||
AIDPA3 AID = 0x6B
|
AIDPA3 AID = 0x6B
|
||||||
AIDClear AID = 0x6D
|
AIDClear AID = 0x6D
|
||||||
|
|
||||||
|
aidQueryResponse AID = 0x88
|
||||||
)
|
)
|
||||||
|
|
||||||
func readResponse(c net.Conn, fm fieldmap) (Response, error) {
|
func readResponse(c net.Conn, fm fieldmap, dev DevInfo) (Response, error) {
|
||||||
var r Response
|
var r Response
|
||||||
aid, err := readAID(c)
|
aid, err := readAID(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -79,7 +79,12 @@ func readResponse(c net.Conn, fm fieldmap) (Response, error) {
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
row, col, _, err := readPosition(c)
|
cols := 80
|
||||||
|
if dev != nil {
|
||||||
|
_, cols = dev.altDimensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
row, col, _, err := readPosition(c, cols)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
@@ -87,7 +92,7 @@ func readResponse(c net.Conn, fm fieldmap) (Response, error) {
|
|||||||
r.Row = row
|
r.Row = row
|
||||||
|
|
||||||
var fieldValues map[string]string
|
var fieldValues map[string]string
|
||||||
if fieldValues, err = readFields(c, fm); err != nil {
|
if fieldValues, err = readFields(c, fm, cols); err != nil {
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@ func readAID(c net.Conn) (AID, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPosition(c net.Conn) (row, col, addr int, err error) {
|
func readPosition(c net.Conn, cols int) (row, col, addr int, err error) {
|
||||||
raw := make([]byte, 2)
|
raw := make([]byte, 2)
|
||||||
|
|
||||||
// Read two bytes
|
// Read two bytes
|
||||||
@@ -128,8 +133,8 @@ func readPosition(c net.Conn) (row, col, addr int, err error) {
|
|||||||
|
|
||||||
// Decode the raw position
|
// Decode the raw position
|
||||||
addr = decodeBufAddr([2]byte{raw[0], raw[1]})
|
addr = decodeBufAddr([2]byte{raw[0], raw[1]})
|
||||||
col = addr % 80
|
col = addr % cols
|
||||||
row = (addr - col) / 80
|
row = (addr - col) / cols
|
||||||
|
|
||||||
debugf("Got position bytes %02x %02x, decoded to %d\n", raw[0], raw[1],
|
debugf("Got position bytes %02x %02x, decoded to %d\n", raw[0], raw[1],
|
||||||
addr)
|
addr)
|
||||||
@@ -137,7 +142,7 @@ func readPosition(c net.Conn) (row, col, addr int, err error) {
|
|||||||
return row, col, addr, nil
|
return row, col, addr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readFields(c net.Conn, fm fieldmap) (map[string]string, error) {
|
func readFields(c net.Conn, fm fieldmap, cols int) (map[string]string, error) {
|
||||||
var infield bool
|
var infield bool
|
||||||
var fieldpos int
|
var fieldpos int
|
||||||
var fieldval bytes.Buffer
|
var fieldval bytes.Buffer
|
||||||
@@ -174,7 +179,7 @@ func readFields(c net.Conn, fm fieldmap) (map[string]string, error) {
|
|||||||
fieldval = bytes.Buffer{}
|
fieldval = bytes.Buffer{}
|
||||||
fieldpos = 0
|
fieldpos = 0
|
||||||
|
|
||||||
if _, _, fieldpos, err = readPosition(c); err != nil {
|
if _, _, fieldpos, err = readPosition(c, cols); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -203,20 +208,13 @@ func handleField(addr int, value []byte, fm fieldmap, values map[string]string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// decodeBufAddr decodes a raw 2-byte encoded buffer address and returns the
|
// decodeBufAddr decodes a raw 2-byte encoded buffer address and returns the
|
||||||
// integer value of the address (i.e. 0-1919)
|
// integer value of the address.
|
||||||
func decodeBufAddr(raw [2]byte) int {
|
func decodeBufAddr(raw [2]byte) int {
|
||||||
if decodes[raw[0]] > 254 {
|
// 16-bit addressing
|
||||||
fmt.Fprintf(os.Stderr,
|
if raw[0]&0xc0 == 0 {
|
||||||
"UNEXPECTED VALUE: decodeBufAddr got raw value of %02x %02x\n",
|
return int(raw[0])<<8 + int(raw[1])
|
||||||
raw[0], raw[1])
|
|
||||||
}
|
|
||||||
if decodes[raw[1]] > 254 {
|
|
||||||
fmt.Fprintf(os.Stderr,
|
|
||||||
"UNEXPECTED VALUE: decodeBufAddr got raw value of %02x %02x\n",
|
|
||||||
raw[0], raw[1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hi := decodes[raw[0]] << 6
|
// 12-bit addressing
|
||||||
lo := decodes[raw[1]]
|
return int(raw[0]&0x3f)<<6 + int(raw[1]&0x3f)
|
||||||
return hi | lo
|
|
||||||
}
|
}
|
||||||
@@ -98,6 +98,19 @@ type Screen []Field
|
|||||||
// ScreenOpts are the options that callers may set when sending a screen
|
// ScreenOpts are the options that callers may set when sending a screen
|
||||||
// to the 3270 client.
|
// to the 3270 client.
|
||||||
type ScreenOpts struct {
|
type ScreenOpts struct {
|
||||||
|
// If AltScreen is non-nil, the screen will be written to the "alternate"
|
||||||
|
// screen size, which is the non-default (24x80) screen dimensions that
|
||||||
|
// the terminal supports (although for many terminals, the alternate
|
||||||
|
// screen is still just 24x80). If AltScreen is nil, the default (24x80)
|
||||||
|
// mode will be used. Never switch between AltScreen and normal screen
|
||||||
|
// (e.g. AltScreen = nil) unless NoClear is false. (That is, switching
|
||||||
|
// between default and alternate screen size or back requires a screen
|
||||||
|
// clear at the same time.) When AltScreen is nil, field positions in the
|
||||||
|
// screen must be within the 24x80 screen (so rows 0-23 and cols 0-79),
|
||||||
|
// when AltScreen is present, the field positions must be within the
|
||||||
|
// dimensions of the DevInfo.AltDimensions() values.
|
||||||
|
AltScreen DevInfo
|
||||||
|
|
||||||
// NoResponse will draw the screen and immediately return, without
|
// NoResponse will draw the screen and immediately return, without
|
||||||
// waiting for any input data from the remote client.
|
// waiting for any input data from the remote client.
|
||||||
NoResponse bool
|
NoResponse bool
|
||||||
@@ -107,12 +120,16 @@ type ScreenOpts struct {
|
|||||||
// the current screen.
|
// the current screen.
|
||||||
NoClear bool
|
NoClear bool
|
||||||
|
|
||||||
// CursorRow sets the row (0-indexed) to position the cursor after
|
// CursorRow sets the row (0-indexed) to position the cursor after sending
|
||||||
// sending the screen, when NoClear is false. Maximum value is 23.
|
// the screen, when NoClear is false. When AltScreen is nil, maximum value
|
||||||
|
// is 23; otherwise, maximum is 1 less than the number of rows in
|
||||||
|
// AltScreen.
|
||||||
CursorRow int
|
CursorRow int
|
||||||
|
|
||||||
// CursorCol sets the column (0-indexed) to position the cursor after
|
// CursorCol sets the column (0-indexed) to position the cursor after
|
||||||
// sending the screen, when NoClear is false. Maximum value is 79.
|
// sending the screen, when NoClear is false. When AltScreen is nil,
|
||||||
|
// maximum value is 79; otherwise, maximum is 1 less than the number of
|
||||||
|
// columns in AltScreen.
|
||||||
CursorCol int
|
CursorCol int
|
||||||
|
|
||||||
// PostSendCallback is a function that, if non-nil, will be called after
|
// PostSendCallback is a function that, if non-nil, will be called after
|
||||||
@@ -159,13 +176,15 @@ type fieldmap map[int]string
|
|||||||
// invalid. That is to say, while waiting for a response, don't perform other
|
// invalid. That is to say, while waiting for a response, don't perform other
|
||||||
// actions from another thread that could layout the user input fields
|
// actions from another thread that could layout the user input fields
|
||||||
// differently.
|
// differently.
|
||||||
func ShowScreenOpts(screen Screen, values map[string]string, conn net.Conn,
|
|
||||||
|
func ShowScreenOpts(screen Screen,
|
||||||
|
values map[string]string, conn net.Conn,
|
||||||
opts ScreenOpts) (Response, error) {
|
opts ScreenOpts) (Response, error) {
|
||||||
|
|
||||||
var resp Response
|
var resp Response
|
||||||
|
|
||||||
fm, err := showScreenInternal(screen, values, opts.CursorRow,
|
fm, err := showScreenInternal(screen, values, opts.CursorRow,
|
||||||
opts.CursorCol, conn, !opts.NoClear)
|
opts.CursorCol, conn, !opts.NoClear, opts.AltScreen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -178,7 +197,7 @@ func ShowScreenOpts(screen Screen, values map[string]string, conn net.Conn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !opts.NoResponse {
|
if !opts.NoResponse {
|
||||||
resp, err = readResponse(conn, fm)
|
resp, err = readResponse(conn, fm, opts.AltScreen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -216,13 +235,22 @@ func ShowScreenNoResponse(screen Screen, values map[string]string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showScreenInternal(screen Screen, values map[string]string,
|
func showScreenInternal(screen Screen, values map[string]string,
|
||||||
crow, ccol int, conn net.Conn, clear bool) (fieldmap, error) {
|
crow, ccol int, conn net.Conn, clear bool, dev DevInfo) (fieldmap, error) {
|
||||||
|
|
||||||
|
rows, cols := 24, 80
|
||||||
|
if dev != nil {
|
||||||
|
rows, cols = dev.altDimensions()
|
||||||
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
var fm = make(fieldmap) // field buffer positions -> name
|
var fm = make(fieldmap) // field buffer positions -> name
|
||||||
|
|
||||||
if clear {
|
if clear {
|
||||||
b.WriteByte(0xf5) // Erase/Write to terminal
|
if !(rows == 24 && cols == 80) {
|
||||||
|
b.WriteByte(0x7e) // Erase/Write Alternate to terminal
|
||||||
|
} else {
|
||||||
|
b.WriteByte(0xf5) // Erase/Write to terminal
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
b.WriteByte(0xf1) // Write to terminal
|
b.WriteByte(0xf1) // Write to terminal
|
||||||
}
|
}
|
||||||
@@ -237,12 +265,12 @@ func showScreenInternal(screen Screen, values map[string]string,
|
|||||||
|
|
||||||
// Build the commands for each field on the screen
|
// Build the commands for each field on the screen
|
||||||
for _, fld := range screen {
|
for _, fld := range screen {
|
||||||
if fld.Row < 0 || fld.Row > 23 || fld.Col < 0 || fld.Col > 79 {
|
if fld.Row < 0 || fld.Row > rows-1 || fld.Col < 0 || fld.Col > cols-1 {
|
||||||
// Invalid field position
|
// Invalid field position
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Write(sba(fld.Row, fld.Col))
|
b.Write(sba(fld.Row, fld.Col, cols))
|
||||||
b.Write(buildField(fld))
|
b.Write(buildField(fld))
|
||||||
|
|
||||||
// Use fld.Content, unless the field is named and appears in the
|
// Use fld.Content, unless the field is named and appears in the
|
||||||
@@ -262,7 +290,7 @@ func showScreenInternal(screen Screen, values map[string]string,
|
|||||||
// because we get the position of the field's first input position,
|
// because we get the position of the field's first input position,
|
||||||
// not the position of the field attribute byte).
|
// not the position of the field attribute byte).
|
||||||
if fld.Write {
|
if fld.Write {
|
||||||
bufaddr := fld.Row*80 + fld.Col
|
bufaddr := fld.Row*cols + fld.Col
|
||||||
fm[bufaddr+1] = fld.Name
|
fm[bufaddr+1] = fld.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,13 +299,13 @@ func showScreenInternal(screen Screen, values map[string]string,
|
|||||||
// caller-provided coordinates.
|
// caller-provided coordinates.
|
||||||
if clear {
|
if clear {
|
||||||
// Set cursor position. Correct out-of-bounds values to 0.
|
// Set cursor position. Correct out-of-bounds values to 0.
|
||||||
if crow < 0 || crow > 23 {
|
if crow < 0 || crow > rows-1 {
|
||||||
crow = 0
|
crow = 0
|
||||||
}
|
}
|
||||||
if ccol < 0 || ccol > 79 {
|
if ccol < 0 || ccol > cols-1 {
|
||||||
ccol = 0
|
ccol = 0
|
||||||
}
|
}
|
||||||
b.Write(ic(crow, ccol))
|
b.Write(ic(crow, ccol, cols))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR
|
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR
|
||||||
@@ -292,10 +320,10 @@ func showScreenInternal(screen Screen, values map[string]string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sba is the "set buffer address" 3270 command.
|
// sba is the "set buffer address" 3270 command.
|
||||||
func sba(row, col int) []byte {
|
func sba(row, col, cols int) []byte {
|
||||||
result := make([]byte, 1, 3)
|
result := make([]byte, 1, 3)
|
||||||
result[0] = 0x11 // SBA
|
result[0] = 0x11 // SBA
|
||||||
result = append(result, getpos(row, col)...)
|
result = append(result, getpos(row, col, cols)...)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,20 +400,37 @@ func sfAttribute(write, intense, hidden, skip, numeric bool) byte {
|
|||||||
|
|
||||||
// ic is the "insert cursor" 3270 command. This function will include the
|
// ic is the "insert cursor" 3270 command. This function will include the
|
||||||
// appropriate SBA command.
|
// appropriate SBA command.
|
||||||
func ic(row, col int) []byte {
|
func ic(row, col, cols int) []byte {
|
||||||
result := make([]byte, 0, 3)
|
result := make([]byte, 0, 3)
|
||||||
result = append(result, sba(row, col)...)
|
result = append(result, sba(row, col, cols)...)
|
||||||
result = append(result, 0x13) // IC
|
result = append(result, 0x13) // IC
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// getpos translates row and col to buffer address control characters.
|
// getpos translates row and col to buffer address control characters.
|
||||||
func getpos(row, col int) []byte {
|
func getpos(row, col, cols int) []byte {
|
||||||
result := make([]byte, 2)
|
address := row*cols + col
|
||||||
address := row*80 + col
|
|
||||||
hi := (address & 0xfc0) >> 6
|
// Use 12-bit addressing if the buffer address fits in 12 bits
|
||||||
lo := address & 0x3f
|
if address < 1<<12 {
|
||||||
result[0] = codes[hi]
|
hi := (address & 0xfc0) >> 6
|
||||||
result[1] = codes[lo]
|
lo := address & 0x3f
|
||||||
return result
|
return []byte{codes[hi], codes[lo]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use 14-bit addressing. The library limits terminal size to
|
||||||
|
// fit within 14-bit addressing, because 16-bit addressing would require
|
||||||
|
// us to track state that the current API design doesn't lend itself to.
|
||||||
|
// Someday, perhaps in a v2 library version, we'll support absurdly large
|
||||||
|
// terminal sizes. But for now, 14 bits is as big as we can go.
|
||||||
|
hi := (address & 0x3f00) >> 8
|
||||||
|
lo := address & 0xff
|
||||||
|
|
||||||
|
// It's possible the low byte is 0xff, in which case we need to telnet-
|
||||||
|
// escape it.
|
||||||
|
if lo == 0xff {
|
||||||
|
return []byte{byte(hi), 0xff, byte(lo)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte{byte(hi), byte(lo)}
|
||||||
}
|
}
|
||||||
@@ -1,48 +1,337 @@
|
|||||||
// This file is part of https://github.com/racingmars/go3270/
|
// This file is part of https://github.com/racingmars/go3270/
|
||||||
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
|
// Copyright 2020, 2025 by Matthew R. Wilson, licensed under the MIT license.
|
||||||
// LICENSE in the project root for license information.
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
package go3270
|
package go3270
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DevInfo provides information about the terminal that is connected.
|
||||||
|
//
|
||||||
|
// 3270 terminals operate at a default screen size of 24 rows that are 80
|
||||||
|
// columns wide. The normal "Write/Erase" datastream command always writes to
|
||||||
|
// the default 24x80 buffer. But some terminals support more rows and/or
|
||||||
|
// columns, and the alternate sized buffer may be written to with the
|
||||||
|
// "Write/Erase Alternate" command.
|
||||||
|
type DevInfo interface {
|
||||||
|
// AltDimensions returns the number or rows and columns on the alternate
|
||||||
|
// screen size.
|
||||||
|
AltDimensions() (rows, cols int)
|
||||||
|
|
||||||
|
// TerminalType reports the terminal-provided identification string. All
|
||||||
|
// modern tn3270 clients will report one of the IBM-3278 models (-2, -3,
|
||||||
|
// -4, or -5), or IBM-DYNAMIC if the alternate screen size isn't one of
|
||||||
|
// the fixed sizes of the 3278 models. This string is purely
|
||||||
|
// informational; the actual size of the alternate screen is available
|
||||||
|
// from AltDimensions().
|
||||||
|
TerminalType() string
|
||||||
|
|
||||||
|
// Private version of AltDimensions() so callers can't fake us out; only
|
||||||
|
// real implementations returned by NegotiateTelnet() will work.
|
||||||
|
altDimensions() (rows, cols int)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
binary = 0
|
se = 240 // 0xf0
|
||||||
send = 1
|
sb = 250 // 0xfa
|
||||||
se = 240 // f0
|
will = 251 // 0xfb
|
||||||
sb = 250 // fa
|
wont = 252 // 0xfc
|
||||||
will = 251 // fb
|
do = 253 // 0xfd
|
||||||
wont = 252 // fc
|
dont = 254 // 0xfe
|
||||||
do = 253 // fd
|
iac = 255 // 0xff
|
||||||
dont = 254 // fe
|
|
||||||
iac = 255 // ff
|
// Options
|
||||||
terminalType = 24 // 18
|
|
||||||
eoroption = 25 // 19
|
binaryOption = 0
|
||||||
eor = 239 // f1
|
|
||||||
|
eorOption = 25 // 0x19
|
||||||
|
eor = 239 // 0xf1
|
||||||
|
|
||||||
|
terminalType = 24 // 0x18
|
||||||
|
terminalTypeIs = 0
|
||||||
|
terminalTypeSend = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// NegotiateTelnet will naively (e.g. not checking client responses) negotiate
|
// ErrNo3270 indicates that the telnet client did not respond properly to the
|
||||||
// the options necessary for tn3270 on a new telnet connection, conn.
|
// options negotiation that are expected for a tn3270 client.
|
||||||
func NegotiateTelnet(conn net.Conn) error {
|
var ErrNo3270 = errors.New("couldn't negotiate telnet options for tn3270")
|
||||||
conn.Write([]byte{iac, do, terminalType})
|
|
||||||
conn.Write([]byte{iac, sb, terminalType, send, iac, se})
|
// ErrTelnetError indicates an unexpected response was encountered in the
|
||||||
conn.Write([]byte{iac, do, eoroption})
|
// telnet protocol.
|
||||||
conn.Write([]byte{iac, do, binary})
|
var ErrTelnetError = errors.New("telnet or 3270 protocol error")
|
||||||
conn.Write([]byte{iac, will, eoroption, iac, will, binary})
|
|
||||||
flushConnection(conn, time.Second*5)
|
// ErrUnknownTerminal indicates the client did not identify itself as an
|
||||||
|
// IBM-3277, 3278, 3279, or IBM-DYNAMIC model. All modern tn3270 clients
|
||||||
|
// should report as IBM-3278 models or IBM-DYNAMIC.
|
||||||
|
var ErrUnknownTerminal = errors.New("unknown terminal type")
|
||||||
|
|
||||||
|
var errOptionRejected = errors.New("option rejected")
|
||||||
|
|
||||||
|
// NegotiateTelnet will negotiate the options necessary for tn3270 on a new
|
||||||
|
// telnet connection, conn.
|
||||||
|
func NegotiateTelnet(conn net.Conn) (DevInfo, error) {
|
||||||
|
|
||||||
|
// Enable terminal type option
|
||||||
|
if _, err := conn.Write([]byte{iac, do, terminalType}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err := checkOptionResponse(conn, terminalType, do)
|
||||||
|
if err == errOptionRejected || err == ErrTelnetError {
|
||||||
|
return nil, ErrNo3270
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to the first available terminal type
|
||||||
|
conn.Write([]byte{iac, sb, terminalType, terminalTypeSend, iac, se})
|
||||||
|
devtype, err := getTerminalType(conn)
|
||||||
|
if err == ErrTelnetError {
|
||||||
|
return nil, ErrNo3270
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request end of record mode
|
||||||
|
conn.Write([]byte{iac, do, eorOption})
|
||||||
|
err = checkOptionResponse(conn, eorOption, do)
|
||||||
|
if err == errOptionRejected || err == ErrTelnetError {
|
||||||
|
return nil, ErrNo3270
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request binary mode
|
||||||
|
conn.Write([]byte{iac, do, binaryOption})
|
||||||
|
err = checkOptionResponse(conn, binaryOption, do)
|
||||||
|
if err == errOptionRejected || err == ErrTelnetError {
|
||||||
|
return nil, ErrNo3270
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter end of record mode
|
||||||
|
conn.Write([]byte{iac, will, eorOption})
|
||||||
|
err = checkOptionResponse(conn, eorOption, will)
|
||||||
|
if err == errOptionRejected || err == ErrTelnetError {
|
||||||
|
return nil, ErrNo3270
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter binary mode
|
||||||
|
conn.Write([]byte{iac, will, binaryOption})
|
||||||
|
err = checkOptionResponse(conn, binaryOption, will)
|
||||||
|
if err == errOptionRejected || err == ErrTelnetError {
|
||||||
|
return nil, ErrNo3270
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
devinfo, err := makeDeviceInfo(conn, devtype)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return devinfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOptionResponse will check for the client's "will/wont" (if mode is do)
|
||||||
|
// or "do/dont" (if mode is will) response. mode is the option command the
|
||||||
|
// server just sent, and option is the option code to check for.
|
||||||
|
func checkOptionResponse(conn net.Conn, option, mode byte) error {
|
||||||
|
var buf [3]byte
|
||||||
|
|
||||||
|
var expectedYes, expectedNo byte
|
||||||
|
switch mode {
|
||||||
|
case do:
|
||||||
|
expectedYes = will
|
||||||
|
expectedNo = wont
|
||||||
|
case will:
|
||||||
|
expectedYes = do
|
||||||
|
expectedNo = dont
|
||||||
|
default:
|
||||||
|
return ErrTelnetError
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := conn.Read(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n < 3 || buf[0] != iac {
|
||||||
|
return ErrTelnetError
|
||||||
|
}
|
||||||
|
if buf[1] == expectedNo {
|
||||||
|
// Was the correct option rejected?
|
||||||
|
if buf[2] != option {
|
||||||
|
return ErrTelnetError
|
||||||
|
}
|
||||||
|
return errOptionRejected
|
||||||
|
}
|
||||||
|
if buf[1] != expectedYes {
|
||||||
|
return ErrTelnetError
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have "will" now. But for the right option?
|
||||||
|
if buf[2] != option {
|
||||||
|
return ErrTelnetError
|
||||||
|
}
|
||||||
|
|
||||||
|
// All good, client accepted the option we requested.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTerminalType reads the response to a "send terminal type" option
|
||||||
|
// subfield command.
|
||||||
|
func getTerminalType(conn net.Conn) (string, error) {
|
||||||
|
var buf [100]byte
|
||||||
|
var termtype string
|
||||||
|
|
||||||
|
n, err := conn.Read(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return termtype, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// At a minimum, with a one-character terminal type name, we expect
|
||||||
|
// 7 bytes
|
||||||
|
if n < 7 {
|
||||||
|
return termtype, ErrTelnetError
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll check the expected control bytes all in one go...
|
||||||
|
if buf[0] != iac || buf[1] != sb || buf[2] != terminalType ||
|
||||||
|
buf[3] != terminalTypeIs || buf[n-2] != iac || buf[n-1] != se {
|
||||||
|
return termtype, ErrTelnetError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything looks good. The terminal type is an ASCII string between all
|
||||||
|
// the control/command bytes.
|
||||||
|
return string(buf[4 : n-2]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) {
|
||||||
|
// Known fixed size device types. All modern tn3270 clients should
|
||||||
|
// report as 3278, but we'll also include 3277 and 3279 just in case.
|
||||||
|
switch termtype {
|
||||||
|
case "IBM-3277-2", "IBM-3277-2-E", "IBM-3278-2", "IBM-3278-2-E",
|
||||||
|
"IBM-3279-2", "IBM-3279-2-E":
|
||||||
|
return &deviceInfo{24, 80, termtype}, nil
|
||||||
|
case "IBM-3278-3", "IBM-3278-3-E", "IBM-3279-3", "IBM-3279-3-E":
|
||||||
|
return &deviceInfo{32, 80, termtype}, nil
|
||||||
|
case "IBM-3278-4", "IBM-3278-4-E":
|
||||||
|
return &deviceInfo{43, 80, termtype}, nil
|
||||||
|
case "IBM-3278-5", "IBM-3278-5-E":
|
||||||
|
return &deviceInfo{27, 132, termtype}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a fixed-size type, it should be IBM-DYNAMIC. If it isn't,
|
||||||
|
// we don't know how to deal with it.
|
||||||
|
if termtype != "IBM-DYNAMIC" {
|
||||||
|
return nil, ErrUnknownTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// For IBM-DYNAMIC, we need to discover the alternate screen size with
|
||||||
|
// a structured field query.
|
||||||
|
|
||||||
|
// First, we perform an ERASE / WRITE ALTERNATE to clear the screen
|
||||||
|
// and put it in alternate screen mode. (EWA, reset WCC, telnet EOR)
|
||||||
|
if _, err := conn.Write([]byte{0x7e, 0xc3, 0xff, 0xef}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we need to send the Write Structured Field command (0xf3) with the
|
||||||
|
// "Read Partition - Query" structured field. Note that we're
|
||||||
|
// telnet-escaping the 0xff in the data, but the subfield length is the
|
||||||
|
// *unescaped* length (7).
|
||||||
|
if _, err := conn.Write([]byte{0xf3, 0, 7, 0x01, 0xff, 0xff, 0x02,
|
||||||
|
0xff, 0xef}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var aid [1]byte
|
||||||
|
n, err := conn.Read(aid[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n != 1 || aid[0] != byte(aidQueryResponse) {
|
||||||
|
return nil, ErrTelnetError
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows, cols int
|
||||||
|
// There are an arbitrary number of query reply structured fields. We
|
||||||
|
// are only interested in the "Usable Area" SFID=0x81 QCODE=0x81 field,
|
||||||
|
// so we'll just consume any others. Consume all data until the EOR is
|
||||||
|
// received.
|
||||||
|
for {
|
||||||
|
// Two bytes are big-endian length.
|
||||||
|
buf, err := telnetReadN(conn, 2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if buf == nil {
|
||||||
|
// EOR. We're out of fields.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var l int = int(buf[0])<<8 + int(buf[1])
|
||||||
|
|
||||||
|
// Field length includes the 2 length bytes
|
||||||
|
buf, err = telnetReadN(conn, l-2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if buf == nil {
|
||||||
|
return nil, ErrTelnetError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that because length isn't at the beginning, offsets in buf
|
||||||
|
// are 2 less than in the 3270 datastream documentation.
|
||||||
|
|
||||||
|
if !(buf[0] == 0x81 && buf[1] == 0x81) {
|
||||||
|
// Not 'Usable Area' query reply
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// A valid Usable Area reply will always include at least 18 (20 with
|
||||||
|
// length) bytes.
|
||||||
|
if l < 18 {
|
||||||
|
return nil, ErrTelnetError
|
||||||
|
}
|
||||||
|
|
||||||
|
// big-endian two byte values
|
||||||
|
cols = int(buf[4])<<8 + int(buf[5])
|
||||||
|
rows = int(buf[6])<<8 + int(buf[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows == 0 || cols == 0 {
|
||||||
|
// We got an IBM-DYNAMIC device type, but it didn't include a
|
||||||
|
// Usable Area query response.
|
||||||
|
return nil, ErrUnknownTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// We support 12- and 14-bit addressing. Using 16-bit addressing would
|
||||||
|
// require a mode change and the current API design doesn't support
|
||||||
|
// tracking the state necessary for that.
|
||||||
|
//
|
||||||
|
// We'll limit the reported screen size to what fits in 14-bit addressing
|
||||||
|
// by removing rows if necessary.
|
||||||
|
for rows*cols >= 1<<14 {
|
||||||
|
rows--
|
||||||
|
}
|
||||||
|
|
||||||
|
return &deviceInfo{rows, cols, termtype}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UnNegotiateTelnet will naively (e.g. not checking client responses) attempt
|
// UnNegotiateTelnet will naively (e.g. not checking client responses) attempt
|
||||||
// to restore the telnet options state to what it was before NegotiateTelnet()
|
// to restore the telnet options state to what it was before NegotiateTelnet()
|
||||||
// was called.
|
// was called.
|
||||||
func UnNegotiateTelnet(conn net.Conn, timeout time.Duration) error {
|
func UnNegotiateTelnet(conn net.Conn, timeout time.Duration) error {
|
||||||
conn.Write([]byte{iac, wont, eoroption, iac, wont, binary})
|
conn.Write([]byte{iac, wont, eorOption, iac, wont, binaryOption})
|
||||||
conn.Write([]byte{iac, dont, binary})
|
conn.Write([]byte{iac, dont, binaryOption})
|
||||||
conn.Write([]byte{iac, dont, eoroption})
|
conn.Write([]byte{iac, dont, eorOption})
|
||||||
conn.Write([]byte{iac, dont, terminalType})
|
conn.Write([]byte{iac, dont, terminalType})
|
||||||
flushConnection(conn, timeout)
|
flushConnection(conn, timeout)
|
||||||
return nil
|
return nil
|
||||||
@@ -138,3 +427,48 @@ func telnetRead(c net.Conn, passEOR bool) (b byte, valid, isEor bool, err error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// telnetReadN reads n unescaped, valid, non-EOR characters. The returned byte
|
||||||
|
// slice will always be length n (see special case below, though), unless
|
||||||
|
// error is non-nil, in which case the byte slice will be nil. Invalid or
|
||||||
|
// early EOR will return ErrTelnetError.
|
||||||
|
//
|
||||||
|
// AS A SPECIAL CASE, if the first byte read is EOR, then the returned byte
|
||||||
|
// slice AND error will be nil.
|
||||||
|
func telnetReadN(conn net.Conn, n int) ([]byte, error) {
|
||||||
|
buf := make([]byte, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
b, valid, isEor, err := telnetRead(conn, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if i == 0 && isEor {
|
||||||
|
// If we're still on the first byte and it's EOR, return a
|
||||||
|
// non-error nil value.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if !valid || isEor {
|
||||||
|
return nil, ErrTelnetError
|
||||||
|
}
|
||||||
|
buf[i] = b
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceInfo struct {
|
||||||
|
rows, cols int
|
||||||
|
termtype string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceInfo) AltDimensions() (rows, cols int) {
|
||||||
|
return d.rows, d.cols
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceInfo) TerminalType() string {
|
||||||
|
return d.termtype
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceInfo) altDimensions() (rows, cols int) {
|
||||||
|
return d.rows, d.cols
|
||||||
|
}
|
||||||
+23
-10
@@ -7,30 +7,43 @@ package go3270
|
|||||||
import "net"
|
import "net"
|
||||||
|
|
||||||
// Tx is a function that serves as one transaction in a go3270 application.
|
// Tx is a function that serves as one transaction in a go3270 application.
|
||||||
// The Tx function is called with the network connection to the client, and a
|
// The Tx function is called with the network connection to the client, the
|
||||||
// "data" value provided by the previous transaction. Tx functions return the
|
// DevInfo for use with alternate screen writes, and a "data" value provided
|
||||||
// next transaction to run (or nil to indicate the RunTransactions() function
|
// by the previous transaction. Tx functions return the next transaction to
|
||||||
// should terminate), the data to pass into the next transaction, and any
|
// run (or nil to indicate the RunTransactions() function should terminate),
|
||||||
// error. If the error is non-nil, the RunTransactions() function will
|
// the data to pass into the next transaction, and any error. If the error is
|
||||||
// terminate and return the err. A non-nil error is _not_ passed between
|
// non-nil, the RunTransactions() function will terminate and return the err.
|
||||||
// transactions, it terminates transaction processing.
|
// A non-nil error is _not_ passed between transactions, it terminates
|
||||||
type Tx func(conn net.Conn, data any) (next Tx, newdata any, err error)
|
// transaction processing.
|
||||||
|
type Tx func(conn net.Conn, dev DevInfo, data any) (
|
||||||
|
next Tx, newdata any, err error)
|
||||||
|
|
||||||
// RunTransactions begins running transaction functions, starting with the
|
// RunTransactions begins running transaction functions, starting with the
|
||||||
// initial transaction, until a transaction eventually returns nil for the
|
// initial transaction, until a transaction eventually returns nil for the
|
||||||
// next transaction, or until a transaction function returns a non-nil error
|
// next transaction, or until a transaction function returns a non-nil error
|
||||||
// value. data (which may be nil, if the initial transaction does not require
|
// value. data (which may be nil, if the initial transaction does not require
|
||||||
// data) is passed in as the data to the initial transaction.
|
// data) is passed in as the data to the initial transaction.
|
||||||
func RunTransactions(conn net.Conn, initial Tx, data any) error {
|
//
|
||||||
|
// dev is the DevInfo of the connected client, as obtained from
|
||||||
|
// NegotiateTelnet(). It is safe to pass a nil DevInfo, in which case all
|
||||||
|
// transactions will only be able to operate with the default 24x80 screen
|
||||||
|
// size.
|
||||||
|
func RunTransactions(conn net.Conn, dev DevInfo, initial Tx,
|
||||||
|
data any) error {
|
||||||
|
|
||||||
var next Tx
|
var next Tx
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
next = initial
|
next = initial
|
||||||
|
|
||||||
|
if dev == nil {
|
||||||
|
dev = &deviceInfo{rows: 24, cols: 80, termtype: "DEFAULT"}
|
||||||
|
}
|
||||||
|
|
||||||
// We run transactions until there isn't a next transaction to run, or
|
// We run transactions until there isn't a next transaction to run, or
|
||||||
// an error.
|
// an error.
|
||||||
for {
|
for {
|
||||||
next, data, err = next(conn, data)
|
next, data, err = next(conn, dev, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Error means we bail out and return the error to the caller.
|
// Error means we bail out and return the error to the caller.
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ func debugf(format string, a ...interface{}) {
|
|||||||
fmt.Fprintf(Debug, format, a...)
|
fmt.Fprintf(Debug, format, a...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// codes are the 3270 control character I/O codes, pre-computed as provided
|
// codes are the 3270 control character I/O codes for 12-bit addressing,
|
||||||
// at http://www.tommysprinkle.com/mvs/P3270/iocodes.htm
|
// from Figure D-1 of GA23-0059-00. (Figure C-1 in later editions.)
|
||||||
var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
|
var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
|
||||||
0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4,
|
0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4,
|
||||||
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60,
|
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60,
|
||||||
@@ -32,23 +32,6 @@ var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
|
|||||||
0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
||||||
0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f}
|
0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f}
|
||||||
|
|
||||||
// decodes is the inverse of the above table; -1 is used in invalid positions
|
|
||||||
var decodes = []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, 42, 43, 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2,
|
|
||||||
3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 17, 18, 19, 20, 21, 22,
|
|
||||||
23, 24, 25, -1, -1, -1, -1, -1, -1, -1, -1, 34, 35, 36, 37, 38, 39, 40,
|
|
||||||
41, -1, -1, -1, -1, -1, -1, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1,
|
|
||||||
-1, -1, -1, -1}
|
|
||||||
|
|
||||||
// AIDtoString returns a string representation of an AID key name.
|
// AIDtoString returns a string representation of an AID key name.
|
||||||
func AIDtoString(aid AID) string {
|
func AIDtoString(aid AID) string {
|
||||||
switch aid {
|
switch aid {
|
||||||
|
|||||||
Reference in new issue
Block a user