├── .gitignore ├── README.md ├── WikiTelFax.sla ├── config.js ├── constants.js ├── hamm-table.js ├── hamm.js ├── hammgen └── hammgen.js ├── html └── index.html ├── keystroke.js ├── log.js ├── package-lock.json ├── package.json ├── page.js ├── pagetest.js ├── private ├── R10000.TTIx ├── muttlee.scss ├── muttlee_logo.svg ├── p404.tti ├── p404_editable.tti ├── zapper_compact.svg └── zapper_standard.svg ├── public ├── assets │ ├── R10000.txt │ ├── WikiTelFax.pdf │ ├── pixelate.svg │ ├── teletext2.otf │ ├── teletext2.ttf │ ├── teletext2.woff │ ├── teletext2.woff2 │ ├── teletext4.otf │ ├── teletext4.ttf │ ├── teletext4.woff │ └── teletext4.woff2 ├── charchanged.js ├── clut.js ├── cursor.js ├── edittest.js ├── edittf.js ├── favicon.ico ├── libraries │ ├── p5.js │ ├── p5.min.js │ └── socket.io.min.js ├── log.js ├── muttlee.css ├── muttlee.css.map ├── muttlee.min.css ├── muttlee.min.css.map ├── sketch.js ├── ttxpage.js ├── ttxproperties.js └── uifield.js ├── service.js ├── servicelist.js ├── teletextserver.js ├── update-service-pages.js ├── utils.js ├── weather.js └── x28test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # IDE config files 33 | .idea 34 | 35 | # Locally generated SSL certificate files 36 | *.pem 37 | 38 | # Local config variants 39 | config.*.js 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | 44 | # Pages 45 | BBCNEWS/BBC100.ttix.BAK 46 | *.bak 47 | *.ttix 48 | *.tti 49 | !private/p404.tti 50 | !private/p404_editable.tti 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Muttlee 2 | Multi User TeleText Live Edit Environment 3 | 4 | The advantage of Teletext is that it is easy to view. 5 | Most people already know how to do that. 6 | What is difficult is creating and publishing your own pages. 7 | 8 | Editors like wxTED give full access to all of teletext's features. 9 | edit.tf lets you create pages without even needing an installation. 10 | 11 | The difficulty is running your own service. 12 | 13 | It is hard to believe but there are people who couldn't open an SSH session to save their life. 14 | People who think that VIM is for cleaning the bathroom. 15 | 16 | Just imagine if your teletext viewer could turn into an editor. 17 | An editor with no complicated menus or options. 18 | 19 | A complete teletext service in your browser. 20 | It will allow you to view or edit a teletext service on a single browser page, not even a save button. 21 | 22 | The code here is used in the online Teefax browser based viewer. 23 | The editing features are included but these are NOT promoted as they are only experimental so far. 24 | 25 | The rest is a statement of intent. Not much of this is implemented yet... 26 | 27 | 28 | ## What it does 29 | You can navigate pages on a service using the usual number keys or Fastext links. 30 | 31 | You can edit any page that you view (In future it will be subject to permissions). 32 | 33 | You enter and exit editing by pressing escape. Edit mode is signalled by the page number 34 | turning yellow. Normally it will just say a page number. 35 | 36 | `P123` 37 | 38 | In edit mode it becomes 39 | 40 | `123.00` 41 | 42 | Where the text is yellow and the subpage number is added. 43 | 44 | The subpage that you are editing can be selected by using the PAGE_UP and PAGE_DOWN keys. 45 | 46 | Anything that you type will instantly appear on your viewer as you'd expect. 47 | 48 | Any change you make will instantly appear on all other viewers that happen to be on the same page. 49 | 50 | Raspberry Pi client viewers will [in the future] also update VBIT-Pi instantly. 51 | 52 | 53 | ## How it works 54 | 55 | The client viewer is javascript. It uses the p5.js library to simplify the rendering code enormously. 56 | 57 | It makes a connection to the server. 58 | 59 | The basic data message consists of a keystroke, a row and column position, and a page number. 60 | 61 | Any keystroke that you make gets turned into a message packet and is sent to the server. 62 | 63 | The server records this keystroke and updates its copy of the page. 64 | 65 | The server forwards the same message packet to all clients. 66 | 67 | The clients pick up the packet, and if it is for their current page then the keystroke is applied to the page. 68 | 69 | The server is also javascript. 70 | 71 | It runs on the node.js environment with Express for web services and socket.io for message passing. 72 | 73 | Express is used to serve static http from the `public` folder. 74 | 75 | 76 | ## Installation 77 | Install the project dependencies via the Node package manager: 78 | 79 | `npm install` 80 | 81 | 82 | ### Running the server 83 | The system stays alive by using PM2. update-service-pages.js is set to run every five minutes. The environment is Debian so this is what worked for me. 84 | 85 | ``` 86 | sudo npm install pm2@latest -g 87 | pm2 start teletextserver.js 88 | pm2 start update-service-pages.js --cron "*/5 * * * *" --no-autorestart 89 | pm2 save 90 | ``` 91 | 92 | 93 | ## Development 94 | 95 | There are a lot of print messages that are useful in debugging the system. 96 | 97 | Stop the system and run it in a shell: 98 | 99 | `pm2 stop teletextserver.js` 100 | 101 | `node teletextserver.js` 102 | 103 | Don't forget to restart the automatic service afterwards. 104 | 105 | `pm2 start teletextserver.js` 106 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const CONST = require('./constants.js'); 2 | 3 | 4 | const CONFIG = { 5 | [CONST.CONFIG.IS_DEV]: false, 6 | 7 | [CONST.CONFIG.LOG_LEVEL_TELETEXT_SERVER]: CONST.LOG_LEVEL_VERBOSE, 8 | [CONST.CONFIG.LOG_LEVEL_TELETEXT_VIEWER]: CONST.LOG_LEVEL_INFO, 9 | 10 | // this is used by `update-service-pages.js` as the place where the 11 | // raw source service pages directories (likely SVN repositories) 12 | // are located. 13 | // (Note: service pages are not actually served by Muttlee from here 14 | // they need additional renaming first, and are then served from 15 | // CONST.CONFIG.SERVICE_PAGES_SERVE_DIR defined below). 16 | [CONST.CONFIG.SERVICE_PAGES_DIR]: '/var/www/teletext-services', 17 | 18 | // (Note: this is the root directory for the live service pages, 19 | // within it should be individual service subdirectories 20 | // e.g. /var/www/private/onair/teefax, /var/www/private/onair/d2k, 21 | // etc. matching the id's of the services defined below in 22 | // CONST.CONFIG.SERVICES_AVAILABLE) 23 | [CONST.CONFIG.SERVICE_PAGES_SERVE_DIR]: '/var/www/private/onair', 24 | 25 | [CONST.CONFIG.PAGE_404_PATH]: '/var/www/private/p404.tti', 26 | [CONST.CONFIG.PAGE_404_EDITABLE_PATH]: '/var/www/private/p404_editable.tti', 27 | 28 | [CONST.CONFIG.LOGO_SVG_PATH]: '/var/www/private/muttlee_logo.svg', 29 | [CONST.CONFIG.ZAPPER_STANDARD_SVG_PATH]: '/var/www/private/zapper_standard.svg', 30 | [CONST.CONFIG.ZAPPER_COMPACT_SVG_PATH]: '/var/www/private/zapper_compact.svg', 31 | 32 | [CONST.CONFIG.SHOW_CONSOLE_LOGO]: true, 33 | [CONST.CONFIG.CONSOLE_LOGO_CHAR_ARRAY]: [ 34 | '████████████ ███ ███ ███ ', 35 | '███ ██ ██ ██ ███ ██ ███████ ███████ ███ ██████ ██████', 36 | '███ ██ ██ ██ ███ ██ ███ ███ ███ ███ ██ ███ ██', 37 | '███ ██ ██ ██ ███ ██ ███ ███ ███ ██████ ██████', 38 | '███ ██ ██ ██ ███ ██ ███ ███ ███ ███ ███ ', 39 | '███ ██ ██ ██ ██████ █████ █████ ███ ██████ ██████', 40 | ], 41 | 42 | [CONST.CONFIG.TITLE]: 'Muttlee', 43 | [CONST.CONFIG.HEADER_TITLE]: 'Teefax', 44 | 45 | // Banned IP addresses, all of them Amazon AWS bots making annoying connections during debugging 46 | [CONST.CONFIG.BANNED_IP_ADDRESSES]: [ 47 | '54.159.215.81', 48 | '54.161.11.39', 49 | '54.235.50.87', 50 | '54.162.45.98', 51 | '54.162.186.216', 52 | ], 53 | 54 | [CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTP]: true, 55 | [CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTP_PORT]: 8080, 56 | 57 | [CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTPS]: false, 58 | [CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTPS_PORT]: 443, 59 | 60 | // Use LetsEncrypt (recommended), or otherwise OpenSSL to generate a local unsigned certificate 61 | // (will raise a warning in the visitor's web browser): 62 | // > openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 63 | // > openssl rsa -in keytmp.pem -out key.pem 64 | [CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTPS_KEY_PATH]: '/var/www/key.pem', 65 | [CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTPS_CERT_PATH]: '/var/www/cert.pem', 66 | 67 | // Allow the viewer to make HTTPS socket.io connections even if SSL certificate is not fully valid? 68 | // DO NOT SET THIS TO TRUE IN PRODUCTION! 69 | [CONST.CONFIG.TELETEXT_VIEWER_HTTPS_REJECT_UNAUTHORIZED]: false, 70 | 71 | 72 | // service definitions, in the following format: 73 | // [id_of_service]: { 74 | // // (optional) group name of service 75 | // group: str, 76 | // // display name of service 77 | // name: str, 78 | // // (optional) header display name of service 79 | // headerTitle: str 80 | // // (optional) service credit 81 | // credit: str, 82 | // 83 | // // url should be protocol independent (start with //), so that http and https can both be accommodated) 84 | // // 85 | // // To use this local teletext server, set `url` to '//localhost' 86 | // // (also ensure that `port` matches one of the enabled 87 | // // TELETEXT_VIEWER_SERVE_HTTPS_PORT or TELETEXT_VIEWER_SERVE_HTTPS_PORT above) 88 | // url: str, 89 | // port: int, 90 | // 91 | // // (optional) the character used to separate seconds in the page header display - use ':' or '/' 92 | // secondsSeparator: str, 93 | // // (optional) whether to force render a service header overriding the page-specific header - false if not defined 94 | // forceServiceHeader: bool, 95 | // 96 | // // (optional) whether the service is considered editable - false if not defined 97 | // isEditable: bool, 98 | // 99 | // // (optional) the type of repo - Can be 'git' or 'svn' if not defined 100 | // repoType: str, 101 | // 102 | // // (optional) SVN or GIT repository containing the service's pages 103 | // // If isEditable===true then you must use a git repo, not https 104 | // updateUrl: str, 105 | // // (optional) number of minutes to wait before checking for updates 106 | // updateInterval: int, 107 | // } 108 | // [!] When adding or removing items, constants.js also needs to be updated 109 | [CONST.CONFIG.SERVICES_AVAILABLE]: { 110 | [CONST.SERVICE_TEEFAX]: { 111 | name: 'Teefax', 112 | url: '//www.xenoxxx.com', 113 | port: 80, 114 | 115 | secondsSeparator: '/', 116 | forceServiceHeader: false, 117 | 118 | autoplaySkip: [ 119 | [741, 759], 120 | [769, 799], 121 | ], 122 | repoType: 'svn', 123 | updateUrl: 'http://teastop.plus.com/svn/teletext/', 124 | updateInterval: 60, 125 | }, 126 | 127 | [CONST.SERVICE_SPARK]: { 128 | name: 'TVARK Spark', 129 | headerTitle: 'SPARK', 130 | credit: 'Pages via TVARK', 131 | url: '//www.xenoxxx.com', 132 | port: 80, 133 | 134 | secondsSeparator: '/', 135 | forceServiceHeader: true, 136 | 137 | repoType: 'git', 138 | updateUrl: 'https://github.com/spark-teletext/spark-teletext.git', 139 | updateInterval: 60, 140 | }, 141 | 142 | [CONST.SERVICE_ARTFAX]: { 143 | name: 'Artfax', 144 | credit: 'Teletext Sofa Club', 145 | url: '//www.xenoxxx.com', 146 | port: 80, 147 | 148 | repoType: 'git', 149 | updateUrl: 'git@github.com:teletexx/service-artfax.git', 150 | updateInterval: 60, 151 | 152 | isEditable: true, 153 | }, 154 | 155 | [CONST.SERVICE_NEMETEXT]: { 156 | name: 'Nemetext', 157 | credit: 'Jamie Nemeth', 158 | url: '//www.xenoxxx.com', 159 | port: 80, 160 | 161 | repoType: 'git', 162 | updateUrl: 'https://github.com/JamieNemeth/nemetext.git', 163 | updateInterval: 60, 164 | isEditable: false, 165 | }, 166 | 167 | [CONST.SERVICE_BBC1980]: { 168 | name: 'BBC 1980', 169 | url: '//www.xenoxxx.com', 170 | port: 80, 171 | 172 | repoType: 'git', 173 | updateUrl: 'https://github.com/teletexx/service-bbc1980.git', 174 | updateInterval: 1440, 175 | }, 176 | 177 | [CONST.SERVICE_DIGITISER]: { 178 | name: 'Digitiser2000', 179 | url: '//www.xenoxxx.com', 180 | port: 80, 181 | 182 | repoType: 'git', 183 | updateUrl: 'https://github.com/teletexx/service-digitiser2k.git', 184 | updateInterval: 1440, 185 | }, 186 | 187 | [CONST.SERVICE_KINDIE]: { 188 | name: 'Kindie', 189 | url: '//www.xenoxxx.com', 190 | port: 80, 191 | 192 | repoType: 'git', 193 | updateUrl: 'https://github.com/teletexx/service-kindie.git', 194 | updateInterval: 1440, 195 | }, 196 | 197 | [CONST.SERVICE_ARCHIVE]: { 198 | name: 'Archive', 199 | url: '//www.xenoxxx.com', 200 | port: 80, 201 | 202 | repoType: 'git', 203 | updateUrl: 'https://github.com/teletexx/service-archive.git', 204 | updateInterval: 1440, 205 | }, 206 | 207 | [CONST.SERVICE_TURNER]: { 208 | name: 'Turner the Worm', 209 | url: '//www.xenoxxx.com', 210 | port: 80, 211 | 212 | repoType: 'git', 213 | updateUrl: 'https://github.com/teletexx/service-turner.git', 214 | updateInterval: 1440, 215 | }, 216 | 217 | [CONST.SERVICE_WIKI]: { 218 | name: 'Wiki', 219 | url: '//www.xenoxxx.com', 220 | port: 80, 221 | 222 | repoType: 'git', 223 | updateUrl: 'git@github.com:peterkvt80/service-wiki.git', 224 | updateInterval: 10, 225 | 226 | isEditable: true, 227 | }, 228 | 229 | 230 | // NMS Ceefax services 231 | [CONST.SERVICE_NMS_CEEFAX_NATIONAL]: { 232 | // group: 'NMS Ceefax', 233 | name: 'Ceefax (National)', 234 | credit: 'Pages via NMS Ceefax', 235 | url: '//www.xenoxxx.com', 236 | port: 80, 237 | 238 | repoType: 'svn', 239 | updateUrl: 'https://feeds.nmsni.co.uk/svn/ceefax/national', 240 | updateInterval: 5, 241 | isEditable: false, 242 | }, 243 | /* 244 | [CONST.SERVICE_NMS_CEEFAX__EAST]: { 245 | group: 'NMS Ceefax', 246 | name: 'East', 247 | credit: 'Pages via NMS Ceefax', 248 | url: '//www.xenoxxx.com', 249 | port: 80, 250 | 251 | repoType: 'git', 252 | updateUrl: 'https://github.com/teletexx/service-nms-i--east.git', 253 | updateInterval: 30, 254 | }, 255 | 256 | [CONST.SERVICE_NMS_CEEFAX__EASTMIDLANDS]: { 257 | group: 'NMS Ceefax', 258 | name: 'East Midlands', 259 | credit: 'Pages via NMS Ceefax', 260 | url: '//www.xenoxxx.com', 261 | port: 80, 262 | 263 | repoType: 'git', 264 | updateUrl: 'https://github.com/teletexx/service-nms-i--eastmidlands.git', 265 | updateInterval: 30, 266 | }, 267 | 268 | [CONST.SERVICE_NMS_CEEFAX__LONDON]: { 269 | group: 'NMS Ceefax', 270 | name: 'London', 271 | credit: 'Pages via NMS Ceefax', 272 | url: '//www.xenoxxx.com', 273 | port: 80, 274 | 275 | repoType: 'git', 276 | updateUrl: 'https://github.com/teletexx/service-nms-i--london.git', 277 | updateInterval: 30, 278 | }, 279 | 280 | [CONST.SERVICE_NMS_CEEFAX__NORTHERNIRELAND]: { 281 | group: 'NMS Ceefax', 282 | name: 'Northern Ireland', 283 | credit: 'Pages via NMS Ceefax', 284 | url: '//www.xenoxxx.com', 285 | port: 80, 286 | 287 | repoType: 'git', 288 | updateUrl: 'https://github.com/teletexx/service-nms-i--northernireland.git', 289 | updateInterval: 30, 290 | }, 291 | 292 | [CONST.SERVICE_NMS_CEEFAX__SCOTLAND]: { 293 | group: 'NMS Ceefax', 294 | name: 'Scotland', 295 | credit: 'Pages via NMS Ceefax', 296 | url: '//www.xenoxxx.com', 297 | port: 80, 298 | 299 | repoType: 'git', 300 | updateUrl: 'https://github.com/teletexx/service-nms-i--scotland.git', 301 | updateInterval: 30, 302 | }, 303 | 304 | [CONST.SERVICE_NMS_CEEFAX__SOUTH]: { 305 | group: 'NMS Ceefax', 306 | name: 'South', 307 | credit: 'Pages via NMS Ceefax', 308 | url: '//www.xenoxxx.com', 309 | port: 80, 310 | 311 | repoType: 'git', 312 | updateUrl: 'https://github.com/teletexx/service-nms-i--south.git', 313 | updateInterval: 30, 314 | }, 315 | 316 | [CONST.SERVICE_NMS_CEEFAX__SOUTHWEST]: { 317 | group: 'NMS Ceefax', 318 | name: 'South West', 319 | credit: 'Pages via NMS Ceefax', 320 | url: '//www.xenoxxx.com', 321 | port: 80, 322 | 323 | repoType: 'svn', 324 | updateUrl: 'https://internal.nathanmediaservices.co.uk/svn/ceefax/SouthWest', 325 | updateInterval: 30, 326 | }, 327 | 328 | [CONST.SERVICE_NMS_CEEFAX__WALES]: { 329 | group: 'NMS Ceefax', 330 | name: 'Wales', 331 | credit: 'Pages via NMS Ceefax', 332 | url: '//localhost', 333 | port: 8080, 334 | 335 | repoType: 'git', 336 | updateUrl: 'https://github.com/teletexx/service-nms-i--wales.git', 337 | updateInterval: 30, 338 | }, 339 | 340 | [CONST.SERVICE_NMS_CEEFAX__WEST]: { 341 | group: 'NMS Ceefax', 342 | name: 'West', 343 | credit: 'Pages via NMS Ceefax', 344 | url: '//www.xenoxxx.com', 345 | port: 80, 346 | 347 | repoType: 'svn', 348 | updateUrl: 'https://internal.nathanmediaservices.co.uk/svn/ceefax/West', 349 | updateInterval: 30, 350 | }, 351 | 352 | // [CONST.SERVICE_NMS_CEEFAX__YORKSLINCS]: { 353 | // group: 'NMS Ceefax', 354 | // name: 'Yorks & Lincs', 355 | // credit: 'Pages via NMS Ceefax', 356 | // url: '//www.xenoxxx.com', 357 | // port: 80, 358 | // 359 | // repoType: 'git', 360 | // updateUrl: 'https://github.com/teletexx/service-nms-i--yorkslincs.git', 361 | // updateInterval: 30, 362 | // }, 363 | */ 364 | }, 365 | 366 | // defaults 367 | [CONST.CONFIG.DEFAULT_SERVICE]: CONST.SERVICE_TEEFAX, 368 | [CONST.CONFIG.OPEN_SERVICE_IN_NEW_WINDOW]: false, 369 | 370 | [CONST.CONFIG.DEFAULT_CONTROLS]: CONST.CONTROLS_STANDARD, 371 | [CONST.CONFIG.DEFAULT_DISPLAY]: CONST.DISPLAY_STANDARD, 372 | [CONST.CONFIG.DEFAULT_MENU_OPEN]: true, 373 | [CONST.CONFIG.DEFAULT_SCALE]: 1, 374 | [CONST.CONFIG.DEFAULT_AUTOPLAY]: CONST.AUTOPLAY_NONE, 375 | 376 | [CONST.CONFIG.DEFAULT_AUTOPLAY_INTERVAL]: 35, // seconds 377 | [CONST.CONFIG.DEFAULT_AUTOSAVE_INTERVAL]: 60, // seconds 378 | 379 | 380 | // rendering 381 | [CONST.CONFIG.NUM_COLUMNS]: 40, 382 | [CONST.CONFIG.NUM_ROWS]: 25, 383 | 384 | [CONST.CONFIG.CANVAS_WIDTH]: 600, 385 | [CONST.CONFIG.CANVAS_HEIGHT]: 510, 386 | [CONST.CONFIG.CANVAS_PADDING_RIGHT_SINGLE_COLUMN]: true, 387 | [CONST.CONFIG.TELETEXT_FONT_SIZE]: 20, 388 | 389 | 390 | // explicitly whitelist config values that are available in the frontend 391 | [CONST.CONFIG.FRONTEND_CONFIG_KEYS]: [ 392 | CONST.CONFIG.LOG_LEVEL_TELETEXT_VIEWER, 393 | 394 | CONST.CONFIG.TITLE, 395 | CONST.CONFIG.HEADER_TITLE, 396 | 397 | CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTP, 398 | CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTP_PORT, 399 | 400 | CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTPS, 401 | CONST.CONFIG.TELETEXT_VIEWER_SERVE_HTTPS_PORT, 402 | 403 | CONST.CONFIG.TELETEXT_VIEWER_HTTPS_REJECT_UNAUTHORIZED, 404 | 405 | CONST.CONFIG.SERVICES_AVAILABLE, 406 | 407 | CONST.CONFIG.DEFAULT_SERVICE, 408 | CONST.CONFIG.DEFAULT_CONTROLS, 409 | CONST.CONFIG.DEFAULT_DISPLAY, 410 | CONST.CONFIG.DEFAULT_MENU_OPEN, 411 | CONST.CONFIG.DEFAULT_SCALE, 412 | CONST.CONFIG.DEFAULT_AUTOPLAY, 413 | 414 | CONST.CONFIG.DEFAULT_AUTOPLAY_INTERVAL, 415 | CONST.CONFIG.DEFAULT_AUTOSAVE_INTERVAL, 416 | 417 | CONST.CONFIG.OPEN_SERVICE_IN_NEW_WINDOW, 418 | 419 | CONST.CONFIG.NUM_COLUMNS, 420 | CONST.CONFIG.NUM_ROWS, 421 | 422 | CONST.CONFIG.CANVAS_WIDTH, 423 | CONST.CONFIG.CANVAS_HEIGHT, 424 | CONST.CONFIG.CANVAS_PADDING_RIGHT_SINGLE_COLUMN, 425 | CONST.CONFIG.TELETEXT_FONT_SIZE, 426 | ], 427 | }; 428 | 429 | 430 | if (typeof exports === 'object') { 431 | module.exports = CONFIG; 432 | } 433 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const CONST = { 3 | // page numbers 4 | PAGE_MIN: 0x100, 5 | // PAGE_MAX: 0x8ff, // This is the proper value 6 | PAGE_MAX: 0x9ff, // However, Muttlee can make forbidden pages 7 | 8 | PAGE_404: 0x404, 9 | 10 | // page files 11 | PAGE_EXT_TTI: '.tti', 12 | ENCODING_ASCII: 'ascii', 13 | 14 | // log levels 15 | LOG_LEVEL_VERBOSE: 3, 16 | LOG_LEVEL_INFO: 2, 17 | LOG_LEVEL_MANDATORY: 1, 18 | LOG_LEVEL_ERROR: 0, 19 | LOG_LEVEL_NONE: -1, 20 | 21 | // custom HTML attributes 22 | ATTR_DATA_SERVICE: 'data-service', 23 | ATTR_DATA_SERVICE_MANIFEST: 'data-service-manifest', 24 | ATTR_DATA_SERVICE_EDITABLE: 'data-service-editable', 25 | ATTR_DATA_READY: 'data-ready', 26 | 27 | ATTR_DATA_CONTROLS: 'data-controls', 28 | ATTR_DATA_DISPLAY: 'data-display', 29 | ATTR_DATA_SCALE: 'data-scale', 30 | ATTR_DATA_AUTOPLAY: 'data-autoplay', 31 | ATTR_DATA_GRID: 'data-grid', 32 | ATTR_DATA_MENU_OPEN: 'data-menu-open', 33 | 34 | // edit modes 35 | EDITMODE_NORMAL: 0, // normal viewing 36 | EDITMODE_EDIT: 1, // edit mode 37 | EDITMODE_ESCAPE: 2, // expect next character to be either an edit.tf function or Escape again to exit. 38 | EDITMODE_INSERT: 3, // The next character is ready to insert. Not sure what this does. @todo 39 | EDITMODE_PROPERTIES: 4,// Editing page properties. Description, X26/X28 enhancements etc. 40 | EDITMODE_MAX: 5, // Range check 41 | 42 | // state signals 43 | SIGNAL_PAGE_NOT_FOUND: -1, 44 | SIGNAL_INITIAL_LOAD: 2000, 45 | SIGNAL_DESCRIPTION_CHANGE: 2001, 46 | 47 | // display modes 48 | DISPLAY_STANDARD: 'standard', 49 | DISPLAY_FITSCREEN: 'fitscreen', 50 | DISPLAY_FULLSCREEN: 'fullscreen', 51 | 52 | // control modes 53 | CONTROLS_STANDARD: 'standard', 54 | CONTROLS_ZAPPER: 'zapper', 55 | CONTROLS_MINIMAL: 'minimal', 56 | CONTROLS_BIGSCREEN: 'bigscreen', 57 | 58 | // autoplay modes 59 | AUTOPLAY_NONE: 'none', 60 | AUTOPLAY_SEQUENTIAL: 'sequential', 61 | AUTOPLAY_RANDOM: 'random', 62 | 63 | // services 64 | // (these ID's are also the name of the subdirectories of 65 | // CONFIG.SERVICE_PAGES_DIR and CONFIG.SERVICE_PAGES_SERVE_DIR 66 | // that contains the service's pages) 67 | SERVICE_TEEFAX: 'teefax', 68 | SERVICE_SPARK: 'spark', 69 | SERVICE_ARTFAX: 'artfax', 70 | SERVICE_NEMETEXT: 'nemetext', 71 | // SERVICE_AMIGAROB: 'amigarob', 72 | SERVICE_BBC1980: 'bbc1980', 73 | SERVICE_DIGITISER: 'd2k', 74 | SERVICE_KINDIE: 'kindie', 75 | SERVICE_ARCHIVE: 'readback', 76 | SERVICE_TURNER: 'turner', 77 | SERVICE_WIKI: 'wtf', 78 | //SERVICE_CHANNEL19: 'channel19', 79 | //SERVICE_CHRISLUCA: 'chrisluca', 80 | 81 | 82 | SERVICE_NMS_CEEFAX_NATIONAL: 'nms_cf_national', 83 | //SERVICE_NMS_CEEFAX__EAST: 'nms_cf_east', 84 | //SERVICE_NMS_CEEFAX__EASTMIDLANDS: 'nms_cf_eastmidlands', 85 | //SERVICE_NMS_CEEFAX__LONDON: 'nms_cf_london', 86 | //SERVICE_NMS_CEEFAX__NORTHERNIRELAND: 'nms_cf_northernireland', 87 | //SERVICE_NMS_CEEFAX__SCOTLAND: 'nms_cf_scotland', 88 | //SERVICE_NMS_CEEFAX__SOUTH: 'nms_cf_south', 89 | //SERVICE_NMS_CEEFAX__SOUTHWEST: 'nms_cf_southwest', 90 | //SERVICE_NMS_CEEFAX__WALES: 'nms_cf_wales', 91 | //SERVICE_NMS_CEEFAX__WEST: 'nms_cf_west', 92 | //SERVICE_NMS_CEEFAX__YORKSLINCS: 'nms_cf_yorkslincs', 93 | 94 | // config keys 95 | CONFIG: { 96 | IS_DEV: 'IS_DEV', 97 | 98 | LOG_LEVEL_TELETEXT_SERVER: 'LOG_LEVEL_TELETEXT_SERVER', 99 | LOG_LEVEL_TELETEXT_VIEWER: 'LOG_LEVEL_TELETEXT_VIEWER', 100 | 101 | SERVICE_PAGES_DIR: 'SERVICE_PAGES_DIR', 102 | SERVICE_PAGES_SERVE_DIR: 'SERVICE_PAGES_SERVE_DIR', 103 | 104 | PAGE_404_PATH: 'PAGE_404_PATH', 105 | PAGE_404_EDITABLE_PATH: 'PAGE_404_EDITABLE_PATH', 106 | 107 | LOGO_SVG_PATH: 'LOGO_SVG_PATH', 108 | ZAPPER_STANDARD_SVG_PATH: 'ZAPPER_STANDARD_SVG_PATH', 109 | ZAPPER_COMPACT_SVG_PATH: 'ZAPPER_COMPACT_SVG_PATH', 110 | 111 | SHOW_CONSOLE_LOGO: 'SHOW_CONSOLE_LOGO', 112 | CONSOLE_LOGO_CHAR_ARRAY: 'CONSOLE_LOGO_CHAR_ARRAY', 113 | 114 | TITLE: 'TITLE', 115 | HEADER_TITLE: 'HEADER_TITLE', 116 | 117 | BANNED_IP_ADDRESSES: 'BANNED_IP_ADDRESSES', 118 | 119 | TELETEXT_VIEWER_SERVE_HTTP: 'TELETEXT_VIEWER_SERVE_HTTP', 120 | TELETEXT_VIEWER_SERVE_HTTP_PORT: 'TELETEXT_VIEWER_SERVE_HTTP_PORT', 121 | 122 | TELETEXT_VIEWER_SERVE_HTTPS: 'TELETEXT_VIEWER_SERVE_HTTPS', 123 | TELETEXT_VIEWER_SERVE_HTTPS_PORT: 'TELETEXT_VIEWER_SERVE_HTTPS_PORT', 124 | TELETEXT_VIEWER_SERVE_HTTPS_KEY_PATH: 'TELETEXT_VIEWER_SERVE_HTTPS_KEY_PATH', 125 | TELETEXT_VIEWER_SERVE_HTTPS_CERT_PATH: 'TELETEXT_VIEWER_SERVE_HTTPS_CERT_PATH', 126 | 127 | TELETEXT_VIEWER_HTTPS_REJECT_UNAUTHORIZED: 'TELETEXT_VIEWER_HTTPS_REJECT_UNAUTHORIZED', 128 | 129 | SERVICES_AVAILABLE: 'SERVICES_AVAILABLE', 130 | 131 | DEFAULT_SERVICE: 'DEFAULT_SERVICE', 132 | DEFAULT_CONTROLS: 'DEFAULT_CONTROLS', 133 | DEFAULT_DISPLAY: 'DEFAULT_DISPLAY', 134 | DEFAULT_MENU_OPEN: 'DEFAULT_MENU_OPEN', 135 | DEFAULT_SCALE: 'DEFAULT_SCALE', 136 | DEFAULT_AUTOPLAY: 'DEFAULT_AUTOPLAY', 137 | 138 | DEFAULT_AUTOPLAY_INTERVAL: 'DEFAULT_AUTOPLAY_INTERVAL', 139 | DEFAULT_AUTOSAVE_INTERVAL: 'DEFAULT_AUTOSAVE_INTERVAL', 140 | 141 | OPEN_SERVICE_IN_NEW_WINDOW: 'OPEN_SERVICE_IN_NEW_WINDOW', 142 | 143 | // rendering 144 | NUM_COLUMNS: 'NUM_COLUMNS', 145 | NUM_ROWS: 'NUM_ROWS', 146 | 147 | CANVAS_WIDTH: 'CANVAS_WIDTH', 148 | CANVAS_HEIGHT: 'CANVAS_HEIGHT', 149 | CANVAS_PADDING_RIGHT_SINGLE_COLUMN: 'CANVAS_PADDING_RIGHT_SINGLE_COLUMN', 150 | TELETEXT_FONT_SIZE: 'TELETEXT_FONT_SIZE', 151 | 152 | 153 | FRONTEND_CONFIG_KEYS: 'FRONTEND_CONFIG_KEYS', 154 | 155 | }, 156 | // UI Fields for property editing 157 | UI_FIELD: { 158 | FIELD_HEXCOLOUR: 0, // Three digit hex colour 159 | FIELD_CHECKBOX: 1, // Checkbox 160 | FIELD_NUMBER: 2, // Decimal number 161 | FIELD_COMBO: 3, // Combo box 162 | }, 163 | }; 164 | 165 | 166 | if (typeof exports === 'object') { 167 | module.exports = CONST; 168 | } 169 | -------------------------------------------------------------------------------- /hamm-table.js: -------------------------------------------------------------------------------- 1 | /* Generated file, do not edit! */ 2 | 3 | /* node hammgen.js > hamm-table.js * / 4 | 5 | /* This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Library General Public 7 | License as published by the Free Software Foundation; either 8 | version 2 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Library General Public License for more details. 14 | 15 | You should have received a copy of the GNU Library General Public 16 | License along with this library; if not, write to the 17 | Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 | Boston, MA 02110-1301 USA. */ 19 | 20 | const _vbi_bit_reverse = [ 21 | 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 22 | 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, 23 | 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 24 | 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, 25 | 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 26 | 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4, 27 | 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 28 | 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc, 29 | 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 30 | 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2, 31 | 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 32 | 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, 33 | 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 34 | 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, 35 | 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, 36 | 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, 37 | 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, 38 | 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, 39 | 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, 40 | 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, 41 | 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, 42 | 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, 43 | 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, 44 | 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, 45 | 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, 46 | 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, 47 | 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 48 | 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, 49 | 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, 50 | 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7, 51 | 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 52 | 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff 53 | ]; 54 | 55 | const _vbi_hamm8_fwd = [ 56 | 0x15, 0x02, 0x49, 0x5e, 0x64, 0x73, 0x38, 0x2f, 57 | 0xd0, 0xc7, 0x8c, 0x9b, 0xa1, 0xb6, 0xfd, 0xea 58 | ]; 59 | 60 | const _vbi_hamm8_inv = [ 61 | 0x01, 0xff, 0x01, 0x01, 0xff, 0x00, 0x01, 0xff, 62 | 0xff, 0x02, 0x01, 0xff, 0x0a, 0xff, 0xff, 0x07, 63 | 0xff, 0x00, 0x01, 0xff, 0x00, 0x00, 0xff, 0x00, 64 | 0x06, 0xff, 0xff, 0x0b, 0xff, 0x00, 0x03, 0xff, 65 | 0xff, 0x0c, 0x01, 0xff, 0x04, 0xff, 0xff, 0x07, 66 | 0x06, 0xff, 0xff, 0x07, 0xff, 0x07, 0x07, 0x07, 67 | 0x06, 0xff, 0xff, 0x05, 0xff, 0x00, 0x0d, 0xff, 68 | 0x06, 0x06, 0x06, 0xff, 0x06, 0xff, 0xff, 0x07, 69 | 0xff, 0x02, 0x01, 0xff, 0x04, 0xff, 0xff, 0x09, 70 | 0x02, 0x02, 0xff, 0x02, 0xff, 0x02, 0x03, 0xff, 71 | 0x08, 0xff, 0xff, 0x05, 0xff, 0x00, 0x03, 0xff, 72 | 0xff, 0x02, 0x03, 0xff, 0x03, 0xff, 0x03, 0x03, 73 | 0x04, 0xff, 0xff, 0x05, 0x04, 0x04, 0x04, 0xff, 74 | 0xff, 0x02, 0x0f, 0xff, 0x04, 0xff, 0xff, 0x07, 75 | 0xff, 0x05, 0x05, 0x05, 0x04, 0xff, 0xff, 0x05, 76 | 0x06, 0xff, 0xff, 0x05, 0xff, 0x0e, 0x03, 0xff, 77 | 0xff, 0x0c, 0x01, 0xff, 0x0a, 0xff, 0xff, 0x09, 78 | 0x0a, 0xff, 0xff, 0x0b, 0x0a, 0x0a, 0x0a, 0xff, 79 | 0x08, 0xff, 0xff, 0x0b, 0xff, 0x00, 0x0d, 0xff, 80 | 0xff, 0x0b, 0x0b, 0x0b, 0x0a, 0xff, 0xff, 0x0b, 81 | 0x0c, 0x0c, 0xff, 0x0c, 0xff, 0x0c, 0x0d, 0xff, 82 | 0xff, 0x0c, 0x0f, 0xff, 0x0a, 0xff, 0xff, 0x07, 83 | 0xff, 0x0c, 0x0d, 0xff, 0x0d, 0xff, 0x0d, 0x0d, 84 | 0x06, 0xff, 0xff, 0x0b, 0xff, 0x0e, 0x0d, 0xff, 85 | 0x08, 0xff, 0xff, 0x09, 0xff, 0x09, 0x09, 0x09, 86 | 0xff, 0x02, 0x0f, 0xff, 0x0a, 0xff, 0xff, 0x09, 87 | 0x08, 0x08, 0x08, 0xff, 0x08, 0xff, 0xff, 0x09, 88 | 0x08, 0xff, 0xff, 0x0b, 0xff, 0x0e, 0x03, 0xff, 89 | 0xff, 0x0c, 0x0f, 0xff, 0x04, 0xff, 0xff, 0x09, 90 | 0x0f, 0xff, 0x0f, 0x0f, 0xff, 0x0e, 0x0f, 0xff, 91 | 0x08, 0xff, 0xff, 0x05, 0xff, 0x0e, 0x0d, 0xff, 92 | 0xff, 0x0e, 0x0f, 0xff, 0x0e, 0x0e, 0xff, 0x0e 93 | ]; 94 | 95 | const _vbi_hamm24_fwd_0 = [ 96 | 0x8b, 0x8c, 0x92, 0x95, 0xa1, 0xa6, 0xb8, 0xbf, 97 | 0xc0, 0xc7, 0xd9, 0xde, 0xea, 0xed, 0xf3, 0xf4, 98 | 0x0a, 0x0d, 0x13, 0x14, 0x20, 0x27, 0x39, 0x3e, 99 | 0x41, 0x46, 0x58, 0x5f, 0x6b, 0x6c, 0x72, 0x75, 100 | 0x09, 0x0e, 0x10, 0x17, 0x23, 0x24, 0x3a, 0x3d, 101 | 0x42, 0x45, 0x5b, 0x5c, 0x68, 0x6f, 0x71, 0x76, 102 | 0x88, 0x8f, 0x91, 0x96, 0xa2, 0xa5, 0xbb, 0xbc, 103 | 0xc3, 0xc4, 0xda, 0xdd, 0xe9, 0xee, 0xf0, 0xf7, 104 | 0x08, 0x0f, 0x11, 0x16, 0x22, 0x25, 0x3b, 0x3c, 105 | 0x43, 0x44, 0x5a, 0x5d, 0x69, 0x6e, 0x70, 0x77, 106 | 0x89, 0x8e, 0x90, 0x97, 0xa3, 0xa4, 0xba, 0xbd, 107 | 0xc2, 0xc5, 0xdb, 0xdc, 0xe8, 0xef, 0xf1, 0xf6, 108 | 0x8a, 0x8d, 0x93, 0x94, 0xa0, 0xa7, 0xb9, 0xbe, 109 | 0xc1, 0xc6, 0xd8, 0xdf, 0xeb, 0xec, 0xf2, 0xf5, 110 | 0x0b, 0x0c, 0x12, 0x15, 0x21, 0x26, 0x38, 0x3f, 111 | 0x40, 0x47, 0x59, 0x5e, 0x6a, 0x6d, 0x73, 0x74, 112 | 0x03, 0x04, 0x1a, 0x1d, 0x29, 0x2e, 0x30, 0x37, 113 | 0x48, 0x4f, 0x51, 0x56, 0x62, 0x65, 0x7b, 0x7c, 114 | 0x82, 0x85, 0x9b, 0x9c, 0xa8, 0xaf, 0xb1, 0xb6, 115 | 0xc9, 0xce, 0xd0, 0xd7, 0xe3, 0xe4, 0xfa, 0xfd, 116 | 0x81, 0x86, 0x98, 0x9f, 0xab, 0xac, 0xb2, 0xb5, 117 | 0xca, 0xcd, 0xd3, 0xd4, 0xe0, 0xe7, 0xf9, 0xfe, 118 | 0x00, 0x07, 0x19, 0x1e, 0x2a, 0x2d, 0x33, 0x34, 119 | 0x4b, 0x4c, 0x52, 0x55, 0x61, 0x66, 0x78, 0x7f, 120 | 0x80, 0x87, 0x99, 0x9e, 0xaa, 0xad, 0xb3, 0xb4, 121 | 0xcb, 0xcc, 0xd2, 0xd5, 0xe1, 0xe6, 0xf8, 0xff, 122 | 0x01, 0x06, 0x18, 0x1f, 0x2b, 0x2c, 0x32, 0x35, 123 | 0x4a, 0x4d, 0x53, 0x54, 0x60, 0x67, 0x79, 0x7e, 124 | 0x02, 0x05, 0x1b, 0x1c, 0x28, 0x2f, 0x31, 0x36, 125 | 0x49, 0x4e, 0x50, 0x57, 0x63, 0x64, 0x7a, 0x7d, 126 | 0x83, 0x84, 0x9a, 0x9d, 0xa9, 0xae, 0xb0, 0xb7, 127 | 0xc8, 0xcf, 0xd1, 0xd6, 0xe2, 0xe5, 0xfb, 0xfc 128 | ]; 129 | 130 | const _vbi_hamm24_fwd_1 = [ 131 | 0x00, 0x89, 0x8a, 0x03, 0x8b, 0x02, 0x01, 0x88, 132 | 0x01, 0x88, 0x8b, 0x02, 0x8a, 0x03, 0x00, 0x89, 133 | 0x02, 0x8b, 0x88, 0x01, 0x89, 0x00, 0x03, 0x8a, 134 | 0x03, 0x8a, 0x89, 0x00, 0x88, 0x01, 0x02, 0x8b, 135 | 0x03, 0x8a, 0x89, 0x00, 0x88, 0x01, 0x02, 0x8b, 136 | 0x02, 0x8b, 0x88, 0x01, 0x89, 0x00, 0x03, 0x8a, 137 | 0x01, 0x88, 0x8b, 0x02, 0x8a, 0x03, 0x00, 0x89, 138 | 0x00, 0x89, 0x8a, 0x03, 0x8b, 0x02, 0x01, 0x88, 139 | 0x08, 0x81, 0x82, 0x0b, 0x83, 0x0a, 0x09, 0x80, 140 | 0x09, 0x80, 0x83, 0x0a, 0x82, 0x0b, 0x08, 0x81, 141 | 0x0a, 0x83, 0x80, 0x09, 0x81, 0x08, 0x0b, 0x82, 142 | 0x0b, 0x82, 0x81, 0x08, 0x80, 0x09, 0x0a, 0x83, 143 | 0x0b, 0x82, 0x81, 0x08, 0x80, 0x09, 0x0a, 0x83, 144 | 0x0a, 0x83, 0x80, 0x09, 0x81, 0x08, 0x0b, 0x82, 145 | 0x09, 0x80, 0x83, 0x0a, 0x82, 0x0b, 0x08, 0x81, 146 | 0x08, 0x81, 0x82, 0x0b, 0x83, 0x0a, 0x09, 0x80, 147 | 0x09, 0x80, 0x83, 0x0a, 0x82, 0x0b, 0x08, 0x81, 148 | 0x08, 0x81, 0x82, 0x0b, 0x83, 0x0a, 0x09, 0x80, 149 | 0x0b, 0x82, 0x81, 0x08, 0x80, 0x09, 0x0a, 0x83, 150 | 0x0a, 0x83, 0x80, 0x09, 0x81, 0x08, 0x0b, 0x82, 151 | 0x0a, 0x83, 0x80, 0x09, 0x81, 0x08, 0x0b, 0x82, 152 | 0x0b, 0x82, 0x81, 0x08, 0x80, 0x09, 0x0a, 0x83, 153 | 0x08, 0x81, 0x82, 0x0b, 0x83, 0x0a, 0x09, 0x80, 154 | 0x09, 0x80, 0x83, 0x0a, 0x82, 0x0b, 0x08, 0x81, 155 | 0x01, 0x88, 0x8b, 0x02, 0x8a, 0x03, 0x00, 0x89, 156 | 0x00, 0x89, 0x8a, 0x03, 0x8b, 0x02, 0x01, 0x88, 157 | 0x03, 0x8a, 0x89, 0x00, 0x88, 0x01, 0x02, 0x8b, 158 | 0x02, 0x8b, 0x88, 0x01, 0x89, 0x00, 0x03, 0x8a, 159 | 0x02, 0x8b, 0x88, 0x01, 0x89, 0x00, 0x03, 0x8a, 160 | 0x03, 0x8a, 0x89, 0x00, 0x88, 0x01, 0x02, 0x8b, 161 | 0x00, 0x89, 0x8a, 0x03, 0x8b, 0x02, 0x01, 0x88, 162 | 0x01, 0x88, 0x8b, 0x02, 0x8a, 0x03, 0x00, 0x89 163 | ]; 164 | 165 | const _vbi_hamm24_fwd_2 = [ 166 | 0x00, 0x0a, 0x0b, 0x01 167 | ]; 168 | 169 | const _vbi_hamm24_inv_par = [[ 170 | 0x00, 0x21, 0x22, 0x03, 0x23, 0x02, 0x01, 0x20, 171 | 0x24, 0x05, 0x06, 0x27, 0x07, 0x26, 0x25, 0x04, 172 | 0x25, 0x04, 0x07, 0x26, 0x06, 0x27, 0x24, 0x05, 173 | 0x01, 0x20, 0x23, 0x02, 0x22, 0x03, 0x00, 0x21, 174 | 0x26, 0x07, 0x04, 0x25, 0x05, 0x24, 0x27, 0x06, 175 | 0x02, 0x23, 0x20, 0x01, 0x21, 0x00, 0x03, 0x22, 176 | 0x03, 0x22, 0x21, 0x00, 0x20, 0x01, 0x02, 0x23, 177 | 0x27, 0x06, 0x05, 0x24, 0x04, 0x25, 0x26, 0x07, 178 | 0x27, 0x06, 0x05, 0x24, 0x04, 0x25, 0x26, 0x07, 179 | 0x03, 0x22, 0x21, 0x00, 0x20, 0x01, 0x02, 0x23, 180 | 0x02, 0x23, 0x20, 0x01, 0x21, 0x00, 0x03, 0x22, 181 | 0x26, 0x07, 0x04, 0x25, 0x05, 0x24, 0x27, 0x06, 182 | 0x01, 0x20, 0x23, 0x02, 0x22, 0x03, 0x00, 0x21, 183 | 0x25, 0x04, 0x07, 0x26, 0x06, 0x27, 0x24, 0x05, 184 | 0x24, 0x05, 0x06, 0x27, 0x07, 0x26, 0x25, 0x04, 185 | 0x00, 0x21, 0x22, 0x03, 0x23, 0x02, 0x01, 0x20, 186 | 0x28, 0x09, 0x0a, 0x2b, 0x0b, 0x2a, 0x29, 0x08, 187 | 0x0c, 0x2d, 0x2e, 0x0f, 0x2f, 0x0e, 0x0d, 0x2c, 188 | 0x0d, 0x2c, 0x2f, 0x0e, 0x2e, 0x0f, 0x0c, 0x2d, 189 | 0x29, 0x08, 0x0b, 0x2a, 0x0a, 0x2b, 0x28, 0x09, 190 | 0x0e, 0x2f, 0x2c, 0x0d, 0x2d, 0x0c, 0x0f, 0x2e, 191 | 0x2a, 0x0b, 0x08, 0x29, 0x09, 0x28, 0x2b, 0x0a, 192 | 0x2b, 0x0a, 0x09, 0x28, 0x08, 0x29, 0x2a, 0x0b, 193 | 0x0f, 0x2e, 0x2d, 0x0c, 0x2c, 0x0d, 0x0e, 0x2f, 194 | 0x0f, 0x2e, 0x2d, 0x0c, 0x2c, 0x0d, 0x0e, 0x2f, 195 | 0x2b, 0x0a, 0x09, 0x28, 0x08, 0x29, 0x2a, 0x0b, 196 | 0x2a, 0x0b, 0x08, 0x29, 0x09, 0x28, 0x2b, 0x0a, 197 | 0x0e, 0x2f, 0x2c, 0x0d, 0x2d, 0x0c, 0x0f, 0x2e, 198 | 0x29, 0x08, 0x0b, 0x2a, 0x0a, 0x2b, 0x28, 0x09, 199 | 0x0d, 0x2c, 0x2f, 0x0e, 0x2e, 0x0f, 0x0c, 0x2d, 200 | 0x0c, 0x2d, 0x2e, 0x0f, 0x2f, 0x0e, 0x0d, 0x2c, 201 | 0x28, 0x09, 0x0a, 0x2b, 0x0b, 0x2a, 0x29, 0x08 202 | ],[ 203 | 0x00, 0x29, 0x2a, 0x03, 0x2b, 0x02, 0x01, 0x28, 204 | 0x2c, 0x05, 0x06, 0x2f, 0x07, 0x2e, 0x2d, 0x04, 205 | 0x2d, 0x04, 0x07, 0x2e, 0x06, 0x2f, 0x2c, 0x05, 206 | 0x01, 0x28, 0x2b, 0x02, 0x2a, 0x03, 0x00, 0x29, 207 | 0x2e, 0x07, 0x04, 0x2d, 0x05, 0x2c, 0x2f, 0x06, 208 | 0x02, 0x2b, 0x28, 0x01, 0x29, 0x00, 0x03, 0x2a, 209 | 0x03, 0x2a, 0x29, 0x00, 0x28, 0x01, 0x02, 0x2b, 210 | 0x2f, 0x06, 0x05, 0x2c, 0x04, 0x2d, 0x2e, 0x07, 211 | 0x2f, 0x06, 0x05, 0x2c, 0x04, 0x2d, 0x2e, 0x07, 212 | 0x03, 0x2a, 0x29, 0x00, 0x28, 0x01, 0x02, 0x2b, 213 | 0x02, 0x2b, 0x28, 0x01, 0x29, 0x00, 0x03, 0x2a, 214 | 0x2e, 0x07, 0x04, 0x2d, 0x05, 0x2c, 0x2f, 0x06, 215 | 0x01, 0x28, 0x2b, 0x02, 0x2a, 0x03, 0x00, 0x29, 216 | 0x2d, 0x04, 0x07, 0x2e, 0x06, 0x2f, 0x2c, 0x05, 217 | 0x2c, 0x05, 0x06, 0x2f, 0x07, 0x2e, 0x2d, 0x04, 218 | 0x00, 0x29, 0x2a, 0x03, 0x2b, 0x02, 0x01, 0x28, 219 | 0x30, 0x19, 0x1a, 0x33, 0x1b, 0x32, 0x31, 0x18, 220 | 0x1c, 0x35, 0x36, 0x1f, 0x37, 0x1e, 0x1d, 0x34, 221 | 0x1d, 0x34, 0x37, 0x1e, 0x36, 0x1f, 0x1c, 0x35, 222 | 0x31, 0x18, 0x1b, 0x32, 0x1a, 0x33, 0x30, 0x19, 223 | 0x1e, 0x37, 0x34, 0x1d, 0x35, 0x1c, 0x1f, 0x36, 224 | 0x32, 0x1b, 0x18, 0x31, 0x19, 0x30, 0x33, 0x1a, 225 | 0x33, 0x1a, 0x19, 0x30, 0x18, 0x31, 0x32, 0x1b, 226 | 0x1f, 0x36, 0x35, 0x1c, 0x34, 0x1d, 0x1e, 0x37, 227 | 0x1f, 0x36, 0x35, 0x1c, 0x34, 0x1d, 0x1e, 0x37, 228 | 0x33, 0x1a, 0x19, 0x30, 0x18, 0x31, 0x32, 0x1b, 229 | 0x32, 0x1b, 0x18, 0x31, 0x19, 0x30, 0x33, 0x1a, 230 | 0x1e, 0x37, 0x34, 0x1d, 0x35, 0x1c, 0x1f, 0x36, 231 | 0x31, 0x18, 0x1b, 0x32, 0x1a, 0x33, 0x30, 0x19, 232 | 0x1d, 0x34, 0x37, 0x1e, 0x36, 0x1f, 0x1c, 0x35, 233 | 0x1c, 0x35, 0x36, 0x1f, 0x37, 0x1e, 0x1d, 0x34, 234 | 0x30, 0x19, 0x1a, 0x33, 0x1b, 0x32, 0x31, 0x18 235 | ],[ 236 | 0x3f, 0x0e, 0x0d, 0x3c, 0x0c, 0x3d, 0x3e, 0x0f, 237 | 0x0b, 0x3a, 0x39, 0x08, 0x38, 0x09, 0x0a, 0x3b, 238 | 0x0a, 0x3b, 0x38, 0x09, 0x39, 0x08, 0x0b, 0x3a, 239 | 0x3e, 0x0f, 0x0c, 0x3d, 0x0d, 0x3c, 0x3f, 0x0e, 240 | 0x09, 0x38, 0x3b, 0x0a, 0x3a, 0x0b, 0x08, 0x39, 241 | 0x3d, 0x0c, 0x0f, 0x3e, 0x0e, 0x3f, 0x3c, 0x0d, 242 | 0x3c, 0x0d, 0x0e, 0x3f, 0x0f, 0x3e, 0x3d, 0x0c, 243 | 0x08, 0x39, 0x3a, 0x0b, 0x3b, 0x0a, 0x09, 0x38, 244 | 0x08, 0x39, 0x3a, 0x0b, 0x3b, 0x0a, 0x09, 0x38, 245 | 0x3c, 0x0d, 0x0e, 0x3f, 0x0f, 0x3e, 0x3d, 0x0c, 246 | 0x3d, 0x0c, 0x0f, 0x3e, 0x0e, 0x3f, 0x3c, 0x0d, 247 | 0x09, 0x38, 0x3b, 0x0a, 0x3a, 0x0b, 0x08, 0x39, 248 | 0x3e, 0x0f, 0x0c, 0x3d, 0x0d, 0x3c, 0x3f, 0x0e, 249 | 0x0a, 0x3b, 0x38, 0x09, 0x39, 0x08, 0x0b, 0x3a, 250 | 0x0b, 0x3a, 0x39, 0x08, 0x38, 0x09, 0x0a, 0x3b, 251 | 0x3f, 0x0e, 0x0d, 0x3c, 0x0c, 0x3d, 0x3e, 0x0f, 252 | 0x1f, 0x2e, 0x2d, 0x1c, 0x2c, 0x1d, 0x1e, 0x2f, 253 | 0x2b, 0x1a, 0x19, 0x28, 0x18, 0x29, 0x2a, 0x1b, 254 | 0x2a, 0x1b, 0x18, 0x29, 0x19, 0x28, 0x2b, 0x1a, 255 | 0x1e, 0x2f, 0x2c, 0x1d, 0x2d, 0x1c, 0x1f, 0x2e, 256 | 0x29, 0x18, 0x1b, 0x2a, 0x1a, 0x2b, 0x28, 0x19, 257 | 0x1d, 0x2c, 0x2f, 0x1e, 0x2e, 0x1f, 0x1c, 0x2d, 258 | 0x1c, 0x2d, 0x2e, 0x1f, 0x2f, 0x1e, 0x1d, 0x2c, 259 | 0x28, 0x19, 0x1a, 0x2b, 0x1b, 0x2a, 0x29, 0x18, 260 | 0x28, 0x19, 0x1a, 0x2b, 0x1b, 0x2a, 0x29, 0x18, 261 | 0x1c, 0x2d, 0x2e, 0x1f, 0x2f, 0x1e, 0x1d, 0x2c, 262 | 0x1d, 0x2c, 0x2f, 0x1e, 0x2e, 0x1f, 0x1c, 0x2d, 263 | 0x29, 0x18, 0x1b, 0x2a, 0x1a, 0x2b, 0x28, 0x19, 264 | 0x1e, 0x2f, 0x2c, 0x1d, 0x2d, 0x1c, 0x1f, 0x2e, 265 | 0x2a, 0x1b, 0x18, 0x29, 0x19, 0x28, 0x2b, 0x1a, 266 | 0x2b, 0x1a, 0x19, 0x28, 0x18, 0x29, 0x2a, 0x1b, 267 | 0x1f, 0x2e, 0x2d, 0x1c, 0x2c, 0x1d, 0x1e, 0x2f 268 | ]]; 269 | 270 | const _vbi_hamm24_inv_d1_d4 = [ 271 | 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 272 | 0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 273 | 0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 274 | 0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f, 275 | 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 276 | 0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 277 | 0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 278 | 0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f 279 | ]; 280 | 281 | const _vbi_hamm24_inv_err = [ 282 | 0x00000000, 0x80000000, 0x80000000, 0x80000000, 283 | 0x80000000, 0x80000000, 0x80000000, 0x80000000, 284 | 0x80000000, 0x80000000, 0x80000000, 0x80000000, 285 | 0x80000000, 0x80000000, 0x80000000, 0x80000000, 286 | 0x80000000, 0x80000000, 0x80000000, 0x80000000, 287 | 0x80000000, 0x80000000, 0x80000000, 0x80000000, 288 | 0x80000000, 0x80000000, 0x80000000, 0x80000000, 289 | 0x80000000, 0x80000000, 0x80000000, 0x80000000, 290 | 0x00000000, 0x00000000, 0x00000000, 0x00000001, 291 | 0x00000000, 0x00000002, 0x00000004, 0x00000008, 292 | 0x00000000, 0x00000010, 0x00000020, 0x00000040, 293 | 0x00000080, 0x00000100, 0x00000200, 0x00000400, 294 | 0x00000000, 0x00000800, 0x00001000, 0x00002000, 295 | 0x00004000, 0x00008000, 0x00010000, 0x00020000, 296 | 0x80000000, 0x80000000, 0x80000000, 0x80000000, 297 | 0x80000000, 0x80000000, 0x80000000, 0x80000000 298 | ]; 299 | 300 | module.exports = { 301 | _vbi_bit_reverse, 302 | _vbi_hamm8_inv, 303 | _vbi_hamm24_fwd_0, 304 | _vbi_hamm24_fwd_1, 305 | _vbi_hamm24_fwd_2, 306 | _vbi_hamm24_inv_par, 307 | _vbi_hamm24_inv_d1_d4, 308 | _vbi_hamm24_inv_err 309 | }; 310 | -------------------------------------------------------------------------------- /hamm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libzvbi -- Error correction functions 3 | * 4 | * Copyright (C) 2001, 2002, 2003, 2004, 2007 Michael H. Schimek 5 | * Converted to Javascript. 2024 Peter Kwan 6 | * 7 | * This library is free software; you can redistribute it and/or 8 | * modify it under the terms of the GNU Library General Public 9 | * License as published by the Free Software Foundation; either 10 | * version 2 of the License, or (at your option) any later version. 11 | * 12 | * This library is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | * Library General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Library General Public 18 | * License along with this library; if not, write to the 19 | * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 20 | * Boston, MA 02110-1301 USA. 21 | */ 22 | 23 | /* $Id: hamm.c,v 1.11 2013-07-10 11:37:08 mschimek Exp $ */ 24 | 25 | let hamm = require('./hamm-table.js'); // Hamming tables 26 | 27 | 28 | /** 29 | * @ingroup Error 30 | * 31 | * @param p Array of unsigned bytes. 32 | * @param n Size of array. 33 | * 34 | * Of each byte of the array, changes the most significant 35 | * bit to make the number of set bits odd. 36 | * 37 | * @since 0.2.12 38 | */ 39 | // TODO 40 | //function vbi_par (uint8_t * p, 41 | //unsigned int n) 42 | //{ 43 | // while (n-- > 0) { 44 | //uint8_t c = *p; 45 | // 46 | ///* if 0 == (inv_par[] & 32) change msb of *p. */ 47 | //*p++ = c ^ (128 & ~(_vbi_hamm24_inv_par[0][c] << 2)); 48 | //} 49 | //} 50 | 51 | 52 | /** 53 | * @ingroup Error 54 | * @param p Array of unsigned bytes. 55 | * @param n Size of array. 56 | * 57 | * Tests the parity and clears the most significant bit of 58 | * each byte of the array. 59 | * 60 | * @return 61 | * A negative value if any byte of the array had even 62 | * parity (sum of bits modulo 2 is 0). 63 | * 64 | * @since 0.2.12 65 | */ 66 | //int 67 | //vbi_unpar (uint8_t * p, 68 | //unsigned int n) 69 | //{ 70 | // int r = 0; 71 | // 72 | // while (n-- > 0) { 73 | //uint8_t c = *p; 74 | // 75 | ///* if 0 == (inv_par[] & 32) set msb of r. */ 76 | //r |= ~ _vbi_hamm24_inv_par[0][c] 77 | // << (sizeof (int) * CHAR_BIT - 1 - 5); 78 | // 79 | //*p++ = c & 127; 80 | //} 81 | // 82 | // return r; 83 | //} 84 | 85 | /** 86 | * @ingroup Error 87 | * @param p A Hamming 24/18 protected 24 bit word will be stored here, 88 | * last significant byte first, lsb first transmitted. 89 | * @param c Integer between 0 ... 1 << 18 - 1. 90 | * 91 | * Encodes an 18 bit word with Hamming 24/18 protection 92 | * as specified in ETS 300 706, Section 8.3. 93 | * 94 | * @since 0.2.27 95 | */ 96 | 97 | /* @TODO 98 | void 99 | vbi_ham24p (uint8_t * p, 100 | unsigned int c) 101 | { 102 | unsigned int D5_D11; 103 | unsigned int D12_D18; 104 | unsigned int P5, P6; 105 | unsigned int Byte_0; 106 | 107 | Byte_0 = (_vbi_hamm24_fwd_0 [(c >> 0) & 0xFF] 108 | ^ _vbi_hamm24_fwd_1 [(c >> 8) & 0xFF] 109 | ^ _vbi_hamm24_fwd_2 [(c >> 16) & 0x03]); 110 | p[0] = Byte_0; 111 | 112 | D5_D11 = (c >> 4) & 0x7F; 113 | D12_D18 = (c >> 11) & 0x7F; 114 | 115 | P5 = 0x80 & ~(_vbi_hamm24_inv_par[0][D12_D18] << 2); 116 | p[1] = D5_D11 | P5; 117 | 118 | P6 = 0x80 & ((_vbi_hamm24_inv_par[0][Byte_0] 119 | ^ _vbi_hamm24_inv_par[0][D5_D11]) << 2); 120 | p[2] = D12_D18 | P6; 121 | } 122 | */ 123 | 124 | /** 125 | * @ingroup Error 126 | * @param p Array of Hamming 24/18 protected 24 bit words, 127 | * least significant byte first, lsb first transmitted. 128 | * @param offset Index of the first byte of the triplet 129 | * Decodes a Hamming 24/18 protected byte triplet 130 | * as specified in ETS 300 706, Section 8.3. 131 | * 132 | * @return 133 | * Triplet data bits D18 [msb] ... D1 [lsb] or a negative value 134 | * if the triplet contained uncorrectable errors. 135 | * 136 | * @since 0.2.12 137 | */ 138 | vbi_unham24p = function(p, offset) { 139 | let D1_D4; 140 | let D5_D11; 141 | let D12_D18; 142 | let ABCDEF; 143 | let d; 144 | 145 | // let t0 = hamm._vbi_bit_reverse[p[offset].charCodeAt()] // !!Convert to int 146 | // let t1 = hamm._vbi_bit_reverse[p[offset+1].charCodeAt()] 147 | // let t2 = hamm._vbi_bit_reverse[p[offset+2].charCodeAt()] 148 | let t0 = p[offset].charCodeAt() & 0x7f// !!Convert to int 149 | let t1 = p[offset+1].charCodeAt() & 0x7f 150 | let t2 = p[offset+2].charCodeAt() & 0x7f 151 | // console.log("triplet = " + t0.toString(16) + " " + t1.toString(16) + " " + t2.toString(16)) 152 | 153 | // D1_D4 = ((t0 >> 2) & 0x01) | t0 >> 3 // Do this because tti parity bit has a 50/50 chance of being valid 154 | D1_D4 = hamm._vbi_hamm24_inv_d1_d4[t0 >> 2]// Do this because tti parity bit has a 50/50 chance of being valid 155 | D5_D11 = t1 & 0x7F; 156 | D12_D18 = t2 & 0x7F; 157 | 158 | d = D1_D4 | (D5_D11 << 4) | (D12_D18 << 11); 159 | 160 | ABCDEF = (hamm._vbi_hamm24_inv_par[0][t0] 161 | ^ hamm._vbi_hamm24_inv_par[1][t1] 162 | ^ hamm._vbi_hamm24_inv_par[2][t2]); 163 | 164 | /* Correct single bit error, set MSB on double bit error. */ 165 | return d //^ hamm._vbi_hamm24_inv_err[ABCDEF]; 166 | } 167 | -------------------------------------------------------------------------------- /hammgen/hammgen.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libzvbi -- Error correction tables generator 3 | * 4 | * Copyright (C) 2007 Michael H. Schimek 5 | * 6 | * This program is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 19 | * MA 02110-1301, USA. 20 | */ 21 | "use strict"; 22 | /* $Id: hammgen.c,v 1.2 2008-07-26 06:22:00 mschimek Exp $ */ 23 | 24 | /* Converted to javascript. Copyright (c) 2024 Peter Kwan 25 | * Support for Muttlee to create Hamming tables: 26 | * eg. 27 | * node hammgen.js 28 | */ 29 | 30 | //#ifdef HAVE_CONFIG_H 31 | //# include "config.h" 32 | //#endif 33 | 34 | //#include 35 | //#include 36 | //#include 37 | //#include 38 | 39 | const _vbi_bit_reverse = []; // 256 40 | const _vbi_hamm8_fwd = new Array(16); 41 | const _vbi_hamm8_inv = new Array(256); 42 | const _vbi_hamm24_fwd_0 = new Array(256); 43 | const _vbi_hamm24_fwd_1 = new Array(256); 44 | const _vbi_hamm24_fwd_2 = new Array(4); 45 | 46 | let _vbi_hamm24_inv_par = new Array(new Array()); // [3][256]; 47 | for (let x = 0; x < 3; x++) { 48 | _vbi_hamm24_inv_par.push([]) 49 | for (let y = 0; y < 256; y++) { 50 | _vbi_hamm24_inv_par[x].push(-2) 51 | } 52 | } 53 | 54 | let _vbi_hamm24_inv_d1_d4 = []; // 64 55 | let _vbi_hamm24_inv_err = []; // 64 56 | 57 | function generate_hamm24_inv_tables () 58 | { 59 | /* EN 300 706 section 8.3, Hamming 24/18 inverse */ 60 | 61 | /* D1_D4 = _vbi_hamm24_inv_d1_d4[Byte_0 >> 2]; 62 | D5_D11 = Byte_1 & 0x7F; 63 | D12_D18 = Byte_2 & 0x7F; 64 | d = D1_D4 | (D5_D11 << 4) | (D12_D18 << 11); 65 | ABCDEF = ( _vbi_hamm24_inv_par[0][Byte_0] 66 | ^ _vbi_hamm24_inv_par[1][Byte_1] 67 | ^ _vbi_hamm24_inv_par[2][Byte_2]); 68 | // Correct single bit error, set bit 31 on double bit error. 69 | d ^= _vbi_hamm24_inv_err[ABCDEF]; 70 | 71 | This algorithm is based on an idea by R. Gancarz in 72 | AleVT 1.5.1. */ 73 | 74 | for (let i = 0; i < 256; ++i) { 75 | let D1, D2, D3, D4, D5, D6, D7, D8; 76 | let D9, D10, D11, D12, D13, D14, D15, D16; 77 | let D17, D18; 78 | let P1, P2, P3, P4, P5, P6; 79 | let A, B, C, D, E, F; 80 | let j; 81 | 82 | P1 = (i >> 0) & 1; 83 | P2 = (i >> 1) & 1; 84 | D1 = (i >> 2) & 1; 85 | P3 = (i >> 3) & 1; 86 | D2 = (i >> 4) & 1; 87 | D3 = (i >> 5) & 1; 88 | D4 = (i >> 6) & 1; 89 | P4 = (i >> 7) & 1; 90 | 91 | D5 = (i >> 0) & 1; 92 | D6 = (i >> 1) & 1; 93 | D7 = (i >> 2) & 1; 94 | D8 = (i >> 3) & 1; 95 | D9 = (i >> 4) & 1; 96 | D10 = (i >> 5) & 1; 97 | D11 = (i >> 6) & 1; 98 | P5 = (i >> 7) & 1; 99 | 100 | D12 = (i >> 0) & 1; 101 | D13 = (i >> 1) & 1; 102 | D14 = (i >> 2) & 1; 103 | D15 = (i >> 3) & 1; 104 | D16 = (i >> 4) & 1; 105 | D17 = (i >> 5) & 1; 106 | D18 = (i >> 6) & 1; 107 | P6 = (i >> 7) & 1; 108 | 109 | _vbi_hamm24_inv_d1_d4 [i >> 2] = (+ (D1 << 0) 110 | + (D2 << 1) 111 | + (D3 << 2) 112 | + (D4 << 3)); 113 | 114 | A = P1 ^ D1 ^ D2 ^ D4; 115 | B = P2 ^ D1 ^ D3 ^ D4; 116 | C = P3 ^ D2 ^ D3 ^ D4; 117 | D = P4; 118 | E = 0; 119 | F = P1 ^ P2 ^ D1 ^ P3 ^ D2 ^ D3 ^ D4 ^ P4; 120 | 121 | _vbi_hamm24_inv_par [0][i] = (+ (A << 0) 122 | + (B << 1) 123 | + (C << 2) 124 | + (D << 3) 125 | + (E << 4) 126 | + (F << 5)); 127 | 128 | A = D5 ^ D7 ^ D9 ^ D11; 129 | B = D6 ^ D7 ^ D10 ^ D11; 130 | C = D8 ^ D9 ^ D10 ^ D11; 131 | D = D5 ^ D6 ^ D7 ^ D8 ^ D9 ^ D10 ^ D11; 132 | E = P5; 133 | F = D5 ^ D6 ^ D7 ^ D8 ^ D9 ^ D10 ^ D11 ^ P5; 134 | 135 | _vbi_hamm24_inv_par [1][i] = (+ (A << 0) 136 | + (B << 1) 137 | + (C << 2) 138 | + (D << 3) 139 | + (E << 4) 140 | + (F << 5)); 141 | 142 | A = D12 ^ D14 ^ D16 ^ D18; 143 | B = D13 ^ D14 ^ D17 ^ D18; 144 | C = D15 ^ D16 ^ D17 ^ D18; 145 | D = 0; 146 | E = D12 ^ D13 ^ D14 ^ D15 ^ D16 ^ D17 ^ D18; 147 | F = D12 ^ D13 ^ D14 ^ D15 ^ D16 ^ D17 ^ D18 ^ P6; 148 | 149 | _vbi_hamm24_inv_par [2][i] = (+ (A << 0) 150 | + (B << 1) 151 | + (C << 2) 152 | + (D << 3) 153 | + (E << 4) 154 | + (F << 5)); 155 | 156 | /* For compatibility with earlier versions of libzvbi. */ 157 | _vbi_hamm24_inv_par [2][i] ^= 0x3F; 158 | } 159 | 160 | for (let i = 0; i < 64; ++i) { 161 | let ii; 162 | 163 | /* Undo the ^ 0x3F in _vbi_hamm24_inv_par[2][]. */ 164 | ii = i ^ 0x3F; 165 | 166 | if (0 == ii) { 167 | /* No errors. */ 168 | _vbi_hamm24_inv_err[ii] = 0; 169 | } else if (0 == (ii & 0x1F) && 0x20 == (ii & 0x20)) { 170 | /* Ignore error in P6. */ 171 | _vbi_hamm24_inv_err[ii] = 0; 172 | } else if (0x20 == (i & 0x20)) { 173 | /* Double bit error. */ 174 | _vbi_hamm24_inv_err[ii] = 1 << 31; 175 | } else { 176 | let Byte_0_3 = 1 << ((ii & 0x1F) - 1); 177 | 178 | /* Single bit error. */ 179 | 180 | if (Byte_0_3 >= (1 << 23)) { 181 | /* Invalid. (Error in P6 or outside the 182 | 24 bit word.) */ 183 | _vbi_hamm24_inv_err[ii] = 1 << 31; 184 | continue; 185 | } 186 | 187 | _vbi_hamm24_inv_err[ii] = 188 | (+ ((Byte_0_3 & 0x000004) >> (3 - 1)) 189 | + ((Byte_0_3 & 0x000070) >> (5 - 2)) 190 | + ((Byte_0_3 & 0x007F00) >> (9 - 5)) 191 | + ((Byte_0_3 & 0x7F0000) >> (17 - 12))); 192 | } 193 | } 194 | } 195 | 196 | function generate_hamm24_fwd_tables () 197 | { 198 | /* EN 300 706 section 8.3, Hamming 24/18 forward */ 199 | 200 | /* Byte_0 = ( _vbi_hamm24_fwd_0 [(d >> 0) & 0xFF] 201 | ^ _vbi_hamm24_fwd_1 [(d >> 8) & 0xFF] 202 | ^ _vbi_hamm24_fwd_2 [d >> 16]; 203 | D5_D11 = (d >> 4) & 0x7F; 204 | D12_D18 = (d >> 11) & 0x7F; 205 | P5 = 1 ^ parity (D12_D18); 206 | Byte_1 = D5_D11 | (P5 << 7); 207 | P6 = 1 ^ parity (Byte_0) ^ parity (Byte_1) ^ parity (D12_D18); 208 | P6 = 1 ^ parity (Byte_0) ^ parity (D5_D11) ^ P5 ^ parity (D12_D18); 209 | P6 = parity (Byte_0) ^ parity (D5_D11); 210 | Byte_2 = D12_D18 | (P6 << 7); */ 211 | 212 | for (let i = 0; i < 256; ++i) { 213 | let D1, D2, D3, D4, D5, D6, D7, D8; 214 | let P1, P2, P3, P4; 215 | 216 | D1 = (i >> 0) & 1; 217 | D2 = (i >> 1) & 1; 218 | D3 = (i >> 2) & 1; 219 | D4 = (i >> 3) & 1; 220 | D5 = (i >> 4) & 1; 221 | D6 = (i >> 5) & 1; 222 | D7 = (i >> 6) & 1; 223 | D8 = (i >> 7) & 1; 224 | 225 | P1 = 1 ^ D1 ^ D2 ^ D4 ^ D5 ^ D7; 226 | P2 = 1 ^ D1 ^ D3 ^ D4 ^ D6 ^ D7; 227 | P3 = 1 ^ D2 ^ D3 ^ D4 ^ D8; 228 | P4 = 1 ^ D5 ^ D6 ^ D7 ^ D8; 229 | 230 | _vbi_hamm24_fwd_0[i] = (+ (P1 << 0) 231 | + (P2 << 1) 232 | + (D1 << 2) 233 | + (P3 << 3) 234 | + (D2 << 4) 235 | + (D3 << 5) 236 | + (D4 << 6) 237 | + (P4 << 7)); 238 | } 239 | 240 | for (let i = 0; i < 256; ++i) { 241 | let D9, D10, D11, D12, D13, D14, D15, D16; 242 | let P1, P2, P3, P4; 243 | 244 | D9 = (i >> 0) & 1; 245 | D10 = (i >> 1) & 1; 246 | D11 = (i >> 2) & 1; 247 | D12 = (i >> 3) & 1; 248 | D13 = (i >> 4) & 1; 249 | D14 = (i >> 5) & 1; 250 | D15 = (i >> 6) & 1; 251 | D16 = (i >> 7) & 1; 252 | 253 | P1 = D9 ^ D11 ^ D12 ^ D14 ^ D16; 254 | P2 = D10 ^ D11 ^ D13 ^ D14; 255 | P3 = D9 ^ D10 ^ D11 ^ D15 ^ D16; 256 | P4 = D9 ^ D10 ^ D11; 257 | 258 | _vbi_hamm24_fwd_1[i] = (+ (P1 << 0) 259 | + (P2 << 1) 260 | + (P3 << 3) 261 | + (P4 << 7)); 262 | } 263 | 264 | for (let i = 0; i < 4; ++i) { 265 | let D17, D18; 266 | let P1, P2, P3, P4; 267 | 268 | D17 = (i >> 0) & 1; 269 | D18 = (i >> 1) & 1; 270 | 271 | P1 = D18; 272 | P2 = D17 ^ D18; 273 | P3 = D17 ^ D18; 274 | P4 = 0; 275 | 276 | _vbi_hamm24_fwd_2[i] = (+ (P1 << 0) 277 | + (P2 << 1) 278 | + (P3 << 3) 279 | + (P4 << 7)); 280 | } 281 | } 282 | 283 | function generate_hamm8_tables () 284 | { 285 | /* EN 300 706 section 8.2, Hamming 8/4 */ 286 | 287 | /* Uncorrectable double bit errors. */ 288 | // memset (_vbi_hamm8_inv, -1, sizeof (_vbi_hamm8_inv)); 289 | for (let i = 0; i < 256; i++) { 290 | _vbi_hamm8_inv[i] = -1 291 | } 292 | 293 | for (let i = 0; i < 16; ++i) { 294 | let D1, D2, D3, D4; 295 | let P1, P2, P3, P4; 296 | let j; 297 | let c; 298 | 299 | D1 = (i >> 0) & 1; 300 | D2 = (i >> 1) & 1; 301 | D3 = (i >> 2) & 1; 302 | D4 = (i >> 3) & 1; 303 | 304 | P1 = 1 ^ D1 ^ D3 ^ D4; 305 | P2 = 1 ^ D1 ^ D2 ^ D4; 306 | P3 = 1 ^ D1 ^ D2 ^ D3; 307 | P4 = 1 ^ P1 ^ D1 ^ P2 ^ D2 ^ P3 ^ D3 ^ D4; 308 | 309 | c = (+ (P1 << 0) 310 | + (D1 << 1) 311 | + (P2 << 2) 312 | + (D2 << 3) 313 | + (P3 << 4) 314 | + (D3 << 5) 315 | + (P4 << 6) 316 | + (D4 << 7)); 317 | 318 | _vbi_hamm8_fwd[i] = c; 319 | _vbi_hamm8_inv[c] = i; 320 | 321 | for (j = 0; j < 8; ++j) { 322 | /* Single bit errors. */ 323 | _vbi_hamm8_inv[c ^ (1 << j)] = i; 324 | } 325 | } 326 | } 327 | 328 | function generate_tables () 329 | { 330 | for (let i = 0; i < 256; ++i) { 331 | _vbi_bit_reverse[i] = (+ ((i & 0x80) >> 7) 332 | + ((i & 0x40) >> 5) 333 | + ((i & 0x20) >> 3) 334 | + ((i & 0x10) >> 1) 335 | + ((i & 0x08) << 1) 336 | + ((i & 0x04) << 3) 337 | + ((i & 0x02) << 5) 338 | + ((i & 0x01) << 7)); 339 | } 340 | 341 | generate_hamm8_tables (); 342 | 343 | generate_hamm24_fwd_tables (); 344 | 345 | generate_hamm24_inv_tables (); 346 | } 347 | 348 | /* Can't convert this code easily 349 | function print_tables () 350 | { 351 | console.log ("\ 352 | /* Generated file, do not edit! * /\n\n\ 353 | /* node hammgen.js > hamm_table.js * /\n\n\ 354 | /* This library is free software; you can redistribute it and/or\n\ 355 | modify it under the terms of the GNU Library General Public\n\ 356 | License as published by the Free Software Foundation; either\n\ 357 | version 2 of the License, or (at your option) any later version.\n\ 358 | \n\ 359 | This library is distributed in the hope that it will be useful,\n\ 360 | but WITHOUT ANY WARRANTY; without even the implied warranty of\n\ 361 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n\ 362 | Library General Public License for more details.\n\ 363 | \n\ 364 | You should have received a copy of the GNU Library General Public\n\ 365 | License along with this library; if not, write to the\n\ 366 | Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n\ 367 | Boston, MA 02110-1301 USA. * /\n\n"); 368 | 369 | 370 | #define PRINT(type, array) \ 371 | do { \ 372 | const let n_elements = sizeof (array); \ 373 | \ 374 | printf ("%s\n%s [%u] = {", \ 375 | #type, #array, n_elements); \ 376 | for (i = 0; i < n_elements; ++i) { \ 377 | printf ("%s0x%02x%s", \ 378 | 0 == (i % 8) ? "\n\t" : "", \ 379 | array[i] & 0xFF, \ 380 | i < (n_elements - 1) ? ", " : ""); \ 381 | } \ 382 | printf ("\n};\n\n"); \ 383 | } while (0) 384 | 385 | PRINT (const uint8_t, _vbi_bit_reverse); 386 | PRINT (const uint8_t, _vbi_hamm8_fwd); 387 | PRINT (const int8_t, _vbi_hamm8_inv); 388 | PRINT (static const uint8_t, _vbi_hamm24_fwd_0); 389 | PRINT (static const uint8_t, _vbi_hamm24_fwd_1); 390 | PRINT (static const uint8_t, _vbi_hamm24_fwd_2); 391 | 392 | printf ("const int8_t\n_vbi_hamm24_inv_par [3][256] " 393 | "= {\n\t{"); 394 | 395 | for (i = 0; i < 3; ++i) { 396 | let j; 397 | 398 | for (j = 0; j < 256; ++j) { 399 | printf ("%s0x%02x%s", 400 | 0 == (j % 8) ? "\n\t\t" : "", 401 | _vbi_hamm24_inv_par[i][j], 402 | j < 255 ? ", " : ""); 403 | } 404 | printf ("\n\t}%s", 405 | i < 2 ? ", {" : "\n};\n\n"); 406 | } 407 | 408 | PRINT (static const uint8_t, _vbi_hamm24_inv_d1_d4); 409 | 410 | printf ("static const int32_t\n_vbi_hamm24_inv_err [64] = {"); 411 | 412 | for (let i = 0; i < 64; ++i) { 413 | printf ("%s0x%08x%s", 414 | 0 == (i % 4) ? "\n\t" : "", 415 | _vbi_hamm24_inv_err[i], 416 | i < 63 ? ", " : ""); 417 | } 418 | 419 | printf ("\n};\n"); 420 | } 421 | */ 422 | 423 | function print_array (data, size) { 424 | let line = ' ' 425 | for (let ix = 0; ix < size; ix++) { 426 | let hex = ('0000' + data[ix].toString(16)).slice(-2); // Adjust slice value for hex value size 427 | if (hex === "-1") { 428 | hex = "ff" 429 | } 430 | line = line + '0x' + hex; 431 | 432 | // Add a comma unless it is the very last value 433 | if (ix === size-1) { 434 | console.log (line) // Last line 435 | } else { 436 | if (((ix+1) % 8) != 0) { 437 | line = line + ', ' // More numbers to come 438 | } else { 439 | console.log (line + ',') // This line is done 440 | line = ' ' // Ready the next line 441 | } 442 | } 443 | } 444 | } 445 | 446 | function print_array2 (data, size) { 447 | let line = ' ' 448 | for (let ix = 0; ix < size; ix++) { 449 | let hex = ('00000000' + data[ix].toString(16)).slice(-8); // Adjust slice value for hex value size 450 | if (hex === "-1") { 451 | hex = "ff" 452 | } 453 | line = line + '0x' + hex; 454 | 455 | // Add a comma unless it is the very last value 456 | if (ix === size-1) { 457 | console.log (line) // Last line 458 | } else { 459 | if (((ix+1) % 4) != 0) { 460 | line = line + ', ' // More numbers to come 461 | } else { 462 | console.log (line + ',') // This line is done 463 | line = ' ' // Ready the next line 464 | } 465 | } 466 | } 467 | } 468 | 469 | function print_tables () { 470 | console.log ("\ 471 | /* Generated file, do not edit! */\n\n\ 472 | /* node hammgen.js > hamm_table.js * /\n\n\ 473 | /* This library is free software; you can redistribute it and/or\n\ 474 | modify it under the terms of the GNU Library General Public\n\ 475 | License as published by the Free Software Foundation; either\n\ 476 | version 2 of the License, or (at your option) any later version.\n\ 477 | \n\ 478 | This library is distributed in the hope that it will be useful,\n\ 479 | but WITHOUT ANY WARRANTY; without even the implied warranty of\n\ 480 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n\ 481 | Library General Public License for more details.\n\ 482 | \n\ 483 | You should have received a copy of the GNU Library General Public\n\ 484 | License along with this library; if not, write to the\n\ 485 | Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n\ 486 | Boston, MA 02110-1301 USA. */\n\n"); 487 | 488 | console.log("const _vbi_bit_reverse = [") 489 | print_array(_vbi_bit_reverse, 256) 490 | console.log("];\n") 491 | 492 | console.log("const _vbi_hamm8_fwd = [") 493 | print_array(_vbi_hamm8_fwd, 16) 494 | console.log("];\n") 495 | 496 | console.log("const _vbi_hamm8_inv = [") 497 | print_array(_vbi_hamm8_inv, 256) 498 | console.log("];\n") 499 | 500 | console.log("const _vbi_hamm24_fwd_0 = [") 501 | print_array(_vbi_hamm24_fwd_0, 256) 502 | console.log("];\n") 503 | 504 | console.log("const _vbi_hamm24_fwd_1 = [") 505 | print_array(_vbi_hamm24_fwd_1, 256) 506 | console.log("];\n") 507 | 508 | console.log("const _vbi_hamm24_fwd_2 = [") 509 | print_array(_vbi_hamm24_fwd_2, 4) 510 | console.log("];\n") 511 | 512 | console.log("const _vbi_hamm24_inv_par = [[") 513 | print_array(_vbi_hamm24_inv_par[0], 256) 514 | console.log("],[") 515 | print_array(_vbi_hamm24_inv_par[1], 256) 516 | console.log("],[") 517 | print_array(_vbi_hamm24_inv_par[2], 256) 518 | console.log("]];\n") 519 | 520 | console.log("const _vbi_hamm24_inv_d1_d4 = [") 521 | print_array(_vbi_hamm24_inv_d1_d4, 64) 522 | console.log("];\n") 523 | 524 | console.log("const _vbi_hamm24_inv_err = [") 525 | print_array2(_vbi_hamm24_inv_err, 64) 526 | console.log("];\n") 527 | 528 | console.log("module.exports = {") 529 | console.log(" _vbi_bit_reverse,") 530 | console.log(" _vbi_hamm8_inv,") 531 | console.log(" _vbi_hamm24_fwd_0,") 532 | console.log(" _vbi_hamm24_fwd_1,") 533 | console.log(" _vbi_hamm24_fwd_2,") 534 | console.log(" _vbi_hamm24_inv_par,") 535 | console.log(" _vbi_hamm24_inv_d1_d4,") 536 | console.log(" _vbi_hamm24_inv_err") 537 | console.log("};") 538 | 539 | /* PRINT (const uint8_t, _vbi_hamm8_fwd); 540 | PRINT (const int8_t, _vbi_hamm8_inv); 541 | PRINT (static const uint8_t, _vbi_hamm24_fwd_0); 542 | PRINT (static const uint8_t, _vbi_hamm24_fwd_1); 543 | PRINT (static const uint8_t, _vbi_hamm24_fwd_2); 544 | */ 545 | } 546 | 547 | generate_tables (); 548 | print_tables (); -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% if LOGO_CHARS %} 3 | 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | {{ TITLE }} 12 | 13 | {% if LOGO_CHARS %} 14 | 19 | {% endif %} 20 | 21 | {% if IS_DEV %} 22 | 23 | 24 | {% else %} 25 | 26 | 27 | {% endif %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |

47 | {{ TITLE }} 48 |

49 | 50 | {% if LOGO_SVG %} 51 | 54 | {% endif %} 55 | 56 | 230 | 231 | 232 | 233 |
234 |
235 | 236 | 237 |
238 |
239 | 240 | 241 | 242 | 243 |
244 | 245 |
246 |
247 | 250 | 267 |
268 | 269 |
270 | 273 | 283 | 284 | 289 |
290 |
291 | 292 | 293 |
294 | 295 | 296 | 297 | 298 |
299 | 300 |
301 |
302 | 303 | 304 | {% if ZAPPER_STANDARD_SVG %} 305 |
306 | {{ ZAPPER_STANDARD_SVG | safe }} 307 |
308 | {% endif %} 309 | 310 | {% if ZAPPER_COMPACT_SVG %} 311 |
312 | {{ ZAPPER_COMPACT_SVG | safe }} 313 |
314 | {% endif %} 315 |
316 | 317 | 318 |
319 |
320 |
321 |

322 | 323 | Manifest 324 | 325 | 326 |

327 | 328 | 347 |
348 | 349 |
350 | [Loading...] 351 |
352 |
353 |
354 | 355 | 356 |
357 |
358 |
359 |

360 | Cheatsheet 361 | 362 | PDF 363 | 364 |

365 | 366 | 385 |
386 | 387 |
388 |
389 |

390 | Coloured text 391 |

392 | 393 | 394 | 395 | 398 | 401 | 402 | 403 | 404 | 407 | 410 | 411 | 412 | 413 | 416 | 419 | 420 | 421 | 422 | 425 | 428 | 429 | 430 | 431 | 434 | 437 | 438 | 439 | 440 | 443 | 446 | 447 | 448 | 449 | 452 | 455 | 456 | 457 | 458 | 461 | 464 | 465 | 466 |
396 | r 397 | 399 | red text 400 |
405 | g 406 | 408 | green text 409 |
414 | y 415 | 417 | yellow text 418 |
423 | b 424 | 426 | blue text 427 |
432 | m 433 | 435 | magenta text 436 |
441 | c 442 | 444 | cyan text 445 |
450 | w 451 | 453 | white text 454 |
459 | k 460 | 462 | black text 463 |
467 | 468 | 469 |

470 | Coloured graphics 471 |

472 | 473 | 474 | 475 | 478 | 481 | 482 | 483 | 484 | 487 | 490 | 491 | 492 | 493 | 496 | 499 | 500 | 501 | 502 | 505 | 508 | 509 | 510 | 511 | 514 | 517 | 518 | 519 | 520 | 523 | 526 | 527 | 528 | 529 | 532 | 535 | 536 | 537 | 538 | 541 | 544 | 545 | 546 |
476 | R 477 | 479 | red graphics 480 |
485 | G 486 | 488 | green graphics 489 |
494 | Y 495 | 497 | yellow graphics 498 |
503 | B 504 | 506 | blue graphics 507 |
512 | M 513 | 515 | magenta graphics 516 |
521 | C 522 | 524 | cyan graphics 525 |
530 | W 531 | 533 | white graphics 534 |
539 | K 540 | 542 | black graphics 543 |
547 |
548 | 549 | 550 |
551 |

552 | Subpages 553 |

554 | 555 | 556 | 557 | 560 | 563 | 564 | 565 | 566 | 569 | 572 | 573 | 574 | 575 | 578 | 581 | 582 | 583 | 584 | 587 | 590 | 591 | 592 |
558 | Page Up 559 | 561 | Next subpage 562 |
567 | Page Down 568 | 570 | Previous subpage 571 |
576 | Insert 577 | 579 | Add a subpage 580 |
585 | Delete 586 | 588 | Remove a subpage 589 |
593 | 594 | 595 |

596 | Effects 597 |

598 | 599 | 600 | 601 | 604 | 607 | 608 | 609 | 610 | 613 | 616 | 617 | 618 | 619 | 622 | 625 | 626 | 627 | 628 | 631 | 634 | 635 | 636 | 637 | 640 | 643 | 644 | 645 | 646 | 649 | 652 | 653 | 654 |
602 | f / F 603 | 605 | steady / flash 606 |
611 | h / H 612 | 614 | release / hold graphics 615 |
620 | d / D 621 | 623 | normal / double height 624 |
629 | O 630 | 632 | conceal 633 |
638 | S 639 | 641 | separated graphics 642 |
647 | s 648 | 650 | contiguous graphics 651 |
655 | 656 | 657 |

658 | Colour effects 659 |

660 | 661 | 662 | 663 | 666 | 669 | 670 | 671 | 672 | 675 | 678 | 679 | 680 |
664 | N 665 | 667 | new background 668 |
673 | n 674 | 676 | end / black background 677 |
681 |
682 | 683 | 684 |
685 |
686 |

687 | Layout 688 |

689 | 690 | 691 | 692 | 695 | 698 | 699 | 700 | 701 | 704 | 707 | 708 | 709 |
693 | x 694 | 696 | toggle grid 697 |
702 | Z 703 | 705 | clear page 706 |
710 | 711 | 712 |

713 | Navigation (no esc) 714 |

715 | 716 | 717 | 718 | 724 | 727 | 728 | 729 |
719 | Left, 720 | Right, 721 | Up, 722 | Down 723 | 725 | Move cursor 726 |
730 | 731 | 732 | 733 | 734 | 737 | 740 | 741 | 742 | 743 | 746 | 749 | 750 | 751 |
735 | Tab 736 | 738 | Insert a space 739 |
744 | Backspace 745 | 747 | Delete a space 748 |
752 |
753 | 754 | 755 |
756 |

757 | Sixel Graphics (no esc) 758 |

759 |

760 | In a graphics region, use these keys to toggle sixels. 761 |

762 | 763 | 764 | 765 | 768 | 771 | 772 | 773 | 774 | 777 | 780 | 781 | 782 | 783 | 786 | 789 | 790 | 791 | 792 | 793 | 794 | 797 | 800 | 801 | 802 | 803 | 806 | 809 | 810 | 811 | 812 | 815 | 818 | 819 | 820 |
766 | Q 767 | 769 | W 770 |
775 | A 776 | 778 | S 779 |
784 | Z 785 | 787 | X 788 |
795 | R 796 | 798 | Reverse all bits 799 |
804 | F 805 | 807 | Fill all bits 808 |
813 | C 814 | 816 | Clear all bits 817 |
821 |
822 |
823 |
824 |
825 |
826 | 827 | 828 |
829 |
830 |
831 |

832 | About {{ TITLE }} 833 |

834 | 835 | 854 |
855 | 856 |

857 | Type in a number to get to a page or try a Fastext button. 858 |

859 | 860 |

861 | Pages 100 to 131 are automatically generated from BBC News. 862 |

863 | 864 |

865 | Page 400 is live data from a Cumulus weather station. 866 |

867 | 868 |

869 | All other pages are archived from BBC ONE on 20/11/2007. 870 |

871 | 872 |

873 | Project source is on GitHub at Muttlee Project 874 |

875 |
876 |
877 | 878 | 879 | -------------------------------------------------------------------------------- /keystroke.js: -------------------------------------------------------------------------------- 1 | /** keystroke.js 2 | * Part of the Muttlee system 3 | * Copyright Peter Kwan (c) 2018 4 | * MIT license blah blah. 5 | * Records keystrokes received from connected clients 6 | * Used to keep a record of edits to pages 7 | * 8 | * addEvent(data) - Adds a key event to the list 9 | * replayEvents() - Replays the events to newly connected clients 10 | * saveEvents() - Saves the events to file 11 | * matchPage(event) - Returns a matching event if there is one in the list. (page, subpage, service) 12 | * 13 | & @todo Move all the file stuff out of here! 14 | */ 15 | /* global Page, fs */ // Keeps this happy: $ standard keystroke.js --fix 16 | 'use strict' 17 | const path = require('path') 18 | 19 | // import constants and config for use server-side 20 | const CONST = require('./constants.js') 21 | const CONFIG = require('./config.js') 22 | 23 | require('./page.js') 24 | 25 | // import logger 26 | const LOG = require('./log.js') 27 | 28 | const page = new Page() 29 | 30 | global.KeyStroke = function () { 31 | const that = this // Make the parent available in the nested functions. Probably 100 reasons why you shouldn't do this. 32 | this.sourceFile = '' 33 | this.destFile = '' 34 | 35 | this.event = undefined // Used to talk to inner function 36 | this.outfile = undefined 37 | 38 | this.eventList = [] 39 | 40 | // Not sophisticated. Just set all the characters to space 41 | this.clearPage = function (data) { 42 | LOG.fn( 43 | ['keystroke', 'clearPage'], 44 | `Clearing page data.S=${data.S}, data.p=${data.p}, data.s=${data.s}`, 45 | LOG.LOG_LEVEL_INFO 46 | ) 47 | 48 | data.k = ' ' 49 | 50 | for (let row = 0; row < 24; row++) { 51 | data.y = row 52 | 53 | for (let col = 0; col < 40; col++) { 54 | const d = { 55 | S: data.S, // service number 56 | p: data.p, 57 | s: data.s, 58 | k: data.k, 59 | x: col, 60 | y: row, 61 | id: data.id 62 | } 63 | 64 | this.addEvent(d) 65 | } 66 | } 67 | } 68 | 69 | /** Add a keystroke event to the list */ 70 | this.addEvent = function (data) { 71 | // Unfortunately, we need to check that we don't already have a character at that location 72 | // @todo Search through the list and if the character location matches 73 | // then replace the key entry for that location 74 | // otherwise push the event 75 | 76 | let overwrite = false 77 | 78 | // Packet X28 skips this check. 79 | if (data.y === 28) { 80 | // Don't check for an existing x28 81 | } 82 | else 83 | { 84 | for (let i = 0; i < this.eventList.length; i++) { 85 | if (this.sameChar(data, this.eventList[i])) { 86 | this.eventList[i].k = data.k // replace the key as this overwrites the original character 87 | overwrite = true 88 | 89 | LOG.fn( 90 | ['keystroke', 'addEvent'], 91 | 'Overwriting character', 92 | LOG.LOG_LEVEL_VERBOSE 93 | ) 94 | 95 | break 96 | } 97 | } 98 | } 99 | if (!overwrite) { 100 | this.eventList.push(data) 101 | } 102 | 103 | LOG.fn( 104 | ['keystroke', 'addEvent'], 105 | `queue length=${this.eventList.length}`, 106 | LOG.LOG_LEVEL_INFO 107 | ) 108 | } 109 | 110 | /** b.S) return 1 201 | // page sort 202 | if (a.p < b.p) return -1 203 | if (a.p > b.p) return 1 204 | // subpage sort 205 | if (a.s < b.s) return -1 206 | if (a.s > b.s) return 1 207 | // row sort 208 | if (a.y < b.y) return -1 209 | if (a.y > b.y) return 1 210 | 211 | return 0 // same 212 | } 213 | ) 214 | 215 | // Now that we are sorted we can apply the edits 216 | // However, due to the async nature, we only do one file at a time 217 | if (this.eventList.length > 0) { 218 | let event = this.eventList[0] 219 | 220 | // Get the filename 221 | let service = event.S 222 | if (!service) { 223 | service = CONFIG[CONST.CONFIG.DEFAULT_SERVICE] 224 | } 225 | 226 | const pageNumber = event.p 227 | 228 | const filename = path.join( 229 | CONFIG[CONST.CONFIG.SERVICE_PAGES_SERVE_DIR], 230 | service, 231 | `p${pageNumber.toString(16)}${CONST.PAGE_EXT_TTI}` // The filename of the original page 232 | ) 233 | 234 | const that = this 235 | 236 | page.loadPage( 237 | filename, 238 | function () { 239 | LOG.fn( 240 | ['keystroke', 'saveEdits'], 241 | 'Loaded. Now edit...', 242 | LOG.LOG_LEVEL_VERBOSE 243 | ) 244 | 245 | // apply all the edits that refer to this page 246 | for (; ((that.eventList.length > 0) && (pageNumber === event.p) && (service === event.S)); event = that.eventList[0]) { 247 | 248 | /* 249 | // Send the message to the page. X28 is a whole row, so send it differently 250 | if (event.y === 28) { 251 | console.log("[KeyStroke:saveEdits] Packet 28 not implemented") 252 | // TODO: Probably page.keyMessage(event) is the right way to go as this is the routine that parses the page file 253 | 254 | } 255 | else { 256 | 257 | page.keyMessage(event) 258 | } 259 | */ 260 | page.keyMessage(event) 261 | that.eventList.shift() // Remove the event we just processed 262 | } 263 | page.validatePage() // Check for badly formed teletext page 264 | page.print() 265 | 266 | // At this point we trigger off a timer 267 | setTimeout(this.savePage, 500) 268 | }, 269 | function (err) { 270 | LOG.fn( 271 | ['keystroke', 'saveEdits'], 272 | `Edit failed: ${err}`, 273 | LOG.LOG_LEVEL_ERROR 274 | ) 275 | } 276 | ) 277 | } 278 | return true // we have done edits 279 | } 280 | 281 | this.copyback = function () { 282 | copyFile( 283 | that.sourceFile, 284 | that.destFile, 285 | function (err) { 286 | if (err !== undefined) { 287 | LOG.fn( 288 | ['keystroke', 'copyback'], 289 | `Failed: ${err}`, 290 | LOG.LOG_LEVEL_ERROR 291 | ) 292 | } 293 | } 294 | ) 295 | } 296 | 297 | /** Dump the summary of the contents of the key events list */ 298 | this.dump = function () { 299 | console.log('Dump ' + this.eventList.length + ' items') 300 | 301 | for (let i = 0; i < this.eventList.length; i++) { 302 | console.log( 303 | 'p:' + this.eventList[i].p.toString(16) + 304 | ' s:' + this.eventList[i].s + 305 | ' k:' + this.eventList[i].k + 306 | ' x:' + this.eventList[i].x + 307 | ' y:' + this.eventList[i].y 308 | ) 309 | } 310 | } 311 | } 312 | 313 | /** Utility */ 314 | function setCharAt (str, index, chr) { 315 | if (index > str.length - 1) return str 316 | return str.substr(0, index) + chr + str.substr(index + 1) 317 | } 318 | 319 | /** copyFile - Make a copy of a file 320 | * @param source - Source file 321 | * @param target - Destination file 322 | * @param cb - Callback when completed, with an error message 323 | */ 324 | function copyFile (source, target, cb) { 325 | LOG.fn( 326 | ['keystroke', 'copyFile'], 327 | `Copying ${source} to ${target}`, 328 | LOG.LOG_LEVEL_INFO 329 | ) 330 | 331 | let cbCalled = false 332 | 333 | let rd = fs.createReadStream(source) 334 | rd.on('error', function (err) { 335 | done(err) 336 | }) 337 | 338 | const wr = fs.createWriteStream(target) 339 | wr.on('error', function (err) { 340 | done(err) 341 | }) 342 | 343 | wr.on('end', function (ex) { 344 | LOG.fn( 345 | ['keystroke', 'copyFile'], 346 | 'Closing files... (end)', 347 | LOG.LOG_LEVEL_INFO 348 | ) 349 | 350 | done() 351 | }) 352 | 353 | wr.on('close', function (ex) { 354 | LOG.fn( 355 | ['keystroke', 'copyFile'], 356 | 'Closing files... (close)', 357 | LOG.LOG_LEVEL_INFO 358 | ) 359 | 360 | done() 361 | }) 362 | 363 | rd.pipe(wr) 364 | 365 | function done (err) { 366 | if (!cbCalled) { 367 | cb(err) 368 | cbCalled = true 369 | } 370 | 371 | rd.close() 372 | rd = null 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /log.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const colorette = require('colorette'); 3 | 4 | const CONST = require('./constants.js'); 5 | const CONFIG = require('./config.js'); 6 | 7 | 8 | const SEVERITY_COLOUR_MAP = { 9 | [CONST.LOG_LEVEL_VERBOSE]: 'greenBright', 10 | [CONST.LOG_LEVEL_INFO]: 'blueBright', 11 | [CONST.LOG_LEVEL_MANDATORY]: 'yellowBright', 12 | [CONST.LOG_LEVEL_ERROR]: 'redBright', 13 | }; 14 | 15 | 16 | const LOG = { 17 | blank: function () { 18 | console.log(''); 19 | }, 20 | 21 | fn: function (context, message, severity = CONST.LOG_LEVEL_VERBOSE, blankLineAfter = false) { 22 | // only output log message if severity is within current log output level config value 23 | if (severity <= CONFIG[CONST.CONFIG.LOG_LEVEL_TELETEXT_SERVER]) { 24 | let contextString = ''; 25 | if (context) { 26 | contextString = Array.isArray(context) ? 27 | context.join('::') : 28 | context; 29 | 30 | contextString = `[${contextString}] `; 31 | } 32 | 33 | if (Array.isArray(message)) { 34 | for (let i in message) { 35 | console.log( 36 | colorette[SEVERITY_COLOUR_MAP[severity]]( 37 | contextString + message[i] 38 | ) 39 | ); 40 | } 41 | 42 | } else { 43 | console.log( 44 | colorette[SEVERITY_COLOUR_MAP[severity]]( 45 | contextString + message 46 | ) 47 | ); 48 | } 49 | 50 | if (blankLineAfter) { 51 | console.log(''); 52 | } 53 | } 54 | }, 55 | 56 | // re-export log levels for convenience 57 | LOG_LEVEL_VERBOSE: CONST.LOG_LEVEL_VERBOSE, 58 | LOG_LEVEL_INFO: CONST.LOG_LEVEL_INFO, 59 | LOG_LEVEL_MANDATORY: CONST.LOG_LEVEL_MANDATORY, 60 | LOG_LEVEL_ERROR: CONST.LOG_LEVEL_ERROR, 61 | LOG_LEVEL_NONE: CONST.LOG_LEVEL_NONE, 62 | }; 63 | 64 | 65 | if (typeof exports === 'object') { 66 | module.exports = LOG; 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "muttlee", 3 | "version": "1.2.6", 4 | "description": "Teletext live on the web", 5 | "main": "teletextserver.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/peterkvt80/Muttlee.git" 9 | }, 10 | "scripts": { 11 | "start": "node teletextserver.js", 12 | "monitor": "nodemon teletextserver.js", 13 | "css:build": "npm run css:compile && npm run css:prefix && npm run css:minify", 14 | "css:compile": "node-sass --output-style=expanded private/muttlee.scss -o public", 15 | "css:compile-watch": "node-sass --output-style=expanded private/muttlee.scss -wo public", 16 | "css:prefix": "postcss public/muttlee.css --use=autoprefixer --map=false --output=public/muttlee.css", 17 | "css:minify": "cleancss --with-rebase --source-map --source-map-inline-sources --output public/muttlee.min.css public/muttlee.css", 18 | "css:watch": "npm run css:compile && npm run css:compile-watch", 19 | "js:build": "npx terser public/libraries/p5.js -o public/libraries/p5.min.js -c -m && npx terser public/libraries/p5.dom.js -o public/libraries/p5.dom.min.js -c -m", 20 | "watch": "npm run css:watch", 21 | "build": "npm run js:build && npm run css:build" 22 | }, 23 | "browserslist": [ 24 | "> 1%", 25 | "last 4 versions", 26 | "not dead" 27 | ], 28 | "keywords": [ 29 | "teletext", 30 | "muttlee", 31 | "node" 32 | ], 33 | "author": "Peter Kwan", 34 | "license": "ISC", 35 | "dependencies": { 36 | "@pacote/xxhash": "^0.2.3", 37 | "@taiyosen/easy-svn": "^1.0.8", 38 | "colorette": "^2.0.8", 39 | "datastore": "^1.8.2", 40 | "deep-object-diff": "^1.1.0", 41 | "express": "^4.17.1", 42 | "nunjucks": "^3.2.3", 43 | "request": "^2.88.2", 44 | "simple-git": "^3.27.0", 45 | "socket.io": "^4.8.0", 46 | "socket.io-client": "^4.8.0" 47 | }, 48 | "devDependencies": { 49 | "autoprefixer": "latest", 50 | "browserslist": "latest", 51 | "clean-css-cli": "latest", 52 | "command-line-args": "^5.2.0", 53 | "command-line-usage": "^6.1.1", 54 | "postcss": "latest", 55 | "postcss-cli": "latest", 56 | "terser": "latest" 57 | }, 58 | "engines": { 59 | "node": ">=0.12.7" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/peterkvt80/Muttlee/issues" 63 | }, 64 | "homepage": "https://github.com/peterkvt80/Muttlee#readme" 65 | } 66 | -------------------------------------------------------------------------------- /page.js: -------------------------------------------------------------------------------- 1 | /** page.js 2 | * Encapsulate a teletext page object. 3 | * Used to load, edit and save a tti file (MRG format teletext file) 4 | */ 5 | /* global DeEscapePrestel, EscapePrestel */ 6 | 'use strict' 7 | // io stream stuff 8 | const fs = require('fs') 9 | const readline = require('readline') 10 | 11 | // import constants for use server-side 12 | const CONST = require('./constants.js') 13 | 14 | require('./utils.js') // Prestel and other string handling 15 | 16 | // import logger 17 | const LOG = require('./log.js') 18 | 19 | global.Page = function () { 20 | // basic properties 21 | this.pageNumber = 0x100 22 | this.subpageNumber = 0 23 | this.ttiLines = [] // Each line in a tti file 24 | this.ttiLines.push('DE,random comment 1') 25 | this.changed = false /// true if the page has been edited 26 | this.filename = '' 27 | this.pageTime = 8 /// seconds per carousel page 28 | 29 | const that = this // should use bind(this) instead! 30 | 31 | this.loadPage = function (filename, callback, error) { 32 | that.filename = filename 33 | this.filename = filename 34 | this.cb = callback 35 | this.err = error 36 | 37 | const that2 = this 38 | 39 | LOG.fn( 40 | ['page', 'loadPage'], 41 | `Loading filename=${filename}`, 42 | LOG.LOG_LEVEL_INFO 43 | ) 44 | 45 | that.ttiLines = [] // Clear the tti array 46 | 47 | const instream = fs.createReadStream( 48 | filename, 49 | { 50 | // ascii strips bit 7 without messing up the rest of the text. latin1 does not work :-( 51 | encoding: CONST.ENCODING_ASCII 52 | } 53 | ) 54 | 55 | instream.on('error', function (err) { 56 | LOG.fn( 57 | ['page', 'loadPage'], 58 | `Error: ${err}`, 59 | LOG.LOG_LEVEL_ERROR 60 | ) 61 | 62 | that2.err(err) 63 | }) 64 | 65 | const rl = readline.createInterface({ 66 | input: instream, 67 | terminal: false 68 | }) 69 | 70 | rl.on('line', function (line) { 71 | that.ttiLines.push(DeEscapePrestel(line)) 72 | }) 73 | 74 | rl.on('close', function (line) { 75 | that2.cb(that.ttiLines) // probably don't want to return this except for testing 76 | }) 77 | } 78 | 79 | // Editing 80 | // Handle keyMessage 81 | this.keyMessage = function (key) { 82 | let subcode = -1 // Signal an invalid code until we get a real one 83 | let insert = true 84 | let pageNumber = 0x100 // the mpp of mppss 85 | let pageSubCode = 0 // The ss of mppss 86 | let rowIndex = -1 // The index of the line OR where we want to splice. -1 = not found 87 | let fastext = '8ff,8ff,8ff,8ff,8ff,8ff' 88 | let noDescription = true // If we don't have a description, we need to add one 89 | 90 | LOG.fn( 91 | ['page', 'keyMessage'], 92 | `Entered, row=${key.y}`, 93 | LOG.LOG_LEVEL_VERBOSE 94 | ) 95 | 96 | // Scan for the subpage of the key 97 | for (let i = 0; i < this.ttiLines.length; i++) { 98 | let line = this.ttiLines[i] // get the next line 99 | const code = line.substring(0, 2) // get the two character command 100 | line = line.substring(3) // get the tail from the line 101 | 102 | if (code === 'FL') { // Fastext Link: Save the fastext link 103 | fastext = line 104 | if ((rowIndex === -1) && (key.subcode === pageSubCode)) { // did we get to the end of the page without finding any rows? 105 | rowIndex = i - 1 // Splice before the FL // [!] todo Not sure that i is correct. 106 | } 107 | } 108 | 109 | if (code === 'PN') { // Page Number: Get the MPP 110 | pageNumber = parseInt(line.substring(0, 3), 16) 111 | // Check the SS. Don't rely on the SC. 112 | pageSubCode = parseInt(line.substring(3, 5)) // Get SS 113 | LOG.fn( 114 | ['page', 'keyMessage'], 115 | `Parser in pageSubCode=${pageSubCode}`, 116 | LOG.LOG_LEVEL_VERBOSE 117 | ) 118 | } 119 | 120 | if (code === 'SC') { 121 | subcode++ // Don't use the file numbering, just increment 122 | 123 | LOG.fn( 124 | ['page', 'keyMessage'], 125 | `Parser in subcode=${subcode}`, 126 | LOG.LOG_LEVEL_VERBOSE 127 | ) 128 | } 129 | 130 | if (code === 'DE') { // Description 131 | LOG.fn( 132 | ['page', 'keyMessage'], 133 | `Parser in description=${line}`, 134 | LOG.LOG_LEVEL_VERBOSE 135 | ) 136 | noDescription = false 137 | // If new description comes in, it replaces this 138 | if (key.x === CONST.SIGNAL_DESCRIPTION_CHANGE) { 139 | this.ttiLines[i] = 'DE,' + key.k 140 | } 141 | } 142 | 143 | if (code === 'CT') { // Counter/timer 144 | // Ignore C/T, just assume the number is the seconds for all the subpages 145 | const tokens = line.split(',') 146 | this.pageTime = parseInt(tokens[0]) // Not actually used. 147 | } 148 | 149 | if (key.x === CONST.SIGNAL_DESCRIPTION_CHANGE) { 150 | console.log('[Page::Validate] Encountered a description: ' + key.k) 151 | } 152 | 153 | if (code === 'OL') { // Output Line 154 | // TODO: X28 row inserts need to be first, before row 1. 155 | let ix = 0 156 | let row = 0 157 | let ch = line.charAt(ix++) 158 | row = ch 159 | ch = line.charAt(ix) 160 | if (ch !== ',') { 161 | row = row + ch // haha. Javascript maths 162 | ix++ 163 | } 164 | 165 | row = parseInt(row) 166 | line = line.substring(++ix) 167 | 168 | // if (key.y === 28) { 169 | // console.log("[Page::keyMessage] X28 insert TODO") 170 | // } 171 | if (key.s === pageSubCode) { // if we are on the right page 172 | LOG.fn( 173 | ['page', 'keyMessage'], 174 | `Subcode matches, decoded row=${row}, line=${line}`, 175 | LOG.LOG_LEVEL_VERBOSE 176 | ) 177 | 178 | // [!] @TODO Doesn't this assume that rows are ordered? 179 | if (key.y >= row) { // Save the new index if it is ahead of here 180 | if (key.y === 28) { // We want the X28 to be first of the OL 181 | if (rowIndex === -1) { 182 | rowIndex = i 183 | } 184 | } 185 | else { 186 | rowIndex = i 187 | } 188 | if (key.y === row) { // If we have found the line that we want 189 | insert = false 190 | break 191 | } 192 | } 193 | } 194 | // How do we choose the insert point? 195 | // 1) If there is a matching row we edit that 196 | // 2) If there are other rows we add the new row in the correct order 197 | // 3) If there is NO row, then add it before the FL 198 | // 4) If there is no FL then add it before the next SC 199 | // 5) If we reach the end then put it at the end 200 | } // OL 201 | } // Find the splice point 202 | 203 | if ((key.s === pageSubCode) && (rowIndex === -1)) { // If no splice point was found then add to the end of the file 204 | rowIndex = this.ttiLines.length - 1 205 | } 206 | 207 | LOG.fn( 208 | ['page', 'keyMessage'], 209 | `Insert point=${rowIndex}`, 210 | LOG.LOG_LEVEL_VERBOSE 211 | ) 212 | 213 | if (key.s > pageSubCode) { // We didn't find the subcode? Lets add it 214 | LOG.fn( 215 | ['page', 'keyMessage'], 216 | `Adding subpage: key.s > subcode ${key.s}>=${pageSubCode}`, 217 | LOG.LOG_LEVEL_VERBOSE 218 | ) 219 | this.ttiLines.push('CT,8,T') 220 | 221 | let str = 'PN,' + pageNumber.toString(16) 222 | str += ('0' + key.s).slice(-2) 223 | 224 | this.ttiLines.push(str) // add the subcode 225 | this.ttiLines.push('SC,' + ('000' + key.s).slice(-4)) // add the four digit subcode 226 | this.ttiLines.push('PS,8000') 227 | this.ttiLines.push('RE,0') 228 | 229 | rowIndex = this.ttiLines.length - 1 230 | 231 | this.ttiLines.push('FL,' + fastext) 232 | } 233 | 234 | // we should now have the line in which we are going to do the insert 235 | if (insert) { 236 | this.ttiLines.splice(++rowIndex, 0, 'OL,' + key.y + ',xyzzy ') 237 | } 238 | 239 | let offset = 5 // OL,n, 240 | if (key.y > 9) { 241 | offset = 6 // OL,nn, 242 | } 243 | 244 | if (key.y === 28) { 245 | console.log("[page::keyMessage] ROW 28 not implemented") 246 | this.ttiLines[rowIndex] = "OL,28,0000000000000000000000000000000000000000" // Placeholder 247 | // TODO: At this point convert the message to x28f1 TTI 248 | } 249 | else 250 | { 251 | if (this.ttiLines[rowIndex].length < 45) { 252 | this.ttiLines[rowIndex] = this.ttiLines[rowIndex] + ' ' 253 | } 254 | 255 | this.ttiLines[rowIndex] = setCharAt(this.ttiLines[rowIndex], key.x + offset, key.k) 256 | LOG.fn( 257 | ['page', 'keyMessage'], 258 | `Setting a character at row = ${rowIndex}:<${this.ttiLines[rowIndex]}>`, 259 | LOG.LOG_LEVEL_VERBOSE 260 | ) 261 | } 262 | if (noDescription) { 263 | if (key.x === CONST.SIGNAL_DESCRIPTION_CHANGE) { 264 | this.ttiLines.unshift('DE,' + key.k) 265 | } else { 266 | this.ttiLines.unshift('DE,No description') 267 | } 268 | } 269 | } 270 | 271 | this.savePage = function (filename, cb, error) { 272 | // WARNING. We are saving to the stored filename! 273 | 274 | LOG.fn( 275 | ['page', 'savePage'], 276 | `Saving, filename=${this.filename}`, 277 | LOG.LOG_LEVEL_INFO 278 | ) 279 | 280 | this.callback = cb 281 | // this.filename='/dev/shm/test.tti' // @todo Check the integrity 282 | 283 | let txt = '' 284 | 285 | // Copy and escape the resulting lines, being careful not to escape the terminator 286 | for (let i = 0; i < this.ttiLines.length; i++) { 287 | txt += (EscapePrestel(this.ttiLines[i]) + '\n') 288 | } 289 | 290 | fs.writeFile( 291 | this.filename, 292 | txt, 293 | function (err) { 294 | if (err) { 295 | LOG.fn( 296 | ['page', 'savePage'], 297 | `Error: ${err}`, 298 | LOG.LOG_LEVEL_ERROR 299 | ) 300 | 301 | error(err) 302 | } else { 303 | // this.callback() // Can't get this callback to work. Says NOT a function 304 | } 305 | } 306 | ) 307 | }.bind(this) 308 | 309 | this.print = function () { 310 | for (let i = 0; i < this.ttiLines.length; i++) { 311 | console.log('[' + i + '] ' + this.ttiLines[i]) 312 | } 313 | } 314 | 315 | /** 316 | * @brief Validate a teletext page, and remove bits that are invalid 317 | * Rules: 318 | * 1) A header starts with the first line of the page 319 | * 2) A header can contain any of these line types in any order DE, DS, SP, CT, PN, SC, PS, RE 320 | * 3) Each header can have zero or one occurence of these line types, except PN which must exist. 321 | * 4) A header must have a valid PN. Any OL that arrives with a valid PN is discarded. 322 | * 5) When OL is received, it becomes the page body. 323 | * 6) When FL is received, expect the next header. 324 | * 7) If a header arrives while in body mode, go to header mode 325 | * 8) The next header must have a page number mppss, where mpp is the same and ss is a higher number than the previous header. 326 | * The general idea is that if we don't like a line, make it null in the first pass, then splice out the nulls in the second pass 327 | * @param page : A page is a ttifile that has been loaded into an array of strings 328 | */ 329 | this.validatePage = function () { 330 | // State machine constants 331 | const EXPECT_HEADER = 0 // Initial condition. (rule 1) 332 | const IN_HEADER = 1 // While parsing header (rule 2) 333 | const IN_VALID_PN = 2 // Found a valid PN in the header (rule 4) 334 | const IN_BODY = 3 // While parsing OL rows (rule 5) 335 | 336 | // Parse values 337 | let parseState = EXPECT_HEADER // state machine (rule 1) 338 | let mpp = -1 // PN: Magazine and page number 100 .. 8ff 339 | let ss = -1 // PN: Subpage 00..79 340 | console.log('[validatePage] ' + this.ttiLines) 341 | 342 | LOG.fn( 343 | ['page', 'validatePage'], 344 | `Line count = ${this.ttiLines.length}`, 345 | LOG.LOG_LEVEL_VERBOSE 346 | ) 347 | for (const li in this.ttiLines) { 348 | const line = this.ttiLines[li] 349 | console.log('The next line is ' + line) 350 | const tokens = line.split(',') 351 | // Handle the header rows (rule 2) 352 | const isHeader = ['DE', 'DS', 'SP', 'CT', 'PN', 'SC', 'PS', 'RE'].includes(tokens[0]) 353 | console.log('isHeader = ' + isHeader) 354 | if (isHeader) { 355 | if (parseState === EXPECT_HEADER) { 356 | console.log('[PARSER] IN_HEADER') 357 | parseState = IN_HEADER 358 | } 359 | // [!] @TODO Check the occurrences of each type. Allow no more than one (rule 3) 360 | // [!] @TODO Null out duplicates 361 | if (tokens[0] === 'PN') { // Does this header have a valid PN? (Rule 4) 362 | const mag = parseInt(tokens[1], 16) >> 8 363 | const subpage = parseInt(tokens[1].substring(3)) 364 | mpp = mag 365 | // If the PN valid? (rule 8) 366 | if ((mpp > 0 && mpp !== mag) || (ss > -1 && ss >= subpage)) { 367 | // Subsequent subpage is invalid. 368 | // Either the mpp doesn't match or subpage is not increasing 369 | this.ttiLines[li] = 'RM,1,INVALID PN. MARKED FOR DELETION' 370 | } else { 371 | mpp = mag 372 | ss = subpage 373 | console.log('[PARSER] IN_VALID_PN') 374 | parseState = IN_VALID_PN // We can now accept an OL (rule 4) 375 | } 376 | console.log('PN mag = ', mag.toString(16)) 377 | console.log('PN sub = ', subpage) 378 | console.log('PN = ', tokens[1]) 379 | } 380 | } // isHeader 381 | // Is it a valid row? 382 | const isBody = tokens[0] === 'OL' 383 | if (isBody) { 384 | switch (parseState) { 385 | case IN_VALID_PN: // OL follows a valid header 386 | parseState = IN_BODY // (rule 5) 387 | console.log('[PARSER] IN_BODY') 388 | break 389 | case EXPECT_HEADER: // OL not yet expected 390 | console.log('[PARSER] Unexpected OL before header') 391 | this.ttiLines[li] = 'RM,2,UNEXPECTED OL. MARKED FOR DELETION' 392 | break 393 | case IN_HEADER: // OL not yet expected 394 | console.log('[PARSER] Unexpected OL in header') 395 | this.ttiLines[li] = 'RM,3,UNEXPECTED OL. MARKED FOR DELETION' 396 | break 397 | case IN_BODY: // Another OL. Carry on 398 | break 399 | } 400 | } 401 | 402 | // 403 | if (parseState === IN_BODY) { 404 | if (tokens[0] === 'OL') { 405 | // [!] @todo Check that there are no more than one rows in the range 0..24 406 | // [!] @TODO Null out duplicates 0..24 407 | // [!] @TODO Limit duplicates for special packets 408 | // Is it a fastext link? (rule 6) 409 | } else if (tokens[0] === 'FL') { 410 | parseState = EXPECT_HEADER // Next subpage 411 | } else { 412 | console.log('[PARSER] EXPECT_HEADER') 413 | parseState = EXPECT_HEADER // (rule 7) 414 | } 415 | } 416 | } // For all lines 417 | 418 | // [!] @TODO Parse again and splice out null lines 419 | this.removeMarkedLines() 420 | } // Validatepage 421 | 422 | /** Remove lines starting "RM," from 423 | * ttiLines[] 424 | */ 425 | this.removeMarkedLines = function () { 426 | // [!] @TODO 427 | } 428 | } 429 | 430 | /** Utility */ 431 | function setCharAt (str, index, chr) { 432 | if (index > str.length - 1) return str 433 | return str.substr(0, index) + chr + str.substr(index + 1) 434 | } 435 | -------------------------------------------------------------------------------- /pagetest.js: -------------------------------------------------------------------------------- 1 | // Unit test for Page.js 2 | "use strict"; 3 | require('./page.js') 4 | 5 | var pageName 6 | 7 | var page=new Page() 8 | 9 | var eventList=[] 10 | 11 | var cb=function(rc) 12 | { 13 | console.log('[callback]'+rc.length) 14 | //for (var i=0;i0x8ff) ) { return 0 } 57 | if ( (subpage<0) || (subpage>99) ) { return 0 } 58 | if ( (column<0) || (column>39) ) { return 0 } 59 | if ( (row<0) || (row>39) ) { return 0 } 60 | var event= 61 | { 62 | S: service, 63 | k: key, 64 | p: pagenum, 65 | s: subpage, 66 | x: column, 67 | y: row 68 | } 69 | eventList.push(event) 70 | } 71 | 72 | eventSet1=function() // writing to an existing line 73 | { 74 | eventList=[] 75 | // Row 6 already exists. Add "insert" 76 | addEvent('wtf', 0x100, 0, 6, 12, 'i') 77 | addEvent('wtf', 0x100, 0, 6, 13, 'n') 78 | addEvent('wtf', 0x100, 0, 6, 14, 's') 79 | addEvent('wtf', 0x100, 0, 6, 15, 'e') 80 | addEvent('wtf', 0x100, 0, 6, 16, 'r') 81 | addEvent('wtf', 0x100, 0, 6, 17, 't') 82 | 83 | // Row 10 does not exist. Add "row 10" 84 | addEvent('wtf', 0x100, 0, 10, 20, 'r') 85 | addEvent('wtf', 0x100, 0, 10, 21, 'o') 86 | addEvent('wtf', 0x100, 0, 10, 22, 'w') 87 | addEvent('wtf', 0x100, 0, 10, 24, '1') 88 | addEvent('wtf', 0x100, 0, 10, 25, '0') 89 | 90 | // Subpage 1, row 4 does not exist. Add subpage 1 "New line 4" 91 | addEvent('wtf', 0x100, 1, 4, 5, 'N') 92 | addEvent('wtf', 0x100, 1, 4, 6, 'e') 93 | addEvent('wtf', 0x100, 1, 4, 7, 'w') 94 | addEvent('wtf', 0x100, 1, 4, 9, 'l') 95 | addEvent('wtf', 0x100, 1, 4, 10, 'i') 96 | addEvent('wtf', 0x100, 1, 4, 11, 'n') 97 | addEvent('wtf', 0x100, 1, 4, 12, 'e') 98 | addEvent('wtf', 0x100, 1, 4, 13, '4') 99 | } 100 | 101 | test4=function(rc) 102 | { 103 | cb(rc) 104 | console.log("[TEST 3] Result") 105 | eventSet1() 106 | for (var i=0;i!## OL,4,D] S j5 ssjupjws1  jws1~'o4  OL,5,D] S "! "#!"## ## # ## # "! # OL,6,D] C CITV THE WESTA GX MEN OL,7,A``````````````````````````````````````` OL,8,E OL,9,MYOUR IDEAL BREAK A MOUSE CLICK AWAY 522 OL,10, OL,11,E OL,12,CLOVEFis in the air ` and S7Qh'U(?$S"#k OL,13,Fyou can woo your special S5U|tp*kW~54 OL,14,Fperson by mobile text S5U#+/}Wovot OL,15,Fthe Atlantic. Vqp0 `ppp~ OL,16,F Vt o OL,17,CUS/Canada flights G235 V}| OL,18,CHoliday index G200 V]W`0x|}p`00 OL,19,E OL,20,D] FFULLCTELETEXTFCONTENTSG101 OL,21,A`````````Cwww.teletext.co.ukA`````````` OL,22,B]DPWINH2IWWF tickets for 30 May!!!AP355 OL,23,D]G CAPITAL ONE BANK. SEECC4 p682 OL,24,ATV Plus BSport CNews FHolidays FL,110,400,300,200,101,F @ -------------------------------------------------------------------------------- /private/muttlee_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /private/p404.tti: -------------------------------------------------------------------------------- 1 | DE,404 Page Not Found 2 | DS,inserter 3 | SP, 4 | CT,8,T 5 | PN,40400 6 | SC,0000 7 | PS,8000 8 | RE,0 9 | OL,0,XXXXXXXXTEEFAX mpp DAY dd MTH C hh:nn.ss 10 | OL,1,T]G 11 | OL,2,T]GPAGE NOT FOUND 12 | OL,3,T]G 13 | OL,4,T]GTry again later, it might work 14 | OL,5,T]G(no it won't) 15 | OL,6,T]G 16 | OL,7,T]GYou might as well accept that this 17 | OL,8,T]Gpage has gone to Atlanta and isn't 18 | OL,9,T]Gcoming back. 19 | OL,10,Tgso 20 | OL,11,T?# !p0k?# ! k%"o 21 | OL,12,U . o 5j . o7 j5  22 | OL,13,Q|| ~0#!z|| ~7 " j5  23 | OL,14,Qo5 / j5  24 | OL,15,S5 ?//% //! / j#+5 / 25 | OL,16,S5 '!  //7##! 75 + 26 | OL,17,W 2a `ppppp0 ss1``` (,,,$ 27 | OL,18,W !4!4! ( $(,,,,,$ ,,$((( !!! bs $ 28 | OL,19,W ! ! ! "" 1"#####! """ "#" d !, 29 | OL,20,W `>m0//j5 x/t t *o?%x/t d),, , 30 | OL,21,W ~qr}  j5 hwp{4j4 j5hwp{4 d),, , 31 | OL,22,W ##  j}|j7#k5 + j5j7#k5 ),, 32 | OL,23, 33 | OL,24,A Index BNews CEvents FArchive 34 | FL,100,104,800,200,8ff,100 35 | -------------------------------------------------------------------------------- /private/p404_editable.tti: -------------------------------------------------------------------------------- 1 | DE,404 Page Not Found 2 | DS,inserter 3 | SP, 4 | CT,8,T 5 | PN,40400 6 | SC,0000 7 | PS,8000 8 | RE,0 9 | OL,0,XXXXXXXXTEEFAX mpp DAY dd MTH C hh:nn.ss 10 | OL,1,T]G 11 | OL,2,T]GPAGE NOT FOUND 12 | OL,3,T]G 13 | OL,4,T]GTry again later, it might work 14 | OL,5,T]G(no it won't) 15 | OL,6,T]G 16 | OL,7,T]GYou might as well accept that this 17 | OL,8,T]Gpage has gone to Atlanta and isn't 18 | OL,9,T]Gcoming back. 19 | OL,10,Tgso 20 | OL,11,T?# !p0k?# ! k%"o 21 | OL,12,U . o 5j . o7 j5  22 | OL,13,Q|| ~0#!z|| ~7 " j5  23 | OL,14,Qo5 / j5  24 | OL,15,S5 ?//% //! / j#+5 / 25 | OL,16,S5 '!  //7##! 75 + 26 | OL,17,W 2a `ppppp0 ss1``` (,,,$ 27 | OL,18,W !4!4! ( $(,,,,,$ ,,$((( !!! bs $ 28 | OL,19,W ! ! ! "" 1"#####! """ "#" d !, 29 | OL,20,W `>m0//j5 x/t t *o?%x/t d),, , 30 | OL,21,W ~qr}  j5 hwp{4j4 j5hwp{4 d),, , 31 | OL,22,W ##  j}|j7#k5 + j5j7#k5 ),, 32 | OL,23, *CYELLOWGcreates a new page 33 | OL,24,A Index BNews CCreateNewPage FArchive 34 | FL,100,104,800,200,8ff,100 35 | -------------------------------------------------------------------------------- /public/assets/R10000.txt: -------------------------------------------------------------------------------- 1 | DE,Read back page10000 2 | PN,10000 3 | CT,10,C 4 | SC,0001 5 | PS,8000 6 | MS,1 7 | OL,1,G 8 | OL,2,D] S `pp0`p0`0 pp ppp pp p `0ppp 9 | OL,3,D] S "k7!sj5 jw{5##jw{5+t>!## 10 | OL,4,D] S j5 ssjupjws1  jws1~'o4  11 | OL,5,D] S "! "#!"## ## # ## # "! # 12 | OL,6,D] C CITV THE WESTA GX MEN 13 | OL,7,A``````````````````````````````````````` 14 | OL,8,E 15 | OL,9,MYOUR IDEAL BREAK A MOUSE CLICK AWAY 522 16 | OL,10, 17 | OL,11,E 18 | OL,12,CLOVEFis in the air ` and S7Qh'U(?$S"#k 19 | OL,13,Fyou can woo your special S5U|tp*kW~54 20 | OL,14,Fperson by mobile text S5U#+/}Wovot 21 | OL,15,Fthe Atlantic. Vqp0 `ppp~ 22 | OL,16,F Vt o 23 | OL,17,CUS/Canada flights G235 V}| 24 | OL,18,CHoliday index G200 V]W`0x|}p`00 25 | OL,19,E 26 | OL,20,D] FFULLCTELETEXTFCONTENTSG101 27 | OL,21,A`````````Cwww.teletext.co.ukA`````````` 28 | OL,22,B]DPWINH2IWWF tickets for 30 May!!!AP355 29 | OL,23,D]G CAPITAL ONE BANK. SEECC4 p682 30 | OL,24,ATV Plus BSport CNews FHolidays 31 | FL,110,400,300,200,101,F 32 | @ -------------------------------------------------------------------------------- /public/assets/WikiTelFax.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/WikiTelFax.pdf -------------------------------------------------------------------------------- /public/assets/pixelate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/assets/teletext2.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/teletext2.otf -------------------------------------------------------------------------------- /public/assets/teletext2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/teletext2.ttf -------------------------------------------------------------------------------- /public/assets/teletext2.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/teletext2.woff -------------------------------------------------------------------------------- /public/assets/teletext2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/teletext2.woff2 -------------------------------------------------------------------------------- /public/assets/teletext4.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/teletext4.otf -------------------------------------------------------------------------------- /public/assets/teletext4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/teletext4.ttf -------------------------------------------------------------------------------- /public/assets/teletext4.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/teletext4.woff -------------------------------------------------------------------------------- /public/assets/teletext4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/assets/teletext4.woff2 -------------------------------------------------------------------------------- /public/charchanged.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** charchanged.js 3 | * Class to indicate which keys have changed and have not been processed by the server. 4 | * This is an array 24 x 40 flags, one per character. 5 | * When a new character is set, the corresponding flag is set. 6 | * When the character is returned by the server, the flag is reset and the highlight removed. 7 | * @todo Range checking 8 | */ 9 | 10 | const CHARCHANGED = function () { 11 | // Create 24 rows of 40 characters 12 | this.rows = new Array(26) 13 | for (let row = 0; row < this.rows.length; row++) { 14 | this.rows[row] = new Array(40) 15 | for (let ch = 0; ch < 40; ch++) { 16 | this.rows[row][ch] = false 17 | } 18 | } 19 | 20 | this.get = function (x, y) { 21 | return (this.rows[y][x]) 22 | } 23 | 24 | this.set = function (x, y) { 25 | this.rows[y][x] = true 26 | } 27 | 28 | this.clear = function (x, y) { 29 | this.rows[y][x] = false 30 | } 31 | } // CHARCHANGED 32 | -------------------------------------------------------------------------------- /public/clut.js: -------------------------------------------------------------------------------- 1 | /* 2 | # clut.js. 3 | # 4 | # clut.js Teletext colour lookup table 5 | # Maintains colour lookups and all data in X28/F1 packet 6 | # 7 | # Copyright (c) 2024 Peter Kwan 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | # This holds the colour lookup tables read in by packet 28 etc. 29 | # I think we have four CLUTs 0 to 3. Here is what the standard says: 30 | ## 8 background full intensity colours: 31 | ## Magenta, Cyan, White. Black, Red, Green, Yellow, Blue, 32 | ## 7 foreground full intensity colours: 33 | ## Cyan, White. Red, Green, Yellow, Blue, Magenta, 34 | ## Invoked as spacing attributes via codes in packets X/0 to X/25. 35 | ## Black foreground: Invoked as a spacing attribute via codes in packets X/0 36 | ## to X/25. 37 | ## 32 colours per page. The Colour Map contains four CLUTs 38 | ## (numbered 0 - 3), each of 8 entries. Each entry has a four bit resolution for 39 | ## the RGB components, subclause 12.4. 40 | ## Presentation 41 | ## Level 42 | ## 1 1.5 2.5 3.5 43 | ## { { ~ ~ 44 | ## ~ ~ ~ ~ 45 | ## { { ~ ~ 46 | ## { { ~ ~ 47 | ## Colour Definition 48 | ## CLUT 0 defaults to the full intensity colours used as spacing colour 49 | ## attributes at Levels 1 and 1.5. 50 | ## CLUT 1, entry 0 is defined to be transparent. CLUT 1, entries 1 to 7 default 51 | ## to half intensity versions of CLUT 0, entries 1 to 7. 52 | ## CLUTs 2 and 3 have the default values specified in subclause 12.4. CLUTs 53 | ## 2 and 3 can be defined for a particular page by packet X/28/0 Format 1, or 54 | ## for all pages in magazine M by packet M/29/0. 55 | ## Colour Selection 56 | ## CLUT 0, entries 1 to 7 are selectable directly by the Level 1 data as 57 | ## spacing attributes. CLUTs 0 to 3 are selectable via packets 26 or objects 58 | ## as non-spacing attributes. 59 | ## The foreground and background colour codes on the Level 1 page may be 60 | ## used to select colours from other parts of the Colour Map. Different CLUTs 61 | ## may be selected for both foreground and background colours. 62 | ## This mapping information is transmitted in packet X/28/0 Format 1 for the 63 | ## associated page and in packet M/29/0 for all pages in magazine M. 64 | ## With the exception of entry 0 in CLUT 1 (transparent), CLUTs 0 and 1 can 65 | ## be redefined for a particular page by packet X/28/4, or 66 | ## 67 | */ 68 | 'use strict'; 69 | class Clut { 70 | constructor() { 71 | console.log ('Clut loaded') 72 | this.dc = 0 // Should always be 0 for this packet 73 | this.pageFunction = 0 74 | this.pageCoding = 0 75 | this.defaultG0G2CharacterSet 76 | this.clut0 = new Array(8) // default full intensity colours 77 | this.clut1 = new Array(8) // default half intensity colours 78 | this.clut2 = new Array(8) 79 | this.clut3 = new Array(8) 80 | this.defaultScreenColour = 0 // black. [!] Assume that bits 4,3 are CLUT, 2,1,0 are Colour 81 | this.defaultRowColour = 0 // black 82 | this.remap = 0 // 0..7 Colour Table remapping 83 | this.blackBackgroundSub = false // Allow CLUT to change the background colour 84 | this.enableLeftPanel = false 85 | this.enableRightPanel = false 86 | this.sidePanelStatusFlag = false 87 | this.leftColumns = -1 // 0..15 88 | this.rightColumns = 0 // implied 89 | // set defaults 90 | this.resetTable() 91 | 92 | // Setters and Getters 93 | let self = this // You'll thank me later 94 | this.getDefaultScreenClut = function() { 95 | return (self.defaultScreenColour >> 3) & 0x03 96 | } 97 | this.getDefaultScreenColourIndex = function() { 98 | return self.defaultScreenColour & 0x07 99 | } 100 | this.getDefaultRowClut = function() { 101 | return (self.defaultRowColour >> 3) & 0x03 102 | } 103 | this.getDefaultRowColourIndex = function() { 104 | return self.defaultRowColour & 0x07 105 | } 106 | 107 | /// Replace the screen clut value 108 | this.setDefaultScreenClut = function(value) { 109 | self.defaultScreenColour &= 0x07 110 | self.defaultScreenColour |= value << 3 111 | } 112 | 113 | /// Replace the screen colour index value 114 | this.setDefaultScreenColourIndex = function(value) { 115 | self.defaultScreenColour &= 0x18 116 | self.defaultScreenColour |= (value & 0x07) 117 | } 118 | 119 | /// Replace the row clut value 120 | this.setDefaultRowClut = function(value) { 121 | self.defaultRowColour &= 0x07 122 | self.defaultRowColour |= value << 3 123 | } 124 | 125 | /// Replace the row colour index value 126 | this.setDefaultRowColourIndex = function(value) { 127 | self.defaultRowColour &= 0x18 128 | self.defaultRowColour |= (value & 0x07) 129 | } 130 | } // constructor 131 | 132 | // Setters and getters 133 | 134 | setDC(value) { 135 | this.dc = value // Should always be 0 136 | } 137 | 138 | setPageFunction(value) { 139 | this.pageFunction = value 140 | } 141 | 142 | setPageCoding(value) { 143 | this.coding = value 144 | } 145 | 146 | setDefaultG0G2CharacterSet(value) { 147 | this.defaultG0G2CharacterSet = value 148 | } 149 | 150 | setSecondG0G2CharacterSet(value) { 151 | this.secondG0G2CharacterSet = value 152 | } 153 | 154 | setDefaultScreenColour(value) { 155 | // @todo Check that it is a 5 bit value 156 | this.defaultScreenColour = value 157 | } 158 | 159 | setDefaultRowColour(value) { 160 | // @todo Check that it is a 5 bit value 161 | this.defaultRowColour = value 162 | } 163 | 164 | setLeftColumns(value) { 165 | // @todo Check that it is 20 or less 166 | this.leftColumns = value 167 | } 168 | 169 | setEnableLeftPanel(value) { 170 | this.enableLeftPanel = value 171 | } 172 | 173 | setEnableRightPanel(value) { 174 | this.enableRightPanel = value 175 | } 176 | 177 | setSidePanelStatusFlag(value) { 178 | this.sidePanelStatusFlag = value 179 | } 180 | setRemap(remap) { 181 | this.remap = remap & 0x7 182 | } 183 | 184 | setBlackBackgroundSub(bgFlag) { 185 | this.blackBackgroundSub = bgFlag!==0 186 | } 187 | 188 | /** Used by X28/0 to swap entire cluts 189 | * @param colour - Colour index 0..7 190 | * @param foreground - True for foreground colour, or False for background 191 | * @return - Colour string for tkinter. eg. 'black' or '#000' 192 | * Given a colour, it maps the colour according to the remapping Table 4 193 | * and whether it is a background or a foreground colour 194 | */ 195 | remapColourTable(colourIndex, foreground) { 196 | let clutIndex = 0 197 | if (foreground) { 198 | if (this.remap > 4) { 199 | clutIndex = 2 200 | } else if (this.remap < 3) { 201 | clutIndex = 0 202 | } else { 203 | clutIndex = 1 204 | } 205 | } else { 206 | if (this.remap < 3) { // background 207 | clutIndex = this.remap 208 | } else if (this.remap === 3 || this.remap === 5) { 209 | clutIndex = 1 210 | } else if (this.remap === 4 || this.remap === 6) { 211 | clutIndex = 2 212 | } else { 213 | clutIndex = 3 214 | } 215 | } 216 | // Black Background Colour Substitution 217 | // If this is set, then colour 0 in a row is replaced by the default row colour 218 | if (this.blackBackgroundSub && !foreground && colourIndex===0) { 219 | let value = this.defaultRowColour 220 | let clut = (value >> 3) & 0x03 221 | let colour = value & 0x07 222 | switch (clut) { 223 | case 0: return this.clut0[colour] 224 | case 1: return this.clut1[colour] 225 | case 2: return this.clut2[colour] 226 | case 3: return this.clut3[colour] 227 | } 228 | return color(0, 0, 255) 229 | } 230 | if (colourIndex === 0) { 231 | // print("This is a test clutIndex = " + clutIndex) 232 | // @todo Implement black background colour substitution 233 | } 234 | return this.getValue(clutIndex, colourIndex) 235 | } 236 | 237 | resetTable() { // Default values from table 12.4 238 | // CLUT 0 full intensity 239 | this.clut0[0] = color(0) // black 240 | this.clut0[1] = color(255, 0, 0) // red 241 | this.clut0[2] = color(0, 255, 0) // green 242 | this.clut0[3] = color(255, 255, 0) // yellow 243 | this.clut0[4] = color(0, 0, 255) // blue 244 | this.clut0[5] = color(255, 0, 255) // magenta 245 | this.clut0[6] = color(0, 255, 255) // cyan 246 | this.clut0[7] = color(255, 255, 255) // white 247 | 248 | // CLUT 1 half intensity 249 | this.clut1[0] = color(0,0,0) // transparent @todo ? 250 | this.clut1[1] = color(127, 0, 0) // half red 251 | this.clut1[2] = color(0, 127, 0) // half green 252 | this.clut1[3] = color(127, 127, 0) // half yellow 253 | this.clut1[4] = color(0, 0, 127) // half blue 254 | this.clut1[5] = color(127, 0, 127) // half magenta 255 | this.clut1[6] = color(0, 127, 127) // half cyan 256 | this.clut1[7] = color(127, 127, 127) // half white 257 | 258 | // CLUT 2 lovely colours 259 | this.clut2[0] = color(0xff, 0x00, 0x55) // crimsonish 260 | this.clut2[1] = color(0xff, 0x77, 0x00) // orangish 261 | this.clut2[2] = color(0x00, 0xff, 0x77) // blueish green 262 | this.clut2[3] = color(0xff, 0xff, 0xbb) // pale yellow 263 | this.clut2[4] = color(0x00, 0xcc, 0xaa) // cyanish 264 | this.clut2[5] = color(0x55, 0x00, 0x00) // dark red 265 | this.clut2[6] = color(0x66, 0x55, 0x22) // hint of a tint of runny poo 266 | this.clut2[7] = color(0xcc, 0x77, 0x77) // gammon 267 | 268 | // CLUT 3 more lovely colours 269 | this.clut3[0] = color(48, 48, 48) // pastel black 270 | this.clut3[1] = color(255, 127, 127) // pastel red 271 | this.clut3[2] = color(127, 255,127) // pastel green 272 | this.clut3[3] = color(255, 255, 127) // pastel yellow 273 | this.clut3[4] = color(127, 127, 255) // pastel blue 274 | this.clut3[5] = color(255, 127, 255) // pastel magenta 275 | this.clut3[6] = color(127, 255, 255) // pastel cyan 276 | this.clut3[7] = color(0xdd, 0xdd, 0xdd) // pastel white 277 | 278 | this.blackBackgroundSub = false 279 | this.remap = 0 // Default to CLUT 0 280 | } 281 | 282 | /** set a value in a particular clut 283 | * Get the colour from a particular clut 284 | * Probably want to record which cluts are selected 285 | * Lots of stuff 286 | 287 | * @param colour - p5js color object 288 | * @param clutIndex CLUT index 0 to 3 289 | * @param clrIndex - 0..7 colour index 290 | */ 291 | setValue(colour, clutIndex, clrIndex) { 292 | clrIndex = clrIndex % 8 // need to trap this a bit better. This is masking a problem 293 | clutIndex = clutIndex % 4 294 | switch (clutIndex) { 295 | case 0: 296 | this.clut0[clrIndex] = colour 297 | break 298 | case 1: 299 | this.clut1[clrIndex] = colour 300 | break 301 | case 2: 302 | this.clut2[clrIndex] = colour 303 | break 304 | case 3: 305 | this.clut3[clrIndex] = colour 306 | break 307 | } 308 | // console.log("clut value: clut" + clutIndex + " set[" + clrIndex + '] = ' + colour) 309 | } 310 | 311 | /** 312 | * @param clutIndex CLUT index 0 to 3 313 | * @param clrIndex - 0..7 colour index 314 | * @return colour - 12 bit web colour number eg. 0x1ab 315 | * // [!] Hmm seems to return p5js colour type? 316 | */ 317 | getValue(clutIndex, clrIndex) { 318 | clutIndex = clutIndex % 4 319 | clrIndex = clrIndex % 8 320 | // console.log("[getValue] clutIndex = " + clutIndex + " clrIndex = " + clrIndex) 321 | switch (clutIndex) { 322 | case 0: 323 | return this.clut0[clrIndex] 324 | case 1: 325 | return this.clut1[clrIndex] 326 | case 2: 327 | //console.log("colour selected = " + this.clut2[clrIndex]) 328 | return this.clut2[clrIndex] 329 | case 3: 330 | return this.clut3[clrIndex] 331 | default: 332 | return this.clut0[clrIndex] // just in case! 333 | } 334 | } 335 | 336 | /** colour12to24 337 | * @param colour12 - a 12 bit colour 338 | * @return - p5js colour 339 | */ 340 | static colour12to24(colour12) { 341 | print(colour12) 342 | colour12 = parseInt(colour12, 16) 343 | let r = (colour12 >> 8) & 0x0f 344 | let g = (colour12 >> 4) & 0x0f 345 | let b = colour12 & 0x0f 346 | return color( 347 | (r<<4 || r), 348 | (g<<4 || g), 349 | (b<<4 || b)) 350 | } 351 | 352 | /** colour24to12 353 | * @param p5js colour 354 | * @return - colour12 - a 12 bit colour 355 | */ 356 | static colour24to12(colour24) { 357 | print(colour24) 358 | let c = colour24 // The p5js colour 359 | let cs = (c.levels[0]>>4) << 8 | (c.levels[1]>>4) << 4 | (c.levels[2]>>4) // The 12 bit colour 360 | return cs 361 | } 362 | 363 | /** 364 | * Deep copy clut. 365 | */ 366 | static copyClut(src, dest) { 367 | for (let i=0; i<8; i++) { 368 | dest.clut0[i]=src.clut0[i] 369 | dest.clut1[i]=src.clut1[i] 370 | dest.clut2[i]=src.clut2[i] 371 | dest.clut3[i]=src.clut3[i] 372 | } 373 | dest.defaultScreenColour = src.defaultScreenColour 374 | dest.defaultRowColour = src.defaultRowColour 375 | dest.remap = src.remap 376 | dest.blackBackgroundSub = src.blackBackgroundSub 377 | dest.enableLeftPanel = src.enableLeftPanel 378 | dest.enableRightPanel = src.enableRightPanel 379 | dest.leftColumns = src.leftColumns 380 | } 381 | 382 | /** debug dump the clut contents 383 | * Don't need this right now 384 | */ 385 | /* 386 | dump() { 387 | console.log("[Dump] CLUT values") 388 | for (let i=0; i<8; i++) { 389 | (this.clut0[i] + ', ', end='') 390 | print() 391 | for i in range(8): 392 | print(this.clut1[i] + ', ', end='') 393 | print() 394 | for i in range(8): 395 | print(this.clut2[i] + ', ', end='') 396 | print() 397 | for i in range(8): 398 | print(this.clut3[i] + ', ', end='') 399 | print() 400 | } 401 | */ 402 | 403 | /** Return the 12 bit RGB value of the default screen colour 404 | */ 405 | getDefaultScreenRGB() { 406 | let val = this.getValue(this.getDefaultScreenClut(), this.getDefaultScreenColourIndex()) 407 | print("screen clut = " + this.getDefaultScreenClut() + " screen index = " + this.getDefaultScreenColourIndex() + " value = " + val) 408 | return val 409 | } 410 | 411 | /** Return the 12 bit RGB value of the default screen colour 412 | */ 413 | getDefaultRowRGB() { 414 | return this.getValue(this.getDefaultRowClut(), this.getDefaultRowColourIndex()) 415 | } 416 | 417 | } -------------------------------------------------------------------------------- /public/cursor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global constrain */ 3 | /** Cursor class for teletext 4 | */ 5 | class TTXCURSOR { 6 | constructor () { 7 | print("[TTXCURSOR] Constructor") 8 | this.x = 0 9 | this.y = 0 10 | this.hide = true // Not used atm 11 | this.callback = null // Callback function for when the cursor is moved 12 | } 13 | 14 | right () { 15 | this.x++ 16 | if (this.x > 39) { 17 | this.x = 39 18 | } 19 | this.doCallback('R') 20 | return this.x 21 | } 22 | 23 | left () { 24 | this.x-- 25 | if (this.x < 0) { 26 | this.x = 0 27 | } 28 | this.doCallback('L') 29 | return this.x 30 | } 31 | 32 | up () { 33 | this.y-- 34 | if (this.y < 0) { 35 | this.y = 0 36 | } 37 | this.doCallback('U') 38 | return this.y 39 | } 40 | 41 | down () { 42 | this.y++ 43 | if (this.y > 24) { 44 | this.y = 24 45 | } 46 | this.doCallback('D') 47 | return this.y 48 | } 49 | 50 | newLine () { 51 | this.down() 52 | this.x = 0 53 | this.doCallback('N') 54 | return this.y 55 | } 56 | 57 | moveTo (x, y) { 58 | this.x = constrain(x, 0, 39) 59 | this.y = constrain(y, 0, 24) 60 | this.doCallback('M') 61 | } 62 | 63 | /** Set a callback 64 | * The callback should have the X and Y coordinates 65 | */ 66 | setCallback(callback) { 67 | this.callback = callback 68 | } 69 | 70 | /** Execute a callback for when the cursor moves 71 | * @param ch - Can be used for debugging 72 | */ 73 | doCallback (ch) { 74 | if (this.callback !== null && this.callback !==undefined) { 75 | this.callback(this.x, this.y) 76 | } 77 | else 78 | { 79 | print("There is no callback") 80 | } 81 | // console.log("cursor="+ch+" ("+this.x+","+this.y+")") 82 | } 83 | } // TTXCursor 84 | -------------------------------------------------------------------------------- /public/edittest.js: -------------------------------------------------------------------------------- 1 | // Run this unit test under node 2 | 'use strict' 3 | /* global TTXPAGE, saveToHash */ 4 | const EDITMODE_NORMAL = 0 // normal viewing 5 | const EDITMODE_EDIT = 1 // edit mode 6 | const EDITMODE_ESCAPE = 2 // expect next character to be either an edit.tf function or Escape again to exit. 7 | const EDITMODE_INSERT = 3 // The next character is ready to insert 8 | 9 | require('./edittf.js') 10 | require('./cursor.js') 11 | require('./ttxpage.js') 12 | 13 | console.log('Test configuring') 14 | 15 | const page = new TTXPAGE() 16 | page.init(0x190) 17 | page.setSubPage(1) 18 | 19 | // var website="http://edit.tf" // could be zxnet. Up to you 20 | const website = 'https://zxnet.co.uk/teletext/editor/' // could be zxnet. Up to you 21 | const cset = 0 // WST EN 22 | 23 | let encoding 24 | 25 | console.log('Test started') 26 | encoding = saveToHash(cset, website, page) 27 | console.log('Result=') 28 | console.log(encoding) 29 | console.log('Test finished') 30 | -------------------------------------------------------------------------------- /public/edittf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** This code adapted from Simon Rawles 3 | * by Peter Kwan 4 | * with contributions from Alistair Cree. 5 | * 6 | // Copyright (C) 2015 Simon Rawles 7 | // Reference: https://github.com/rawles/edit-tf/wiki/Teletext-page-hashstring-format 8 | // 9 | // The JavaScript code in this page is free software: you can 10 | // redistribute it and/or modify it under the terms of the GNU 11 | // General Public License (GNU GPL) as published by the Free Software 12 | // Foundation, either version 3 of the License, or (at your option) 13 | // any later version. The code is distributed WITHOUT ANY WARRANTY; 14 | // without even the implied warranty of MERCHANTABILITY or FITNESS 15 | // FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. 16 | // 17 | // As additional permission under GNU GPL version 3 section 7, you 18 | // may distribute non-source (e.g., minimized or compacted) forms of 19 | // that code without the copy of the GNU GPL normally required by 20 | // section 4, provided you include this license notice and a URL 21 | // through which recipients can access the Corresponding Source. 22 | */ 23 | 24 | /* @todo load from edit.tf 25 | function load_from_hash(TTXPage* page, char* str) 26 | { 27 | const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 28 | char* hashstring=strchr(str,'#'); // Find the start of hash string 29 | // @todo Get the metadata 30 | 31 | if (hashstring){ 32 | hashstring=strchr(hashstring,':');// move past metadata 33 | if (hashstring){ 34 | // Is it valid length? 35 | hashstring++; 36 | uint16_t len=strlen(hashstring); 37 | if (len<1120) return; 38 | 39 | // Initialise before the loop 40 | uint8_t currentCode=0; 41 | uint8_t outBit=0x40; 42 | uint8_t outCol=0; 43 | uint8_t outRow=0; // Teletext40 uses the header row 44 | char line[40]; 45 | char* pos; 46 | // for (int i=0;i<40;i++)line[i]=0; 47 | for (uint16_t i=0; i<1167; i++) 48 | { 49 | char ch=*hashstring ; 50 | hashstring++; 51 | pos=strchr(base64,ch); 52 | if (pos==NULL) 53 | { 54 | std::cout << "can not find character " << ch << std::endl; 55 | } 56 | uint32_t code=(pos-base64) &0xff; 57 | 58 | for (uint8_t b=0x20;b>0;b>>=1) // Source bit mask 59 | { 60 | if ((b&code)>0) 61 | { 62 | currentCode|=outBit; 63 | } 64 | outBit>>=1; // next output bit 65 | if (outBit==0) // Character done? 66 | { 67 | assert(currentCode<0x80); 68 | if (currentCode<' ') // Control codes. Only null and CR cause problems. 69 | { 70 | currentCode|=0x80; 71 | } 72 | line[outCol]=currentCode; // Save the character 73 | currentCode=0; 74 | outBit=0x40; 75 | assert(outCol<40); 76 | outCol++; // next column 77 | if (outCol>=40) 78 | { 79 | page->SetRow(outRow,std::string(line)); 80 | outCol=0; 81 | outRow++; 82 | } 83 | } 84 | } // bit mask 85 | } // For each encoded source char 86 | // @todo At this point, if the first row contains a page number in the right place, use it as an initial value 87 | // parse the rest of the string which is a set of :key=value pairs eg. 88 | // :PN=550:PS=400c:SC=2 89 | printf("hashstring=%s\n",hashstring); 90 | char * pair = strtok(hashstring,":"); 91 | while (pair) { 92 | char * eq=strchr(pair,'='); 93 | if (!eq) return; // oops! 94 | char * key=pair; 95 | *eq=0; 96 | char * value=eq; 97 | value++; 98 | if (!strcmp("PN",key)) { // Page Number - 3 hex digits 99 | int page_num=std::strtol(value,NULL,16); 100 | page->SetPageNumber(page_num*0x100); 101 | } 102 | if (!strcmp("SC",key)) { // Subcode - 4 hex digits 103 | int subcode_num=std::strtol(value,NULL,16); 104 | page->SetSubCode(subcode_num); 105 | } 106 | if (!strcmp("PS",key)) { // Page Status - 4 hex digits 107 | int status_num=std::strtol(value,NULL,16); 108 | page->SetPageStatus(status_num); 109 | } 110 | if (!strcmp("X270",key)) { // X/27/0 fastext. 6 x MPPSSSSS 111 | for (int i=0;i<6;i++) { 112 | int link; 113 | sscanf(&value[i*7],"%3x",&link); 114 | page->SetFastextLink(i,link); 115 | } 116 | } 117 | if (!strcmp("X280",key)) { // X/28/0 format 1 enhancement data 118 | } 119 | *eq=' '; // Hack it so tokens still work 120 | pair=strtok(NULL,":"); 121 | } 122 | } // if hashstring metadata 123 | } // if hashstring 124 | } 125 | */ 126 | 127 | // Similarly, we want to save the page to the hash. This simply 128 | // converts the character set and page data into a hex string and 129 | // puts it there. 130 | 131 | /** 132 | * \param cset A character set 0-Eng 1-Ger 2-Swe 3-Ita 4-Bel 5-ASCII 6=Heb 7=Cyr 133 | * \param website The website prefix eg. "http://edit.tf" 134 | * \param encoding 135 | */ 136 | // function saveToHash(int cset, char* encoding, uint8_t cc[25][40], const char* website, TTXPage* page) 137 | const saveToHash = function (cset, website, page) { 138 | // Construct the metadata as described above. 139 | const metadata = '/#' + cset + ':' 140 | 141 | let encoding = website + metadata 142 | 143 | // Construct a base-64 array by iterating over each character 144 | // in the frame. 145 | const b64 = [] // was 1300 long 146 | for (let i = 0; i < 1300; i++) { 147 | b64[i] = 0 148 | } 149 | let framebit = 0 150 | console.log(page) 151 | const p = page.subPageList[page.subPage] // This is the page that we are sending 152 | for (let r = 0; r < 25; r++) { // Now include fastext 153 | const txt = p[r].txt // This is the text of the row that we are sending 154 | console.log('row=' + txt) 155 | for (let c = 0; c < 40; c++) { 156 | const ch = txt.charCodeAt(c) 157 | for (let b = 0; b < 7; b++) { // 7 bits per teletext character 158 | // Read a bit and write a bit. 159 | const bitval = ch & (1 << (6 - b)) 160 | if (bitval) { 161 | // Work out the position of the character in the 162 | // base-64 encoding and the bit in that position. 163 | const b64bitoffset = framebit % 6 164 | const b64charoffset = (framebit - b64bitoffset) / 6 165 | b64[b64charoffset] |= 1 << (5 - b64bitoffset) 166 | } 167 | framebit++ 168 | } 169 | } 170 | } 171 | 172 | // Encode bit-for-bit. 173 | // const sz = encoding.length 174 | const base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' 175 | for (let i = 0; i < 1167; i++) { 176 | encoding += base64[b64[i]] 177 | } 178 | // metadata (@todo for zxnet ) 179 | const pageNumber = page.pageNumber 180 | const subcode = page.subPage 181 | // var status="0x8000" // page.GetPageStatus() 182 | encoding += ':PN=' + (pageNumber.toString(16)) // 3 183 | encoding += ':SC=' + subcode // 4 184 | encoding += ':X270=' + page.redLink.toString(16) + '0000' + // The six Fastext links 185 | page.greenLink.toString(16) + '0000' + 186 | page.yellowLink.toString(16) + '0000' + 187 | page.cyanLink.toString(16) + '0000' + 188 | '8ff0000' + 189 | page.indexLink + '0000' 190 | if (page.redLink !== 0x900) { 191 | encoding += 'F' // If we got a Fastext FL line, display it. 192 | } 193 | // encoding+=":X270=12300008FF00008FF000070000008FF00008FF0000" // @todo 194 | // encoding[1167+sz]=0; 195 | return encoding 196 | } 197 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/Muttlee/e643558d040d819b9451f977e4dd2653115d3059/public/favicon.ico -------------------------------------------------------------------------------- /public/log.js: -------------------------------------------------------------------------------- 1 | /* global CONST, CONFIG */ 2 | 'use strict' 3 | const LOG = { 4 | blank: function () { 5 | console.log('') 6 | }, 7 | 8 | fn: function (context, message, severity = CONST.LOG_LEVEL_VERBOSE, blankLineAfter = false) { 9 | // only output log message if severity is within current log output level config value 10 | if (severity <= CONFIG[CONST.CONFIG.LOG_LEVEL_TELETEXT_VIEWER]) { 11 | let contextString = '' 12 | if (context) { 13 | contextString = Array.isArray(context) 14 | ? context.join('::') 15 | : context 16 | 17 | contextString = `[${contextString}] ` 18 | } 19 | 20 | if (Array.isArray(message)) { 21 | for (const i in message) { 22 | console.log(contextString + message[i]) 23 | } 24 | } else { 25 | console.log(contextString + message) 26 | } 27 | 28 | if (blankLineAfter) { 29 | console.log('') 30 | } 31 | } 32 | }, 33 | 34 | // re-export log levels for convenience 35 | LOG_LEVEL_VERBOSE: CONST.LOG_LEVEL_VERBOSE, 36 | LOG_LEVEL_INFO: CONST.LOG_LEVEL_INFO, 37 | LOG_LEVEL_MANDATORY: CONST.LOG_LEVEL_MANDATORY, 38 | LOG_LEVEL_ERROR: CONST.LOG_LEVEL_ERROR, 39 | LOG_LEVEL_NONE: CONST.LOG_LEVEL_NONE 40 | } 41 | 42 | if (typeof exports === 'object') { 43 | module.exports = LOG 44 | } 45 | -------------------------------------------------------------------------------- /public/uifield.js: -------------------------------------------------------------------------------- 1 | /* 2 | # uifield.js. 3 | # 4 | # uifield.js Class for teletext based ui editing 5 | # Manage UI fields for user input of data 6 | # 7 | # Copyright (c) 2025 Peter Kwan 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | #*/ 27 | "use strict" 28 | 29 | class uiField { 30 | 31 | /** 32 | * @param uiType - CONST.UI_FIELD.FIELD_HEXCOLOUR | FIELD_CHECKBOX | FIELD_NUMBER | FIELD_COMBO 33 | * @param xLoc - X origin of the field 34 | * @param yLoc - Y origin of the field 35 | * @param xWidth - Width of the field 36 | * @param yHeight - Width of the field 37 | * @param clutIndex - index of the CLUT this colour comes from 38 | */ 39 | constructor(uiType, xLoc, yLoc, xWidth, yHeight, clutIndex, hint) { 40 | this.uiType = uiType 41 | this.xLoc = xLoc 42 | this.yLoc = yLoc 43 | this.xWidth = xWidth 44 | this.yHeight = yHeight 45 | this.clutIndex = clutIndex 46 | this.hint = hint 47 | } 48 | 49 | /** validateKey 50 | * Update this field according to the key press and its location 51 | * This comes from a callback in TTXPROPERTIES 52 | */ 53 | validateKey(key) 54 | { 55 | // Is the cursor in our field 56 | // Is the key valid for this type of field? 57 | if (Number.isInteger(key)) { // TAB, Page Up, Page Down etc. 58 | return key 59 | } 60 | key = key.toLowerCase() 61 | switch (this.uiType) { 62 | case CONST.UI_FIELD.FIELD_HEXCOLOUR: // Three digit hex value. 63 | if ( ((key >= '0') && (key <='9')) || 64 | ((key >='a') && (key <='f'))) { 65 | return key 66 | } 67 | break 68 | case CONST.UI_FIELD.FIELD_CHECKBOX: // Yes/No but this should be made less language dependent 69 | // = YES, = NO, = toggle 70 | if ( key ==='y' || key ==='n' || key ===' ') { 71 | return key 72 | } 73 | break 74 | case CONST.UI_FIELD.FIELD_NUMBER: // Arbitrary decimal number. [@todo Could use limits] 75 | if ( (key >= '0') && (key <='9')) { 76 | return key 77 | } 78 | break 79 | case CONST.UI_FIELD.FIELD_COMBO: // Yeah. Could be interesting! Don't know how to control this 80 | return key // @todo Replace this placeholder. 81 | break 82 | } 83 | // update the display 84 | // update the source data 85 | return 0xff // Valid field but invalid key 86 | } 87 | 88 | // Set a hint string for the user 89 | setHint(hint) { 90 | // Probably should limit size and contents. Todo 91 | this.hint = hint 92 | } 93 | 94 | // Return The hint string 95 | getHint() { 96 | return this.hint 97 | } 98 | 99 | // Return true if (xp, yp) is in this field 100 | inField(xp, yp) { 101 | if ((xp >= this.xLoc) && (xp < this.xLoc + this.xWidth) && 102 | (yp >= this.yLoc) && (yp < this.yLoc + this.yHeight)) { 103 | return true 104 | } 105 | return false 106 | } 107 | 108 | 109 | } // uiField 110 | -------------------------------------------------------------------------------- /service.js: -------------------------------------------------------------------------------- 1 | /** service.js 2 | * Encapsulates a teletext service object 3 | * Environment: node.js server side script 4 | * @brief A service is a service name and a set of pages. 5 | */ 6 | 'use strict' 7 | // import logger 8 | const LOG = require('./log.js') 9 | 10 | /** Constructor 11 | * @param serviceName - a Service name such as BBCONE_2007_06_19 12 | */ 13 | global.Service = function (serviceName) { 14 | // member variables 15 | this.name = serviceName 16 | this.pages = [] 17 | 18 | // What do we want to do with a service? 19 | // 1) Keep pages in numerical order 20 | // 2) Load pages and save pages 21 | // 3) Expire pages and remove them 22 | /** Give a teletext page, inserts it into the local page list 23 | * @todo What do we do about a duplicate page? 24 | * We don't intend to cache pages forever so there won't ever be many pages to search through 25 | */ 26 | this.addPage = function (page) { 27 | if (this.findPage(page.pageNumber) === false) { 28 | this.pages.push(page) 29 | } 30 | } 31 | 32 | /** Seek the three digit page number that we are looking for 33 | * This is a part of a cacheing scheme so a missing page is not an errot 34 | * @return false if the page does not exist 35 | */ 36 | this.findPage = function (mpp) { 37 | LOG.fn( 38 | ['service', 'findPage'], 39 | `Looking for page=${mpp}`, 40 | LOG.LOG_LEVEL_VERBOSE 41 | ) 42 | 43 | // Page out of range? 44 | if (mpp < 0x100 || mpp > 0x7ff) { 45 | return false 46 | } 47 | 48 | // For each page in the service... 49 | for (let p = 0; p < this.pages.length; p++) { 50 | if (this.pages[p].pageNumber === mpp) { 51 | return this.pages[p] 52 | } 53 | } 54 | 55 | return false 56 | } 57 | 58 | /** Switcher. 59 | * If the message is for this service, send it to the pages 60 | */ 61 | this.keyMessage = function (key) { 62 | LOG.fn( 63 | ['service', 'keyMessage'], 64 | `Got a keymessage, name=${this.name}, data=${key.s}`, 65 | LOG.LOG_LEVEL_INFO 66 | ) 67 | 68 | LOG.fn( 69 | ['service', 'keyMessage'], 70 | `key data=${JSON.stringify(key, null, 4)}`, 71 | LOG.LOG_LEVEL_VERBOSE 72 | ) 73 | } 74 | 75 | /** Match the given name with the service name 76 | * @return true if the service name matches 77 | */ 78 | this.matchName = function (name) { 79 | return this.name === name 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /servicelist.js: -------------------------------------------------------------------------------- 1 | /** servicelist.js 2 | * Object that handles teletext services. 3 | * This maintains a list of services. 4 | * 5 | * The methods that it should handle are: 6 | * Add service 7 | * Delete service 8 | * Find service 9 | * doLoad should find the relevant service, then let service find the page, then the page will load the 10 | */ 11 | "use strict"; 12 | var fs = require('fs'); 13 | var readline = require('readline'); 14 | 15 | require('./service.js'); // teletext service 16 | 17 | // @todo: should server-side code be importing frontend files? 18 | require('./public/page.js'); // Page to put in a service 19 | 20 | // import logger 21 | const LOG = require('./log.js'); 22 | 23 | 24 | ServiceList = function (callback) { // This callback is used to send a page 25 | var data = { 26 | S: 'BBCNEWS/BBC', 27 | page: 100, // Page mpp 28 | s: 0, // subpage 0 29 | x: 0, // This signals that we should load the initial page (usually 100) 30 | y: 0, 31 | rowText: '', 32 | }; 33 | 34 | var targetPage = new TextPage(data); 35 | 36 | this.initialPage = 100; 37 | this.services = []; 38 | //this.callback=callback; 39 | 40 | /** Finds the service with the required name. 41 | * @return Index of service, or false; 42 | */ 43 | const findService = function (services, name) { 44 | LOG.fn( 45 | ['servicelist', 'findService'], 46 | `seeking ${name}`, 47 | LOG.LOG_LEVEL_VERBOSE, 48 | ); 49 | 50 | if (services.length === 0) { 51 | // No services 52 | return false; 53 | } 54 | 55 | for (var i = 0; i < services.length; i++) { 56 | if (services[i].matchName(name)) 57 | return i; 58 | } 59 | 60 | return false; // Not found 61 | }; 62 | 63 | /** 64 | * doLoad loads a page. 65 | * It loads the page into a service cache and then emits it to the client. 66 | * On subsequent loads, the cache is sent. 67 | * The cache also accepts updates from the client. 68 | * When updated, the updates are emitted to all the clients that are looking at this page. 69 | * The cache is written back to the disk periodically. 70 | * A long interval is used to expire a cache entry. If it is not being used then remove it. 71 | * A short interval is used to save the cache back to disk. This keeps the disk version up to date without saving after every single keystroke. 72 | */ 73 | this.doLoad = function (pageData) { 74 | targetPage.blank(); 75 | 76 | var data = pageData; // copy needed here??? 77 | 78 | // For some reason, services does not get initialised in the contructor. 79 | if (typeof this.services === 'undefined') { 80 | this.services = []; 81 | } 82 | 83 | LOG.fn( 84 | ['servicelist', 'doLoad'], 85 | `doing a load, pageData=${pageData}, data=${data}`, 86 | LOG.LOG_LEVEL_VERBOSE, 87 | ); 88 | 89 | // Find the filename that we want to load 90 | if (data.x === 2000) { 91 | data.p = 100;//initialPage; 92 | data.x = 0; 93 | } 94 | 95 | var filename = data.S + data.p + '.ttix'; // This is the actual filename that we want to load 96 | 97 | LOG.fn( 98 | ['servicelist', 'doLoad'], 99 | `filename=${filename}, services.length=${this.services.length}`, 100 | LOG.LOG_LEVEL_VERBOSE, 101 | ); 102 | 103 | // Which service object do we want to send it to? 104 | 105 | // Check if the page is already in cache 106 | var found = findService(this.services, data.S); 107 | if (found === false) { 108 | LOG.fn( 109 | ['servicelist', 'doLoad'], 110 | `Adding service, data.S=${data.S}`, 111 | LOG.LOG_LEVEL_VERBOSE, 112 | ); 113 | 114 | this.services.push(new Service(data.S)); // create the service 115 | found = this.services.length - 1; // The index of the service we just created 116 | } 117 | 118 | // Now we have a service name. Does it contain our page? 119 | var svc = this.services[found]; 120 | var page = svc.findPage(data.p); 121 | 122 | LOG.fn( 123 | ['servicelist', 'doLoad'], 124 | `Setting page to ${data.p}`, 125 | LOG.LOG_LEVEL_VERBOSE, 126 | ); 127 | 128 | targetPage.pageNumber = data.p; 129 | 130 | if (page === false) { 131 | LOG.fn( 132 | ['servicelist', 'doLoad'], 133 | `Page not found ${data.p}`, 134 | LOG.LOG_LEVEL_VERBOSE, 135 | ); 136 | 137 | // What happens here? The page is loaded from disk into cache and added to the page list 138 | var fail = false; 139 | 140 | var instream; 141 | instream = fs.createReadStream(filename); 142 | instream.on('error', function () { 143 | var data2 = { 144 | x: -1 // Signal a 404 error 145 | }; 146 | 147 | this.doLoad(data2); 148 | }); 149 | 150 | // Make a new page object 151 | targetPage = new TextPage(data); 152 | 153 | LOG.fn( 154 | ['servicelist', 'doLoad'], 155 | `Made a new page`, 156 | LOG.LOG_LEVEL_VERBOSE, 157 | ); 158 | 159 | rl = readline.createInterface({ 160 | input: instream, 161 | terminal: false, 162 | }); 163 | 164 | /** This procedure is called for each line itself until the whole page is read 165 | * We take this opportunity to populate a page object. 166 | */ 167 | rl.on('line', function (line) { 168 | if (line.indexOf('DE,') == 0) { // Detect a description row 169 | var desc = line.substring(3); 170 | 171 | //this.io.sockets.emit('description',desc); // Probably a huge mistake having this here !!!!!!! Move io back to top level/ 172 | 173 | LOG.fn( 174 | ['servicelist', 'doLoad', 'line'], 175 | `Description=${desc}`, 176 | LOG.LOG_LEVEL_VERBOSE, 177 | ); 178 | 179 | targetPage.setDescription(desc); 180 | 181 | } else if (line.indexOf('FL,') === 0) { // Detect a Fastext link 182 | var ch; 183 | var ix = 3; 184 | 185 | data.fastext = []; 186 | 187 | for (var link = 0; link < 4; link++) { 188 | var flink = ''; 189 | for (ch = line.charAt(ix++); ch != ',';) { 190 | flink = flink + ch; 191 | ch = line.charAt(ix++); 192 | } 193 | 194 | data.fastext[link] = flink; 195 | 196 | LOG.fn( 197 | ['servicelist', 'doLoad', 'line'], 198 | `Link ${link}=${flink}`, 199 | LOG.LOG_LEVEL_VERBOSE, 200 | ); 201 | } 202 | 203 | //this.io.sockets.emit('fastext',data); 204 | 205 | LOG.fn( 206 | ['servicelist', 'doLoad', 'line'], 207 | `fastext ${data}`, 208 | LOG.LOG_LEVEL_VERBOSE, 209 | ); 210 | 211 | return; 212 | 213 | } else if (line.indexOf('OL,') === 0) { // Detect a teletext row 214 | var p = 0; 215 | var ix = 3; 216 | var row = 0; 217 | 218 | var ch; 219 | ch = line.charAt(ix); 220 | if (ch != ',') { 221 | row = ch; 222 | } 223 | 224 | ix++; 225 | 226 | ch = line.charAt(ix); 227 | if (ch != ',') { 228 | row = row + ch; // haha. Strange maths 229 | ix++; 230 | } 231 | row = parseInt(row); 232 | 233 | ix++; // Should be pointing to the first character now 234 | 235 | } else { 236 | // Not a row. Not interested 237 | return; 238 | } 239 | 240 | data.y = row; 241 | // @todo - Handle strings shorter than 40 characters 242 | 243 | // Here is a line at a time 244 | var result = ''; 245 | for (var i = 0; i < 40; i++) { 246 | var ch = line.charAt(ix++); 247 | if (ch === '\u001b') { // Prestel escape 248 | ch = line.charAt(ix++).charCodeAt(0) - 0x40;// - 0x40; 249 | ch = String.fromCharCode(ch); 250 | } 251 | 252 | data.k = ch; 253 | data.x = i; 254 | // io.sockets.emit('keystroke', data); 255 | result += ch; 256 | } 257 | 258 | data.y = row; 259 | data.rowText = result; 260 | 261 | // this.io.sockets.emit('row',data); 262 | targetPage.setRow(data.p, data.y, data.rowText); 263 | targetPage.pageNumber = data.p; // having trouble setting this and making it stick 264 | 265 | }.bind(this)); 266 | 267 | // Handler for the close event when this file is finished. 268 | rl.on('close', function () { 269 | callback(data, targetPage); 270 | }.bind(this).bind(targetPage)); 271 | 272 | } else { 273 | LOG.fn( 274 | ['servicelist', 'doLoad'], 275 | `Setting page to ${data.p}`, 276 | LOG.LOG_LEVEL_VERBOSE, 277 | ); 278 | 279 | targetPage.pageNumber = data.p; 280 | } 281 | }; 282 | }; 283 | -------------------------------------------------------------------------------- /update-service-pages.js: -------------------------------------------------------------------------------- 1 | // update-service-pages.js 2 | // - Fetches / updates teletext services pages from remote repositories to a local location (as defined in config.js) 3 | // How to auto run this... 4 | // pm2 start update-service-pages.js --cron "*/5 * * * *" --no-autorestart 5 | // by Danny Allen (me@dannya.com) 6 | // 7 | // When problem solving, remove the pm2 process and run this manually and see if it crashes. 8 | // $ pm2 delete update-service-pages.js 9 | // $ node update-service-pages.js -v 10 | 'use strict' 11 | const fs = require('fs') 12 | const path = require('path') 13 | 14 | const commandLineArgs = require('command-line-args') 15 | const commandLineUsage = require('command-line-usage') 16 | const colorette = require('colorette') 17 | 18 | const svn = require('@taiyosen/easy-svn') 19 | const git = require('simple-git') 20 | git().clean(git.CleanOptions.FORCE) 21 | 22 | const deletedDiff = require('deep-object-diff').deletedDiff 23 | const xxhash = require('@pacote/xxhash') 24 | 25 | // import package.json so we can get the current version 26 | const PACKAGE_JSON = JSON.parse( 27 | fs.readFileSync('./package.json') 28 | ) 29 | 30 | // import constants and config for use server-side 31 | const CONST = require('./constants.js') 32 | const CONFIG = require('./config.js') 33 | 34 | // variables 35 | const PAGE_FILE_EXT = '.tti' 36 | 37 | const FILE_ENCODING_INPUT = CONST.ENCODING_ASCII 38 | const FILE_ENCODING_OUTPUT = CONST.ENCODING_ASCII 39 | 40 | const FILE_CHAR_REPLACEMENTS = { 41 | '\x8d': '\x1bM' 42 | } 43 | 44 | const DESCRIPTION_NULLIFY = [ 45 | 'Description goes here' 46 | ] 47 | 48 | // parse command line options 49 | const availableOptions = [ 50 | { 51 | name: 'help', 52 | description: 'Print this usage guide.', 53 | alias: 'h', 54 | type: Boolean 55 | }, 56 | 57 | { 58 | name: 'silent', 59 | description: 'No log messages output to the console.', 60 | alias: 's', 61 | type: Boolean 62 | }, 63 | { 64 | name: 'verbose', 65 | description: 'Verbose log messages output to the console.', 66 | alias: 'v', 67 | type: Boolean 68 | }, 69 | 70 | { 71 | name: 'force', 72 | description: 'Force an update of all services, regardless of last update time', 73 | alias: 'f', 74 | type: Boolean 75 | } 76 | ] 77 | 78 | const options = commandLineArgs(availableOptions) 79 | 80 | if (options.help) { 81 | const usage = commandLineUsage( 82 | [ 83 | { 84 | header: 'update-service-pages.js', 85 | content: 'Fetches / updates teletext services pages from remote repositories to a local location (as defined in config.js)' 86 | }, 87 | { 88 | header: 'Options', 89 | optionList: availableOptions 90 | } 91 | ] 92 | ) 93 | 94 | console.log(usage) 95 | 96 | process.exit(0) 97 | } 98 | 99 | // initialise hasher 100 | const hasher = xxhash.xxh64(2654435761) 101 | 102 | let global_changed = [] 103 | 104 | /** For editable services, write back the pages 105 | */ 106 | async function readBackServices () { 107 | // For all the services 108 | global_changed = new Array() // If an editable service changed, then add it to the list of services not to be overwritten 109 | for (const serviceId in CONFIG[CONST.CONFIG.SERVICES_AVAILABLE]) { 110 | const serviceData = CONFIG[CONST.CONFIG.SERVICES_AVAILABLE][serviceId] 111 | // Filter only the editable services 112 | if (serviceData.isEditable) { 113 | console.log('[readBackServices] editable Service id = ' + serviceId) 114 | // cd /var/www/teletext-services 115 | // cp /var/www/private/onair//*.tti . 116 | 117 | /// /// copy changed onair pages back to teletext-service ////// 118 | const serviceRepoDir = path.join( 119 | CONFIG[CONST.CONFIG.SERVICE_PAGES_DIR], 120 | serviceId 121 | ) 122 | 123 | const serviceOnairDir = path.join( 124 | CONFIG[CONST.CONFIG.SERVICE_PAGES_SERVE_DIR], 125 | serviceId 126 | ) 127 | console.log('from:' + serviceOnairDir + ' to ' + serviceRepoDir) 128 | const util = require('util') 129 | const exec = util.promisify(require('child_process').exec) 130 | 131 | // Copy updated files 132 | async function cp (src, dest) { 133 | // const { stdout, stderr } = await exec('cp --update -v ' + src + ' ' + dest + '2>/dev/null || :') //fail silently the first time this is run 134 | const { stdout, stderr } = await exec('cp --update -v ' + src + ' ' + dest) 135 | console.log('src:', src) 136 | console.log('dest:', dest) 137 | console.log('stdout:', stdout) 138 | console.log('stderr:', stderr) 139 | if (stdout.length > 0) { 140 | console.log('A page file changed') 141 | global_changed.push(serviceId) 142 | } 143 | } 144 | cp(serviceOnairDir + '/*.tti', serviceRepoDir) 145 | 146 | /// /// Add pages to repo. Warning. Only Git repos can be made editable ////// 147 | console.log('Destination URL = ' + serviceData.updateUrl) 148 | git(serviceRepoDir) 149 | .add('*.tti') 150 | .commit('Muttlee auto commit v1', ['-a']) 151 | .push() 152 | } // editable service 153 | } // for each service 154 | } // readBackServices 155 | 156 | async function updateServices () { 157 | for (const serviceId in CONFIG[CONST.CONFIG.SERVICES_AVAILABLE]) { 158 | // console.log("Service id = " + serviceId); 159 | 160 | // If the read back changed something, then DO NOT let the repo update 161 | // or we risk losing a few keystrokes 162 | if (global_changed.includes(serviceId)) { // It changed? 163 | if (options.verbose) { 164 | console.log("Skipping update of " + serviceId) // Updating could overwrite, so don't do it 165 | } 166 | continue 167 | } 168 | 169 | const serviceData = CONFIG[CONST.CONFIG.SERVICES_AVAILABLE][serviceId] 170 | 171 | const serviceTargetDir = path.join( 172 | CONFIG[CONST.CONFIG.SERVICE_PAGES_DIR], 173 | serviceId 174 | ) 175 | 176 | const serviceServeDir = path.join( 177 | CONFIG[CONST.CONFIG.SERVICE_PAGES_SERVE_DIR], 178 | serviceId 179 | ) 180 | 181 | const serviceManifestFile = path.join( 182 | serviceServeDir, 183 | 'manifest.json' 184 | ) 185 | 186 | // read / initialise service manifest file 187 | let serviceManifest = {} 188 | 189 | if (fs.existsSync(serviceManifestFile)) { 190 | serviceManifest = JSON.parse( 191 | fs.readFileSync(serviceManifestFile) 192 | ) 193 | } else { 194 | // initialise 195 | serviceManifest.id = serviceId 196 | } 197 | 198 | if (!serviceManifest.pages) { 199 | serviceManifest.pages = {} 200 | } 201 | 202 | let shouldUpdate = false 203 | 204 | // if service has an update URL... 205 | if (serviceData.updateUrl) { 206 | // ...and was last updated outside of its updateInterval... 207 | shouldUpdate = ( 208 | !serviceData.updateInterval || 209 | !serviceManifest.lastUpdated 210 | ) 211 | 212 | if (serviceManifest.lastUpdated) { 213 | shouldUpdate = ((Date.parse(serviceManifest.lastUpdated) + (serviceData.updateInterval * 1000 * 60)) < Date.now()) 214 | if (options.verbose) { 215 | console.log( 216 | 'Service id = ' + serviceId + 217 | ' shouldUpdate = ' + shouldUpdate + 218 | ' mins to next update = ' + parseInt(((Date.parse(serviceManifest.lastUpdated) + (serviceData.updateInterval * 1000 * 60)) - Date.now()) / 60000) // minutes to next update 219 | ) 220 | } 221 | } 222 | 223 | if (shouldUpdate || options.force) { 224 | // ...use Subversion (or git) to checkout / update pages... 225 | const svnClient = new svn.SVNClient() 226 | svnClient.setConfig({ 227 | silent: !options.verbose 228 | }) 229 | 230 | if (!fs.existsSync(serviceTargetDir)) { 231 | if (!options.silent) { 232 | console.log( 233 | colorette.blueBright( 234 | `First time checkout of '${serviceId}' service page files (to ${serviceTargetDir})...` 235 | ) 236 | ) 237 | } 238 | if (serviceData.repoType === 'svn') { 239 | // checkout svn service pages... 240 | await svnClient.checkout( 241 | serviceData.updateUrl, 242 | serviceTargetDir 243 | ) 244 | } else { 245 | // checkout git service pages... 246 | await git().clone( 247 | serviceData.updateUrl, 248 | serviceTargetDir 249 | ) 250 | } 251 | } else { 252 | if (!options.silent) { 253 | console.log( 254 | `Updating '${serviceId}' service page files...` 255 | ) 256 | } 257 | // update service pages... 258 | if (serviceData.repoType === 'svn') { 259 | await svnClient.update( 260 | serviceTargetDir 261 | ) 262 | } else { 263 | await git().pull( 264 | ) 265 | } 266 | } // update or pull repo 267 | } // if shouldupdate 268 | } // if has updateURL 269 | 270 | // The repos have now been updated 271 | // Now update the service folders 272 | 273 | // ensure service serve directory exists 274 | if (!fs.existsSync(serviceServeDir)) { 275 | // create directory 276 | if (options.verbose) { 277 | console.log( 278 | `Creating ${serviceServeDir} output directory` 279 | ) 280 | } 281 | 282 | fs.mkdirSync(serviceServeDir, { recursive: true }) 283 | } 284 | 285 | // copy service pages to serve directory, renamed to be compatible with server expectations 286 | if (!options.silent) { 287 | console.log( 288 | `\nChecking '${serviceId}' for updates...` 289 | ) 290 | } 291 | 292 | const recalculatedManifestPages = {} 293 | 294 | const servicePageFiles = fs.readdirSync(serviceTargetDir) 295 | let manifestChanged = false // Flag if the manifest needs rewriting 296 | 297 | for (const filename of servicePageFiles) { 298 | // if (options.verbose) { 299 | // console.log( 300 | // `\nChecking 'file ${filename}' for updates...` 301 | // ) 302 | // } 303 | if (filename.endsWith(PAGE_FILE_EXT)) { 304 | // determine full source filepath 305 | const sourceFilePath = path.join( 306 | serviceTargetDir, 307 | filename 308 | ) 309 | 310 | // read file content as a string 311 | let fileContent = fs.readFileSync( 312 | sourceFilePath, 313 | FILE_ENCODING_INPUT 314 | ).toString() 315 | 316 | // hash the original file content string 317 | hasher.reset() 318 | const fileContentHash = hasher.update(fileContent).digest('hex') 319 | 320 | // make specified character replacements 321 | for (const char in FILE_CHAR_REPLACEMENTS) { 322 | fileContent = fileContent.replace(char, FILE_CHAR_REPLACEMENTS[char]) 323 | } 324 | 325 | // hash the modified (above) file content string 326 | hasher.reset() 327 | const fileContentUpdatedHash = hasher.update(fileContent).digest('hex') 328 | 329 | // attempt to extract useful data items from the file content... 330 | let description 331 | let pageNumber 332 | 333 | const fileContentLines = fileContent.split('\n') 334 | 335 | for (const i in fileContentLines) { 336 | if (fileContentLines[i].startsWith('DE,')) { 337 | description = fileContentLines[i].slice(3) 338 | } 339 | 340 | if (fileContentLines[i].startsWith('PN,')) { 341 | pageNumber = fileContentLines[i].slice(3, 6) 342 | } 343 | } 344 | 345 | // normalise the description data item 346 | if (!description) { 347 | description = null 348 | } else { 349 | description = description.trim() 350 | 351 | if (!description || DESCRIPTION_NULLIFY.includes(description)) { 352 | description = null 353 | } 354 | } 355 | 356 | // if we have a valid page number... 357 | if (pageNumber) { 358 | if (recalculatedManifestPages[pageNumber]) { 359 | console.log( 360 | colorette.redBright( 361 | `ERROR: p${pageNumber} already defined in ${recalculatedManifestPages[pageNumber].f}, please fix this in ${filename} (change to an unused page number)` 362 | ) 363 | ) 364 | 365 | continue // next service page file 366 | } 367 | 368 | // if no changes to this page file, no further processing needed... 369 | if (serviceManifest.pages[pageNumber] && (serviceManifest.pages[pageNumber].oh === fileContentHash)) { 370 | // add unmodified manifest page object into processed data structure 371 | recalculatedManifestPages[pageNumber] = serviceManifest.pages[pageNumber] 372 | 373 | continue // next service page file 374 | } 375 | manifestChanged = true // something changed 376 | 377 | // if changes have been made to this page file, freshly recreate its manifest page object 378 | const manifestPageEntry = { 379 | f: filename, 380 | p: pageNumber, 381 | oh: fileContentHash 382 | } 383 | 384 | if (fileContentUpdatedHash !== fileContentHash) { 385 | manifestPageEntry.nh = fileContentUpdatedHash 386 | } 387 | 388 | if (description) { 389 | manifestPageEntry.d = description 390 | } 391 | 392 | recalculatedManifestPages[pageNumber] = manifestPageEntry 393 | 394 | // determine new filename and target file path 395 | const targetFilePath = path.join( 396 | serviceServeDir, 397 | filename 398 | ) 399 | 400 | // write file contents out to file 401 | try { 402 | fs.writeFileSync( 403 | targetFilePath, 404 | fileContent, 405 | FILE_ENCODING_OUTPUT 406 | ) 407 | 408 | if (options.verbose) { 409 | console.log( 410 | `p${pageNumber} (${filename}) has changed, copied to live` 411 | ) 412 | } 413 | } catch (err) { 414 | if (!options.silent) { 415 | console.error(err) 416 | } 417 | } 418 | 419 | // update the last modified timestamp 420 | serviceManifest.lastModified = new Date() // [!] Not sure if we need to do something with manifestChanged at this point 421 | } else { 422 | // page number could not be extracted 423 | if (!options.silent) { 424 | console.log(`ERROR: Page number could not be extracted from ${sourceFilePath}`) 425 | } 426 | } 427 | } 428 | } // for all servicePageFiles (continue resumes here) 429 | 430 | // if pages have been removed from the repository since the last run, also delete them from the target directory 431 | const deletedPageFiles = deletedDiff(serviceManifest.pages, recalculatedManifestPages) 432 | 433 | if (Object.keys(deletedPageFiles).length > 0) { 434 | for (const pageNumber in deletedPageFiles) { 435 | try { 436 | fs.unlinkSync( 437 | path.join(serviceServeDir, serviceManifest.pages[pageNumber].f) 438 | ) 439 | 440 | if (options.verbose) { 441 | console.log( 442 | `Page removed from source, deleting p${pageNumber} (${serviceManifest.pages[pageNumber].f})` 443 | ) 444 | } 445 | } catch (err) {} 446 | } 447 | 448 | // update the last modified timestamp 449 | serviceManifest.lastModified = new Date() 450 | manifestChanged = true 451 | } 452 | 453 | // update manifest fields 454 | if (manifestChanged) { 455 | serviceManifest.systemName = PACKAGE_JSON.name 456 | serviceManifest.systemVersion = PACKAGE_JSON.version 457 | serviceManifest.lastUpdated = new Date() 458 | if (options.verbose) { 459 | console.log( 460 | 'Manifest updated - lastUpdated = ' + serviceManifest.lastUpdated 461 | ) 462 | } 463 | 464 | if (serviceData.updateInterval) { 465 | serviceManifest.updateInterval = serviceData.updateInterval 466 | } 467 | 468 | serviceManifest.pages = recalculatedManifestPages 469 | 470 | // write out updated service file manifest 471 | fs.writeFileSync( 472 | serviceManifestFile, 473 | JSON.stringify( 474 | serviceManifest 475 | ) 476 | ) // write out manifest 477 | } 478 | } // for each service (contnue here if there are no manifest updates) 479 | } // updateServices 480 | 481 | // For editable pages, write them back to the repo 482 | const changed = readBackServices() 483 | // run update function 484 | updateServices(changed) 485 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | /** < Miscellaneous utilities for teletext 2 | * \author Peter Kwan 2018. 3 | */ 4 | 'use strict' 5 | /** < De-escape Prestel style 7 bit encoding. 6 | * A Prestel encoded string is escaped so that 7 | * it only needs 7 bit characters. 8 | * It does this by taking control code characters 9 | * and writing them as followed by the code 10 | * plus 0x40. 11 | * \param str - Prestel encoded string 12 | */ 13 | 14 | require('./hamm.js') // Hamming decoding 15 | 16 | global.DeEscapePrestel = function (str) { 17 | let result = '' 18 | 19 | for (let i = 0; i < str.length; i++) { 20 | let ch = str.charAt(i) 21 | 22 | // Prestel escape 23 | if (ch === '\u001b') { 24 | ch = str.charAt(++i).charCodeAt(0) - 0x40 25 | ch = String.fromCharCode(ch & 0x7f) 26 | } 27 | 28 | result += ch 29 | } 30 | 31 | return result 32 | } 33 | 34 | /** < Escape Prestel style 7 bit encoding. 35 | * A Prestel encoded string is escaped so that 36 | * Control code characters (<' ') 37 | * are written as followed by the code plus 0x40. 38 | * \param str - Raw teletext string 39 | */ 40 | global.EscapePrestel = function (str) { 41 | let result = '' 42 | 43 | for (let x = 0; x < str.length; x++) { 44 | const ch = str.charAt(x) 45 | 46 | if (ch.charCodeAt(0) < 32) { 47 | result = result + '\u001b' + String.fromCharCode((ch.charCodeAt(0) & 0x7f) | 0x40) 48 | } else { 49 | result += ch 50 | } 51 | } 52 | 53 | return result 54 | } 55 | 56 | /** < Extract triplets from X26, X27 or X28 57 | */ 58 | /* 59 | global.DecodeRowOfTriplets = function(X28) { 60 | let triplets = [] 61 | for (let ix = 0; ix<13; ix++) { 62 | // Extract the triplet 63 | let i = ix * 3 64 | let ch1 = X28[i].charCodeAt() 65 | let ch2 = X28[i+1].charCodeAt() 66 | let ch3 = X28[i+2].charCodeAt() 67 | let triplet = ch1*0x10000 + ch2*0x100 + ch3 // NAH! This is backwards 68 | console.log("Triplet["+ix+"] = " + parseInt(triplet,16) + " " + parseInt(ch1,16) + " " + parseInt(ch2,16) +" " + parseInt(ch3,16)) 69 | triplets.push(triplet) 70 | } 71 | return triplets 72 | } 73 | */ 74 | 75 | /** < Decode X28/0 format 1 packet 76 | * This packet controls appearance especially colours 77 | * Given the X28 payload of 39 characters arranged as 13 triplets, 78 | * it decodes the triplets then extracts the relevant data. 79 | * @return data as an object containing individual parameters 80 | * or -1 if it fails 81 | * @param rowText The text string from the OL,28 packet 82 | * eg. @@@|_yCu_@|wKpZA`UBsxLcz}ww]_}_wmg} 83 | */ 84 | global.DecodeOL28 = function(rowText) { 85 | 86 | // Thirteen triplets with 18 bits each 87 | let triples = [] 88 | for (let i = 0; i < 13; i++) { 89 | let a = rowText[i*3+1].charCodeAt() & 0x3f 90 | let b = rowText[i*3+2].charCodeAt() & 0x3f 91 | let c = rowText[i*3+3].charCodeAt() & 0x3f 92 | let x = (c << 12) | (b << 6) | a 93 | // console.log("rowText = " + rowText) 94 | // console.log("Decoded [" + i + "] = " + x.toString(16)) 95 | triples.push(x) 96 | } 97 | 98 | if (false) { 99 | let result = " " 100 | let s = "" 101 | for (let i=0; i<13; ++i) { 102 | s+=hex(triples[i],5)+" " // [!] hex is p5js function 103 | } 104 | // console.log ("triples dec = " + s) 105 | } 106 | 107 | // Now pick the bones. See Page 32 Table 4 for X/28 values 108 | let result = {} 109 | let dc = rowText[0] & 0x3f // designation code 110 | result.dc = dc 111 | 112 | // Bits we are ignoring for now but we need to preserve them 113 | result.pageFunction = triples[0] & 0x0f // t1, 1..4 114 | result.pageCoding = (triples[0] >> 4) & 0x07 // t1, 5..7 115 | result.defaultG0G2CharacterSet = (triples[0] >> 7) & 0x7f // t1, 8..14 116 | result.secondG0G2CharacterSet = ( // t1, 15..18, t2, 1-3 117 | ((triples[0] >> 14) & 0x0f) | ((triples[1] & 0x07) << 4) 118 | ) 119 | 120 | // Colour mappings 121 | result.colourMap = [] // t2 11-18, t3-t12 1-18, t13 1-4 122 | let bitIndex = 10 123 | let tripletStart = 1 124 | let colour = 0 125 | for (let i = 0; i<16*3; i++) { // 16 x R, G, B 126 | // work out the indices 127 | let startBit = (i * 4) + bitIndex 128 | let tripletIndex = tripletStart + Math.trunc(startBit / 18) 129 | startBit = startBit % 18 130 | let colorIndex = Math.trunc(i / 3) // CLUT 0/1 for dc === 4 131 | let colourValue = i % 3 // RGB 132 | let clutIx = 1 // CLUT 0/1 where dc === 4 133 | if (i < (8*3)) { 134 | clutIx = 0 135 | } 136 | if (dc === 0) { // CLUT 2/3 for dc === 0 137 | clutIx = clutIx + 2 138 | } 139 | // extract the four bit colour value 140 | let t = triples[tripletIndex] // Get the triplet 141 | t = (t >> startBit) & 0x0f // Shift and mask 142 | // does the data cross a triplet boundary? (ie. the bits go past 18) 143 | if (startBit > 14) { 144 | let split = 18 - startBit // This is always 2! Could assert that 145 | let t2 = triples[tripletIndex + 1] & 0x03 // Triplets only ever break on two bits 146 | t2 = t2 << split 147 | t = t | t2 148 | } 149 | colour = colour | t << ((2-colourValue) * 4) 150 | if (colourValue === 2) { // Done an RGB value 151 | result.colourMap.push(colour) 152 | colour = 0 153 | } 154 | } 155 | 156 | // Screen colour remapping 157 | result.defaultScreenColour = (triples[12] >> 4) & 0x1f // t13, 5..9 158 | result.defaultRowColour = (triples[12] >> 9) & 0x1f // t13, 10..14 159 | result.blackBackgroundSubRow = (triples[12] >> 14) & 0x01 // t13, 15 160 | result.colourTableRemapping = (triples[12] >> 15) & 0x07 // t13, 16..18 161 | // left and right extension panels 162 | result.enableLeftPanel = (triples[1] & 0x08) > 0 // t2, 4 163 | result.enableRightPanel = (triples[1] & 0x10) > 0 // t2, 5 164 | result.sidePanelStatusFlag = (triples[1] & 0x20) > 0 // t2, 6 165 | result.leftColumns = (triples[1] >> 6) & 0x0f // t2, 7..10 166 | // result.rightColumns = (triples[12]) Implied. Always 16-leftColumns 167 | 168 | if (false) { 169 | console.log(result) 170 | for (let i=0; i<8; i++) { 171 | console.log(result.colourMap[i].toString(16)) 172 | } 173 | } 174 | return result 175 | } // DecodeOL28 176 | 177 | /** < Encode X28/0 format 1 data into a tti OL,28 packet 178 | * Packs the colour palette and colour remappings into triplets 179 | * @return OL,28 line or -1 if it fails 180 | */ 181 | global.EncodeOL28 = function(data) { 182 | let triples = Array.apply(0, {length: 13}) 183 | for (let i=0; i<13; i++) { 184 | triples[i]=0 185 | } 186 | 187 | /** AddX28 188 | * Places bitCount bits of value into the triple[tripleIndex] and can 189 | * overflow into the next triple if needed. 190 | * @param value : Data to add to the packet 191 | * @param tripleIndex : Number of triple that the value starts in 1..13 192 | * @param bitIndex : The bit offset where the value starts in the triple 193 | * @param bitCount : The number of bits to use from value 194 | */ 195 | let AddX28 = function(value, tripleIndex, bitIndex, bitCount) { 196 | // Mask off bitCount bits 197 | let mask = (1 << bitCount) - 1 198 | let v2 = value & mask 199 | if (value == undefined) { 200 | // console.log("Bad args " + tripleIndex + " " + bitIndex + " " + bitCount) 201 | } 202 | // console.log("AddX28 enters value = " + hex(v2,3)+ " masked = " + hex(v2,6)) 203 | 204 | // Shift to the required bit index and trim any overflow 205 | v2 = (v2 << (bitIndex-1)) & 0x3ffff 206 | triples[tripleIndex-1] |= v2 207 | // console.log ("v2 = " + hex(v2, 6) + " triples[i] = " + hex(triples[tripleIndex-1], 5)) 208 | 209 | // Overflow of high bits goes into the next triple 210 | if ((bitIndex + bitCount) > 18) { 211 | v2 = value >>= 18 - bitIndex + 1 // 212 | // console.log("v2 overflow = " + hex(v2,3)) 213 | triples[tripleIndex] |= v2 214 | } 215 | } 216 | 217 | // Work our way along the packet 218 | AddX28(data.pageFunction, 1, 1, 4) // 1: 1-4 Page function. 4 bits 219 | AddX28(data.pageCoding, 1, 5, 3)// 1: 5-7 Page coding. 3 bits 220 | // @todo Implement X28 character sets 221 | AddX28(data.defaultG0G2CharacterSet, 1, 8, 7) // 1: 8-14 Default G0 and G2 character set designation. 7 bits 222 | AddX28(data.secondG0G2CharacterSet, 1, 15, 7) // 1: 15-18, 2: 1-3 Second G0 Set designation 223 | AddX28(data.enableLeftPanel, 2, 4, 1) 224 | AddX28(data.enableRightPanel, 2, 5, 1) 225 | AddX28(data.enableSidePanelStatus, 2, 6, 1) 226 | AddX28(data.leftColumns, 2, 7, 4) 227 | // 2: 11-18, 3:1-18, 13: 1-4 228 | // 16x12 bit values 229 | let tr=2 // triple 230 | let bi = 11 // bit offset 231 | for (let colourix=0; colourix<16; ++colourix) { 232 | let c = data.colourMap[colourix] 233 | // Need to swap red and blue because X28 does colours backwards 234 | let colour = ((c & 0x00f) << 8) | (c & 0x0f0) | (c & 0xf00) >> 8 235 | AddX28(colour, tr, bi, 12) 236 | // console.log ("triple: "+tr+" bit: "+(bi+1)) 237 | bi += 12 238 | if (bi >= 18) { 239 | bi = bi - 18 240 | tr++ 241 | } 242 | } 243 | 244 | AddX28(data.defaultScreenColour, 13, 5, 5) // t13 5..9 245 | AddX28(data.defaultRowColour, 13, 10, 5) // t13 10..14 246 | AddX28(data.blackBackgroundSubRow, 13, 15, 1) // t13 15 247 | AddX28(data.colourTableRemapping, 13, 16, 3) // t13 16..18 248 | 249 | if (false) { 250 | let result = " " 251 | let s = "" 252 | for (let i=0; i<13; ++i) { 253 | s+=hex(triples[i],5)+" " 254 | } 255 | console.log("triples enc = " + s) 256 | } 257 | 258 | // Pack the triples into a tti OL,28 259 | let result = "" 260 | result += String.fromCharCode(0 | 0x40) 261 | for (let tr=0; tr<13; ++tr) { 262 | let t = triples[tr] 263 | result += String.fromCharCode( (t & 0x3f) | 0x40 ) 264 | result += String.fromCharCode( ((t>>6) & 0x3f) | 0x40 ) 265 | result += String.fromCharCode( ((t>>12) & 0x3f) | 0x40 ) 266 | } 267 | // console.log ("result = " + result) 268 | return result 269 | } // EncodeOL28 270 | -------------------------------------------------------------------------------- /weather.js: -------------------------------------------------------------------------------- 1 | /**--/Weather Stuff from Darren Storer /--**/ 2 | "use strict"; 3 | var request=require('request'); 4 | 5 | 6 | global.Weather = function(callBack) { 7 | 8 | var fs = require('fs'); 9 | 10 | var _callback=callBack; 11 | 12 | // io stream 13 | //var fs=require('fs'); 14 | //var readline=require('readline'); 15 | //var stream=require('stream'); 16 | 17 | var gResponse; 18 | 19 | 20 | // Request weather from Darren Storer's server 21 | this.doLoadWeather = function (req,res) { 22 | gResponse=res; 23 | var weatherdata="http://g7lwt.com/realtime.txt"; 24 | request.get(weatherdata, this.gotWeather); 25 | } 26 | 27 | function myPage(w) { 28 | var page="DE,Weather data courtesy of Darren Storer\r\n\ 29 | DS,inserter\r\n\ 30 | SP,E:\dev\muttlee\weather.tti\r\n\ 31 | CT,8,T\r\n\ 32 | PS,8000\r\n\ 33 | RE,0\r\n\ 34 | PN,41000\r\n\ 35 | SC,0000\r\n\ 36 | OL,0,XXXXXXXXTEDFAX mpp DAY dd MTH C hh:nn.ss\r\n\ 37 | OL,1,SxCWEATHERC"+w[1]+"S$\r\n\ 38 | OL,2,Q|||C C in out feels like \r\n\ 39 | OL,3,Q|||GTempBG"+w[22]+"BG"+w[2]+"RBG"+w[54]+"BG \r\n\ 40 | OL,4,Qj|||||||||||||||||||||||||||\r\n\ 41 | OL,5,Q|||C mph now ave. gust dir\r\n\ 42 | OL,6,Q|||GWindFG"+w[6]+"FG"+w[5]+"FG"+w[32]+"FG"+w[11]+"\r\n\ 43 | OL,7,Qj|||||||||||||||||||||||||||\r\n\ 44 | OL,8,Q|||C % in out dew \r\n\ 45 | OL,9,Q|||GHum EG"+w[23]+"%EG"+w[3]+"%EG"+w[4]+"EG \r\n\ 46 | OL,10,Qj|||||||||||||||||||||||||||\r\n\ 47 | OL,11,Q|||C mm today hour \r\n\ 48 | OL,12,Q|||GRainDG"+w[9]+"DG "+w[8]+"DG DG \r\n\ 49 | OL,13,Qj|||||||||||||||||||||||||||\r\n\ 50 | OL,14,Q|||C hPa now trend \r\n\ 51 | OL,15,Q|||GPresAG"+w[10]+"AG"+w[18]+"AG AG \r\n\ 52 | OL,16,Qj|||||||||||||||||||||||||||\r\n\ 53 | OL,17,Q|||G \r\n\ 54 | OL,18,QGWind chill: "+w[24]+"C \r\n\ 55 | OL,19,QGHeat index: "+w[41]+" \r\n\ 56 | OL,20,QGUV Index : "+w[43]+" \r\n\ 57 | OL,21,QF]DWeather station: Loc: Herts \r\n\ 58 | OL,22,QF]Dhttp://g7lwt.com/realtime.txt \r\n\ 59 | OL,23,Q+] \r\n\ 60 | OL,24,ARefreshBFirst storyCHeadlinesFMain Menu\r\n\ 61 | FL,400,104,102,120,100,100"; 62 | 63 | console.log("got here"); 64 | if (gResponse!=0) 65 | { 66 | gResponse.writeHead(200, {'Content-Type': 'application/octet-stream'}); 67 | //Content-Disposition: attachment;filename=\"weather.tti\" 68 | gResponse.write(page); 69 | gResponse.end(); 70 | } 71 | else 72 | { 73 | var outstream; 74 | // var filename="BBCNEWS/BBC400.ttix"; 75 | var filename="/var/www/onair/p410.tti"; 76 | // var filename="i:/dev/onair/p410.tti"; 77 | 78 | outstream = fs.createWriteStream(filename); 79 | fs.writeFile(filename,page,function (err){ 80 | if (!err) 81 | { 82 | console.log("Page written OK"); 83 | var data={ 84 | S:0, 85 | p:0x410, 86 | s:0, 87 | y:1, 88 | x:1 // Signal that we can now render the page 89 | }; 90 | _callback(data); 91 | return; 92 | } 93 | else 94 | console.log('error='+err); 95 | }); 96 | 97 | } 98 | } 99 | // Got the weather, tokenise it and generate teletext 100 | this.gotWeather=function(error, res, body) { 101 | if (!error && res.statusCode==200) 102 | { 103 | var weather=body.split(' '); 104 | console.log("weather="+weather); 105 | } 106 | myPage(weather); 107 | } 108 | 109 | }; 110 | -------------------------------------------------------------------------------- /x28test.js: -------------------------------------------------------------------------------- 1 | /** < Temporary test for X28 decoding 2 | * \author Peter Kwan 2018. 3 | */ 4 | 'use strict' 5 | require('./utils.js') 6 | let X280="OL,28,@@@|g@@@cB|ps@@OgObOwLs_w}ww]_}_wM@p" // Correct colour Fore 2 Back 2 7 | let X281="OL,28,@@@|g@@@cB|ps@@OgObOwLs_w}ww]_}_wM@@" // Level 1 colours Fore 0 Back 0 8 | let X282="OL,28,@@@|g@@@cB|ps@@OgObOwLs_w}ww]_}_wM@t" // Black background flag 9 | let X283="OL,28,@@@|g@@@cB|ps@@OgObOwLs_w}ww]_}_wM@x" // Fore 2, Back 3 10 | 11 | let X284="OL,28,@@@|gpC@@@@@@@@@@@@@@pLs_w}ww]_}_wM@@" // red clut2/0 12 | let X285="OL,28,@@@|g@@@@@@@@@@@@@@@@pLs_w}ww]_}_wM@p" // black clut2/0 13 | 14 | // Why 7? The line command is 6 characters. The first byte is the designation code 15 | let r 16 | r=DecodeOL28(X284.substring(7),0) // Correct colour Fore 2 Back 2 17 | console.log(r.colourMap[0].toString(16)) 18 | r=DecodeOL28(X285.substring(7),0) // Correct colour Fore 2 Back 2 19 | console.log(r.colourMap[0].toString(16)) 20 | //DecodeOL28(X281.substring(7),0) // Level 1 colours Fore 0 Back 0 21 | //DecodeOL28(X282.substring(7),0) // Black background flag 22 | //DecodeOL28(X283.substring(7),0) // Fore 2, Back 3 23 | --------------------------------------------------------------------------------