Sunday, 27 May 2018

Making an A frame for a Sailfish 18

I saw some pictures of the aluminium angle A frames on Marika and Brown Owl, and decided to try to make one, in order to make raising the mast quicker, safer and easier.

A frame ready to hoist mast:

The frame itself is made from 25mm aluminium angle, 3mm thick. The brackets are cut from 75mm x 3mm angle, and the bit at the bow that fits over the existing bow fitting is 4" x 1/4" angle.

The bracket fits under the back mounting of the pulpit. Unlike the other versions, I made the brackets to be as near in line fore and aft as possible.

Photos of one of the brackets loose on the deck, from different angles:




The starboard bracket after fitting:




Notice how the frame angle iron has been bent in a vice, so that it is parallel to the bracket - otherwise a lot of slop has to be left in the fixing bolt to allow the frame to rotate. This was arrived at late in the process - I should probably have bent it a bit more, so the fixing bolt could have been shorter.

There is another, smaller bend in the middle of the frame, to make it clear the pulpit easily.

The part that fits over the existing bow fitting:



I used 2 pieces of 4" angle - I initially tried to use 3", but it wasn't tall enough. With 4" it completely covers the bow fitting, and extends the full width of the bow, even on the port side (which is larger, because the existing bow fitting is offset to the starboard side).

I made it stick out at the front so I could move the existing bow roller onto the new fitting (although I think I will get a wider roller at some point, as I find the anchor line jumps off the roller onto the nuts). 



I used 3 10mm stainless bolts with nuts to space the sides apart, as this allows for adjustment to make it a perfect fit. I did try aluminium tubing as spacers, but found it too difficult to get them exactly the right length. The other bolts are 1 for the bow roller, 1 for the forestay attachment, 1 for the top pulleys, and 1 to hold the top pulley spacers  

I use a drop pin through the existing holes in the bow fitting to fix the frame in the down position. 

The pulley system:



I bought the pulley wheels on eBay - there is a vendor who obviously makes them any size to order. The lower pulley replaces the old bow roller. Offcuts of aluminium act as spacers, and stop the line jumping off the wheels. The right most bit of rope is the fixed end, and covers a nut which holds the axle bolt in place. The head of the bolt has been cut off, so it does not stick out either side of the old bow fitting. I cut a screwdriver slot in it instead, so I could hold it steady while tightening the nut against the bow fitting.

Triangle holding mast steadying line:


In addition to the teak block I have put under the tabernacle (so the mast can sit horizontal without cracking the hatch), I have added a couple of triangles of aluminium fixed to the cabin top handrail. These hold shackles which line up exactly with the drop pin which holds the mast in the tabernacle. Pre-stretched 6mm halyard line goes from these to the mast to hold it steady while raising and lowering. Because it's line rather than wire, it does allow the mast to move from side to side a little, but it can't fall sideways. I leave these lines, and the stays, connected all the time.

I also re-routed the jib furling line through a pulley and fairlead attached to the A frame, so the length doesn't change when the mast goes up and down.

I keep the foot of the forestay permanently attached to the A frame, so raising the mast only involves rolling it back, attaching the top of the forestay, rolling it back some more (which pulls the A frame to the position shown in the photo), attaching the foot, pulling the A frame raising line (helping the mast up a bit with the other hand to start with), cleating it off, and inserting the drop pin in the bow fitting. Oh, and tightening the stays.

The combination of the A frame, replacing a couple of shackles with clip shackles, and replacing the bolt which used to hold the mast foot with a drop pin, has cut time to launch by about 10-15 minutes.

If you have a Sailfish, or are thinking of buying one, can I heartily recommend the Sailfish Association. A great bunch of people, with a website, forum and Facebook page full of helpful advice.


Friday, 19 January 2018

Showing progress of BackupPC backup while it is happening

I have just finished upgrading my central linux gateway box from ClearOs 6 to 7. I took the opportunity to do a complete rebuild, and evaluate what apps I run on it.

I discovered BackupPC is a free option in the ClearOs Marketplace, so decided to try it, as the application I used before, CrashPlan, is no longer free.

It is quite handy, once you have understood how to configure it. I now have it set up to backup the gateway box itself, plus all my PCs. It keeps 3 weeks backups of essential files on a twice-daily basis, so I should be able to recover any version of any file for any day up to 3 weeks ago.

While setting it up, I wanted to monitor the progress of my initial backups, to make sure they were doing what I wanted. Unfortunately, BackupPC does not support this out of the box. The backup log is kept in a compressed file, in a slightly non-standard format.

However, with the aid of a little research, I found the file was just missing the gzip header, so was able to devise a shell script which would show me the log output (almost) as it happened.

Here is the script - call it with the name of the host being backed up, and it will tail the log file continuously.

#! /bin/bash
# Show progress of BackupPC backup
cd /var/lib/BackupPC/pc/$1
(printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" ; tail -f -c+0 XferLOG.z) | gzip -dc | tail -f

Wednesday, 25 October 2017

Xamarin cross platform apps for phones, etc.

I am starting to develop an app to run on phones and tablets, and, as I am familiar with C#, I decided to try Xamarin.

 It is an excellent environment, but full of pitfalls for the unwary.

 I installed Xamarin as part of Visual Studio 2017 Community Edition (which is free).

Unfortunately creating a new Xamarin project leaves you with a pile of NuGet packages, many of which are out of date, and some of which are installed in multiple versions.

Attempting to run the automatic update on all of them results in a cryptic error message.

You are trying to install this package into a project that targets 'MonoAndroid,Version=v7.1', but the package does not contain any assembly references or content files that are compatible with that framework. For more information, contact the package author. 

It appears that the versions of the Android NuGet packages you have to use depend on the version of the Android SDK you are targeting. Plus, as supplied, VS 2017 does not include an Android SDK which matches the latest NuGet packages.

There are 2 answers.

Either:

Upgrade your Visual Studio to the latest version, then go into Tools/Android/SDK Manager and update everything. Exit (it has probably updated itself) and re-enter. Repeat until all is up to date. Then make sure you have downloaded the latest SDK Platform. (You need to have all the SDK platforms between your Minimum Android Version and your Target Android Version, apparently.) Finally, adjust your Target Android Platform in your Android project properties, Application and Android Manifest tags, to the latest version. I understand it is best to select a specific version, rather than "Use latest".

Or:

Find the version of the SDK you are targeting, and upgrade all the Android packages individually by hand to the same major version number.

Wednesday, 22 July 2015

Setting up ClearOS as a Google Cloud Print server

I have ClearOs 6 running 24/7 as my main Internet-facing router, providing firewalling and routing for my LAN.

I also have a number of Android devices, and it has always annoyed me that Google Cloud Printing seemed to require a Windows PC switched on.

Some Googling revealed that a Linux CUPS server can be made to share its printers with Google Cloud Print. so I decided to try to set this up.

First, I installed the Advanced Print Server into ClearOs, from its Marketplace (it's free).

Then I set up my printer by following the instructions.

Then I logged on to the server as root, using putty.

I downloaded cloudprint from https://github.com/armooo/cloudprint, and unzipped it to /usr/share/cloudprint.

I installed the packages it needs (mostly worked out by trial and error)

yum install python python-pip gcc python-devel cups-devel

pip install pycups argparse cloudprint[daemon]

pip install --upgrade cloudprint[daemon]

cloudprint

(The square brackets are typed in as shown - they aren't code for an optional argument.)

This gave me a url to put into a web browser to add the printer to my Google Cloud.

In order to have this run automatically, in daemon mode, at startup, I added

cloudprint -d

to /etc/rc.d/rc.local 

That was pretty much it (aside from all the messing about finding out what was needed). Note that I did not do everything in one go, the way I have documented it, just installed each bit I needed when I found out what I had didn't work.

Hope this helps anyone else wanting to do it

Thursday, 9 July 2015

My accounting software is now available for download at http://www.trumphurst.com/accounts.php.

It is now Open Source on GitHub.

Thursday, 18 June 2015

Free Accounting Software

I have continued to work on my accounting software, while using it in my business and for my personal accounts (in separate databases).

I have added a number of new functions:

  • Import statements from QIF files
  • Copy and paste statement data from bank web pages
  • Investments (shares, unit trusts, etc.)
  • Import data from Quicken
I'm really keen to get some more beta testers - anyone running Windows or Linux want to try a free small business accounting or personal package to run on their own computer, but which they can access from any computer, phone or tablet?


Thursday, 9 April 2015

Quick Books replacement

I knew it was a while since I had posted anything on here, but I didn't realize how long! Comes of being too busy, I guess.

In my small business, I have been using Quick Books for years. Although when I started the business I had been working for an accounting software company, I still chose Quick books because it was simple and easy to use - no remembering obscure account codes or customer numbers, everything works by name.

Since upgrading to Windows 8, Quick Books has become significantly less functional - none of the pages that use the Internet Explorer web control work any more - they just come out blank. However, it is no longer possible to upgrade or buy Quick books - you have to subscribe to a web service instead.

Doing this was just not worthwhile for a small business like mine, so I had a look at alternatives. Unfortunately, none of the alternatives I could find would import my existing Quick Books data. As I know a lot about accounting software, and figured there would be lots of people in the same position as me, I decided I should write my own.

I have just completed a beta test version, which has the following features:

  • Imports data exported from Quick Books. Journals are exported using a Quick Books report. The only limitation is that all sub-accounts must have unique names, as Quick Books does not distinguish between sub-accounts and main accounts in its journal report.
  • Does all the basic accounting tasks:
    • Sales invoices, credit notes, payments
    • Customer maintenance
    • Product maintenance
    • Optional Days:Hours:Minutes or Hours:Minutes units on product quantities, to make life easier for people like me who do hourly billing.
    • Purchase invoices, credit notes and payments
    • Supplier maintenance
    • VAT
    • Banking, cheques, deposits, transfers, reconcile
    • Name maintenance
    • Accounting, journals, VAT return
    • Reminders
    • Memorised transactions
    • Import (from Quick Books, CSV or tab delimited files)
    • Data integrity check
    • Backup and restore data to json format
    • Audit trail shows details of every transaction added/deleted or changed
    • Payment history shows details of how each payment is allocated to invoices.
  • Provides helpful features like Quick Books:
    • Names (Customer, Supplier, Cheque payee, etc.) all either selected from drop-down lists, or use auto-complete (type part of a name, and choose the full name from a drop-down list).
    • Choosing an existing name for a payment, cheque, journal, etc. automatically populates the form with whatever values were used last time.
  • Uses either SQLite or MySQL database (set in a configuration file)
  • Runs as a Web Server (on port set in configuration file), so can be accessed by any web browser (Chrome recommended for best usability). Tablet and phone friendly, so you can use it from anywhere with access to the computer running the server.
  • Runs on Windows or Linux (uses Mono on Linux, .Net on Windows).
  • Customisable - the user interface is all in HTML/CSS/Javascript, so if you understand these you can customise the user interface however you like.


    I am therefore looking for people who would like to beta test the software. You get a free copy of the software, along with updates as bugs are identified and fixed. You also get free support via email during the beta test programme. Add a comment here, or contact me via my web site http://www.trumphurst.com.

    Tuesday, 19 March 2013

    0x8024402c the proxy server or target server names can't be resolved with Microsoft Security Essentials

    I found the above error on a Windows XP machine, when trying to update the virus definitions.

    I checked the Internet Explorer proxy settings - automatically determine was not checked, and there was no proxy setting.

    I also ran proxycfg -d, just to make sure.

    This still didn't fix it.

    In the end, I ran wireshark during the update attempt. I discovered that, despite the proxy settings being turned off, MSE was still querying the dns for host name wpad, and downloading the proxy setting script from http://wpad/wpad.dat.

    Which would have been fine, except that I had a stray CNAME record for wpad in my local DNS, that pointed to my local webserver, that had a stray wpad.dat file in it, which set the proxy to a machine that no longer exists!

    What was worse, the stray entry only existed in one of the two DNS servers on my network, so the problem was intermittent, depending on which DNS server Windows used for the query.

    Once I deleted the stray DNS entry, and removed the wpad.dat file from my web server, it all started working.

    The moral of the story is, if you experience the same problem, try typing http://wpad/wpad.dat into your web browser, and see if you get a file. If you do, open it with a text editor, and it will show you what proxy settings MSE will decide to use (even if you have told it not to)!

    Thursday, 3 January 2013

    Windows Vista Media Centre Program Guide disappeared on 1 Jan 2013

    I have had a Windows Media Centre almost since they were first invented. When I bought it, it ran Windows XP, but I "upgraded" it to Vista when Vista came out. (Although Vista had many problems, the Media Centre part was better than the XP version.)

    On 1st Jan 2013, the program guide suddenly stopped working. It claimed it had downloaded the guide, but every program had "No data available" on it.

    After some research on the 'Net, I discovered that the UK TV guide was downloaded from Broadcasting Data Services, and that Microsoft's agreement with them appeared to expire on 31 December 2012!

    I spent a whole day investigating what to do about this. Microsoft were no help at all. I considered upgrading to Windows 8, but the Microsoft free offer of a Media Centre registration key via email doesn't work (at least, I had no reply to the request I sent a week ago).

    The only obvious solution I found on the net was to use a separate program to extract the guide information from the broadcast EPG, and put it into Media Centre. To that end, I installed a program called EPG Collector from SourceForge, installed it, and got it working. Except that it couldn't put the data into Media Centre, with the error message "Could not load file or assembly 'mcepg, Version=6.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'".

    Further research, and a posting to the relevant forum, suggested I install something called the Microsoft TV Pack, an upgrade to Media Centre - this does not seem to be available from Microsoft, but I found it at http://digiex.net/guides-reviews/guides-tutorials/media-guides/699-windows-media-center-tv-pack-2008-download-installation-guide.html. I downloaded it, and the PlayReady PC Runtime (also from Microsoft, and linked from the same page), and installed them both.

    I restarted the PC, and Media Centre - all my TV settings had gone! (This is normal, I think, as TV Pack is a major upgrade.) I went into the option to set up TV, and it appeared to hang at the point where it should have downloaded the guide. I panicked, and did some more research - others had the same problem, but none of their solutions seemed to work. So I started it again, and left it overnight.

    In the morning, success! It had probably abandoned the download, and continued - it had scanned the available TV channels, and I was able to proceed (after another delay while it tried to download guide information again).

    I then laboriously went through all the channels (some of which had got guide information from somewhere, and some hadn't). I pressed the "i" button while the cursor was on a channel name in the guide, and chose "Edit Channel" (this is a new option, which only appears after you have installed TV Pack). This gave me the option to renumber the channel (for some reason Al Jazeera had joined BBC2 on channel 2!), and to set up listings for the channel. In most cases, there was an option to "Use listings from broadcaster" - which I assume gets the listings from the broadcast signal, just as a Freeview TV would. There were a few obscure channels where this was not an option, but I left those alone (I wasn't interested in any of them, and most had too weak a signal to actually get a picture).

    I now have a full guide, and I am hoping I will not need to use EPG Collector after all. I have lost all my series recording settings, but they were about due for a clear out anyway.

    Friday, 28 December 2012

    Making a cheap concertina blind using greenhouse shading


    Vertical blind, half open
    Roof blinds - 1 closed, 1 open

    For Christmas, I wanted to give my wife a set of greenhouse shading blinds for her new greenhouse.

    However, when I priced them up, the cost was going to be over £500. I decided to make some instead.

    Turns out they are not difficult to do, and, as I could find no instructions on the Web, I decided to write up how to do it here on this blog.

    The blinds shown here can be used vertically (as in the picture above), or horizontally (on the roof). In either case the moving rail (white in the picture) always stays parallel to the top and bottom rails, due to the guide lines threaded through it.

    The blinds can easily be opened and closed by pulling or pushing either end of the moving rail, and will stay in any position.

    Materials

    • Greenhouse shade netting
    • Strimmer line
    • Plastic electrical conduit, oval section
    • Wood strips (about 1" x 1" - 25cm x 25cm)
    • Rust proof screws (brass or stainless steel)

    Method

    First measure the area to be covered. I suggest making the blinds no wider than about 4 ft (120 cm), as the electrical conduit which forms the handle to open and close the blind bends if you use longer lengths.
    Cut the shade netting to the width to be covered, and about one and a quarter times the length.

    Pleating the blind


    Using a ruler to guide the pleating

    Using an iron on the appropriate setting for your material (nylon for shade netting - not too hot, or it will melt), fold over about an inch of one end of the blind, and iron the fold in.

    I used a long metal ruler about an inch wide to help me get the folds straight and the correct width.

    Fold and iron again, and repeat until you have pleated the full length of the blind.

    I found the weight of the material hanging off the ironing board tended to pull the pleats out, so I used some weights (a couple of square bottles of water in the picture) to stop this happening.

    Obviously you will have to unfold earlier pleats as you go, otherwise the material gets too thick and difficult to handle.

    The pleats do not have to be totally accurate, but it is important to make sure they are reasonable square to the edges of the material (and that the material is cut squarely).

    When you have finished, gather the pleats up carefully (I found starting from the middle was easiest), and tie with some pieces of string to hold them in place. Make sure the edges are all squared up.

    Make the mechanism

    Pleated blind with top rail threaded


    Cut 2 pieces of the wood the same width as the blind, and 1 piece of the electrical conduit.

    Drill holes for the strimmer cord - one close to each end, and one in the middle. The holes in the 2 pieces of wood and the conduit should all line up. Also drill diagonal holes in the ends of each piece of wood as shown.

    Thread the blind

    Threading diagram

    The 3 main strimmer lines should be a little longer than the drop of the blind (enough to allow plenty for knots, and a bit extra for adjustment). The 2 guide lines (the ones that go through the conduit) should be longer by the length of the conduit - best to allow a bit more for adjustment, as these lines should not be as tight as the main lines.

    First thread the two guide lines though the length of the conduit. Then thread the 3 main strimmer lines through the top wooden rail, the blind, the conduit and the bottom rail as shown. Tie figure eight knots in the ends.

    Thread the ends of the guide lines though the diagonal holes - notice that one line goes from top left, through the conduit, and down to bottom right, and the other vice versa. Again, tie them off with figure eight knots.
    Now fasten the blind to the top rail (I used a staple gun), and to the conduit (I used gaffer/duck/duct tape).
    Detail showing guide line (blue) threaded in top rail and conduit

    Fit

    Drill holes in the top and bottom rails for the fitting screws - the placing depends on what you are screwing them to. Fasten the top rail in place, and hold the bottom rail where it is to go. Adjust the length of the main lines so they will be quite tight (a little stretched) when the bottom rail is in place. Temporarily fasten the bottom rail, ready to adjust the guide lines. For horizontal blinds the guide lines can be quite loose, but for vertical blinds, they must be tight enough so the blind does not close under its own weight. Once you have measured and tested, undo the bottom rail and tie the knots to make the lines the right length. Professionally made blinds do not use knots, they have a small piece of metal tube with a grub screw, so you can pull the right length of line through the tube, and tighten the screw.



    Wednesday, 3 October 2012

    Using reCAPTCHA from C++

    I wanted to add reCAPTCHA to some old C++ CGI code, but I couldn't find a reCAPTCHA library for C++. Neither could a few other people I saw looking. So I wrote the following code, which I am happy to share:
    #include <string>
    #include <iostream>
    #include <sstream>
    #include <fstream>
    #include <iomanip>
    #include <curl/curl.h>
    
    static ostringstream curl_result;
    
    /* curl write callback, to fill tidy's input buffer...  */
    uint write_cb(char *in, uint size, uint nmemb, void *out)
    {
      int r = size * nmemb;
      in[r] = '\0';
      curl_result << in;
      return(r);
    }
    
    static bool recaptcha_check_answer(const char *private_key, const char *remoteip, 
     const char *challenge, const char *response, std::string &error) {
     if (!remoteip || !*remoteip) {
      error = "For security reasons, you must pass the remote ip to reCAPTCHA";
      return false;
     }
     //discard spam submissions
     if (!challenge || !*challenge || !response || !*response) {
      error = "Captcha challenge or response missing";
      return false;
     }
    
     CURL *curl;
     CURLcode res;
     struct curl_httppost *formpost=NULL;
     struct curl_httppost *lastptr=NULL;
    
     if(curl_global_init(CURL_GLOBAL_SSL)) {
      error = "Error initialising curl library";
      return false;
     }
     curl = curl_easy_init();
     if(!curl) {
      error = "Error initialising curl library";
      return false;
     }
     curl_formadd(&formpost,
      &lastptr,
      CURLFORM_COPYNAME, "privatekey",
      CURLFORM_COPYCONTENTS, private_key,
      CURLFORM_END);
     curl_formadd(&formpost,
      &lastptr,
      CURLFORM_COPYNAME, "remoteip",
      CURLFORM_COPYCONTENTS, remoteip,
      CURLFORM_END);
     curl_formadd(&formpost,
      &lastptr,
      CURLFORM_COPYNAME, "challenge",
      CURLFORM_COPYCONTENTS, challenge,
      CURLFORM_END);
     curl_formadd(&formpost,
      &lastptr,
      CURLFORM_COPYNAME, "response",
      CURLFORM_COPYCONTENTS, response,
      CURLFORM_END);
    
     const char *surl = "https://api-verify.recaptcha.net/verify";
     curl_easy_setopt(curl, CURLOPT_URL, surl);
     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1L);
     curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
     #ifdef SKIP_HOSTNAME_VERFICATION
     /*
      * If the site you're connecting to uses a different host name that what
      * they have mentioned in their server certificate's commonName (or
      * subjectAltName) fields, libcurl will refuse to connect. You can skip
      * this check, but this will make the connection less secure.
      */
     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
     #endif
     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    
     res = curl_easy_perform(curl);
    
     /* always cleanup */
     curl_easy_cleanup(curl);
     if(res && res != CURLE_RECV_ERROR)
      {
      std::ostringstream err;
      err << "Error " << res << " contacting captcha server";
      error = err.str();
      return false;
      }
    
     std::string resp = curl_result.str();
     std::string::size_type pos = resp.find("\n");
     std::string part1 = pos == std::string::npos ? resp : resp.substr(0, pos);
     if(part1 == "true")
      return true;
     error = pos == std::string::npos ? "Unknown captcha error" : "Captcha error:" + resp.substr(pos);
     return false;
    }
    
    This code relies on the curl library to handle the communication with the reCAPTCHA server.

    Thursday, 6 September 2012

    Using the word breaker from Microsoft SQL Server 2005 in a stand-alone C# program


    I have been writing a program which will query a full text search index on Microsoft SQL Server 2005. The data being indexed are large blobs of text (multiple lines).
    I need to extract only those lines of text which contain the search words for display with the results.
    Full Text Search uses a word breaker (IWordBreaker COM interface) and stemmer (IStemmer COM interface) to break the indexed text into words, and to enable matching of alternate word forms (e.g. "welcome" will also match "welcomes", "welcomed" and "welcoming").
    I therefore need to use the Sql Server word breaker and stemmer to determine if each line of text from the blob matches the search string, as I believe the standard word breaker and stemmer in Windows search uses a different algorithm.
    I thought if I copied the two dlls (infosoft and langwrbk) from a SQL Server installation to the machine on which I am running my program, and ran regsvr32 on them, they would be installed in the registry, and I would be able to use them. Unfortunately, this does not work, as regsvr32 does not add any information to the registry.
    I therefore needed to load the COM components in the dlls "by hand". I started with John Jeffery's code to load COM components by hand. Once I had got over some 32-bit/64-bit problems (your DLL has to match the bit size of your calling code), it worked.
    Here is the LinqPad script I used to test the two interfaces, in case someone else needs to do the same thing:

    const string SqlServerDllFolder = @"C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Binn";
    static LibraryModule langwrbk = LibraryModule.LoadModule(Path.Combine(SqlServerDllFolder, "LangWrbk.dll"));
    static LibraryModule infosoft = LibraryModule.LoadModule(Path.Combine(SqlServerDllFolder, "infosoft.dll"));
    static Guid stemmer = new Guid("D99F7670-7F1A-11CE-BE57-00AA0051FE20");
    static Guid breaker = new Guid("173C97E2-AEBE-437C-9445-01B237ABF2F6");
    
    void Main()
    {
     WordBreaker breaker = new WordBreaker();
     string search;
     while((search = Util.ReadLine("Query string:").Trim()) != "") {
      Console.WriteLine("Original text:" + search);
      foreach (string word in breaker.Search(search)) {
       Console.WriteLine(word);
      }
     }
    }
    
    // COM Interface to Microsoft word breaker and stemmer
    [Flags]
    public enum WORDREP_BREAK_TYPE {
     WORDREP_BREAK_EOW = 0,
     WORDREP_BREAK_EOS = 1,
     WORDREP_BREAK_EOP = 2,
     WORDREP_BREAK_EOC = 3
    }
    
    [ComImport]
    [Guid("CC907054-C058-101A-B554-08002B33B0E6")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IWordSink {
     void PutWord([MarshalAs(UnmanagedType.U4)] int cwc,
      [MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf,
      [MarshalAs(UnmanagedType.U4)] int cwcSrcLen,
      [MarshalAs(UnmanagedType.U4)] int cwcSrcPos);
     void PutAltWord([MarshalAs(UnmanagedType.U4)] int cwc,
      [MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf,
      [MarshalAs(UnmanagedType.U4)] int cwcSrcLen,
      [MarshalAs(UnmanagedType.U4)] int cwcSrcPos);
     void StartAltPhrase();
     void EndAltPhrase();
     void PutBreak(WORDREP_BREAK_TYPE breakType);
    }
    
    [ComImport]
    [Guid("CC906FF0-C058-101A-B554-08002B33B0E6")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IPhraseSink {
     void PutSmallPhrase([MarshalAs(UnmanagedType.LPWStr)] string pwcNoun,
      [MarshalAs(UnmanagedType.U4)] int cwcNoun,
      [MarshalAs(UnmanagedType.LPWStr)] string pwcModifier,
      [MarshalAs(UnmanagedType.U4)] int cwcModifier,
      [MarshalAs(UnmanagedType.U4)] int ulAttachmentType);
     void PutPhrase([MarshalAs(UnmanagedType.LPWStr)] string pwcPhrase,
      [MarshalAs(UnmanagedType.U4)] int cwcPhrase);
    }
    [ComImport]
    [Guid("fe77c330-7f42-11ce-be57-00aa0051fe20")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IWordFormSink {
     void PutAltWord([MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf, [MarshalAs(UnmanagedType.U4)] int cwc);
     void PutWord([MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf, [MarshalAs(UnmanagedType.U4)] int cwc);
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct TEXT_SOURCE {
     [MarshalAs(UnmanagedType.FunctionPtr)]
     public delFillTextBuffer pfnFillTextBuffer;
     [MarshalAs(UnmanagedType.LPWStr)]
     public string awcBuffer;
     [MarshalAs(UnmanagedType.U4)]
     public int iEnd;
     [MarshalAs(UnmanagedType.U4)]
     public int iCur;
    }
    
    // used to fill the buffer for TEXT_SOURCE
    public delegate uint delFillTextBuffer([MarshalAs(UnmanagedType.Struct)]
     ref TEXT_SOURCE pTextSource);
    
    [ComImport]
    [Guid("D53552C8-77E3-101A-B552-08002B33B0E6")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IWordBreaker {
     void Init([MarshalAs(UnmanagedType.Bool)] bool fQuery,
      [MarshalAs(UnmanagedType.U4)] int maxTokenSize,
      [MarshalAs(UnmanagedType.Bool)] out bool pfLicense);
     void BreakText([MarshalAs(UnmanagedType.Struct)] ref TEXT_SOURCE pTextSource,
      [MarshalAs(UnmanagedType.Interface)] IWordSink pWordSink,
      [MarshalAs(UnmanagedType.Interface)] IPhraseSink pPhraseSink);
     void GetLicenseToUse([MarshalAs(UnmanagedType.LPWStr)] out string ppwcsLicense);
    }
    
    [ComImport]
    [Guid("EFBAF140-7F42-11CE-BE57-00AA0051FE20")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IStemmer {
     void Init([MarshalAs(UnmanagedType.U4)] int ulMaxTokenSize, [MarshalAs(UnmanagedType.Bool)] out bool pfLicense);
     void GenerateWordForms([MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf, 
      [MarshalAs(UnmanagedType.U4)] int cwc, 
      [MarshalAs(UnmanagedType.Interface)] IWordFormSink pStemSink);
     void GetLicenseToUse([MarshalAs(UnmanagedType.LPWStr)] out string ppwcsLicense);
    }
    
    // Word breaker to break a line of text into words, stem them, 
    // and return a HashSet of all the words and stems
    public class WordBreaker : IWordSink, IWordFormSink {
     IWordBreaker wordBreaker;
     HashSet words; // Words to search for
     IStemmer istm;
     
     public WordBreaker() {
      wordBreaker = (IWordBreaker)ComHelper.CreateInstance(langwrbk, breaker);
      bool pfLicense = true;
      wordBreaker.Init(true, 1000, out pfLicense);
      istm = (IStemmer)ComHelper.CreateInstance(infosoft, stemmer);
      pfLicense = true;
      istm.Init(1000, out pfLicense);
     }
     
     public HashSet Search(string text) {
      words = new HashSet();
      TEXT_SOURCE pTextSource = new TEXT_SOURCE();
      pTextSource.pfnFillTextBuffer = new delFillTextBuffer(pfnFillTextBuffer);
      pTextSource.awcBuffer = text.ToLower();
      pTextSource.iCur = 0;
      pTextSource.iEnd = text.Length;
      wordBreaker.BreakText(ref pTextSource, (IWordSink)this, null);
      return words;
     }
     
     #region IWordFormSink Members
     public void PutAltWord(string pwcInBuf, int cwc) {
      words.Add(pwcInBuf.Substring(0, cwc));
     }
    
     public void PutWord(string pwcInBuf, int cwc) {
      words.Add(pwcInBuf.Substring(0, cwc));
     }
     #endregion
    
     #region IWordSink Members
     public void PutWord(int cwc, string pwcInBuf, int cwcSrcLen, int cwcSrcPos) {
      istm.GenerateWordForms(pwcInBuf.Substring(0, cwc), cwc, this);
     }
     
     public void PutAltWord(int cwc, string pwcInBuf, int cwcSrcLen, int cwcSrcPos) {
     }
    
     public void StartAltPhrase() {
     }
     
     public void EndAltPhrase() {
     }
    
     public void PutBreak(WORDREP_BREAK_TYPE breakType) {
     }
     #endregion
    }
    
    static uint pfnFillTextBuffer(ref TEXT_SOURCE pTextSource) {
     // return WBREAK_E_END_OF_TEXT
     return 0x80041780;
    }
    
    // Code from https://gist.github.com/1568627
    // By John Jeffery
    static class ComHelper
    {
     private delegate int DllGetClassObject(ref Guid clsid, ref Guid iid, [Out, MarshalAs(UnmanagedType.Interface)] out IClassFactory classFactory);
    
     public static object CreateInstance(LibraryModule libraryModule, Guid clsid)
     {
      var classFactory = GetClassFactory(libraryModule, clsid);
      var iid = new Guid("00000000-0000-0000-C000-000000000046"); // IUnknown
      object obj;
      classFactory.CreateInstance(null, ref iid, out obj);
      return obj;
     }
    
     static IClassFactory GetClassFactory(LibraryModule libraryModule, Guid clsid)
     {
      IntPtr ptr = libraryModule.GetProcAddress("DllGetClassObject");
      var callback = (DllGetClassObject) Marshal.GetDelegateForFunctionPointer(ptr, typeof (DllGetClassObject));
    
      var classFactoryIid = new Guid("00000001-0000-0000-c000-000000000046");
      IClassFactory classFactory;
      var hresult = callback(ref clsid, ref classFactoryIid, out classFactory);
    
      if (hresult != 0)
      {
       throw new Win32Exception(hresult, "Cannot create class factory");
      }
      return classFactory;
     }
    }
    
    
    [Guid("00000001-0000-0000-c000-000000000046")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComImport]
    interface IClassFactory
    {
     void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject);
     void LockServer(bool fLock);
    }
    
    class LibraryModule : IDisposable
    {
     private readonly IntPtr _handle;
     private readonly string _filePath;
    
     private static class Win32
     {
      [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
      public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
    
      [DllImport("kernel32.dll")]
      public static extern bool FreeLibrary(IntPtr hModule);
    
      [DllImport("kernel32.dll", SetLastError = true)]
      public static extern IntPtr LoadLibrary(string lpFileName);
     }
    
    
     public static LibraryModule LoadModule(string filePath)
     {
      var libraryModule = new LibraryModule(Win32.LoadLibrary(filePath), filePath);
      if (libraryModule._handle == IntPtr.Zero)
      {
       int error = Marshal.GetLastWin32Error();
       throw new Win32Exception(error, "Cannot load library: " + filePath);
      }
    
      return libraryModule;
     }
    
     private LibraryModule(IntPtr handle, string filePath)
     {
      _filePath = filePath;
      _handle = handle;
     }
    
     ~LibraryModule()
     {
      if (_handle != IntPtr.Zero)
      {
       Win32.FreeLibrary(_handle);
      }
     }
    
     public void Dispose()
     {
      if (_handle != IntPtr.Zero)
      {
       Win32.FreeLibrary(_handle);
      }
      GC.SuppressFinalize(this);
     }
    
     public IntPtr GetProcAddress(string name)
     {
      IntPtr ptr = Win32.GetProcAddress(_handle, "DllGetClassObject");
      if (ptr == IntPtr.Zero)
      {
       int error = Marshal.GetLastWin32Error();
       string message = string.Format("Cannot find proc {0} in {1}", name, _filePath);
       throw new Win32Exception(error, message);
      }
      return ptr;
     }
    
     public string FilePath
     {
      get { return _filePath; }
     }
    }
    

    Monday, 23 April 2012

    Naerok 12" bandsaw manual

    I recently inherited a workshop containing a Naerok 12" bandsaw, and went looking on the Internet for a manual. Although I found manuals for similar saws, I couldn't find one for this model.

    However, when tidying the workshop, I found the real manual! In order to help anyone else with the same model of bandsaw, I have uploaded a scan of the manual to http://www.trumphurst.com/naerok.zip.


    Monday, 7 November 2011

    ACS AET62 Fingerprint Reader and C#

    I'm currently trying to interface to an ACS AET62 Fingerprint Reader from C#. The reader comes with an SDK, along with what appear to be extensive example applications. However, the applications contain bugs which the documentation does not help to resolve. I thought I would record the bugs I have found, and how I have resolved them.

    ABSEnroll has a ref int parameter which is returned as a pointer to an ABS_BIR structure. This is declared in the provided BSTypes.cs file as containing a header followed by 2560 bytes of data. Unfortunately, this is not correct - it contains a header followed by up to 2560 bytes of data. The actual length of the whole structure (including the header) is contained in the first uint Length member of the structure.

    This kind of variable length structure is quite common in C++, but there is no built-in way to marshal it correctly in C#.

    The code provided in the SDK just calls Marshal.PtrToStructure on it - this will run off the end of the allocated memory, and assign garbage to the last (should be unused) bytes of the ABS_BIR structure. That's sort of OK in a 32-bit operating system (it is unlikely to try to access memory that doesn't exist), but in a 64-bit one with properly protected memory, it may well throw an exception.

    The correct way to marshal the structure is in two parts - first marshal the header part with Marshal.PtrToStructure, then allocate the data byte array to the length therein (first subtracting the length of the header itself), and use Marshal.Copy to copy the remainder.

    ppEnrolledTemplate = new BSTypes.ABS_BIR();
    BSTypes.ABS_BIR_HEADER hdr = (BSTypes.ABS_BIR_HEADER) Marshal.PtrToStructure(ptr, typeof(BSTypes.ABS_BIR_HEADER));
    ppEnrolledTemplate.Header = hdr;
    int hdrlen = Marshal.SizeOf(hdr);
    int bodylen = (int)(hdr.Length - hdrlen);
    ppEnrolledTemplate.Data = new byte[bodylen];
    Marshal.Copy(new IntPtr(tset + hdrlen), ppEnrolledTemplate.Data, 0, bodylen);
    

    ABSVerify takes a reference to an IntPtr which is represented in C++ as **ABS_BIR - it is supposed to be an array of pointers to ABS_BIR. The example code provided just passed one ABS_BIR - it used Marshal to allocate a chunk of global memory and marshal the structure into it. The resulting IntPtr was passed as the ref parameter. This is all very fine if you only want to verify a single fingerprint, but if you want to determine which of a list of fingerprints is being scanned (as I did), it is no help at all.

    My knowledge of C++ finally allowed me to figure out what was needed. - first, allocate an array of IntPtr (one for each ABS_BIR fingerprint scan you wish to pass). Then allocate memory for each structure into its corresponding IntPtr, and use Marshal to copy the structure into the allocated memory. Finally, pass a reference to the 0'th element of the array.

    In actual fact, there is no need to Marshal the ABS_BIR structure during Enroll or Verify - it can be treated as an opaque array of bytes (with the length of the whole thing in the first 4 bytes) - other than the length, there is no other interesting data in the ABS_BIR structure.

    Also, during Verify, you can make a single call to allocate enough memory for all the structures you want to pass, and calculate the values of the IntPtrs to pack the data into the single block of memory.

    I hope this helps anyone else struggling with this SDK.

    In response to anoop's comment, here is the code I use to call verify, to see if a scan corresponds to one of the scans in the database

    BSTypes.ABS_OPERATION op = operation(1);
    op.Timeout = 60000;
    int res;
    int matching_slot = 0;
    res = BSApi.ABSVerify(_conn, ref op, (ushort)_templateIndex.Length, ref _templates[0], 
      ref matching_slot, 0); // BSTypes.ABS_FLAG_NOTIFICATION | BSTypes.ABS_FLAG_AUTOREPEAT);
    

    To create the _templates array, you do this

    int size = Marshal.SizeOf(typeof(BSTypes.ABS_BIR));
    IntPtr[] templates = new IntPtr[number_of_elements];
    templates[0] = Marshal.AllocHGlobal(size * number_of_elements);
    byte[] zeroes = new byte[size];
    int offset = 0;
    int ind = 0;
    foreach (byte[] b in list_of_scan_records) {
     if(ind > 0) templates[ind] = new IntPtr((int)templates[0] + offset);
     offset += size;
     Marshal.Copy(zeroes, 0, templates[ind], zeroes.Length);
     Marshal.Copy(b, 0, templates[ind], b.Length);
     ind++;
    }
    

    number_of_elements is the number of scans in the database, and list_of_scan_records is a list containing a byte array for each record

    Saturday, 19 February 2011

    VMWare Workstation on Windows

    Having abandoned CentOs as the host for my VMWare VMs, I went back to running Windows XP as the host.

    A bit of a pain (two sets of virus detectors, and all the other bloatedness of Windows). That would have been OK, except that most times I started the client, it decided it had only 1 monitor, and rearranged my desktop accordingly. So every morning, the first few minutes would be spent putting things back on the correct monitor. Would you believe that moving a quick launch toolbar from one monitor to another reorders all the icons?

    At least my USB 3 drive worked (and aren't they _fast_ !). And my headset.

    However, performance was not good, and too many things caused screen updating to stop working. And VM clients had a tendency to jump from one (ATI Hydravision) virtual desktop to another (on the server).

    The NTFS filesystem going corrupt, so that one of the 2GB files that made up my hard disk suddenly thought it was 2TB was the last straw.

    I finally gave in, and decided to "de-virtualise" my desktop. I took loads of backups, made a BartPE CD, booted it, and repartitioned my disk. Then I restored the backup of my VM client desktop, and rebooted.

    Then I was stuck in a boot loop - as soon as the Windows logo appeared, the machine rebooted. Safe mode (even safe mode with command line) did not help. The only way out was a repair install with the latest CD I had (XP64 SP1), followed by a whole day downloading updates and service packs and copies of Internet Explorer.

    So I can now advise that virtualisation is not ready to host your desktop on the one machine, and that changing the hardware of your Windows working environment is still a horrible nightmare.

    CentOs and VMWare Workstation

    I had other problems with my CentOs VMWare WOrkstation host environment. I have a Logitek USB headset, which I use with Skype in my VM client running Windows XP.

    However, a while ago the microphone stopped working - all I got from it was a load crackling noise. I tried buying a new headset, but that was the same - it seems to be a reaction between CentOs and VMWare.

    Yet another reason to abandon CentOs :-(

    CentOs and USB 3.0

    I run CentOs 5 as a host operating system for VMWare Workstation, and use a VM client as my main working environment.
    I was finding backing up my environment to be a slow and painful process using USB2 disks, or the Gigabit network card.
    I read that USB 3.0 was much faster, and that Linux was the first O/S to support it, so I bought a controller card and disk.
    Then I found CentOs didn't support it :-(
    I was encouraged by the nice people at el-repo to try their experimental kernel for CentOs, and, after a lot of messing about, I managed to get it booted, and accessing the USB 3 disk. However, I couldn't also use their fglrx ATI Radeon driver at the same time (it only works with the stock CentOs kernel). As dual monitor support is vital to me, I gave up, and finally abandoned CentOs.

    Thursday, 3 December 2009

    U8230 MoDaCo ROM makes ShopSavvy work on Android T-Mobile Pulse

    Well, I installed the U8230 MoDaCo ROM, and it sort of worked.

    Unfortunately, things (especially the front screen) kept crashing.

    However, after advice from the nice MoDaCo people, I did a factory reset...
    • Use the Quick Boot app to go into recovery mode
    • Allow the root shell access when the prompt comes up
    • Choose the factory reset option
    • When the system restarts it will come up with the standard French options - go into Settings/Locale and text/Select locale and choose English.
    • I found I had to reboot the phone again to make that setting take properly.
    • Now you have to reinstall all your apps (but, if you go to Market, My downloads, they are all listed ready to install). I have installed CoPilot Live (paid for), but it downloaded and installed OK, and re-registered fine.

    Because this version of the ROM has moved Google Maps into the data partition (to save space), the factory reset removed it (as it wipes the data partition). So I then had to reinstall the update again. I would advise factory resetting before installing this ROM.

    There are some good things about this ROM though:

    • ShopSavvy can use the camera like it should (the old version just showed a black screen).
    • Google contacts will sync OK (even if you add people to your Favorites).

    ShopSavvy doesn't work on Android T-Mobile Pulse

    One of the highly recommended applications on Android is ShopSavvy. Apparently this lets you use the phone's camera to scan barcodes of products, and then tells you the best prices to buy them, both locally and on the 'Net.

    Sounds good, but doesn't work on my Pulse :-(

    However, the excellent Paul at MoDaCo has produced a custom rom based on the French U8230 version of this phone, which fixes the problem. (For those not wishing to root their phone, ShopSavvy have been told by the phone manufacturer that a new ROM image has been supplied to T-Mobile, so just wait patiently!)

    So I'm loading it now - for full instructions see http://trumphurst.blogspot.com/2009/12/rooting-android-on-t-mobile-pulse.html - but use http://www.romraid.com/paul/pulse/update-pulse-u8230edition-1.0-core-signed.zip instead of update-pulse-1.3-core-signed.zip.

    SSH into my Android T-Mobile Pulse phone

    Having installed the MoDaCo Custom ROM into my rooted phone, I then tried to ssh into it. The first thing I needed was the password - the instructions weren't clear, but I eventually found it in Settings/About Phone/Setup Wizard.

    I used Putty to SSH into the phone (having got its IP address from my DHCP server logs) as root (using port 2222), and it worked. Now I wanted to use my public key, so I wouldn't be asked for the password again. Instructions were a bit fragmented, and I had a few false starts, but this is what I ended up doing:
    • cd /data/dropbear
    • mkdir .ssh
    • chmod 700 .ssh
    • cd .ssh
    • echo 'my public key' >authorized_keys
    • chmod 644 authorized_keys
    • cd ..
    • mount -o rw,remount -t yaffs2 /dev/block/mtdblock1 /system
    • edit /system/bin/dropbear.sh to add
      -R /data/dropbear/.ssh/authorized_keys
      to the end of the command line.
    • mount -o ro,remount -t yaffs2 /dev/block/mtdblock1 /system
    • reboot the phone (type "reboot" at the command line, or use the Quick Boot app on the phone).

    The mount commands switch the filesystem from read-only (its usual state) to read-write and back again.


    Brilliant - I now have password-free root access to my phone using ssh from my PC (which has a real keyboard). Some experimentation shows I have most of the commonly-used Linux commands available to me.