Waves GTR Ground MIDI Protocol

Waves GTR Ground

This information is also available at GitHub.

The Waves GTR Ground is a MIDI foot controller with 11 buttons and 2 foot pedals. It has three 15-segment LEDs for seven buttons, plus two LEDs and brightness control for the LEDs.

The device is a USB MIDI compliant device with a USB Vendor ID of 0x6824 and a USB Product ID of 0x0100. My device returned Version “ 1.01” and “Waves Audio LTD.” as the Manufacturer. The MIDI properties for my device are:

  • SerialNumber = “Ver. 1.4”;
  • Manufacturer = “Waves Audio LTD.”;
  • Model = “GTR Ground Ver 1.4”;
  • Name = “GTR Ground Ver 1.4”;

The following MIDI events are send from the device:

  • MIDI CC 0x0E = Right Pedal (0x00..0x7F)
  • MIDI CC 0x0F = Left Pedal (0x00..0x7F)
  • MIDI CC 0x10 = Button ‘A’ (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x11 = Button ‘B’ (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x12 = Button ‘C’ (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x13 = Button ‘D’ (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x14 = Button ‘E’ (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x15 = Button ‘F’ (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x16 = Button A/B (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x17 = Button Down (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x18 = Button Up (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x19 = Button Preset/Stomp (0x7F: pushed down, 0x00:released)
  • MIDI CC 0x1A = Button Tuner/Tap Tempo (0x7F: pushed down, 0x00:released)

To control the LEDs on the device, a USB Real Time message can be send to the device: The message is always 48 bytes long, all unused bytes are set to 0x00, the last byte is 0xF7.

0xF0, 0x7F, 0x00, 0x20, 0x66, <type>, <variable data>, 0x00*, 0xF7

An example

All LED segments on for button A (1st event), LED to max brightness (2nd event), both A/B LEDs on (3rd event)

F0 7F 00 20 66 41 00 38 7F 7F 7F 7F 7F 7F 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F7
F0 7F 00 20 66 51 0F 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F7
F0 7F 00 20 66 62 03 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F7

Controlling the LEDs next to A/B button

  • 0x62, 0x01 = B LED on
  • 0x62, 0x02 = A LED on
  • 0x62, 0x03 = A LED and B LED on

Controlling the brightness of the LED displays

  • 0x51, value Brightness for ‘A’
  • 0x52, value Brightness for ‘B’
  • 0x53, value Brightness for ‘C’
  • 0x54, value Brightness for ‘D’
  • 0x55, value Brightness for ‘E’
  • 0x56, value Brightness for ‘F’
  • 0x57, value Brightness for Preset

value =

  • 0x00 .. 0x07 LEDs off
  • 0x08 .. 0x0F Brightness (dark … bright)

Setting the segments in the LED displays

  • 0x41 Set LED Text above the ‘A’ button
  • 0x42 Set LED Text above the ‘B’ button
  • 0x43 Set LED Text above the ‘C’ button
  • 0x44 Set LED Text above the ‘D’ button
  • 0x45 Set LED Text above the ‘E’ button
  • 0x46 Set LED Text above the ‘F’ button
  • 0x47 Set LED Text below the Preset button

There are the 3 15 Segment-LEDs per button:

  -     -     -      A
|\|/| |\|/| |\|/|  BCDEF
 - -   - -   - -    G H
|/|\| |/|\| |/|\|  IJKLM
  - .   - .   - .    N  O
 1st   2nd   3rd

The ‘A’ to ‘O’ above are the references to a specific LED segment.

The format is as followed:

1. Byte
    0x00 (this byte seems to be unused and always 0x00)
2. Byte (by bits)
    6 - (unused)
    5 - Dot behind the 3rd segment
    4 - Dot behind the 2nd segment
    3 - Dot behind the 1st segment
    2 - (unused)
    1 - (unused)
    0 - (unused)
3. Byte (by bits) (1. LED)
    6 - G (left middle bar)
    5 - B
    4 - I
    3 - N (the 6 outside segments, counter clockwise)
    2 - M
    1 - F
    0 - A
4. Byte (by bits)
    6 - J
    5 - K
    4 - L
    3 - E (the 6 inside segments, counter clockwise)
    2 - D
    1 - C
    0 - H (right middle bar)
5. Byte (by bits) (2. LED)
    6 - G (left middle bar)
    5 - B
    4 - I
    3 - N (the 6 outside segments, counter clockwise)
    2 - M
    1 - F
    0 - A
6. Byte (by bits)
    6 - J
    5 - K
    4 - L
    3 - E (the 6 inside segments, counter clockwise)
    2 - D
    1 - C
    0 - H (right middle bar)
7. Byte (by bits) (3. LED)
    6 - G (left middle bar)
    5 - B
    4 - I
    3 - N (the 6 outside segments, counter clockwise)
    2 - M
    1 - F
    0 - A
8. Byte (by bits)
    6 - J
    5 - K
    4 - L
    3 - E (the 6 inside segments, counter clockwise)
    2 - D
    1 - C
    0 - H (right middle bar)

PC Protocol of Suunto D9

This document describes the transfer protocol and memory layout of these suunto diving computers:

  • Suunto D6 (a D9 without the air integration)
  • Suunto D9

Suunto changed the protocol for these models – again. One reason is that the D9 is capable in recording not only the depth, but also pressure and temperature throughout the dive. The computers also have no longer just 8kb, but 32kb of memory for the profile information.

Communication

The interface is technically still a RS232 interface, but with a USB connector. With a device driver from the USB chipset vendor FTDI it will be recognized as a standard serial port.

The RS232 should be 9600 baud, 8N1.

Transfer

The protocol for the Suunto D9 is slightly different from other Suunto computers. The first thing is that the interface is USB only. But with the standard driver is behaves like a serial port (see above). These standard drivers are available for Mac OS X and Linux, so there should be no reason to go into too much detail.

All data is send in packages to and from the computer. Every package is followed by a CRC for checksum reasons.

unsigned char checksum = 0x00;
for(int i=0; i<packageLen; ++i)
 checksum ^= package[i];

The package starts with a command byte, followed by the length of bytes for the parameters ($00 $03 = 3 Bytes), the actual parameters and the CRC at the end. Here are examples of the most common commands:

  • ReadMemory – reads memory from the Suunto
    • to Suunto: $05 + $00 + $03 + addr_high + addr_low + count (1..$78) + CRC
    • from Suunto: $05 + $00 + $03 + addr_high + addr_low + count (1..$78) + count Bytes + CRC
  • WriteMemory – changes the memory inside the Suunto (WARNING: this can damage your watch!!!)
    • to Suunto: $06 + $00 + $03 + addr_high + addr_low + count (1..$78) + CRC
    • from Suunto: $06 + $00 + $03 + addr_high + addr_low + count (1..$78) + count Bytes + CRC
  • GetVersion / check for connected computer
    • to Suunto: $0F + $00 + $00 + CRC
    • from Suunto: $0F + $00 + $00 + ID + HIGH + MID + LOW + CRC = Version HIGH.MID.LOW (ID)
  • ResetMaxDepth – resets maximum depth in the history to 0.0m
    • to Suunto: $20 + $00 + $00 + CRC
    • from Suunto: $20 + $00 + $00 + CRC

The DiveManager Software from Suunto does the following when reading data from a D6/D9: – GetVersion() – ReadMemory($0, $78) – ReadMemory($168, $78) (Reading the memory header) – ReadMemory($F0, $78)

After that the DiveManager reads the headers of all profiles first (by following the linked list in the headers), but always $78 bytes and later fills in the holes in the memory, if necessary. This allows the DiveManager to display a progress bar for the import.

To change the personal information, it does the following: – GetVersion() – Write($015E,$28) – Read($0168,$78) – GetVersion() – Read($0000,$78)

To reset the max. depth to zero it does the following: – GetVersion() – ResetMaxDepth() – Read($00F0,$78) – Read($0168,$78) – GetVersion() – Read($0000,$78)

Firmware versions

There have been two recalls of the Suunto D9 so far.

  • The first one was a hang of the D9, which is fixed in firmware version 1.2.8 and later (serial numbers 4xxxxxxxx – 450000699)
  • The second one (serial numbers below 62102582, an inacuracy of the clock in 0.6% of the cases) seemed to be fixed in firmware version 1.2.11 and later. This one also affects the Suunto D6 (serial numbers below 62103693)

Memory layout

The 32kb of memory is split into a $019A bytes header and the rest is a ring-buffer with the dive profiles. The ring-buffer is a stream of data, which ends at the position, that is marked in the header. At this position the computer starts writing the information from the next dive. If the write pointer reaches the value $8000, it jumps back to $019A.

I am using the following parameter types:

parameter types Description
B 8 bit Byte
Bn n Bytes
W 16 bit LSB integer
An ASCII string, max. n Bytes long

All units inside the Suunto are metric (meter/centimeter, bar)!

Memory Header

 offset |     format   | testvalue    | content
$00 B $00 empty
$01-$02 A2 “D9” Type of the dive computer, “D9”, “D6” or “D4”
$03-$0A A8 “20050228” Manufacturing date (28 Feb 2005)
$0B-$12 A8 “00001204” Serial number?
$13-$15 B $00 $55 $00 ???
$16-$22 B .. $FF .. unused
$23-$26 B4 $50 $90 $12 $34 official serial number ($50901234) Production date: 2000+5 / 09 week, watch 234
$27 B $FF unused
$28 B $0E model ($0E=D9, $0F=D6 (can also be $0E?), $12=D4)
$29-$2A B2 $01,$02 Version (1.2.10 – well, were does the 10 come from?)
$2B-$2D B $04,$09,$82 ???
$2E B $82 XOR Checksum $28..$2D
$2F-$31 B .. $FF .. unused
$32-$33 B $01 $01 ???
$34-$35 B2 $00 $00 $0000 = metric, $0101 = imperial
$36 B $FF unused
$37-$3A B $83 $00 $82 $F2 ???
$3B B $0C XOR Checksum $32..$3A
$3C-$3F B $FE $00 $00 $00 ???
$40-$63 B .. $FF .. unused
$64-$6F B .. calibration data for sensors
$70 B $.. XOR Checksum $64..$6F
$71-$73 B .. $FF .. unused
$74-$7D B .. calibration data for sensors
$7E B $.. XOR Checksum $74..$7D
$7F-$83 B .. $FF .. unused
$84-$8D B .. calibration data for sensors
$8E B $.. XOR Checksum $84..$8D
$8F-$015D B .. $FF .. unused
$015E-$0185 A40 .. owner name (“Suunto Diver”), $00 terminated if short than 40 bytes
$0186-$0187 W $F010 max. depth ($10F0 = 4336 cm)
$0188-$0189 W $AB02 total dive time ($02AB = 683 minutes)
$018A-$018B W $0C00 total number of dives ($000C = 12)
$018C B $.. XOR Checksum $0186..$018B
$018D-$018F B .. $FF .. unused
$0190-$0191 W $0A $46 Ptr to the last dive in the ring-buffer (Offset $460A)
$0192-$0193 W $0C $00 Number of dives in the ring-buffer ($000C = 12)
$0194-$0195 W $7E $4B Ptr to behind the latest dive in the ring-buffer (Offset $4B7E)
$0196-$0197 W $9A $01 Ptr to the oldest dive in the ring-buffer (Offset $019A)
$0198-$0199 W $5C $0E checksum ??? (if changed SDM can no longer read the profiles)

Dive Profile Header

Every dive in the profile memory has a $4A bytes header.

offset format testvalue content
$00-$01 W $9A $01 Offset to previous dive ($019A – this is the very first)
$02-$03 W $CF $06 Offset to next dive ($06CF)
$04-$0C B .. ???
$0D-$0E W $F0 $10 max. depth ($10F0 = 4336 cm)
$0F-$10 W $1E $00 dive time ($001E = 30 minutes)
$11-$12 W .. if value is $FFFF, there is no pressure information for the dive
$13 B $03 OLF PCT (3%)
$14 B .. ???
$15 B $0A Date: hour (10 AM)
$16 B $24 Date: minutes (36)
$17 B $0E Date: seconds (14)
$18-$19 W $D5 $07 Date: year (2005)
$1A B $0C Date: month (12)
$1B B $0B Date: day (11)
$1C B $0A Sample recording interval (every 10 seconds)
$1D B $00 Gas model (0=Air, 1=Nitrox, 2=Gauge)
$1E B $01 repetitive dive number (1)
$1F B $00 Surface time (0 minutes)
$20 B $00 Surface time (0 hours)
$21 B $03 RGBM PCT (3%)
$22 B $00 Personal Setting (P0..P2)
$23 B $00 Altitude Setting (A0..A2)
$24 B .. ???
$25 B $20 O2 PCT (only if in Nitrox mode, 32%)
$26 B $15 O2 PCT 2 (only if in Nitrox mode, 21%)
$27 B $15 O2 PCT 3 (only if in Nitrox mode, 21%)
$28 B $00 ???
$29-$2A W $.. $.. PREVTPRESSURE_1
$2B-$2C W $.. $.. PREVTPRESSURE_2
$2D-$2E W $.. $.. PREVTPRESSURE_3
$2F-$30 W $.. $.. PREVTPRESSURE_4
$31-$32 W $.. $.. PREVTPRESSURE_5
$33-$34 W $.. $.. PREVTPRESSURE_6
$35-$36 W $.. $.. PREVTPRESSURE_7
$37-$38 W $.. $.. PREVTPRESSURE_8
$39-$3A W $.. $.. PREVTPRESSURE_9
$3B-$46 B .. ???
$47 B $05 Temperature recording interval (every 5 depth samples)
$48-$4B B .. ???
$4C-$4D W $4D $00 Offset to the first marker position in the profile

Dive Profile Data

The length of the profile data can be found by looking into the profile header: the difference between the offset of the next dive to the current offset minus the size of the header ($4A).

Warning: Remember that the data is stored in a ring-buffer! The next dive pointer might actually be smaller than the current dive pointer.

To parse the profile data, you start with a time of 0:00 minutes and read to LSB 16 bit values. The first value is the current depth in cm. The second value is the tank pressure in bar * 100. Before reading the next value you increment the time by the “Sample recording interval” (Header offset $1C). Every “Temperature recording interval” (Header offset $47) there is another byte following with the temperature in degrees celcius. You stop once you’ve read the beginning of the next dive.

But, there is another twist: the dive profile can contain events, which are encoded into the profile information. For this you need check for the “Offset to the first marker position” (Header offset $4C). The first offset into the dive profile is $0000, you increment it with every byte you’ve read from the profile (4 or 5 per sample, see above). If the marker is equal to the current profile position, the next byte is an event type. You have to read all events till you will get Event $01 “Next Event Marker”. After you’ve read this one, you can continue normal parsing till you hit the next marker position.

Event Type Parameter Function
$01 Next Event Marker WW set the position of the next event marker (1st: current position, 2nd: next position)
$02 Surfaced BB Reached Surface at current timestamp + 2nd byte: seconds (for a more exact value)
$03 Event BB Other dive related events (1st: type, 2nd: time offset like in “Surfaced”)
$04 Bookmark/Heading BBW Bookmark & Heading (1st:???, 2nd: time offset like in “Surfaced”, 3rd: heading * 4, if < 0: Bookmark instead)

The Dive Events from Event $03 are 7 bit values. If bit 7 is set, like in $8E which only seems to occur in that form, it means: Event has ended. So you can get a start and a stop time for one event.

Dive Event Description
$01 Mandatory Safety Stop
$04 Asc Warning
$06 Mandatory Safety Stop Ceiling Error
$07 ???
$09 Depth alarm
$0D Air Time Warning
$8E Rgbm Warning (End) (always before the first profile data at the beginning of the dive)

Inside information of the Pronto IR Remote

The Philips Pronto is a programmable remote control. It looks a bit like a Palm Pilot, but it is the most flexible remote control on the market (below the $1000…). The interesting thing with the Pronto is, that it is fully customizable with the Pronto Edit software.

Unfortuanlly the Pronto Edit software is for Windows only, so i reverse engineered the format of the CCF files and also tried to get information about the communication with the Pronto.

CCF file format

Source code for parsing a CCF file:

  • Parse.cp (last change: 10. Nov. 1999; now with support for hidden panels, timers and beeps)
  • ParseGCC.cp (last change: 12. Jul. 2001, source code for the GCC on Linux systems)

Serial Communication

Pronto has a serial port for the communication with a computer. This port works at 115200 baud, 8N1. The communication is pretty simple:

First: get attention of the Pronto. This is done by sending an ACK (0x06) to the Pronto. The Pronto should send a ‘!’ (standby mode) or a ‘*’ (awaiting data, done by holding the left and right key down, while the serial cable was plugged in). If the Pronto don’t respond, try again until timeout.

When the Pronto awaits a command, you can send strings (terminated with CR (0x0D)) like:

irlearn 3500

The Pronto now awaits an infra red signal to learn. The result is send via YModem CRC protocol.

q ccf

Inquiry. The Pronto sends a string containing 4 decimal numbers. The first is the dirty flag (CCF data in the Pronto changed by the user and is not uploaded to the computer yet), the second is the size of the CCF file, the third the the date (in a 8 digit decimal BCD format), the fourth is the time (in a 6 digit decimal BCD format) of the last change of the CCF file.

ul ccf

Uploading a CCF file from the Pronto to the computer. The CCF file is simply send via YModem CRC protocol to the computer.

dl ccf

Downloading a CCF file to the Pronto from the computer. The CCF file is simply send via YModem CRC protocol to the Pronto.

reboot

Reboot the Pronto.

Some links to interesting Pronto websites

PC Protocol of Suunto Spyder

This document describes the transfer protocol and memory layout of these Suunto diving computers:

  • Suunto Sypder

Communication

The protocol uses 2400 8O1, which means 2400 baud with 8 bits, odd parity and 1 stop-bit. The check for the interfaces uses 2400 8N1, but it also works with 2400 8O1. This was the trivial part… But some lines have a special meaning with the interface!

The DTR line should always be set. It is used as a power supply for the interface.

RTS is a toggle for the direction of the half-duplex interface. To send a command to the interface, set RTS. When you await a command, clear RTS. Timing also seems to be critical! To make sure that all data is sent to the interface, before clearing RTS, I wait about 200ms. After clearing RTS, I also have to wait 400ms and use a 500ms timeout for receiving data.

The test for the existance of the interface is simple: the computer sends AT plus a CR ($41, $54, $0D) and awaits the same answer.

Transfer

The protocol for the Suunto Spyder, Vyper and Cobra are identical, but the memory layout of the Spyder is different. All data is send in packages to and from the computer. Every package is followed by a CRC for checksum reasons.

unsigned char checksum = 0x00;
for(int i=0; i<packageLen; ++i)
 checksum ^= package[i];
  • Read memory
    • to Spyder: 05 + addr_high + addr_low + count (1..32) + CRC
    • from Spyder: 05 + addr_high + addr_low + count (1..32) + n Bytes + CRC
  • Write memory
    • to Spyder: 06 + addr_high + addr_low + count (1..31)+ n Bytes + CRC
    • from Spyder: 06 + addr_high + addr_low + count (1..31) + CRC
  • Used before every ‘Write memory’ call
    • to Spyder: 07 + $a5 + CRC
    • from Spyder: 07 + $a5 + CRC
  • Get first dive profile
    • to Spyder: 08 + $a5 + CRC
    • from Spyder: 08 + count (1..32) + n Bytes + CRC (if more than 32 bytes needs to be transmitted, they are split in 32 byte packages)
  • Get next dive profile
    • to Spyder: 09 + $a5 + CRC
    • from Spyder: 09 + count (1..32) + n Bytes + CRC (if more than 32 bytes needs to be transmitted, they are split in 32 byte packages)

The DiveManager Software from Suunto does the following when reading data from a Spyder: – Check for correct computer – Read Memory ($24, 1) – Read the internal memory: – Read Memory ($1E, 14) – Read Memory ($2C, 32) – Read Memory ($53, 30) – Get first dive profile – Get next dive profile – … – If “next dive profile” returns a null package, the end of the log memory is reached.

Memory layout

Default name of the LOG file: ‘‘PROFILE.ACW’’

  offset |     format   |  testvalue  | content
$00-$15 MSB binary unused by the PC software, probably configuration values for the computer
$16-$17 MSB binary $0102 Firmware version of the Spyder (old ACW $0101, new ACW $0102)
$18-$1B MSB binary Serialnumber (new ACW, e.g. $0002.0F7F = 203.967) or ID no. (old ACW, e.g. $2E.61.122E = 469704654) of the Spyder
$1C-$1D MSB binary Ptr to the last $82 byte in the profile ringbuffer
$1E-$1F MSB binary $3F46 max. depth in ft * 128.0
$20-$21 MSB binary total dive time in minutes
$22-$23 MSB binary total number of dives
$24 MSB binary $14 interval (20s, 30s or 60s, Changing ist not allowed by DiveManager software! Why? Marketing?)
$25 MSB binary altitude and personal settings (height (0..2) + 3 * personal (0..2))
$26-$2B MSB binary ? ($0B,$03,$1F,$FF,$1F,$FF : identical on all Suunto computers?)
$2C-$49 ASCII ACW Diver personal information (“ACW Diver”, otherwise filled to the maximum length with spaces)
$4A-$4B MSB binary ? ($01, $01 : identical on all ACW?!? Version of the profile memory?)
$4C-$1FFF MSB binary ring buffer for the profile memory

The ring-buffer is a stream of data, which ends at the position, that is marked in the header. At this position the computer starts writing the information from the next dive. If the write pointer reaches the value $2000, it jumps back to $4C.

Format for one dive

offset format description
0 MSB binary unknown — air preassure at the end of the dive?
1 MSB binary temperature in degress celcius.
2… binary profile data from the end to the beginning of the dive,this means reverse order!
n MSB binary minutes at the beginning of the dive
n + 1 MSB binary hours at the beginning of the dive
n + 2 MSB binary day at the beginning of the dive
n + 3 MSB binary month at the beginning of the dive
n + 4 MSB binary year at the beginning of the dive (90..99 = 1990..1999, 00..89 = 2000..2089)
n + 5 MSB binary altitude and personal settings (height (0..2) + 3 * personal (0..2))
n + 6 MSB binary unknown — air preassure at the beginning of the dive?
n + 7 MSB binary interval (20s, 30s or 60s)
n + 8 MSB binary dive number in the Spyder (for repetitive dives)
n + 9 MSB binary hours of the surface interval
n + 10 MSB binary minutes of the surface interval

Profile information

The profile data is a stream of bytes. Every minute (or 30s or 20s – see the profile interval) a byte is recorded. This byte is the delta depth in ft to the last depth! E.g. you start your dive at a depth of 0 feet go down to 30ft in a minute, so the value is –30ft (because you go 30ft down) or $E2 in binary, if you then go up to 20ft, the next value will be +10ft (because you go 10ft up) or $0A in binary.

Some values have special meanings:

Byte Type Description
$7d Surfaced you have reached the surface while (or after) the dive
$7e ASC dive now is a decompression dive
$7f ERR decompression missed
$80 End end of the dive. The next byte is n + 1 in the format description of the dive.
$81 Slow Slow warning while the dive. If the dive ends with $7d8180 (Surfaced, Slow, End) it means, you finished the dive with a blinking SLOW warning in the display.
$82 End of data set after the last dive (written after the dive as a marker, so technically not profile information)

Necessary conversions

meter = (int)(feet * 0.3048 * 10) / 10
psi = bar * 14.50377377
fahrenheit = celcius * 1.8 + 32

Altitude:

value meters feet
0 700m 2300ft
1 1500m 5000ft
2 2400m 8000ft

ATTN: the computers don’t round after the 2. digit, when calculating feet => meter! They cut it after the 2. digit. This results to the modified formula.

PC Protocol of Suunto Cobra, Vyper, Stinger, Mosquito, Vytec and Gekko

This document describes the transfer protocol and memory layout of these suunto diving computers:

  • Suunto Cobra
  • Suunto Vyper 1 & 2
  • Suunto Stinger
  • Suunto Mosquito
  • Suunto Vytec DS
  • Suunto Gekko

Communication

The protocol uses 2400 8O1, which means 2400 baud with 8 bits, odd parity and 1 stop-bit. The check for the interfaces uses 2400 8N1, but it also works with 2400 8O1. This was the trivial part… But some lines have a special meaning with the interface!

The DTR line should always be set. It is used as a power supply for the interface.

RTS is a toggle for the direction of the half-duplex interface. To send a command to the interface, set RTS. When you await a command, clear RTS. Timing also seems to be critical! To make sure that all data is sent to the interface, before clearing RTS, I wait about 200ms. After clearing RTS, I also have to wait 400ms and use a 500ms timeout for receiving data.

The test for the existance of the interface is simple: the computer sends AT plus a CR ($41, $54, $0D) and awaits the same answer.

Transfer

The protocol for the [[Suunto Spyder]], Vyper and Cobra are identical, but the memory layout of the Spyder is different. All data is send in packages to and from the computer. Every package is followed by a CRC for checksum reasons.

unsigned char checksum = 0x00;
for(int i=0; i<packageLen; ++i)
 checksum ^= package[i];
  • Read memory
    • to Cobra: 05 + addr_high + addr_low + count (1..32) + CRC
    • from Cobra: 05 + addr_high + addr_low + count (1..32) + n Bytes + CRC
  • Write memory
    • to Cobra: 06 + addr_high + addr_low + count (1..31)+ n Bytes + CRC
    • from Cobra: 06 + addr_high + addr_low + count (1..31) + CRC
  • Used before every ‘Write memory’ call
    • to Cobra: 07 + $a5 + CRC
    • from Cobra: 07 + $a5 + CRC
  • Get first dive profile
    • to Cobra: 08 + $a5 + CRC
    • from Cobra: 08 + count (1..32) + n Bytes + CRC (if more than 32 bytes needs to be transmitted, they are split in 32 byte packages)
  • Get next dive profile
    • to Cobra: 09 + $a5 + CRC
    • from Cobra: 09 + count (1..32) + n Bytes + CRC (if more than 32 bytes needs to be transmitted, they are split in 32 byte packages)

The DiveManager Software from Suunto does the following when reading data from a Cobra: – Check for correct computer – Read Memory ($24, 1) – Read the internal memory: – Read Memory ($1E, 14) – Read Memory ($2C, 32) – Read Memory ($53, 30) – Get first dive profile – Get next dive profile – … – If “next dive profile” returns a null package (count = 0), the end of the log memory is reached.

Memory layout

Default name of the LOG file: ‘‘PROFILE.VPR’’

  offset |     format   |  testvalue  | content
$00-$1D MSB binary unused by the PC software, probably configuration values for the computer
$1E-$1F MSB binary $6038 max. depth in ft * 128.0
$20-$21 MSB binary total dive time in minutes
$22-$23 MSB binary total number of dives
$24 MSB binary $0C Type of the computer ($03:Stinger, $04:Mosquito, $0A:new Vyper, $0C:Vyper or Cobra, $0B:Vytec, $0D:Gekko)
$25 MSB binary firmware version (30: Cobra, 33: Cobra, 21: Stinger, 15: Mosquito), the minor part is always .0.0
$26-$29 MSB binary $00005A38 serial number e.g. a Vyper: $00.01.04.41 (= 00010465)
$2A-$2B MSB binary $0000 unknown
$2C-$49 ASCII Vyper Diver personal information (“Vyper Diver”, “Cobra Diver”, “ACW Diver”, “Mosquito Diver” fill with zero bytes, if changed by the user filled to the maximum length with spaces)
$4A-$50 MSB binary $00..$00 unknown (only the first 2 bytes are read by DM software)
$51-$52 MSB binary Ptr to the last $82 byte in the profile ringbuffer (not read by DM software)
$53 MSB binary $14 interval (10s, 20s, 30s or 60s)
$54 MSB binary altitude and personal settings (height (0..2) + 3 * personal (0..2))
$55-$5B MSB binary unknown ($0E.05.1F.FF.1F.FF.01 : identical on all Suunto computers?)
$5C-$5D MSB binary $0000 max. freediving depth (only on Mosquito and Stinger) in ft * 128.0
$5E-$5F MSB binary $0000 total freediving time in minutes (only on Mosquito and Stinger)
$60 MSB binary $01 12/24 hour flag, 00 = 24 hours, 01 = 12 hours plus AM/PM
$61 MSB binary $00 unknown
$62 MSB binary $01 0 = imperial, 1 = metric
$63 MSB binary $01 Model (0: Air, 1:Nitrox/EAN, 2:Gauge; Mosquito, Stinger: Bit 3 set: Diving active, Bit 7 set: Free Diving active)
$64 MSB binary $8A Light (Bit 7: on; Bit 0..6: time in s)
$65 MSB binary $03 Bit 0: dive time alarm = on, Bit 1: dive depth alarm = on
$66-$67 MSB binary $0037 dive time for the alarm in minutes (max. 999 minutes, normal max. 4:59)
$68-$69 MSB binary $3138 depth for the alarm in ft * 128.0 (rounded to 0.5m or 1ft; valid: 0m, 3m–150m (Cobra, Vyper: 100m))
$6A-$70 MSB binary unknown ($46.00.00.00.00.00.00)
$71-$1FFF MSB binary ring buffer for the profile memory

The ring-buffer is a stream of data, which ends at the position, that is marked in the header. At this position the computer starts writing the information from the next dive. If the write pointer reaches the value $2000, it jumps back to $71.

Format for one dive

offset format description
0 MSB binary 00, with Nitrox: OLF % * 2 ($32 * 2 = 100 CNS), if bit 7 is set: OTU instead of CNS
1 MSB binary pressure at the end of the dive in bar / 2
2 MSB binary temperature at the end of the dive in degress celcius
3 MSB binary temperature at the max depth in degress celcius
4… binary profile data from the end to the beginning of the dive,this means reverse order!
n MSB binary minutes at the beginning of the dive
n + 1 MSB binary hours at the beginning of the dive
n + 2 MSB binary day at the beginning of the dive
n + 3 MSB binary month at the beginning of the dive
n + 4 MSB binary year at the beginning of the dive (90..99 = 1990..1999, 00..89 = 2000..2089)
n + 5 MSB binary temperature of the air in degress celcius
n + 6 MSB binary ? unused? (PO2??? 0=1.2, 1=1.3, 2=1.4, 3=1.5, 4=1.6???)
n + 7 MSB binary Oxygen in % (= Nitrox mode, air mode: 0); Bit 6 & 7 are ignored.
n + 8 MSB binary pressure at the beginning of the dive in bar / 2
n + 9 MSB binary altitude and personal settings (height (0..2) + 3 * personal (0..2)). Bit 6:Gauge active (on Stinger and Mosquito)
n + 10 MSB binary interval (10s, 20s, 30s or 60s)
n + 11 MSB binary dive number in the Vyper/Cobra (for repetitive dives)
n + 12 MSB binary hours of the surface interval
n + 13 MSB binary minutes of the surface interval

Profile information

The profile data is a stream of bytes. Every minute (or 30s or 20s – see the profile interval) a byte is recorded. This byte is the delta depth in ft to the last depth! E.g. you start your dive at a depth of 0 feet go down to 30ft in a minute, so the value is –30ft (because you go 30ft down) or $E2 in binary, if you then go up to 20ft, the next value will be +10ft (because you go 10ft up) or $0A in binary.

Some values have special meanings:

Byte Type Description
$79 unused
$7a Slow The diver ascended above dive depth limit. This symbol marks every interval in which the SLOW indicator appeared
$7b Attn/Violation Dive Attention Mark as described in the dive computer user manual
$7c Bookmark/Heading The diver pressed the PLAN button during this interval
$7d Surfaced The diver ascended above the minimum diving depth (1.2m) during this interval
$7e Deco A decompression ceiling first appeared during this interval. The dive computer’s ASC indicator appeared
$7f Ceiling The diver ascended above the decompression ceiling during this interval
$80 End end of the dive. The next byte is n + 1 in the format description of the dive
$81 Safety Stop The diver ascended above a mandatory safety stop ceiling during this interval
$82 End of data set after the last dive (written after the dive as a marker, so technically not profile information)
$83 Increased workload (not generated by the computers)
$84 unused
$85 Cold water (not generated by the computers)
$86 unused
$87 Gas change Vytec only: switched to a different gas. The following byte contains the percent of oxygen in that gas

Necessary conversions

meter = (int)(feet * 0.3048 * 10) / 10
psi = bar * 14.50377377
fahrenheit = celcius * 1.8 + 32

Altitude:

value meters feet
0 700m 2300ft
1 1500m 5000ft
2 2400m 8000ft

ATTN: the computers don’t round after the 2. digit, when calculating feet => meter! They cut it after the 2. digit. This results to the modified formula.

PC Protocol of Suunto EON/Solution/Vario

This document describes the transfer protocol and memory layout of these suunto diving computers:

  • Suunto Eon
  • Suunto Eon Lux
  • Suunto Solution Alpha
  • Suunto Solution Alpha Lux
  • Suunto Solution Nitrox
  • Suunto Vario (same as the Nitrox model)

The [[Suunto Solution]] is an older model with a different communication scheme.

Open Questions / Unknown things

  • Does more than the 3 bytes for the transmission exist? I checked all letters, but there may be others?!?
  • Any way to set the time of the EON? I don’t think so 🙁
  • Does the EON really has a 3 byte counter for the number of dives? Or is it only 2 bytes and the first byte has a special meaning?
  • What meanings have the other bytes in the header (32..255 with the exception of the serial number in 244..246)? I think 32..210 are unused, but the other contains constant (they never changed at my EON) data.
  • How does the PC software find out, where the first dive in the log starts? At the moment I get the $82 position from the header and search for next $80 (end of dive), skip the temperature and pressure byte and – viola – the first (oldest) dive.
  • information about the Nitrox model missing

Communication

The protocol uses 1200 8N2, which means 1200 baud with 8 bits, no parity and 2 stop-bits.

Transfer

You can send the following data to the EON:

  • ‘N’ ($4E) followed by 20 bytes, sets the owner name of the dive computer.
  • ‘T’ ($54) followed by 1 byte ($14, $1E, $3C, which are 20s, 30s and 60s) sets the data rate for the diving profile. Other values may be possible, so the profile values may be recorded with a 1s to a 255s interval – but they should not be used (information from the developer of the EON)
  • ‘P’ ($50) The computer sends $901 bytes (I call it the P-block) back to the computer.

Format of the P-block

The P-block has a $100 byte header and a $800 byte ring-buffer for the profile data plus a one byte checksum. The ring-buffer on a new interface only consists of $FF bytes.

The last byte after the P-block is a checksum: c
unsigned char chk = 0x00;
for(int i=0; i<0x900; ++i)
chk += buf[i];

The default name of the LOG files are: * ‘‘PROFILE.EON’’ for the Suunto Eon * ‘‘PROFILE.SOA’’ for the Suunto Solution Alpha * ‘‘PROFILE.SNV’’ for the Suunto Solution Nitrox/Vario

Header of the P-block

offset |     format     |  testvalue  | content
0–2 MSB binary $00015E number of dives made with the computer (here: 180; from the history). I am not sure, if the upper byte is used (I haven’t made 65536 dives yet 🙂
3–4 MSB binary $0258 divetime under water in minutes (here: 600; from the history)
5–6 MSB binary $3F46 max. depth (here: $3F46 / 128.0 = 126.5 feet; from the history)
7–8 MSB binary $0100 $900 – this value = offset after the last dive (here: $800)
9 MSB binary $3C current profile interval (here: 60s)
10 MSB binary $18 altitude settings (value / 32 = Altitude, here: $18 / 32 = 0, so A0) [bit 0: unknown (always = 0?), bit 1: nitrox, bit 2: unknown (always = 0?), bit 3: metric, bit 4: air (= EON)]
11 MSB binary $60 current year – 1900 (here: 96 => 1996, 00 would be 2000)
12..31 ASCII “EON – Markus Fritze ” 20 bytes string, that is filled with spaces. Not zero terminated! If the owner name is never set, it contains $FF..$FF
32..210 binary $FF unused?
211..243 binary $?? unknown
244..246 BCD $502159 serial number of the computer (here: 502159)
247..255 binary $?? unknown

Ring-Buffer of the P-block

The ring-buffer is a stream of data, which ends at the position, that is marked in the header. At this position the computer starts writing the information from the next dive. If the write pointer reaches the value $900, it jumps back to $100.

Format for one dive

offset format testvalue content
0 MSB binary $05 surface interval (minutes)
1 MSB binary $01 surface interval (hours) (here: 1:05)
2 MSB binary $01 repetitive dive counter (here: the first dive). The counter resets to 1, when the computer turns off after a longer pause. Every dive before that is a repetitive dive and increments the counter.
3 MSB binary $3C profile interval for this dive (here: 60s), this is necessary, because the interval may be changed after every dive and the ring-buffer still contains information about dives with other profile intervals.
4 MSB binary $38 altitude settings (value / 32 = Altitude, here: $18 / 32 = 0, so A0) – bit 0: unknown (always = 0?), bit 3: metric, bit 4: air (= EON)
5 MSB binary $64 A solution alpha always transmits a 0. on nitrox: nitrogen level
6 BCD $97 year (here: 1997). Year 2000 is $00
7 BCD $01 month (here: 1 = January)
8 BCD $31 day (here: 31)
9 BCD $13 hour (here: 13 or 1pm)
10 BCD $35 minute (here: 35)
11 … binary profile data
n MSB binary $80 end of the dive marker
n + 1 MSB binary $3C temperature at the dive in degree celcius – 40 (here: 60 – 40 = 20 degree celcius)
n + 2 MSB binary $19 tank preassure at the end of the dive in bar (here: 25 * 2 = 50bar) – A solution alpha always transmits a 0.

Profile information

The profile data is a stream of bytes. Every minute (or 30s or 20s – see the profile interval) a byte is recorded. This byte is the delta depth in ft to the last depth! E.g. you start your dive at a depth of 0 feet go down to 30ft in a minute, so the value is –30ft (because you go 30ft down) or $E2 in binary, if you then go up to 20ft, the next value will be +10ft (because you go 10ft up) or $0A in binary.

Some values have special meanings:

Byte Type Description
$7d Surfaced you have reached the surface while (or after) the dive
$7e ASC dive now is a decompression dive
$7f ERR decompression missed
$80 End end of the dive.
$81 Slow Slow warning while the dive. If the dive ends with $7d8180 (Surfaced, Slow, End) it means, you finished the dive with a blinking SLOW warning in the display.
$82 End of data set after the last dive (written after the dive as a marker, so technically not profile information)

Necessary conversions

meter = (int)(feet * 0.3048 * 10) / 10
psi = bar * 14.50377377
fahrenheit = celcius * 1.8 + 32

Altitude:

value meters feet
0 700m 2300ft
1 1500m 5000ft
2 2400m 8000ft

ATTN: the computers (checked on the solution alpha) don’t round after the 2. digit, when calculating feet => meter! They cut it after the 2. digit. This results to the modified formula.

PC Protocol of Suunto EON/Solution/Vario

This document describes the transfer protocol and memory layout of these Suunto diving computers:

  • Suunto Eon
  • Suunto Eon Lux
  • Suunto Solution Alpha
  • Suunto Solution Alpha Lux
  • Suunto Solution Nitrox
  • Suunto Vario (same as the Nitrox model)

The [[Suunto Solution]] is an older model with a different communication scheme.

Open Questions / Unknown things

  • Does more than the 3 bytes for the transmission exist? I checked all letters, but there may be others?!?
  • Any way to set the time of the EON? I don’t think so 🙁
  • Does the EON really has a 3 byte counter for the number of dives? Or is it only 2 bytes and the first byte has a special meaning?
  • What meanings have the other bytes in the header (32..255 with the exception of the serial number in 244..246)? I think 32..210 are unused, but the other contains constant (they never changed at my EON) data.
  • How does the PC software find out, where the first dive in the log starts? At the moment I get the $82 position from the header and search for next $80 (end of dive), skip the temperature and pressure byte and – viola – the first (oldest) dive.
  • information about the Nitrox model missing

Communication

The protocol uses 1200 8N2, which means 1200 baud with 8 bits, no parity and 2 stop-bits.

Transfer

You can send the following data to the EON:

  • ‘N’ ($4E) followed by 20 bytes, sets the owner name of the dive computer.
  • ‘T’ ($54) followed by 1 byte ($14, $1E, $3C, which are 20s, 30s and 60s) sets the data rate for the diving profile. Other values may be possible, so the profile values may be recorded with a 1s to a 255s interval – but they should not be used (information from the developer of the EON)
  • ‘P’ ($50) The computer sends $901 bytes (I call it the P-block) back to the computer.

Format of the P-block

The P-block has a $100 byte header and a $800 byte ring-buffer for the profile data plus a one byte checksum. The ring-buffer on a new interface only consists of $FF bytes.

The last byte after the P-block is a checksum: c
unsigned char chk = 0x00;
for(int i=0; i<0x900; ++i)
chk += buf[i];

The default name of the LOG files are: * ‘‘PROFILE.EON’’ for the Suunto Eon * ‘‘PROFILE.SOA’’ for the Suunto Solution Alpha * ‘‘PROFILE.SNV’’ for the Suunto Solution Nitrox/Vario

Header of the P-block

offset |     format     |  testvalue  | content
0–2 MSB binary $00015E number of dives made with the computer (here: 180; from the history). I am not sure, if the upper byte is used (I haven’t made 65536 dives yet 🙂
3–4 MSB binary $0258 divetime under water in minutes (here: 600; from the history)
5–6 MSB binary $3F46 max. depth (here: $3F46 / 128.0 = 126.5 feet; from the history)
7–8 MSB binary $0100 $900 – this value = offset after the last dive (here: $800)
9 MSB binary $3C current profile interval (here: 60s)
10 MSB binary $18 altitude settings (value / 32 = Altitude, here: $18 / 32 = 0, so A0) [bit 0: unknown (always = 0?), bit 1: nitrox, bit 2: unknown (always = 0?), bit 3: metric, bit 4: air (= EON)]
11 MSB binary $60 current year – 1900 (here: 96 => 1996, 00 would be 2000)
12..31 ASCII “EON – Markus Fritze ” 20 bytes string, that is filled with spaces. Not zero terminated! If the owner name is never set, it contains $FF..$FF
32..210 binary $FF unused?
211..243 binary $?? unknown
244..246 BCD $502159 serial number of the computer (here: 502159)
247..255 binary $?? unknown

Ring-Buffer of the P-block

The ring-buffer is a stream of data, which ends at the position, that is marked in the header. At this position the computer starts writing the information from the next dive. If the write pointer reaches the value $900, it jumps back to $100.

Format for one dive

offset format testvalue content
0 MSB binary $05 surface interval (minutes)
1 MSB binary $01 surface interval (hours) (here: 1:05)
2 MSB binary $01 repetitive dive counter (here: the first dive). The counter resets to 1, when the computer turns off after a longer pause. Every dive before that is a repetitive dive and increments the counter.
3 MSB binary $3C profile interval for this dive (here: 60s), this is necessary, because the interval may be changed after every dive and the ring-buffer still contains information about dives with other profile intervals.
4 MSB binary $38 altitude settings (value / 32 = Altitude, here: $18 / 32 = 0, so A0) – bit 0: unknown (always = 0?), bit 3: metric, bit 4: air (= EON)
5 MSB binary $64 A solution alpha always transmits a 0. on nitrox: nitrogen level
6 BCD $97 year (here: 1997). Year 2000 is $00
7 BCD $01 month (here: 1 = January)
8 BCD $31 day (here: 31)
9 BCD $13 hour (here: 13 or 1pm)
10 BCD $35 minute (here: 35)
11 … binary profile data
n MSB binary $80 end of the dive marker
n + 1 MSB binary $3C temperature at the dive in degree celcius – 40 (here: 60 – 40 = 20 degree celcius)
n + 2 MSB binary $19 tank preassure at the end of the dive in bar (here: 25 * 2 = 50bar) – A solution alpha always transmits a 0.

Profile information

The profile data is a stream of bytes. Every minute (or 30s or 20s – see the profile interval) a byte is recorded. This byte is the delta depth in ft to the last depth! E.g. you start your dive at a depth of 0 feet go down to 30ft in a minute, so the value is –30ft (because you go 30ft down) or $E2 in binary, if you then go up to 20ft, the next value will be +10ft (because you go 10ft up) or $0A in binary.

Some values have special meanings:

Byte Type Description
$7d Surfaced you have reached the surface while (or after) the dive
$7e ASC dive now is a decompression dive
$7f ERR decompression missed
$80 End end of the dive.
$81 Slow Slow warning while the dive. If the dive ends with $7d8180 (Surfaced, Slow, End) it means, you finished the dive with a blinking SLOW warning in the display.
$82 End of data set after the last dive (written after the dive as a marker, so technically not profile information)

Necessary conversions

meter = (int)(feet * 0.3048 * 10) / 10
psi = bar * 14.50377377
fahrenheit = celcius * 1.8 + 32

Altitude:

value meters feet
0 700m 2300ft
1 1500m 5000ft
2 2400m 8000ft

ATTN: the computers (checked on the solution alpha) don’t round after the 2. digit, when calculating feet => meter! They cut it after the 2. digit. This results to the modified formula.

Newton Keyboard Enabler

The Newton Keyboard Enabler (NKE) allows you to connect the Newton keyboard to any Macintosh and use it as a replacement for the big ADB keyboards. Very nice for presentations where space is rare.

The NKE is a system extension, but it does not do any trap patching. To be exact: the NKE is a FBA (faceless background application), that works like a normal application, but without any human interface. The NKE should work on any Macintosh with Mac OS 7 and at least one free serial port. NKE will grab the serial port, so it is not possible for any other application to use the port.

You may quit the NKE with a “Quit” AppleEvent or with the hotkey Control-Option-^. Hold the three keys for about 2 seconds down, if it doesn’t work: retry. If you are lucky, you hear three beeps, then you have to press a key on the Newton keyboard. After that, the NKE has quit.

The Newton Keyboard Enabler is freeware. You may use it as you wish, but need written permission for any commercial distribution!

Configuring the NKE

The configuration is a bit tricky, you have to use ResEdit or Resorcerer!

  • Choose the serial port — Open NKE with ResEdit and open the STR# 128 Resource. For the modem port (the default) enter “.AIn” and “.AOut”; for the printer port enter “.BIn” and “.BOut”. Pay attention to internal modems on PowerBooks etc.
  • Choose the keyboard layout and language — Open the PREF 128 resource (attention: only a Resorcerer template is available, ResEdit users have to use the hex-editor). The resource is 4 bytes long:
    • The last byte is the keyboard language. 3 is default (Germany), you may want to change it to 0 (US-English).
    • The first byte (default: 0 = off) allows you to send different codes for the shift keys of the Newton keyboard (set the value to 1). You should leave this value unchanged.

After installing NKE (dragging it onto the system folder) and connecting the keyboard to the right serial port, restart your Macintosh. If NKE can’t recognize the keyboard, you may have to check the serial port. If the keyboard layout is wrong, you have to change the keyboard language.

Hint: You may change the Filetype of NKE from “appe” to “APPL” and launch NKE directly from your desktop – no restart necessary!

The current version

NKE MacOpen Artikel

Newton Keyboard am Mac

Dieses mal will ich etwas zu dem Anschluß eines Newton Keyboards am Mac schreiben. Das Newton Keyboard ist nämlich durch seine kleinen Abmessungen ideal für Messen etc. zu verwenden.

Der Anschluß

Das Newton Keyboard ist für etwas über 100,- DM im Apple Fachhandel zu erhalten. Das kleine schwarze Keyboard wird inklusiver einer Tasche geliefert. Da es beim Newton an die serielle Schnittstelle angeschlossen wird, kam mir die Idee das Keyboard an einen Mac anzuschließen. Mit Hilfe eines Terminalprogramms habe ich schnell ermitteln können, daß das Keyboard mit 9600 Baud seine Daten an den Rechner sendet – 8 Bits pro Zeichen, keine Parität, ein Stop-Bit. Elektrisch war die Ansteuerung also kein Problem.

Nun mußte ich rausbekommen, was für Daten von der Tastatur gesendet werden. Auch hier: keine Überraschungen. Die Tastatur sendet bei jedem Tastendruck bzw. beim Loslassen einer Taste ein Byte. Unterscheiden kann man das Drücken bzw. Loslassen einer Taste daran, daß beim Loslassen Bit 7 gesetzt wird. Der 7 Bit Scancode entspricht quasi zu 100% denen der normalen Mac-Keyboards.

Sobald die Schnittstelle geöffnet wird, sendet das Keyboard einen String mit Identifikationsdaten. Leider habe ich keine genaueren Informationen über den Aufbau der Daten, es reicht jedoch zur Erkennung der Newton-Tastatur.

Die Software

Die Aufgabe eines Newton Keyboard Treibers ist also recht klar umrissen: Serielle Schnittstelle öffnen, die ersten Bytes ignorieren, dann bei jedem gesendeten Byte entweder das Drücken oder das Loslassen einer Taste melden. So weit so gut. Nur wie macht man das?

Ich wollte versuchen keine Systemerweiterung zu schreiben, die irgendwelche Traps patched, sondern dachte zuerst: das muß ja einfach sein, einfach per PostEvent() das Drücken und Loslassen melden und fertig. Leider ist es “etwas” komplizierter…

Der einfache Teil ist klar: ich schreibe eine “faceless background application” kurz FBA. Ein FBA verhält sich für den normalen Anwender, wie eine Systemerweiterung und wird auch in dem Ordner installiert. Für das MacOS handelt es sich aber um ein ganz normales Mac-Programm mit Event-Loop. FBAs dürfen jedoch – mit Ausnahme des Notification Managers – keine Bildschirmausgaben tätigen. Für einen Tastaturtreiber stellt dies aber kein Problem dar. FBAs erlauben AppleEvents und so kann man sehr elegant kleinere Erweiterungen für viele Programme als FBA implementieren z.B. AppleScript-Erweiterungen für FileMaker Pro.

Die serielle Schnittstelle

Das Öffnen der Schnittstelle mit OpenDriver() ist relativ einfach und klar. Die Namen der Ports habe ich in den Resource-Fork abgelegt, so daß man sie bei Bedarf leicht ändern kann. Besser wäre natürlich ein Kontrollfeld, wo man den Port direkt wählen kann – oder die Möglichkeit den Port automatisch zu ermitteln. Die Baudrate setze ich danach auf 9600 8N1 – ist normalerweise eh Standard, aber sicher ist sicher. Handshaking ist für die Tastatur nicht nötig. Um der Tastatur die Möglichkeit zu geben ihre Identifikation zu senden, wartet der Treiber 1/2 Sekunde und liest dann alle an der seriellen Schnittstelle anliegenden Bytes ein. Welche Bytes erwartet werden, kann man dem Listing entnehmen. Ein kleiner Plausibilitätstest stellt sicher, daß auch wirklich eine Newton Tastatur angeschlossen ist.

Nun kommt aber der Trick: anstatt im Event-Loop die serielle Schnittstelle zu pollen, lese ich asynchron von der Schnittstelle! Dies belastet das MacOS fast gar nicht und bei jedem Byte, das an der Schnittstelle ankommt, wird automatisch eine Callback-Routine von mir aufgerufen. Dort verarbeite ich das gelesene Byte und starte den nächsten Lesevorgang. Einzige Falle ist hier, daß man den Parameterblock und andere Variablen globale halten muß, denn auf dem lokalen Stack würden sie ja beim Verlassen des Unterprogramms gleich gelöscht werden. Abstürze wären die Folge.

In der Testphase hat es sich als nützlich erwiesen, wenn man den Treiber beenden kann. Dazu setzt man den Dateityp nicht auf ‘appe’ (der Filetype von FBAs), sondern ganz normal auf ‘APPL’. Nun kann man den Treiber einfach per Doppelklick starten. Per Hot-Key (Control-Option-^) bzw. Quit-AppleEvent verläßt der Treiber nun den Event-Loop. Nach drei SysBeeps() wartet er auf ein letztes Byte an der serielle Schnittstelle und beendet sich dann selbst. Den Hot-Key muß man etwas länger drücken, denn der Event-Loop wird nur einmal pro Sekunde durchlaufen. Normalerweise braucht der Treiber ja diesen Event-Loop gar nicht und so kostet er wenigstens kaum Rechenzeit.

Die Scancodes

Die Tastatur meldet ja nur ein Byte für einen Tastendruck. Dieses Byte (Scancode genannt) muß nun in einen ASCII-Code umgerechnet werden. Dazu gibt es im MacOS die KMAP- und KCHR-Resourcen. KMAP erlaubt es Scancodes von verschiedenen Tastaturen zu normalisieren, d.h. auf gleiche Scancodes umzurechnen. Jede Tastatur kann ja völlig eigene Scancodes senden. Ferner wird in der KMAP-Resource definiert, bei welchen Tastendrücken die LEDs am erweiterten Keyboard angeschaltet werden. Das Newton Keyboard hat keine LEDs, so erübrigt sich weiterer Aufwand in diese Richtung.

Ebenfalls muß das Verhalten der Capslock-Taste simuliert werden. Beim Newton Keyboard wird sie ja nicht mechanisch blockiert. Man muß also per Software einen Caps-Schalter implementieren.

Den erzeugten normalisierten Keycode muß man nun in die KeyMap-Bitmaske übertragen. Diese Bitmaske erhält man, wenn man die Funktion GetKeys() aufruft. Viele Programme tun dies und wir müssen diese Bitmaske selbst aktualisieren. Hierbei gibt es folgendes zu beachten: es können mehrere Tastaturen angeschlossen sein! Jede Tastatur braucht ihre eigene interne KeyMap, die bei jedem Tastendruck einfach in die KeyMap des Systems kopiert wird.

Ferner müssen noch einige (undokumentierte) globale Variablen richtig gesetzt werden, damit u.a. Auto-Repeat funktioniert. Diese Variablen habe ich durch Analyse des originalen ADB-Treibers von Apple gefunden. Nunja, eine andere Möglichkeit kenne ich nicht und Auto-Repeat ist irgendwie zu witzig, als das man es nicht unterstützt.

Zu guter Letzt wird der Scancode in einen (oder zwei) ASCII-Codes umgerechnet. Dazu braucht man lediglich die Funktion KeyTranslate() aufrufen. Diese Funktion bekommt eine KCHR-Routine übergeben, welche beschreibt, wie man einen Scancode in einen ASCII-Code umrechnet. Unterschiedliche KCHR-Resourcen erlauben es z.B. für verschiedene Sprachen verschiedene Tastaturlayouts zu unterstützen. Die Scancodes sind nämlich – trotz unterschiedlich beschrifteter Tastenkappen – gleich! Es gibt aber noch ein paar Dinge zu beachten: Die Funktion kann zwei ASCII-Codes erzeugen, da einige Tastaturen auf einen realen Tastendruck zwei Tastendrücke simulieren. Ich glaube in Japan ist soetwas nicht unüblich. Ferner muß man den Status “state” global halten. In diesem Status wird zwischen zwei Tastendrücken gespeichert, ob z.B. ein Dead-Key gedrückt wurde. Beispiel: man drückt ein Taste, die ein Akzent einleitet und danach eine Taste, auf welche das Akzent gesetzt werden soll.

Zu guter Letzt wird der Tastendruck als keyDown bzw. keyUp Message an MacOS gepostet. Natürlich nicht ohne vorher noch ein paar undokumentierte Variablen im MacOS zu setzen. Nunja.

Die Kompilierung

Um das Programm zu kompilieren muß lediglich der Source, wie auch die Resource kompiliert werden. Das Programm bekommt als Dateityp ‘appe’ und als Creatorcode ‘NwtK’ zugewiesen. 64k Speicherzuteilung reichen dicke. Lediglich die Flags müssen sorgfältig gesetzt werden (siehe Grafik). Wer will, kann den Dateityp auf ‘APPL’ setzen und den Treiber per Doppelklick bei Bedarf starten.

Um die serielle Schnittstelle zu ändern, muß man lediglich in der STR# 128 Resource die Einträge von “.AIn” und “.AOut” (Modemanschluß) z.B. auf “.BIn” und “.BOut” (Druckeranschluß) ändern.

In der PREF 128 Resource kann man sowohl das Keyboard Layout umschalten: das letzte Byte ist 3 für Deutschland oder 0 für US-Englisch. Weitere Layouts sind möglich, man muß lediglich entsprechende KCHR-Resourcen zur Verfügung stellen. Üblichweise sind seit MacOS 7.5 alle internationalen Keyboard-Layouts bereits im System vorhanden.

Das erste Byte ermöglicht es zwischen der KMAP 0 und KMAP 1 Resource zu wählen. Diese Layouts unterscheiden sich durch die Behandlung der gemeinsamen Shift-Tasten. Üblich ist es, daß beide Shift, Command, etc. Tasten den gleichen Scancode an die Anwenderprogramme melden. Wählt man die KMAP 1 Resource, so werden diese Tasten unterschiedlich gemeldet. Programme können dies zwar per Software umschalten (MagicMac nutzt dies z.B.), aber dies erfordert die direkte Programmierung der ADB-Tastaturen, was bei unserer Newton Tastatur an der seriellen Schnittstelle halt nicht möglich ist.

Die aktuelle Version des Newton Keyboard Enablers findet sich auf http://www.emagic.de/mmm/ – neben vielen anderen Programmen von mir.

########################################
############### LISTINGs ###############
########################################
/***
 *  Newton Keyboard Enabler.c
 *
 *  Erlaubt die Nutzung eines seriellen Newton-
 *  Keyboards an einem Mac. Das Keyboard verhält
 *  sich in fast allen Fällen genau wie ein
 *  originales ADB-Keyboard (Ausnahme: MacsBug
 *  kann es nicht nutzen)
 *
 *  Entwickelt mit dem CodeWarrior 9 von
 *  Metrowerks.
 *
 *  (c)1996 MAXON Computer, Markus Fritze
 ***/

// c_moeller@macopen.com

// true, wenn das Programm beendet werden soll
Boolean gDoQuitFlag;

/***
 *  unsere AppleEvent-Routinen
 *  (schließlich sind wir ein ordentliches
 *  MacOS Programm)
 ***/
static pascal OSErr DoAENoErr(
    const AppleEvent*, AppleEvent*, long)
{
  return noErr;   // AppleEvent ist ok
}

static pascal OSErr DoAEQuitAppl(
    const AppleEvent*, AppleEvent*, long)
{
  gDoQuitFlag = true; // Programm beenden
  return noErr;
}


// einen (hoffentlich) undefinierten Code
// benutzen wir als ID-Code für die Tastatur
#define NEWTON_KEYBOARD_CODE    117L


// Zugriffsfunktionen ähnlich <LowMem.h>
// für den Tastaturtreiber
static inline SInt16 LMGetKeyLast()
        { return *(SInt16*)0x0184; };
static inline void LMSetKeyLast(SInt16 value)
        { *(SInt16*)0x0184 = value; };
static inline SInt16 LMGetHiKeyLast()
        { return *(SInt16*)0x0216; };
static inline void LMSetHiKeyLast(SInt16 value)
        { *(SInt16*)0x0216 = value; };

static inline SInt32 LMGetKeyTime()
        { return *(SInt32*)0x0186; };
static inline void LMSetKeyTime(SInt32 value)
        { *(SInt32*)0x0186 = value; };
static inline SInt32 LMGetKeyRepTime()
        { return *(SInt32*)0x018A; };
static inline void LMSetKeyRepTime(SInt32 value)
        { *(SInt32*)0x018A = value; };

// ohne "inline", wegen eines 68k Compilerbugs
// beim CodeWarrior 9
static /*inline*/ KeyMap *LMGetKeyMapPtr()
        { return (KeyMap*)0x0174; };

// Unsere globalen Variablen für die Tastatur
Handle      gKMAP;
Handle      gKCHR;
UInt8     gKeyMap[16];

/***
 *  Keyboard-Variablen initialisieren
 ***/
static void InitKeyboard()
{
  Handle  thePref =
    ::Get1Resource('PREF', 128);

  // eigener Typ: Newton Keyboard
  gKMAP = ::Get1Resource('KMAP', **thePref);
  if(!gKMAP) ::ExitToShell();
  ::HLockHi(gKMAP);

  // ein deutsches Keyboard:
  gKCHR = ::GetResource('KCHR',
          ((short*)*thePref)[1]);
  if(!gKCHR)
    // ein US-Keyboard:
    gKCHR = ::GetResource('KCHR', 0);
  if(!gKCHR) ::ExitToShell();
  ::HLockHi(gKCHR);

  // eigene Keymap löschen
  for(int i=0; i<sizeof(gKeyMap); i++)
    gKeyMap[i] = 0;

  ::ReleaseResource(thePref);
}

/***
 *  Tastencode senden
 ***/
static void PostKeyMessage(
        UInt8 inKey, UInt8 inKeyCode)
{
  // keine Taste => raus
  if(inKey == 0x00L) return;

  // Message zusammensetzen
  UInt32  theMessage = inKey
        | UInt16(inKeyCode << 8)
        | (NEWTON_KEYBOARD_CODE << 16);

  // Taste gedrückt
  if(!(inKeyCode & 0x80)) {
    SInt32  theTicks = LMGetTicks();
    LMSetKeyTime(theTicks);
    LMSetKeyRepTime(theTicks);
    LMSetKeyLast(theMessage);
    LMSetHiKeyLast(NEWTON_KEYBOARD_CODE);
    ::PostEvent(keyDown, theMessage);

  // Taste losgelassen
  } else {
    // Key-Up-Flag löschen
    theMessage &= 0xFFFF7FFF;
    ::PostEvent(keyUp, theMessage);
  }
}

/***
 *  Tastendruck (bzw. das Loslassen) dem MacOS
 *  melden
 ***/
static void EnterKeycode(UInt8 inCode)
{
  // aktuelle Taste im System löschen
  LMSetKeyLast(0);
  LMSetHiKeyLast(0);

  // true, wenn Taste losgelassen wurde
  Boolean theDownFlag =
                (inCode & 0x80) == 0x80;

  // MacOS-Keycode erzeugen
  UInt8 theKeyCode;
  Ptr   theKMAP = *gKMAP;
  theKeyCode = theKMAP[(inCode & 0x7F) + 4];
  // Sondercode erkannt?
  if(theKeyCode & 0x80) {

    // erstmal das Kennungs-Bit löschen
    theKeyCode &= 0x7F;

    // Anzahl der Sondereinträge
    SInt16  theCount =
      *reinterpret_cast<SInt16*>
            (&theKMAP[0x84]);

    // ab hier geht es mit den Tabellen los
    UInt8 *theKMapP =
      reinterpret_cast<UInt8*>
            (&theKMAP[0x86]);
    while(theCount-- > 0) {
      // Code gefunden?
      if(*theKMapP++ != theKeyCode) {
        // zum nächsten Eintrag
        theKMapP += theKMapP[1] + 2;
        continue;
      }
      if((*theKMapP & 0x0F) == 0x00)
        return;
      break;
    }
  }

  // Capslock Abfrage
  if(theKeyCode == 0x39) {
    if(theDownFlag) { // Taste gedrückt?

      // Caps bereits gesetzt?
      if(gKeyMap[theKeyCode >> 3]
        & (1 << (theKeyCode & 7))) {
        // dann lösen!
        theDownFlag = false;
      }
    } else {  // Taste losgelassen?
      // (das interessiert uns nie!)
      return;
    }
  }

  // in die KeyMap eintragen (vorerst nur in
  // die eigene)
  if(theDownFlag) {
    gKeyMap[theKeyCode >> 3] |=
        1 << (theKeyCode & 7);
  } else {
    gKeyMap[theKeyCode >> 3] &=
        ~(1 << (theKeyCode & 7));

    // Flag für "losgelassen"
    theKeyCode |= 0x80;
  }

  // Tastencodes in globalen Variablen merken
  LMSetKbdLast(theKeyCode);
  LMSetKbdType(NEWTON_KEYBOARD_CODE);

  // globale KeyMap updaten
  ::BlockMoveData(gKeyMap, LMGetKeyMapPtr(),
          sizeof(KeyMap));

  // aktuelle Modifiers für KeyTranslate lesen
  UInt16  theModifiers = *(3 +
          reinterpret_cast<UInt16*>
          (LMGetKeyMapPtr()));

  // ROL.W #1,<ea>
  theModifiers = (theModifiers >> 15)
        | (theModifiers << 1);

  // ASCII-Codes (denkbar: zwei pro
  // Tastendruck!) errechnen
  static UInt32 state = 0;
  UInt32  lStructure = ::KeyTranslate(*gKCHR,
        theKeyCode | (theModifiers << 8),
                    &state);

  // ggf. zwei Tasten posten
  PostKeyMessage(lStructure >> 16, theKeyCode);
  PostKeyMessage(lStructure, theKeyCode);
}

/***
 *  diese asynchrone Routine pollt das Keyboard
 *  an der Seriellen
 ***/
#include <Serial.h>

// UPP für die Callback-Routine
IOCompletionUPP gIOUPP;

// Refnums für Serial ein/aus
SInt16      gSDIn, gSDOut;

// das empfangene Zeichen
UInt8     gInChar;

// der Parameterblock (asynchron!)
ParamBlockRec gParamBlk;

/***
 *  das nächste Byte von der
 *  Tastatur asynchron lesen
 ***/
static void   GetNextByte()
{
  if(gDoQuitFlag) return;
  // Callback setzen
  gParamBlk.ioParam.ioCompletion = gIOUPP;
  // Port lesen
  gParamBlk.ioParam.ioRefNum = gSDIn;
  // Buffer auf unser Byte
  gParamBlk.ioParam.ioBuffer = (Ptr)&gInChar;
    // ein Byte lesen
  gParamBlk.ioParam.ioReqCount = 1L;
  // ab der aktuellen Position
  gParamBlk.ioParam.ioPosMode = fsAtMark;
  // kein Offset...
  gParamBlk.ioParam.ioPosOffset = 0L;
  // Anforderung absetzen
  PBReadAsync(&gParamBlk);
}

/***
 *  Diese Routine wird angesprungen,
 *  wenn ein Byte eingetroffen ist.
 ***/
static void   MyCompletion(
          ParmBlkPtr ioParam : __A0)
{
#pragma unused(ioParam)

  // Byte verarbeiten
  EnterKeycode(gInChar);

  // nächstes Byte anfordern
  GetNextByte();
}

/***
 *  main()
 ***/
void    main()
{
  // 16k anstatt 2k an Stack!
  ::SetApplLimit((Ptr)((UInt32)
          ::GetApplLimit() - 0x4000));

  // Crasht vor MacOS 7.5.4, falls eine zweite
  // FBA ebenfalls MaxApplZone() aufruft:
  // ::MaxApplZone();

  // weitere Init-Calls sind bei FBAs nicht
  // erlaubt
  ::InitGraf(&qd.thePort);

  // AppleEvents installieren (wenn vorhanden)
  long  response;
  if(!::Gestalt(gestaltAppleEventsAttr,
          &response)) {
    if(response &
      (1L<<gestaltAppleEventsPresent)) {

      if(::AEInstallEventHandler(
            kCoreEventClass,
            kAEOpenApplication,
            NewAEEventHandlerProc(DoAENoErr),
            0L, 0))
        return;

      if(::AEInstallEventHandler(
            kCoreEventClass,
            kAEOpenDocuments,
            NewAEEventHandlerProc(DoAENoErr),
            0L, 0))
        return;

      if(::AEInstallEventHandler(
            kCoreEventClass,
            kAEPrintDocuments,
            NewAEEventHandlerProc(DoAENoErr),
            0L, 0))
        return;

      if(::AEInstallEventHandler(
            kCoreEventClass,
            kAEQuitApplication,
            NewAEEventHandlerProc(DoAEQuitAppl),
            0L, 0))
        return;
    }
  }

  // globale Keyboard-Variablen initialisieren
  InitKeyboard();

  // ".AIn" und ".AOut" öffnen
  OSErr theErr;
  Str255  theStr;
  ::GetIndString(theStr, 128, 2);
  theErr = ::OpenDriver(theStr, &gSDOut);
  if(theErr) ::ExitToShell();
  ::GetIndString(theStr, 128, 1);
  theErr = ::OpenDriver(theStr, &gSDIn);
  if(theErr) goto raus;

  // 9600 8N1
  theErr = ::SerReset(gSDOut,
                baud9600+data8+stop10+noParity);
  if(theErr) goto raus;

  // Handshaking ausschalten
  SerShk  theSHandShk;
  theSHandShk.fXOn = 0;
  theSHandShk.fCTS = 0;
  theSHandShk.errs = 0;
  theSHandShk.evts = 0;
  theSHandShk.fInX = 0;
  theSHandShk.fDTR = 0;
  theErr = ::Control(gSDOut, 14, &theSHandShk);
  if(theErr) goto raus;

  long  theTicks;
  // 1/2 Sekunde auf das Keyboard warten
  ::Delay(30, &theTicks);

  // Anzahl der Byte an der Schnittstelle ermitteln
  SInt32  theCount;
  ::SerGetBuf(gSDIn, &theCount);

  // und alle lesen
  Str255  theBuf;
  ::FSRead(gSDIn, &theCount, &theBuf);

  // Daten von der Tastatur zum Rechner, wenn die
  // Schnittstelle angeschaltet wird (9600 8N1):
  //  <0x16><0x10> 0x02,
  //  'd_id', 0x0CL,        // Device-ID?
  //  'kybd','appl', 0x01L,   // Keyboard-Typ
  //  'nofm', 0L, 0x1003dde7L   // ???
  if(reinterpret_cast<long*>(&theBuf)[3]
        != 'ybda')
    goto raus;

  gIOUPP = NewIOCompletionProc(MyCompletion);
  GetNextByte();  // erstes Byte erwarten

  gDoQuitFlag = false;
  while(!gDoQuitFlag) {
    EventRecord theEvent;
    // nur einmal pro Sekunde erwarten wir einen
    // Null-Event!
    ::WaitNextEvent(
            everyEvent, &theEvent, 60, 0L);
    if(theEvent.what == kHighLevelEvent)
      ::AEProcessAppleEvent(&theEvent);

#if DEBUG
    // zum Debuggen: '^' + Control + Option
    // beendet das Programm!
    KeyMap  theMap;
    ::GetKeys(theMap);
    if((theMap[0] & 0x40000) &&
        ((theMap[1] & 0xC) == 0xC)) {
      break;
    }
#endif
  }
  // auf ein letztes Byte warten!
  SysBeep(10); SysBeep(10); SysBeep(10);

  // auf Abschluß des aktuellen Polls warten
  while(gParamBlk.ioParam.ioResult > 0) {}

  // Tastaturstatus zurücksetzen
  LMSetKeyLast(0);
  LMSetHiKeyLast(0);
  for(int i=0; i<sizeof(gKeyMap); i++)
    gKeyMap[i] = 0;
  ::BlockMoveData(gKeyMap, LMGetKeyMapPtr(),
                  sizeof(KeyMap));

raus:
  if(gSDOut) ::KillIO(gSDOut);
  if(gSDIn) ::CloseDriver(gSDIn);
  if(gSDOut) ::CloseDriver(gSDOut);
}


/***
 *  Newton Keyboard.r
 ***/
resource 'KMAP' (0) {
    0,
    0,
    {   0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,
        18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,
        33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,
        48,49,50,51,52,53,59,55,56,57,58,59,56,58,59,
        63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,
        78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,
        93,94,95,96,97,98,99,100,101,102,103,104,105,
        106,107,108,109,110,111,112,113,114,115,116,
        117,118,119,120,121,122,123,124,125,126,127
    },
    {
    }
};

resource 'KMAP' (1) {
    0,
    0,
    {   0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,
        18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,
        33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,
        48,49,50,51,52,53,59,55,56,57,58,59,60,61,62,
        63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,
        78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,
        93,94,95,96,97,98,99,100,101,102,103,104,105,
        106,107,108,109,110,111,112,113,114,115,116,
        117,118,119,120,121,122,123,124,125,126,127
    },
    {
    }
};

resource 'STR#' (128, "Portnames") {
    {   ".AIn",
        ".AOut"
    }
};

data 'TMPL' (128, "PREF") {
    /* .Different Shift */
    $"1544 6966 6665 7265 6E74 2053 6869 6674"
    /* -Keys?BOOL.Keybo */
    $"2D4B 6579 733F 424F 4F4C 0E4B 6579 626F"
    /* ardregionRGNC */
    $"6172 6472 6567 696F 6E52 474E 43"
};

data 'PREF' (128) {
    $"0000 0003"
};

/***
 *  ab hier: optional!
 ***/
resource 'vers' (1) {
    0x1,
    0x0,
    release,
    0x0,
    verGermany,
    "1.0",
    "1.0, ©1996 MAXON Computer, Markus Fritze"
};

resource 'BNDL' (128) {
    'NwtK',
    0,
    {   'FREF', { 0, 128 },
        'ICN#', { 0, 128 }
    }
};

resource 'FREF' (128) {
    'appe',
    0,
    ""
};

resource 'icl4' (128) {
    $"0FFF FFFF FFFF FFFF FFFF FFFF FFFF 0000"
    $"F000 0000 0000 0000 0000 0000 000C F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"FF0F CCCC CCCC CCCC CCCC CCCC CCCC F0F0"
    $"F0FF CCCC CCCC CCCC CCCC CCCC CCCC FF0F"
    $"000F CC99 9999 9999 9999 9999 99CC F0CF"
    $"000F CC99 9999 9999 9999 9999 99CC CCCF"
    $"000F CC99 C9C9 C9C9 C9C9 C9C9 99CC CCCF"
    $"000F CC99 9999 9999 9999 9999 99CC CCCF"
    $"000F CC99 9C9C 9C9C 9C9C 9C9C 99CC CCCF"
    $"000F CC99 9999 9999 9999 9999 99CC CCCF"
    $"000F CC99 C9C9 C9C9 C9C9 C9C9 99CC CCCF"
    $"000E CC99 9999 9999 9999 9999 99CC CCCF"
    $"000E CC99 9C9C 9CCC CCCC 9C9C 99CC CCCF"
    $"000F CC99 9999 9999 9999 9999 99CC FCCF"
    $"F00F CC99 9999 9999 9999 9999 99CC FFCF"
    $"FF0F CCCC CCCC CCCC CCCC CCCC CCCC F0F0"
    $"F0F0 CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"F0CC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"FCCC CCCC CCCC CCCC CCCC CCCC CCCC F000"
    $"0FFF FFFF FFFF FFFF FFFF FFFF FFFF"
};

resource 'icl8' (128, purgeable) {
    $"00FF FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
    $"FFFF FFFF FFFF FFFF FFFF FFFF 0000 0000"
    $"FF00 0000 0000 0000 0000 0000 0000 0000"
    $"0000 0000 0000 0000 0000 00F6 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FFFF 00FF F8F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 FF00"
    $"FF00 FFFF F8F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FFFF 00FF"
    $"0000 00FF F8F6 A5A5 A5A5 A5A5 A5A5 A5A5"
    $"A5A5 A5A5 A5A5 A5A5 A5A5 F6F8 FF00 F6FF"
    $"0000 00FF F8F6 A5A5 A5A5 A5A5 A5A5 A5A5"
    $"A5A5 A5A5 A5A5 A5A5 A5A5 F6F6 F6F6 F8FF"
    $"0000 00FF F8F6 A5A5 F8A5 F8A5 F8A5 F8A5"
    $"F8A5 F8A5 F8A5 F8A5 A5A5 F6F6 F6F6 F8FF"
    $"0000 00FF F8F6 A5A5 A5A5 A5A5 A5A5 A5A5"
    $"A5A5 A5A5 A5A5 A5A5 A5A5 F6F6 F6F6 F8FF"
    $"0000 00FF F8F6 A5A5 A5F8 A5F8 A5F8 A5F8"
    $"A5F8 A5F8 A5F8 A5F8 A5A5 F6F6 F6F6 F8FF"
    $"0000 00FF F8F6 A5A5 A5A5 A5A5 A5A5 A5A5"
    $"A5A5 A5A5 A5A5 A5A5 A5A5 F6F6 F6F6 F8FF"
    $"0000 00FF F8F6 A5A5 F8A5 F8A5 F8A5 F8A5"
    $"F8A5 F8A5 F8A5 F8A5 A5A5 F6F6 F6F6 F8FF"
    $"0000 00FC F8F6 A5A5 A5A5 A5A5 A5A5 A5A5"
    $"A5A5 A5A5 A5A5 A5A5 A5A5 F6F6 F6F6 F8FF"
    $"0000 00FC F8F6 A5A5 A5F8 A5F8 A5F8 F8F8"
    $"F8F8 F8F8 A5F8 A5F8 A5A5 F6F6 F6F6 F8FF"
    $"0000 00FF F8F6 A5A5 A5A5 A5A5 A5A5 A5A5"
    $"A5A5 A5A5 A5A5 A5A5 A5A5 F6F8 FFF8 F8FF"
    $"FF00 00FF F8F6 A5A5 A5A5 A5A5 A5A5 A5A5"
    $"A5A5 A5A5 A5A5 A5A5 A5A5 F6F8 FFFF F8FF"
    $"FFFF 00FF F8F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 FF00"
    $"FF00 FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FF00 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F6"
    $"F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00 0000"
    $"FFF6 F8F8 F8F8 F8F8 F8F8 F8F8 F8F8 F8F8"
    $"F8F8 F8F8 F8F8 F8F8 F8F8 F8F8 FF00 0000"
    $"00FF FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
    $"FFFF FFFF FFFF FFFF FFFF FFFF"
};

resource 'ICN#' (128) {
    {   $"7FFF FFF0 8000 0008 8000 0008 8000 0008"
        $"8000 0008 8000 0008 8000 0008 8000 0008"
        $"8000 0008 D000 000A B000 000D 13FF FFC9"
        $"13FF FFC1 1355 55C1 13FF FFC1 13AA AAC1"
        $"13FF FFC1 1355 55C1 13FF FFC1 13A8 0AC1"
        $"13FF FFC9 93FF FFCD D000 000A A000 0008"
        $"8000 0008 8000 0008 8000 0008 8000 0008"
        $"8000 0008 8000 0008 8000 0008 7FFF FFF0",

        $"7FFF FFF0 FFFF FFF8 FFFF FFF8 FFFF FFF8"
        $"FFFF FFF8 FFFF FFF8 FFFF FFF8 FFFF FFF8"
        $"FFFF FFF8 DFFF FFFA 9FFF FFFF 1FFF FFFF"
        $"1FFF FFFF 1FFF FFFF 1FFF FFFF 1FFF FFFF"
        $"1FFF FFFF 1FFF FFFF 1FFF FFFF 1FFF FFFF"
        $"1FFF FFFF 9FFF FFFF DFFF FFFA FFFF FFF8"
        $"FFFF FFF8 FFFF FFF8 FFFF FFF8 FFFF FFF8"
        $"FFFF FFF8 FFFF FFF8 FFFF FFF8 7FFF FFF0"
    }
};

resource 'ics#' (128) {
    {   $"FFFE 8002 8002 8002 C003 DFFB 5559 5FF9"
        $"5A29 5FF9 DFFB C003 8002 8002 8002 FFFE",

        $"FFFE FFFE FFFE FFFE FFFE FFFF 7FFF 7FFF"
        $"7FFF 7FFF FFFF FFFE FFFE FFFE FFFE FFFE"
    }
};

resource 'ics4' (128) {
    $"FFFF FFFF FFFF FFF0 FCCC CCCC CCCC CCF0"
    $"FCCC CCCC CCCC CCF0 FCCC CCCC CCCC CCF0"
    $"FFCC CCCC CCCC CCFF FFC9 9999 9999 9CFF"
    $"0FC9 D9D9 D9D9 9CCF 0FC9 9999 9999 9CCF"
    $"0FC9 9D9D DD9D 9CCF 0EC9 9999 9999 9CCF"
    $"FFC9 9999 9999 9CFF FFCC CCCC CCCC CCFF"
    $"FCCC CCCC CCCC CCF0 FCCC CCCC CCCC CCF0"
    $"FCCC CCCC CCCC CCF0 FFFF FFFF FFFF FFF0"
};

resource 'ics8' (128) {
    $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FF00"
    $"FFF6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00"
    $"FFF6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00"
    $"FFF6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00"
    $"FFFF F8F6 F6F6 F6F6 F6F6 F6F6 F6F8 FFFF"
    $"FFFF F8A5 A5A5 A5A5 A5A5 A5A5 A5F8 FFFF"
    $"00FF F8A5 F8A5 F8A5 F8A5 F8A5 A5F6 F6FF"
    $"00FF F8A5 A5A5 A5A5 A5A5 A5A5 A5F6 F6FF"
    $"00FF F8A5 A5F8 A5F8 F8F8 A5F8 A5F6 F6FF"
    $"00FC F8A5 A5A5 A5A5 A5A5 A5A5 A5F6 F6FF"
    $"FFFF F8A5 A5A5 A5A5 A5A5 A5A5 A5F8 FFFF"
    $"FFFF F8F6 F6F6 F6F6 F6F6 F6F6 F6F8 FFFF"
    $"FFF6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00"
    $"FFF6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00"
    $"FFF6 F6F6 F6F6 F6F6 F6F6 F6F6 F6F8 FF00"
    $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FF"
};

data 'NwtK' (0, "Owner resource") {
    $"00"
};

Futura Aquariencomputer Firmware

Die Firmware vom Futura Aquariencomputer ist für die Z80 CPU geschrieben worden. Ich habe sie ausgelesen und kommentiert. Der komplette Download auf GitHub.

Future Aquarium Computer

 

 

 

 

 

 

 

 

;   Futura Aquariencomputer ROM F- 1.89 (8K)
                PRINT   "Anfang..."
;Speicheraufteilung:
;0000h-1FFFh    8K EPROM
;2000h-27FFh    2K RAM
;4000h          Uhrenchip mit 4 Bit Registern
;4000h : Register lesen (0Fh = Fehler)
;4001h : Register wählen
;4002h : Register-Schreiben
;Register: 4,5: Stunden (BCD); 2,3: Minuten (BCD); 0,1: Sekunden (BCD)
;6000h          Keyboard-Spalte auf 0 zurücksetzen. Zeilendaten mit IN A,(00h) abfragen
;8000h          Port-Adresse (0...2:frei, 3...9: Keyboard, 10...15: Display)
;A000h          LED-Daten an gewähltes LED-Segment; (Port-Adresse zurücksetzen?)
;C000h          Schreibzugriff => Watchdog zurücksetzen
;E000h          Ausgangsport für die Steckdosen

ROMTop          = 1FF0h         ;bis hier wird die Prüfsumme berechnet
RAMBase         = 2000h
RAMTop          = 2800h         ;Endadresse vom RAM + 1

NewVersion      = 0             ;0 = Originalversion, 1 = neue Version

                ORG RAMBase     ;Basisadresse vom RAM. IX zeigt stets auf diese Adresse => (IX+d)
DisplayBufStart
KLED            DEFS    2       ;LEDs der Tasten
;  Bit
;   0       0:Tag-LED an
;   1       0:Nacht-LED an
;   2       0:Ein-LED an
;   3       0:Aus-LED an
;   4       0:Zeit-LED an
;   5       0:Momentan-LED an
;   6       0:Manuelle-LED an
;   7       0:Setzen-LED an
;  Bit
;   0       0:pH-LED an
;   1       0:Temp-LED an
;   2       0:Leitwert-LED an
;   3       0:Redox-LED an
;   4       0:Kanal 1-LED an
;   5       0:Kanal 2-LED an
;   6       0:Licht-LED an
;   7       0:CO2-LED an

WarnLED         DEFS    1       ;6 Warn-LEDs neben dem Display
;  Bit
;   0       0:Kanal 2 an
;   1       0:CO2 an
;   2       0:ph-Alarm an
;   3       0:Kanal 1 an
;   4       0:Heizung an
;   5       0:Temp-Alarm an
;   6       unbenutzt
;   7       unbenutzt


DisplayBuf      DEFS    6
DisplayBufEnd:

;Display-Buffer, wird bei jedem Mainloop-Durchlauf aus dem Display-RAM aufgebaut.
;Hier liegen die für die LEDs kodierten Zeichen drin.

;Font => LED-Tabelle
;    01
;    --
;20 |40| 02     low-active!
;    --
;10 |  | 04
;    --  .
;    08  80


Display         DEFS    6       ;Display-RAM

;Zeichensatz im Display:
;00 - 0   01 - 1   02 - 2   03 - 3
;04 - 4   05 - 5   06 - 6   07 - 7
;08 - 8   09 - 9   0A - A   0B - B
;0C - C   0D - D   0E - E   0F - F
;10 - H   11 - L   12 - P   13 - r
;14 - U   15 - µ   16 - u   17 - n
;18 - °   19 - o   1A - /F  1B - /A         "/" = umgedrehter Buchstabe
;1C - -   1D - _   1E - N   1F - Space

LastKey         DEFS    1       ;zuletzt gedrückte Taste (FFh = keine)

Flags           DEFS    1       ;diverse Flags
;      Bit      Aktion, wenn gesetzt
;       0       Zahleingabe an, ansonsten wird ein Wert dargestellt
;       1       zuletzt gedrückte Taste abgearbeitet. Wird erst gelöscht, wenn Taste losgelassen
;       2       Strom wurde eingeschaltet. Uhrzeit beim Einschalten blinkt.
;       3       Momentane Werte durchschalten
;       4       String im Display (0 = Zahl im Display)
;       5       (unbenutzt)
;       6       während der Kommunikation mit dem Hauptgerät (Meßwerte abholen)
;       7       Führende Zeichen aufgetreten. Nullen ab jetzt ausgeben.

DispLine        DEFS    1       ;"Rasterzeile" beim Display-Refresh
DPunkt          DEFS    1       ;Punkte im Display (Bit 0 = 6.Stelle, Bit 1 = 5.Stelle, ...)
BCDZahl         DEFS    2       ;gewandelte BCD-Zahl
CO2EinZeit      DEFS    3       ;Einschaltzeit von CO2
CO2AlarmZeit    DEFS    3       ;Alarmzeit, wenn pH-Wert nicht den Sollwert erreicht hat
LichtEin        DEFS    3       ;Uhrzeit, wann das Licht angeschaltet wird (3 Bytes: ss:mm:hh)
CO2Ein          DEFS    3       ;Uhrzeit, wann das CO2 ausgeschaltet wird (3 Bytes: ss:mm:hh)
Mult24          DEFS    3       ;Multiplikator
Mult24Erg       DEFS    3       ;Ergebnis der 24 Bit Multiplikation
LichtAus        DEFS    3       ;Uhrzeit, wann das Licht ausgeschaltet wird (3 Bytes: ss:mm:hh)
CO2Aus          DEFS    3       ;Uhrzeit, wann das CO2 ausgeschaltet wird (3 Bytes: ss:mm:hh)
TagZeit         DEFS    3       ;Uhrzeit, wann der Tag beginnt (3 Bytes: ss:mm:hh)
SollTempTag     DEFS    1       ;Temperatur für den Tag
NachtZeit       DEFS    3       ;Uhrzeit, wann die Nacht beginnt (3 Bytes: ss:mm:hh)
SollTempNacht   DEFS    1       ;Temperatur für die Nacht
ManuellZeit     DEFS    3       ;Zeit, wie lange das Licht nach Druck auf "Manuell" an bleibt
SollpH          DEFS    1       ;Soll-pH-Wert

MWpHLow         DEFS    1       ;unteres Byte des Meßwertes
MWphHigh        DEFS    1       ;oberes Byte des Meßwertes (nur 4 Bit)
IstpH           DEFS    1       ;gemessener skalierter pH-Wert (obere 8 Bits des Meßwertes)
MWTempLow       DEFS    1       ;unteres Byte des Meßwertes
MWTempHigh      DEFS    1       ;oberes Byte des Meßwertes (nur 4 Bit)
IstTemp         DEFS    1       ;gemessener skalierter Temp-Wert (obere 8 Bits des Meßwertes)
MWLeitwLow      DEFS    1       ;unteres Byte des Meßwertes
MWLeitwHigh     DEFS    1       ;oberes Byte des Meßwertes (nur 4 Bit)
IstLeitw        DEFS    1       ;gemessener skalierter Leitwert-Wert (obere 8 Bits des Meßwertes)
MWRedoxLow      DEFS    1       ;unteres Byte des Meßwertes
MWRedoxHigh     DEFS    1       ;oberes Byte des Meßwertes (nur 4 Bit)
IstRedox        DEFS    1       ;gemessener skalierter Redox-Wert (obere 8 Bits des Meßwertes)

Messcounter     DEFS    1       ;Zähler von 16 abwärts; es wird nur alle 16 Durchläufe gemessen
TempTime        DEFS    3       ;ss:mm:hh (temporäre Zeit während der Eingabeauswertung)
ManuellEinZeit  DEFS    3       ;Zeit, wann "Manuell" gedrückt wurde
Counter         DEFS    2       ;Blink-Timer (Bit 0 toggelt mit 0.5Hz; wird im IRQ abwärts gezählt)
Steckdosen      DEFS    1       ;Steckdose (Bit = 1: Steckdose an)
;      Bit      Steckdose
;       0       CO2
;       1       Heizung
;       2       Licht
;       3       Kanal 1
;       4       Kanal 2
;       5,6,7   die oberen 3 Bits dienen der Kommunikation mit dem Hauptgerät

AktTime         DEFS    3       ;aktuelle Uhrzeit (ss:mm:hh)
PowerOnZeit     DEFS    3       ;Uhrzeit beim Einschalten des Stromes
ManuellAusZeit  DEFS    3       ;Ausschaltzeit nach Druck auf "Manuell"
TempAlarmZeit   DEFS    3       ;Heizungsalarm (Einschaltzeit + 1h)
                DEFS    3
AktSchaltzeit   DEFS    1       ;aktuelle Schaltzeit der Universaltimer (1...10 sind möglich)
SollLeitwertS   DEFS    1       ;Soll-Leitwert (Süßwasser)
SollLeitwertM   DEFS    1       ;Soll-Leitwert (Meerwasser)
Uni1Flag        DEFS    1       ;55h => Kanal 1 = UNI-1
                                ;AAh => Kanal 1 = Redox-Regler
                                ;<>  => Kanal 1 = inaktiv
Uni2Flag        DEFS    1       ;55h => Kanal 2 = UNI-2
                                ;AAh => Kanal 2 = Leitwert-Regler
                                ;<>  => Kanal 2 = inaktiv
Uni2Flag2       DEFS    1       ;55h = Leitwert EIN Regelung
                                ;AAh = Leitwert AUS Regelung
SollRedox       DEFS    1       ;Soll-Redoxwert
LeitwertKomp    DEFS    1       ;kompensierter Leitwert
AktSollTemp     DEFS    3       ;aktuelle Solltemperatur (Tag oder Nacht)
Kanal1Uni       DEFS    11*2*3  ;Universaltimer-Zeiten von Kanal 1 (10 Stück a 3 Bytes, erst Ein-, dann Ausschaltzeiten)
Kanal2Uni       DEFS    10*2*3  ;Universaltimer-Zeiten von Kanal 1 (10 Stück a 3 Bytes, erst Ein-, dann Ausschaltzeiten)
MomentanSek     DEFS    1       ;Momentan-Sekunden-Merker für Momentan-Momentan
DelayTimer      DEFS    1       ;Variable für Verzögerungen, etc.
KeyboardMatrix  DEFS    7       ;Keyboard-Matrix-Zeilen (untere 4 Bit, gelöscht = gedrückt)
InputBuf:       DEFS    10      ;Buffer für GetNumInput()
                IF !NewVersion
LaufschriftFlag DEFS    1       ;55h = Laufschrift an
LaufschriftInit DEFS    1       ;55h = Laufschrift ist initialisiert
LaufschriftPtr  DEFS    2       ;Ptr auf eine Laufschrift
ScrollPtr       DEFS    2       ;Ptr auf das nächste Zeichen in der Laufschrift
                ENDIF
SollChecksum    DEFS    2       ;Soll-Prüfsumme über die Sollwerte
                IF !NewVersion
                DEFS    2
Dummy0:         DEFS    2       ;= 0, wird in der Init-Laufschrift ausgegeben
                ENDIF
AktROMChecksum  DEFS    2       ;Prüfsumme über das ROM _während_ der Berechnung
CalcChecksum    DEFS    2       ;letzte errechnete Prüfsumme
ChecksumFinal   DEFS    1       ;Prüfsumme in CalcChecksum ist gültig (aber evtl. falsch!)
ROMTopAdr       DEFS    2       ;Endadresse vom ROM (läuft bis 0 rückwärts während der Prüfsummenberechnung)
ErrorCode       DEFS    1       ;aufgetretener Fehler (0 = keiner)
GesamtBZeit     DEFS    5       ;Gesamt-Betriebsstunden
Kanal1BZeit     DEFS    4       ;Betriebsstunden für Kanal 1 (4 Bytes: mm:hhhhhh)
Kanal2BZeit     DEFS    4       ;Betriebsstunden für Kanal 2 (4 Bytes: mm:hhhhhh)
CO2BZeit        DEFS    4       ;Betriebsstunden für CO2 (4 Bytes: mm:hhhhhh)
TempBZeit       DEFS    4       ;Betriebsstunden für Heizung (4 Bytes: mm:hhhhhh)
LichtBZeit      DEFS    4       ;Betriebsstunden für Licht (4 Bytes: mm:hhhhhh)
                IF !NewVersion
StringBuf       DEFS    200     ;???
StringBufPtr    DEFS    2       ;???
InitLaufschr    DEFS    1       ;0xAA = Init-Laufschrift an, 0x55 = Init-Laufschrift aus
InitLaufschrSek DEFS    1       ;Sekunden-Merker für den Init-Laufschrift-Start
Dummy           DEFS    3       ;??? wird in der Init-Laufschrift ausgegeben, aber nie gesetzt
                ENDIF
                IF NewVersion
MomentanZeit    DEFS    1       ;Weiterschaltzeit für Momentan (in Sekunden)
                ENDIF
StackTop        = RAMTop        ;der Stack fängt ganz oben im RAM an

;Flags von: IN C,(C) (externe Schalter)
;       4       0:Programmiersperre an
;       5       0:Meerwasser, 1:Süßwasser

                ORG 0000h
                DI                              ;IRQs aus (an sich unnötig, sind nach einem Reset eh aus...)
                IM      1                       ;bei IRQs stets RST 38h ausführen!
                LD      SP,StackTop
                LD      (C000h),A
                LD      IX,RAMBase              ;Basisadresse vom RAM
                LD      B,FFh
                LD      (IX+DispLine),DisplayBufEnd ;Display-Refresh (im IRQ)
                IF !NewVersion
                LD      A,AAh
                LD      (InitLaufschr),A        ;Init-Laufschrift AN
                ENDIF
                EI                              ;IRQs wieder an
                LD      HL,0
                LD      (AktROMChecksum),HL     ;ROM-Prüfsumme zurücksetzen
                LD      HL,ROMTop
                LD      (ROMTopAdr),HL          ;Endadresse vom ROM
                JP      Startup

                ORG 0038h                       ;der RST 0x38 bzw. RST 7 Interrupt
                DI
                EXX
                EX      AF,AF'
                JP      DoIRQ
                IF !NewVersion
                JP      DoIRQ                   ;???
                ENDIF

                ORG 0066h                       ;der NMI-Vektor des Z80
;NMI-Routine ("Reset" = alles zurücksetzen)
DoNMI:          IF NewVersion
                CALL    ResetVars
                RETN
                ENDIF

;sämtliche Variablen zurücksetzen!
ResetVars:      PUSH    HL
                PUSH    AF
                PUSH    BC
                PUSH    DE
                LD      BC,23
                LD      HL,Kanal1BZeit
                LD      DE,Kanal1BZeit+1
                LD      (HL),0
                LDIR                            ;Betriebszeiten löschen
                LD      DE,GesamtBZeit
                LD      HL,TempTime             ;temp.Zeit übertragen
                LD      BC,3
                LDIR                            ;Gesamtbetriebszeit setzen
                IN      C,(C)
                BIT     5,C                     ;Süßwasser/Meerwasser-Schalter abfragen
                JR      Z,DoNMI1                ;Meerwasser =>
                LD      A,64                    ;Soll-pH-Wert ((64/2+38)/10 = 7.0)
                JR      DoNMI2
DoNMI1:         LD      A,90                    ;(90/2+38)/10 = 8.3
DoNMI2:         LD      (IX+SollpH),A           ;Soll-pH-Wert
                LD      A,145                   ;(145+100)/10 = 24.5°
                LD      (IX+SollTempTag),A      ;Soll-Temperatur (Tag)
                LD      A,130                   ;(130+100)/10 = 23°
                LD      (IX+SollTempNacht),A    ;Soll-Temperatur (Nacht)
                LD      A,150                   ;150/10+35.0 = 50mS
                LD      (IX+SollLeitwertM),A    ;Soll-Leitwert (Meerwasser)
                LD      A,80                    ;80*10 = 800µS
                LD      (IX+SollLeitwertS),A    ;Soll-Leitwert (Süßwasser)
                LD      A,125                   ;125*2 = 250µV
                LD      (IX+SollRedox),A        ;Soll-Redoxwert
                LD      A,0
                LD      (IX+AktSchaltzeit),A    ;keine aktuelle Schaltzeit
                LD      (IX+Uni1Flag),A         ;Kanal 1 inaktiv schalten
                LD      (IX+Uni2Flag),A         ;Kanal 2 inaktiv schalten
                LD      (IX+Uni2Flag2),A        ;Leitwert-Regelung inaktiv
                LD      HL,CO2Ein
                LD      B,3
DoNMI3:         LD      (HL),A                  ;CO2 Sperrzeit ein = 00.00.00
                INC     HL
                DJNZ    DoNMI3
                LD      HL,CO2Aus
                LD      B,3
DoNMI4:         LD      (HL),A                  ;CO2 Sperrzeit aus = 00.00.00
                INC     HL
                DJNZ    DoNMI4
                IF      NewVersion
                LD      A,7
                LD      (MomentanZeit),A        ;Momentan-Zeit : 7 Sekunden
                ENDIF
                LD      A,80h
                LD      (LichtEin+2),A
                LD      (TagZeit+2),A
                LD      A,81h
                LD      (LichtAus+2),A
                LD      (NachtZeit+2),A
                POP     DE
                POP     BC
                POP     AF
                POP     HL
                IF NewVersion
                RET
                ELSE
                RETN
                ENDIF

;IRQ-Routine für Tastatur und Display
DoIRQ:          LD      A,0
                LD      (A000h),A               ;Port-Adresse auf 0 zurücksetzen
                LD      HL,8000h
                LD      B,7                     ;7 Keyboard-Spalten
                LD      DE,KeyboardMatrix+6
                LD      (HL),A
                LD      (HL),A                  ;auf Adresse 3 weiterschalten
                LD      (HL),A
                LD      (6000h),A               ;Port auslesen
DoIRQ1:         IN      A,(00h)                 ;Keyboard-Spalte auslesen
                LD      (DE),A                  ;und merken
                LD      (HL),A                  qqww;Port-Adresse hochzählen
                DEC     DE                      ;eine Spalte nach vorne
                DJNZ    DoIRQ1                  ;alle Spalten durch? Nein =>

                LD      B,(IX+DispLine)         ;LED-Daten
                DJNZ    DoIRQ2                  ;einmal durch?
                LD      B,DisplayBufEnd         ;wieder von vorne
DoIRQ2:         LD      (IX+DispLine),B         ;aktuelle Zeile setzen
                LD      H,DisplayBufStart>>8
                LD      L,B                     ;DisplayBuf + B - 1 (DisplayBuf...DisplayBufEnd)
                DEC     L
                LD      A,(HL)                  ;Speicherzelle aus dem Display auslesen
                LD      HL,8000h
DoIRQ3:         LD      (HL),A                  ;Port-Adresse hochzählen (10...15)
                DJNZ    DoIRQ3                  ;und zwar B mal
                CPL
                LD      (A000h),A               ;und das Display-Segment setzen
                LD      HL,(Counter)
                DEC     HL                      ;IRQ-Zähler (für Display-Blinken)
                LD      (Counter),HL
                EXX
                EX      AF,AF'
                EI
                RETI


Startup:        LD      (C000h),A
                HALT                            ;Verzögerung
                DJNZ    Startup
                LD      HL,VersionNoDisp
                LD      DE,DisplayBuf
                LD      BC,6
                LDIR                            ;Versionsnummer in die LED-Anzeige "F- 1.89"
                LD      B,FFh
Startup1:       HALT
                HALT
                LD      (C000h),A               ;vierfache Verzögerung
                HALT
                HALT
                LD      (C000h),A
                DJNZ    Startup1
                CALL    KeyStern                ;Display löschen
                SET     2,(IX+Flags)            ;PowerOn-Flag setzen
                SET     6,(IX+KLED)             ;Manuell-LED aus
                LD      DE,PowerOnZeit
                LD      HL,AktTime
                LD      BC,3
                LDIR                            ;aktuelle Uhrzeit merken
                IF !NewVersion
                LD      DE,StringBuf
                LD      (StringBufPtr),DE
                LD      HL,MsgEscHEscJ
                CALL    CopyString
                LD      HL,MsgMessdaten
                CALL    CopyString
                LD      A,55h
                LD      (InitLaufschr),A        ;Init-Laufschrift AUS
                JP      DoLEDKonv
                ENDIF

; Hier beginnt die Hauptschleife...
DoLEDKonv:      LD      B,6                     ;6 LED-Anzeigen updaten
                LD      IY,Display              ;Ptr auf Display-RAM (unkodiert)
                LD      HL,DisplayBuf
                RES     7,(IX+Flags)            ;noch kein 1.Zeichen ausgegeben
                LD      C,(IX+DPunkt)           ;(6) Dezimalpunkte holen
                SLA     C
                SLA     C                       ;um 2 Bits nach oben an den "Byterand"
DoLEDKonv1:     PUSH    HL
                LD      HL,FontLEDTable         ;Ptr auf "Zeichensatz"-Tabelle
                LD      E,(IY+0)                ;Zeichen aus dem Display-RAM
                LD      D,0
                ADD     HL,DE
                LD      A,(HL)                  ;Zeichencode holen
                SLA     A                       ;A << 1
                SLA     C                       ;C << 1 (ins Carry)
                RR      A                       ;A >> 1; Carry in Bit 7
                BIT     7,(IX+Flags)            ;1.Zeichen schon ausgegeben?
                JR      NZ,DoLEDKonv3           ;Ja! =>
                CP      C0h                     ;"0"?
                JR      NZ,DoLEDKonv3           ;Nein => normal ausgeben
                LD      A,B
                CP      1                       ;letztes Anzeigeelement?
                JR      Z,DoLEDKonv2            ;Ja! =>
                LD      A,FFh                   ;LED komplett ausschalten (keine führenden Nullen ausgeben)(
                JR      DoLEDKonv4
DoLEDKonv2:     LD      A,C0h                   ;"0" darstellen
DoLEDKonv3:     SET     7,(IX+Flags)            ;1.Zeichen bereits ausgegeben
DoLEDKonv4:     POP     HL
                LD      (HL),A                  ;LED-Element neu setzen
                INC     IY                      ;weiter im Display-RAM
                INC     HL                      ;zum nächsten Element
                DJNZ    DoLEDKonv1              ;alle LEDs durch?

DoGetMess:      LD      B,(IX+Messcounter)      ;alle 16 Durchläufe umrechnen?
                DJNZ    DoGetMess2              ;Nein! =>
                LD      B,4                     ;4 Meßwerte holen (pH-Wert, Temperatur, Leitwert, Redox)
                LD      IY,MWpHLow
DoGetMess1:     LD      L,(IY+0)                ;unteres Byte lesen
                LD      H,(IY+1)                ;oberes Byte lesen
                ADD     HL,HL
                ADD     HL,HL
                ADD     HL,HL                   ;mal 8
                ADD     HL,HL
                LD      (IY+2),H                ;nur das obere Byte merken
                LD      (IY+0),0                ;Meßwert zurücksetzen
                LD      (IY+1),0
                INC     IY                      ;zum nächsten Meßwert
                INC     IY
                INC     IY
                DJNZ    DoGetMess1
                LD      B,16                    ;Durchlaufzähler neu setzen
DoGetMess2:     LD      (IX+Messcounter),B

                LD      HL,SpezKeyTable         ;Ptr auf den Tabellenanfang
DoSpezKey:      LD      DE,KeyboardMatrix
                LD      B,7                     ;7 Bytes pro Eintrag (= 7 Zeilen) (+ 2 Byte Adresse)
DoSpezKey1:     LD      A,(DE)                  ;Spaltenwert holen
                OR      F0h
                CP      (HL)                    ;Eintrag in der Tabelle?
                JR      Z,DoSpezKey3            ;Ja! => stimmen die nächsten 6 Bytes auch?
DoSpezKey2:     INC     HL
                DJNZ    DoSpezKey2              ;Eintrag überspringen
                INC     HL
                INC     HL
                LD      A,(HL)                  ;Folgebyte holen
                CP      0                       ;Tabellenende?
                JR      Z,DoKeyboard            ;Ja! =>
                JR      DoSpezKey               ;weiter vergleichen...
DoSpezKey3:     INC     DE                      ;nächste Tastaturspalte
                INC     HL
                DJNZ    DoSpezKey1              ;alle 7 Bytes gleich? Nein! => Weiter
                LD      D,(HL)
                INC     HL
                LD      E,(HL)                  ;Sprungadresse holen
                PUSH    DE                      ;Sprungadresse merken
                POP     IY
                CALL    CallIY                  ;gefundene Routine anspringen

DoKeyboard:     LD      A,0                     ;Tastencode = 0
                LD      B,7                     ;7 Tastaturspalten abklopfen
                LD      HL,KeyboardMatrix       ;Ptr auf Tastaturmatrix-Basis
DoKeyboard1:    LD      C,4                     ;maximal 4 Zeilen pro Spalte
                LD      D,(HL)                  ;Byte holen
DoKeyboard2:    RR      D
                JR      NC,DoKeyboard3          ;Bit gesetzt? (Taste gedrückt) => raus
                INC     A                       ;Tastencode++
                DEC     C                       ;alle Zeilen dieser Spalte zusammen?
                JR      NZ,DoKeyboard2          ;Nein =>
                INC     HL                      ;zur nächsten Spalte
                LD      (C000h),A
                DJNZ    DoKeyboard1             ;alle Spalten durch?
                LD      A,FFh                   ;dann keine Taste gedrückt
DoKeyboard3:    CP      (IX+LastKey)            ;mit der zuletzt gedrückten Taste vergleichen
                LD      (IX+LastKey),A          ;als letzte Taste merken
                JR      NZ,DoRecMess            ;ungleich? => ignorieren (entprellen)
                BIT     1,(IX+Flags)            ;Taste abgearbeitet?
                JR      Z,DoKeyboard4           ;Nein =>
                CP      FFh                     ;keine Taste gedrückt?
                JR      NZ,DoRecMess            ;doch! =>
                RES     1,(IX+Flags)            ;Abgearbeitet-Flag löschen
                JR      DoRecMess
DoKeyboard4:    CP      FFh                     ;keine Taste gedrückt?
                JR      Z,DoRecMess             ;genau =>
                SET     1,(IX+Flags)            ;Taste abgearbeitet!

                LD      HL,TastaturTab          ;Tastaturtabelle
                ADD     A,A
                LD      E,A
                LD      D,0
                ADD     HL,DE
                LD      D,(HL)                  ;Sonderflag
                INC     HL
                LD      E,(HL)                  ;Tastencode
                BIT     7,D                     ;normale Ziffer?
                JR      Z,DoKeyboard6           ;Nein! =>
                BIT     0,(IX+Flags)            ;Zahleingabe an?
                JR      NZ,DoKeyboard5          ;Ja! =>
                CALL    KeyStern
                SET     0,(IX+Flags)            ;Zahleingabe an!
DoKeyboard5:    LD      A,E                     ;gedrückte Ziffer
                LD      BC,5
                LD      DE,Display
                LD      HL,Display+1
                LDIR                            ;Display ein Zeichen nach links
                LD      (DE),A                  ;neues Zeichen einfügen
                SCF                             ;Carry-Flag setzen
                RL      (IX+DPunkt)             ;Punkte auch ein Zeichen nach links
                LD      (C000h),A
                JR      DoRecMess
DoKeyboard6:    PUSH    DE                      ;Sprungadresse merken
                POP     IY
                CALL    CallIY                  ;Sondertaste behandeln

;Meßwerte vom Hauptgerät empfangen
DoRecMess:      LD      B,4                     ;4 Meßwerte
                LD      IY,MWRedoxLow           ;Ptr auf den letzten Meßwert
                LD      (C000h),A
DoRecMess1:     LD      C,B
                DEC     C                       ;Meßwertnummer (0...3)
                PUSH    BC
                CALL    GetMesswert             ;empfangen
                POP     BC
                JR      C,DoRecMess2            ;ok? Ja =>
                PUSH    BC
                CALL    GetMesswert             ;nochmal probieren
                POP     BC
                JR      C,DoRecMess2            ;ok? Ja =>
                LD      A,82h
                LD      (ErrorCode),A           ;Übertragungsfehler!
                JR      DoRecMess3              ;=> zum nächsten Meßwert
DoRecMess2:     LD      H,(IY+1)                ;Highbyte vom Meßwert
                LD      L,(IY+0)                ;Lowbyte vom Meßwert
                LD      E,A                     ;Meßwert dazuaddieren
                LD      D,0
                ADD     HL,DE
                LD      (IY+1),H                ;Meßwert neu setzen
                LD      (IY+0),L
DoRecMess3:     DEC     IY
                DEC     IY
                DEC     IY
                LD      (C000h),A
                DJNZ    DoRecMess1              ;alle Meßwerte durch? Nein =>

;Uhrzeit (Uhrenchip liegt ab 4000h) auslesen
DoReadClock:    LD      IY,4000h                ;Basisadresse des Uhrenchips
                LD      DE,AktTime+2            ;Ptr auf die Stunden der Uhrzeit
                LD      B,3                     ;3 Werte (Stunden,Minuten,Sekunden)
                LD      C,5                     ;mit Register 5 geht es los
                HALT
DoReadClock1:   LD      (IY+1),C                ;Register 5 auswählen
                LD      A,(IY+0)                ;Register auslesen
                AND     0Fh                     ;nur 4 Bit-Register!
                CP      0Fh                     ;0Fh?
                JR      Z,DoReadClock3          ;Fehler =>
                ADD     A,A
                ADD     A,A
                ADD     A,A                     ;mal 16 + 0Fh
                ADD     A,A
                OR      0Fh
                DEC     C
                LD      (IY+1),C                ;Register 4 auswählen
                AND     (IY+0)                  ;unteren Teil der BCD-Zahl dazu
                PUSH    AF
                AND     0Fh
                CP      0Fh                     ;0Fh?
                JR      NZ,DoReadClock2         ;Nein => ok!
                POP     AF
                JR      DoReadClock3            ;Fehler =>
DoReadClock2:   POP     AF
                LD      (DE),A                  ;Stunden, Minuten, Sekunden merken
                DEC     DE                      ;eine Stelle weiter
                DEC     C                       ;Register 3,2 und dann Register 1,0
                DJNZ    DoReadClock1            ;alle Register durch? Nein =>
                RES     7,(IX+AktTime+2)        ;Uhrzeit fehlerfrei gelesen
                JR      DoErrorOut              ;Ok =>
DoReadClock3:   LD      A,83h
                LD      (ErrorCode),A           ;Fehler im interen Zeitschalter

DoErrorOut:     LD      (C000h),A
                LD      A,(ErrorCode)           ;Fehlercode lesen
                CP      0                       ;kein Fehler?
                JR      Z,DoFlashResTime        ;genau =>
                LD      E,A
                JP      ErrorOut

DoFlashResTime: BIT     2,(IX+Flags)            ;PowerOn-Flag gesetzt?
                JR      Z,DoPrintTime           ;Nein =>
                BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JP      Z,EraseDisplay
                LD      HL,PowerOnZeit
                CALL    PrintTime               ;Uhrzeit beim Einschalten ausgeben
                JP      DoLicht

DoPrintTime:    BIT     5,(IX+KLED)             ;"Zeit" an?
                JP      NZ,DoLicht              ;Nein =>
                BIT     4,(IX+KLED)             ;"Momentan" an?
                JR      NZ,DoDispMess           ;Nein =>
                LD      HL,AktTime
                CALL    PrintTime               ;aktuelle Uhrzeit ausgeben

DoDispMess:     LD      B,4                     ;4 Meßwerte
                LD      C,(IX+KLED+1)           ;Meßwert-Tastatur-LED auslesen
                LD      E,11h                   ;mit Fehlermeldung 11 geht es los
                LD      HL,IstpH                ;Ptr auf Ist-Wert vom pH-Wert
                LD      IY,DoDispMessTab        ;Ptr auf Sprungtabelle für die verschiedenen Meßwerte
DoDispMess1:    SRL     C                       ;LED nach unten schieben
                JR      NC,DoDispMess2          ;LED an? => ja
                INC     HL
                INC     HL                      ;zum nächsten Meßwert
                INC     HL
                INC     IY                      ;ein Eintrag weiter in der Sprungtabelle
                INC     IY
                INC     E                       ;Fehlermeldung + 2
                INC     E
                DJNZ    DoDispMess1
                JR      DoLicht                 ;keinen Meßwert darstellen
DoDispMess2:    LD      A,(HL)                  ;Meßwert auslesen
                CP      0                       ;= 0?
                JR      Z,ErrorOut              ;Meßbereich unterschritten =>
                CP      FFh
                JR      NZ,DoDispMess3          ;Meßbereich i.O. =>
                INC     E
                JR      ErrorOut                ;Meßbereich überschritten =>
DoDispMess3:    LD      D,(IY+0)                ;Sprungtabelle auslesen
                LD      E,(IY+1)
                PUSH    DE                      ;Sprungadresse merken
                POP     IY
                LD      L,A                     ;Meßwert / 2
                SRL     A
                CALL    CallIY                  ;gefundene Routine anspringen
                JR      DoLicht

CallIY:         JP      (IY)

;Fehlermeldung ausgeben, Fehlercode in E
ErrorOut:       BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JR      NZ,ErrorOut1            ;An =>
EraseDisplay:   LD      HL,Display+5
                LD      DE,Display+4
                LD      BC,5
                LD      (HL),1Fh                ;Display mit Leerzeichen füllen
                LDDR
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                SET     4,(IX+Flags)            ;keine Zahl im Display
                JR      DoLicht
ErrorOut1:      LD      A,E
                CALL    MakeErrCode

DoLicht:        LD      (C000h),A
                LD      HL,LichtEin
                LD      DE,LichtAus
                CALL    InTimeRange             ;am Tage?
                JR      NC,DoLicht9             ;ja => Licht an
                BIT     6,(IX+KLED)             ;Manuell?
                JR      NZ,DoLicht8             ;Nein => Licht aus
                LD      HL,ManuellZeit          ;Momentan-Timer-Wert (ss:mm:hh)
                LD      DE,ManuellEinZeit       ;Zeit, wann "Manuell" gedrückt wurde
                LD      BC,ManuellAusZeit
                LD      A,(DE)                  ;Sekunden der Einschaltzeit
                ADD     A,(HL)                  ;+ Sekunden der Dauer
                DAA
                INC     DE                      ;Ptr auf die Minuten
                INC     HL
                JR      NC,DoLicht2             ;ein Sekundenüberlauf? Nein =>
DoLicht1:       SUB     60h                     ;Sekunden um 60 zurücksetzen
                DAA
                SCF                             ;Carry setzen (Sekunden-Übertrag)
                JR      DoLicht3
DoLicht2:       CP      60h                     ;Sekunden-Überlauf?
                JR      NC,DoLicht1             ;Ja =>
                CCF                             ;Nein, Carry löschen
DoLicht3:       LD      (BC),A                  ;Ausschaltzeit-Sekunden
                INC     BC
                LD      A,(DE)
                ADC     A,(HL)                  ;Ausschaltzeit-Minuten (+ Sekunden-Übertrag)
                DAA
                INC     DE                      ;Ptr auf die Stunden
                INC     HL
                JR      NC,DoLicht5             ;ein Minutenüberlauf? Nein =>
DoLicht4:       SUB     60h                     ;Minuten um 60 zurücksetzen
                DAA
                SCF                             ;Carry setzen (Minuten-Übertrag)
                JR      DoLicht6
DoLicht5:       CP      60h                     ;Minuten-Überlauf?
                JR      NC,DoLicht4             ;Ja =>
                CCF                             ;Nein, Carry löschen
DoLicht6:       LD      (BC),A                  ;Ausschaltzeit-Minuten
                INC     BC
                LD      A,(DE)
                ADC     A,(HL)                  ;Ausschaltzeit-Stunden (+ Minuten-Übertrag)
                DAA
                CP      24h                     ;24h Überlauf?
                JR      C,DoLicht7              ;Nein =>
                SUB     24h                     ;Uhrzeit des nächsten Tages
                DAA
DoLicht7:       LD      (BC),A                  ;Aussschaltzeit-Stunden
                LD      HL,ManuellEinZeit
                LD      DE,ManuellAusZeit
                CALL    InTimeRange             ;im "Manuell"-Einschaltzeitraum?
                JR      NC,DoLicht9             ;Ja => Licht an
                SET     6,(IX+KLED)             ;Manuell aus
DoLicht8:       RES     2,(IX+Steckdosen)       ;Licht aus
                JR      DoKanal1
DoLicht9:       SET     2,(IX+Steckdosen)       ;Licht an

DoKanal1:       LD      HL,Kanal1Uni            ;Einschaltzeiten
                LD      DE,Kanal1Uni+30         ;Ausschaltzeiten
                CALL    Kanal1Regel
                JR      C,DoKanal11
                SET     3,(IX+Steckdosen)       ;Kanal 1 an
                RES     3,(IX+WarnLED)          ;Kanal 1-LED an
                JR      DoKanal2
DoKanal11:      RES     3,(IX+Steckdosen)       ;Kanal 1 aus
                SET     3,(IX+WarnLED)          ;Kanal 1-LED aus

DoKanal2:       LD      HL,Kanal2Uni            ;Einschaltzeiten
                LD      DE,Kanal2Uni+30         ;Ausschaltzeiten
                CALL    Kanal2Regel
                JR      C,DoKanal21
                SET     4,(IX+Steckdosen)       ;Kanal 2 an
                RES     0,(IX+WarnLED)          ;Kanal 2-LED an
                JR      DoTemp
DoKanal21:      RES     4,(IX+Steckdosen)       ;Kanal 2 aus
                SET     0,(IX+WarnLED)          ;Kanal 2-LED aus

DoTemp:         LD      HL,TagZeit
                LD      DE,NachtZeit
                CALL    InTimeRange             ;ist es Tag?
                JR      C,DoTemp1               ;Nein =>
                LD      A,(IX+SollTempTag)      ;Soll-Temperatur (Tag)
                JR      DoTemp2
DoTemp1:        LD      A,(IX+SollTempNacht)    ;Soll-Temperatur (Nacht)
DoTemp2:        PUSH    AF                      ;Soll-Temperatur merken
                LD      (IX+AktSollTemp),A      ;aktuelle Soll-Temperatur merken
                BIT     4,(IX+WarnLED)          ;Heizung-LED an?
                JR      Z,DoTemp6               ;Ja! => Heizung regeln
                CP      (IX+IstTemp)            ;Ist-Temp-Wert >= Soll-Temperatur?
                JR      C,DoTemp4               ;Ja! => Heizung ausschalten
                SET     1,(IX+Steckdosen)       ;Heizung an
                RES     4,(IX+WarnLED)          ;Heizung-LED an
                LD      HL,AktTime
                LD      DE,TempAlarmZeit
                LD      BC,3
                LDIR                            ;Einschaltzeit der Heizung merken
                DEC     DE
                LD      A,(DE)                  ;Stunden holen
                INC     A                       ;+1
                CP      24h                     ;24 Uhr?
                JR      C,DoTemp3               ;kleiner als 24 Uhr? Ja =>
                LD      A,0                     ;0 Uhr annehmen
DoTemp3:        LD      (DE),A                  ;Stunden setzen
                POP     AF
                JR      DoTemp10

DoTemp4:        POP     AF
                SET     5,(IX+WarnLED)          ;Temp.Alarm aus
DoTemp5:        SET     4,(IX+WarnLED)          ;Heizung-LED aus
                RES     1,(IX+Steckdosen)       ;Heizung aus
                JR      DoTemp10

DoTemp6:        LD      HL,AktTime
                LD      DE,TempAlarmZeit
                CALL    CompareTimes            ;eine Stunde heizen um?
                JR      NC,DoTemp7              ;Nein =>
                BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JR      Z,DoTemp7               ;Nein =>
                RES     5,(IX+WarnLED)          ;Temp.Alarm an
                JR      DoTemp8
DoTemp7:        SET     5,(IX+WarnLED)          ;Temp.Alarm aus
DoTemp8:        POP     AF                      ;Soll-Temperatur wieder vom Stack holen
                ADD     A,1
                CP      (IX+IstTemp)            ;Ist-Temp-Wert >= Soll-Temp + 0.1°?
                JR      C,DoTemp5               ;Ja =>
                SET     1,(IX+Steckdosen)       ;Heizung an

DoTemp10:       LD      A,(IX+IstTemp)          ;Ist-Temp-Wert
                SUB     8
                CP      (IX+AktSollTemp)        ;Soll-Temp >= Ist-Temp - 0.8°? (Temperatur zu kalt?)
                JR      C,DoPh                  ;Ja =>
                BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JR      Z,DoTemp11              ;Nein =>
                SET     5,(IX+WarnLED)          ;Temp.Alarm aus
                JR      DoPh
DoTemp11:       RES     5,(IX+WarnLED)          ;Temp.Alarm an

DoPh:           LD      (C000h),A
                LD      B,(IX+IstpH)            ;Ist-pH-Wert
                SRL     B
                PUSH    BC
                LD      HL,CO2Ein
                LD      DE,CO2Aus
                CALL    InTimeRange             ;CO2-Sperrzeit?
                JR      C,DoPh2                 ;Ja =>
                BIT     1,(IX+WarnLED)          ;CO2-LED an?
                JR      Z,DoPh4                 ;Ja! =>
                LD      A,(IX+SollpH)           ;Soll-pH-Wert
                POP     BC
                CP      B                       ;>= Ist-pH-Wert?
                JR      NC,DoPh3                ;zu groß =>
                RES     1,(IX+WarnLED)          ;CO2-LED an
                SET     0,(IX+Steckdosen)       ;CO2 an
                LD      HL,AktTime
                LD      DE,CO2EinZeit
                LD      BC,3
                LDIR                            ;Einschaltzeit des CO2
                LD      HL,AktTime
                LD      BC,3
                LDIR                            ;Alarmzeit des CO2
                DEC     DE
                LD      A,(DE)
                ADD     A,3                     ;= Einschaltzeit + 3h
                DAA
                CP      24h                     ;24h Überlauf?
                JR      C,DoPh1                 ;Nein =>
                SUB     24h                     ;- 24h
DoPh1:          LD      (DE),A
                JR      DoPh7

DoPh2:          POP     BC
DoPh3:          SET     2,(IX+WarnLED)          ;CO2-Alarm aus
                SET     1,(IX+WarnLED)          ;CO2-LED aus
                RES     0,(IX+Steckdosen)       ;CO2 aus
                JR      DoPh7

DoPh4:          LD      HL,CO2EinZeit
                LD      DE,CO2AlarmZeit
                CALL    InTimeRange             ;CO2 schon 3h an?
                JR      NC,DoPh5                ;Nein =>
                LD      A,(IX+IstpH)            ;Ist-pH-Wert
                SRL     A
                CP      (IX+SollpH)             ;= Soll-pH-Wert
                JR      Z,DoPh5                 ;Ja => (kein Alarm)
                BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JR      Z,DoPh5
                RES     2,(IX+WarnLED)          ;pH-Alarm an
                JR      DoPh6
DoPh5:          SET     2,(IX+WarnLED)          ;pH-Alarm aus
DoPh6:          LD      A,(IX+SollpH)           ;Soll-pH-Wert
                SUB     1                       ;- 0.05
                POP     BC
                CP      B                       ;=> Ist-pH-Wert? (Vergleich: Ist-pH-Wert < Soll-pH-Wert)
                JR      NC,DoPh3                ;zu groß => CO2 aus

DoPh7:          LD      A,(IX+IstpH)            ;Ist-pH-Wert
                SRL     A
                ADD     A,6                     ;+ 0.3
                CP      (IX+SollpH)             ;Soll-pH-Wert (Ist-pH-Wert <= Soll-pH-Wert + 0.35)
                JR      NC,DoMomentan
                BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JR      Z,DoPh8
                SET     2,(IX+WarnLED)          ;pH-Alarm aus
                JR      DoMomentan
DoPh8:          RES     2,(IX+WarnLED)          ;pH-Alarm an

DoMomentan:     CALL    TempKomp                ;Leitwert mit der Temperatur kompensieren
                LD      (C000h),A

                BIT     5,(IX+KLED)             ;"Momentan" an?
                JR      NZ,DoLaufschr           ;Nein =>
                BIT     3,(IX+Flags)            ;Momentane Werte durchschalten?
                JR      Z,DoLaufschr            ;Nein =>
                LD      HL,MomentanSek
                LD      A,(AktTime)             ;Sekunden beim letzten Durchlauf
                CP      (HL)                    ;Sekunden geändert?
                JR      Z,DoLaufschr            ;Nein =>
                LD      (HL),A                  ;letzten Sekundenstand merken
                LD      HL,DelayTimer           ;Pause für die Darstellung
                LD      A,(HL)
                INC     A
                IF      NewVersion
                PUSH    HL
                LD      HL,MomentanZeit
                CP      (HL)                    ;Momentane Sekunden abgelaufen?
                POP     HL
                ELSE
                CP      7                       ;7 Sekunden darstellen
                ENDIF
                JR      C,DoMomentan4           ;Zeit abgelaufen? Nein =>
                LD      A,(IX+KLED+1)           ;LEDs rechte Spalte auslesen
                OR      F0h
                CP      FFh                     ;alle LEDs aus?
                JR      NZ,DoMomentan1          ;Nein =>
                SET     4,(IX+KLED)             ;Uhrzeit ausschalten
                RES     0,(IX+KLED+1)           ;pH-Wert anschalten
                JR      DoMomentan3
DoMomentan1:    RLCA                            ;Anzeige weiterschalten
                BIT     4,A                     ;Überlauf?
                JR      NZ,DoMomentan2          ;Nein =>
                LD      A,(IX+KLED+1)
                OR      0Fh                     ;rechte Spalte ausschalten
                LD      (IX+KLED+1),A
                RES     4,(IX+KLED)             ;Uhrzeit anschalten
                JR      DoMomentan3
DoMomentan2:    LD      B,A                     ;rotierte Matrix merken
                LD      A,(IX+KLED+1)           ;rechte Spalte erneut auslesen
                OR      0Fh                     ;alle LEDs aus
                AND     B                       ;rotierte Matrix dazu
                LD      (IX+KLED+1),A           ;und neue LEDs anschalten
DoMomentan3:    LD      A,0                     ;Pause wieder zurücksetzen
DoMomentan4:    LD      (HL),A                  ;Delay-Timer setzen

DoLaufschr:     IF !NewVersion
                LD      (C000h),A
                LD      A,(LaufschriftFlag)
                CP      55h
                JR      NZ,DoSollChecksum       ;keine Laufschrift =>
                LD      A,(LaufschriftInit)
                CP      55h                     ;Laufschrift initialisiert?
                JR      Z,DoLaufschr1           ;Ja =>
                LD      HL,(LaufschriftPtr)
                LD      (ScrollPtr),HL          ;Laufschrift-Text setzen
                LD      A,55h
                LD      (LaufschriftInit),A     ;aktiv schalten
                LD      (IX+KLED),FFh           ;alle Tasten-LEDs aus
                LD      (IX+KLED+1),FFh

DoLaufschr1:    LD      A,(DelayTimer)
                DEC     A                       ;DelayTimer runterzählen
                LD      (DelayTimer),A
                JR      NZ,DoSollChecksum       ;noch nicht abgelaufen =>
                LD      A,12
                LD      (DelayTimer),A          ;DelayTimer neu setzen
                LD      HL,(ScrollPtr)
                LD      A,(HL)                  ;nächstes Zeichen aus dem Scrollstring
                INC     HL
                LD      (ScrollPtr),HL
                CP      40h                     ;"Display löschen"? (Stringanfang)
                JR      NZ,DoLaufschr3          ;Nein! =>
                LD      HL,Display              ;Display löschen
                LD      B,6
DoLaufschr2:    LD      (HL),1Fh                ;Leerzeichen
                INC     HL
                DJNZ    DoLaufschr2
                JR      DoSollChecksum
DoLaufschr3:    CP      42h                     ;"Leerzeichen (langsam)"?
                JR      Z,DoLaufschr4           ;Ja! =>
                CP      41h                     ;Neustart vom Anfang an? (Stringende)
                JR      NZ,DoLaufschr5          ;Nein! =>
                LD      HL,(LaufschriftPtr)
                LD      (ScrollPtr),HL
                LD      A,80
                LD      (DelayTimer),A          ;6.7-fache Pause vorher einlegen
                JR      DoSollChecksum          ;nix ausgeben =>
DoLaufschr4:    LD      A,48
                LD      (DelayTimer),A          ;4-fache Pause
                LD      A,1Fh                   ;Leerzeichen ausgeben
DoLaufschr5:    LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                LD      (IX+KLED),FFh           ;alle Tasten-LEDs aus
                LD      (IX+KLED+1),FFh
                LD      DE,Display
                LD      HL,Display+1
                LD      BC,5
                LDIR                            ;nach links scrollen
                LD      (DE),A                  ;neues Zeichen einfügen
                ENDIF

DoSollChecksum: LD      (C000h),A
                CALL    CalcSollChecksum        ;Prüfsumme über die Sollwerte errechnen
                LD      HL,(SollChecksum)       ;alte Prüfsumme holen
                XOR     A                       ;kein Fehler
                SBC     HL,DE                   ;Prüfsummen gleich?
                JR      Z,DoBetrStd             ;Ja! =>
                LD      (SollChecksum),DE       ;als neue Prüfsumme merken
                LD      A,80h                   ;Prüfsumme über die Sollwerte geändert!
                LD      (ErrorCode),A

DoBetrStd:      LD      (C000h),A
                LD      A,(IX+AktTime+2)        ;aktuelle Stunden
                LD      HL,GesamtBZeit+3
                CP      (HL)                    ;= Gesamtzeitstunden (low-Byte der Stunden)
                JR      Z,DoBetrStd1            ;Ja =>
                LD      (HL),A
                LD      HL,GesamtBZeit
                CALL    IncHour                 ;Gesamtzeit um eine Stunde erhöhen
DoBetrStd1:     LD      A,(IX+AktTime+1)        ;aktuelle Minuten
                LD      HL,GesamtBZeit+4
                CP      (HL)                    ;= Gesamtzeitminuten
                JR      Z,DoInitStr             ;Ja => (noch keine Minute rum)
                LD      (HL),A                  ;neue Minuten merken

                BIT     3,(IX+WarnLED)          ;Kanal 1-LED an?
                JR      NZ,DoBetrStd2           ;Nein =>
                LD      HL,Kanal1BZeit
                CALL    IncMinute
DoBetrStd2:     BIT     0,(IX+WarnLED)          ;Kanal 2-LED an?
                JR      NZ,DoBetrStd3           ;Nein =>
                LD      HL,Kanal2BZeit
                CALL    IncMinute
DoBetrStd3:     BIT     1,(IX+WarnLED)          ;CO2-LED an?
                JR      NZ,DoBetrStd4           ;Nein =>
                LD      HL,CO2BZeit
                CALL    IncMinute
DoBetrStd4:     BIT     4,(IX+WarnLED)          ;Heizung-LED an?
                JR      NZ,DoBetrStd5           ;Nein =>
                LD      HL,TempBZeit
                CALL    IncMinute
DoBetrStd5:     BIT     2,(IX+Steckdosen)       ;Licht an?
                JR      Z,DoInitStr             ;Nein =>
                LD      HL,LichtBZeit
                CALL    IncMinute

DoInitStr:      IF !NewVersion
                LD      (C000h),A
                LD      HL,InitLaufschrSek
                LD      A,(AktTime)             ;Sekunden auslesen
                CP      (HL)
                JP      Z,DoROMChksum           ;gleich der gemerkten Sekunden? =>
                LD      (HL),A
                LD      A,(InitLaufschr)        ;Init-Laufschrift?
                CP      55h
                JP      Z,DoROMChksum           ;gleich =>
                LD      IY,Dummy0
                LD      (IY+0),0                ;???
                LD      (IY+1),0

                LD      DE,StringBuf
                LD      A,2
                LD      (DE),A
                INC     DE
                LD      HL,AktTime
                LD      B,3
DoInitStr1:     LD      A,(HL)
                CALL    HexByteOut              ;Uhrzeit ausgeben
                INC     HL
                DJNZ    DoInitStr1
                LD      A,' '
                LD      (DE),A
                INC     DE
                LD      HL,Dummy                ;???
                LD      B,3
DoInitStr2:     LD      A,(HL)
                CALL    HexByteOut
                INC     HL
                DJNZ    DoInitStr2
                LD      A,' '
                LD      (DE),A
                INC     DE
                LD      A,(IX+IstpH)            ;Ist-pH-Wert
                SRL     A
                CALL    HexByteOut
                LD      A,' '
                LD      (DE),A
                INC     DE
                LD      A,(IX+IstTemp)          ;Ist-Temp-Wert
                CALL    HexByteOut
                LD      A,' '
                LD      (DE),A
                INC     DE
                LD      A,(IX+LeitwertKomp)     ;kompensierter Leitwert
                CALL    HexByteOut
                LD      A,' '
                LD      (DE),A
                INC     DE
                LD      A,(IX+IstRedox)         ;Ist-Redox-Wert
                CALL    HexByteOut
                LD      A,' '
                LD      (DE),A
                INC     DE
                IN      A,(C)
                AND     20h                     ;Süßwasser/Meerwasser-Schalter
                LD      B,A
                LD      A,(IX+Steckdosen)       ;Steckdosen-Status
                AND     1Fh
                OR      B
                CALL    HexByteOut
                LD      A,' '
                LD      (DE),A
                INC     DE
                LD      HL,(Dummy0)             ;= 0
                LD      A,H
                CALL    HexByteOut
                LD      A,L
                CALL    HexByteOut
                LD      A,13
                LD      (DE),A
                INC     DE
                LD      A,3
                LD      (DE),A
                LD      HL,StringBuf
                LD      (StringBufPtr),HL
                LD      A,55h
                LD      (InitLaufschr),A        ;Init-Laufschrift AUS
                ENDIF

;Prüfsummenberechnung über das ROM
DoROMChksum:    LD      (C000h),A
                LD      DE,(AktROMChecksum)     ;alte Prüfsumme lesen
                LD      HL,(ROMTopAdr)          ;Endadresse vom ROM - 1
                DEC     HL
                LD      (ROMTopAdr),HL          ;Endadresse - 1
                LD      A,L
                OR      H                       ;Adresse zusammen"OR"n
                LD      L,(HL)                  ;Speicherstelle auslesen
                LD      H,00h
                ADD     HL,DE                   ;alte Prüfsumme dazuaddieren
                LD      (AktROMChecksum),HL     ;Prüfsumme neu merken
                CP      00h                     ;Anfang vom ROM erreicht?
                JR      NZ,DoMainloop           ;Nein =>
                LD      HL,(AktROMChecksum)     ;Prüfsumme auslesen
                LD      (CalcChecksum),HL       ;errechnete Prüfsumme merken
                LD      A,0
                LD      (ChecksumFinal),A
                LD      DE,(ROMChecksum)        ;erwartete Prüfsumme
                XOR     A
                SBC     HL,DE                   ;Prüfsumme gleich?
                JR      Z,DoROMChksum1          ;Ja! =>
                LD      A,81h                   ;Programmstörung!
                LD      (ErrorCode),A
DoROMChksum1:   LD      HL,0
                LD      (AktROMChecksum),HL     ;alte Prüfsumme zurücksetzen
                LD      HL,ROMTop
                LD      (ROMTopAdr),HL          ;Ende vom ROM neu setzen

DoMainloop:     LD      (C000h),A
                JP      DoLEDKonv               ;und wieder von vorne...

; Unbenutzer Code:
                IF !NewVersion
DoComm:         LD      A,(IX+Steckdosen)       ;Steckdoses-Status
                RES     5,A
                CALL    DoComm4                 ;0??
                LD      B,7
DoComm1:        RR      C
                JR      C,DoComm2               ;8 Bits übertragen
                RES     5,A
                JR      DoComm3
DoComm2:        SET     5,A
DoComm3:        CALL    DoComm4
                DJNZ    DoComm1
                PUSH    HL
                POP     HL
                SET     5,A
                LD      (E000h),A               ;1??
                RET
DoComm4:        LD      (E000h),A
                NOP
                NOP
                NOP
                RET
                ENDIF

;Hexbyte nach DE schreiben. (IY+0/1) enthält die Prüfsumme
                IF !NewVersion
HexByteOut:
                PUSH    AF
                SRL     A
                SRL     A
                SRL     A
                SRL     A
                CALL    HexByteOut1
                POP     AF
                AND     0Fh
HexByteOut1:    CP      10                      ;größer als 10?
                JR      C,HexByteOut2           ;Nein! =>
                ADD     A,'7'                   ;+ '7' = 'A'...'F'
                JR      HexByteOut3
HexByteOut2:    ADD     A,'0'                   ;sonst + '0' = '0'...'9'
HexByteOut3:    LD      (DE),A                  ;in den Buffer schreiben
                INC     DE
                ADD     A,(IY+0)                ;alte Summe dazuaddieren
                LD      (IY+0),A                ;als neue Summe merken
                LD      (C000h),A
                RET     NC                      ;Überlauf der Prüfsumme? Nein => raus
                INC     (IY+1)                  ;Prüfsummen-Highbyte hochzählen
                RET
                ENDIF

; String ab HL nach DE bis zum "$" kopieren
                IF !NewVersion
CopyString:
                LD      (C000h),A
                LD      A,(HL)                  ;Zeichen aus dem String holen
                CP      '$'                     ;Textende erkannt?
                RET     Z                       ;dann raus =>
                LD      (DE),A                  ;Zeichen übertragen
                INC     DE
                INC     HL
                JR      CopyString

; String mit Fehlermeldung in HL (zwei ASCII-Zeichen) zusammensetzen
ErrorString:
                PUSH    HL
                LD      HL,MsgFehl              ;"FEHL." ausgeben
                CALL    CopyString
                POP     HL
                LD      A,H
                LD      (DE),A                  ;Fehlernummer übertragen
                INC     DE
                LD      A,L
                LD      (DE),A
                INC     DE
                LD      HL,Msg6Space            ;"      ",13,10,10 anhängen
                CALL    CopyString
                RET

; "." an den String anhängen
ConcatPunkt:    EX      DE,HL
                LD      (HL),'.'
                EX      DE,HL
                INC     DE
                RET
                ENDIF

; Prüfsumme über die Sollwerte berechnen, Ergebnis nach DE
CalcSollChecksum:
                PUSH    HL
                LD      HL,0
                LD      D,0
                LD      E,(IX+SollpH)           ;Soll-pH-Wert
                ADD     HL,DE
                LD      E,(IX+SollTempTag)      ;Soll-Temperatur (Tag)
                ADD     HL,DE
                LD      E,(IX+SollTempNacht)    ;Soll-Temperatur (Nacht)
                ADD     HL,DE
                LD      E,(IX+SollLeitwertS)    ;Soll-Leitwert (Süßwasser)
                ADD     HL,DE
                LD      E,(IX+SollLeitwertM)    ;Soll-Leitwert (Meerwasser)
                ADD     HL,DE
                LD      E,(IX+SollRedox)        ;Soll-Redoxwert
                ADD     HL,DE
                EX      DE,HL
                POP     HL
                LD      (C000h),A
                RET

;langer Timer (1 Byte Minuten, 3 Bytes Stunden) um eine Minute erhöhen
IncMinute:      LD      A,(HL)
                INC     A
                CP      60                      ;Sekundenüberlauf?
                JR      C,IncMinute1            ;Nein =>
                LD      (HL),0                  ;Sekunden auf 0 zurücksetzen
                INC     HL
                JR      IncHour
IncMinute1:     LD      (HL),A
                RET
IncHour:        LD      B,3                     ;3 Bytes für Stunden
IncHour1:       LD      A,(HL)
                ADD     A,1                     ;Stunden um eins erhöhen
                DAA
                LD      (HL),A
                JR      NC,IncHour2
                INC     HL
                DJNZ    IncHour1
IncHour2:       RET

;"Zeit" gedrückt
KeyZeit:        RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED)             ;Tastenlampen holen
                BIT     4,A                     ;Zeit war bereits an?
                JR      NZ,KeyZeit6             ;Nein =>
                BIT     4,(IX+KLED+1)           ;"Kanal 1"
                JR      NZ,KeyZeit1             ;Nein =>
                LD      HL,Kanal1BZeit+1
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET
KeyZeit1:       BIT     5,(IX+KLED+1)           ;"Kanal 2"
                JR      NZ,KeyZeit2             ;Nein =>
                LD      HL,Kanal2BZeit+1
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET
KeyZeit2:       BIT     7,(IX+KLED+1)           ;"CO2"
                JR      NZ,KeyZeit3             ;Nein =>
                LD      HL,CO2BZeit+1
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET
KeyZeit3:       BIT     6,(IX+KLED+1)           ;"Licht"
                JR      NZ,KeyZeit4             ;Nein =>
                LD      HL,LichtBZeit+1
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET
KeyZeit4:       BIT     1,(IX+KLED+1)           ;"Temperatur"
                JR      NZ,KeyZeit5             ;Nein =>
                LD      HL,TempBZeit+1
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET
KeyZeit5:       LD      HL,GesamtBZeit          ;Gesamtbetriebsstunden
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET

KeyZeit6:       RES     4,A                     ;Zeit-LED-Flag an
                OR      0Fh                     ;Tag,Nacht,Ein,Aus-LEDs aus
                LD      (IX+KLED),A             ;LED-Status setzen
                LD      A,(IX+KLED+1)
                IF NewVersion
                OR      0Fh                     ;alle LEDs aus
                ELSE
                OR      0Dh                     ;bis auf Temperatur alle LEDs aus (WARUM???)
                ENDIF
                LD      (IX+KLED+1),A
                BIT     7,(IX+KLED)             ;"Setzen" an?
                JR      NZ,KeyZeit8             ;Nein =>
                BIT     4,(IX+KLED+1)           ;"Kanal 1" an?
                JR      NZ,KeyZeit7             ;Nein! =>
                LD      (IX+Uni1Flag),55h
                LD      HL,StrLrUNI1            ;" UNI-1"
                SET     7,(IX+KLED)             ;"Setzen" aus
                JP      SetDisplayString
KeyZeit7:       BIT     5,(IX+KLED+1)           ;"Kanal 2" an?
                RET     NZ                      ;Nein =>
                LD      (IX+Uni2Flag),55h
                LD      HL,StrLrUNI2            ;" UNI-2"
                SET     7,(IX+KLED)             ;"Setzen" aus
                JP      SetDisplayString
KeyZeit8:       RET

;"Ein" gedrückt
KeyEin:         LD      HL,StrLrEIN             ;"Lr-EIn"
                LD      B,55h                   ;Ein-Flag
                RES     2,(IX+KLED)             ;Ein-LED an
                SET     3,(IX+KLED)             ;Aus-LED aus
                JR      KeyAus1

;"Aus" gedrückt
KeyAus:         LD      HL,StrLrAUS             ;"Lr-AUS"
                LD      B,AAh                   ;Aus-Flag
                RES     3,(IX+KLED)             ;Aus-LED an
                SET     2,(IX+KLED)             ;Ein-LED aus
KeyAus1:        RES     0,(IX+Flags)            ;Zahleingabe aus
                BIT     5,(IX+KLED+1)           ;"Kanal 2"
                JP      NZ,KeyAus2              ;Nein =>
                BIT     2,(IX+KLED+1)           ;"Leitwert"
                JP      NZ,KeyAus2              ;Nein =>
                BIT     7,(IX+KLED)             ;"Setzen" an?
                JP      NZ,KeyAus2              ;Nein! =>
                LD      (IX+Uni2Flag),AAh       ;Leitwert-Regelung
                LD      (IX+Uni2Flag2),B        ;Ein- oder Aus-Regelung setzen
                SET     7,(IX+KLED)             ;"Setzen"-LED aus
                JP      SetDisplayString

;Display löschen
KeyStern:       LD      HL,Display
                LD      B,6
KeyStern1:      LD      (HL),0                  ;6 mal '0' ins Display (führende Nullen werden NICHT ausgegeben)
                INC     HL
                DJNZ    KeyStern1
                INC     HL
                RES     0,(HL)                  ;Zahleingabe aus
                INC     HL
                INC     HL
                LD      (HL),FFh                ;alle Punkte im Display aus
                LD      B,A
                LD      (IX+KLED+1),FFh
                LD      A,BFh
                OR      (IX+KLED)
                LD      (IX+KLED),A             ;Bis auf manuelles Licht alle Tasten-LEDs aus
                SET     0,(IX+Flags)            ;Zahleingabe an
                RES     2,(IX+Flags)            ;PowerOn-Flag zurücksetzen
                RES     3,(IX+Flags)            ;keine Momentan-Werte durchschalten
                IF !NewVersion
                LD      A,AAh
                LD      (LaufschriftFlag),A     ;Laufschrift ausschalten
                LD      (LaufschriftInit),A
                ENDIF
                LD      A,6
                LD      (DelayTimer),A
                RES     4,(IX+Flags)            ;Zahl im Display
                LD      A,0
                LD      (ErrorCode),A           ;Fehlercode löschen
                LD      A,B
                LD      (C000h),A
                RET

;Diese Routine wird bei Druck auf "." angesprungen
KeyPunkt:       BIT     0,(IX+Flags)            ;Zahleingabe an?
                CALL    Z,KeyStern              ;Nein! => erstmal das Display löschen
                SET     0,(IX+Flags)            ;Zahleingabe aktivieren
                LD      A,(IX+DPunkt)
                XOR     01h                     ;Dezimalpunkt toggeln
                LD      (IX+DPunkt),A
                LD      (C000h),A
                RET

;pH-Taste gedrückt
KeyPh:          LD      (IX+KLED+1),FEh         ;pH-LED an
                RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED)
                OR      18h                     ;Zeit und Aus LEDs aus
                LD      (IX+KLED),A
                BIT     5,(IX+KLED)             ;"Momentan"?
                RET     Z                       ;Ja => raus
                BIT     7,(IX+KLED)             ;"Setzen" an?
                JR      Z,KeyPh1                ;Ja! =>
                JP      DispSollPh              ;Soll-pH-Wert darstellen
KeyPh1:         LD      HL,Display
                IF !NewVersion
                LD      A,0
                OR      (HL)
                INC     HL
                OR      (HL)
                INC     HL
                OR      (HL)
                JR      NZ,KeyPh2
                INC     HL
                LD      A,(HL)
                CP      1
                JR      NZ,KeyPh2               ;"15.0" eingegeben?
                INC     HL
                LD      A,(HL)
                CP      5
                JR      NZ,KeyPh2               ;Nein =>
                INC     HL
                LD      A,(HL)
                CP      0
                JR      NZ,KeyPh2
                LD      A,(IX+DPunkt)
                CP      FDh                     ;Dezimalpunkt
                JR      NZ,KeyPh2
                LD      A,55h
                LD      (LaufschriftFlag),A     ;Laufschrift an
                LD      HL,MsgBasis
                LD      (LaufschriftPtr),HL     ;Laufschrift-Text
                RET
                ENDIF
KeyPh2:         CALL    GetNumInput             ;Eingabe holen
                LD      A,D                     ;Anzahl der Dezimalpunkte holen
                CP      0
                JR      Z,KeyPh3
                CP      1                       ;0 oder 1 ist i.O.
                JR      Z,KeyPh4
                LD      A,3
                JP      MakeErrCode             ;mehrere Dezimalpunkte bei pH-Werteingabe
KeyPh3:         LD      IY,InputBuf+7           ;Ptr auf die letzte Ziffer
KeyPh4:         LD      B,E                     ;Position des Dezimalpunktes (1...6)
                LD      A,0
                INC     IY
                PUSH    IY
KeyPh5:         OR      (IY-2)                  ;alle Ziffern _VOR_ der 1.Vorkommastelle zusammen"OR"n
                DEC     IY
                DJNZ    KeyPh5
                POP     IY
                CP      0                       ;gibt es dort Ziffern <> "0"?
                JR      Z,KeyPh6                ;Nein! =>
                LD      A,1
                JP      MakeErrCode             ;pH-Wert zu groß!
KeyPh6:         CALL    ConvertInput            ;pH-Wert holen
                LD      A,L
                SUB     38                      ;3.8 ist für einen pH-Wert zu klein!
                JR      C,KeyPh7                ;< 3.8 => Fehler
                JR      Z,KeyPh7                ;= 3.8 => Fehler
                JR      KeyPh8                  ;alles ok =>
KeyPh7:         LD      A,2
                JP      MakeErrCode             ;pH-Wert zu klein
KeyPh8:         SLA     A                       ;mal 2
                LD      B,A
                LD      A,(IY+1)                ;2.Nachkommastelle holen
                CP      3
                JR      C,KeyPh10               ;<0.03? => nicht aufrunden
                CP      8
                JR      C,KeyPh9                ;<0.08? => auf 0.05 aufrunden
                INC     B                       ;>=0.08? => auf 0.10 aufrunden
KeyPh9:         INC     B
KeyPh10:        LD      (IX+SollpH),B           ;als neuen pH-Sollwert merken ((pH-Wert*10-38)*2)
                CALL    CalcSollChecksum        ;Prüfsumme über die Sollwerte errechnen
                LD      (SollChecksum),DE       ;und merken
                SET     7,(IX+KLED)             ;"Setzen" aus
                LD      (C000h),A
DispSollPh:     LD      A,(IX+SollpH)           ;neuen pH-Wert holen

;pH-Wert darstellen
DispPh:         BIT     0,A                     ;Bit 0 = 2.Nachkommastelle
                JR      Z,DispPh1
                LD      B,5                     ;gesetzt = 0.05
                JR      DispPh2
DispPh1:        LD      B,0                     ;gelöscht = 0.00
DispPh2:        LD      (IX+Display+2),B
                SRL     A                       ;(pH-Wert / 2) + 38
                ADD     A,38
                LD      E,A
                LD      D,0
                PUSH    DE
                POP     IY                      ;Zahl nach IY
                CALL    MakeBCD
                LD      HL,BCDZahl              ;Ptr auf die BCD-Zahl
                LD      A,0
                RRD     (HL)                    ;unteres Nibble ab HL nach A holen
                LD      (IX+Display+1),A        ;2.Stelle
                RRD     (HL)                    ;oberes Nibble ab HL nach A holen
                LD      (IX+Display),A          ;1.Stelle
                LD      (IX+Display+3),1Fh      ;Leerzeichen
                LD      (IX+Display+4),12h      ;"P"
                LD      (IX+Display+5),10h      ;"H"
                LD      (IX+DPunkt),DFh         ;Dezimalpunkt nach der 1.Stelle
                LD      (C000h),A
                RET

;"Setzen" gedrückt
KeySetzen:      RES     0,(IX+Flags)            ;Zahleingabe aus
                IN      A,(C)                   ;Sperre gesetzt?
                BIT     4,A                     ;Ja! =>
                JR      Z,KeySetzen1
                BIT     4,(IX+Flags)            ;Zahl im Display?
                CALL    NZ,KeyStern             ;Nein! =>
                LD      A,(IX+KLED)
                XOR     80h                     ;Setzen-toggeln
                OR      3Fh                     ;bis auf "Manuell" alle LEDs ausschalten
                LD      (IX+KLED),A
                LD      (IX+KLED+1),FFh
                LD      (C000h),A
                RET
KeySetzen1:     LD      A,99h                   ;Programmiersperre gesetzt
                JP      MakeErrCode

;"Momentan" gedrückt
KeyMomentan:    RES     0,(IX+Flags)            ;Zahleingabe aus
                BIT     5,(IX+KLED)             ;"Momentan" bereits an?
                JR      NZ,KeyMomentan1         ;Nein =>
                SET     3,(IX+Flags)            ;Momentane Werte durchschalten
                IF NewVersion
                LD      A,0
                LD      (DelayTimer),A          ;sofortige Ausgabe der Werte erzwingen
                DEC     A
                LD      (MomentanSek),A
                ENDIF
                JR      KeyMomentan2
KeyMomentan1:   RES     3,(IX+Flags)            ;Momentane Werte nicht mehr durchschalten
KeyMomentan2:   RES     5,(IX+KLED)             ;Momentan-LED an
                LD      A,(IX+KLED)
                OR      0Fh                     ;Tag, Nacht, Ein, Aus LEDs ausschalten
                LD      (IX+KLED),A
                LD      (IX+KLED+1),FFh         ;rechte LEDs ausschalten
                BIT     7,(IX+KLED)             ;"Setzen" an?
                LD      (C000h),A
                RET     NZ                      ;Nein =>
                BIT     4,(IX+KLED)             ;"Zeit" an?
                JR      Z,KeyMomentan4          ;Ja! =>
                IF NewVersion
                LD      A,0
                LD      B,4
                LD      HL,Display              ;die ersten 4 Ziffern müssen stets = 0 sein
KeyMomentan6:   OR      (HL)
                INC     HL
                DJNZ    KeyMomentan6
                JR      NZ,KeyMomentan7         ;wenn nicht => Fehler
                LD      IY,Display+5            ;Ptr auf die letzte Stelle vom Display
                CALL    ConvertInput            ;Zahl nach HL holen
                LD      A,L
                LD      HL,MomentanZeit
                LD      (HL),A                  ;aktuelle Schaltzeit merken
                SET     7,(IX+KLED)             ;Setzen-LED aus
                ENDIF
KeyMomentan3:   SET     5,(IX+KLED)             ;Momentan-LED aus
                RET
                IF NewVersion
KeyMomentan7:   LD      A,20h
                JP      MakeErrCode             ;Eingabe falsch!
                ENDIF
KeyMomentan4:   LD      BC,0A00h                ;2560 Schleifendurchläufe
KeyMomentan5:   HALT
                LD      A,(KeyboardMatrix+6)
                BIT     2,A                     ;Manuell immer noch gedrückt?
                JR      NZ,KeyMomentan3         ;Nein => Zeit nicht neu setzen
                LD      (C000h),A
                HALT
                DEC     BC                      ;Zähler runterzählen
                LD      A,B                     ;0 erreicht?
                OR      C
                JR      NZ,KeyMomentan5         ;Nein => weiter warten
                SET     5,(IX+KLED)             ;Momentan-LED aus
                JP      SetSystemTime

;"Temperatur"-Taste gedrückt
KeyTemperatur:  RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED+1)
                RES     1,A                     ;Temperatur an
                OR      FDh                     ;andere LEDs aus
                LD      (IX+KLED+1),A
                LD      A,(IX+KLED)
                OR      1Fh                     ;außer Momentan, Manuell und Setzen alles aus
                LD      (IX+KLED),A
                LD      (C000h),A
                RET

;"Kanal 1" gedrückt
KeyKanal1:      RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED)
                OR      1Fh                     ;Tag,Nacht,Ein,Aus,Zeit im linken Bereich aus
                LD      (IX+KLED),A
                LD      A,(IX+KLED+1)
                OR      FFh                     ;alle LEDs im rechten Bereich aus
                RES     4,A                     ;und "Kanal 1"-LED an
                LD      (IX+KLED+1),A
                BIT     5,(IX+KLED)             ;"Momentan" an?
                RET     NZ                      ;Nein => raus
                LD      A,(IX+Uni1Flag)         ;Zustand von Kanal 1
                CP      55h
                JR      Z,KeyKanal12            ;=> Universaltimer
                CP      AAh
                JR      Z,KeyKanal11            ;=> Redox-Regler
                LD      HL,Str6Minus            ;"------"
                JP      SetDisplayString
KeyKanal11:     LD      HL,StrrErE              ;" rE-rE"
                JP      SetDisplayString
KeyKanal12:     LD      HL,StrLrUNI1            ;" UNI-1"
                JP      SetDisplayString

;"Kanal 2" gedrückt
KeyKanal2:      RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED)
                OR      1Fh                     ;Tag,Nacht,Ein,Aus,Zeit im linken Bereich aus
                LD      (IX+KLED),A
                LD      A,(IX+KLED+1)
                OR      FFh                     ;alle LEDs im rechten Bereich aus
                RES     5,A                     ;und "Kanal 2"-LED an
                LD      (IX+KLED+1),A
                BIT     5,(IX+KLED)             ;"Momentan" an?
                RET     NZ                      ;Nein => raus
                LD      A,(IX+Uni2Flag)         ;Zustand von Kanal 2
                CP      55h
                JR      Z,KeyKanal24            ;=> Universaltimer
                CP      AAh
                JR      Z,KeyKanal21            ;=> Leitwert-Regler
                LD      HL,Str6Minus            ;"------"
                JP      SetDisplayString
KeyKanal21:     LD      A,(IX+Uni2Flag2)        ;Ein- oder Aus-Regelung?
                CP      55h
                JR      Z,KeyKanal23            ;=> Ein-Regelung
                CP      AAh
                JR      Z,KeyKanal22            ;=> Aus-Regelung
                LD      HL,Str6Minus            ;"------"
                JP      SetDisplayString
KeyKanal22:     LD      HL,StrLrAUS             ;"Lr-AUS"
                JP      SetDisplayString
KeyKanal23:     LD      HL,StrLrEIN             ;"Lr-EIN"
                JP      SetDisplayString
KeyKanal24:     LD      HL,StrLrUNI2            ;" UNI-2"
                JP      SetDisplayString

;"CO2" gedrückt
KeyCO2:         RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED+1)
                RES     7,A                     ;CO2-LED an
                OR      7Fh                     ;alle LEDs aus
                LD      (IX+KLED+1),A
                LD      A,(IX+KLED)
                OR      3Fh                     ;bis auf "Manuell" und "Setzen" alle LEDs aus
                LD      (IX+KLED),A
                LD      (C000h),A
                RET

;"Licht" gedrückt
KeyLicht:       RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED+1)
                RES     6,A                     ;Licht-LED an
                OR      BFh                     ;alle LEDs aus
                LD      (IX+KLED+1),A
                LD      A,(IX+KLED)
                OR      3Fh                     ;bis auf "Manuell" und "Setzen" alle LEDs aus
                LD      (IX+KLED),A
                LD      (C000h),A
                RET

;"Tag" gedrückt
KeyTag:         LD      HL,TagZeit              ;Ptr auf Tagdaten
                RES     0,(IX+KLED)             ;Tag an
                SET     1,(IX+KLED)             ;Nacht aus
                JR      KeyNacht1

;"Nacht" gedrückt
KeyNacht:       LD      HL,NachtZeit            ;Ptr auf Nachtdaten
                RES     1,(IX+KLED)             ;Nacht an
                SET     0,(IX+KLED)             ;Tag aus
KeyNacht1:      RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED)
                OR      2Ch                     ;Ein, Aus und Momentan aus
                LD      (IX+KLED),A
                LD      A,(IX+KLED+1)
                OR      FDh                     ;bis auf "Temperatur" alles aus
                LD      (IX+KLED+1),A
                BIT     7,(IX+KLED)             ;"Setzen" an?
                JR      Z,KeyNacht2             ;Ja! =>
                BIT     4,(IX+KLED)             ;"Zeit" an?
                JP      Z,PrintTime             ;Ja! =>
                BIT     1,(IX+KLED+1)           ;"Temperatur" an?
                JP      Z,PrintSollTemp         ;°C ausgeben
                JR      KeyNacht3
KeyNacht2:      BIT     4,(IX+KLED)             ;"Zeit" an?
                JP      Z,GetDispTime           ;Ja! => Zeit für Tag oder Nacht setzen
                BIT     1,(IX+KLED+1)           ;"Temperatur" an?
                JP      Z,SetSollTemp           ;Temperatur für Tag oder Nacht setzen =>
                SET     7,(IX+KLED)             ;"Setzen" aus
KeyNacht3:      SET     1,(IX+KLED)             ;Nacht aus
                SET     0,(IX+KLED)             ;Tag aus
                LD      (C000h),A
                RET

;"Manuell" gedrückt
KeyManuell:     RES     0,(IX+Flags)            ;Zahleingabe aus
                LD      A,(IX+KLED)
                BIT     7,A                     ;"Setzen" an?
                JR      Z,KeyManuell2           ;Ja! =>
                LD      BC,3
                LD      DE,ManuellEinZeit
                LD      HL,AktTime              ;Uhrzeit retten
                LDIR
                LD      HL,ManuellZeit
                BIT     4,A                     ;"Zeit" an?
                JR      NZ,KeyManuell1          ;Nein =>
                BIT     5,A                     ;"Momentan" an?
                JP      NZ,PrintTime            ;Nein! (Zeit an, Momentan aus) => Ausschaltzeit ausgeben
KeyManuell1:    XOR     40h
                LD      (IX+KLED),A             ;Manuell-Flag toggeln
                RET
KeyManuell2:    LD      HL,ManuellZeit
                IF !NewVersion
                CALL    GetDispTime             ;Zeit für "Manuell"-Taste setzen
                LD      A,(IX+Display)
                CP      0Fh                     ;"FEHL"er...
                RET     Z                       ;Ja => raus
                RES     4,(IX+KLED)             ;Zeit-LED an
                LD      B,6
KeyManuell3:    LD      HL,Display+5
                LD      A,(HL)
                CP      1                       ;"11.11.11" eingegeben?
                RET     NZ                      ;Nein =>
                INC     HL
                DJNZ    KeyManuell3
                LD      A,55h
                LD      (LaufschriftFlag),A     ;Laufschrift an
                LD      HL,MsgPause
                LD      (LaufschriftPtr),HL     ;Laufschrift-Text
                RET
                ENDIF

;Uhrzeit nach HL aus dem Display setzen
GetDispTime:    LD      DE,TempTime
                LD      A,(IX+DPunkt)           ;Dezimalpunkte holen
                CP      FBh                     ;hh.mm
                JR      Z,GetDispTime2
                CP      EBh                     ;hh.mm.ss
                JR      Z,GetDispTime1
                LD      A,7
                JP      MakeErrCode             ;Dezimalpunkte an falscher Position
GetDispTime1:   LD      B,2                     ;zwei Dezimalpunkte (noch zwei Zahlen holen)
                LD      C,4                     ;zuerst: Fehler bei den Sekunden
                JR      GetDispTime4
GetDispTime2:   LD      A,0
                OR      (IX+Display+1)          ;zwei Ziffern (ganz links) eingegeben?
                OR      (IX+Display)
                JR      Z,GetDispTime3          ;Nein =>
                LD      A,6
                JP      MakeErrCode             ;mehr als 23h eingegeben
GetDispTime3:   LD      (DE),A                  ;ohne Sekunden: 0 Sekunden setzen
                INC     DE
                LD      B,1                     ;ein Dezimalpunkt (noch eine Zahl holen)
                LD      C,5                     ;zuerst: Fehler bei den Minuten
GetDispTime4:   LD      IY,Display+5            ;Ptr auf die letzte Ziffer im Display
GetDispTime5:   LD      A,(IY-1)                ;Ziffer davor holen
                ADD     A,A
                ADD     A,A                     ;*16
                ADD     A,A
                ADD     A,A
                OR      (IY+0)                  ;und die Ziffer dazu (=> BCD-Zahl)
                CP      5Ah                     ;5A = 50+10 = 60!
                JR      C,GetDispTime6          ;kleiner? => ja
                SET     7,C                     ;Fehler!
GetDispTime6:   LD      (DE),A                  ;Zahl merken
                INC     DE
                DEC     IY                      ;zwei Ziffern nach vorne
                DEC     IY
                INC     C                       ;Fehlernummer hochsetzen (Sekunden => Minuten)
                DJNZ    GetDispTime5            ;alle Dezimalpunkte durch?
                LD      A,(IY-1)
                ADD     A,A
                ADD     A,A
                ADD     A,A                     ;Stunden in BCD wandeln
                ADD     A,A
                OR      (IY+0)
                CP      24h                     ;größer als 24h?
                JR      C,GetDispTime7          ;Nein =>
                SET     7,C                     ;Fehler!
GetDispTime7:   LD      (DE),A                  ;Stunden merken
                BIT     7,C                     ;ein Fehler aufgetreten?
                JR      Z,GetDispTime8          ;Nein =>
                RES     7,C                     ;Flag löschen
                LD      A,C
                JP      MakeErrCode             ;Fehler melden
GetDispTime8:   EX      DE,HL
                DEC     HL
                DEC     HL
                LD      BC,3
                LDIR                            ;Uhrzeit nach HL übertragen
                SET     7,(IX+KLED)             ;"Setzen" aus
                LD      (C000h),A
                RET

; 3 Bytes ab HL als 6 stellige Uhrzeit ausgeben
PrintTime:      LD      DE,Display+5            ;Ptr auf die letzte Stelle vom Display
                LD      B,3
                PUSH    HL
                INC     HL
                INC     HL
                LD      A,'0'
                CP      (HL)                    ;Stunden < '0'?
                POP     HL
                JR      NC,PrintTime1           ;Nein =>
                LD      HL,Str6Minus            ;"------" (Uhrzeit nicht gesetzt)
                JP      SetDisplayString
PrintTime1:     LD      A,(HL)                  ;Byte holen
                AND     0Fh
                LD      (DE),A                  ;unteres Nibble nach (DE)
                DEC     DE
                LD      A,(HL)
                RRCA
                RRCA
                RRCA
                RRCA
                AND     0Fh
                LD      (DE),A                  ;oberes Nibble nach (DE-1)
                DEC     DE
                INC     HL
                DJNZ    PrintTime1
                LD      (IX+DPunkt),EBh         ;Dezimalpunkte nach der 2. und der 4.Ziffer
                LD      (C000h),A
                RET

;Tag-/Nachttemperatur setzen
SetSollTemp:    PUSH    HL
                CALL    GetNumInput
                POP     HL
                LD      A,D                     ;Anzahl der Dezimalpunkte
                CP      0
                JR      Z,SetSollTemp1
                CP      1
                JR      Z,SetSollTemp2
                LD      A,10h
                JP      MakeErrCode             ;Dezimalpunkte bei der Temperatur...
SetSollTemp1:   LD      IY,InputBuf+7
                IF !NewVersion
                LD      A,0
                CP      (IY+0)
                JR      NZ,SetSollTemp2
                CP      (IY-1)                  ;100° C
                JR      NZ,SetSollTemp2
                LD      A,1
                CP      (IY-2)
                JR      NZ,SetSollTemp2
                LD      A,55h
                LD      (LaufschriftFlag),A     ;Laufschrift an
                LD      HL,MsgHeiss
                LD      (LaufschriftPtr),HL     ;Laufschrift-Text
                RET
                ENDIF
SetSollTemp2:   LD      B,E
                LD      A,0
                INC     IY
                PUSH    IY
SetSollTemp3:   OR      (IY-3)
                DEC     IY
                DJNZ    SetSollTemp3
                POP     IY
                CP      0
                JR      Z,SetSollTemp4
                LD      A,08h
                JP      MakeErrCode             ;eingegeben Temp. zu groß
SetSollTemp4:   PUSH    HL
                CALL    ConvertInput
                LD      A,(IY+1)
                CP      5
                JR      C,SetSollTemp5          ;<0.05°? =>
                INC     HL                      ;aufrunden (+ 0.1°)
SetSollTemp5:   LD      DE,100
                XOR     A
                SBC     HL,DE
                JR      C,SetSollTemp8          ;<10.0° =>
                JR      Z,SetSollTemp8          ;=10.0° =>
                LD      A,00h
                CP      H
                JR      NZ,SetSollTemp6         ;>(10° + 25.5°) =>
                LD      A,FFh
                CP      L
                JR      NZ,SetSollTemp7         ;<>(10° + 25.5°) =>
SetSollTemp6:   LD      A,8
                POP     HL
                JP      MakeErrCode             ;eing. Temp zu groß
SetSollTemp7:   LD      A,L
                POP     HL
                INC     HL
                INC     HL
                INC     HL
                LD      (HL),A                  ;Temperatur (10.1°...35.4°) setzen
                CALL    CalcSollChecksum        ;Prüfsumme über die Sollwerte errechnen
                LD      (SollChecksum),DE       ;und merken
                SET     7,(IX+KLED)             ;"Setzen" aus
                LD      L,A
                LD      (C000h),A
                JR      DispTemp
SetSollTemp8:   LD      A,09h
                POP     HL
                JP      MakeErrCode             ;eing. Temp zu tief

;Soll-Tag-/Nachttemperatur ausgeben
PrintSollTemp:  INC     HL
                INC     HL
                INC     HL
                LD      L,(HL)                  ;Soll-Temperatur auslesen

;Temperatur-Wert darstellen
DispTemp:       LD      H,0
                LD      DE,100
                ADD     HL,DE                   ;+10.0°
                PUSH    HL
                POP     IY
                CALL    MakeBCD
                LD      HL,(BCDZahl)
                LD      DE,Display
                LD      B,3                     ;3 Ziffern
DispTemp1:      LD      A,H
                AND     0Fh
                LD      (DE),A
                ADD     HL,HL
                ADD     HL,HL                   ;HL *= 16
                ADD     HL,HL
                ADD     HL,HL
                INC     DE
                DJNZ    DispTemp1
                EX      DE,HL
                LD      (HL),1Fh                ;Space
                INC     HL
                LD      (HL),18h                ;°
                INC     HL
                LD      (HL),0Ch                ;C
                LD      (IX+DPunkt),EFh         ;Punkt in der 2.Ziffer an
                LD      (C000h),A
                RET

;Meßwert C (0...3) vom Hauptgerät empfangen (Wert nach A)
GetMesswert:    LD      HL,E000h
                LD      A,(IX+Steckdosen)       ;Steckdosen-Bits
                AND     5Fh                     ;Bit 0...4 übernehmen, Bit 5&7 löschen
                OR      40h                     ;Bit 6 setzen
                SET     6,(IX+Flags)
                HALT
                LD      (HL),A                  ;567:010 (Bit 7 löschen = Übertragung init)
                CALL    Delay
                LD      E,A
                SET     5,E
                LD      (HL),E                  ;567:110 (Bit 5 toggeln: Übertragung start)
                CALL    Delay

;Meßwert-Nummer (2 Bits) senden
                LD      D,A
                LD      B,2                     ;2 Bits (für 4 Meßwerte) senden
                LD      (HL),D                  ;567:010
                CALL    Delay
                LD      (HL),E                  ;567:110 (1-Bit senden = Startbit)
                CALL    Delay
                LD      (HL),D                  ;567:010
GetMesswert1:   SRL     C
                JR      C,GetMesswert2          ;gesetzt =>
                RES     6,D                     ;Bit 6 löschen, wenn Carry gelöscht
                RES     6,E
                LD      (HL),D                  ;567:000
                CALL    Delay
                LD      (HL),E                  ;567:100 (0-Bit senden)
                CALL    Delay
                LD      (HL),D                  ;567:000
                DJNZ    GetMesswert1            ;alle 2 Bits übertragen? Nein =>
                JR      GetMesswert3
GetMesswert2:   SET     6,D                     ;Bit 6 setzen, wenn Carry gesetzt
                SET     6,E
                LD      (HL),D                  ;567:010
                CALL    Delay
                LD      (HL),E                  ;567:110 (1-Bit senden)
                CALL    Delay
                LD      (HL),D                  ;567:010
                DJNZ    GetMesswert1            ;alle 2 Bits übertragen? Nein =>

GetMesswert3:   LD      B,8                     ;8 Bits empfangen
                LD      (HL),E                  ;567:1?0 (Bit erwarten)
                CALL    Delay
                IN      C,(C)                   ;Bit auslesen
                BIT     5,C                     ;0-Startbit?
                JR      Z,GetMesswert4          ;Ja =>
                SET     7,E
                SET     6,E
                LD      (IX+Steckdosen),E
                LD      (HL),E                  ;567:1?1 (Übertragung beendet)
                SCF
                CCF                             ;Carry = NOT 1 = 0 (Übertragung mit Fehler)
                RET
GetMesswert4:   LD      (HL),D                  ;567:0?0 (Bit empfangen)
                CALL    Delay
                LD      A,0                     ;Bytewert = 0
                LD      (HL),E                  ;567:1?0 (Bit erwarten)
GetMesswert5:   CALL    Delay
                ADD     A,A                     ;Bytewert * 2
                IN      C,(C)
                BIT     5,C                     ;Bit abfragen
                LD      (HL),D                  ;567:0?0 (Bit empfangen)
                CALL    Delay
                JR      Z,GetMesswert6          ;Bit gelöscht =>
                SET     0,A                     ;unterstes Bit setzen
GetMesswert6:   LD      (HL),E                  ;567:1?0 (Bit erwarten)
                CALL    Delay
                DJNZ    GetMesswert5            ;alle 8 Bits empfangen? Nein =>
                SET     7,E
                SET     6,E
                LD      (IX+Steckdosen),E
                LD      (HL),E                  ;567:111 (Übertragung beendet)
                CALL    Delay
                RES     6,(IX+Flags)
                SCF                             ;Carry = 1 (Übertragung ok)
                RET

;ein paar Takte verzögern
Delay:          NOP
                NOP
                NOP
                NOP
                NOP
                NOP
                NOP
                NOP
                NOP
                NOP
                NOP
                NOP
                NOP
                RET

;Potential in L ausgeben
DispRedox:      LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                LD      (IX+Display+5),19h      ;o
                LD      (IX+Display+4),12h      ;P
                LD      (IX+Display+3),1Fh      ;Space
                LD      H,0
                ADD     HL,HL
                PUSH    HL
                POP     IY
                CALL    MakeBCD
                LD      HL,(BCDZahl)
                LD      DE,Display
                LD      B,3
DispRedox1:     LD      A,H
                AND     0Fh
                LD      (DE),A
                ADD     HL,HL
                ADD     HL,HL                   ;HL *= 16
                ADD     HL,HL
                ADD     HL,HL
                INC     DE
                DJNZ    DispRedox1
                LD      (C000h),A
                RET

;Systemzeit (aus dem Display) in dem RTC setzen
SetSystemTime:  LD      HL,AktTime
                CALL    GetDispTime             ;Uhrzeit aus dem Display lesen
                LD      A,(IX+Display)
                CP      0Fh                     ;"F"ehler?
                RET     Z                       ;Ja => raus
                LD      HL,AktTime
                CALL    PrintTime               ;Uhrzeit formatiert ausgeben
                LD      HL,Display
                SET     3,(HL)                  ;Bit 3 in der 1.Stundenziffer setzen (24h Format)
                LD      B,6                     ;6 Register setzen
                LD      C,5
                LD      IY,4000h                ;Adresse vom RTC
                HALT
SetSystemTime1: LD      (IY+1),C                ;Register auswählen
                LD      A,(HL)                  ;Ziffer auslesen
                LD      (IY+2),A                ;und ins RTC-Register schreiben
                INC     HL
                DEC     C                       ;Register - 1
                DJNZ    SetSystemTime1
                RES     5,(IX+KLED)             ;"Momentan"-LED ausschalten
                LD      (C000h),A
                RET

;Universaltimer-Verwaltung
UniTimer:       LD      B,10                    ;maximal 10 Zeiten
UniTimer1:      PUSH    HL
                INC     HL
                INC     HL
                BIT     7,(HL)                  ;Ende der Liste? (Einschaltzeit)
                POP     HL
                JR      Z,UniTimer2             ;Nein =>
                SCF                             ;Ja, Carry setzen und raus
                RET
UniTimer2:      PUSH    DE
                INC     DE
                INC     DE
                LD      A,(DE)                  ;Ende der Liste? (Ausschaltzeit)
                POP     DE
                BIT     7,A
                JR      Z,UniTimer3             ;Nein =>
                SCF                             ;Ja, Carry setzen und raus
                RET
UniTimer3:      PUSH    BC
                PUSH    DE
                PUSH    HL
                CALL    InTimeRange             ;im Einschaltbereich?
                POP     HL
                POP     DE
                POP     BC
                RET     NC                      ;Ja! => raus
                INC     HL
                INC     HL                      ;nächste Einschaltzeit
                INC     HL
                INC     DE
                INC     DE                      ;nächste Ausschaltzeit
                INC     DE
                DJNZ    UniTimer1               ;alle Zeiten durch?
                LD      (C000h),A
                RET

;HL: Einschaltzeit
;DE: Ausschaltzeit
;Carry = 0, wenn im Zeitraum
InTimeRange:    PUSH    DE
                PUSH    HL
                CALL    CompareTimes            ;sind die beiden Zeiten gleich?
                POP     HL
                POP     DE
                RET     Z                       ;Ja => raus
                JR      C,InTimeRange1          ;Ausschaltzeit < Einschaltzeit? => Zeiten und Logik drehen
                PUSH    DE
                LD      DE,AktTime
                CALL    CompareTimes            ;HL mit der aktuellen Uhrzeit vergleichen
                POP     DE
                RET     Z                       ;Einschaltzeit = aktuelle Zeit? => raus
                RET     C                       ;Einschaltzeit > aktuelle Zeit? => raus
                LD      HL,AktTime
                CALL    CompareTimes
                RET     C                       ;Ausschaltzeit > aktuelle Zeit? => raus
                RET     NZ                      ;Ausschaltzeit <> aktuelle Zeit? => raus
                SCF                             ;Carry = 1 (außerhalb)
                RET
InTimeRange1:   EX      DE,HL
                PUSH    DE
                LD      DE,AktTime
                CALL    CompareTimes
                POP     DE
                CCF                             ;Carry = NOT Carry
                RET     NC
                JR      NZ,InTimeRange2
                SCF                             ;Carry = 1 (außerhalb)
                RET
InTimeRange2:   LD      HL,AktTime
                CALL    CompareTimes
                RET     Z
                CCF                             ;Carry = NOT Carry
                RET

;Zeit DE und HL vergleichen, Z = 1, wenn gleich
CompareTimes:   LD      BC,0300h                ;3 Bytes (Sekunden,Minuten,Stunden) vergleichen
                XOR     A
CompareTimes1:  LD      A,(DE)
                SBC     A,(HL)
                JR      Z,CompareTimes2
                SET     0,C                     ;Flag setzen, wenn ungleich!
CompareTimes2:  INC     HL
                INC     DE
                DJNZ    CompareTimes1
                LD      (C000h),A
                BIT     0,C                     ;Z = 1, wenn gleich
                RET

;Leitwert-Wert darstellen
DispLeitw:      LD      H,0
                BIT     5,(IX+KLED)             ;"Momentan" an?
                JR      NZ,DispLeitw4           ;Nein =>
                LD      A,(IX+IstTemp)          ;Ist-Temp-Wert
                CP      FFh
                JR      Z,DispLeitw6            ;Temperatur außerhalb des Meßbereiches?
                CP      00h
                JR      Z,DispLeitw6            ;Ja =>
                LD      A,(IX+LeitwertKomp)     ;kompensierter Leitwert
                LD      L,A
                CP      FFh                     ;ungültig?
                JR      NZ,DispLeitw2           ;Nein =>
                BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JR      Z,DispLeitw1
                LD      HL,Str6Space            ;"      "
                JP      SetDisplayString
DispLeitw1:     LD      HL,StrFEHL16            ;"FEHL16"
                JP      SetDisplayString
DispLeitw2:     CP      00h                     ;Bereich unterschritten?
                JR      NZ,DispLeitw4           ;Nein =>
                BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JR      Z,DispLeitw3
                LD      HL,Str6Space            ;"      "
                JP      SetDisplayString
DispLeitw3:     LD      HL,StrFEHL15            ;"FEHL15"
                JP      SetDisplayString

DispLeitw4:     IN      C,(C)
                BIT     5,C                     ;Süßwasser/Meerwasser-Schalter abfragen
                JR      Z,DispLeitw5            ;Meerwasser =>
                LD      DE,1505h                ;"µS"
                LD      (IX+Display+3),00h      ;"0"
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                JR      DispLeitw9
DispLeitw5:     LD      DE,350                  ;35.0mS Vorgabe (Minumum bei Meerwasser-Leitwert)
                ADD     HL,DE
                LD      DE,1705h                ;"nS"
                LD      (IX+Display+3),1Fh      ;Space
                LD      (IX+DPunkt),EFh
                JR      DispLeitw9

DispLeitw6:     LD      L,(IX+IstLeitw)         ;Ist-Leitwert (nicht kompensiert)
                IN      C,(C)
                BIT     5,C                     ;Süßwasser/Meerwasser-Schalter abfragen
                JR      Z,DispLeitw7            ;Meerwasser =>
                LD      DE,1505h                ;"µS"
                LD      (IX+Display+3),00h      ;"0"
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                JR      DispLeitw8
DispLeitw7:     LD      DE,350                  ;35.0mS Vorgabe (Minumum bei Meerwasser-Leitwert)
                ADD     HL,DE
                LD      DE,1705h                ;"nS"
                LD      (IX+Display+3),1Fh      ;Space
                LD      (IX+DPunkt),EFh         ;Punkt nach der 2.Ziffer
DispLeitw8:     BIT     0,(IX+Counter+1)        ;Blink-Timer gesetzt?
                JR      Z,DispLeitw9
                LD      DE,1F1Fh                ;"  "

DispLeitw9:     LD      (IX+Display+5),E        ;Einheit setzen
                LD      (IX+Display+4),D
                PUSH    HL
                POP     IY
                CALL    MakeBCD                 ;Leitwert nach BCD wandeln
                LD      HL,(BCDZahl)            ;BCD-Zahl holen
                LD      DE,Display
                LD      B,3                     ;3 Ziffern ins Display
DispLeitw10:    LD      A,H
                AND     0Fh
                LD      (DE),A                  ;Ziffer ins Display
                ADD     HL,HL
                ADD     HL,HL                   ;HL * 16
                ADD     HL,HL
                ADD     HL,HL
                INC     DE                      ;eine Stelle weiter
                DJNZ    DispLeitw10             ;alle drei Ziffern durch? Nein =>
                LD      (C000h),A
                RET

;Fehlermeldung A für das Display zusammensetzen
MakeErrCode:    LD      HL,Display
                LD      (HL),0Fh                ;"F"
                INC     HL
                LD      (HL),0Eh                ;"E"
                INC     HL
                LD      (HL),10h                ;"H"
                INC     HL
                LD      (HL),11h                ;"L"
                LD      (IX+DPunkt),FBh         ;FBh (Bit 2 gelöscht): Punkt in der 4.Stelle setzen?!?
                LD      B,A
                AND     0Fh
                LD      (IX+Display+5),A        ;Fehlercode (untere Ziffer)
                LD      A,B
                RRCA
                RRCA
                RRCA
                RRCA
                AND     0Fh
                JR      NZ,MakeErrCode1         ;zweistellige Ziffer? Ja =>
                LD      A,1Fh                   ;Nein = Feld frei
MakeErrCode1:   LD      (IX+Display+4),A        ;erste Ziffer
                SET     4,(IX+Flags)            ;keine Zahl im Display
                LD      (C000h),A
                RET

;Eingabe (drei Ziffern) in eine binäre Zahl in HL wandeln
ConvertInput:   LD      HL,0                    ;Zahlenwert
                LD      A,(IY-2)                ;1.Ziffer holen
                CP      0                       ;= "0"?
                JR      Z,ConvertInput2         ;Ja! =>
                LD      B,A
                LD      DE,100
ConvertInput1:  ADD     HL,DE                   ;100 * Wert der 1.Ziffer addieren
                DJNZ    ConvertInput1
ConvertInput2:  LD      A,(IY-1)                ;2.Ziffer holen
                CP      0                       ;= "0"?
                JR      Z,ConvertInput4         ;Ja! =>
                LD      B,A
                LD      DE,10
ConvertInput3:  ADD     HL,DE                   ;10 * Wert der 2.Ziffer addieren
                DJNZ    ConvertInput3
ConvertInput4:  LD      E,(IY+0)                ;3.Ziffer
                LD      D,0
                ADD     HL,DE                   ;zum Wert addieren
                LD      (C000h),A
                RET

;binäre Zahl in IY nach BCDZahl in gepacktem BCD wandeln
MakeBCD:        ADD     IY,IY                   ;Zahl * 4
                ADD     IY,IY
                LD      B,14                    ;14 Bits (2 Bits sind durch * 4 weg)
                LD      HL,BinDezTableEnd-1     ;Multiplikationstabelle
                LD      DE,BCDZahl
                LD      (IX+BCDZahl),0
                LD      (IX+BCDZahl+1),0
MakeBCD1:       ADD     IY,IY                   ;Zahl * 2
                JR      C,MakeBCD2              ;Überlauf? => Ja!
                DEC     HL                      ;eine Stelle in der Mult-Tabelle zurück
                DEC     HL
                DJNZ    MakeBCD1                ;alle Stellen durch? Nein =>
                RET
MakeBCD2:       XOR     A                       ;A = 0
                LD      A,(DE)
                ADC     A,(HL)                  ;untere Bytes addieren
                DAA                             ;in gepacktes BCD wandeln
                LD      (DE),A                  ;und zurückschreiben
                DEC     HL
                INC     DE
                LD      A,(DE)                  ;obere Bytes addieren
                ADC     A,(HL)
                DAA                             ;in gepacktes BCD wandeln
                LD      (DE),A                  ;und zurückschreiben
                DEC     HL
                DEC     DE
                LD      (C000h),A
                DJNZ    MakeBCD1                ;zur nächsten Stelle
                RET

KeyAus2:        BIT     4,(IX+KLED+1)           ;"Kanal 1"
                JP      Z,KeyAus15              ;Ja =>
                BIT     5,(IX+KLED+1)           ;"Kanal 2"
                JP      Z,KeyAus16              ;Ja =>
                BIT     6,(IX+KLED+1)           ;"Licht"
                JR      NZ,KeyAus4              ;Nein =>
                BIT     2,(IX+KLED)             ;"Ein"
                JR      NZ,KeyAus3              ;Nein =>
                BIT     7,(IX+KLED)             ;"Setzen" an?
                LD      HL,LichtEin
                JP      Z,GetDispTime           ;Ja! =>
                JP      PrintTime
KeyAus3:        BIT     7,(IX+KLED)             ;"Setzen" an?
                LD      HL,LichtAus
                JP      Z,GetDispTime           ;Ja! =>
                JP      PrintTime
KeyAus4:        BIT     7,(IX+KLED+1)           ;"CO2"
                JR      NZ,KeyAus6
                BIT     2,(IX+KLED)             ;"Ein"
                JR      NZ,KeyAus5
                BIT     7,(IX+KLED)             ;"Setzen" an?
                LD      HL,CO2Ein
                JP      Z,GetDispTime           ;Ja! =>
                JP      PrintTime
KeyAus5:        BIT     7,(IX+KLED)             ;"Setzen" an?
                LD      HL,CO2Aus
                JP      Z,GetDispTime           ;Ja! =>
                JP      PrintTime
KeyAus6:        BIT     4,(IX+KLED)             ;"Zeit"
                JR      Z,KeyAus7               ;Ja =>
                LD      A,(IX+KLED)
                OR      0Ch                     ;Ein und Aus LEDs ausschalten
                LD      (IX+KLED),A
                RET
KeyAus7:        BIT     7,(IX+KLED)             ;"Setzen" an?
                JR      Z,KeyAus11              ;Ja! =>
                LD      HL,Display
                LD      B,6
KeyAus8:        LD      (HL),0                  ;Display löschen
                INC     HL
                DJNZ    KeyAus8
                LD      (IX+DPunkt),FEh         ;Dezimalpunkt in der 6.Ziffer an
                LD      A,(IX+AktSchaltzeit)    ;aktuelle Schaltzeit
                AND     0Fh
                INC     A
                CP      10
                JR      Z,KeyAus9
                LD      (IX+Display+5),A        ;1...9
                JR      KeyAus10
KeyAus9:        LD      (IX+Display+4),1        ;'10'
KeyAus10:       LD      (C000h),A
                RET
KeyAus11:       LD      A,0
                LD      B,4
                LD      HL,Display              ;die ersten 4 Ziffern müssen stets = 0 sein
KeyAus12:       OR      (HL)
                INC     HL
                DJNZ    KeyAus12
                JR      NZ,KeyAus13             ;wenn nicht => Fehler
                LD      IY,Display+5            ;Ptr auf die letzte Stelle vom Display
                CALL    ConvertInput            ;Zahl nach HL holen
                LD      A,L
                CP      0
                JR      Z,KeyAus13              ;Schaltzeiten zwischen 1 und 10
                CP      11
                JR      NC,KeyAus13
                DEC     A
                LD      (IX+AktSchaltzeit),A    ;aktuelle Schaltzeit merken
                SET     7,(IX+KLED)             ;"Setzen" aus
                LD      (C000h),A
                RET
KeyAus13:       LD      A,19h
                JP      MakeErrCode             ;ill. Nummer für die Schaltzeiten

                IF      !NewVersion
KeyAus14:       LD      A,20h
                JP      MakeErrCode             ;???
                ENDIF

KeyAus15:       LD      HL,Kanal1Uni            ;Kanal 1 Schaltzeiten
                JR      KeyAus17

KeyAus16:       LD      HL,Kanal2Uni            ;Kanal 2 Schaltzeiten
KeyAus17:       BIT     2,(IX+KLED)             ;"Ein" gedrückt?
                JR      Z,KeyAus18              ;Ja! =>
                LD      DE,30
                ADD     HL,DE                   ;Ausschaltzeiten
KeyAus18:       LD      DE,3
                LD      A,(IX+AktSchaltzeit)    ;aktuelle Schaltzeit holen
                CP      0
                JR      Z,KeyAus21              ;Schaltzeit gültig?
                CP      10                      ;Ja =>
                JR      C,KeyAus19
                LD      A,0
                LD      (IX+AktSchaltzeit),A    ;Schaltzeit löschen
                JR      KeyAus21
KeyAus19:       LD      B,A
KeyAus20:       ADD     HL,DE                   ;je 3 Bytes pro Schaltzeit
                DJNZ    KeyAus20
KeyAus21:       BIT     7,(IX+KLED)             ;"Setzen" an?
                JP      NZ,PrintTime            ;Nein! =>
                LD      A,(IX+Display)
                OR      (IX+Display+1)
                OR      (IX+Display+2)
                OR      (IX+Display+3)          ;0 als Uhrzeit eingegeben?
                OR      (IX+Display+4)
                OR      (IX+Display+5)
                JR      Z,KeyAus22              ;Ja =>
                JP      GetDispTime             ;Schaltzeit setzen
KeyAus22:       LD      A,(IX+DPunkt)           ;Dezimalpunkte?
                CP      FFh
                JP      NZ,GetDispTime          ;Ja =>
                INC     HL
                INC     HL
                SET     7,(HL)                  ;Alarmzeit ungültig machen
                LD      DE,30
                BIT     2,(IX+KLED)             ;"Ein"?
                JR      Z,KeyAus23              ;Ja =>
                XOR     A
                SBC     HL,DE                   ;Ptr auf Einschaltzeit
                JR      KeyAus24
KeyAus23:       ADD     HL,DE                   ;Ptr auf Ausschaltzeit
KeyAus24:       SET     7,(HL)                  ;entsprechende Zeit ebenfalls ausschalten
                SET     7,(IX+KLED)             ;"Setzen" aus
                LD      HL,Str6Minus            ;"------"
                JP      SetDisplayString

;"Redox"-Taste gedrückt
KeyRedox:       RES     0,(IX+Flags)            ;Zahleingabe aus
                BIT     5,(IX+KLED)             ;"Momentan"?
                JR      NZ,KeyRedox1            ;Nein =>
                LD      A,(IX+KLED)
                OR      9Fh                     ;Mometan und Manuell an lassen (Rest aus)
                LD      (IX+KLED),A
                LD      A,(IX+KLED+1)
                OR      F7h                     ;LEDs bis auf Redox ausschalten
                LD      (IX+KLED+1),A
                RES     3,(IX+KLED+1)           ;Redox anschalten
                RET
KeyRedox1:      BIT     7,(IX+KLED)             ;"Setzen" an?
                JR      Z,KeyRedox2             ;Ja! =>
                LD      A,(IX+KLED)
                OR      BFh                     ;bis auf Manuell alles ausschalten
                LD      (IX+KLED),A
                LD      A,(IX+KLED+1)
                OR      F7h                     ;bis auf Redox alles ausschalten
                LD      (IX+KLED+1),A
                RES     3,(IX+KLED+1)           ;Redox anschalten
                LD      L,(IX+SollRedox)        ;Soll-Redoxwert
                JP      DispRedox               ;Potential anzeigen
KeyRedox2:      RES     3,(IX+KLED+1)           ;Redox anschalten
                BIT     4,(IX+KLED+1)           ;"Kanal 1" an
                JR      NZ,KeyRedox3            ;Nein =>
                LD      (IX+Uni1Flag),AAh       ;Kanal 1 auf Redox-Regelung schalten
                SET     7,(IX+KLED)             ;"Setzen" aus
                LD      HL,StrrErE              ;" rE-rE"
                JP      SetDisplayString
KeyRedox3:      CALL    GetNumInput             ;Eingabe holen
                LD      A,D
                CP      0
                JR      Z,KeyRedox4             ;0 oder 1 Dezimalpunkt in der Eingabe
                CP      1
                JR      Z,KeyRedox5
                LD      A,23h
                JP      MakeErrCode             ;mehrere Dezimalpunkte bei Redox-Eingabe
KeyRedox4:      LD      IY,InputBuf+7
                LD      E,5
KeyRedox5:      LD      B,E
                DEC     B
                JR      Z,KeyRedox7
                LD      B,1
                LD      A,0
                PUSH    IY
KeyRedox6:      OR      (IY-3)                  ;Ziffern vor der erwarteten 1.Ziffer zusammen'OR'n
                DEC     IY
                DJNZ    KeyRedox6
                POP     IY
                CP      0
                JR      Z,KeyRedox7
                LD      A,21h
                JP      MakeErrCode             ;zu großer Redox-Wert eingegeben
KeyRedox7:      CALL    ConvertInput
                BIT     0,L                     ;Redox-Wert gerade?
                JR      Z,KeyRedox8             ;Ja =>
                INC     HL                      ;ansonsten aufrunden
KeyRedox8:      LD      A,L
                OR      H                       ;Redox-Wert = 0?
                JR      NZ,KeyRedox9            ;Nein =>
                LD      A,22h
                JP      MakeErrCode             ;0 Volt Redox-Wert eingegeben
KeyRedox9:      LD      DE,509                  ;509mV = maximaler Redox-Wert
                XOR     A
                PUSH    HL
                SBC     HL,DE
                POP     HL
                JR      C,KeyRedox10
                LD      A,21h
                JP      MakeErrCode             ;Redox-Wert zu groß!
KeyRedox10:     RR      H                       ;Redox-Wert / 2
                RR      L
                LD      (IX+SollRedox),L        ;Soll-Redoxwert setzen
                CALL    CalcSollChecksum        ;Prüfsumme über die Sollwerte errechnen
                LD      (SollChecksum),DE       ;und merken
                SET     7,(IX+KLED)             ;"Setzen" aus
                JP      DispRedox               ;Potential anzeigen

;"Leitwert"-Taste gedrückt
KeyLeitwert:    RES     0,(IX+Flags)            ;Zahleingabe aus
                BIT     5,(IX+KLED)             ;"Momentan"?
                JR      NZ,KeyLeitwert1         ;Nein =>
                LD      A,(IX+KLED)
                OR      9Fh                     ;Mometan und Manuell an lassen (Rest aus)
                LD      (IX+KLED),A
                LD      A,(IX+KLED+1)
                OR      FBh                     ;LEDs bis auf Leitwert ausschalten
                LD      (IX+KLED+1),A
                RES     2,(IX+KLED+1)           ;Leitwert anschalten
                LD      (C000h),A
                RET
KeyLeitwert1:   BIT     7,(IX+KLED)             ;"Setzen" an?
                JR      Z,KeyLeitwert3          ;Ja! =>
                LD      A,(IX+KLED)
                OR      3Fh                     ;bis auf Manuell und Setzen alles ausschalten
                LD      (IX+KLED),A
                LD      A,(IX+KLED+1)
                OR      EFh                     ;bis auf Kanal 1 alles ausschalten
                LD      (IX+KLED+1),A
                RES     2,(IX+KLED+1)           ;Leitwert anschalten
                IN      C,(C)
                BIT     5,C                     ;Süßwasser/Meerwasser-Schalter abfragen
                JR      Z,KeyLeitwert2          ;Meerwasser =>
                LD      L,(IX+SollLeitwertS)    ;Soll-Leitwert (Süßwasser)
                JP      DispLeitw
KeyLeitwert2:   LD      L,(IX+SollLeitwertM)    ;Soll-Leitwert (Meerwasser)
                JP      DispLeitw
KeyLeitwert3:   BIT     5,(IX+KLED+1)           ;"Kanal 2"?
                JR      NZ,KeyLeitwert4         ;Nein =>
                RES     2,(IX+KLED+1)           ;Leitwert an
                LD      (C000h),A
                RET
KeyLeitwert4:   RES     2,(IX+KLED+1)           ;Leitwert an
                LD      A,(IX+KLED)
                OR      3Fh                     ;bis auf Manuell und Setzen alles ausschalten
                LD      (IX+KLED),A
                LD      A,(IX+KLED+1)
                OR      FBh                     ;bis auf Leitwert alles ausschalten
                LD      (IX+KLED+1),A
                IN      C,(C)
                BIT     5,C                     ;Süßwasser/Meerwasser-Schalter abfragen
                JR      Z,KeyLeitwert15         ;Meerwasser =>
                LD      A,(IX+DPunkt)           ;Dezimalpunkt
                CP      DFh                     ;an 2.Stelle?
                JR      NZ,KeyLeitwert5         ;Nein =>
                LD      A,(IX+Display)
                CP      5                       ;5?
                JR      C,KeyLeitwert13
                LD      L,1
                JR      KeyLeitwert14
KeyLeitwert5:   CALL    GetNumInput
                DEC     IY
                LD      A,D
                CP      0
                JR      Z,KeyLeitwert6          ;0 oder 1 Dezimalpunkt?
                CP      1                       ;Nein =>
                JR      Z,KeyLeitwert7
                LD      A,26h
                JP      MakeErrCode             ;zu viele Dezimalpunkte beim Leitwert
KeyLeitwert6:   LD      IY,InputBuf+6
                LD      B,2
                JR      KeyLeitwert8
KeyLeitwert7:   LD      A,E
                SUB     4
                JR      Z,KeyLeitwert10
                JR      C,KeyLeitwert10
                LD      B,A
KeyLeitwert8:   LD      A,0
                PUSH    IY
KeyLeitwert9:   OR      (IY-3)                  ;Ziffern vor der erwarteten Eingabe zusammen 'OR'n
                DEC     IY
                DJNZ    KeyLeitwert9
                POP     IY
                CP      0
                JR      Z,KeyLeitwert10
                LD      A,24h
                JP      MakeErrCode             ;Leitwert zu groß!
KeyLeitwert10:  CALL    ConvertInput
                LD      A,(IY+1)                ;4.Ziffer
                CP      5                       ;>= 5?
                JR      C,KeyLeitwert11         ;Nein =>
                INC     HL                      ;aufrunden
KeyLeitwert11:  LD      DE,255
                PUSH    HL
                XOR     A
                SBC     HL,DE                   ;Leitwert >= 255? (2550 µS)
                POP     HL
                JR      C,KeyLeitwert12         ;Nein =>
                LD      A,24h
                JP      MakeErrCode             ;Leitwert zu groß!
KeyLeitwert12:  LD      A,L
                OR      H                       ;Leitwert = 0?
                JR      NZ,KeyLeitwert14        ;Nein =>
KeyLeitwert13:  LD      A,25h
                JP      MakeErrCode             ;Leitwert zu klein
KeyLeitwert14:  LD      (IX+SollLeitwertS),L    ;Soll-Leitwert (Süßwasser) setzen
                CALL    CalcSollChecksum        ;Prüfsumme über die Sollwerte errechnen
                LD      (SollChecksum),DE       ;und merken
                SET     7,(IX+KLED)             ;"Setzen" aus
                JP      DispLeitw

;Leitwert für Meerwasser:
KeyLeitwert15:  CALL    GetNumInput
                LD      A,D
                CP      0
                JR      Z,KeyLeitwert16
                CP      1
                JR      Z,KeyLeitwert17
                LD      A,26h
                JP      MakeErrCode             ;Dezimalpunktfehler beim Leitwert
KeyLeitwert16:  LD      IY,InputBuf+7
KeyLeitwert17:  LD      B,E
                LD      A,0
                INC     IY
                PUSH    IY
KeyLeitwert18:  OR      (IY-3)
                DEC     IY
                DJNZ    KeyLeitwert18
                POP     IY
                CP      0
                JR      Z,KeyLeitwert19
                LD      A,24h
                JP      MakeErrCode             ;Leitwert zu groß
KeyLeitwert19:  CALL    ConvertInput
                LD      A,(IY+1)                ;3.Ziffer
                CP      5                       ;>= 5?
                JR      C,KeyLeitwert20         ;Nein =>
                INC     HL                      ;aufrunden
KeyLeitwert20:  LD      DE,353
                XOR     A
                SBC     HL,DE                   ;35.3mS abziehen
                JR      NC,KeyLeitwert21        ;Unterlauf? Nein =>
                LD      A,25h
                JP      MakeErrCode             ;Leitwert zu klein
KeyLeitwert21:  ADC     HL,DE                   ;wieder dazuaddieren
                LD      DE,601
                XOR     A
                SBC     HL,DE                   ;60.1mS abziehen
                JR      C,KeyLeitwert22         ;Überlauf? Nein =>
                LD      A,24h
                JP      MakeErrCode             ;Leitwert zu groß
KeyLeitwert22:  ADC     HL,DE
                LD      DE,350                  ;35.0mS abziehen
                SBC     HL,DE
                LD      (IX+SollLeitwertM),L    ;Soll-Leitwert (Meerwasser)
                CALL    CalcSollChecksum        ;Prüfsumme über die Sollwerte errechnen
                LD      (SollChecksum),DE       ;und merken
                SET     7,(IX+KLED)             ;"Setzen" aus
                JP      DispLeitw

;String ab HL ins Display übertragen
SetDisplayString:
                LD      BC,6
                LD      DE,Display
                LDIR                            ;String ins Display
                INC     DE
                INC     DE
                INC     DE
                LDI                             ;Dezimalpunkte übertragen
                SET     4,(IX+Flags)            ;keine Zahl im Display
                RET

;Kanal 1-Regelung
Kanal1Regel:
                LD      A,(IX+Uni1Flag)
                CP      55h                     ;Universal-Timer?
                JP      Z,UniTimer              ;Ja =>
                CP      AAh                     ;Redox-Regelung?
                JR      Z,Kanal1Regel1          ;Ja =>
                SCF
                RET
Kanal1Regel1:   CALL    UniTimer
                RET     C                       ;nichts gefunden =>
                LD      A,(IX+SollRedox)        ;Soll-Redoxwert
                BIT     3,(IX+WarnLED)          ;Kanal 1-LED an?
                JR      NZ,Kanal1Regel2         ;Nein =>
                ADD     A,1                     ;Soll-Wert um 0.5µV erhöhen, wenn Regelung bereits an
Kanal1Regel2:   CP      (IX+IstRedox)           ;mit Sollwert vergleichen
                RET

;Kanal 2-Regelung
Kanal2Regel:
                LD      A,(IX+Uni2Flag)
                CP      55h                     ;Universal-Timer?
                JP      Z,UniTimer              ;Ja =>
                CP      AAh                     ;Leitwert-Regelung?
                JR      Z,Kanal2Regel1          ;Ja =>
                SCF
                RET
Kanal2Regel1:   CALL    UniTimer
                RET     C                       ;nichts gefunden =>
                LD      A,(IX+IstTemp)          ;Ist-Temp-Wert
                CP      FFh
                JR      Z,Kanal2Regel2          ;außerhalb des Meßbereiches?
                CP      0
                JR      Z,Kanal2Regel2          ;Ja =>
                LD      D,(IX+LeitwertKomp)     ;kompensierter Leitwert
                JR      Kanal2Regel3
Kanal2Regel2:   LD      D,(IX+IstLeitw)         ;Ist-Leitwert
Kanal2Regel3:   IN      C,(C)
                BIT     5,C                     ;Süßwasser/Meerwasser-Schalter abfragen
                JR      Z,Kanal2Regel4          ;Meerwasser =>
                LD      B,(IX+SollLeitwertS)    ;Soll-Leitwert (Süßwasser)
                JR      Kanal2Regel5
Kanal2Regel4:   LD      B,(IX+SollLeitwertM)    ;Soll-Leitwert (Meerwasser)
Kanal2Regel5:   LD      A,(IX+Uni2Flag2)
                CP      AAh                     ;Aus-Regelung
                JR      Z,Kanal2Regel8
                CP      55h                     ;Ein-Regelung
                JR      Z,Kanal2Regel6
                SCF                             ;Regelung illegal => raus
                RET
Kanal2Regel6:   LD      A,B
                BIT     0,(IX+WarnLED)          ;Kanal 2-LED an?
                JR      Z,Kanal2Regel7          ;Ja =>
                ADD     A,2                     ;Soll-Wert um 2 erhöhen, wenn Regelung bereits an
Kanal2Regel7:   CP      D                       ;mit Sollwert vergleichen
                CCF
                RET
Kanal2Regel8:   LD      A,B
                BIT     0,(IX+WarnLED)          ;Kanal 2-LED an?
                JR      NZ,Kanal2Regel9         ;Nein =>
                ADD     A,2                     ;Soll-Wert um 2 erhöhen, wenn Regelung bereits an
Kanal2Regel9:   CP      D                       ;mit Sollwert vergleichen
                RET

;Temperatur-Kompensation des Leitwertes errechnen (er weicht etwa 2% pro Grad Temperatur-Änderung
;von 25° vom Sollwert ab)
TempKomp:       LD      HL,65
                LD      (Mult24),HL
                LD      C,(IX+IstTemp)          ;Ist-Temp-Wert (= (Temperatur-10.0°)*10)
                CALL    Mult24Bit
                LD      HL,42518
                LD      DE,(Mult24Erg)          ;DE = 42518 - Ist-Temp * 65 (Ist-Temp = 25°: DE = 8000h = 1)
                XOR     A
                SBC     HL,DE                   ;DE: Bit 15 = 1, Bit 14...0 = Nachkommastellen
                LD      (Mult24),HL             ;als neuen Multiplikator merken
                PUSH    HL
                LD      C,(IX+IstLeitw)         ;Ist-Leitwert als Multiplikant
                CALL    TempKomp3               ;= 2 * Kompensations-Wert (= ganzer Anteil im oberen Byte!)
                POP     HL                      ;Leitwert = Mess-Leitwert * (1 - 2% * (Temp - 25°))
                LD      A,(IX+LeitwertKomp)     ;Kompensations-Wert holen
                CP      FFh
                RET     Z                       ;ungültig => raus
                CP      0
                RET     Z
                IN      C,(C)
                BIT     5,C                     ;Süßwasser/Meerwasser-Schalter abfragen
                RET     NZ                      ;Süßwasser => raus
                LD      C,175
                LD      (Mult24),HL
                CALL    Mult24Bit               ;(Multiplikator * 175) * 2
                LD      HL,(Mult24Erg+1)
                ADD     HL,HL
                LD      A,H
                SUB     175
                ADD     A,A                     ;((Erg/256) - 175) * 2
                JP      P,TempKomp1             ;Positiv =>
                CPL                             ;negieren
                LD      B,A
                LD      A,(IX+LeitwertKomp)     ;Temp.Kompensation holen
                SUB     B
                JR      NC,TempKomp2            ;Wert groß genug? Ja =>
                LD      A,0                     ;Unterlauf der Kompensation!
                JR      TempKomp2
TempKomp1:      ADD     A,(IX+LeitwertKomp)     ;jetzige Temp.Kompensation dazu
                JR      NC,TempKomp2            ;Überlauf? Nein =>
                LD      A,FFh
TempKomp2:      LD      (IX+LeitwertKomp),A     ;Temp.Kompensation setzen
                RET

TempKomp3:      CALL    Mult24Bit
                LD      HL,(Mult24Erg+1);16-Bit Kompensation holen
                BIT     7,H                     ;Bit 15 gesetzt (Wert zu groß)
                JR      Z,TempKomp5             ;Nein =>
TempKomp4:      LD      (IX+LeitwertKomp),FFh   ;Temp.Kompensation ungültig!
                RET
TempKomp5:      ADD     HL,HL                   ;Wert * 2
                LD      A,H                     ;oberes Byte nehmen
                CP      FFh                     ;ungültig?
                JR      Z,TempKomp4             ;Ja =>
                BIT     7,L                     ;Bit 7 gesetzt?
                JR      Z,TempKomp6             ;Nein =>
                INC     A                       ;aufrunden (auf 8 Bit)
                CP      FFh                     ;ungültig?
                JR      Z,TempKomp4             ;Ja =>
TempKomp6:      LD      (IX+LeitwertKomp),A     ;Temp.Kompensation setzen
                RET

;24 Bit Multiplikation (Mult24...Mult24+2) * C = (Mult24Erg...Mult24Erg+2)
Mult24Bit:      LD      (C000h),A
                LD      B,4
                LD      HL,Mult24Erg+2
Mult24Bit1:     LD      (HL),0                  ;Ergebniss und Buffer löschen
                DEC     HL
                DJNZ    Mult24Bit1
                LD      B,8                     ;8 Bits
Mult24Bit2:     LD      HL,Mult24               ;Multiplikator
                LD      DE,Mult24Erg            ;Ergebnis
                RRC     C                       ;Temperatur nach rechts ins Carry schieben
                JR      NC,Mult24Bit3

                LD      A,(DE)
                ADD     A,(HL)
                LD      (DE),A
                INC     HL                      ;(DE) = (DE)+(HL)   (16 Bit Addition mit 24 Bit Ergebnis)
                INC     DE
                LD      A,(DE)
                ADC     A,(HL)
                LD      (DE),A
                INC     HL
                INC     DE
                LD      A,(DE)
                ADC     A,(HL)
                LD      (DE),A

                LD      HL,Mult24
Mult24Bit3:     SLA     (HL)
                INC     HL
                RL      (HL)                    ;Summand * 2 (24 Bit)
                INC     HL
                RL      (HL)
                DJNZ    Mult24Bit2              ;8 mal durchlaufen (8 Bit Multiplikant)
                LD      (C000h),A
                RET

;Eingabe aus dem Display holen und Dezimalpunkte auswerten
;D-Register = Anzahl der Punkte
;IY zeigt auf die Nachkommastellen
GetNumInput:    LD      A,0
                LD      DE,InputBuf
                LD      (DE),A                  ;Byte 1 und 2 im Buffer löschen
                INC     DE
                LD      (DE),A
                INC     DE
                LD      HL,Display
                LD      BC,6
                LDIR                            ;Anzeige in den Buffer (Byte 3...8) übertragen
                LD      (DE),A                  ;Byte 9 und 10 im Buffer löschen
                INC     DE
                LD      (DE),A
                LD      IY,InputBuf+7           ;Ptr auf die letzte Ziffer
                LD      C,(IX+DPunkt)           ;Dezimalpunkte holen
                LD      D,0                     ;Anzahl der Punkte = 0
                LD      B,6                     ;maximal 6 Punkte auswerten
GetNumInput1:   LD      E,B                     ;Position des _letzten_ Punktes (= 6.Stelle)
                BIT     0,C                     ;Punkt gesetzt?
                JR      Z,GetNumInput2          ;Ja (low-active!) => Nachkommastellenanfang gefunden
                RR      C                       ;Punkte eine Position nach rechts
                DEC     IY                      ;IY zeigt auf die letzte Vorkommastelle
                DJNZ    GetNumInput1            ;weiter nach Dezimalpunkt suchen
                RET
GetNumInput2:   INC     D                       ;ein Dezimalpunkt mehr...
GetNumInput3:   DJNZ    GetNumInput4            ;alle Punktpositionen durch? Nein =>
                RET
GetNumInput4:   RR      C                       ;Punkte eine Position nach rechts
                BIT     0,C                     ;Punkt gesetzt?
                JR      NZ,GetNumInput3         ;Nein (low-active!) => (nächste Position)
                JR      GetNumInput2            ;Punkt zählen

StrrErE:        DEFB 1Fh,13h,0Eh,1Ch,13h,0Eh,F6h    ;" rE-rE"
StrLrEIN:       DEFB 11h,13h,1Ch,0Eh,01h,1Eh,EFh    ;"Lr-EIN"
StrLrAUS:       DEFB 11h,13h,1Ch,0Ah,14h,05h,EFh    ;"Lr-AUS"
StrLrUNI1:      DEFB 1Fh,14h,1Eh,01h,1Ch,01h,FBh    ;" UNI-1"
StrLrUNI2:      DEFB 1Fh,14h,1Eh,01h,1Ch,02h,FBh    ;" UNI-2"
Str6Space:      DEFB 1Fh,1Fh,1Fh,1Fh,1Fh,1Fh,FFh    ;"      "
                IF NewVersion
StrFEHL16:      DEFB 0Fh,0Eh,10h,11h,01h,06h,FBh    ;"FEHL16"
                ELSE
StrFEHL16:      DEFB 0Fh,0Eh,10h,11h,01h,06h,FFh    ;"FEHL16"
                ENDIF
Str6Minus:      DEFB 1Ch,1Ch,1Ch,1Ch,1Ch,1Ch,FFh    ;"------"
                IF NewVersion
StrFEHL15:      DEFB 0Fh,0Eh,10h,11h,01h,05h,FBh    ;"FEHL15"
                ELSE
StrFEHL15:      DEFB 0Fh,0Eh,10h,11h,01h,05h,FFh    ;"FEHL15"
                ENDIF

;Versionsdatum im LED-Format
                IF NewVersion
VersionNoDisp:  DEFB 8Eh,BFh,FFh,79h,90h,C0h        ;"F- 1.90"
                ELSE
VersionNoDisp:  DEFB 8Eh,BFh,FFh,79h,80h,90h        ;"F- 1.89"
                ENDIF

;Font => LED-Tabelle
;                     0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
FontLEDTable:   DEFB C0h,F9h,A4h,B0h,99h,92h,82h,F8h,80h,90h,88h,83h,C6h,A1h,86h,8Eh
                DEFB 89h,C7h,8Ch,AFh,C1h,8Dh,E3h,ABh,9Ch,A3h,87h,FEh,BFh,F7h,C8h,FFh
;                     H   L   P   r   U   µ   u   n   °   o  /F  /A   -   _   N  ' '

;Rechentabelle Binär => Dezimal
BinDezTable:    DEFW 0001h,0002h,0004h,0008h,0016h,0032h,0064h
                DEFW 0128h,0256h,0512h,1024h,2048h,4096h,8192h
BinDezTableEnd:

;Tastaturtabelle
TastaturTab:
;                     "."      "3"   "6"   "9"   "0"   "2"   "5"   "8"   "*"      "1"   "4"   "7"
                DEFW KeyPunkt,8003h,8006h,8009h,8000h,8002h,8005h,8008h,KeyStern,8001h,8004h,8007h
;                     "Re"     "Lw"        "Tp"          "pH"  "CO2"  "Li"    "K2"       "K1"
                DEFW KeyRedox,KeyLeitwert,KeyTemperatur,KeyPh,KeyCO2,KeyLicht,KeyKanal2,KeyKanal1
;                     "Aus"  "Ein"  "Nacht"  "Tag"  "Setzen"  "Manuell"  "Momentan"  "Zeit"
                DEFW KeyAus,KeyEin,KeyNacht,KeyTag,KeySetzen,KeyManuell,KeyMomentan,KeyZeit

SpezKeyTable:   DEFB FFh,FEh,FDh,FFh,FFh,FFh,FEh    ;"0" "1" "Setzen" - Seriennummer (= 283062)
                DEFB PrintSerial>>8,PrintSerial
                DEFB FBh,FEh,FFh,FFh,FFh,FFh,FEh    ;"0" "6" "Setzen" - Soll-ROM-Prüfsumme (= 5Fd6)
                DEFB PrintROMChksum>>8,PrintROMChksum
                DEFB FDh,FEh,FFh,FFh,FFh,FFh,FEh    ;"0" "3" "Setzen" - Produktionsdatum (= 692)
                DEFB PrintProdDatum>>8,PrintProdDatum
                DEFB FFh,FEh,FBh,FFh,FFh,FFh,FEh    ;"0" "4" "Setzen" - (1FF9h) = 12050
                DEFB PrintUnknown>>8,PrintUnknown
                DEFB FFh,FEh,F7h,FFh,FFh,FFh,FEh    ;"0" "7" "Setzen" - Errechnete ROM-Prüfsumme
                DEFB PrintRealChksum>>8,PrintRealChksum
SpezKeyLicht:   DEFB FFh,FEh,FFh,FFh,FDh,FFh,FEh    ;"0" "Licht" "Setzen" - alle LEDs anschalten
                DEFB CheckAllLED>>8,CheckAllLED
                DEFB FFh,FEh,FFh,FFh,FFh,FDh,FEh    ;"0" "Ein" "Setzen" - alle Relais testen
                DEFB CheckDosen>>8,CheckDosen
                IF  NewVersion
                DEFB FFh,FEh,FFh,FFh,FFh,FFh,FAh    ;"0" "Momentan" "Setzen" - Computer zurücksetzen
                DEFB ResetComputer>>8,ResetComputer
                ENDIF
                DEFB 00h

PrintSerial:    CALL    KeyStern
                LD      HL,SerialNo             ;Seriennummer des Gerätes (6 BCD-Stellen)
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET

PrintROMChksum: CALL    KeyStern
                LD      HL,ROMChecksum          ;Prüfsumme über das ROM (binäres Wort)
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET

PrintProdDatum: CALL    KeyStern
                LD      HL,ProduktDatum         ;Produktionsdatum (oberes Byte: Monat, unteres Byte: Jahr)
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET

PrintUnknown:   CALL    KeyStern
                LD      HL,Unknown              ;???
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET

PrintRealChksum:CALL    KeyStern
                LD      HL,CalcChecksum
                CALL    PrintTime
                LD      (IX+DPunkt),FFh         ;Dezimalpunkte aus
                RET

;_ALLE_ LEDs am Bedienteil an
CheckAllLED:    LD      HL,KLED
                LD      B,9
CheckAllLED1:   LD      (HL),0                  ;2000h-2008h löschen => alle LEDs an
                INC     HL
                DJNZ    CheckAllLED1
CheckAllLED2:   LD      HL,SpezKeyLicht
                LD      DE,KeyboardMatrix
                LD      B,7
CheckAllLED3:   LD      A,(DE)
                OR      F0h
                CP      (HL)
                JR      NZ,CheckAllLED4         ;anlassen, solange die Tastenkombination gedrückt wird
                INC     HL
                INC     DE
                DJNZ    CheckAllLED3
                LD      (C000h),A
                JR      CheckAllLED2
CheckAllLED4:   CALL    KeyStern                ;Display löschen
                SET     6,(IX+KLED)             ;Manuell-LED ausschalten
                RET

;_ALLE_ Steckdosen an/auschalten
CheckDosen:     IF NewVersion
                IN      A,(C)                   ;Sperre gesetzt?
                BIT     4,A                     ;Nein! =>
                JR      NZ,CheckDosen0
                JP      KeySetzen1              ;Fehler 99!
                ENDIF
CheckDosen0:    LD      A,(004Eh)               ;??? sollte wohl (IX+Steckdosen) heissen (ist eh unnötig)
                LD      C,A
                LD      A,0
                CALL    SetDoseStatus           ;alle Steckdosen aus
                LD      B,3
                LD      A,1
CheckDosen1:    CALL    SetDoseStatus           ;3 Steckdosen (CO2, Heizung, Licht) nacheinander an
                SLA     A
                DJNZ    CheckDosen1
                SLA     A
                CALL    SetDoseStatus           ;4.Steckdose (Kanal 1) an
                SRL     A
                CALL    SetDoseStatus           ;5.Steckdose (Kanal 2) an
                LD      A,1Fh
                CALL    SetDoseStatus           ;alle Steckdosen aus
                LD      A,C
                LD      (004Eh),A               ;??? sollte wohl (IX+Steckdosen) heissen (ist eh unnötig)
                RET

SetDoseStatus:  LD      (E000h),A               ;Port schreiben
                PUSH    BC
                LD      B,0
SetDoseStatus1: LD      (C000h),A               ;kleine Pause
                HALT
                DJNZ    SetDoseStatus1
                POP     BC
                RET

;Computer zurücksetzen
ResetComputer:  IF NewVersion
                IN      A,(C)                   ;Sperre gesetzt?
                BIT     4,A                     ;Nein! =>
                JR      NZ,ResetComputer1
                JP      KeySetzen1              ;Fehler 99!
ResetComputer1: JP      ResetVars
                ENDIF

;Sprungtabelle für die Ausgabe eines Meßwertes im Akku
DoDispMessTab:  DEFW DispPh,DispTemp,DispLeitw,DispRedox

                IF !NewVersion
; 40h,"PAUL UND ULLI P0PPEN HANNA UND ELLI",42h,"SIE HABEN SPASS AN BUSEN UND PO",42h,42h,"OO LA-LA",41h
MsgFutura:      DEFB 40h,12h,0Ah,14h,11h,1Fh,14h,17h,0Dh,1Fh,14h,11h,11h,01h,1Fh,12h
                DEFB 00h,12h,12h,0Eh,1Eh,1Fh,10h,0Ah,1Eh,1Eh,0Ah,1Fh,14h,17h,0Dh,1Fh
                DEFB 0Eh,11h,11h,01h,42h,05h,01h,0Eh,1Fh,10h,0Ah,0Bh,0Eh,1Eh,1Fh,05h
                DEFB 12h,0Ah,05h,05h,1Fh,0Ah,17h,1Fh,0Bh,14h,05h,0Eh,1Eh,1Fh,14h,17h
                DEFB 0Dh,1Fh,12h,00h,42h,42h,00h,10h,1Fh,11h,0Ah,1Ch,11h,0Ah,41h
; 40h,"HALLO",42h,"SIE HABEN PAUSE",41h
MsgPause:       DEFB 40h,10h,0Ah,11h,11h,00h,42h,05h,01h,0Eh,1Fh,10h,0Ah,0Bh,0Eh,1Eh
                DEFB 1Fh,12h,0Ah,14h,05h,0Eh,41h
; 40h,"HUI-",42h,"DAS S0LL ABER SEHR HEISS SEIN",41h
MsgHeiss:       DEFB 40h,10h,14h,01h,1Ch,42h,0Dh,0Ah,05h,1Fh,05h,00h,11h,11h,1Fh,0Ah
                DEFB 0Bh,0Eh,13h,1Fh,05h,0Eh,10h,13h,1Fh,10h,0Eh,01h,05h,05h,1Fh,05h
                DEFB 0Eh,01h,17h,41h
; 40h,"HUI-",42h,"DAS S0LL ABER SEHR SAUER SEIN",41h
MsgSauer:       DEFB 40h,10h,14h,01h,1Ch,42h,0Dh,0Ah,05h,1Fh,05h,00h,11h,11h,1Fh,0Ah
                DEFB 0Bh,0Eh,13h,1Fh,05h,0Eh,10h,13h,1Fh,05h,0Ah,14h,0Eh,13h,1Fh,05h
                DEFB 0Eh,01h,17h,41h
; 40h,"HUI-",42h,"DA5 S0LL ABER SEHR BASISCH SEIN",41h
MsgBasis:       DEFB 40h,10h,14h,01h,1Ch,42h,0Dh,0Ah,05h,1Fh,05h,00h,11h,11h,1Fh,0Ah
                DEFB 0Bh,0Eh,13h,1Fh,05h,0Eh,10h,13h,1Fh,0Bh,0Ah,05h,01h,05h,0Ch,10h
                DEFB 1Fh,05h,0Eh,01h,17h,41h

MsgMessdaten:   DEFM "************************** Messdatenerfassung **************************"
                DEFB 13,10,10
                DEFM "(c) By FUTURA Aquarien-Systeme, KLEVE, Kalkarer Str.24  Tel. 02821/17574"
                DEFB 13,10,10,3,'$'
MsgEscH4Lf:     DEFB 1Bh,'H',10,10,10,10,'$'
MsgEscHEscJ:    DEFB 1Bh,'H',1Bh,'J','$'
Msg6Space:      DEFM "      "
                DEFB 13,10,10,'$'
MsgZeit:        DEFM "ZEIT   : $"
MsgPhWert:      DEFM "PH-WERT: $"
MsgGrad:        DEFM "GRAD   : $"
MsgMikroS:      DEFM "MIKRO-S: $"
MsgMilliS:      DEFM "MILLI-S: $"
MsgMilliV:      DEFM "MILLI-V: $"
MsgFehl:        DEFM "FEHL.$"
MsgCopyright:   DEFM "Copyrigt (c) 1989 by Ulrich Forke & FUTURA Aquariensysteme"
                DEFB 13,10
                DEFM "4190 Kleve, Deutschland"
                DEFB 13,10
                DEFM "Die Verwendung dieses Programms oder Teilen davon ist nicht gestattet !"
                DEFB 13,10
                ENDIF

                ORG ROMTop
SerialNo:       DEFB 62h,30h,28h        ;Seriennummer (= 283062)
ROMChecksum:    DEFB D6h,5Fh,00h        ;Prüfsumme über das ROM, "5Fd6"
ProduktDatum:   DEFB 92h,06h,00h        ;Produktionsdatum 6.92
Unknown:        DEFB 50h,20h,01h        ;??? (= 12050)
                PRINT   "Ende..."