![]() |
Forum Index : Microcontroller and PC projects : MMB4L: MMBasic for Linux version 0.7
Page 1 of 2 ![]() ![]() |
|||||
Author | Message | ||||
thwill![]() Guru ![]() Joined: 16/09/2019 Location: United KingdomPosts: 4252 |
Previous MMB4L release thread It's time to play the music. It's time to light the lights ... ![]() README.md ChangeLog Downloads for x86_64, aarch64, arm6vl + Game*Pack It seems highly unlikely I've managed to roll this all out correctly, but hopefully someone will let me know. Whilst I will attempt to respond to questions and bug reports within a couple of days I'm otherwise having a break so any requests for new or missing features will be recorded but probably not acted upon for several months. Best wishes, Tom The ChangeLog in full: Version 0.7 alpha 1 - 19-Jan-2025: - Added support for hi-res graphics: - The GRAPHICS command is used to create and manipulate up to 256 hi-res surfaces with ids 0 - 255: GRAPHICS BUFFER id, width, height - Creates an off-screen buffer surface. - Note that surface 0 cannot be a buffer. GRAPHICS CLS id [, colour] - Clears a surface. GRAPHICS COPY src_id TO dst_id [, when] [, transparent] - Copies one surface to another (c.f. PAGE COPY on the CMM2). GRAPHICS DESTROY { id | ALL } - Destroys a surface. GRAPHICS INTERRUPT id, {interrupt|0} - Sets or clears the interrupt handler for a window surface. GRAPHICS LIST - Lists all surfaces. GRAPHICS SPRITE id, width, height - Creates a sprite surface. - Note that all graphics primitive commands can be used on sprite surfaces; this is different to other MMBasics. - Note that surface 0 cannot be a sprite. GRAPHICS TITLE id, title$ - Sets the title of a window surface. GRAPHICS WINDOW id, width, height [, x] [, y] [, title$] [, scale] [, interrupt] - Creates a window surface. - Note that surface 0 can (only) be a window. GRAPHICS WRITE { id | NONE } - Selects the surface to direct other graphics commands to (c.f. PAGE WRITE on the CMM2). - The following standard MMBasic graphics commands are implemented including supporting array parameters where appropriate: ARC x, y, r1, [r2], arcrad1, arcrad2 [, colour] BLIT x1, y1, x2, y2, width, height [, src_id] [, flags] BLIT CLOSE [#]id BLIT CLOSE ALL BLIT COMPRESSED address, x, y [, transparent] BLIT MEMORY address, x, y [, transparent] BLIT READ [#]dst_id, x, y, width, height [, src_id] BLIT WRITE [#]src_id, x, y [, orientation] BOX x, y, w, h [, lw] [, c] [, fill] CIRCLE x, y, radius [, line_width] [, aspect_ratio] [, colour] [, fill] CLS [colour] - Note that if there is no current graphics write surface then this will clear the console. To clear the console explicitly use the CLS CONSOLE or CONSOLE CLEAR commands. DEFINEFONT [#]font_id FONT [#]font_id [, scale] GUI BITMAP x, y, bits [, width] [, height] [, scale] [, fcolour] [, bcolour] IMAGE RESIZE_FAST x, y, width, height, new_x, new_y, new_width, new_height [, src_id] [, flag] LINE x1, y1, x2, y2 [, width [, colour]] LINE GRAPH x(), y() [, colour] LINE PLOT ydata() [, nbr] [, xstart] [, xinc] [, ystart] [, yinc] [, colour] LOAD BMP file$ [, x] [, y] LOAD IMAGE file$ [, x] [, y] LOAD PNG file$ [, x] [, y] [, transparency_cut_off] PIXEL x, y [, colour] POLYGON n, x(), y() [, bordercolour] [, fillcolour] RBOX x, y, width, height [, radius] [, colour] [, fill] SPRITE CLOSE [#]id SPRITE CLOSE ALL SPRITE HIDE [#]id SPRITE INTERRUPT interrupt SPRITE HIDE ALL SPRITE HIDE SAFE [#]id SPRITE LOAD file$ [, start_sprite] [, colour_mode] SPRITE MOVE SPRITE NEXT [#]id, x, y SPRITE NOINTERRUPT SPRITE READ [#]id, x, y, w, h [, src_id] SPRITE RESTORE SPRITE SCROLL x, y [, colour] SPRITE SET TRANSPARENT rgb121_colour SPRITE SHOW [#]id, x, y, layer [, flags] SPRITE SHOW SAFE [#]id, x, y, layer [, flags] [, ontop] SPRITE WRITE [#]id, x, y [, flags] TEXT x, y, string$ [, alignment$] [, font] [, scale] [, fcolour] [, bcolour] TRIANGLE x1, y1, x2, y2, x3, y3 [, colour] [, fill] - The following standard MMBasic SPRITE functions are implemented: SPRITE(A, [#]id) SPRITE(C, [#]id [, m]) SPRITE(D, [#]id1, [#]id2) SPRITE(E, [#]id) SPRITE(H, [#]id) SPRITE(L, [#]id) SPRITE(N [, layer]) SPRITE(S) SPRITE(V, [#]id1, [#]id2) SPRITE(T, [#]id) SPRITE(W, [#]id) SPRITE(X, [#]id) SPRITE(Y, [#]id) - Added support for music and sound-fx playback. - The following standard MMBasic audio commands are supported: PLAY EFFECT file$ [, interrupt] PLAY FLAC file$ [, interrupt] PLAY MODFILE file$ [, sample_rate] [, interrupt] PLAY MODSAMPLE sample_num, channel_num [, volume] [, sample_rate] PLAY MP3 file$ [, interrupt] PLAY PAUSE PLAY PREVIOUS PLAY RESUME PLAY SOUND sound_no, channel_no, type [, frequency] [, volume] PLAY STOP PLAY TONE left [, right] [, duration] [, interrupt] PLAY VOLUME left [, right] PLAY WAV file$ [, interrupt] - Added support for up to 4 USB gamepads: - Commands: DEVICE GAMEPAD CLOSE id DEVICE GAMEPAD OPEN id [, interrupt] [, bitmask] DEVICE GAMEPAD VIBRATE id [, low_freq] [, high_freq] [, duration_ms] DEVICE GAMEPAD VIBRATE id OFF - Functions: DEVICE(GAMEPAD id, funct) MM.INFO$(GAMEPAD id) - Retrieves SDL mapping for gamepad. - Added OPTION SIMULATE to enable platform specific compatibility with other MMBasics. OPTION SIMULATE device$ - device$ is one of "Colour Maximite 2", "Game*Mite", "PicoMiteVGA", "MMBasic for Windows" or "MMB4L" to return to default behaviour. - Causes MM.DEVICE$, MM.INFO$(DEVICE) and MM.INFO$(PLATFORM) to return the appropriate simulated value. - If required MM.INFO$(DEVICE X) can be used to retrieve the real device name; this relies on the lenient error checking currently exhibited by other MMBasic platforms not reporting the "extraneous" X as a syntax error. Colour Maximite 2: - Simulates USB controllers 1-3 being read as if they were Wii Classic controllers attached to I2C channels 1-3: - USB1 = I2C3, USB2 = I2C1, USB1 = I2C2. - Commands: CONTROLLER CLASSIC CLOSE [i2c] CONTROLLER CLASSIC OPEN [i2c] [, interrupt] [, bitmask] MODE, PAGE COPY, PAGE SCROLL, PAGE WRITE - Functions: CLASSIC({B | LX | LY | RX | RY | L | R | T} [, i2c]) Game*Mite (RP2040): - Simulates sufficient GPIO to support USB controller 1 being read using SETPIN and PORT() as if it were the 8-button Game*Mite controller. - Commands: FRAMEBUFFER, BLIT FRAMEBUFFER, FLASH DISK LOAD, SETPIN - Functions: MM.INFO(CPUSPEED), MM.INFO(DRIVE), MM.INFO(FLASH ADDRESS), MM.INFO(PINNO GPxx), PORT() MMBasic for Windows: - As "Colour Maximite 2" except supports a single USB controller being read with the GAMEPAD command and function. PicoMiteVGA (RP2040): - Simulates sufficient GPIO to support USB controllers 1 & 2 being read using PIN, PULSE and SETPIN as if they were 12-button SNES controllers wired according to the PicoGAME VGA 2.0 schematic. - Commands: FRAMEBUFFER, BLIT FRAMEBUFFER, FLASH DISK LOAD, MODE, PIN, PULSE, SETPIN - Functions: MM.INFO(CPUSPEED), MM.INFO(DRIVE), MMM.INFO(FLASH ADDRESS), MM.INFO(PINNO GPxx), PIN() - Note that MODE 1 coloured tiles are not currently supported. Added other commands: ON PS2 {interrupt | 0} - As per PicoMite. OPTION ANGLE {DEGREES | RADIANS} - As per PicoMite. OPTION AUDIO {ON | OFF} - Persistent option to enable/disable audio. - Default ON. - The intended purpose of this OPTION is to allow MMB4L to be easily run on Linux devices (in particular Docker containers) where the SDL audio is too difficult for the user to be bothered to configure ;-). - Note that when OFF all the underlying audio boilerplate is still executed except for the bit that makes the noise and the calling of audio interrupts. OPTION AUTOSCALE {ON | OFF} - Persistent option to enable/disable automatic scaling of windows when using OPTION SIMULATE. - Default ON. - When ON the window for the simulated device will be scaled up (in whole number multipliers) to be the largest that will fit on the display. SETENV name$ = value$ - Synonym for existing SYSTEM SETENV. - Note that the value may also be an integer array containing a LONGSTRING. Added other functions: ATAN2(y, x) KEYDOWN(n) MM.INFO(PS2) - Added CONSOLE TITLE command as preferred synonym for CONSOLE SETTITLE so as to match the GRAPHICS TITLE command. - Added optional parameter to MM.INFO$(ERRMSG [errno%]) so it can now be used to retrieve the default message for any given 'errno%' as well as the current error message (when errno% is not specified). - Note that currently error numbers are not stable between MMB4L releases. - Changed DATE$(), DATETIME$(), DAY$() and TIME$() to be consistent with other MMBasics that do not have timezone support whilst returning local timezone values where most appropriate: TIME$() - returns current time in local timezone. DATE$() - returns current date in local timezone. DATETIME$(NOW) - returns current date/time in local timezone. DATETIME$(epoch) - returns date/time in UTC. DAY$(NOW) - returns current day in local timezone. DAY$(epoch) - returns day in UTC. Additionally: EPOCH(datetme$) - assumes the date/time is in UTC. - Changed MATH(SD a()) function to use "Sample Standard Deviation Formula" instead of "Population Standard Deviation Formula". - This matches PicoMite MMBasic v6.0.0. - Changed SORT to allow bit 2 of the 'flags' to be set when sorting strings so that empty strings are considered to have the "biggest" value. - This matches PicoMite MMBasic v0.6.0 - Fixed bug where ANSI control codes for setting the terminal title, showing the cursor, and resetting graphics styles and colours were written to STDOUT even when running without the MMBasic prompt. Edited 2025-01-20 01:39 by thwill MMBasic for Linux, Game*Mite, CMM2 Welcome Tape, Creaky old text adventures |
||||
Volhout Guru ![]() Joined: 05/03/2018 Location: NetherlandsPosts: 4920 |
Hi Tom, I'll give it a try. I am extremely positive about 0.6. It works rock stable in my automatic test setup on Ubuntu 20.04. Not a single glitch. I will run the same test program on 0.7 and see if that works as well, and similar my MODBUS controller. Then explore the new functions. Did you implement the whole PicoMite MATH package (including CRC, C_ADD and WINDOW) ? But what is Game*Pack architecture ? Volhout Edited 2025-01-20 02:43 by Volhout PicomiteVGA PETSCII ROBOTS |
||||
Amnesie Guru ![]() Joined: 30/06/2020 Location: GermanyPosts: 531 |
Woah cool! Does that mean MMBASIC will finally run on a raspberry pi? Greetings Daniel |
||||
thwill![]() Guru ![]() Joined: 16/09/2019 Location: United KingdomPosts: 4252 |
Hi folks, If by "implement" you mean "lift wholesale without testing from Peter's work" then I believe the answer is "yes". MATH should be up to date as of a couple of months ago ... though recall leaving some PID (?) stuff out. Sorry for the confusion, the Game*Pack download is a collection of curated games for MMB4L. We've had MMBasic on the Pis since Peter's now "abandoned" PiCroMite. What I suspect you are asking is "Do we have an up-to-date MMBasic that runs on the Pi and has access to the GPIO using MMBasic syntax?" to which the answer is still sadly "no". MMB4L WILL run on the Pi though I've only tested it on the original Pi Zero, Pi 3B and a Pi 4 CM - the graphics/audio are unusably slow on the Pi Zero. HOWEVER MMB4L has no convenient GPIO support though you can use the SYSTEM command to call out to the legacy sysfs interface to the GPIO. GPIO may come in time, though I'm not as selfless as Peter so I concentrate my efforts on things that I want (ability to write games on Linux that will then run on PicoMite VGA and Game*Mite) rather than what the more hardware orientated majority are interested in. Best wishes, Tom Edited 2025-01-20 04:42 by thwill MMBasic for Linux, Game*Mite, CMM2 Welcome Tape, Creaky old text adventures |
||||
lizby Guru ![]() Joined: 17/05/2016 Location: United StatesPosts: 3326 |
Thanks, Tom. Note, FWIW, that the latest Raspbian OS includes libgpiod, which is designed as a thread-safe replacement for the deprecated system sysfs calls to manipulate I/O (sysfs calls from MMB4L illustrated here ). This includes the tool programs, gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind As an example of the possibilities, gpioset --help provides: Usage: gpioset [OPTIONS] <chip name/number> <offset1>=<value1> <offset2>=<value2> ... Set GPIO line values of a GPIO chip and maintain the state until the process exits Options: -h, --help: display this message and exit -v, --version: display the version and exit -l, --active-low: set the line active state to low -B, --bias=[as-is|disable|pull-down|pull-up] (defaults to 'as-is'): set the line bias -D, --drive=[push-pull|open-drain|open-source] (defaults to 'push-pull'): set the line drive mode -m, --mode=[exit|wait|time|signal] (defaults to 'exit'): tell the program what to do after setting values -s, --sec=SEC: specify the number of seconds to wait (only valid for --mode=time) -u, --usec=USEC: specify the number of microseconds to wait (only valid for --mode=time) -b, --background: after setting values: detach from the controlling terminal Biases: as-is: leave bias unchanged disable: disable bias pull-up: enable pull-up pull-down: enable pull-down Drives: push-pull: drive the line both high and low open-drain: drive the line low or go high impedance open-source: drive the line high or go high impedance Modes: exit: set values and exit immediately wait: set values and wait for user to press ENTER time: set values and sleep for a specified amount of time signal: set values and wait for SIGINT or SIGTERM Note: the state of a GPIO line controlled over the character device reverts to default when the last process referencing the file descriptor representing the device file exits. This means that it's wrong to run gpioset, have it exit and expect the line to continue being driven high or low. It may happen if given pin is floating but it must be interpreted as undefined behavior. ~ Edited 2025-01-22 00:44 by lizby PicoMite, Armmite F4, SensorKits, MMBasic Hardware, Games, etc. on fruitoftheshed |
||||
PeteCotton![]() Guru ![]() Joined: 13/08/2020 Location: CanadaPosts: 529 |
Awesome work Tom! ![]() |
||||
thwill![]() Guru ![]() Joined: 16/09/2019 Location: United KingdomPosts: 4252 |
Is this libgpiod v1.6.x ... which confusingly is implemented by the library libgpiod2 on Debian Linux' ? I check every 6 months but consider the libgpiod saga to still be a s**t show: https://forums.raspberrypi.com/viewtopic.php?t=377766 I won't be doing anything in this area until API v2 is available as standard in the distributions ... where by the sounds of it, it will be implemented in a library named libgpiod3 !!! In the meantime I'm not entirely certain how you would go about using `gpioset` from MMB4L because of "... the state of a GPIO line controlled over the character device reverts to default when the last process referencing the file descriptor representing the device file exits ..." I think that suggests you need to keep the `gpioset` program running in the background which in turn I think means you need a way to attach to its stdout or get its pid so you can later stop it ... I suppose the latter may be possible using `SYSTEM` but I haven't the time or inclination to check myself. If anyone does have a play with this and need some enhancements to `SYSTEM` then let me know and I *might* be able to do something. Whilst we wait, and if/when I get my mojo back, my inclination is to use MMB4L as a test bed for the following features in order: 1. Overcome the 128 (127?) internal MMBasic functions limitation. 2. Implement structured types for MMBasic. 3. Implement exception handling for MMBasic. Obviously with the intention of making them available for PicoMite (or whatever the platform du jour is at the time). Best wishes, Tom MMBasic for Linux, Game*Mite, CMM2 Welcome Tape, Creaky old text adventures |
||||
LeoNicolas![]() Guru ![]() Joined: 07/10/2020 Location: CanadaPosts: 500 |
Tom I've been using MMB4L on my Knightmare game port, and it works perfectly. It is much more reliable than running MMB4W on wine. Thank you for your great work. Do you need any help with its development? |
||||
lizby Guru ![]() Joined: 17/05/2016 Location: United StatesPosts: 3326 |
Yes, and maybe--especially considering the state of documentation. It appears to me (as best I recollect from 3+ years ago--sketchy) that the things that were lacking then are available now: pull-up, pull-down, open collector, and some others (if actually implemented as the documentation implies). (I do not understand the actual practical implications of "the state of a GPIO line controlled over the character device reverts to default when the last process referencing the file descriptor representing the device file exits".) PicoMite, Armmite F4, SensorKits, MMBasic Hardware, Games, etc. on fruitoftheshed |
||||
JohnS Guru ![]() Joined: 18/11/2011 Location: United KingdomPosts: 4006 |
I think it means that, to set some state or other, it is achieved using a file descriptor (which may be shared among processes) and so will persist only as long as some process(es) keep that descriptor open (it will be auto-closed per process as each process exits). If you were to create the process from within MMB4L you would need to background it, because otherwise as it exits the file descriptor would be closed. The posted details (above) show a -b option which probably needs to be used. And then you need to keep track of the process ID, most likely, so you can kill it when appropriate. Or, find the API and use from MMB4L if you can - OPEN might or might not be up to the job. If you need IOCTL, however, I don't think MMB4L has it. John Edited 2025-01-22 04:38 by JohnS |
||||
Volhout Guru ![]() Joined: 05/03/2018 Location: NetherlandsPosts: 4920 |
Hi Tom, On Ubuntu 20.04 I also needed to switch to ALSA audio driver to get audio (the default pulse audio did not work at all, dead silence). Although not disturbing audible, there are some problems with syncing audio (tested on Flappy Bird) ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred As if speeds do not exactly line up. Regards, Volhout EDIT: It may be a CPU resource issue. When I move the graphics window (using the mouse on the window frame) actively around the messages are far more frequent, and audio starts hicking up / crackling. But in normal use, it is perfectly workable. This is an i5 laptop DELL Latitude E7450. Amazing that you actually made all these games work. Great job Tom. Even PETSCII robots. Wauw. Edited 2025-01-22 05:06 by Volhout PicomiteVGA PETSCII ROBOTS |
||||
thwill![]() Guru ![]() Joined: 16/09/2019 Location: United KingdomPosts: 4252 |
I'll leave most answers for tomorrow, but to run another process in the background and get its PID the current formulation (using xclock as an example) is: SYSTEM "xclock & echo $! > my_pid.txt" and then read the PID from "my_pid.txt". You CANNOT do it like this: SYSTEM "xclock & echo $!", output$ because that won't return until xclock is closed. Best wishes, Tom Edited 2025-01-22 05:24 by thwill MMBasic for Linux, Game*Mite, CMM2 Welcome Tape, Creaky old text adventures |
||||
lizby Guru ![]() Joined: 17/05/2016 Location: United StatesPosts: 3326 |
Tom--I looked back over our correspondence regarding GPIO and libgpiod, and on Nov 24, 2021 you wrote this: So three-plus years later, it's still V1.6 right now, with, I guess the same limitations we found then. Sorry to have created this diversion from your impressive new release. PicoMite, Armmite F4, SensorKits, MMBasic Hardware, Games, etc. on fruitoftheshed |
||||
thwill![]() Guru ![]() Joined: 16/09/2019 Location: United KingdomPosts: 4252 |
Thanks Leo. Can I ask what Linux flavour/version you are using? And did you have any problems with the default PulseAudio*? * My (limited) understanding is that depending on the Linux version it might actually have the newer PipeWire which provides a compatibility layer with the older PulseAudio API. Absolutely, if (where possible) you don't mind writing automated tests to accompany any contributions. There are already some issues filed: https://github.com/thwill1000/mmb4l/issues And also a lengthy TODO Much of the work consists of lifting code from the PicoMite repository, writing automated tests for it and sometimes massaging it into something a little less shonky. If you find something that takes your interest then let me know before you start on it so I can share my thoughts. Best wishes, Tom MMBasic for Linux, Game*Mite, CMM2 Welcome Tape, Creaky old text adventures |
||||
thwill![]() Guru ![]() Joined: 16/09/2019 Location: United KingdomPosts: 4252 |
Hi Volhout, I wonder if perhaps you don't have PulseAudio/PipeWire installed/running - this all fortunately "just worked" for me during development (on Ubuntu 24.04) ? What's the output of running the following? ps -axuww | grep -e pulse -e wire See https://github.com/thwill1000/mmb4l/issues/15 In summary, I saw it during development and I think I know how to fix it, but I haven't done so yet. Yes it is a resource issue, MMB4L isn't generating the sound samples fast enough when it isn't getting more than its fair share of the CPU time. Just to check, you don't have your "Power Mode" set to "Power Saver" do you ? I've found MMB4L works on a Pi3 which should be much less performant than your i5. Thanks. I actually fixed a bug and made some tweaks to PETSCII Robots. The easiest way to share this would be to make my "mmbasic-robots" github repository public (and add you as a co-contributor). What are your thoughts on this ? @Martin H. could I move Gems'N'Rocks into a public github repository ? Best wishes, Tom Edited 2025-01-22 21:15 by thwill MMBasic for Linux, Game*Mite, CMM2 Welcome Tape, Creaky old text adventures |
||||
Volhout Guru ![]() Joined: 05/03/2018 Location: NetherlandsPosts: 4920 |
Hi Tom, I will look at the power settings of the laptop tomorrow (when I am back home) and check for pulse wire. You are free to put the PETSCII public. But appart from that I would like to know what the problem, and what the fix was. Is that described somewhere ? Don't need extensive text, but just to understand, learn, and do better next time. Volhout PicomiteVGA PETSCII ROBOTS |
||||
thwill![]() Guru ![]() Joined: 16/09/2019 Location: United KingdomPosts: 4252 |
Thanks. See this post: https://www.thebackshed.com/forum/ViewTopic.php?PID=232880#232880#232880 Best wishes, Tom Edited 2025-01-22 22:58 by thwill MMBasic for Linux, Game*Mite, CMM2 Welcome Tape, Creaky old text adventures |
||||
LeoNicolas![]() Guru ![]() Joined: 07/10/2020 Location: CanadaPosts: 500 |
I'm using the Linux Mint 22.1. No problems at all with Pulse Audio. The sound is working perfectly. I will take a look at the source code and how the unit tests are structured. It will be a pleasure to contribute to the MMB4L project. |
||||
thwill![]() Guru ![]() Joined: 16/09/2019 Location: United KingdomPosts: 4252 |
![]() OK, note there are tests on two levels: * `googletest` unit-tests written in C++ for low-level stuff, e.g. https://github.com/thwill1000/mmb4l/blob/main/src/common/gtest/stack_test.cxx * `sptest` integration-tests written in MMBasic for higher-level stuff, e.g. https://github.com/thwill1000/mmb4l/blob/main/tests/tst_memory.bas In general if I'm implementing (copying from PicoMite) a new command or function I would write the latter sort of test as they are (a) simpler to write for testing commands/functions and (b) should be runnable on other MMBasic platforms which ensures consistency and gives back to the rest of the MMBasic community. Best wishes, Tom MMBasic for Linux, Game*Mite, CMM2 Welcome Tape, Creaky old text adventures |
||||
JohnS Guru ![]() Joined: 18/11/2011 Location: United KingdomPosts: 4006 |
I get sound. I have pipewire installed. The test above doesn't output anything with wire in it - should it? (Just a bit puzzled. All seems OK.) John |
||||
Page 1 of 2 ![]() ![]() |
![]() |
![]() |
The Back Shed's forum code is written, and hosted, in Australia. | © JAQ Software 2025 |