← All changes
|
includes/Services/Providers/Epson_Sdp_Adapter.php
+177
-6
1.10.0
→
1.10.21
View file →
| @@ -6,15 +6,28 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | namespace WCPOS\WooCommercePOS\Services\Providers; |
| 9 | 9 | |
| 10 | -use WCPOS\WooCommercePOS\Interfaces\Provider_Adapter_Interface; | |
| 10 | +use WCPOS\WooCommercePOS\Interfaces\Poll_Provider_Adapter_Interface; | |
| 11 | 11 | |
| 12 | 12 | /** |
| 13 | 13 | * Epson_Sdp_Adapter class. |
| 14 | 14 | */ |
| 15 | -class Epson_Sdp_Adapter implements Provider_Adapter_Interface { | |
| 15 | +class Epson_Sdp_Adapter implements Poll_Provider_Adapter_Interface { | |
| 16 | 16 | /** |
| 17 | + * Milliseconds an Epson Server Direct Print printer waits for the local | |
| 18 | + * print device to become printable before it gives up and reports | |
| 19 | + * EX_TIMEOUT. | |
| 20 | + * | |
| 21 | + * Epson allows 5000–300000. The previous 10000 (near the floor) was | |
| 22 | + * unforgiving for printers that briefly go not-ready — a paper change, a | |
| 23 | + * sleep/wake, or a momentary network blip — declaring the job failed | |
| 24 | + * before the printer could recover. 60000 gives those transient states | |
| 25 | + * room to clear without letting a genuinely offline printer hang for long. | |
| 26 | + */ | |
| 27 | + const EPSON_SDP_PRINT_TIMEOUT_MS = 60000; | |
| 28 | + | |
| 29 | + /** | |
| 17 | 30 | * Return the ePOS-Print XML content type. |
| 18 | 31 | * |
| 19 | 32 | * @return string |
| 20 | 33 | */ |
| @@ -32,15 +45,15 @@ | ||
| 32 | 45 | */ |
| 33 | 46 | public function format( array $printer, array $template ): array { |
| 34 | 47 | if ( 'thermal' !== (string) ( $template['engine'] ?? '' ) ) { |
| 35 | 48 | return array( |
| 36 | - 'kind' => '', | |
| 49 | + 'kind' => '', | |
| 37 | 50 | 'content_type' => '', |
| 38 | 51 | ); |
| 39 | 52 | } |
| 40 | 53 | |
| 41 | 54 | return array( |
| 42 | - 'kind' => 'epos-xml', | |
| 55 | + 'kind' => 'epos-xml', | |
| 43 | 56 | 'content_type' => $this->content_type(), |
| 44 | 57 | ); |
| 45 | 58 | } |
| 46 | 59 | |
| @@ -54,14 +67,16 @@ | ||
| 54 | 67 | public function diagnostic( string $printer_name ): array { |
| 55 | 68 | $name = (string) preg_replace( '/[\x00-\x1F\x7F]/', '', $printer_name ); |
| 56 | 69 | $text = "WCPOS - Cloud Print Test\nPrinter: " . $name . "\n"; |
| 57 | 70 | $text .= 'Date: ' . gmdate( 'Y-m-d H:i' ) . "\nIf you can read this, printing works!\n"; |
| 71 | + // Three blank lines before the cut, the same bottom margin the receipt | |
| 72 | + // templates use, so the last line is not flush against the blade. | |
| 58 | 73 | $xml = '<epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print">' |
| 59 | - . '<text>' . esc_html( $text ) . '</text><cut type="feed"/></epos-print>'; | |
| 74 | + . '<text>' . esc_html( $text ) . '</text><feed line="3"/><cut type="feed"/></epos-print>'; | |
| 60 | 75 | |
| 61 | 76 | return array( |
| 62 | 77 | 'content_type' => $this->content_type(), |
| 63 | - 'payload' => base64_encode( $xml ), | |
| 78 | + 'payload' => base64_encode( $xml ), | |
| 64 | 79 | ); |
| 65 | 80 | } |
| 66 | 81 | |
| 67 | 82 | /** |
| @@ -86,6 +101,162 @@ | ||
| 86 | 101 | return 'waiting'; |
| 87 | 102 | } |
| 88 | 103 | |
| 89 | 104 | return ( (int) $context['now'] - $seen ) <= (int) $context['seen_ttl'] ? 'connected' : 'offline'; |
| 105 | + } | |
| 106 | + | |
| 107 | + /** | |
| 108 | + * Parse the vendor request. | |
| 109 | + * | |
| 110 | + * @param array $request Request data. | |
| 111 | + */ | |
| 112 | + public function parse( array $request ): array { | |
| 113 | + $params = $request['params']; | |
| 114 | + // Server Direct Print multiplexes three different request types onto the | |
| 115 | + // one configured URL, as URL-encoded form data distinguished by | |
| 116 | + // ConnectionType (User's Manual Rev.K, ch.3 and the Test_print.php | |
| 117 | + // reference implementation): | |
| 118 | + // | |
| 119 | + // GetRequest — poll for a job | |
| 120 | + // SetResponse — printing result; the XML rides in the ResponseFile field | |
| 121 | + // SetStatus — status notification; the XML rides in the Status field | |
| 122 | + // | |
| 123 | + // Answering a status notification with print data hands the job to a | |
| 124 | + // request that discards it, so the printer never prints and the job stays | |
| 125 | + // claimed. Dispatching on ConnectionType is what keeps the job on the | |
| 126 | + // GetRequest that is actually asking for one. | |
| 127 | + $connection_type = (string) ( $params['ConnectionType'] ?? '' ); | |
| 128 | + $result_xml = (string) ( $params['ResponseFile'] ?? '' ); | |
| 129 | + if ( '' === $result_xml && false !== strpos( $request['body'], '<response' ) ) { | |
| 130 | + $result_xml = $request['body']; | |
| 131 | + } | |
| 132 | + // Idle PrintResponseInfo and SetStatus posts must never consume a job. | |
| 133 | + $phase = false !== strpos( $result_xml, '<response' ) ? 'result' : ( 'SetStatus' === $connection_type || 'SetResponse' === $connection_type || '' !== $result_xml ? 'advertise' : 'fetch' ); | |
| 134 | + return array( | |
| 135 | + 'phase' => $phase, | |
| 136 | + 'route' => $request['route'], | |
| 137 | + 'result_xml' => $result_xml, | |
| 138 | + 'busy' => 'advertise' === $phase, | |
| 139 | + ); | |
| 140 | + } | |
| 141 | + | |
| 142 | + /** | |
| 143 | + * Build an offer or acknowledgement. | |
| 144 | + * | |
| 145 | + * @param array $printer Printer data. | |
| 146 | + * @param array $poll Parsed request. | |
| 147 | + * @param array|null $next_job Available job. | |
| 148 | + */ | |
| 149 | + public function advertise( array $printer, array $poll, ?array $next_job ): array { | |
| 150 | + return array( | |
| 151 | + 'status' => 200, | |
| 152 | + 'headers' => array( 'Content-Type' => 'text/xml; charset=utf-8' ), | |
| 153 | + 'body' => '<response success="true" code="" status=""/>', | |
| 154 | + ); | |
| 155 | + } | |
| 156 | + | |
| 157 | + /** | |
| 158 | + * Choose a format before claiming. | |
| 159 | + * | |
| 160 | + * @param array $printer Printer data. | |
| 161 | + * @param array $poll Parsed request. | |
| 162 | + * @param array $job Candidate job. | |
| 163 | + */ | |
| 164 | + public function negotiate( array $printer, array $poll, array $job ): array { | |
| 165 | + // SDP uses the provider default render, not an HTTP Accept media type. | |
| 166 | + return array( 'media_type' => '' ); | |
| 167 | + } | |
| 168 | + | |
| 169 | + /** | |
| 170 | + * Build the print-data response, including empty renders. | |
| 171 | + * | |
| 172 | + * @param array $job Claimed job. | |
| 173 | + * @param array $render Rendered bytes. | |
| 174 | + * @param array $poll Parsed request. | |
| 175 | + */ | |
| 176 | + public function deliver( array $job, array $render, array $poll ): array { | |
| 177 | + if ( '' === $render['body'] ) { | |
| 178 | + return $this->advertise( array(), $poll, null ); | |
| 179 | + } | |
| 180 | + // SDP 1.00 is supported by every printer family; this is not a SOAP envelope. | |
| 181 | + $envelope = '<?xml version="1.0" encoding="utf-8"?>'; | |
| 182 | + $envelope .= '<PrintRequestInfo Version="1.00"><ePOSPrint>'; | |
| 183 | + $envelope .= '<Parameter><devid>local_printer</devid><timeout>' . self::EPSON_SDP_PRINT_TIMEOUT_MS . '</timeout></Parameter>'; | |
| 184 | + $envelope .= '<PrintData>' . $render['body'] . '</PrintData>'; | |
| 185 | + $envelope .= '</ePOSPrint></PrintRequestInfo>'; | |
| 186 | + return array( | |
| 187 | + 'status' => 200, | |
| 188 | + 'headers' => array( 'Content-Type' => 'text/xml; charset=utf-8' ), | |
| 189 | + 'body' => $envelope, | |
| 190 | + ); | |
| 191 | + } | |
| 192 | + | |
| 193 | + /** | |
| 194 | + * Identify the result target. | |
| 195 | + * | |
| 196 | + * @param array $poll Parsed request. | |
| 197 | + */ | |
| 198 | + public function match_result( array $poll ): array { | |
| 199 | + // SDP 1.00 carries no job token: match the active or latest unconfirmed claim. | |
| 200 | + return array( 'job_id' => null ); | |
| 201 | + } | |
| 202 | + | |
| 203 | + /** | |
| 204 | + * Decode the printer result. | |
| 205 | + * | |
| 206 | + * @param array $poll Parsed request. | |
| 207 | + */ | |
| 208 | + public function interpret_result( array $poll ): array { | |
| 209 | + $result_xml = $poll['result_xml']; | |
| 210 | + $code = 'unknown'; | |
| 211 | + if ( 1 === preg_match( '/\bcode="([^"]*)"/', $result_xml, $matches ) ) { | |
| 212 | + $code = sanitize_text_field( $matches[1] ); | |
| 213 | + } | |
| 214 | + $status = null; | |
| 215 | + if ( 1 === preg_match( '/\bstatus="(\d+)"/', $result_xml, $matches ) ) { | |
| 216 | + // Do not misdecode unsigned bit 31 on a 32-bit PHP build. | |
| 217 | + $status = (float) $matches[1] <= PHP_INT_MAX ? (int) $matches[1] : null; | |
| 218 | + } | |
| 219 | + $flags = null === $status ? '' : implode( ', ', self::describe_epson_status( $status ) ); | |
| 220 | + $detail = null === $status ? '' : sprintf( ' (0x%08X%s)', $status, '' === $flags ? '' : ': ' . $flags ); | |
| 221 | + return array( | |
| 222 | + 'ok' => false !== strpos( $result_xml, 'success="true"' ), | |
| 223 | + 'code' => $code, | |
| 224 | + 'detail' => $detail, | |
| 225 | + ); | |
| 226 | + } | |
| 227 | + | |
| 228 | + /** | |
| 229 | + * Decode an Epson ePOS-Print response status bitmask. | |
| 230 | + * | |
| 231 | + * @param int $status Decimal ASB status bitmask. | |
| 232 | + * | |
| 233 | + * @return array<int, string> | |
| 234 | + */ | |
| 235 | + private static function describe_epson_status( int $status ): array { | |
| 236 | + // Epson ePOS-Print XML User's Manual, response `status` table (ASB bits). | |
| 237 | + // Fault bits only: informational ones (print complete, drawer pin, feed | |
| 238 | + // button, panel switch, buzzer) say nothing about why a print failed. | |
| 239 | + $labels = array( | |
| 240 | + 0x00000001 => __( 'no response from printer', 'woocommerce-pos' ), | |
| 241 | + 0x00000008 => __( 'offline', 'woocommerce-pos' ), | |
| 242 | + 0x00000020 => __( 'cover open', 'woocommerce-pos' ), | |
| 243 | + 0x00000100 => __( 'waiting for online recovery', 'woocommerce-pos' ), | |
| 244 | + 0x00000400 => __( 'mechanical error', 'woocommerce-pos' ), | |
| 245 | + 0x00000800 => __( 'autocutter error', 'woocommerce-pos' ), | |
| 246 | + 0x00002000 => __( 'unrecoverable error', 'woocommerce-pos' ), | |
| 247 | + 0x00004000 => __( 'auto-recoverable error', 'woocommerce-pos' ), | |
| 248 | + 0x00020000 => __( 'paper near end', 'woocommerce-pos' ), | |
| 249 | + 0x00080000 => __( 'paper end', 'woocommerce-pos' ), | |
| 250 | + 0x80000000 => __( 'spooler stopped', 'woocommerce-pos' ), | |
| 251 | + ); | |
| 252 | + | |
| 253 | + $descriptions = array(); | |
| 254 | + foreach ( $labels as $bit => $label ) { | |
| 255 | + if ( 0 !== ( $status & $bit ) ) { | |
| 256 | + $descriptions[] = $label; | |
| 257 | + } | |
| 258 | + } | |
| 259 | + | |
| 260 | + return $descriptions; | |
| 90 | 261 | } |
| 91 | 262 | } |