Home
Name Modified Size InfoDownloads / Week
uws-taskKS-readme.txt 2020-04-10 25.2 kB
taskKS-0.2.0.zip 2019-05-14 44.0 kB
taskKS-0.1.0.tar.gz 2019-01-26 15.5 kB
Totals: 3 Items   84.7 kB 0
 t a s k K S
=============

### content
+ overview
+ release-notes
+ work flow
+/+



### overview

--- taskKS ---                 A small framework to handle task-function-calling


taskKS is  _NOT_  an full-featured OS, 
it is  _NO_  Operating-System at all.

It is just an very tiny framework to support function-calling on demand.
They are called in taskKS : task-functions. (in short: taskFns)

Up to 32 tasks i.e. task-functions (shortly named taskFns) you could 
manage with one taskKS instance 
(nearly an object (obj) in C; of course you'll need the 
instance pointer (iPtr) of the specific taskKS).

The tasks have to be served by frequently calling of an service-function, 
here supported by a RUN Macro.
One call serves all waiting tasks, i.e. if they are ready to run (started).

All tasks are cooperative together (KS = Kooperatives-System / DE); 
just one is active while the others are non-called at this time. 
This is fully single-threaded, _not_ preemptive. 
This may save semaphore usage, but you should call all functions of one taskKS 
obj in just one thread! (pure single-threaded)

New features in 0.2.0:
Signaling and Messaging.
-
Signaling means: Every taskFn could have up to X signalFns which will be 
called directly and before the taskFn (also without taskFn possible) 
if the *_SET_SIGNAL macro has been called. 
Must be instantiated before, of course.
Hence you could inject direct functionality. (ok, delayed by RUN-macro call)
-
Messaging means: Every taskFn could instantiate an own Message-Box service i.e. 
could have a mailbox with waiting messages (msgs), which could be checked and 
served during the RUN-macro call.
Other taskFns of this taskKS obj or Fns from the service-thread could 
send these Messages to this tasks.
For those receiving tasks a small framework could be used to make handling of 
messages easier. The Message-Boxes are instantiated as Ring-Buffers. They are 
not endless, like many like to do in times were PCs have Gigabytes of RAM.
But they could be differ in size for every taskFn; i.e. a service taskFn 
should get bigger msg-boxes than a seldom used taskFn, but i think they 
could be organized big enough.
The macros for taskFn msg-boxes have an own Prefix : "TASKKSM_" (instead 
of the usual taskKS prefix "TASKKS_"). Every message has an Id, and may be 
additional Data which is needed to service this specific msg-type (id).

-------------------------------------------------------------------------------







### release-notes

* 0.1.0
java:   no support
C++:    
Impl    general support via C-functions. (extern "C" block)
-
C:      ..
Impl    First integration supports the following features:
-   Calling upto X Task-Functions by one service Macro, 
    called TASKKS_RUN_TASK(iPtr), on demand or periodically till it is stopped 
    (for polling in a task-fn, should be avoided or only used for a short time).
    Currently X-max = 32.
    -> TASKKS_MAX_TASKS <------------------------------------------------------!
    -
-   C-object-like instance-pointer (iPtr) for one taskKS instance (obj).
    Although, like in CPP world a no-go, all will be public but should not 
    be used directly (use only API macros ((or know very well what you 
    do and be prepared that it may not work with a new version of taskKS)) ).
    -> tstTaskKS_OneSysInstanceCB* <-------------------------------------------!
    -
-   Functions of one taskKS instance should be called by one thread only, 
    because no mutex-support included.
    -> only-one-thread-is-calling-macros-of-one-taskKS-obj rule <-!
    This means also that all taskKS functions (in following abbreviated by 
    taskFn or function with "Fn") 
    are cooperative together; while one is called the others are not called 
    (of course this may only be possible by different threads in a 
    preemptive OS which is not the scope of taskKS, see rule).
    (in other way: By the restriction that all taskKS-Fns & macros are only 
    called by one thread we has nearly a single-thread with different 
    asynchronous called taskFns, of course)
    DE-name is Kooperativ, so taskKS means -> task for taskFns, KS for 
    Kooperatives-System (cooperative-system).
    This means also: all taskKS taskFns must not be blocking, or you block the 
    whole system (RUN-macro..). 
    It could be for instance a state-machine function, which has some 
    wait-states and will wake up by other task-functions to evaluate 
    an event.
    -> TASKKS_SET_TASKFN() -> tstTaskKS_TaskCB <-------------------------------!
    -
-   Usually a taskFn is started by using the signal-macro (in newer version 
    it is the start-macro due to avoid misunderstandings to the signal-Fns 
    in 0.2.0 or greater - better use 0.2.0 directly).
    Then the taskFn is called one-time (unless the one-time flag is cleared 
    which means you setup a poll taskFn). This is the default of taskKS after 
    creation. The specific SET macro could clear or set the one-time flag.
    -> TASKKS_SET_ONETIMEF() <-------------------------------------------------!
    -
-   poll taskFns are initialized be clearing the one-time flag. Then, 
    after signaling (starting) the taskFn the poll-taskFn will be called on 
    every RUN-macro call. This is till the one-time flag is set or if the 
    unsignal (stop) macro is called.
    -> TASKKS_SIGNAL_TASKFN() <------------------------------------------------!
    -> TASKKS_UNSIGNAL_TASKFN() <----------------------------------------------!
    Attention: in Version 0.2.0 these macros are deprecated and usually not 
    longer supported. Instead the START & STOP macro has this Fn in 0.2.0.
    -
-   A taskKS instance could be created and deleted dynamically. The number 
    of created taskKS instances is upto the power and RAM-size of the 
    underlying system only. (iPtr == instance or obj pointer)
    -> taskks_createSys() <---------(returns iPtr)-----------------------------!
    -> iPtr->deleteTaskKSSys(&iPtr) <------------------------------------------!
    -
-   A taskFn could be any Fn with the given prototype 
    which is generic, but not type-safe (C++ could do this better of course).
    This means a specific User-struct ptr could be used as the arg for 
    each taskFn; this should contain the ptr to the taskKS-instance (the iPtr)!
    (Sry. may be it willB fixed in future)
    -> tfnTaskKS_TaskFn <------------------------------------------------------!
    -
-   A taskFn has a one-time flag, which is set by default.
    If this flag is True, the taskFn is called one-time after signaling 
    (in V>=0.2.0: starting) and then 'sleeps' again (like after init).
    If this flag is False, the taskFn is called whenever the RUN-macro is 
    called when it was already signaled (in V>=0.2.0: started). To stop this you 
    must either set the one-time flag again (will just called once more) or 
    you must unsignal (in V>=0.2.0: stopping) it, which stops calling of this 
    taskFn immediately.
    -> TASKKS_RUN_TASK(iPtr) <-------------------------------------------------!
    -
-   After initialization (create taskKS instance with the createSys Fn - 
    all other calls must be done usually by macros with this iPtr, setup taskFns 
    and args of them and begin calling the RUN macro cyclic in a worker-thread
    ),
    you had to start some calls in this taskKS-obj (nearly an object like in 
    CPP, but without the comfort using the instance ptr implicit).
    For the identification of a taskFn in a taskKS-obj we need the index of 
    the task (also ID named, sry). In macro abbreviated with 'tix' as short 
    for task-index.
    Example: one taskFn is called always (one-time-flag is false) and checks 
    some states of the I/O. If a specific change is detected it prepares 
    data for taskFn-x and then starts this taskFn - with this concept the 
    detection and work is apart.
    Task-indices could be defined in your program by using enum-types.
    -> arg2 in TASKKS macros <-(usually)---------------------------------------!
    -
-   In one taskKS-obj only one taskFn is called at a time 
    (if only-one-thread-is-calling-macros-of-one-taskKS-obj rule is obeyed),    (mustB rule!)
    which is the same in cooperative operating systems. This allows the taskFn 
    to identify itself - he could get the tix. This means - if you had to 
    serve 3 serial lines in the same way for instance - you could use only 
    one taskFn and determine implicit by tix and/or taskFn data ptr which 
    line is meant. 
    -> TASKKS_CURRENT_TASKINDEX(iPtr) <----------------------------------------!
    -
-   Also, before using the RUN macro, you could determine if it necessary to 
    call it (may be it is necessary for some use-cases).
    -> TASKKS_IS_TASKWAITING(iPtr) <-------------------------------------------!
    -
-   You could check, if a given tix is prepared to work yet. This means that 
    the tix is in range (is expected of course) and the tix could use a 
    taskFn, which is necessary if you will signal (start) this task; otherwise 
    the signal-macro has no effect of course (in V>=0.2.0: start-macro).
    -> TASKKS_IS_VALID_TASKINDEX() <-------------------------------------------!
    -
-   By using access Fns via iPtr (obj) and free access to them you could 
    made easy wrapper functions.
    E.g. if you do not like to call the RUN-macro always you could change 
    the 'signalTaskFn' (V>=0.2.0: startTaskFn) in the obj and 
    signal your own RUN-macro start and include the std. start-Fn of this 
    taskKS. Example: >>
        -setup-
        ptr_environment->orig_signalTaskFn= iPtr->signalTaskFn;
        iPtr->signalTaskFn= myNewSignalTaskFn;
        -use-
        ..
        TASKKS_SIGNAL_TASKFN(iPtr, 0 )
        --> iPtr->signalTaskFn(iPtr, 0 );
        ..
        -wrapper-Fn-
        void myNewSignalTaskFn(struct TaskKS_OneTaskSysInstanceCB*iPtr,unsigned TaskIndex)
        {
            ActivateRUNMacro( ptr_environment, iPtr);
            ptr_environment->orig_signalTaskFn(iPtr,TaskIndex);
        }
    <<
    Hint:pls. do this in Version 0.2.0 or later, due to changed function name
    (signalTaskFn -> startTaskFn, old macro could be used furthermore but 
    should be changed to do not use "TASKKS_IS_DEPRECATED_WANTED" true)
    -> indirect Fns in tstTaskKS_OneSysInstanceCB <----------------------------!
    (Usually this Fns in the obj are only called by the TASKKS macros)
    -
-/-

Impl    Integration of simpletest01, using 4 taskFns with one polling 
        with one global obj of a TaskKS. 
        Support for linux (POSIX?) only.
        

* 0.2.0
java:   no support
C++:    no changes
-
C:      ..
Add     Define TASKKS_FEATURE_SETbm to make it possible to control 
        the feature-set of taskKS from outside. (Optional global define)
        Default: Newer features signals&messages are enabled.
        0: Nearly features only like in version 0.1.0 with small changes.
        It is a bit-mask to control all features by one define. Remark: if 
        a feature is disabled, it could not be used, if a feature is enabled, 
        it may be used (in some taskKS instances). 
Info    "signal/unsignal" of 0.1.0 is moved to "start/stop". Reason: 
        avoid complications with the new signal-features which meant to 
        have X specific Fns for one taskFn which could be called directly 
        without any further data-set. (E.g. to clear an HW-flag-bit)
        Signaling in 0.2.0 and above version meant feature
        TASKKS_IS_SIGNALLING_WANTED.
Add     Signaling feature (User supports Fns of type tfnTaskKS_SignalFn ..),
        available if define TASKKS_IS_SIGNALLING_WANTED is not 0.
Add     Messaging feature (User could use Ring-buffer on every taskFn for task-
        communication with events with optional data <-> messages),
        needs additional source-files taskksmsg.c/.h.
        available if define TASKKS_IS_MESSAGES_WANTED is not 0. 
Chg     rename "signalTaskFn" to "startTaskFn" (avoid complic. w. signal feature) 
        and have usually only the new START-macro.
Chg     rename "unsignalTaskFn" to "stopTaskFn" (avoid complic.w. signal feature)
        and have usually only the new STOP-macro.
Add/Chg Internal behaviour optimization, e.g. for poll-taskFns.
Add     taskks_exit as an Fatal-Program-Exit fn
Add     check macro which prooves task-ix, if it is a real one,
        TASKKS_IS_VALID_INTERNAL_TASKIX().
Upd     Comments enhanced and corrected
Add     Enhance simpletest01 with 2 other tests. One for signaling and one for 
        messaging.

-------------------------------------------------------------------------------







### work flow

1. creation
Create the taskKS subsystem via createSys function.
Afterwards the RUN-macro is ready to use, but without any effect yet.
Has to be called to get a instance ptr (iPtr) which is needed for any 
access to this taskKS. 
Example code:
>>
  tstTaskKS_OneSysInstanceCB*     pstOneTaskKS;
  unsigned                        uErrorCode= 0xFEFE;
  ...
  pstOneTaskKS= taskks_createSys( &uErrorCode );
  if( pstOneTaskKS==NULL) {
        print_log with uErrorCode as info
        return;
  }
  /* now you can use TASKKS_* and TASKKSM_* macros with iPtr "pstOneTaskKS" 
   * as arg1
   */
  ...
<<
You could increase TASKKS_MAX_TASKS, if you need more taskFns for a taskKS.


2. Initialization
At least you need one or more taskFns which has some tasks to do. Probably 
they has the intention of State-Machines, who gets events to work with 
and may be switch his machine-state.
Nearly every macro is of the concept to returning a error-code. Usually 
return-code 0 means success, everything is ok, and other codes means, something 
went wrong.
In the illustrated examples we ignore this, but if you work with dynamic 
arguments or starting a big SW project with many taskKS objs it may be good to 
detect software-bugs via the return-code check.


2.1 task (function) definition
Define for each task, you want to serve or you had a dedicated task for, a task-
function and you may use an datainstance-via-ptr for this 
taskFn (the argument when calling), if you will 
not use global variables for instance to have 
multi-process opportunity (multiple: many taskKS objs uses the same taskFN or 
one taskKS which uses a taskFn in different tasks (e.g. taskFn1 is used in 
task 1, 2 and 3 {tix==1..3}).
Here you have to integrate the kind of work for this 
task. May be you'll read a command/event from a Q (for instance the 0.2.0 
messaging feature) and now you are going to do the specific acts 
when the task is called after reading from the Q (the 1st part in the task).
Example code to initialize a taskFn:
>>
  ..
  typedef struct {
      tstTaskKS_OneSysInstanceCB*  pstTaskSysCB;
      struct MyInput  InData;
      bool  isInputBusy;
      ..
  } tstMyTaskContext;
  ..
  tstMyTaskContext stMyTaskContext;
  ..
  void  MyTaskFn_getInput(void* pTaskData)
  {
      tstMyTaskContext*  pstMyTC= (tstMyTaskContext*) pTaskData; 
      // alternative you could use global data if it is sufficient for you 
      ..
  }
  ..
  // init of stMyTaskContext has to be finished, iPtr is the result of step 1.
  // GetInputTaskIndex is a definition or enum with an task-index (tix) of
  // this taskKS.
  void anTaskSysInitFunction(void)
  {
      ..
      stMyTaskContext.pstTaskSysCB= ~iPtr~  // iPtr got from createSys call
      ..
      TASKKS_SET_TASKFN(iPtr, GetInputTaskIndex, MyTaskFn_getInput, &stMyTaskContext);
      ..
  }
  ..
  int  anInputFunction( tstMyTaskContext* pstMyTC, struct MyInput* pInput)
  {                                     // this is already for starting the taskFn, no init
      tstTaskKS_OneSysInstanceCB*   iPtr= pstMyTC->pstTaskSysCB; // == iPtr
      if(pstMyTC->isInputBusy)  return 1;
      pstMyTC->isInputBusy= TRUE;
      memcpy( &pstMyTC->InData, pInput, sizeof(*pInput));
      TASKKS_START_TASKFN(iPtr, GetInputTaskIndex);
      // the specific taskFn is called with the next RUN-macro call
      ..
  }
  ..
<<


2.2 optional defining one or more signal functions
Signal-Number in range 0..31 possible.
After signaling a specific SignalFn is called in a taskKS obj.
Special : It is possible to use only SignalFunctions without a defined taskFn; 
but if both exist at 1st the signalFn is called and afterwards the taskFn.
-
Setup your signal Fn with
TASKKS_SET_SIGNALFN().
Example code (belonging to previous Ex.code):
>>
  ..
  void  MySignalFn_triggerCounter(void* pTaskData, unsigned SignalNo)
  {
      tstMyTaskContext*             pstMyTC= (tstMyTaskContext*) pTaskData;
      tstTaskKS_OneSysInstanceCB*   pstTaskSysCB= pstMyTC->pstTaskSysCB; // == iPtr
      unsigned                      uCurTaskIX= TASKKS_CURRENT_TASKINDEX(pstTaskSysCB);
      unsigned long                 uSignalId= pstTaskSysCB->astTaskCB[uCurTaskIX].apstSignalTaskCB[SignalNo].ulSignalId;
      
      // do something immediate on HW (in taskKS-environment, may be depending on uCurTaskIX, SignalNo, uSignalId, ..)
      ..
  }
  ..
  void anTaskSysInitFunction(void)
  {
      ..
      // define SignelFn#1 for task with tix "ServeHardwareTaskIndex"
      TASKKS_SET_SIGNALFN(iPtr, ServeHardwareTaskIndex, 1/*sigNo*/,
                          MySignalFn_triggerCounter, 0x2345 );
      ..
  }
  ..
  int  anTriggerCtrFunction( tstMyTaskContext* taskCtx)
  {                                     // this is already for signaling, no init
      TASKKS_SET_SIGNAL(iPtr, ServeHardwareTaskIndex, 1/*sigNo*/);
      // the specific signalFn & taskFn is called with the next RUN-macro call
      // Attention: using another TaskIndex will call another SignalFn if defined
  }
  ..
<<


2.3 optional defining mailboxes (messaging) for specific or all taskFns
Ring-Buffer-Size-Bits in range 2..16 possible.
This defines the Buffer-Size in potences of 2 - i.e. the mail-box could 
accept max. ( 2^(SizeBits) - 1 ) messages before reading them.
For instance if you are using 6 bit (_RiBuSizeBits = 6) the mailbox stores 
maximum 63 messages.
The message itself is stored in malloc-memmory; not very fast but due to 
wrapping with alloc/free Fns in taskksmsg.c you could speed it up by your own 
message allocation system/software if you need more performance.
-
Setup your your mailbox for every taskFn who should be able to receive 
messages with
TASKKSM_MESSAGE_INITCB().
Example code (belonging to previous Ex.code):
>>
  ..
  void anTaskSysInitFunction(void)
  {
      ..
      // define a mailbox for task with tix "LogServiceTaskIndex" for buffering max. 255 messages 
      TASKKSM_MESSAGE_INITCB(iPtr, LogServiceTaskIndex, 8 );
      ..
  }
  ..
<<


2.4 defining a frame-work in taskFns who uses mailboxes
When a taskFn should use the taskKS message functionality, it may be easier 
to use the taskksm style to handle messages inside the taskFn.
-
Use the following macros in a taskFn to evaluate messages via default ptr name.
TASKKSM_DECLARE_MESSAGEp            -> define default msg-ptr
TASKKSM_IS_MESSAGE_WAITING()        -> chk and - if available - get a msg       {out of the mailbox}
TASKKSM_SWITCH_MESSAGE_ID()         -> C "switch" with the ID of the msg
TASKKSM_FREE_MESSAGE()              -> release the msg ("free")
-
Example code (belonging to previous Ex.code):
>>
  ..
  void  MyTaskFn_LogService(void* pTaskData)
  {
      tstMyTaskContext*  pstMyTC= (tstMyTaskContext*) pTaskData;
      tstTaskKS_OneSysInstanceCB*   pstTaskSysCB= pstMyTC->pstTaskSysCB; // == iPtr
      ..
      TASKKSM_DECLARE_MESSAGEp;
      int iRC;
      ..

      iRC= TASKKSM_IS_MESSAGE_WAITING( pstTaskSysCB );
      if( iRC!=0)  {                    
          /* we MUST check & if exist (RC!0) free the implicit Msg ptr, 
           * declared by TASKKSM_DECLARE_MESSAGEp, at least
           */
          TASKKSM_SWITCH_MESSAGE_ID()
          {
          case 1:                       //------------------------------------//
              // do something with message, type 1
              ..
              /* if you like to store this msg longer than this call, you could 
               * use : TASKKSM_MOVE_GETMESSAGEp(_saveP)
               * this macro saves the current msg ptr in the given one, and this 
               * should not be a local Fn var of course 
               * (e.g. a member var of pstMyTC)
               */
              break;
    
          default:                      //------------------------------------//
              printf("unknown message Id (%lu) - ignore\n", (unsigned long) TASKKSM_MESSAGE_ID());
              break;
          }
          TASKKSM_FREE_MESSAGE( pstTaskSysCB );
      };
      ..
  }
  ..
<<


2.5 setup & send messages to the specific taskFns with a mailbox
taskFns which works on mailboxes (message Ring-Buffers) must served with 
messages (msgs) from outside (usually).
-
This must be done in the same task-context (thread)! Because no semaphores are 
now used by taskKS.
-
How much msgs a taskFn should get in maximum must be estimated with enough 
reserve in the Ring-Buffer storage for a taskFn (see 2.3).
.
All messages are located in malloc memory. The pointer/memory is (usually) 
created by the Sender (may be a taskFn of this taskKS obj), and stored 
in the mailbox of the Receiver till the Receiver reads out the message. This 
is done in FIFO manner, of course.
-
Allocate memory for a msg, to get the msg-ptr, via macro:
TASKKSM__ALLOC_MESSAGE(),
TASKKSM_ALLOC_MESSAGE().
This macro assures that the returned msg-ptr (arg2) is valid; due to the 
statement :  "memory should never end", i made an program-exit ~exception~ 
when no memory is available. Hence you do not need to check any return-code.
(Although a macro, it is similar like a void Fn)
-
After setting up all data of this message you should transfer it to _one_ 
task (taskFn with mailbox) via macro:
TASKKSM__SEND_MESSAGE(),
TASKKSM_SEND_MESSAGE().
After this call, the memory-control of this msg (ptr) is moved to the other 
task (except the RC is not 0, which is a failure afai-think).
-
Remark: the macros with 2 underscores after TASKKSM should be used normally; 
hence you do not get any trouble between incoming and outgoing messages 
(The one-underscore macro uses the default message ptr, which is used _always_ 
in the Receive macros as described in 2.4, via "TASKKSM_DECLARE_MESSAGEp;").
-
Example code (belonging to previous Ex.code):
>>
  ..
  void  MyTaskFn_AnyMiscThingsToBeDone(void* pTaskData)
  {
      tstMyTaskContext*             pstMyTC= (tstMyTaskContext*) pTaskData;
      tstTaskKS_OneSysInstanceCB*   pstTaskSysCB= pstMyTC->pstTaskSysCB; // == iPtr
      tstTaskKSM_Message*           pstMyMessageIWantToSend= NULL;
      ..
      .. // setup dataBuffer, for this example we assume lenght = 20
      // we now want to setup a message for the Log-task (see 2.4)
      TASKKSM__CURTASK_ALLOC_MESSAGE(iPtr, pstMyMessageIWantToSend, 
                            DebugLogType, 20, dataBuffer );
      // TODO : get message and setup data directly into the message
      // (now it has to be done before in "dataBuffer" and willB copied)
      TASKKSM__SEND_MESSAGE(iPtr, LogServiceTaskIndex, pstMyMessageIWantToSend);
      ..
  }
  ..
<<
The example is based on creating a message in a taskFn. But you could do this 
everywhere if you got the iPtr of this taskKS obj, 
assuming the task-context (thread) is the same.


3. RUN macro embedding in your system
Usually on some process-systems you'll have one 
endless-till-process-ends loop.
Here you can insert the RUN macro, which will 
often called then. May be you call it with a 
periodically time; this service is outside of 
the taskKS. You may create - for instance - an own 
thread which will call the RUN macro every millisecond, but if you  
are going to interact with other threads you have 
to take care of semaphore usage by yourself!


4. trigger task function to run
At least you have to start the task, which 
means it will be executed on next RUN call.
This could be done inside or outside of this 
taskKS but must be done by start macro of 
this taskKS instance (taskKS subsystem ptr).
-
Implicit started is a taskFn with the *_SEND_MESSAGE() and the 
*_SET_SIGNAL() macro. With this you do not need the explicit additional use 
of the start-macro 
TASKKS_START_TASKFN()
-
Usually it will be called one time and after calling it is waiting again 
for the next start trigger.
You could start permanent calling by clearing the one-time flag of this task.


5. optional enable poll-task
If you need [for some time] a permanently called 
task function, e.g for polling, you should 
clear the one-time flag of this task before 
using it. After this, the started task will 
be called as many times as the RUN macro 
is called. This will finish only by setting 
the one-time flag again or by stopping this task.


6. deleting a taskKS obj
If you are sure that memory blocks (especially messages), used by this taskKS 
are all free'd, you could remove this instance completely.
Use taskFn internal Fn "deleteTaskKSSys". (Not wrapped by a macro yet)
iPtr->deleteTaskKSSys(&iPtr);
-
When successful, the iPtr is NULL afterwards. No other copies should exist!
Source: uws-taskKS-readme.txt, updated 2020-04-10