aboutsummaryrefslogtreecommitdiff
path: root/ql/ql_linux.go
blob: 26675ebcb75ec32f7c54a68f6f1d839ad2bb65b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package ql

import (
	"errors"
	"image"
	"io"
	"os"
	"path/filepath"
	"syscall"
	"time"
	"unsafe"
)

// #include <linux/ioctl.h>
import "C"

// -----------------------------------------------------------------------------

func _IOC(dir, typ, nr, size int) uintptr {
	return (uintptr(dir) << C._IOC_DIRSHIFT) |
		(uintptr(typ) << C._IOC_TYPESHIFT) |
		(uintptr(nr) << C._IOC_NRSHIFT) |
		(uintptr(size) << C._IOC_SIZESHIFT)
}

const (
	iocnrGetDeviceID = 1
)

// lpiocGetDeviceID reads the IEEE-1284 Device ID string of a printer.
func lpiocGetDeviceID(fd uintptr) ([]byte, error) {
	var buf [1024]byte
	if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd,
		_IOC(C._IOC_READ, 'P', iocnrGetDeviceID, len(buf)),
		uintptr(unsafe.Pointer(&buf))); err != 0 {
		return nil, err
	}

	// In theory it might get trimmed along the way.
	length := int(buf[0])<<8 | int(buf[1])
	if 2+length > len(buf) {
		return buf[2:], errors.New("the device ID string got trimmed")
	}

	return buf[2 : 2+length], nil
}

// -----------------------------------------------------------------------------

type Printer struct {
	File         *os.File
	Manufacturer string
	Model        string

	LastStatus *Status
	MediaInfo  *MediaInfo

	// StatusNotify is called whenever we receive a status packet.
	StatusNotify func(*Status)
}

// Open finds and initializes the first USB printer found supporting
// the appropriate protocol. Returns nil if no printer could be found.
func Open() (*Printer, error) {
	// Linux usblp module, located in /drivers/usb/class/usblp.c
	paths, err := filepath.Glob("/dev/usb/lp[0-9]*")
	if err != nil {
		return nil, err
	}
	for _, candidate := range paths {
		f, err := os.OpenFile(candidate, os.O_RDWR, 0)
		if err != nil {
			continue
		}
		// Filter out obvious non-printers.
		deviceID, err := lpiocGetDeviceID(f.Fd())
		if err != nil {
			f.Close()
			continue
		}
		parsedID := parseIEEE1284DeviceID(deviceID)
		// Filter out printers that wouldn't understand the protocol.
		if !compatible(parsedID) {
			f.Close()
			continue
		}
		return &Printer{
			File:         f,
			Manufacturer: parsedID.FindFirst("MANUFACTURER", "MFG"),
			Model:        parsedID.FindFirst("MODEL", "MDL"),
		}, nil
	}
	return nil, nil
}

// Initialize initializes the printer for further operations.
func (p *Printer) Initialize() error {
	// Clear the print buffer.
	invalidate := make([]byte, 400)
	if _, err := p.File.Write(invalidate); err != nil {
		return err
	}

	// Initialize.
	if _, err := p.File.WriteString("\x1b\x40"); err != nil {
		return err
	}

	// Flush any former responses in the printer's queue.
	//
	// I haven't checked if this is the kernel driver or the printer doing
	// the buffering that causes data to be returned at this point.
	var dummy [32]byte
	for {
		if _, err := p.File.Read(dummy[:]); err == io.EOF {
			break
		}
	}

	return nil
}

var errTimeout = errors.New("timeout")
var errInvalidRead = errors.New("invalid read")

func (p *Printer) updateStatus(status Status) {
	p.LastStatus = &status
	if p.StatusNotify != nil {
		p.StatusNotify(p.LastStatus)
	}
}

// pollStatusBytes waits for the printer to send a status packet and returns
// it as raw data.
func (p *Printer) pollStatusBytes(
	timeout time.Duration) (*Status, error) {
	start, buf := time.Now(), [32]byte{}
	for {
		if n, err := p.File.Read(buf[:]); err == io.EOF {
			time.Sleep(10 * time.Millisecond)
		} else if err != nil {
			return nil, err
		} else if n < 32 {
			return nil, errInvalidRead
		} else {
			p.updateStatus(Status(buf))
			return p.LastStatus, nil
		}
		if time.Now().Sub(start) > timeout {
			return nil, errTimeout
		}
	}
}

// Request new status information from the printer. The printer
// must be in an appropriate mode, i.e. on-line and not currently printing.
func (p *Printer) UpdateStatus() error {
	// Request status information.
	if _, err := p.File.WriteString("\x1b\x69\x53"); err != nil {
		return err
	}

	// Retrieve status information.
	if _, err := p.pollStatusBytes(time.Second); err != nil {
		p.LastStatus = nil
		return err
	}
	return nil
}

var errErrorOccurred = errors.New("error occurred")
var errUnexpectedStatus = errors.New("unexpected status")
var errUnknownMedia = errors.New("unknown media")

func (p *Printer) Print(image image.Image, rb bool) error {
	data := makePrintData(p.LastStatus, image, rb)
	if data == nil {
		return errUnknownMedia
	}
	if _, err := p.File.Write(data); err != nil {
		return err
	}

	// See diagrams: we may receive an error status instead of the transition
	// to the printing state. Or even after it.
	//
	// Not sure how exactly cooling behaves and I don't want to test it.
	for {
		status, err := p.pollStatusBytes(10 * time.Second)
		if err != nil {
			return err
		}

		switch status.Type() {
		case StatusTypePhaseChange:
			// Nothing to do.
		case StatusTypePrintingCompleted:
			return nil
		case StatusTypeErrorOccurred:
			return errErrorOccurred
		default:
			return errUnexpectedStatus
		}
	}
}

// Close closes the underlying file.
func (p *Printer) Close() error {
	return p.File.Close()
}